下载iOS时流视频

时间:2014-01-03 20:54:26

标签: ios video-streaming mpmovieplayer nsurlsessiondownloadtask progressive-download

我使用的是iOS 7,我需要在我的应用中下载.mp4视频。视频很大(约1 GB),这就是为什么它不包含在应用程序中的原因。我希望用户能够在开始下载后立即开始观看视频。我还希望视频能够在iOS设备上缓存,以便用户以后不需要再次下载。播放视频的常规方法(渐进式下载和实时流式传输)似乎都不允许您缓存视频,因此我创建了自己的Web服务,将视频文件分块并将字节流式传输到客户端。我使用NSURLConnection启动流式HTTP调用:

self.request = [[NSMutableURLRequest alloc] initWithURL:self.url];
[self.request setTimeoutInterval:10]; // Expect data at least every 10 seconds
[self.request setHTTPMethod:@"GET"];
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES];

当我收到数据块时,我将其附加到文件的本地副本的末尾:

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
     NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:[self videoFilePath]];
     [handle truncateFileAtOffset:[handle seekToEndOfFile]];
     [handle writeData:data];
}

如果我让设备运行,文件下载成功,我可以使用MPMoviePlayerViewController播放它:

NSURL *url=[NSURL fileURLWithPath:self.videoFilePath];
MPMoviePlayerViewController *controller = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
controller.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
[self presentMoviePlayerViewControllerAnimated:controller];

但是,如果我在文件完全下载之前启动播放器,则视频开始播放就好了。它甚至可以在顶部擦洗条上显示正确的视频长度。但是当用户到达视频中我在视频开始之前完成下载的位置时,视频就会挂起。如果我关闭并重新打开MPMoviePlayerViewController,那么视频会播放,直到它到达我再次启动MPMoviePlayerViewController时的任何位置。如果我等到整个视频下载完毕,那么视频播放没有问题。

我没有收到任何事件,或者在发生这种情况时打印到控制台的错误消息(视频启动后永远不会发送MPMoviePlayerPlaybackStateDidChangeNotification和MPMoviePlayerPlaybackDidFinishNotification)。似乎还有其他东西告诉控制器视频的长度不是除了洗涤器使用的...

有谁知道导致这个问题的原因是什么?我不一定要使用MPMoviePlayerViewController,所以如果在这种情况下使用不同的视频播放方法,我就是全力以赴。

相关未解决的问题:

AVPlayer and Progressive Video Downloads with AVURLAssets

Progressive Video Download on iOS

How to play an in downloading progress video file in IOS

更新1 我发现视频停顿确实是因为视频开始播放时的文件大小。我可以通过在开始下载之前创建一个归零文件来解决这个问题,并在我去的时候覆盖它。由于我可以控制视频流服务器,因此我添加了一个自定义标头,因此我知道正在流式传输的文件的大小(流式文件的默认文件大小标头为-1)。我在didReceiveResponse方法中创建文件如下:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{        
    // Retrieve the size of the file being streamed.
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
    NSDictionary *headers = httpResponse.allHeaderFields;

    NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
    self.streamingFileSize = [formatter numberFromString:[headers objectForKey:@"StreamingFileSize"]];

    // Check if we need to initialize the download file
    if (![[NSFileManager defaultManager] fileExistsAtPath:self.path])
    {            
        // Create the file being downloaded
        [[NSData data] writeToFile:self.path atomically:YES];

        // Allocate the size of the file we are going to download.
        const char *cString = [self.path cStringUsingEncoding:NSASCIIStringEncoding];
        int success = truncate(cString, self.streamingFileSize.longLongValue);
        if (success != 0)
        {
            /* TODO: handle errors here. Probably not enough space... See 'man truncate' */
        }
    }
}

这很好用,除了truncate导致应用程序挂起大约10秒,同时它在磁盘上创建~1GB文件(在模拟器上它是即时的,只有真正的设备有这个问题)。这就是我现在被困住的地方 - 有没有人知道如何更有效地分配文件,或者以不同的方式让视频播放器识别文件的大小而无需实际分配它?我知道一些文件系统支持“文件大小”和“磁盘大小”作为两个不同的属性...不确定iOS是否有这样的东西?

3 个答案:

答案 0 :(得分:12)

我想出了如何做到这一点,它比我原来的想法简单得多。

首先,由于我的视频是在.mp4中,MPMoviePlayerViewController或AVPlayer类可以直接从Web服务器播放 - 我不需要实现任何特殊的东西,他们仍然可以寻找视频中的任何一点。这必须是.mp4编码如何与电影播放器​​配合使用的一部分。所以,我只是在服务器上提供了原始文件 - 不需要特殊的标题。

接下来,当用户决定播放视频时,我立即开始播放服务器URL中的视频:

NSURL *url=[NSURL fileURLWithPath:serverVidelFileURLString];
controller = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
controller.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
[self presentMoviePlayerViewControllerAnimated:controller];

这使得用户可以观看视频并寻找他们想要的任何位置。然后,我开始使用NSURLConnection手动下载文件,就像我上面所做的那样,除了现在我没有流式传输文件,我只是直接下载它。这样我就不需要自定义标头,因为文件大小包含在HTTP响应中。

当我的后台下载完成后,我将播放项目从服务器URL切换到本地文件。这对于网络性能很重要,因为电影播放器​​只能在用户观看的内容之前下载几秒钟。能够尽快切换到本地文件是避免下载过多重复数据的关键:

NSTimeInterval currentPlaybackTime = videoController.moviePlayer.currentPlaybackTime;

[controller.moviePlayer setContentURL:url];
[controller.moviePlayer setCurrentPlaybackTime:currentPlaybackTime];
[controller.moviePlayer play];

此方法确实让用户最初同时下载两个视频文件,但是对我的用户将使用的网络速度进行初步测试显示它只会将下载时间增加几秒钟。适合我!

答案 1 :(得分:7)

您必须创建一个充当代理的内部网络服务器!然后将播放器设置为从本地主机播放电影。

当使用HTTP协议使用MPMoviePlayerViewController播放视频时,播放器首先要做的是要求字节范围0-1(前2个字节)以获取文件长度。然后,播放器使用“字节范围”HTTP命令询问视频的“块”(目的是节省一些电池)。

您需要做的是实现将视频传送到播放器的内部服务器,但您的“代理”必须将视频长度视为文件的全长,甚至如果实际文件尚未从互联网上完全下载。

然后,您将播放器设置为从“http:// localhost:someport”

播放电影

我以前做过这件事......它完美无缺!

祝你好运!

答案 2 :(得分:0)

我只能假设MPMoviePlayerViewController在启动时缓存文件的文件长度。

解决这个问题的方法是首先确定文件的大小。然后创建一个该长度的文件。保存偏移指针,在文件下载时,您可以使用实际数据覆盖文件中的“空”值。

所以你到了下载的特定点,启动MPMoviePlayerViewController,让它运行。我还建议您使用“F_NOCACHE”标志(使用fcntl()),以便绕过文件块缓存(这意味着您将减少内存占用)。

这种架构的缺点是,如果你停滞不前,电影播放器​​领先于你,那么用户将会遇到非常糟糕的体验。不确定是否有任何方法可以监控并采取先发制人的行动。

编辑:很可能视频不是按顺序读取的,但某些信息要求玩家基本上向前看。如果是这样,那么注定要失败。唯一的另一种可能的解决方案是使用一些软件工具来顺序订购文件(我不是视频专家所以不能根据以上任何经验发表评论)。

为了测试这一点,您可以构建一个不同长度的“损坏”视频,并测试它以查看哪些有效,哪些无效。例如,假设您有一个100Meg文件。编写一个小实用程序,然后用零写下最后50个数据。现在播放这个视频。它应该失败1/2到。如果它立即失败,那么,你现在知道它在文件中的搜索。

如果非顺序,其可能表示它查看最后1000个字节左右,在这种情况下,如果你不覆盖那些东西可以按你的意愿工作。如果你运气好的话,你最终会下载最后1000个字节,然后从文件的前面开始。

之前找到某种方式在图片中引入真正的网络,播放部分文件。你肯定会发现在没有真正实时的情况下人工引入网络条件会更容易。