14

我正在使用 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,所以如果在这种情况下可以使用不同的视频播放方法,我完全赞成。

相关未解决的问题:

使用 AVURLAssets 的 AVPlayer 和渐进式视频下载

iOS 上的渐进式视频下载

如何在 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是否有类似的东西?

4

3 回答 3

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

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

于 2014-01-08T19:30:59.137 回答
9

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

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

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

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

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

祝你好运!

于 2014-01-06T14:30:35.277 回答
0

I can only assume that the MPMoviePlayerViewController caches the file length of the file when you started it.

The way to fix (just) this issue is to first determine how large the file is. Then create a file of that length. Keeping an offset pointer, as the file downloads, you can overwrite the "null" values in the file with the real data.

So you get to a specific point in the download, start the MPMoviePlayerViewController, and let it run. I'd also suggest you use the "F_NOCACHE" flag (with fcntl()) so you bypass the file block cache (which means you will lower your memory footprint).

The downside to this architecture is that if you get stalled, and the movie player gets ahead of you, well, the user is going to have a pretty bad experience. Not sure if there is any way for you to monitor and take preemptive action.

EDIT: its quite possible that the video is not read sequentially, but certain information requires the player to essentially look ahead for something. If so, then this is doomed to fail. The only other possible solution is to use some software tool to sequentially order the file (I'm no video expert so cannot comment from experience on any of the above).

To test this out, you can construct a "damaged" video of varying lengths, and test that to see what works and what does not. For instance, suppose you have a 100Meg file. Write a little utility program, and over write the last 50Megs of data with zeros. Now play this video. Its should fail 1/2 through. If it fails right away, well, you now know that its seeking in the file.

If non sequential, its possible that its looking at the last 1000 bytes or so, in which case if you don't overwrite that things work as you want. If you get lucky and this is the case, you would eventually download the last 1000 bytes, then then start from the front of the file.

It really gets down to finding some way before introducing real networking into the picture, to play a partial file. You will surely find it easier to artificially introduce the networking conditions without really doing it real time.

于 2014-01-03T23:20:53.180 回答