Потоковая загрузка и обработка видео посредством node.js + php и ffmpeg — часть первая

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

Поскольку тема очень обширная, разделю ее на разделы:

  1. Общие проблемы, нюансы, с которыми пришлось столкнуться
  2. Загрузка видео (на этой теме, пожалуй, не буду останавливаться, поскольку она уже поднималась в этом и этом посте.
  3. Обработка видео.
  4. Сохранение в облачном хранилище.


Часть первая


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Мбит видео, то есть загрузка ушла далеко вперед, а обработка не поспевает. Наибольшее время занимает ресайз. Раскадровка и вытягивание аудио-дорожки относительно быстро происходит.

Если пост понравится, опишу реализацию и механизм работы более подробно в следующие разы.

Скриншоты того, что получилось. Сразу оговорюсь, это еще не рабочий вариант, поэтому строго не судите:

Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 32

    0
    Надеюсь уже 12 добавлений в избранное, показатель того что мы ждем продолжения и деталей.
    Тема интересная.
      0
      из продолжений интересует подробности конвертации и более подродно рассказать о настройках ffmpeg
        0
        >Надеюсь уже 12 добавлений в избранное
        12 добавлений не показатель, 112 добавлений — вот показатель!
        0
        У меня единственный вопрос:…

        Который отпал после вот этого пассажа:

        Здесь есть один важный момент: session_id в самой форме нужно ставить вначало, потому что POST-запрос передает параметры в порядке их следования в HTML. То есть, если мы сделаем не так, в таком случае к node.js сначала прийдет сам файл, а потом session_id, что не есть хорошо.


          +1
          > возможности безграничны

          > нужно было как-то передать данные сессии на сервер node.js. Для этих целей использовался memcached.

          Найдите противоречие
            0
            Можно загружать видео на один сервер, а запускать обработку на другом и т.д. (перекодировка на одном, раскадровка на втором, экстракт аудио на третьем) Т.е. распределить загрузку.
              0
              Вы упретесь в memcached. Чудес не бывает.

              Остальное достигается какими угодно средствами, нетолько странной связкой php + memcached + nodejs
                0
                На счет memcached согласен, но все же это можно обойти. Это решение было взято, как «первое попавшееся». Сейчас, думаю, можно будет прибавить mysql, чтобы подстраховать memcached. Если вдруг будет много пользователей.
                  0
                  > Сейчас, думаю, можно будет прибавить mysql,
                  > возможности безграничны

                  Найдите противоречие.
                    0
                    Был бы рад выслушать идеи по улучшению.
                      0
                      Я не про улучшения. Я об излишнем энтузиазме. Особенно учитывая количество движущихся частей (php + memcached + nodejs + ffmpeg)
          +1
          Насчет порядка полей в форме повеселили честное слово *Thumbs up*

          <form method="post" action="http://nodeserver.com/?session_id=my_session_id">
            <input type="file" name="video"/>
            <input type="submit" name="submit"/>
          </form>
          
            0
            У вас тормозит ресайз внутри самого ffmpeg, или самодельный?
              0
              Внутри самого ffmpeg. Для обработки видео использовался только он.
                0
                покажите работу с ффмпег и его опции сборки
                  0
                  ffmpeg version N-35482-gb404ab9, Copyright (c) 2000-2011 the FFmpeg developers
                    built on Dec  7 2011 01:14:33 with gcc 4.4.5 20110214 (Red Hat 4.4.5-6)
                    configuration: --enable-gpl --enable-nonfree --enable-postproc --enable-avfilter --enable-pthreads --enable-libxvid --enable-libx264 --enable-libmp3lame --enable-libfaac --disable-ffserver --disable-ffplay --enable-libtheora --enable-libvorbis --disable-ffplay --enable-shared
                    libavutil    51. 30. 0 / 51. 30. 0
                    libavcodec   53. 41. 0 / 53. 41. 0
                    libavformat  53. 24. 0 / 53. 24. 0
                    libavdevice  53.  4. 0 / 53.  4. 0
                    libavfilter   2. 51. 0 /  2. 51. 0
                    libswscale    2.  1. 0 /  2.  1. 0
                    libpostproc  51.  2. 0 / 51.  2. 0
                  
                  
                    0
                    а где вызов? Да и вы так сильно не любите ffplay, что 2 раза делаете --disable-ffplay? Это один из лучших плееров!
                      0
                      Не знал про ffplay, нужно будет посмотреть.
                      Видео конвертируется следующим скриптом:
                      SourceProcessor.prototype.convertSource = function(onFile)
                      {
                      	var self = this;
                      	if(! self.hasVideo()) // Если нет видео - ничего не делаем
                      		return this;
                      	var needConvert = false // По умолчанию не надо конвертировать
                      		, params = ['-i', (self.fb.WE ? self.fb.filename : '-')] // Файл берем либо из stdin либо из файловой системы
                      		, size;
                      	// Do we need to resize?
                      	if(size = self.countVideoSize(self.getVWidth(), self.getVHeight(), self.MAX_WIDTH, self.MAX_HEIGHT))
                      	{ // getVWidth(), getVHeight() - текущий размер, MAX_WIDTH = 800, MAX_HEIGHT = 450
                      		var width = size[0], height = size[1];
                      		if(self.getVWidth() > width || self.getVHeight() > height) // Если размер больше того, что есть, то конвертируем
                      		{
                      			params.push('-s', (width + 'x' + height));
                      			needConvert = true;
                      		}
                      	}
                      	// Do we need to change format?
                      	// allowedVideoCodecs = 
                       	// ['flv1'
                       	// , 'vp6f'
                       	// , 'flv4'
                       	// , 'h264']
                      	if(! self.allowedVideoCodecs.has(self.getVCodec()))
                      	{
                      		params.push('-vcodec', self.DEFAULT_VIDEO_CODEC);
                      		needConvert = true;
                      	}
                      	// Set default container
                      	params.push('-f', 'flv');
                      	// Detect new video bitrate
                      	// Здесь вычисляем битрейт - сюда можете обратить внимание, я взял приблизительно, чтобы не снизить качество видео
                      	// getVFrameRate() - количество фреймов
                      	var newWidth = params.has('-s') ? width : self.getVWidth()
                      		, newHeight = params.has('-s') ? height : self.getVHeight()
                      		, bitrate = Math.round( ( newWidth * newHeight * self.getVFrameRate() * 4 * 0.07 ) / 1000 );
                      	params.push('-vb', (bitrate + 'k'));
                      	// If there is audio convert to proper format
                      	if(self.hasAudio())
                      	{// Если есть аудио ставим нужный кодек/битрейт
                      		params.push('-acodec', 'libfaac', '-ab', '256k');
                      	}
                      	// If there are subtitles - remove them
                      	if(self.hasSubtitle())
                      	{
                      		params.push('-sn');
                      		needConvert = true;
                      	}
                      	self.needConvert = needConvert;
                      	// В итоге, если нужна конвертация будет примерно так:
                      	// ffmpeg -i - -s 800x450 -vcodec libx264 -f flv -vb 2000k -acodec libfaac -ab 256k -sn pipe:1
                      	onFile((new VideoFile(self.fb)).setParams(params).needConvert(needConvert).afterProcess(self.readFromFS).start());
                      	return this;
                      };
                      
                        0
                        а почему не mp4 контейнер выбрали?
                          0
                          Был выбор из двух: flv или mp4, в конечном итоге с этим будет работать flex-приложение. Честно скажу, не помню, почему именно flv, но кажется был какой-то глюк с mp4. Вообще ffmpeg тулза хорошая, но нюансов там хватает.
              +2
              А зачем здесь node.js?
                +3
                модно же :)
                  0
                  Чем предлагаете заменить?
                    0
                    ссм пост ниже
                  0
                  делал все тоже самое но средствами nginx + php без проблем:
                  загрузка ngx_http_upload_module
                  прогресс ngx_http_upload_progress_module
                  приватность ngx_http_accesskey_module
                  отдача видео ngx_http_flv_module

                  обработка видео ffmpeg на отдельном сервере

                    0
                    В данном случае обработка видео идет по ходу загрузки и на том же сервере, куда загружается видео.
                      0
                      значить вам повезло, что сервер мало загруженный.
                      Обработка видео — это операция требующая не мало процессорного времени, если ваш фронтэнд отжирает 100-120% на ядро, то обрабатывать на этом сервере и видею будет через-чур накладно.
                        0
                        У нас отдельно используются сервера под фронтенд, отдельно под обработку видео. Причем добавить новый сервер под видео — 5 минут. Используется Amazon EC2, там с этим просто.
                        Запросы загрузки кроссдоменные (easyXDM).
                    0
                    Идея поста интересная, хотелось бы увидеть пост с более подробным описанием.
                      0
                      Все здорово. Только зачем нужен node.js?

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

                    Самое читаемое