Потоковое видео во время загрузки iOS

Я использую iOS 7, и у меня есть видео .mp4, которое мне нужно скачать в моем приложении. Видеоролик большой (~ 1 ГБ), поэтому он не входит в состав приложения. Я хочу, чтобы пользователь мог начать просмотр видео, как только начнется загрузка. Я также хочу, чтобы видео можно было кэшировать на устройстве iOS, поэтому пользователю не нужно будет загружать его позже. Как обычные способы воспроизведения видео (прогрессивная загрузка, так и потоковая передача в реальном времени), похоже, не позволяют вам кэшировать видео, поэтому я сделал свой собственный веб-сервис, который разбивает мой видеофайл и передает потоки байтам клиенту. Я запускаю потоковый HTTP-вызов с помощью NSURLConnection:

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 и прогрессивного видео с помощью AVURLAssets

Прогрессивная загрузка видео на 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 заставляет приложение зависать в течение примерно 10 секунд, пока он создает файл размером ~ 1 ГБ на диске (на симуляторе он работает мгновенно, только у этого реального устройства есть эта проблема). Вот где я сейчас застрял - кто-нибудь знает способ более эффективного распределения файла или другой способ заставить видеоплеер распознавать размер файла без необходимости его фактически распределять? Я знаю, что некоторые файловые системы поддерживают "размер файла" и "размер на диске" как два разных свойства... не уверен, что у 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

Вам нужно создать внутренний веб-сервер, который действует как прокси-сервер! Затем установите плеер для воспроизведения фильма с локального хоста.

При использовании протокола HTTP для воспроизведения видео с помощью MPMoviePlayerViewController первое, что делает игрок, это попросить байтовый диапазон 0-1 (первые 2 байта) только для получения длины файла. Затем игрок запрашивает "куски" видео, используя "HTTP-команду" байтового диапазона (целью является сохранение некоторой батареи).

Что вам нужно сделать, так это реализовать этот внутренний сервер, который доставляет видео проигрывателю, , но ваш "прокси" должен учитывать длину вашего видео как полную длину файла, даже если фактический файл не был полностью загружен из Интернета.

Затем вы устанавливаете плеер для воспроизведения фильма с "http://localhost: someport"

Я сделал это раньше... он отлично работает!

Удачи!

Ответ 3

Я могу только предположить, что MPMoviePlayerViewController кэширует длину файла файла при его запуске.

Способ исправления (просто) этой проблемы заключается в том, чтобы сначала определить, насколько велик файл. Затем создайте файл этой длины. Сохраняя указатель смещения, когда файл загружается, вы можете перезаписать "нулевые" значения в файле с реальными данными.

Итак, вы попадаете в определенную точку загрузки, запускаете MPMoviePlayerViewController и запускаете его. Я также предлагаю вам использовать флаг F_NOCACHE (с fcntl()), чтобы вы обошли кеш блока файлов (это означает, что вы уменьшите объем памяти).

Недостатком этой архитектуры является то, что если вы застопорились, а игрок в кино опережает вас, ну, у пользователя будет довольно плохой опыт. Не уверен, что вы можете контролировать и принимать превентивные меры.

EDIT: вполне возможно, что видео не читается последовательно, но определенная информация требует от игрока существенно задуматься о чем-то. Если это так, то это обречено на провал. Единственное другое возможное решение - использовать некоторый программный инструмент для последовательного заказа файла (я не эксперт по видео, поэтому не могу комментировать опыт по любому из вышеперечисленных).

Чтобы проверить это, вы можете создать "поврежденное" видео различной длины и проверить это, чтобы увидеть, что работает, а что нет. Например, предположим, что у вас есть файл 100Meg. Напишите небольшую служебную программу, и запишите последние 50Megs данных с нулями. Теперь воспроизведите это видео. Его не должно проходить через 1/2. Если это не удается сразу, ну, теперь вы знаете, что его поиск в файле.

Если он не является последовательным, его возможно, что он смотрит на последние 1000 байтов или около того, и в этом случае, если вы не перезаписываете то, что работает, как хотите. Если вам повезет, и это так, вы в конечном итоге загрузите последние 1000 байт, а затем начните с передней части файла.

Это действительно помогает найти способ до, введя реальную сеть в изображение, чтобы воспроизвести частичный файл. Вам, несомненно, будет легче искусственно вводить сетевые условия, не делая реального времени.