Задача следующая. Провести совместный просмотр ролика с YouTube в реальном времени несколькими пользователями. Зрители должны получать видео одновременно, с минимальной задержкой.

Ролик как стрим


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

Для того, чтобы разброса не было, нужно раздавать этот ролик всем одновременно. Это можно реализовать, если обернуть ролик в Live-stream. Покажем как это сделать с помощью связки этой библиотеки с ffmpeg.


Нам нужно реализовать схему, описанную выше. А именно, ydl подключается к YouTube и начинает скачивать ролик. FFmpeg подхватывает скачивающийся ролик, оборачивает его в RTMP поток и отправляет на сервер. Сервер раздает полученный поток как WebRTC в реальном времени.

Установка youtube-dl


Начинаем с установки youtube-dl. Процесс установки на Linux предельно простой и подробно описан в Readme под Lin и под Win.

1. Скачиваем.

curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl

2. Даем права на запуск

chmod a+rx /usr/local/bin/youtube-dl

На этом все. YouTube скачивалка готова к работе.

Возьмем ролик с YouTube и посмотрим на его мета-данные:

youtube-dl --list-formats https://www.youtube.com/watch?v=9cQT4urTlXM

Результат будет таким:

[youtube] 9cQT4urTlXM: Downloading webpage
[youtube] 9cQT4urTlXM: Downloading video info webpage
[youtube] 9cQT4urTlXM: Extracting video information
[youtube] 9cQT4urTlXM: Downloading MPD manifest
[info] Available formats for 9cQT4urTlXM:
format code  extension  resolution note
171          webm       audio only DASH audio    8k , vorbis@128k, 540.24KiB
249          webm       audio only DASH audio   10k , opus @ 50k, 797.30KiB
250          webm       audio only DASH audio   10k , opus @ 70k, 797.30KiB
251          webm       audio only DASH audio   10k , opus @160k, 797.30KiB
139          m4a        audio only DASH audio   53k , m4a_dash container, mp4a.40.5@ 48k (22050Hz), 10.36MiB
140          m4a        audio only DASH audio  137k , m4a_dash container, mp4a.40.2@128k (44100Hz), 27.56MiB
278          webm       256x144    144p   41k , webm container, vp9, 30fps, video only, 6.54MiB
242          webm       426x240    240p   70k , vp9, 30fps, video only, 13.42MiB
243          webm       640x360    360p  101k , vp9, 30fps, video only, 20.55MiB
160          mp4        256x144    DASH video  123k , avc1.4d400c, 15fps, video only, 24.83MiB
134          mp4        640x360    DASH video  138k , avc1.4d401e, 30fps, video only, 28.07MiB
244          webm       854x480    480p  149k , vp9, 30fps, video only, 30.55MiB
135          mp4        854x480    DASH video  209k , avc1.4d401f, 30fps, video only, 42.42MiB
133          mp4        426x240    DASH video  274k , avc1.4d4015, 30fps, video only, 57.63MiB
247          webm       1280x720   720p  298k , vp9, 30fps, video only, 59.25MiB
136          mp4        1280x720   DASH video  307k , avc1.4d401f, 30fps, video only, 62.58MiB
17           3gp        176x144    small , mp4v.20.3, mp4a.40.2@ 24k
36           3gp        320x180    small , mp4v.20.3, mp4a.40.2
43           webm       640x360    medium , vp8.0, vorbis@128k
18           mp4        640x360    medium , avc1.42001E, mp4a.40.2@ 96k
22           mp4        1280x720   hd720 , avc1.64001F, mp4a.40.2@192k (best)


Установка ffmpeg


Далее устанавливаем ffmpeg стандартными заклинаниями:

wget http://ffmpeg.org/releases/ffmpeg-3.3.4.tar.bz2
tar -xvjf ffmpeg-3.3.4.tar.bz2
cd  ffmpeg-3.3.4
./configure --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp
make
make install

Проверяем что получилось

ffmpeg -v

Теперь самое интересное. Библиотека youtube-dl предназначена для скачивания. Она так и называется YouTube Download. Т.е. Можно скачать youtube ролик полностью и уже после этого застримить его через ffmpeg как файл.

Но представим такой юзеркейс. Сидят в веб-конференции маркетолог, менеджер и программист. Маркетолог хочет показать всем в реальном времени ролик с YouTube, который весит, скажем 300 мегабайт. Согласитесь, возникнет некая неловкость, если нужно будет выкачать весь ролик перед тем, как начать его показ.

  1. Маркетолог говорит — «А теперь, коллеги, давайте посмотрим этот ролик с котиками, он полностью отвечает нашей стратегии выхода на рынок», и жмет кнопку «показать всем ролик».
  2. На экране появляется прелоадер: «Подождите, ролик с котиками скачивается. Это займет не более 10 минут».
  3. Менеджер идет пить кофе, а программист — читать хабр.

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

Передача данных из youtube-dl в ffmpeg


Граббер youtube-dl сохраняет поток в файловой системе. Нужно подключиться к этому потоку и организовать зачитку из файла ffmpeg-ом по мере его скачивания с помощью youtube-dl.

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

#!/usr/bin/python

import subprocess
import sys

def show_help():
    print 'Usage: '
    print './streamer.py url streamName destination'
    print './streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM streamName rtmp://192.168.88.59:1935/live'
    return

def streamer() :
    url = sys.argv[1]
    if not url :
        print 'Error: url is empty'
        return
    stream_id = sys.argv[2]
    if not stream_id:
        print 'Error: stream name is empty'
        return
    destination = sys.argv[3]
    if not destination:
        print 'Error: destination is empty'
        return

    _youtube_process = subprocess.Popen(('youtube-dl','-f','','--prefer-ffmpeg', '--no-color', '--no-cache-dir', '--no-progress','-o', '-', '-f', '22/18', url, '--reject-title', stream_id),stdout=subprocess.PIPE)
    _ffmpeg_process = subprocess.Popen(('ffmpeg','-re','-i', '-','-preset', 'ultrafast','-vcodec', 'copy', '-acodec', 'copy','-threads','1', '-f', 'flv',destination + "/" + stream_id), stdin=_youtube_process.stdout)
    return

if len(sys.argv) < 4:
    show_help()
else:
    streamer()

Этот питон-скрипт делает следующее:

  1. Создает подпроцесс _youtube_process зачитки ролика библиотекой youtube-dl
  2. Создает второй подпроцесс _ffmpeg_process, которому передаются данные из первого через pipe. Этот процесс уже создает RTMP поток и отправляет его на сервер по указанному адресу.


Тестирование скрипта


Для запуска скрипта нужно установить python. Скачать можно здесь.

Мы при тестировании использовали версию 2.6.6. Скорее всего подойдет любая версия, т.к. скрипт достаточно простой и его задача — передать из одного процесса в другой.

Запуск скрипта:
python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://192.168.88.59:1935/live

Как видите, передается три аргумента:

  1. Адрес youtube ролика.
    www.youtube.com/watch?v=9cQT4urTlXM
  2. Имя потока, с которым будет проходить RTMP-трансляция.
    stream1
  3. Адрес RTMP-сервера.
    rtmp://192.168.88.59:1935/live

Для тестирования мы будем использовать Web Call Server. Он умеет принимать RTMP потоки и раздавать их по WebRTC. Здесь можно скачать и установить WCS5 на свой VPS или локальный тестовый сервер под управлением Linux.

Схема тестирования с Web Call Server:


Ниже мы задействуем для теста один из демо-серверов:

rtmp://wcs5-eu.flashphoner.com:1935/live

Это RTMP адрес, который нужно передать скрипту streamer.py чтобы быстро протестировать трансляцию с нашим демо-сервером.

Запуск должен выглядеть так:

python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live

В консоли stdout увидим следующий вывод:

# python streamer.py https://www.youtube.com/watch?v=9cQT4urTlXM stream1 rtmp://wcs5-eu.flashphoner.com:1935/live
ffmpeg version 3.2.3 Copyright (c) 2000-2017 the FFmpeg developers
  built with gcc 4.4.7 (GCC) 20120313 (Red Hat 4.4.7-11)
  configuration: --enable-shared --disable-logging --enable-gpl --enable-pthreads --enable-libx264 --enable-librtmp --disable-yasm
  libavutil      55. 34.101 / 55. 34.101
  libavcodec     57. 64.101 / 57. 64.101
  libavformat    57. 56.101 / 57. 56.101
  libavdevice    57.  1.100 / 57.  1.100
  libavfilter     6. 65.100 /  6. 65.100
  libswscale      4.  2.100 /  4.  2.100
  libswresample   2.  3.100 /  2.  3.100
  libpostproc    54.  1.100 / 54.  1.100
]# [youtube] 9cQT4urTlXM: Downloading webpage
[youtube] 9cQT4urTlXM: Downloading video info webpage
[youtube] 9cQT4urTlXM: Extracting video information
[youtube] 9cQT4urTlXM: Downloading MPD manifest
[download] Destination: -
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pipe:':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    creation_time   : 2016-08-23T12:21:06.000000Z
  Duration: 00:29:59.99, start: 0.000000, bitrate: N/A
    Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 288 kb/s, 30 fps, 30 tbr, 90k tbn, 60 tbc (default)
    Metadata:
      creation_time   : 2016-08-23T12:21:06.000000Z
      handler_name    : ISO Media file produced by Google Inc.
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 125 kb/s (default)
    Metadata:
      creation_time   : 2016-08-23T12:21:06.000000Z
      handler_name    : ISO Media file produced by Google Inc.
Output #0, flv, to 'rtmp://192.168.88.59:1935/live/stream1':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isommp42
    encoder         : Lavf57.56.101
    Stream #0:0(und): Video: h264 (Main) ([7][0][0][0] / 0x0007), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 288 kb/s, 30 fps, 30 tbr, 1k tbn, 90k tbc (default)
    Metadata:
      creation_time   : 2016-08-23T12:21:06.000000Z
      handler_name    : ISO Media file produced by Google Inc.
    Stream #0:1(und): Audio: aac (LC) ([10][0][0][0] / 0x000A), 44100 Hz, stereo, 125 kb/s (default)
    Metadata:
      creation_time   : 2016-08-23T12:21:06.000000Z
      handler_name    : ISO Media file produced by Google Inc.
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
  Stream #0:1 -> #0:1 (copy)
frame=  383 fps= 30 q=-1.0 size=     654kB time=00:00:12.70 bitrate= 421.8kbits/s speed=   1x

Если бегло пробежать по этому логу, то можно понять, что происходит следующее:

  1. Открывается страница с видеороликом.
  2. Извлекаются данные о видео форматах.
  3. Скачивается mp4 ролик 1280x720, H.264+AAC
  4. Запускается ffmpeg, подхватывает скачиваемые данные и стримит по RTMP с битрейтом 421 kbps. Такой скудный битрейт объясняется выбранным роликом  с таймером. Нормальный видеоролик даст на порядок большее значение битрейта.

После того, как процесс стриминга запустился, пытаемся проиграть поток в WebRTC плеере. Имя потока задается в поле Stream, а адрес сервера в поле Server. Подключение к серверу происходит по протоколу Websocket (wss), а поток приходит на плеер по WebRTC (UDP).


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

Сам тест очень простой. Открываем две вкладки браузера (моделируем двух зрителей), играем этот поток с таймером по нашей схеме, и делаем несколько скриншотов, чтобы запечатлеть разницу во времени прибытия видео. Далее сравниваем миллисекунды и видим кто получил видео раньше, а кто позже и на сколько.

Получаем следующие результаты:

Test 1


Test 2


Test 3


Как видите, каждый из зрителей видит одно и то же видео, с разбросом не более 130 миллисекунд.

Таким образом задача реалтаймовой трансляции ролика с YouTube на WebRTC решена. Зрители получили поток практически одновременно. Менеджер не ушел пить кофе, программист — читать хабр, а маркетолог успешно показал всем ролик с  котиками.

Хорошего стриминга!

Ссылки


youtube-dl  — библиотека для скачивания видео с YouTube
ffmpeg — RTMP encoder
Web Call Server — сервер, умеющий раздвать RTMP поток по WebRTC
streamer.py — скрипт для интеграции youtube-dl и ffmpeg с отправкой RTMP потока

Комментарии (28)


  1. Shtucer
    18.09.2017 08:11
    +4

    curl, make install… всё как мы любим. Стандартные заклинания.
    Как минимум youtube-dl доступен в pypi (pip install youtube-dl) и в этом случае в скрипте одним popen станет меньше. Что, впрочем, не критично.
    Ваш метод установки ffmpeg (или libav) имеет смысл, если в репах пакет собран с неподходящими флагами. Но, скорее всего, он собран подходяще. Тут я могу и ошибаться давненько я это делал.


    1. johny
      18.09.2017 09:08
      +4

      Действительно, тоже пригорело.
      2017 год на дворе, make install — стандартные заклинания.
      Тогда уж опишите, чего не хватает в популярных дистрибах типа debian/rhel


    1. flashphoner Автор
      18.09.2017 15:45

      Когда начинаешь стриминг тестировать, не знаешь сразу, что там вылезет. Бывает надо ассемблерную оптимизацию снять (yasm) или флаг добавить, если что-то пошло не так. Или последнюю ревизию собрать, потому что только в ней есть такая-то фича. Потому, только заклинания, только хардкор.


      1. Shtucer
        20.09.2017 07:42

        Ну, это не повод не использовать хотя бы checkinstall. :)


  1. johny
    18.09.2017 09:08
    -2

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


    1. mayorovp
      18.09.2017 10:52
      -3

      Мифическая фишка какая-то. Я уже не помню когда в последний раз такое работало. Кажется, это было еще в флеш-версии проигрывателя.


      Попытка перемотать тяжелое видео в HTML5-версии в одном случае из трех у меня заканчивается тем, что видео замедляется в два раза (при сохранении скорости воспроизведения звуковой дорожки). Попытка сделать то же самое в проигрывателе VLC приводит к вечной загрузке видео...


  1. MakarkinPRO
    18.09.2017 10:08

    А через VideoLan такое можно сделать?


    1. Aquahawk
      18.09.2017 16:47

      да, лично делал. И даже очень широкое видео резал на куки и отдавал на пачку проекторов. Но там был не web rtc и только по локалке с одним свичом. Менее кадра расхождение было. И транслятор и ресиверы — vlc


  1. TheRaven
    18.09.2017 10:14

    Вот столько пишут про этот замечательный во всех отношениях WebRTC… а что есть опенсорсного на эту тему?


    1. Shtucer
      18.09.2017 10:24

      А что тут не опенсорсного? А что вам показывает гугл по запросу "webrtc open source"?


      1. TheRaven
        18.09.2017 10:47

        Ну, например сам сервер, который 2k баксов стоит.


        1. Shtucer
          18.09.2017 11:03

          Это какой же? Мне, например первой ссылкой показывает какую-то куренту https://www.kurento.org не оно?


          1. TheRaven
            18.09.2017 13:48

            Обозначенный в посте, разумеется. Это вам ответ на вопрос «что тут не опенсорсного».


            1. Shtucer
              18.09.2017 13:57

              А, вы про Web Call Server. А второй вопрос будем считать риторическим.


    1. ozonar
      18.09.2017 10:25
      +1

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

      Сейчас же разве что webtorrent добавился.


      1. ndiezel
        18.09.2017 12:40

        Значит ваше 'серьезно интересовало' это первые выдачи в Гугле. Для Медиа сервера есть Куренто, Ликод, Джитси или Янус. Для простых п2п разговоров есть SimpleWebRTC, с которым идёт ворох либо, которые можно использовать и в решениях повыше.


  1. phikus
    18.09.2017 12:35
    -2

    Usecase какой-то очень надуманный, либо я его не понял.
    Почему участники "веб-конференции" не могут посмотреть ролик напрямую с ютюба?


    1. mayorovp
      18.09.2017 12:46

      Потому что он у них будет воспроизводиться с разной средней скоростью


      1. phikus
        18.09.2017 12:52

        Звучит не очень убедительно.
        Можете придумать реальный пример, где бы это было реальной проблемой?


        1. mayorovp
          18.09.2017 12:57
          -1

          Я не могу придумать реального примера когда нескольким людям в разных местах нужно было бы вместе смотреть ролик на ютубе, обмениваясь комментариями :-)


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


  1. Landgraph
    18.09.2017 14:26

    Автор, а сравнивал решение с Nginx + rtmp-module?
    Лучше, хуже, также?
    Я на его базе делал чат, чтобы можно было совместно видео смотреть :))


    1. dfbios
      18.09.2017 15:31

      Не совсем понятно, как их можно сравнивать, когда RTMP играется только флэшом, который уже всё. Корректнее было бы сравнить с HLS/DASH с выкрученными в минимум target duration, например.


      1. Landgraph
        18.09.2017 16:37

        rtmp-module умеет складывать hls


  1. flashphoner Автор
    18.09.2017 15:40

    Да, RTMP сейчас используется в основном на серверной стороне (между серверами в CDN сетях).

    На браузерах флэш, можно сказать, умер. В Android Chrome его нет, в Mac Safari выключен, в Edge выключен.

    Играть Live-видео можно этим:

    • WebRTC
    • Media Source Extensions
    • HLS/DASH
    • Canvas rendering


  1. Anarions
    18.09.2017 17:13
    +1

    Дракарис — это и есть «жги», команда на которую натренировали драконов, а не имя дракона. Да, очень важная информация в контексте этой статьи.


  1. Demogor
    18.09.2017 19:33

    Пару недель назад делал похожую штуку для заказчика.
    Было лень ковырять ffmpeg, потому схитрожопил. Есть такая штука, как electron(https://electron.atom.io/) — гибрид ноды и браузера.
    В актуальных обозревателях есть такая вещь, как captureStream(https://developer.mozilla.org/ru/docs/Web/API/HTMLMediaElement/captureStream), который умеет брать стрим из audio/video тегов. Дальше относительно просто — через peer.js гоняем стрим клиентам, получаем штуку, умеющую стримить все форматы, поддерживаемые браузером.
    Дальше берем все тот же youtube-dl(или аналоги) под ноду, тащим ссылку на стрим — готово.
    Единственный минус — нужен актуальный браузер. Из плюсов — минимум кода, максимум эффективности.

    P.S. Кстати говоря, captureStream умеет еще и снимать данные с канваса, что позволяет делать штуки вроде стрима WebGL игр.


  1. Arris
    19.09.2017 04:14

    У меня только один вопрос: А что со звуком (аудиопотоком)?


  1. AterCattus
    20.09.2017 22:04

    У youtube-dl есть параметр -g, --get-url отдающий url, который отлично можно скормить ffmpeg напрямую.


    ffmpeg -i youtube-dl -g ... ...