Работаю в аутсорсинговой компании и как-то встала задача загрузки видео с возможностью последующей обработки для внутренних нужд приложения: ресайз до нужного размера, конвертирование к нужному формату, вытягивание аудио-дорожек (если таковые присутствуют), раскадровка видео. В конце результаты нужно сохранить в облачном хранилище для последующего использования в онлайн-редакторе. Требования: масштабируемость, неограниченый размер видео, скорость, кроссбраузерность, наглядность.
Поскольку тема очень обширная, разделю ее на разделы:
- Общие проблемы, нюансы, с которыми пришлось столкнуться
- Загрузка видео (на этой теме, пожалуй, не буду останавливаться, поскольку она уже поднималась в этом и этом посте.
- Обработка видео.
- Сохранение в облачном хранилище.
Часть первая
1. Кроссбраузерность
Для того, чтобы обеспечить это требование нельзя было использовать HTML5 загрузку файлов (не во всех браузерах она реализована). Поэтому мы пошли путем создания обычной HTML4 формы, которая сабмитилась на сервер с node.js. Для того, чтобы получить текущий прогресс загрузки/обработки видео создавались AJAX-запросы. В форму включался случайно сгенерированная строка-идентификатор (например, 32 символа). Node.js привязывал к ней информацию по текущему файлу и при каждом запросе состояния передавал вот такой JSON:
{
"fileid":"uwrd28a9v71j444d260c55hkj6uli06j", // Идентификатор
"name":"test.FLV", // Название
"progress":"7.80", // Проценты загрузки
"status":"uploading", // Статус
"audio":10.02, // Время аудио
"frame":301, // Текущий фрейм, который обрабатывается
"videoProgress":"7.80", // Процент видео
"audioProgress":"3.73", // Процент аудио
"frameProgress":"3.73", // Процент раскадровки
"messages":[], // Сообщение, например об отсутствии аудио дорожки
"expectedBytes":5389574, // Ожидаемый размер
"uploadedBytes":420139, // Загружено байт
"bps":91773.48186981214, // Скорость байт/сек
"uploadEstimated":54, // Время до окончания загрузки
"videoEstimated":42, // Время до окончания обработки видео
"audioEstimated":75, // Время до окончания обработки аудио
"frameEstimated":71 // Время до окончания раскадровки
}
На основании этого пользователь видит состояние загрузки.
2. Авторизация пользователя
Поскольку фронтенд на PHP (там же происходит авторизация пользователя) нужно было как-то передать данные сессии на сервер node.js. Для этих целей использовался memcached. О подмене сессий в PHP можно прочесть тут. Суть проста: сессии сохраняются в memcached, при загрузке через форму передается session_id, который считывается node.js. Далее node.js обращается к memcached в поисках соответствущей сессии, берет user_id и т.д. Здесь есть один важный момент: session_id в самой форме нужно ставить вначало, потому что POST-запрос передает параметры в порядке их следования в HTML. То есть, если мы сделаем так:
<form method="post" action="http://nodeserver.com/">
<input type="file" name="video"/>
<input type="hidden" name="session_id" value="session_id"/>
<input type="submit" name="submit"/>
</form>
В таком случае к node.js сначала прийдет сам файл, а потом session_id, что не есть хорошо. Ведь нам нужно сначала знать, от кого приходит файл и отменить в случае, если пользователь не авторизирован. Если в примере выше поменять местами file и session_id, то сначала мы получаем сессию, притормаживаем загрузку файла, и ждем пока сервер проверит, все ли в порядке с пользователем и только тогда продолжаем загрузку. В node.js можно ставить request на паузу.
3. Потоковая обработка видео
Проблема в том, что не все видео может быть обработано сразу. Например, по спецификации mp4 формата метаданные идут в конце файла. А нам ведь нужно сразу знать какие размеры у видео, какие дорожки есть в файле, длительность и т.д. Плюс к этому некоторые форматы видео при перекодировке нуждаются, чтобы была возможность обращаться к разным частям исходного файла. Исходя из этого есть два случая с двумя вариантами вариантами каждый:
1. Вначале загрузки, когда мы вытягиваем командой ffmpeg информацию о файле, используя node.js:
var spawn = require('child_process').spawn;
var ffmpeg = spawn('ffmpeg', ['-i', '-']);
var ffmpeg_stdout = ''
ffmpeg.stderr.on('data', function(buffer)
{
ffmpeg_stdout += buffer;
// Получаем информацию о файле
});
// file - берется с нашей формы
file.on('data', function(buffer)
{
ffmpeg.stdin.write(buffer);
});
В переменной ffmpeg_stdout мы получим все дорожки, которые содержатся в файле или ошибку. Если есть ошибка, значит ffmpeg не может обработать файл потоком. В таком случае предупреждаем пользователя об этом и загружаем весь файл сразу, и только после этого производим нужные операции.
2. В данном случае скорость важна, поэтому мы пытаемся запустить раскадровку, как только видео только началось загружаться. Но если формат не поддерживает потоковую обработку, то приходится ждать момента полной загрузки.
Получается такой алгоритм: возможно извлечь метаданные, тогда пытаемся налету обработать видео, если и это получается, то радуемся, во всех остальных случаях ждем полной загрузки. На практике лишь небольшое количество видео требуется загружать полностью, чтобы начать обработку.
На сегодня пожалуй все, завтра утром на работу.
Хотел бы завершить на мажорной ноте: возможности безграничны, можно распределить нагрузку на несколько серверов, на ходу показывать результаты, например вытягивать последний готовый фрейм из видео. Работает это все на Amazon EC2. По скорости могу сказать, что скорость загрузки не было возможности проверить (канал слабый), но не меньше 10мбит, думаю точно. А по нагрузке на сервер: Middle server в одиночку одно видео тянет где-то 2Мбит видео, то есть загрузка ушла далеко вперед, а обработка не поспевает. Наибольшее время занимает ресайз. Раскадровка и вытягивание аудио-дорожки относительно быстро происходит.
Если пост понравится, опишу реализацию и механизм работы более подробно в следующие разы.
Скриншоты того, что получилось. Сразу оговорюсь, это еще не рабочий вариант, поэтому строго не судите:
Автор: matchp