I tried to write a Client(iPad)/Server(iMac) application based on the CocoaEcho example. My first simple example worked, but after adding more functionality the client is unable to find the server.
After starting the server, I start the client, both in a local network. The client starts searching for services and gets a "netServiceBrowserWillSearch:" message for its browser, but after that nothing happens. Triggering the search for services again, results in a "didNotsearch:" message with error -72003, 10 (browser is still busy searching).
1) I checked that the server is reachable with the WiTap app. There client and server connect correctly.
2) I checked if the server publishes the service with "dns-sd -B _cocoaecho", it is detected.
3) The nsnetservicebrowser object in the client app is declared a property, so there should not be a scope problem. I also checked in the debugger, it is still there....
My Code:
Client:
#interface MySocketClient : UIResponder <NSNetServiceBrowserDelegate, NSStreamDelegate>
{
...
NSNetService * myServer;
NSString* nextMsg;
}
#property (nonatomic, strong, readwrite) NSMutableArray * services; // of NSNetService
#property (nonatomic, strong, readwrite) NSNetServiceBrowser * serviceBrowser;
#property (nonatomic, strong, readwrite) NSInputStream * inputStream;
#property (nonatomic, strong, readwrite) NSOutputStream * outputStream;
#property (nonatomic, strong, readwrite) NSMutableData * inputBuffer;
#property (nonatomic, strong, readwrite) NSMutableData * outputBuffer;
....
-(void) setup{
...
self.serviceBrowser = [[NSNetServiceBrowser alloc] init];
self.services = [[NSMutableArray alloc] init];
[self.serviceBrowser setDelegate:self];
[self.serviceBrowser searchForServicesOfType:#"_cocoaecho._tcp." inDomain:#"local."];
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didNotSearch:(NSDictionary *)errorInfo
{
NSLog(#"%#", errorInfo);
}
// Sent when browsing begins
- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser
{
NSLog(#"will search \n");
}
// Sent when browsing stops
- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser
{
NSLog(#"stopped search \n");
}
//We broadcast the willChangeValueForKey: and didChangeValueForKey: for the NSTableView binding to work.
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
#pragma unused(aNetServiceBrowser)
#pragma unused(moreComing)
NSLog(#"found a service \n");
if (![self.services containsObject:aNetService]) {
[self willChangeValueForKey:#"services"];
[self.services addObject:aNetService];
[self didChangeValueForKey:#"services"];
myServer = aNetService;
}
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
#pragma unused(aNetServiceBrowser)
#pragma unused(moreComing)
if ([self.services containsObject:aNetService]) {
[self willChangeValueForKey:#"services"];
[self.services removeObject:aNetService];
[self didChangeValueForKey:#"services"];
}
}
And the Server:
- (BOOL)start {
assert(_ipv4socket == NULL && _ipv6socket == NULL); // don't call -start twice!
CFSocketContext socketCtxt = {0, (__bridge void *) self, NULL, NULL, NULL};
_ipv4socket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_STREAM, 0, kCFSocketAcceptCallBack, &EchoServerAcceptCallBack, &socketCtxt);
_ipv6socket = CFSocketCreate(kCFAllocatorDefault, AF_INET6, SOCK_STREAM, 0, kCFSocketAcceptCallBack, &EchoServerAcceptCallBack, &socketCtxt);
if (NULL == _ipv4socket || NULL == _ipv6socket) {
[self stop];
return NO;
}
static const int yes = 1;
(void) setsockopt(CFSocketGetNative(_ipv4socket), SOL_SOCKET, SO_REUSEADDR, (const void *) &yes, sizeof(yes));
(void) setsockopt(CFSocketGetNative(_ipv6socket), SOL_SOCKET, SO_REUSEADDR, (const void *) &yes, sizeof(yes));
// Set up the IPv4 listening socket; port is 0, which will cause the kernel to choose a port for us.
struct sockaddr_in addr4;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_len = sizeof(addr4);
addr4.sin_family = AF_INET;
addr4.sin_port = htons(0);
addr4.sin_addr.s_addr = htonl(INADDR_ANY);
if (kCFSocketSuccess != CFSocketSetAddress(_ipv4socket, (__bridge CFDataRef) [NSData dataWithBytes:&addr4 length:sizeof(addr4)])) {
[self stop];
return NO;
}
// Now that the IPv4 binding was successful, we get the port number
// -- we will need it for the IPv6 listening socket and for the NSNetService.
NSData *addr = (__bridge_transfer NSData *)CFSocketCopyAddress(_ipv4socket);
assert([addr length] == sizeof(struct sockaddr_in));
self.port = ntohs(((const struct sockaddr_in *)[addr bytes])->sin_port);
// Set up the IPv6 listening socket.
struct sockaddr_in6 addr6;
memset(&addr6, 0, sizeof(addr6));
addr6.sin6_len = sizeof(addr6);
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(self.port);
memcpy(&(addr6.sin6_addr), &in6addr_any, sizeof(addr6.sin6_addr));
if (kCFSocketSuccess != CFSocketSetAddress(_ipv6socket, (__bridge CFDataRef) [NSData dataWithBytes:&addr6 length:sizeof(addr6)])) {
[self stop];
return NO;
}
// Set up the run loop sources for the sockets.
CFRunLoopSourceRef source4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _ipv4socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source4, kCFRunLoopCommonModes);
CFRelease(source4);
CFRunLoopSourceRef source6 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _ipv6socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source6, kCFRunLoopCommonModes);
CFRelease(source6);
assert(self.port > 0 && self.port < 65536);
self.netService = [[NSNetService alloc] initWithDomain:#"local." type:#"_cocoaecho._tcp." name:#"" port:(int) self.port];
[self.netService publishWithOptions:0];
return YES;
}
I was just getting that -72003 error all the time unless I disconnected and reconnected again (even the first time). Which lead to this solution:
private let serviceBrowser = NSNetServiceBrowser()
serviceBrowser.stop()
serviceBrowser.searchForServicesOfType(TYPE, inDomain: DOMAIN)
I don't know why this works but I'm no longer getting the error.
I had similar problem. My code successfully registered NSNetService and launched NSNetServiceBrowser but could not -resolveWithTimeout other devices. Strange, but sometimes did work, sometimes not and sometimes worked asymmetrically.
After intense debugging I can give you some tips to check:
Install Bonjour Browser on desktop. Plug out your network cable and check if you are connected to the same WiFi hotspot as mobile devices. Here you should see the same service state as mobile devices will do.
Try with different WiFi hotspot. Strange but my main WiFi performed badly. After I switched to another one it worked like a charm using the very same code. Try unplug WiFi from Internet for testing.
You can add some retains (or assign to static variable) to objects returned from API (like NSNetService). ARC can do silently dealloc if it decides object is not needed anymore. That helped my for some tests.
Related
I've been trying to make a simple app on iOS that will receive a message, and then perform an action based on that message (at the moment I just want to show it via NSLog). I can connect to the service, but the service never seems to receive anything. Here's my .h file:
#import <Foundation/Foundation.h>
#import <arpa/inet.h>
#interface PalServiceController : NSObject <NSNetServiceDelegate>
#property (nonatomic, strong) NSNetService *ns;
#property (nonatomic, strong) NSOutputStream *ostream;
- (void)start;
+ (int)getPort;
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode;
#end
and my .m:
#import "PalServiceController.h"
#implementation PalServiceController
- (void)start
{
// Start a net service
int port = [PalServiceController getPort];
NSLog(#"Opening on port %i", port);
self.ns = [[NSNetService alloc] initWithDomain:#""
type:#"_TestingProtocol._tcp."
name:#"Test Name For iPhone"
port:port];
if (self.ns) {
[self.ns setDelegate:self];
[self.ns publish];
self.ns.delegate = self;
} else {
NSLog(#"Error starting service");
}
}
/*
* Code from: http://stackoverflow.com/a/11723158/657676
*/
+ (int) getPort
{
...
}
- (void)netServiceWillResolve:(NSNetService *)sender
{
NSLog(#"Resolving");
}
- (void)netServiceDidResolveAddress:(NSNetService *)sender
{
NSLog(#"Resolved Address");
}
- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict
{
NSLog(#"Error publishing");
}
- (void)netService:(NSNetService *)sender didAcceptConnectionWithInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream
{
NSLog(#"Got a connection! (server)");
// Close self down
[self.ns stopMonitoring];
[self.ns stop];
}
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
{
NSLog(#"Error resolving");
}
- (void)netServiceDidPublish:(NSNetService *)sender
{
NSLog(#"Published server");
}
- (void)netServiceDidStop:(NSNetService *)sender
{
NSLog(#"Server stopped");
}
- (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data
{
NSLog(#"Updated TXT Record");
}
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
NSLog(#"Event on server");
switch(eventCode) {
...
}
}
#end
This is called via:
self.controller = [[PalServiceController alloc] init];
[self.controller start];
When I use Bonjour Browser, I can see the service, and when I use either my own implementation of an NSNetServiceBrowser or the example from Bill Dudney(blog post, code), it seems to connect (i.e. my own implementation receives the NSStreamEventHasSpaceAvailable and NSStreamEventOpenCompletedevents via stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode). However, none of the methods above get called on the server.
I'm still new to iOS, so I'm hoping that this is just some silly mistake that can be easily fixed.
-[NSNetService publish] simply publishes the service; you're responsible for creating a socket and listening on the port you give to the NSNetService initializer. You won't receive netService:didAcceptConnectionWithInputStream:outputStream: because the NSNetService knows nothing about the socket.
If you want the NSNetService to manage the socket for you—and send didAcceptConnection...—use -[NSNetService publishWithOptions:]:
[self.ns publishWithOptions:NSNetServiceListenForConnections];
In this case, you need to make sure that the port with which you initialize the NSNetService is unused. Your getPort method never closes the socket (and leaks the CFSocketRef), so it will be in use when you publish the service, leading to an error.
I'd recommend that you remove getPort and, as Apple suggests in NSNetServices.h, "[s]pecify a port number of zero to use a random port."
I am trying to send a simple string over UDP in my iOS7 app to a known IP and could not find a simple explanation and sample code on how to do that.
There is plenty out there about TCP but not so much about UDP and it has to be UDP in my case.
You could use https://github.com/robbiehanson/CocoaAsyncSocket, which is an Objective-C wrapper for TCP and UDP connections. It also contains sample code for TCP and UPD clients and servers.
You could use this wrapper-less simple gist.
UDPEchoClient.h
#import <Foundation/Foundation.h>
#interface UDPEchoClient : NSObject
- (BOOL) sendData:(const char *)msg;
#end
UDPEchoClient.m
#import "UDPEchoClient.h"
//
// CFSocket imports
//
#import <CoreFoundation/CoreFoundation.h>
#import <sys/socket.h>
#import <arpa/inet.h>
#import <netinet/in.h>
#define IP "host ip"
#define PORT host_port
static void dataAvailableCallback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info) {
//
// receiving information sent back from the echo server
//
CFDataRef dataRef = (CFDataRef)data;
NSLog(#"data recieved (%s) ", CFDataGetBytePtr(dataRef));
}
#implementation UDPEchoClient
{
//
// socket for communication
//
CFSocketRef cfsocketout;
//
// address object to store the host details
//
struct sockaddr_in addr;
}
- (instancetype)init
{
self = [super init];
if (self) {
//
// instantiating the CFSocketRef
//
cfsocketout = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_DGRAM,
IPPROTO_UDP,
kCFSocketDataCallBack,
dataAvailableCallback,
NULL);
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = inet_addr(IP);
//
// set runloop for data reciever
//
CFRunLoopSourceRef rls = CFSocketCreateRunLoopSource(kCFAllocatorDefault, cfsocketout, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopCommonModes);
CFRelease(rls);
}
return self;
}
//
// returns true upon successfull sending
//
- (BOOL) sendData:(const char *)msg
{
//
// checking, is my socket is valid
//
if(cfsocketout)
{
//
// making the data from the address
//
CFDataRef addr_data = CFDataCreate(NULL, (const UInt8*)&addr, sizeof(addr));
//
// making the data from the message
//
CFDataRef msg_data = CFDataCreate(NULL, (const UInt8*)msg, strlen(msg));
//
// actually sending the data & catch the status
//
CFSocketError socketErr = CFSocketSendData(cfsocketout,
addr_data,
msg_data,
0);
//
// return true/false upon return value of the send function
//
return (socketErr == kCFSocketSuccess);
}
else
{
NSLog(#"socket reference is null");
return false;
}
}
I have a lot of switches in my network which I try to check if they are online. I do this with a small UDP packet to which they respond with an UDP packet of their own to tell me that they are there. In fact it is only one switch simulating 200 hundred for testing but this is not important.
Since I do not like to work low level if I don't have to I use https://github.com/robbiehanson/CocoaAsyncSocket for the UDP thing.
It works... almost. When I ping for example 10 switches repeatedly (I ping them at the same time and then wait for all the responses) it seems to work for the first cycles but then after a few it gets out of hand and almost all seem to not be responding. It is completely random which are and which not. It gets worse when I add more switches (meaning it does not work from the first cycle). With cycles I man: I send pings for all switches an then wait for all the answers (or timeouts) and then send the pings again.
When I check my network traffic with a packet sniffer I see that for the "wrong" switches (meaning they are shown as offline == timeout, but are actually online) there are three possible cases for each switch:
The packet from the iPhone is never send so no answer is received.
The iPhone send the packet but the switch did not respond.
The answer packet is received but the iPhone didn't read it.
It's completely random and they are mixed in one cycle.
When all the switches are actually online the problem is minimized (takes more cycles to appear) but still there .
Since the code is asynchronous there is a high potential for bugs and I think there must be some kind of race condition or something.
I will post my ping class here. It is not very complicated. I stared at this for hours but could not find a bug.
The usage is the following:
You create an instance with:
- (id)initWithTimeout:(NSTimeInterval)timeout delegate:(id<NMASwitchPingerDelegate>)delegate;
Than you just call the following method for every ipAddress you want to ping. The method returns immediately. This means the pings are processed concurrently:
- (void)sendPingToAddress:(NSString*)address;
I added it to a paste bin for your convenience: http://pastebin.com/0LuiXsXY
#import "NMASwitchPinger.h"
/**
* Private interface.
*/
#interface NMASwitchPinger () {
/**
* The delegate of this class.
*/
__weak id<NMASwitchPingerDelegate> delegate;
/**
* The timeout after which the pinger stops waiting for a response.
*/
NSTimeInterval timeout;
/**
* The socket which is used to send the ping.
*/
GCDAsyncUdpSocket *socket;
/**
* List of pings which are awaiting a response.
*/
NSMutableDictionary *pendingPings;
/**
* Dispatch queue which serializes access to the pendingPings dictionary.
*/
dispatch_queue_t pendingPingsAccessQueue;
/**
* The queue on which the delegate methods of the socket are executed.
*/
dispatch_queue_t socketDelegateQueue;
/**
* Is set to true when the SwitchPinger started receiving responses (after first send)
*/
bool receiving;
}
#end
#implementation NMASwitchPinger
#pragma mark - Initialization
- (id)initWithTimeout:(NSTimeInterval)newTimeout delegate:(id<NMASwitchPingerDelegate>)newDelegate {
self = [super init];
if (self) {
// setting passed values
timeout = newTimeout;
delegate = newDelegate;
// init data structures
pendingPings = [[NSMutableDictionary alloc] init];
pendingPingsAccessQueue = dispatch_queue_create("de.nexans-ans.pingerPendingAccess", DISPATCH_QUEUE_SERIAL);
// create the socket for udp sending
socketDelegateQueue = dispatch_queue_create("de.nexans-ans.pingerDelegate", DISPATCH_QUEUE_CONCURRENT);
socket = [[GCDAsyncUdpSocket alloc] initWithDelegate:self delegateQueue:socketDelegateQueue];
}
return self;
}
- (id)init {
NSAssert(NO, #"Use the designated initializer");
return nil;
}
#pragma mark - Sending a ping
- (void)sendPingToAddress:(NSString *)address {
// we allow only one ping at a time to the same ip
__block BOOL alreadyInList = NO;
dispatch_sync(pendingPingsAccessQueue, ^{
if (pendingPings[address]) {
alreadyInList = YES;
} else {
pendingPings[address] = [[NSDate alloc] init];
}
});
// don't send a second ping to the same address
if (alreadyInList) {
NSLog(#"SimplePinger: did not send ping because already a ping pending to this addres: %#", address);
return;
}
// create a minimal packet (3 bytes)
NSMutableData *packet = [[NSMutableData alloc] initWithCapacity:3];
uint16_t vendor_value = CFSwapInt16HostToBig(266);
uint8_t request_type = 1;
[packet appendBytes:&vendor_value length:sizeof(vendor_value)];
[packet appendBytes:&request_type length:sizeof(request_type)];
// send over the wire
[socket sendData:packet toHost:address port:50266 withTimeout:timeout tag:0];
// schedule timeout handler
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
dispatch_after(popTime, pendingPingsAccessQueue, ^(void){
[self removeTimedOutPingWithAddress:address];
});
// start receiving when not already receiving
if (!receiving) {
bool recvGood = [socket beginReceiving:nil];
NSAssert(recvGood, #"SimplePinger: could not start receiving");
receiving = YES;
}
}
#pragma mark - GCDAsyncSocket delegate
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)filterContext {
NSString *ipAddress = [GCDAsyncUdpSocket hostFromAddress:address];
__block BOOL pingStillPending = NO;
dispatch_sync(pendingPingsAccessQueue, ^{
NSDate *sendDate = pendingPings[ipAddress];
if (sendDate) {
[pendingPings removeObjectForKey:ipAddress];
pingStillPending = YES;
}
});
if (pingStillPending) {
dispatch_async(dispatch_get_main_queue(), ^{
[delegate switchPinger:self didReceiveResponse:data fromAddress:ipAddress];
});
}
}
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error {
NSLog(#"didnt send data");
}
- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag {
NSLog(#"did send");
}
#pragma mark - Private methods
/**
* Removes a timed out ping. A call of this function gets scheduled when a ping is send.
*
* #param address The address of the ping which should be removed.
*/
- (void)removeTimedOutPingWithAddress:(NSString*)address {
NSDate *sendDate = pendingPings[address];
if (sendDate) {
NSLog(#"timeout: %#", address);
NSAssert(fabs([sendDate timeIntervalSinceNow]) >= timeout, #"SimplePing: removed ping before timout");
[pendingPings removeObjectForKey:address];
dispatch_async(dispatch_get_main_queue(), ^{
[delegate switchPinger:self didReceiveTimeoutFromAddress:address];
});
}
}
#end
I'm using KxMovie: https://github.com/kolyvan/kxmovie
It appears to stop a stream and close the view controller one should use [pause];
However, I'm trying to receive a stream from a version of gstreamer that has a memory leak if a stream isn't closed properly (it's just left hanging).
So, just [pause]ing isn't an option for me.
I'm trying to use [closeFile] in the KxMovie decoder:
-(void) closeFile
{
[self closeAudioStream];
[self closeVideoStream];
[self closeSubtitleStream];
_videoStreams = nil;
_audioStreams = nil;
_subtitleStreams = nil;
if (_formatCtx) {
_formatCtx->interrupt_callback.opaque = NULL;
_formatCtx->interrupt_callback.callback = NULL;
avformat_close_input(&_formatCtx);
_formatCtx = NULL;
}
}
However, I usually get a EXC_BAD_ACCESS from av_read_frame after [closeFile] issues avformat_close_input.
Can anyone give me some advice on how to cleanly shutdown an RTSP stream using ffmpeg?
Thanks!
I was also confused by this, and I do not quite understand your solution.
I fixed it like below, could you give some advice?
_dispatchQueue is the same queue as doing asyncDecodeFrames work.
- (void)unSetup {
_buffered = NO;
_interrupted = YES;
dispatch_async(_dispatchQueue, ^{
if (_decoder) {
[self pause];
[self freeBufferedFrames];
if (_moviePosition == 0 || _decoder.isEOF)
[gHistory removeObjectForKey:_decoder.path];
else if (!_decoder.isNetwork)
[gHistory setValue:[NSNumber numberWithFloat:_moviePosition]
forKey:_decoder.path];
[_decoder closeFile];
}
});
}
Needed to use the interrupt callbacks to interrupt av_read_frame
_formatCtx->interrupt_callback.opaque
_formatCtx->interrupt_callback.callback
Wait for the callback to be called and return non zero.
After the callback has returned an interrupt value av_close_input can safely be called (after closing any codecs used).
The below code snippets are in Objective-C and the implementation file .m is for the object that handles RTSP stuff (RTSPProvider).
It is tested with Xcode Version 10.1 (10B61) and an FFmpeg manually built version of the current FFmpeg versions to date (4.2.1 / 15.10.2019).
Should you need the build script configuration and or library versions used (just ask).
I had the same issue as the OP but couldn't use his solution.
The full versions was with the interrupt callback I used was:
int interruptCallBack(void *ctx){
RTSPProviderObject *whyFFmpeg = (__bridge RTSPProviderObject*)ctx;
NSLog(#"What is this!");
if(whyFFmpeg.whatIsHappeningSTR) {
return 1;
} else {
return 0;
}
}
The return value 1 should have interrupted the av_read_frame() and exited without a crash as to my current understanding.
It still crashed. My solution was to let av_read_frame() finish reading and terminate the session context which will be freed and don't allow any more reading. This was easy since I had this issue when I deallocated my RTSPProviderObject and no reading was done.
The final usage was:
[self.rtspProvider cleanup];
self.rtspProvider = nil;
Below is the full code snippet:
#import "Don't forget the required ffmpeg headers or header file"
int interruptCallBack(void *ctx){
RTSPProviderObject *whyFFmpeg = (__bridge RTSPProviderObject*)ctx;
NSLog(#"What is this!");
if(whyFFmpeg.whatIsHappeningSTR) {
return 1;
} else {
return 0;
}
}
#interface RTSPProviderObject ()
#property (nonatomic, assign) AVFormatContext *sessionContext;
#property (nonatomic, assign) NSString *whatIsHappeningSTR;
#property (nonatomic, assign) AVDictionary *sessionOptions;
#property (nonatomic, assign) BOOL usesTcp;
#property (nonatomic, assign) BOOL isInputStreamOpen;
#property (nonatomic, strong) NSLock *audioPacketQueueLock;
#property (nonatomic, strong) NSLock *packetQueueLock;
#property (nonatomic, strong, readwrite) NSMutableArray *audioPacketQueue;
#property (nonatomic, assign) int selectedVideoStreamIndex;
#property (nonatomic, assign) int selectedAudioStreamIndex;
#end
#implementation RTSPProviderObject
- (id _Nullable)init
{
self = [super init];
if (!self)
{
return nil;
}
self.sessionContext = NULL;
self.sessionContext = avformat_alloc_context();
AVFormatContext *pFormatCtx = self.sessionContext;
if (!pFormatCtx)
{
// Error handling code...
}
// MUST be called before avformat_open_input().
av_dict_free(&_sessionOptions);
self.sessionOptions = 0;
if (self.usesTcp)
{
// "rtsp_transport" - Set RTSP transport protocols.
// Allowed are: udp_multicast, tcp, udp, http.
av_dict_set(&_sessionOptions, "rtsp_transport", "tcp", 0);
}
// Open an input stream and read the header with the demuxer options.
// rtspURL - connection url to your remote ip camera which supports RTSP 2.0.
if (avformat_open_input(&pFormatCtx, rtspURL.UTF8String, NULL, &_sessionOptions) != 0)
{
self.isInputStreamOpen = NO;
// Error handling code...
}
self.isInputStreamOpen = YES;
// user-supplied AVFormatContext pFormatCtx might have been modified.
self.sessionContext = pFormatCtx;
pFormatCtx->interrupt_callback.callback = interruptCallBack;
pFormatCtx->interrupt_callback.opaque = (__bridge void *)(self);
// ... Other needed but currently not relevant code for codec/stream and other setup.
}
- (BOOL)prepareNextFrame
{
NSLog(#"%s", __PRETTY_FUNCTION__);
int isVideoFrameAvailable = 0;
// The session context is needed to provide frame data. Frame data is provided for video and audio.
// av_read_frame reads from pFormatCtx.
AVFormatContext *pFormatCtx = self.sessionContext;
if (!pFormatCtx) { return NO; }
// Audio packet access is forbidden.
[self.packetQueueLock lock];
BOOL readResult = YES;
// Calling av_read_frame while it is reading causes a bad_exception.
// We read frames as long as the session context cotains frames to be read and cosumed (usually one).
while (!isVideoFrameAvailable && self.isInputStreamOpen && readResult) {
if (packet.buf == nil && self.whatIsHappeningSTR) {
[self.packetQueueLock unlock];
return NO;
}
NSLog(#"New frame will be read.");
if (self.shouldTerminateStreams) {
[self terminate];
[self.packetQueueLock unlock];
return NO;
}
readResult = av_read_frame(pFormatCtx, &packet) >=0;
// Video packet data decoding.
// We need to make sure that the frame video data which is consumed matches the user selected stream.
if(packet.stream_index == self.selectedVideoStreamId) {
// DEPRECIATED:
// avcodec_decode_video2(self.videoCodecContext, self.rawFrameData, &isVideoFrameAvailable, &packet);
// Replaced by this new implememtation. Read more: https://blogs.gentoo.org/lu_zero/2016/03/29/new-avcodec-api/
// *
// We need the video context to decode video data.
AVCodecContext *videoContext = self.videoCodecContext;
if (!videoContext && videoContext->codec_type == AVMEDIA_TYPE_VIDEO) { isVideoFrameAvailable = 1; }
int ret;
// Supply raw packet data as input to a decoder.
ret = avcodec_send_packet(videoContext, &packet);
if (ret < 0)
{
NSLog(#"codec: sending video packet failed");
[self.packetQueueLock unlock];
return NO;
}
// Return decoded output data from a decoder.
ret = avcodec_receive_frame(videoContext, self.rawFrameData);
if (isVideoFrameAvailable < 0 && isVideoFrameAvailable != AVERROR(EAGAIN) && isVideoFrameAvailable != AVERROR_EOF)
{
[self.packetQueueLock unlock];
return NO;
}
if (ret >= 0) { isVideoFrameAvailable = 1; }
// *
} else {
// avcodec_decode_video2 unreference all the buffers referenced by self.rawFrameData and reset the frame fields.
// We must do this manually if we don't use the video frame or we will leak the frame data.
av_frame_unref(self.rawFrameData);
isVideoFrameAvailable = 1;
}
// Audio packet data consumption.
// We need to make sure that the frame audio data which will be consumed matches the user selected stream.
if (packet.stream_index == self.selectedAudioStreamIndex) {
[self.audioPacketQueueLock lock];
[self.audioPacketQueue addObject:[NSMutableData dataWithBytes:&packet length:sizeof(packet)]];
[self.audioPacketQueueLock unlock];
}
}
[self.packetQueueLock unlock];
return isVideoFrameAvailable!=0;
}
- (void)cleanup
{
NSLog(#"%s", __PRETTY_FUNCTION__);
self.shouldTerminateStreams = YES;
self.whatIsHappeningSTR = #"";
}
- (void)terminate
{
avformat_close_input(&_sessionContext);
}
#end
Hope this helps anyone. Thank you for reading and contributing.
Alright, this involves a lot of network coding from this part of a multiplayer tutorial.
Basically, I'm trying to implement a multiplayer game using GameKit as per the tutorial linked above. I put in all of the necessary network coding and more or less understand it, however I've hit a snag somewhere along the line of method calls. Basically, the setup that I have is that one device acts as the host and the rest act as the clients. I have two separate UIViewcontrollers for the host and clients respectively where the connection is established.
Now the thing is, the connection gets established, but it's only the host that recognizes the connection, not the client. The problem is here:
- (void)sendPacketToAllClients:(Packet *)packet
{
[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
{
obj.receivedResponse = [_session.peerID isEqualToString:obj.peerID];
}];
GKSendDataMode dataMode = GKSendDataReliable;
NSData *data = [packet data];
NSError *error;
if (![_session sendDataToAllPeers:data withDataMode:dataMode error:&error])
{
NSLog(#"Error sending data to clients: %#", error);
}
}
This is implemented in GameMultiplayer, where the actual game will be implemented. What this method is supposed to be doing is sending data packets to each of the clients saying that the host received the connection request and is able to connect with them. After [_session sendDataToAllPeers:data withDataMode:dataMode error:&error] is called (the method in the if statement), this method is supposed to be triggered:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
#ifdef DEBUG
NSLog(#"Game: receive data from peer: %#, data: %#, length: %d", peerID, data, [data length]);
#endif
Packet *packet = [Packet packetWithData:data];
if (packet == nil)
{
NSLog(#"Invalid packet: %#", data);
return;
}
Player *player = [self playerWithPeerID:peerID];
if (player != nil)
{
player.receivedResponse = YES; // this is the new bit
}
if (self.isServer)
[self serverReceivedPacket:packet fromPlayer:player];
else
[self clientReceivedPacket:packet];
}
This method is in the next part of the tutorial I linked above (which is here) and is supposed to receive the packets that the host sends to all clients and implement the next methods in this networking chain. However, the method never gets called. No debug breakpoints are triggered and I get nothing in the console.
I understand if I need to provide more source material, but there is a lot of network coding already implemented, so I want to keep it down to what people need to see. Also, [_session setDataReceiveHandler:self withContext:nil] and _session.delegate = self are written in another method that is called in GameMultiplayer, so that's not the problem. Does anyone know what I need to fix?
EDIT: As requested, here's where GKSession is initialized:
#property (nonatomic, strong, readonly) GKSession *session; //This is done in the header file
#synthesize session = _session; //This is done in the main file
- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
if (_serverState == ServerStateIdle)
{
_serverState = ServerStateAcceptingConnections;
_connectedClients = [NSMutableArray arrayWithCapacity:self.maxClients];
_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
_session.delegate = self;
_session.available = YES;
}
}
The session is initialized in MatchmakingServer, which is used in the host view controller. The session is then passed on to the main view controller of the app, which then initializes GameMultiplayer and sends the GKSession to it. Here's where the host view controller sends it to the main view controller:
- (IBAction)startAction:(id)sender
{
if (_matchmakingServer != nil && [_matchmakingServer connectedClientCount] > 0)
{
NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([name length] == 0)
name = _matchmakingServer.session.displayName;
[_matchmakingServer stopAcceptingConnections];
[self.delegate hostViewController:self startGameWithSession:_matchmakingServer.session playerName:name clients:_matchmakingServer.connectedClients];
}
}
and then the main view controller handles that method call here:
- (void)hostViewController:(MatchmakerHost *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
[self dismissViewControllerAnimated:NO completion:^
{
[self startGameWithBlock:^(GameMultiplayer *aGame)
{
[aGame startServerGameWithSession:session playerName:name clients:clients];
}];
}];
}
and finally, this is where that method call is implemented in GameMultiplayer:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
_clients = clients;
const char* className = class_getName([[_clients objectAtIndex:0] class]);
NSLog(#"yourObject is a: %s", className);
self.isServer = YES;
_session = session;
_session.available = NO;
_session.delegate = self;
[_session setDataReceiveHandler:self withContext:nil];
_state = GameStateWaitingForSignIn;
[self.delegate gameWaitingForClientsReady:self];
// Create the Player object for the server.
Player *player = [[Player alloc] init];
player.name = name;
player.peerID = _session.peerID;
player.position = PlayerPositionBottom;
[_players setObject:player forKey:player.peerID];
// Add a Player object for each client.
int index = 0;
for (NSString *peerID in clients)
{
Player *player = [[Player alloc] init];
player.peerID = peerID;
[_players setObject:player forKey:player.peerID];
if (index == 0)
player.position = ([clients count] == 1) ? PlayerPositionTop : PlayerPositionLeft;
else if (index == 1)
player.position = PlayerPositionTop;
else
player.position = PlayerPositionRight;
index++;
}
NSLog(#"Players:");
Packet *packet = [Packet packetWithType:PacketTypeSignInRequest];
[self sendPacketToAllClients:packet];
// for (int i = 0; i < [_players count]; i++) {
// NSLog([NSString stringWithFormat:#"%#", [clients objectAtIndex:i]]);
// }
}
I think you are calling send to fast. When server realize about connection it will send confirmation to client to really establish connection - so client knows about it succeed.
If you are sending packets before that happens - it will be lost.
Just do this:
[self performSelector:#selector(sendPacketToAllClients) withObject:nil afterDelay:1.0];
instead of:
[self sendPacketToAllClients];
I had the same problem that connection is established in different moment with small delay on client. The best is to send first packet from client that he is ready to receive packets from server - and than proceed normally from there.
Also try debugging:
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
On both devices (server and client).
I have also had my troubles with GKSession. I was interested to learn (on this site) today that GKSession is being deprecated in favor of using the Multipeer Connectivity Framework. With luck, Wenderlich et al. will do a tutorial using the new technology. :)
The system has some similarities to GKSession, so is not too hard to wrap your head around.
Apple's doc link.