Pull to refresh

Cтриминг видео для iPad/iPod/iPhone на Bash-е — дёшево и сердито

Reading time 6 min
Views 8.1K
Здравствуйте, уважаемые хаброжители!

В этой короткой статье я хочу поделиться опытом создания системы онлайн-вещания для устройств «одной фруктовой компании» :).




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

Сами видеофрагменты могут передаваться как по HTTP так и по HTTPS – достаточно в директорию на любом веб-сервере своевременно дописывать сами видеофрагменты и обновлять плейлист с информацией о них.

Несмотря на то, что видеофрагменты передаются по протоколу, который не поддерживает управление данными в реальном времени (как те же RTSP/RTP/RTMP), данный подход имеет несколько преимуществ — создать распределённую систему раздачи статического контента может даже школьник и (на мой взгляд — главная фича) данный подход позволяет вообще никак не танцевать с бубном для работы этих протоколов через NAT/Proxy.

В документации Apple на сайте для разработчиков есть картинка, которая в наглядной форме поясняет, как это работает (хотя сам iPad там и не нарисован):

image

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

Когда один из наших заказчиков (телевизионный канал, достаточно известный в Молдове и в Румынии — Jurnal TV) попросил нас реализовать подобную систему вещания для iPhone/iPad/iPod в сети MDX (высокоскоростная сеть внутри страны, к которой подключены все провайдеры и трафик в которой безлимитный) у нас был выбор:
  1. Использовать готовые системы (не буду называть имён производителей, так как NDA) — стоимостью от 10 000 евро и до горизонта (зависит от рюшечек, имеющихся у софта) за программно-аппаратный комплекс, состоящий из одного сервера и ПО, которое со свистелками и перделками позволяет раздавать статический контент на конечные узлы (краевые сервера, edge servers в английской терминологии) — которые, конечно же, в стоимость не входят.
  2. Самостоятельно реализовать подобную систему, тем более, что в наличии имелось несколько свободных бездисковых серверов, которые мы используем для обычного веб-вещания (при помощи VLC и тоже по HTTP, кстати — если будет интересно — расскажу) — с очень шустрыми процессорами и кучей оперативки.
  3. Так как мы не ищем лёгких путей, да и не имело смысла клиенту тратить кучу денег на новую систему, мы выбрали второй вариант.


Что у нас имелось:
  1. Неограниченный доступ к видеосигналу в любом виде, мы выбрали SDI
  2. Конвертер SDI->DV, который мы нормально видели как IEE1394, более известный в народе как «Fire Wire».
  3. Бездисковый сервер с 4х-ядерным Xeon-ом на борту под управлением Ubuntu Maverick.


Вкратце, алгоритм работы системы такой:
  1. Получить видеофрагмент длительностью 10 секунд (в соответствии с рекомендациями от Apple).
  2. Конвертировать его в нужный формат (MPEG-4 в транспортном контейнере от MPEG2)
  3. Обновить плейлист
  4. Вернуться к пункту 1


Теперь, как были реализованы эти пункты алгоритма.

Получать видеофрагменты нужной длительности мы решили с помощью утилиты dvgrab – она зарекомендовала себя с хорошей стороны при круглосуточной работе в системе видеоархива у того же телевидения. Разумеется, сохранять 10-секундные видеофрагменты приходится прямо в оперативную память, на RAM-диск. 10 секунд несжатого видео занимает 35 мегабайт. Сжатый фрагмент занимает примерно 1.2 Мегабайта при битрейте 800kbps.

Конвертировать видеофрагменты решено было при помощи ffmpeg-а — он также довольно-таки давно и прочно поселился в системе того же видеоархива телевидения благодаря своей универсальности. В качестве кодека используется свободная реализация H264 – x264.

Сама система, которая следит за поступлением новых видео фрагментов, запускает конвертирование и обновляет плейлист (при этом, видеофрагменты в плейлисте представляют собой так называемое «окно» — в самом плейлисте хранится только 3 фрагмента, на диске — 10) была написана на Bash-е.

Собственно, вот этот код:

#!/bin/bash
#set -x

VIDEO_FILES=( ); # array to store all available *.ts files at the moment
VIDEO_FILES_MAX=10; # how many elements can be stored in $VIDEO_FILES array
LIST_LEN=0; #*.ts list length

VIDEO_WINDOW=""; # array to store current video files window
VIDEO_WINDOW_LEN=3; # how many files we are storing in the window

LAST_CONVERTED=0; # ID of last converted video slice

RAW_SLICES_PATH="/tmp/DV/"; # where to look for raw video slices
MP4_SLICES_PATH="/tmp/MP4/"; # where to place converted chunks
MP4_SLICES_WEBPATH="http://istream.jurnaltv.md/live/"; # web path from the user`s POV
SLICE_DURATION=10; # seconds, 10-15 seconds recomended by Apple
M3U_FILE_NAME="/tmp/MP4/live.m3u"; # full path to the m3u index file

FFMPEG_CMD="/usr/local/bin/ffmpeg -y -i ";

update_m3u() {
# updating number of elements
LIST_LEN=${#VIDEO_FILES[@]};
echo "Number of elements in array is: $LIST_LEN ";
echo -n "(";
for slice in ${VIDEO_FILES[@]}
do
echo -n "${slice} ";
done
echo ")";
echo;
# getting last $VIDEO_WINDOW_LEN files from array
let LAST_IDX=LIST_LEN-VIDEO_WINDOW_LEN;
if [ $LAST_IDX -le 0 ]
then
LAST_IDX=0;
fi
echo "Last index we must use is $LAST_IDX";
# recreating m3u file
# getting slice id from $LAST_CONVERTED
SLICE_ID=0;
let SLICE_ID=LAST_CONVERTED-VIDEO_WINDOW_LEN;
if [ $SLICE_ID -le 0 ]
then
SLICE_ID=0;
fi
echo "------------- DUMP START ------------- ";
echo "#EXTM3U">$M3U_FILE_NAME;
echo "#EXT-X-TARGETDURATION:$SLICE_DURATION">>$M3U_FILE_NAME;
echo "#EXT-X-MEDIA-SEQUENCE:$SLICE_ID">>$M3U_FILE_NAME;
i=$LAST_IDX;
while [ $i -lt $LIST_LEN ]; do
echo "#EXTINF:${SLICE_DURATION},">>$M3U_FILE_NAME;
echo "${MP4_SLICES_WEBPATH}${VIDEO_FILES[${i}]}">>$M3U_FILE_NAME;
let i++;
done
echo "------------- DUMP END ------------- ";

# if array length is greater than $VIDEO_FILES_MAX - remove first element and compact array: array=( "${array[@]}" )
if [ $LIST_LEN -ge $VIDEO_FILES_MAX ]
then
echo "Packing array by removing first element";
echo ${MP4_SLICES_PATH}${VIDEO_FILES[0]};
rm -f ${MP4_SLICES_PATH}${VIDEO_FILES[0]};
unset VIDEO_FILES[0];
VIDEO_FILES=( "${VIDEO_FILES[@]}" );
fi
echo "-------";
}

# gracefly handle SIG_TERM
on_sigterm() {
echo "Got sigterm, exiting!";
RUN="0";
}

trap 'on_sigterm' TERM

# cleanup source and converted folders
rm -f ${RAW_SLICES_PATH}*.dv;
rm -f ${MP4_SLICES_PATH}*.dv;

# forever do
# convert video
# move to MP4
# erase original
# add converted to the tail of array
# update live.m3u file for $VIDEO_WINDOW_LEN files
# if array len>$VIDEO_FILES_MAX
# then remove first element from array and compact array it
# forever end

RUN="1";
raw_slice="";

while [ $RUN -eq "1" ]; do
#getting oldest file from the list of slices
raw_slice=`ls -tr ${RAW_SLICES_PATH}|head -1`;
if [ "$raw_slice" != "" ];
then
OPEN_FLAG=`lsof|grep $raw_slice|wc -l`;
if [ $OPEN_FLAG -eq 0 ];
then
#converting video
echo "Converting ${raw_slice}">>/tmp/istream.txt
#sleep 6; # simulating transcoding delay
mp4_slice="live-${LAST_CONVERTED}.ts";
$FFMPEG_CMD ${RAW_SLICES_PATH}${raw_slice} -acodec libfaac -ac 1 -ar 48000 -ab 96k -vcodec libx264 -vpre baseline -vpre fast -vpre ipod640 -b 800k -g 5 -async 25 -keyint_min 5 -s 512x256 -aspect 16:9 -bt 100k -maxrate 800k -bufsize 800k -deinterlace -f mpegts ${MP4_SLICES_PATH}${mp4_slice}
rm -f ${RAW_SLICES_PATH}$raw_slice
LIST_LEN=${#VIDEO_FILES[@]};
VIDEO_FILES[${LIST_LEN}]=$mp4_slice;
#generating m3u file
let LAST_CONVERTED++;
update_m3u;
else
sleep 1; # sleep one second
echo "Waiting for file to be closed!";
fi
else
sleep 1; # sleep one second
echo "Sleeping!";
fi



Код может быть несколько не оптимальным, тут есть простор для оптимизации и модификаций (например, можно сделать 2-3 потока с разными битрейтами), но этот код работает — и при таком подходе совершенно отпадает нужда в утилитке-сегментере.

К сожалению, доступен этот видеопоток только для тех, кто подключён к MDX – т.е. только для пользователей из Молдовы, но по отзывам той тысячи с хвостиком пользователей, которые пользуются этим сервисом — им нравится «носить с собой маленький телевизор».

С удовольствием отвечу на вопросы сообщества.

P.S. Спасибо нашему офис-менеджеру Татьяне за согласие попозировать с планшетом, а директору по маркетингу за то, что поработал фотографом :).

Tags:
Hubs:
+60
Comments 65
Comments Comments 65

Articles