AVPlayer 使用资源加载器委托在大型视频文件上停滞

AVPlayer stalling on large video files using resource loader delegate

我正在使用 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 和 iPad - iOS 8.1.3

我对播放视频进行了必要的更改,它运行良好,但当我尝试播放很长的视频(11-12 分钟长,大小约为 85mb)时,视频会在播放后大约 4 分钟停止连接完成加载。我收到一个 playbackBufferEmpty 事件和一个播放器项目停止通知。

这是代码的要点

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];
}

}

问题似乎是在连接完成加载一段时间后,播放器项目缓冲区变空了。我目前的想法是,当连接完成加载并弄乱 playerItem 缓冲区时,某些东西正在被释放。

但是当缓冲区变空时,playerItem 状态良好,视频资产可播放,视频数据良好

如果我通过 charles 限制 wifi 并减慢连接速度,只要连接没有在视频结束后的几分钟内完成加载,视频就会播放。

如果我在完成加载事件上设置连接 nil,资源加载器将在 shouldWaitForLoadingOfRequestedResource 再次触发时启动一个新连接。在这种情况下,加载会重新开始,视频将继续播放。

我应该提一下,如果我将这段长视频作为普通的 http url 资产播放,它可以正常播放,并且在保存到设备并从那里加载后也可以正常播放。

当资源加载器委托启动 NSURLConnection 时,连接会接管将 NSData 保存到挂起的请求并处理它们。当连接完成加载时,资源加载器重新负责处理加载请求。代码将加载请求添加到待处理请求数组,但问题是它们没有被处理。将方法更改为以下并且有效。

//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;
}