Потоковое видео при загрузке 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 есть что-то подобное?
3 ответа
Я понял, как это сделать, и это намного проще, чем моя первоначальная идея.
Во-первых, поскольку мое видео находится в формате.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];
При этом методе изначально пользователь загружает два видеофайла одновременно, но первоначальное тестирование скорости сети, которое будут использовать мои пользователи, показывает, что оно увеличивает время загрузки только на несколько секунд. Работает для меня!
Вы должны создать внутренний веб-сервер, который действует как прокси! Затем настройте плеер для воспроизведения фильма с локального хоста.
При использовании протокола HTTP для воспроизведения видео с MPMoviePlayerViewController первое, что делает игрок, это запрашивает диапазон байтов 0-1 (первые 2 байта) только для получения длины файла. Затем проигрыватель запрашивает "порции" видео, используя HTTP-команду "byte-range" (цель - сэкономить батарею).
Что вам нужно сделать, это реализовать этот внутренний сервер, который доставляет видео на проигрыватель, но ваш "прокси" должен учитывать длину вашего видео как полную длину файла, даже если фактический файл не был полностью загружен из Интернета.
Затем вы настраиваете проигрыватель для воспроизведения фильма с " http:// localhost: someport "
Я делал это раньше... это прекрасно работает!
Удачи!
Я могу только предположить, что MPMoviePlayerViewController кэширует длину файла, когда вы его запустили.
Чтобы решить (просто) эту проблему, сначала определите размер файла. Затем создайте файл такой длины. Сохраняя указатель смещения при загрузке файла, вы можете перезаписать "нулевые" значения в файле реальными данными.
Таким образом, вы попадаете в определенный момент загрузки, запускаете MPMoviePlayerViewController и запускаете его. Я бы также предложил вам использовать флаг "F_NOCACHE" (с fcntl()), чтобы обойти кеш файловых блоков (что означает, что вы уменьшите объем используемой памяти).
Недостатком этой архитектуры является то, что если вы застопорились, и киноплеер опередил вас, ну, у пользователя будет довольно неприятный опыт. Не уверен, есть ли какой-либо способ для вас контролировать и предпринимать упреждающие действия.
РЕДАКТИРОВАТЬ: вполне возможно, что видео не читается последовательно, но определенная информация требует от игрока, чтобы по существу чего-то ожидать. Если так, то это обречено на провал. Единственное другое возможное решение - использовать какой-либо программный инструмент для последовательного заказа файла (я не эксперт по видео, поэтому не могу прокомментировать какой-либо из вышеперечисленных).
Чтобы проверить это, вы можете создать "поврежденное" видео различной длины и проверить, чтобы увидеть, что работает, а что нет. Например, предположим, у вас есть файл 100Meg. Напишите небольшую служебную программу и перепишите последние 50 мегабайт данных с нулями. Теперь играйте это видео. Это должно провалиться 1/2 через. Если это не удалось сразу, ну, теперь вы знаете, что его искать в файле.
Если он не последовательный, возможно, он просматривает последние 1000 байтов или около того, и в этом случае, если вы не перезаписываете, все работает так, как вы хотите. Если вам повезет, и в этом случае вы в конечном итоге загрузите последние 1000 байтов, а затем начнете с начала файла.
Это действительно сводится к тому, чтобы найти какой-то способ, прежде чем вводить реальные картинки в картинку, чтобы воспроизвести частичный файл. Вам наверняка будет проще искусственно представить сетевые условия, не делая это в реальном времени.