I am using this approach to save the buffer data of the AVPlayer for video files. Found as the answer in this question Saving buffer data of AVPlayer.
iPhone and iPad - iOS 8.1.3
I made the necessary changes to play video and it is working very nicely except when I try to play a very long video (11-12 minutes long and about 85mb in size) the video will stall roughly 4 minutes after the connection finishes loading. I get an event for playbackBufferEmpty and a player item stalled notification.
This is the gist of the code
viewController.m
#property (nonatomic, strong) NSMutableData *videoData;
#property (nonatomic, strong) NSURLConnection *connection;
#property (nonatomic, strong) AVURLAsset *vidAsset;
#property (nonatomic, strong) AVPlayerItem *playerItem;
#property (nonatomic, strong) AVPlayerLayer *avlayer;
#property (nonatomic, strong) NSHTTPURLResponse *response;
#property (nonatomic, strong) NSMutableArray *pendingRequests;
/**
Startup a Video
*/
- (void)startVideo
{
self.vidAsset = [AVURLAsset URLAssetWithURL:[self videoURLWithCustomScheme:#"streaming"] options:nil];
[self.vidAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
self.pendingRequests = [NSMutableArray array];
// Init Player Item
self.playerItem = [AVPlayerItem playerItemWithAsset:self.vidAsset];
[self.playerItem addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:NULL];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
// Init a video Layer
self.avlayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.avlayer setFrame:self.view.frame];
[self.view.layer addSublayer:self.avlayer];
}
- (NSURL *)getRemoteVideoURL
{
NSString *urlString = [#"http://path/to/your/long.mp4"];
return [NSURL URLWithString:urlString];
}
- (NSURL *)videoURLWithCustomScheme:(NSString *)scheme
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[self getRemoteVideoURL] resolvingAgainstBaseURL:NO];
components.scheme = scheme;
return [components URL];
}
/**
NSURLConnection Delegate Methods
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"didReceiveResponse");
self.videoData = [NSMutableData data];
self.response = (NSHTTPURLResponse *)response;
[self processPendingRequests];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Received Data - appending to video & processing request");
[self.videoData appendData:data];
[self processPendingRequests];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"connectionDidFinishLoading::WriteToFile");
[self processPendingRequests];
[self.videoData writeToFile:[self getVideoCachePath:self.vidSelected] atomically:YES];
}
/**
AVURLAsset resource loader methods
*/
- (void)processPendingRequests
{
NSMutableArray *requestsCompleted = [NSMutableArray array];
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
{
[self fillInContentInformation:loadingRequest.contentInformationRequest];
BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest];
if (didRespondCompletely)
{
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
[self.pendingRequests removeObjectsInArray:requestsCompleted];
}
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
if (contentInformationRequest == nil || self.response == nil)
{
return;
}
NSString *mimeType = [self.response MIMEType];
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
contentInformationRequest.byteRangeAccessSupported = YES;
contentInformationRequest.contentType = CFBridgingRelease(contentType);
contentInformationRequest.contentLength = [self.response expectedContentLength];
}
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0)
{
startOffset = dataRequest.currentOffset;
}
// Don't have any data at all for this request
if (self.videoData.length < startOffset)
{
NSLog(#"NO DATA FOR REQUEST");
return NO;
}
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.videoData.length - (NSUInteger)startOffset;
// Respond with whatever is available if we can't satisfy the request fully yet
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
[dataRequest respondWithData:[self.videoData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = self.videoData.length >= endOffset;
return didRespondFully;
}
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
if (self.connection == nil)
{
NSURL *interceptedURL = [loadingRequest.request URL];
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = #"http";
NSURLRequest *request = [NSURLRequest requestWithURL:[actualURLComponents URL]];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection setDelegateQueue:[NSOperationQueue mainQueue]];
[self.connection start];
}
[self.pendingRequests addObject:loadingRequest];
return YES;
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
NSLog(#"didCancelLoadingRequest");
[self.pendingRequests removeObject:loadingRequest];
}
/**
KVO
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == StatusObservationContext)
{
AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
if (status == AVPlayerStatusReadyToPlay) {
[self initHud];
[self play:NO];
} else if (status == AVPlayerStatusFailed)
{
NSLog(#"ERROR::AVPlayerStatusFailed");
} else if (status == AVPlayerItemStatusUnknown)
{
NSLog(#"ERROR::AVPlayerItemStatusUnknown");
}
} else if (context == CurrentItemObservationContext) {
} else if (context == RateObservationContext) {
} else if (context == BufferObservationContext){
} else if (context == playbackLikelyToKeepUp) {
if (self.player.currentItem.playbackLikelyToKeepUp)
}
} else if (context == playbackBufferEmpty) {
if (self.player.currentItem.playbackBufferEmpty)
{
NSLog(#"Video Asset is playable: %d", self.videoAsset.isPlayable);
NSLog(#"Player Item Status: %ld", self.player.currentItem.status);
NSLog(#"Connection Request: %#", self.connection.currentRequest);
NSLog(#"Video Data: %lu", (unsigned long)self.videoData.length);
}
} else if(context == playbackBufferFull) {
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
The problem seems to be that some time after the connection finishes loading, the player item buffer goes empty. My thought at the moment is that something is being deallocated when the connection finishes loading and messing up the playerItem buffer.
However at the time the buffer goes empty the playerItem status is good, the video asset is playable, the video data is good
If I throttle the wifi through charles and slow down the connection, the video will play as long as the connection does not finish loading within a few minutes of the end of the video.
If I set the connection nil on the finished loading event, the resource loader will fire up a new connection when shouldWaitForLoadingOfRequestedResource fires again. In this case the loading starts all over again and the video will continue playing.
I should mention that this long video plays fine if I play it as a normal http url asset, and also plays fine after being saved to the device and loaded from there.
when the resource loader delegate fires up the NSURLConnection, the connection takes over saving the NSData to the pending requests and processing them. when the connection finished loading, the resource loader regains responsibility for handling the loading requests. the code was adding the loading request to the pending requests array but the issue was that they were not being processed. changed the method to the following and it works.
//AVAssetResourceLoader
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
if(isLoadingComplete == YES)
{
//NSLog(#"LOADING WAS COMPLETE");
[self.pendingRequests addObject:loadingRequest];
[self processPendingRequests];
return YES;
}
if (self.connection == nil)
{
NSURL *interceptedURL = [loadingRequest.request URL];
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = #"http";
self.request = [NSURLRequest requestWithURL:[actualURLComponents URL]];
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
[self.connection setDelegateQueue:[NSOperationQueue mainQueue]];
isLoadingComplete = NO;
[self.connection start];
}
[self.pendingRequests addObject:loadingRequest];
return YES;
}
Related
My App was rejected by Apple because it can't connect to other device running iOS 10.1.1 on Wi-Fi connected to an IPv6 network.
When I tap on connect, the app continues to search for invitees and no further user action is produced.
I use Multi-peer Connectivity and I never tested my App being connected to an IPv6(It's my first release). But the App run very fine without having any connection or being connected to IPv4 network.
I don't know why the App is running and connecting fine using the IPv4 and doesn't connect to peer if the iPad is connected to an IPv6 network.
So my Question: Is it possible to use Multi-peer Connectivity with IPv6 so that Apple can approve the App or how should I handle this Issue ?
Here is my Code, maybe it is something wrong there.
#interface ConnectionManageriOS7 () <MCSessionDelegate, MCBrowserViewControllerDelegate>
{
UILocalNotification *_expireNotification;
UIBackgroundTaskIdentifier _taskId;
}
#property (nonatomic, strong) MCSession *session;
#property (nonatomic, strong) MCPeerID *localPeerID;
#property (nonatomic, strong) MCBrowserViewController *browserVC;
#property (nonatomic, strong) MCAdvertiserAssistant *advertiser;
#end
static ConnectionManageriOS7 *_manager = nil;
#implementation ConnectionManageriOS7
+ (ConnectionManageriOS7 *)connectManager {
#synchronized([ConnectionManageriOS7 class]){
if (_manager == nil) {
_manager = [[ConnectionManageriOS7 alloc] init];
}
return _manager;
}
return nil;
}
- (id)init {
self = [super init];
if (self) {
[self setupSessionAndAdvertiser];
}
return self;
}
- (void)setupSessionAndAdvertiser {
_localPeerID = [[MCPeerID alloc] initWithDisplayName:[UIDevice currentDevice].name];;
_session = [[MCSession alloc] initWithPeer:_localPeerID];
_session.delegate = self;
}
- (void)connectWithDelegate:(id)delegate {
_delegate = delegate;
if (_session.connectedPeers.count) {
if ([_delegate respondsToSelector:#selector(didConntectedWithManager:)]) {
[_delegate didConntectedWithManager:self];
}
} else {
if (_advertiser == nil) {
_advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:VISUS_Service
discoveryInfo:nil
session:_session];
_isConnected = NO;
[_advertiser start];
}
if (_browserVC == nil) {
_browserVC = [[MCBrowserViewController alloc] initWithServiceType:VISUS_Service session:_session];
_browserVC.delegate = self;
}
[(UIViewController *)delegate presentViewController:_browserVC
animated:YES completion:nil];
}
}
- (void)sendMessage:(NSString *)message {
NSData *textData = [message dataUsingEncoding:NSASCIIStringEncoding];
NSLog(#"Send Data: %#", message);
NSError *error = nil;
[_session sendData:textData
toPeers:_session.connectedPeers
withMode:MCSessionSendDataReliable
error:&error];
if (error) {
//
[self session:_session peer:nil didChangeState:MCSessionStateNotConnected];
NSLog(#"error %#", error.userInfo);
}
}
- (void)stopService {
NSLog(#"Stop Service");
[_advertiser stop];
_advertiser = nil;
_browserVC = nil;
}
#pragma marks -
#pragma marks MCBrowserViewControllerDelegate
- (void) dismissBrowserVC{
[_browserVC dismissViewControllerAnimated:YES completion:nil];
}
// Notifies the delegate, when the user taps the done button
- (void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController {
if ([_delegate respondsToSelector:#selector(didConntectedWithManager:)]) {
[_delegate didConntectedWithManager:self];
}
[self dismissBrowserVC];
}
// Notifies delegate that the user taps the cancel button.
- (void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
if (_browserVC == nil) {
[browserViewController dismissViewControllerAnimated:YES completion:nil];
}else {
[self dismissBrowserVC];
}
}
#pragma marks -
#pragma marks MCBrowserViewControllerDelegate
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
if (state != MCSessionStateConnecting) {
if (state == MCSessionStateConnected) {
_isConnected = true;
if ([_delegate respondsToSelector:#selector(willConntectedWithManager:)]) {
[_delegate willConntectedWithManager:self];
}
}
else {
_isConnected = false;
[self stopService];
if ([_delegate respondsToSelector:#selector(didDisconntectedWithManager:)]) {
[_delegate didDisconntectedWithManager:self];
}
}
}
}
// Received data from remote peer
- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
// Decode data back to NSString
NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Receive Data: %#", message);
// append message to text box:
dispatch_async(dispatch_get_main_queue(), ^{
if ([_delegate respondsToSelector:#selector(connectionManager:receivedString:)]) {
[_delegate connectionManager:self receivedString:message];
}
});
}
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error {
_isConnected = false;
[self stopService];
NSLog(#"----- Error ----- %#", error.localizedDescription);
}
// Received a byte stream from remote peer
- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID {
}
// Start receiving a resource from remote peer
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress {
}
- (void) session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL accept))certificateHandler
{
certificateHandler(YES);
}
- (void) createExpireNotification
{
[self killExpireNotification];
if (_session.connectedPeers.count != 0) // if peers connected, setup kill switch
{
NSTimeInterval gracePeriod = 20.0f;
// create notification that will get the user back into the app when the background process time is about to expire
NSTimeInterval msgTime = UIApplication.sharedApplication.backgroundTimeRemaining - gracePeriod;
UILocalNotification* n = [[UILocalNotification alloc] init];
_expireNotification = n;
_expireNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:msgTime];
_expireNotification.alertBody = #"Bluetooth Connectivity is about to disconnect. Open the app to resume Test";
_expireNotification.soundName = UILocalNotificationDefaultSoundName;
_expireNotification.applicationIconBadgeNumber = 1;
[UIApplication.sharedApplication scheduleLocalNotification:_expireNotification];
}
}
- (void) killExpireNotification
{
if (_expireNotification != nil)
{
[UIApplication.sharedApplication cancelLocalNotification:_expireNotification];
_expireNotification = nil;
}
}
- (void)bacgroundHandling {
_taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^
{
[self stopService];
[[UIApplication sharedApplication] endBackgroundTask:_taskId];
_taskId = UIBackgroundTaskInvalid;
}];
[self createExpireNotification];
}
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler
{
// http://down.vcnc.co.kr/WWDC_2013/Video/708.pdf -- wwdc tutorial, this part is towards the end (p119)
// self.arrayInvitationHandler = [NSArray arrayWithObject:[invitationHandler copy]];
// ask the user
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:peerID.displayName
message:#"Would like to create a session with you"
delegate:self
cancelButtonTitle:#"Decline" otherButtonTitles:#"Accept", nil];
[alertView show];
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
// retrieve the invitationHandler and check whether the user accepted or declined the invitation...
BOOL accept = (buttonIndex != alertView.cancelButtonIndex) ? YES : NO;
// respond
if(accept) {
// void (^invitationHandler)(BOOL, MCSession *) = [self.arrayInvitationHandler objectAtIndex:0];
// invitationHandler(accept, self.mySession);
}
else
{
NSLog(#"Session disallowed");
}
}
- (void)terminate {
[self killExpireNotification];
[self stopService];
}
#end
I have solved the problem. For everybody with simular problem:
It wasn't a problem with IPv6, it is a matter of how to use Multipeer connectivity. In my Case I tryied to the the IPv6 connection with a iPad and a Simulator. And I used my Macbook for creating a nat64 network. And the reason why the simulator and iPad never saw each other the fact that they where not connected to same wifi network.
Solution:
Just take for testing two iPads and use your mac as nat64 network accesspoint.
So, I have all multipeer connectivity related code in the main thread. I have a MCSession, MCNearbyServiceAdvertiser, and a MCNearbyServiceBrowser. These are all created with peerID, and I make sure only one sends an invitation.
The session becomes connected. My problems is that, two clients take around 20-30 seconds to connect. This is unacceptable. The clients are on good Wifi, and bluetooth. I want the browsing, invitation handler, and connection to take place within 1 second. Does anyone have any idea what is slowing things down?
Code is exactly as provided here and I also implemented certificateHandler(YES)
#interface SessionController () // Class extension
#property (nonatomic, strong) MCPeerID *peerID;
#property (nonatomic, strong) MCSession *session;
#property (nonatomic, strong) MCNearbyServiceAdvertiser *serviceAdvertiser;
#property (nonatomic, strong) MCNearbyServiceBrowser *serviceBrowser;
// Connected peers are stored in the MCSession
// Manually track connecting and disconnected peers
#property (nonatomic, strong) NSMutableOrderedSet *connectingPeersOrderedSet;
#property (nonatomic, strong) NSMutableOrderedSet *disconnectedPeersOrderedSet;
#end
#implementation SessionController
static NSString * const kMCSessionServiceType = #"mcsessionp2p";
#pragma mark - Initializer
- (instancetype)init
{
self = [super init];
if (self)
{
_peerID = [[MCPeerID alloc] initWithDisplayName:[[UIDevice currentDevice] name]];
_connectingPeersOrderedSet = [[NSMutableOrderedSet alloc] init];
_disconnectedPeersOrderedSet = [[NSMutableOrderedSet alloc] init];
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
// Register for notifications
[defaultCenter addObserver:self
selector:#selector(startServices)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[defaultCenter addObserver:self
selector:#selector(stopServices)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[self startServices];
_displayName = self.session.myPeerID.displayName;
}
return self;
}
#pragma mark - Memory management
- (void)dealloc
{
// Unregister for notifications on deallocation.
[[NSNotificationCenter defaultCenter] removeObserver:self];
// Nil out delegates
_session.delegate = nil;
_serviceAdvertiser.delegate = nil;
_serviceBrowser.delegate = nil;
}
#pragma mark - Override property accessors
- (NSArray *)connectedPeers
{
return self.session.connectedPeers;
}
- (NSArray *)connectingPeers
{
return [self.connectingPeersOrderedSet array];
}
- (NSArray *)disconnectedPeers
{
return [self.disconnectedPeersOrderedSet array];
}
#pragma mark - Private methods
- (void)setupSession
{
// Create the session that peers will be invited/join into.
_session = [[MCSession alloc] initWithPeer:self.peerID];
self.session.delegate = self;
// Create the service advertiser
_serviceAdvertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peerID
discoveryInfo:nil
serviceType:kMCSessionServiceType];
self.serviceAdvertiser.delegate = self;
// Create the service browser
_serviceBrowser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peerID
serviceType:kMCSessionServiceType];
self.serviceBrowser.delegate = self;
}
- (void)teardownSession
{
[self.session disconnect];
[self.connectingPeersOrderedSet removeAllObjects];
[self.disconnectedPeersOrderedSet removeAllObjects];
}
- (void)startServices
{
[self setupSession];
[self.serviceAdvertiser startAdvertisingPeer];
[self.serviceBrowser startBrowsingForPeers];
}
- (void)stopServices
{
[self.serviceBrowser stopBrowsingForPeers];
[self.serviceAdvertiser stopAdvertisingPeer];
[self teardownSession];
}
- (void)updateDelegate
{
[self.delegate sessionDidChangeState];
}
- (NSString *)stringForPeerConnectionState:(MCSessionState)state
{
switch (state) {
case MCSessionStateConnected:
return #"Connected";
case MCSessionStateConnecting:
return #"Connecting";
case MCSessionStateNotConnected:
return #"Not Connected";
}
}
#pragma mark - MCSessionDelegate protocol conformance
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state
{
NSLog(#"Peer [%#] changed state to %#", peerID.displayName, [self stringForPeerConnectionState:state]);
switch (state)
{
case MCSessionStateConnecting:
{
[self.connectingPeersOrderedSet addObject:peerID];
[self.disconnectedPeersOrderedSet removeObject:peerID];
break;
}
case MCSessionStateConnected:
{
[self.connectingPeersOrderedSet removeObject:peerID];
[self.disconnectedPeersOrderedSet removeObject:peerID];
break;
}
case MCSessionStateNotConnected:
{
[self.connectingPeersOrderedSet removeObject:peerID];
[self.disconnectedPeersOrderedSet addObject:peerID];
break;
}
}
[self updateDelegate];
}
- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID
{
// Decode the incoming data to a UTF8 encoded string
NSString *receivedMessage = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"didReceiveData %# from %#", receivedMessage, peerID.displayName);
}
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress
{
NSLog(#"didStartReceivingResourceWithName [%#] from %# with progress [%#]", resourceName, peerID.displayName, progress);
}
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error
{
NSLog(#"didFinishReceivingResourceWithName [%#] from %#", resourceName, peerID.displayName);
// If error is not nil something went wrong
if (error)
{
NSLog(#"Error [%#] receiving resource from %# ", [error localizedDescription], peerID.displayName);
}
else
{
// No error so this is a completed transfer. The resources is located in a temporary location and should be copied to a permenant location immediately.
// Write to documents directory
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *copyPath = [NSString stringWithFormat:#"%#/%#", [paths firstObject], resourceName];
if (![[NSFileManager defaultManager] copyItemAtPath:[localURL path] toPath:copyPath error:nil])
{
NSLog(#"Error copying resource to documents directory");
}
else
{
// Get a URL for the path we just copied the resource to
NSURL *url = [NSURL fileURLWithPath:copyPath];
NSLog(#"url = %#", url);
}
}
}
// Streaming API not utilized in this sample code
- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID
{
NSLog(#"didReceiveStream %# from %#", streamName, peerID.displayName);
}
#pragma mark - MCNearbyServiceBrowserDelegate protocol conformance
// Found a nearby advertising peer
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
NSString *remotePeerName = peerID.displayName;
NSLog(#"Browser found %#", remotePeerName);
MCPeerID *myPeerID = self.session.myPeerID;
BOOL shouldInvite = ([myPeerID.displayName compare:remotePeerName] == NSOrderedDescending);
if (shouldInvite)
{
NSLog(#"Inviting %#", remotePeerName);
[browser invitePeer:peerID toSession:self.session withContext:nil timeout:30.0];
}
else
{
NSLog(#"Not inviting %#", remotePeerName);
}
[self updateDelegate];
}
- (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID
{
NSLog(#"lostPeer %#", peerID.displayName);
[self.connectingPeersOrderedSet removeObject:peerID];
[self.disconnectedPeersOrderedSet addObject:peerID];
[self updateDelegate];
}
- (void)browser:(MCNearbyServiceBrowser *)browser didNotStartBrowsingForPeers:(NSError *)error
{
NSLog(#"didNotStartBrowsingForPeers: %#", error);
}
#pragma mark - MCNearbyServiceAdvertiserDelegate protocol conformance
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void(^)(BOOL accept, MCSession *session))invitationHandler
{
NSLog(#"didReceiveInvitationFromPeer %#", peerID.displayName);
invitationHandler(YES, self.session);
[self.connectingPeersOrderedSet addObject:peerID];
[self.disconnectedPeersOrderedSet removeObject:peerID];
[self updateDelegate];
}
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didNotStartAdvertisingPeer:(NSError *)error
{
NSLog(#"didNotStartAdvertisingForPeers: %#", error);
}
#end
I've noticed this issue to and i can't seem to figure out whats happening here, it appears when debugging that there is a huge delay between the notification for state changing being fired, perhaps something to do with the view itself.
Update: Well i think i figured the issues out after a bit of reading, the response is pretty much instant now, in the notification in the view i have pushed the process back to the main thread like so:
-(void)peerDidChangeStateWithNotification:(NSNotification *)notification{
MCPeerID *peerID = [[notification userInfo] objectForKey:#"peerID"];
NSString *peerDisplayName = peerID.displayName;
MCSessionState state = [[[notification userInfo] objectForKey:#"state"] intValue];
if (state != MCSessionStateConnecting) {
if (state == MCSessionStateConnected) {
// add the user
[arrConnectedDevices addObject:peerDisplayName];
}
else if (state == MCSessionStateNotConnected){
// do we have connections
if ([arrConnectedDevices count] > 0) {
int indexOfPeer = [arrConnectedDevices indexOfObject:peerDisplayName];
[arrConnectedDevices removeObjectAtIndex:indexOfPeer];
}
}
}
// push to main queue for speedy response
dispatch_async(dispatch_get_main_queue(), ^(void) {
[collView reloadData];
BOOL peersExist = ([[appDelegate.mcManager.session connectedPeers] count] == 0);
NSLog(#"PEER COUNT IS %lu",(unsigned long)[[appDelegate.mcManager.session connectedPeers] count]);
[disconnectButton setEnabled:!peersExist];
if ([disconnectButton isEnabled]) {
[disconnectButton setBackgroundColor:[UIColor colorWithRed:(51/255.0) green:(202/255.0) blue:(168/255.0) alpha:1.0]];
}
else{
[disconnectButton setBackgroundColor:[UIColor colorWithRed:(107/255.0) green:(107/255.0) blue:(107/255.0) alpha:1.0]];
}
});
}
Hope this helps anyone who ran into the issues.
I am trying to play a movie at the beginning of my game. I am using AVPlayer to do this. My problem is, when I register a KVO to check the status of my AVPlayer, my game proceeds as usual without waiting for the video to load and finish. As a result, I can only hear the audio from my .mov file and can't see any video (since my game has already started).
I would like the video to load and finish before proceeding with the game.
Here's the code:
#interface RMVideoView : NSView
{
NSURL* _videoURL;
AVPlayer* _player;
AVPlayerLayer* _playerLayer;
}
#property (nonatomic, readonly, strong) AVPlayer* player;
#property (nonatomic, readonly, strong) AVPlayerLayer* playerLayer;
#property (nonatomic, retain) NSURL* videoURL;
- (void) play;
#end
static void *RMVideoViewPlayerLayerReadyForDisplay = &RMVideoViewPlayerLayerReadyForDisplay;
static void *RMVideoViewPlayerItemStatusContext = &RMVideoViewPlayerItemStatusContext;
#interface RMVideoView()
- (void)onError:(NSError*)error;
- (void)onReadyToPlay;
- (void)setUpPlaybackOfAsset:(AVAsset *)asset withKeys:(NSArray *)keys;
#end
#implementation RMVideoView
#synthesize player = _player;
#synthesize playerLayer = _playerLayer;
#synthesize videoURL = _videoURL;
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.wantsLayer = YES;
_player = [[AVPlayer alloc] init];
[self addObserver:self forKeyPath:#"player.currentItem.status" options:NSKeyValueObservingOptionNew context:RMVideoViewPlayerItemStatusContext];
}
return self;
}
- (void) setVideoURL:(NSURL *)videoURL {
_videoURL = videoURL;
[self.player pause];
[self.playerLayer removeFromSuperlayer];
AVURLAsset *asset = [AVAsset assetWithURL:self.videoURL];
[asset retain];
NSArray *assetKeysToLoadAndTest = [NSArray arrayWithObjects:#"playable", #"hasProtectedContent", #"tracks", #"duration", nil];
[asset loadValuesAsynchronouslyForKeys:assetKeysToLoadAndTest completionHandler:^{
dispatch_async(dispatch_get_main_queue(),^{
[self setUpPlaybackOfAsset:asset withKeys:assetKeysToLoadAndTest];
});
}];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == RMVideoViewPlayerItemStatusContext)
{
AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
switch (status)
{
case AVPlayerItemStatusUnknown:
break;
case AVPlayerItemStatusReadyToPlay:
[self onReadyToPlay];
break;
case AVPlayerItemStatusFailed:
[self onError:nil];
break;
}
}
else if (context == RMVideoViewPlayerLayerReadyForDisplay)
{
if ([[change objectForKey:NSKeyValueChangeNewKey] boolValue])
{
self.playerLayer.hidden = NO;
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - Private
- (void)onError:(NSError*)error {
// Notify delegate
}
- (void)onReadyToPlay {
// Notify delegate
[self.player play];
}
- (void)setUpPlaybackOfAsset:(AVAsset *)asset withKeys:(NSArray *)keys {
for (NSString *key in keys) {
NSError *error = nil;
if ([asset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) {
[self onError:error];
return;
}
}
if (!asset.isPlayable || asset.hasProtectedContent) {
[self onError:nil];
return;
}
if ([[asset tracksWithMediaType:AVMediaTypeVideo] count] != 0)
{ // Asset has video tracks
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.frame = self.layer.bounds;
self.playerLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
self.playerLayer.hidden = NO;
[self.layer addSublayer:self.playerLayer];
[self addObserver:self forKeyPath:#"playerLayer.readyForDisplay" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:RMVideoViewPlayerLayerReadyForDisplay];
}
// Create a new AVPlayerItem and make it our player's current item.
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
[self.player replaceCurrentItemWithPlayerItem:playerItem];
}
#pragma mark - Public
- (void) play {
[self.player play];
}
#end
I am calling the above code from my entry function's -(void)drawView method this way:
-(void)drawView
{
if(playVideo)
{
RMVideoView *rmVid = [[RMVideoView alloc]init];
NSURL* MovieURL;
NSBundle *bundle = [NSBundle mainBundle];
if(bundle != nil)
{
NSString *moviePath = [bundle pathForResource:#"MyVideoResource" ofType:#"mov"];
if (moviePath)
{
MovieURL = [NSURL fileURLWithPath:moviePath];
[MovieURL retain];
[rmVid setVideoURL:MovieURL];
}
}
playVideo = kFalse;
}
}
The call made to [rmVid setVideoURL:MovieURL] returns when KVO is setup and the game runs forward.
Please help!
You can listen for a notification, when the video playback reaches the end like this:
AVPlayerItem *avPlayerItem =[[AVPlayerItem alloc]initWithAsset:avAsset];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachedEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:avPlayerItem];
I have a class that is handling an AVPlayer (and AVPlayerItem) that reports back state, time, and timedMetadata to a delegate.
Works well except that about 70-80% of the time, the initial timedMetadata is not "key value observed". However after the first instance of timedMetadata being missed, all other timedMetadata seems to be observed without issue.
As a temporary fix, I've started to embed dummy timedMetadata tags in the beginning of videos that do nothing but "kick the tires" so to speak and everything works fine after that. Yet this seems pretty kludgy. I suspect that either I'm setting up the AVPlayerItem and KVO in a sub-optimal manner OR there's just a bug here.
Any ideas on why this might be happening are greatly appreciated! Code below....
// CL: Define constants for the key-value observation contexts.
static const NSString *ItemStatusContext;
static const NSString *ItemMetadataContext;
static const NSString *ItemPlaybackForcastContext;
- (id)initWithURL:(NSURL *)url
{
if (self = [super init]) {
__weak TFPAVController *_self = self;
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
NSString *tracksKey = #"tracks";
[asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:tracksKey] completionHandler:
^{
dispatch_async(dispatch_get_main_queue(),
^{
NSError *error = nil;
AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey error:&error];
if (status == AVKeyValueStatusLoaded) {
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
[item addObserver:_self forKeyPath:#"status" options:0 context:&ItemStatusContext];
[item addObserver:_self forKeyPath:#"timedMetadata" options:0 context:&ItemMetadataContext];
[item addObserver:_self forKeyPath:#"playbackLikelyToKeepUp" options:0 context:&ItemPlaybackForcastContext];
[[NSNotificationCenter defaultCenter] addObserver:_self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:item];
AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
_self.totalRunTime = CMTimeGetSeconds(item.duration);
[_self.delegate avPlayerNeedsView:player];
_self.playerItem = item;
_self.player = player;
}
else {
NSLog(#"The asset's tracks were not loaded: %# // [%# %#]",
error.localizedDescription,
NSStringFromClass([self class]),
NSStringFromSelector(_cmd));
}
_self.playerObserver = [_self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, _FrameRate_)
queue:NULL
usingBlock: ^(CMTime time) {
_self.currentVideoTime = CMTimeGetSeconds([_self.playerItem currentTime]);
}];
});
}];
}
return self;
}
#pragma mark - KVO Response Methods
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
__weak TFPAVController *_self = self;
if (context == &ItemStatusContext) {
dispatch_async(dispatch_get_main_queue(),
^{
if (((AVPlayerItem *)object).status == AVPlayerItemStatusReadyToPlay) {
[_self.delegate videoIsLoadedInPlayer:_self];
}
});
return;
}
else if (context == &ItemMetadataContext) {
dispatch_async(dispatch_get_main_queue(),
^{
[_self checkMetaDataForPlayerItem: (AVPlayerItem *)object];
});
return;
}
else if (context == &ItemPlaybackForcastContext) {
dispatch_async(dispatch_get_main_queue(),
^{
AVPlayerItem *playerItem = object;
if (CMTimeGetSeconds([playerItem currentTime]) <= 0) return;
NSDictionary *notificationDictionary = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:playerItem.playbackLikelyToKeepUp]
forKey:kAVPlayerStateKey];
[[NSNotificationCenter defaultCenter] postNotificationName:kAVPlayerNotification
object:self
userInfo:notificationDictionary];
});
return;
}
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void)checkMetaDataForPlayerItem:(AVPlayerItem *)item
{
NSMutableDictionary *metaDict = [NSMutableDictionary dictionary];
// CL: make sure there's stuff there
if (item.timedMetadata != nil && [item.timedMetadata count] > 0) {
// CL: if there is, cycle through the items and create a Dictionary
for (AVMetadataItem *metadata in item.timedMetadata) {
[metaDict setObject:[metadata valueForKey:#"value"] forKey:[metadata valueForKey:#"key"]];
}
// CL: pass it to the delegate
[self.delegate parseNewMetaData:[NSDictionary dictionaryWithDictionary:metaDict]];
}
}
Ahhh, KVO. Probably one of Apple's all-time worst design decisions.
I guess it's no longer relevant, but at a guess the problem you're having is that sometimes the value you're trying to observe has already been assigned to the key when you get around to adding yourself as an observer, so your observer selector isn't called.
To avoid this you can add NSKeyValueObservingOptionInitial to the options when calling addObserver:forKeyPath:options:context:, and your observer method will be invoked immediately with the current value.
I've been having intermittent problems with NSURLConnection requests timing out in our iPhone app. It seems to be occurring more of late. Once it enters this state, it stays in that state. The only resolution seems to be killing the app and restarting it.
Observations:
The core code that executes the NSURLConnection has not changed (except for some custom user-agent code recently added).
Have yet to find a reproducible case, but timeouts seem to occur after the app has been sitting in the background for a while, particularly if running on 3G (no WiFi).
Apache on server is logging no requests from client while it's experiencing these timeouts.
Some indications that other apps, like Mail and Safari are affected (i.e., experiencing timeouts), although not consistently.
3G coverage is solid where I'm at, not to rule out a transitory issue triggering the problem (assumed not likely).
All requests are going to our own API server, and are restful POST requests.
We use our own NSTimer-based timeout, due to the issues with timeoutInterval and POST requests. I've tried playing around with increasing the timeout value -- problem still occurs.
Other miscellaneous stuff:
App was recently converted to ARC.
Running app under iOS 5.1.1.
App uses latest versions of UrbanAirship, TestFlight and Flurry SDKs.
Also using ARC branch of TouchXML to parse responses.
As you can see below, the code runs on the main thread. I assumed something is blocking on that thread, but the stack traces I see when suspending the app suggest the main thread is fine. I take it that NSURLConnection is using its own thread and that must be blocked.
#define relnil(v) (v = nil)
- (id) initWebRequestController
{
self = [super init];
if (self)
{
//setup a queue to execute all web requests on synchronously
dispatch_queue_t aQueue = dispatch_queue_create("com.myapp.webqueue", NULL);
[self setWebQueue:aQueue];
}
return self;
}
- (void) getStuffFromServer
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
dispatch_sync([self webQueue], ^{
error_block_t errorBlock = ^(MyAppAPIStatusCode code, NSError * error){
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didEncounterErrorGettingPointsWithCode:code andOptionalError:error];
});
};
parsing_block_t parsingBlock = ^(CXMLDocument * doc, error_block_t errorHandler){
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/data/stuff" error:&error];
if (error || !node) {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
else {
stuffString = [node stringValue];
}
if (stuffString) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didFinishGettingStuff:stuffString];
});
}
else {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
};
NSURL * url = [[NSURL alloc] initWithString:[NSString stringWithFormat:MyAppURLFormat_MyAppAPI, #"stuff/getStuff"]];
NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:url];
NSMutableDictionary * postDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[[NSUserDefaults standardUserDefaults] objectForKey:MyAppKey_Token], #"token",
origin, #"from",
destination, #"to",
transitTypeString, #"mode",
time, #"time",
nil];
NSString * postString = [WebRequestController httpBodyFromDictionary:postDictionary];
[urlRequest setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
[urlRequest setHTTPMethod:#"POST"];
if (urlRequest)
{
[self performAPIRequest:urlRequest withRequestParameters:postDictionary parsing:parsingBlock errorHandling:errorBlock timeout:kTimeout_Standard];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
relnil(url);
relnil(urlRequest);
});
});
}
- (void) performAPIRequest: (NSMutableURLRequest *) request
withRequestParameters: (NSMutableDictionary *) requestParameters
parsing: (parsing_block_t) parsingBlock
errorHandling: (error_block_t) errorBlock
timeout: (NSTimeInterval) timeout
{
NSAssert([self apiConnection] == nil, #"Requesting before previous request has completed");
NSString * postString = [WebRequestController httpBodyFromDictionary:requestParameters];
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSString * erVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleShortVersionString"];
NSString * erBuildVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
if ([erBuildVersion isEqualToString:erVersion] || [erBuildVersion isEqualToString:#""]) {
erBuildVersion = #"";
} else {
erBuildVersion = [NSString stringWithFormat:#"(%#)", erBuildVersion];
}
NSString * iosVersion = [[UIDevice currentDevice] systemVersion];
NSString * userAgent = [NSString stringWithFormat:#"MyApp/%#%# iOS/%#", erVersion, erBuildVersion, iosVersion];
[request setValue:userAgent forHTTPHeaderField:#"User-Agent"];
[request setTimeoutInterval:(timeout-3.0f)];
dispatch_sync(dispatch_get_main_queue(), ^{
NSURLConnection * urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if (urlConnection)
{
[self setApiConnection:urlConnection];
requestParseBlock = [parsingBlock copy];
requestErrorBlock = [errorBlock copy];
NSMutableData * aMutableData = [[NSMutableData alloc] init];
[self setReceivedData:aMutableData];
relnil(aMutableData);
[urlConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[urlConnection start];
relnil(urlConnection);
NSTimer * aTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:#selector(timeoutTimerFired:) userInfo:nil repeats:NO];
[self setTimeoutTimer:aTimer];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
});
//we want the web requests to appear synchronous from outside of this interface
while ([self apiConnection] != nil)
{
[NSThread sleepForTimeInterval:.25];
}
}
- (void) timeoutTimerFired: (NSTimer *) timer
{
[[self apiConnection] cancel];
relnil(apiConnection);
relnil(receivedData);
[self requestErrorBlock](MyAppAPIStatusCodeTimeout, nil);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self requestErrorBlock](MyAppAPIStatusCodeFailedToConnect, error);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
MyAppAPIStatusCode status = MyAppAPIStatusCodeFailedToParse;
CXMLDocument *doc = [[self receivedData] length] ? [[CXMLDocument alloc] initWithData:[self receivedData] options:0 error:nil] : nil;
DLog(#"response:\n%#", doc);
if (doc)
{
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/result" error:&error];
if (!error && node)
{
status = [[node stringValue] intValue];
if (status == MyAppAPIStatusCodeOK)
{
[self requestParseBlock](doc, [self requestErrorBlock]);
}
else if (status == MyAppAPIStatusCodeTokenMissingInvalidOrExpired)
{
[Definitions setToken:nil];
[self requestMyAppTokenIfNotPresent];
[Definitions logout];
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestControllerDidRecivedExpiredTokenError:self];
});
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
status = MyAppAPIStatusCodeUnexpectedResponse;
[self requestErrorBlock](status, nil);
}
relnil(doc);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
URLs below are some screenshots of the queues/threads when the app was in the problematic state. Note, I believe thread 10 is related to the cancel performed on the previous timeout, although the mutex wait is curious. Also, the bit in thread 22 about Flurry does not consistently appear when experiencing the problem on other occasions.
Stack trace screenshots:
http://img27.imageshack.us/img27/5614/screenshot20120529at236.png
http://img198.imageshack.us/img198/5614/screenshot20120529at236.png
Perhaps I'm overlooking something obviously wrong in those traces, as I'm relatively new to iOS/Apple development.
All of this would be much simpler to solve if I had the source for NSURLConnection and related code, but such as it is, I'm taking stabs in the dark at this point.
Removing the TestFlight 1.0 SDK seemed to fix the problem. TestFlight also confirmed that they're working on a fix. Given that it's been over a month since the bug was originally confirmed by others, I wonder how close we are to getting a fix.