下载 iOS 时流式传输视频

Posted

技术标签:

【中文标题】下载 iOS 时流式传输视频【英文标题】:Stream video while downloading iOS 【发布时间】:2014-01-03 20:54:25 【问题描述】:

我使用的是 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)。似乎还有其他东西告诉控制器视频的长度是什么,而不是scrubber正在使用的......

有谁知道是什么导致了这个问题?我并不一定要使用 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 会导致应用程序在磁盘上创建 ~1GB 文件时挂起大约 10 秒(在模拟器上它是即时的,只有真实设备有这个问题)。这就是我现在卡住的地方 - 有没有人知道一种更有效地分配文件的方法,或者另一种让视频播放器识别文件大小而无需实际分配的方法?我知道一些文件系统支持“文件大小”和“磁盘大小”作为两个不同的属性......不确定 iOS 是否有类似的东西?

【问题讨论】:

无法与我合作 【参考方案1】:

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

首先,由于我的视频是 .mp4 格式的,MPMoviePlayerViewController 或 AVPlayer 类可以直接从网络服务器播放它——我不需要实现任何特殊的东西,它们仍然可以搜索到视频中的任何点。这必须是 .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];

这种方法最初确实让用户同时下载两个视频文件,但对我的用户将使用的网络速度的初步测试表明它只会将下载时间增加几秒钟。为我工作!

【讨论】:

这样可能会浪费用户和服务器的数据流量。 是的,上面的答案说明了这一点,但它允许您同时流式传输和下载。 我同意,但我认为这个功能应该得到苹果的支持。这将为开发人员提供更多便利。【参考方案2】:

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

当使用 HTTP 协议通过 MPMoviePlayerViewController 播放视频时,播放器做的第一件事就是请求字节范围 0-1(前 2 个字节)只是为了获取文件长度。然后,播放器使用“byte-range”HTTP 命令请求视频的“块”(目的是节省一些电池)。

你要做的是实现这个将视频传送到播放器的内部服务器,但你的“代理”必须将视频的长度视为文件的完整长度,即使如果实际文件尚未完全从 Internet 下载。

然后你设置你的播放器从“http://localhost:someport”播放电影

我以前做过这个……效果很好!

祝你好运!

【讨论】:

您能否提供任何有关如何设置内部代理的示例代码?我不确定如何在 iOS 应用程序中执行此操作... @WagnerPatriota 我也想使用本地服务器和 AVPlayer 从服务器获取数据,但我不知道该怎么做。您能否也提供一些示例代码?谢谢。 你必须学习如何制作这个 HTTP 服务器。每种视频格式都有其特定的细节……适应与否,直播与否,如此多的细节……不难,打开一个套接字,解析 HTTP 标头并传递你的内容…… @WagnerPatriota 你能把你的示例代码发给我吗?我一直在尝试使用CocoaHTTPServer 让它工作很长时间,但我的视频在播放 3 秒后就搞砸了。干杯! 我不能,因为代码不是我的财产 :-( 但我可以给你一些提示......你有什么样的问题?【参考方案3】:

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

解决(仅)此问题的方法是首先确定文件的大小。然后创建一个该长度的文件。保留一个偏移量指针,随着文件的下载,您可以用真实数据覆盖文件中的“空”值。

因此,您在下载中到达特定点,启动 MPMoviePlayerViewController,并让它运行。我还建议您使用“F_NOCACHE”标志(使用 fcntl()),以便绕过文件块缓存(这意味着您将降低内存占用)。

这种架构的缺点是,如果您卡住了,而电影播放器​​领先于您,那么用户将获得非常糟糕的体验。不确定您是否有任何方法可以监控并采取先发制人的行动。

编辑:很可能视频不是按顺序读取的,但某些信息需要玩家从本质上向前看。如果是这样,那么这注定要失败。唯一可能的解决方案是使用一些软件工具对文件进行顺序排序(我不是视频专家,因此无法根据经验对上述任何内容发表评论)。

要对此进行测试,您可以构建一个不同长度的“损坏”视频,并对其进行测试,看看哪些有效,哪些无效。例如,假设您有一个 100Meg 的文件。编写一个小实用程序,并用零覆盖最后 50Megs 的数据。现在播放这个视频。它应该通过 1/2 失败。如果它立即失败,那么您现在知道它在文件中查找。

如果是非连续的,它可能查看最后 1000 个字节左右,在这种情况下,如果你不覆盖,事情就会按照你的意愿工作。如果幸运的话,你最终会下载最后 1000 个字节,然后从文件的前面开始。

真正要找到一些方法将真正的网络引入图片中,以播放部分文件。您肯定会发现人为地引入网络条件而不是真正实时地进行操作会更容易。

【讨论】:

我一直在考虑类似的方法。但就像你说的,这不会是一个很好的用户体验。我知道这是可能的,因为当您购买视频并想立即观看时,iTunes 就是这样做的。 如果您可以监控您收到了多少数据,以及消耗了多少,您可以暂停,然后在需要时重新启动视频。 Apple 为下载的视频提供了完整的解决方案 - 忘记名称 - 它适应网络质量。但是,您将无法真正保存和重播这样的视频(质量可能很差)。您的问题是与人类延迟(让某人等待多长时间)与停止输出的概率相关的经典问题。没有完美的解决方案。 这不是用户消耗的数据多于他们下载的数据的问题。当设备下载数据的速度比观看速度快得多时,就会出现我遇到的问题。当文件继续下载时,MPMoviePlayerViewController 未正确处理播放,即使它远远领先于用户观看的内容。 我很困惑 - 我在答案中给了你一个想法,你问了一个关于用户体验的问题,如果它停止了,我回复了一个评论,现在你说视频停止了。所以你试过这个建议吗?我也有另一个想法,请参阅编辑。 所以我更接近于根据您的第一个建议找到解决方案。在开始下载之前,我将文件创建为占位符,但分配空文件需要很长时间(参见上面的编辑)。至于寻找未来的用户,一旦我开始工作,我计划使用元数据文件来跟踪用户下载的字节范围,所以我想我可以解决这个问题。此外,元数据文件可用于确保用户无法在下载之前暂停播放器,如果他们超过了已下载的数据。

以上是关于下载 iOS 时流式传输视频的主要内容,如果未能解决你的问题,请参考以下文章

IOS - 流式传输和下载音频文件

如何通过 RTMP 将视频流式传输到 iOS?

通过 AvPlayerItem 和 AvPlayer 流式传输时监控下载的字节数 - iOS

Chromecast ios:流式传输 html 内容

如何在 iOS 上通过安全连接流式传输视频

如何下载(或以 blob 形式获取)用于流式传输 HTML5 视频的 MediaSource?