Как работает интернет-радиовещание, часть 2

в 8:22, , рубрики: airtime, arecord, icecast, icy, IT-стандарты, liquidsoap, MMS, mp3, p2p, rtsp, shoutcast, ultravox, звук, интернет-радио, кодеки, Работа со звуком, радиовещание, Сетевые технологии, метки: , , , , , , , , , , , , , , ,

Как работает интернет радиовещание, часть 2

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

Предисловие

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

Какие пути есть?

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

IP multicast — пожалуй старейший вид «трансляций», рассылка пакетов на уровне IP протокола (не важно что внутри), клиенту как правило достаточно забиндить порт и сделать IP_ADD_MEMBERSHIP, дабы присоединиться к группе получателей, а сервер не тратит ресурсов на каждого клиента. Идеальное решение для своих сетей.

PNA (PNM) — один из первых протоколов, который использовался в RealMedia/RealVideo/RealAudio, проприетарщина. Если кто помнит, то RealNetworks были пионерами интернет-вещания. Не путать с протоколом People Near Me от небольшой конторки из Редмонда. Протокол почти нигде уже не используется, но можно нагулить немного исходников по 3eac2411-83d5-11d2-f3ea-d7c3a51aa8b0

MMS — творчество небольшой конторки из Редмонда, специально под Windows Media, проприетарщина, но есть спецификации.

RTP — Real-time Transport Protocol, определяет стандартизированный формат пакетов, внутри которых располагается медийная информация. Используется как транспорт для SIP, Jingle, RTSP.

RTSP — разработка Multiparty Multimedia Session Control Working Group (MMUSIC WG) из всем известного Internet Engineering Task Force (IETF). Как правило использует RTP — непосредственно транспорт, который несет данные.

RTMP — протокол от покойной Macromedia, проприетарщина (имеет официальные спецификации, в которых много чего не отражено), позволяет в реальном времени передавать звук/видео/картинки/различные структуры данных, сделано специально для Flash. Умеет http-туннели.

RTMFP — попытка запихать p2p во Flash, клиенты присоединяются к группе получателей и этого достаточно для начала проигрывания мультимедии.

Torrent — известный протокол для p2p-передачи данных. Обычно в торрентах используется механизм хеширования всех данных (sha1) для предотвращения искажений, однако при живой трансляции данные еще неизвестны, поэтому хеши заменяются открытым ключем источника. К примеру, используется в SwarmPlayer. Клиенты могут переключаться на http, если в торренте указаны дополнительные источники.

HTTP — протокол передачи гипертекста, над которым мы издевались в прошлой части. В свою очередь над ним поиздевались и другие, изобретя:
HSS (http smooth streaming) — творение фирмы из Редмонда для SilverLight.
HDS (http dynamic streaming) — от авторов фотошопа для Flash.
HLS (http live streaming) — творение фирмы из Купертино, для устройств с закругленными уголками. Теперь и со вкусом Андроида.
DASH (dynamic adaptive streaming over http) — творчество людей из MPEG
Все эти протоколы объединяет работа с обычным http, который можно проксировать или раздавать через CDN, который не создает проблем с фаерволами так как запросы идут через 80 порт. Во всех четырех вариантах используется нарезка потока на множество мелких файликов и генерация манифеста/плейлиста для них. От клиента сначала приходит запрос манифеста (M3U-плейлиста в случае HLS) и дальнейшее обращение к нарезанным файлам. Клиент может свободно выбирать нужный ему битрейт, в том числе во время проигрывания, избегать рассинхронизации и кешировать это все по своему желанию. Но хотя идеи очень похожи, реализация отличается достаточно сильно.

Есть и другие подвиды http-стриминга:
HPS (http progressive streaming, progressive download) — простое скачивание «бесконечного» файла и незамедлительное проигрывание, был в прошлой части. Используется к примеру на YouTube.
multipart/x-mixed-replace — такое отдают многие вебкамеры с mjpeg на борту.
Windows Media HTTP Streaming Protocol — тоже HTTP, но с кучей своих заголовков и запросов, как наследие MMS
ICY — рассматривался в прошлой части, http с дополнительными заголовками и метаданными в ответе.
Ultravox (более известен unsv:// префиксом) — продолжение ICY
Ultravox 2.1 — сделали почти честное http в плане заголовков, дальнейшая реинкарнация ICY, впервые доступны спецификации.

В исходниках айскаста есть клевый коммент:

/* Shoutcast style is used only by the Shoutcast DSP
   and is a crazy version of HTTP.
*/

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

Что вещать?

Прежде чем отправлять звук, его надо во что-то упаковать. Это может быть как новомодный opus, так и старые и известные wma, mp3, vorbis, aac (со всеми его подвидами), amr и т.д., кодировщиков звука/контейнеров великое множество. А если еще вещать и видео, то количество кодеков возрастает еще сильнее. Вот к примеру вещание в wav:

Чем вещать?

Здесь все гораздо проще, приложений для вещания потоков великое множество для каждого из перечисленных протоколов. Захват звука системы тоже не является проблемой (разве что может потеряться метадата и будет не видно названия играющей песни). Конечно, если человек привык к RadioBoss, то он захочет вещать только в нем, иногда это тоже надо учитывать.

Что же выбрать?

Хотя PNM и MMS мертвы, а компании-изготовители взялись за стандарты, вопрос остается очень сложным. Все зависит от того, где/как именно вы хотите играть получившееся радио. Если у вас своя сеть, то я настоятельно бы рекомендовал вещание с мультикастом. Однако, дабы мультикаст-пакеты дошли до места своего назначения, все роутеры на их пути должны знать как их роутить. В локальных сетях/сетях провайдеров с этим проблем нет, а вот в глобальном интернете — есть, поэтому даже если вы будете отправлять пакеты мультикастом, то не до каждого пользователя они дойдут. Многие сайты дают ссылки пользователям, дабы они открыли их своем аудио-плеере. Другие сайты выводят кнопку «плей» прямо на сайте, достаточно 1 клика, дабы послушать радиостанцию. Третьи заставляют устанаваливать какое-то стороннее ПО и только тогда можно запустить проигрывание станции. А хотите ли вы проигрывать свои потоки на мобильных устройствах? Будут ли это устройства с квадратиками, роботами или яблоками? А может быть вы хотите вести трансляцию через скайп, звоня боту? А телефонные звонки через SIP? Это можно устроить и с жаббер-ботами благодаря xmpp-jingle. У каждого варианта есть свои преимущества и недостатки.

Сайт

Конечно, всем охота сделать кратчайший путь до заветного радио, все прямо на сайте. В 2012 году реалии таковы, что наступила битва между Flash и HTML5 (про SilverLight еще кто-то помнит?), в результате чего сайтам опять нужно по 2 версии контента, как когда-то делали 2 версии сайта для IE и NN (или делали страшнейшую верстку, которую переваривали оба). В случае Flash, поток обычно в формате mp3.

Забавный факт, который упускают многие авторы Flash-приложений для проигрывания радио. Самый простой плеер реализуется в 1 строчку:

new Sound().load(new URLRequest("http://example.tld/stream.mp3"));

Фактически это HTTP Progressive Streaming, но сам Flash не знает об этом, он считает что это такой большой mp3-файл, который надо начать незамедлительно играть (если быть точным, то флеш хочет указание размера файла, в Айскасте много костылей специально для флеша). Как следствие, через 2-12 часов проигрывания оказывается, что браузер (точнее браузерный Flash-плагин) отожрал более гигабайта оперативы. Баг флеша? Плохой плеер? Нет, все так и задумано: вы загружаете большой звуковой объект, с которым можно интерактивно работать, вот оно в памяти и висит, ждет дальнейших действий. Все корректно. Вам нравится, когда браузер на вашем сайте начинает жрать оперативку и сильно свопить? Однако этого можно избежать, если использовать NetStream, который изначально и предназначен для потоковых трансляций. NetStream же в свою очередь изначально предназначен для протокола RTMP, хотя и готов проиграть локальные FLV-файлы, чем и можно воспользоваться, упаковав звук в FLV, в том числе упаковывать можно и MP3. Последний писк моды — это протокол RTMFP и p2p-вещание, которого я пока не стречал на радиосайтах. Замечу, что к популярному ICY (shoutcast-потоки) эти способы проигрывания не имеют никакого отношения, поэтому метаданных (названий песенок) не будет, зато метаданные могут быть внедрены в контейнер FLV, откуда их сможет получать Flash.

Сам Flash поддерживает mp3, aac или speex (хотя наверное есть люди, которые предпочтут NellyMoser или даже adpcm), причем поддерживаются далеко не все их проявления, к примеру mp3 с частотой 48000 герц не поддерживаются, зато поддерживается 44100. Как уже было сказано выше, все это желательно обернуть в FLV-контейнер. Однако с 10 версии можно манипулировать семплами и писать свои собственные декодеры звука, поэтому Flash может проигрывать к примеру OGG-Vorbis потоки.

В случае HTML5 все гораздо интереснее, битва кодеков в самом разгаре, о ней пишут практически на каждом сайте хоть как-то связанном с IT. На сегодняшний день побеждает Vorbis (контейнеры ogg и webm), так как поддерживается в 3 из 5 популярных браузерах. Но это не проблема, в теге AUDIO можно указать несколько источников звука, что позволяет указать не только OGG, но и MP3 как альтернативный поток, который поддерживается в остальных двух браузерах. Активно идет разработка спецификаций для работы с семплами, поэтому уже появляются первые декодеры звука на JS, но на данный момент это скорее красивые демки, нежели что-то пригодное для практического применения. Не стоит забывать и о HTML5 на мобильных устройствах, он там порой в значительной степени отличается от браузерного. Например, на устройствах с надкушенным яблоком используется HLS и в теге AUDIO/VIDEO надо указывать m3u(8) файл. И хотя стандарта на HLS нет, он поддерживается и на Андроид-устройствах тоже.

Есть и браузерные плагины для других типов стримов, как встроенные в некоторые системы, так и поставляемые отдельно. Все помнят времена, когда WMP внедрялся прямо в страницу?

Особые условия

Иногда требуется создать радио, учитывая некоторые особенности. К примеру, при создании одного детского радио была поставлена цель, дабы дети могли слушать радио через дешевые телефоны. Целевая аудитория — дети в сельской местности, живущие далеко от крупных городов. Это не айфоны и андроиды, а телефоны в категории «до $50», которые родители покупают своим детям, дабы всегда быть на связи со своим ребенком. Телефоны есть практически у всех и почти каждый телефон позволяет запускать java-приложения! Казалось бы, тут все просто, но всплыла неприятная подробность — некоторые подобные телефоны не умеют даже mp3, т.е. пищать то они могут, а вот проиграть радио — нет. Бросать идею? Заставлять родителей покупать новые телефоны? Нет! Вспоминаем, что все телефоны умеют wav (pcm) и делаем трансляцию радио в wav! Конечно, для этого надо немного поправить серверный софт, зато потом можно раздать детям jar-приложения для проигрывания радио. Ниже выкладываю код такого плеера (взял готовый пример и немного дописал)

import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.List;
import javax.microedition.media.Manager;
import javax.microedition.media.Player;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
import java.io.*;

public class AudioMIDlet extends MIDlet implements CommandListener, Runnable {
        private Display mDisplay;
        private Form mMainScreen;
        public void startApp() {
                mDisplay = Display.getDisplay(this);
                if (mMainScreen == null) {
                        mMainScreen = new Form("Бомжефон");
                        mMainScreen.addCommand(new Command("Exit", Command.EXIT, 0));
                        mMainScreen.addCommand(new Command("Play", Command.SCREEN, 0));
                        mMainScreen.setCommandListener(this);
                        try {
                                mMainScreen.append(Image.createImage("/icon.png"));
                        } catch (Exception e) {
                                mMainScreen.append(e.toString());
                        }
                }
                mDisplay.setCurrent(mMainScreen);
        }

        public void pauseApp() {
        }

        public void destroyApp(boolean unconditional) {
        }

        public void commandAction(Command c, Displayable s) {
                if (c.getCommandType() == Command.EXIT)
                        notifyDestroyed();
                else {
                        Form waitForm = new Form("Loading...");
                        mDisplay.setCurrent(waitForm);
                        Thread t = new Thread(this);
                        t.start();
                }
        }

        public void run() {
                playFromResource();
        }

        private void playFromResource() {
                try {
                        InputStream templ = getClass().getResourceAsStream("/header");
                        HttpConnection hc = (HttpConnection)Connector.open("http://example.com:8000/wav");
                        hc.setRequestProperty("User-Agent","Bojm play");
                        mDisplay.setCurrent(mMainScreen);

                        int q = hc.getResponseCode();

                        mMainScreen.append("status code: "+q+"n");
                        InputStream source=hc.openInputStream();

                        byte[] buff=new byte[65800];
                        templ.read(buff);
                        int i =0;

                        ByteArrayInputStream main=null;
                        Player player1=null;
                        Player player2=null;
                        Player[] players=new Player[2];
                        int p=0;

                        int count=0;
                        while (true) {
                                long st=System.currentTimeMillis();
                                if (players[p]!=null) {
                                        players[p].start();
                                }
                                Thread.sleep(500);
                                p=1-p;
                                source.read(buff,56,65536);
                                main=new ByteArrayInputStream(buff);
                                if (players[p]!=null) {
                                        players[p].deallocate();
                                        players[p].close();
                                }
                                players[p]=Manager.createPlayer((InputStream)main, "audio/x-wav");
                                // player.setLoopCount(10);
                                players[p].prefetch();
                                count++;
                                long sp=System.currentTimeMillis()-st;
                                int pause=8000-(int)sp;
                                if (pause>100) {
                                        Thread.sleep(pause);
                                }
                                if (i==1) {
                                        break;
                                }
                        }
                } catch (Exception e) {
                        showException(e);
                        return;
                }
                mDisplay.setCurrent(mMainScreen);
        }

        private void showException(Exception e) {
                Alert a = new Alert("Exception", e.toString(), null, null);
                a.setTimeout(Alert.FOREVER);
                mDisplay.setCurrent(a, mMainScreen);
        }
}

Говорят, что не на всех телефонах оно хорошо работало, быть может кто-то может улучшить? Если что, то вы сами можете попробовать, вот вам ссылочки на pcm-вещание:

radio.kanaria.ru:8000/ulaw.wav - контейнер wav
radio.kanaria.ru:8000/ulaw.wav?type=.avi - контейнер avi
radio.kanaria.ru:8000/ulaw.wav?type=.au - контейнер au
radio.kanaria.ru:8000/ulaw.wav?type=.aiff - контейнер aiff

К практике!

image

Не, отрезать у aac-файлов ADTS заголовки и приклеивать к ним замену из RTP + LATM-заголовков мы не будем. Вместо этого я предлагаю поставить Helix Universal Server и попробовать все что есть, триал доступен здеся: www.realnetworks.com/forms/helix-server-evaluation.aspx — нужно заполнить форму, а на почту придет ссылка с демо-лицензией, которая нужна будет при установке.

В комплект поставки входят сразу несколько примеров, поэтому сразу после установки мы можем начать играть с разными потоками! Попробуйте открыть у себя в браузере следующие ссылки, только вместо example.com укажите свой сервер:

http://example.com/asxgen/Desertrace.wmv
http://example.com/ramgen/realvideo10.rm


http://example.com/sdpgen/3gp/Venice-h264.3gp
http://example.com/flashgen/flash/Desertrace.f4v

Что произошло? Что вы увидели? Запускались ли у вас локально-установленные плееры?
Можно попробовать открыть и сами ссылки

mms://example.com:1755/Desertrace.wmv
rtmp://example.com/flash/Desertrace.f4v
rtsp://example.com/iPhone-src/Riders.mp4
rtsp://example.com/3gp/Venice-h264.3gp
rtsp://example.com:554/realvideo10.rm
curl http://example.com/asxgen/Desertrace.wmv

<ASX Version = "3.0">
   <ENTRY>
      <REF HREF = "mms://example.com:1755/Desertrace.wmv"/>
      <REF HREF = "http://example.com:80/Desertrace.wmv"/>
   </ENTRY>
</ASX>

curl http://example.com/ramgen/realvideo10.rm

rtsp://example.com:554/realvideo10.rm

curl http://example.com/flashgen/flash/Desertrace.f4v

<smil>
<head>
<meta base="rtmp://example.com:1935/flash/"/>
</head>
<body>
<switch>
<video src="mp4:Desertrace.f4v?ir=785424" system-bitrate="785424"/>
</switch>
</body>
</smil>

curl -L http://example.com/m3ugen/iPhone-src/Riders.mp4

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-KEY:METHOD=NONE
#EXTINF:10,
/iPhone/iPhone-src/Riders.mp4-20100309-232625/1-88634.ts
#EXTINF:10,
/iPhone/iPhone-src/Riders.mp4-20100309-232625/2-85748.ts
#EXTINF:6,
/iPhone/iPhone-src/Riders.mp4-20100309-232625/3-91111-END.ts
#EXT-X-ENDLIST

curl http://example.com/sdpgen/3gp/Venice-h264.3gp

v=0
o=- 1268113313 1268113313 IN IP4 example.com
s=
i=88740793 supplied by Jorg Greuel/Getty Images 2010
c=IN IP4 0.0.0.0
t=0 0
a=SdpplinVersion:1610641560
a=StreamCount:integer;2
a=control:rtsp://example.com:554/3gp/Venice-h264.3gp
a=Title_@@_TextEncodingType:integer;5
a=Author_@@_TextEncodingType:integer;5
a=Copyright_@@_TextEncodingType:integer;5
a=CreationTime:integer;-2082844800
a=ModificationTime:integer;-2082844800
a=Title:buffer;"VmVuaWNlAAA="
a=Author:buffer;"ODg3NDA3OTMgc3VwcGxpZWQgYnkgSm9yZyBHcmV1ZWwvR2V0dHkgSW1hZ2VzAAA="
a=Copyright:buffer;"MjAxMAAA"
a=Title:string;" "
a=range:npt=0-55.595000
m=video 0 RTP/AVP 96
b=AS:261
b=TIAS:215874
b=RR:4317
b=RS:2158
a=maxprate:85.937102
a=control:rtsp://example.com:554/3gp/Venice-h264.3gp/streamid=65538
a=range:npt=0-55.488000
a=length:npt=55.488000
a=rtpmap:96 H264/90000
a=fmtp:96 profile-level-id=42800d; packetization-mode=1; sprop-parameter-sets=Z0KADZZTAoO/PgKhAAADA+kAALuAnaFCpoA=,aMljUg==
a=mimetype:string;"video/H264"
a=ASMRuleBook:string;"Marker=0,AverageBandwidth=215874,TimeStampDelivery=TRUE,InterDepend=1;Marker=1,AverageBandwidth=0,TimeStampDelivery=TRUE,InterDepend=0;"
a=3GPP-Adaptation-Support:1
a=Helix-Adaptation-Support:1
a=AvgBitRate:integer;215874
a=TIASAvgBitRate:integer;215874
a=AvgPacketSize:integer;314
a=Width:integer;320
a=Height:integer;212
a=HasOutOfOrderTS:integer;1
a=Language:string;"und"
a=maxprate:106
a=framesize:96 320-212
m=audio 0 RTP/AVP 97
b=AS:44
b=TIAS:31871
b=RR:637
b=RS:318
a=maxprate:23.573225
a=control:rtsp://example.com:554/3gp/Venice-h264.3gp/streamid=65537
a=range:npt=0-55.595000
a=length:npt=55.595000
a=rtpmap:97 MP4A-LATM/24000
a=fmtp:97 object=2; cpresent=0; profile-level-id=15; config=400026203FC0
a=mimetype:string;"audio/MP4A-LATM"
a=ASMRuleBook:string;"Marker=0,AverageBandwidth=31871,TimeStampDelivery=TRUE,InterDepend=1;Marker=1,AverageBandwidth=0,TimeStampDelivery=TRUE,InterDepend=0;"
a=3GPP-Adaptation-Support:1
a=Helix-Adaptation-Support:1
a=AvgBitRate:integer;31871
a=TIASAvgBitRate:integer;31871
a=AvgPacketSize:integer;169
a=HasOutOfOrderTS:integer;1
a=Language:string;"und"
a=maxprate:24

Еще рекомендую посмотреть вот этот вот ролик, на сей раз про продукт от авторов фотошопа:

www.adobe.com/devnet/flashmediaserver/articles/single-live-video-stream-http-flash-ios.html

Ну и небольшая подборка радиостанций:
www.radiofeeds.co.uk/commstudent.asp

Я ничего не понимаю, хочу как у соседа!

Я хочу купить машину, но какую? Столько разных марок, столько обзоров, столько условностей! Вот лучше выберу как у соседа, он наверное мужик с головой!

Как уже говорилось в прошлой части, самым популярным вещанием интернет-радио является ICY, поэтому радио вашего соседа скорее всего работает на нем. Для него в свою очередь есть 2 самых популярных сервера — это оригинальный Shoutcast с закрытым кодом и открытый Icecast. Приводить детальное сравнение фич и результаты тестирования я не собираюсь, сравнений и тестов в интернете полно — ищите и обрящете. От себя могу сказать так: если вам срочно приспичило создать свое радио и вам нужно готовое и максимально цельное решение — ваш выбор Shoutcast однозначно. Красивый интерфейс, 100% совместимость с кучей вещалок, отсутствие детских болезней, куча специалистов и хостингов. Shoutcast2 представляет из себя наиболее цельное решение с кучей готовых и законченных фич. У его противника Icecast же есть только одна, зато очень заметная фича — это открытые исходники. Поэтому если же вас не пугает черненький веб-интерфейс с некрасивой картинкой, документация на английском языке и вы любите покомпилировать — Icecast ваш выбор. Собственно, если вы выбрали Shoutcast, дальше можете не читать, ведь что происходит в кишках закрытого проприетарного продукта никто не знает, т.е. изучать нечего.

Icecast — это просто!

Как работает интернет радиовещание, часть 2

Несколько слов о версиях Icecast. Когда-то давно было принято говорить: не пользуйтесь релизным айскастом, он течет. И это было верным, у той версии было много проблем и детских болезней. Тогда рекомендовалось взять SVN-версию и собрать самому, в результате и фичи появлялись новые, и болезненные утечки проходили. С тех пор много воды утекло, а релизные версии по прежнему ставить никто не хочет, следуя древним инструкциях, найденным на просторах интернетов. На всякий случай смотрите на дату таких сообщений и сравните ее с текущей, подумайте о актуальности сказанного.

Еще существует много форков, самый известный это пожалуй Icecast-kh от Karl Heyes. Отличается он в первую очередь поддержкой MP3. Многие наверное спросят: «Как же так? Ведь и с оригинальным Icecast можно транслировать музончик в mp3!». Да, совершенно верно, в манах к убунте даже написали, что Айскаст — это сервер для вещания mp3. Но самим авторам почему-то от mp3 рвет задницу и они пишут: «использование mp3 — это всегда проблемы» и крайне болезненно реагируют на все вопросы связанные с ним. Кто хочет проверить — добро пожаловать на официальный канал Icecast в IRC, там вам популярно пояснят за эту грязную проприетарщину. Да и поддержка mp3 в оригинальном айскасте остается на уровне рассылки байт, т.е. что приняли на одном конце — отдали на другой конец, никакого анализа не производится. Так вот, в Icecast-kh это не так, там работа с mpeg сделана как с одним из основных форматов, осуществляется полный разбор стрима, добавлены корректные синкпоинты, даже упаковка в FLV. Для пользователя это мало что значит, разве что Flash-плееры не будут заикаться, получив некорректный кусок данных (баг в старых версиях флеш-плеера), да и плееры можно строить на основе NetStream, что позволяет браузеру не пожирать гигабайты оперативки. Еще в нем разные приятные бонусы, такие как блеклисты по IP и юзер-агенту (правда юзер-агенты надо указывать целиком, поддержку масок пришлось делать самим), поддержка Flash Policy-запросов (надо сказать, нигде не описано как ими пользоваться, но у нас есть исходники!), авторизация по ссылкам, статистика по трафику (на весь сервер и по отдельным маунтам), контроль битрейка сорс-потоков и многое другое.

В свою очередь мы тоже немного попатчили айскаст, допилив его до нужного нам состояния. К примеру, я долго всех просил вещать в приемлемом качестве, но слова человека — это ничто, поэтому добавил контроль битрейта, сразу все стало на свои места. Некоторые на днях додумались вещать в mono, поэтому теперь надо будет еще контролировать и количество каналов. Ну и разные мелочи, вроде вещания в wav/aiff/au/avi, добавлены протоколы RTSP, XMPP (позвонить ему пока нельзя, пока оно только публикует статус сервера в групчате). Уже писал habrahabr.ru/post/151222/ про юзербарки. Вот примерно так выглядит админка правленного айскаста:
image

Здесь есть 3 новых поля, которые используются для сайта, причем по полю Country можно еще банить ботнеты во времена атак, так как запросов из африки бывает много, а слушателей оттуда — мало. Можно развить применение и других полей, к примеру, можно добавить еще различные заставки, которые будут зависеть от user-agent, а пользователь той или иной операционной системы услышит заготовленное послание специально для него. Есть несколько песен, просто идеально подходящих, словно они специально сделаны для этого.

Сюда можно было бы конечно добавить и ts-контейнер для айфонов, но с практической точки зрения это ничего не дает, кроме дополнительных затрат на сервере и возрастающего трафика, так как HLS не является стандартом, а следовательно не нужен. В честь недавнего треда habrahabr.ru/post/151538/ хотел было добавить поддержку трансляции анимированных GIFов, но руки так и не дошли. Может быть кому-то нужна поддержка WMA/WMV? Можно было бы стримить звук/видео в этом проприетарном формате. Хотя кому это старье может быть нужно?

Вообще, ковыряясь в кишках айскаста можно найти много интересного, к примеру есть вот такая ссылочка:
example.com/admin/streams — отобразит все существующие стримы
Даже нашел слабое место в безопасности айскаста, которое тоже поправил.

Существует и множество других патчей для айскаста, к примеру для поддержки мультикаста:
lists.xiph.org/pipermail/icecast-dev/2003-November/000707.html

Открытый исходный код создает возможность надстраивать и дорабатывать систему практически безгранично, все в ваших руках. Вы не только можете «жрать что дают», как в случае с Shoutcast, но и получить именно то, что нужно именно вам.

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

Предисловие после предисловия. Подготовка к работе

Для начала сегодняшних экспериментов нам нужен установленный и настроенный айскаст (любой версии, но лучше из свежих, так как я надеюсь на них). Как настроить айскаст можно почитать в официальной документации. Ну или вот тута: habrahabr.ru/post/97724/ (дабы далеко не ходить)

Еще нам будет нужна вещалка, но только для тестов можно скачать и установить idjc или butt, последняя есть для win/linux/mac. Ну или можно купить триалочку Sam Broadcaster, для тестов оно сойдет, а пользоваться постоянно им все равно не выйдет.

Проверка настройки. Не убирая далеко документацию, создайте маунт /radio, задайте какой-то пароль и введите в программе-вещалке реквизиты своего сервера, маунт /radio, его пароль, а в качестве кодека можно выбрать что вам по душе. После чего откройте в вашем любимом плеере example.com:8000/radio — вы должны будете услышать свой голос. Если так, то поздравляю, у вас есть свое собственно радио! Но создать радио — это еще не все, надо бы еще понять как именно оно работает.

Как это работает?

В прошлой части мы захватывали звук с локального устройства, кодировали его в mp3 и сохраняли в файл, а наш скрипт на Perl изображал из себя сервер вещания и раздавал слушателям. Теперь в качестве сервера вещания у нас Icecast, следовательно необходимость в нашем скрипте отпадает. Далее, Icecast не умеет вещать из локальных файлов (вообще, тут много оговорок, поэтому будем считать что не умеет), а следовательно нам надо как-то отдать ему наш mp3. Для вещания на Icecast/Shoutcast нам нужна программа-вещалка (source-client в терминологии Icecast), которая и будет пересылать ему стрим. Этим и займемся.

Программы-вещалки используют http-протокол, местами кривой или извращенный. Я напомню комментарий из исходников Icecast:

/* Shoutcast style is used only by the Shoutcast DSP
   and is a crazy version of HTTP.
*/

Но так как мы выбрали открытый Icecast, то заниматься черной магией нам почти не придеться. Сначала давайте пойдем простым путем и используем библиотеку libshout, которая лежит рядом с айскастом и может использоваться для написания вещалок (а так как Icecast во многом копирует Shoutcast, то вещалки могут быть совместимы с обоими серверами). Библиотека простая, в комплекте есть пример использования, вот совсем упрощенный пример (убрал проверки, дабы было совсем просто), который берет данные из stdin, отправляя их на Icecast:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <shout/shout.h>

int main(int argc, char **argv)
{
        unsigned char buff[4096];
        long read, ret, total;
        shout_init();
        shout_t *shout=shout_new();
        shout_set_host(shout, "example.com");
        shout_set_port(shout, 8000);
        shout_set_password(shout, "mypassword");
        shout_set_mount(shout, "/myTestMount");
        shout_set_user(shout, "source");
        shout_set_format(shout, SHOUT_FORMAT_MP3);
        shout_set_protocol(shout, SHOUT_PROTOCOL_HTTP);
        shout_open(shout);
        while(1){
                read = fread(buff, 1, sizeof(buff), stdin);
                if(read>0){
                        ret = shout_send(shout, buff, read);
                        shout_sync(shout);
                } else {
                        break;
                }
        }
        shout_close(shout);
        shout_shutdown();
        return 0;
}

Собираем это линкуясь с libshout (указав -lshout) и запускаем как:

cat sound.mp3 | ./streamerExample

После чего можно открыть в плеере

http://example:8000/myTestMount

И мы услышим нашу mp3!

Что же здесь происходит? Сначала мы создаем и заполняем структуру shout_t реквизитами нашего сервера (вы ведь помните, что во всех хороших программах опции задаются на этапе компиляции?), в конце идет указание, что вещать мы будем SHOUT_FORMAT_MP3, используя протокол SHOUT_PROTOCOL_HTTP. Если мы хотим вещать на Shoutcast, то протокол можно поменять на SHOUT_PROTOCOL_ICY. Далее все просто: прочитали кусочек данных из файла, скормили библиотеке через shout_send() и вызвали shout_sync(), вот собственно и вся вещалка. Первая функция отсылает данные на сервер, а вторая — делает задержку, дабы не отсылать файл на большей скорости. Дело в том, что если отсылать файл на большой скорости, то айскаст будет считать это высокобитрейтным стримом и будет вещать его, однако его клиенты, различные плееры, будут играть звук в реальном времени, не вычитывая весь объем данных сразу, в результате чего получается, что айскаст уже начал получать 5-ю минуту потока, а плееры еще играют первую. Последствия печальны: Icecast считает, что у клиента медленное соединение и отключает его, дабы не тратить память на него.

Если вы боитесь сборки софта как огня, не знаете как собирать софт или являетесь альтернативно-платформенным, то для вас существует множество простых стримилок, к примеру ezstream и ему подобные утильки. Ezstream замечателен тем, что ничего не делает, кроме непосредственной отсылки потока на сервер, сохраняя при этом один из основных принципов: «каждая программа должна делать одно действие, но делать его хорошо».

Что такое myTestMount?

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

http://example.com/точка_монтирования

Т.е. если вы прописали точку монтирования /myCoolRadio, то отправлять звук надо на маунт /myCoolRadio, а слушать по адресу example.com/myCoolRadio! Все очень просто! Многие плееры хотят не просто ссылку на звук, а m3u-файл, для таких плееров рекомендуется отдавать ссылку вида example.com/myCoolRadio.m3u — айскаст сгенерирует плейлист из 1 ссылки в нем.

К практике!

Вспомним наш прошлый пример, где мы хватали звук, но немного модифицируем его:

arecord -fcd -r44100 -c2 | viewSignal | sox .... | other_processing | ... | viewSignal | lame -x -r -s 44.1 --bitwidth 16 -m j - - | ./streamerExample

Что произошло? Мы отрезали редирект в файл и через пайп скормили данные нашей вещалке! Т.е. мы захватили звук, отправили его по цепочке приложений (просмотр сигнала, звуковой процессор, снова посмотрели сигнал, закодировали в mp3) и в финале он добрался до конечного звена нашей цепочки, где его отправили на сервер!

Обратите внимание, что вещалка берет не файл, не звук с микрофона, а чистый mp3-поток! Это позволяет делать вот такое:

curl http://other-cool-radio.ru/other-stream | ./streamerExample

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

Как вы уже поняли, для вещания нам нужен только mp3-поток, достаточно засунуть его в наш streamerExample и будет радость. Но возникает вопрос, а почему мы всегда пользовались стандартными утилитами, а для такой тривиальной вещи нам вдруг понадобились какие-то библиотеки/сторонние приложения? Нельзя ли отказаться от streamerExample? Конечно можно!

(cat <<aaa; curl http://other-cool-radio.ru/other-stream ) | netcat example.com 8000
SOURCE /muCoolRadio HTTP/1.0
Authorization: Basic 83764598726345876
Content-Type: audio/mpeg

aaa

При желании можно записать в 1 строчку и будет красивый рестример из 1 строчки на баше, все только встроенными средствами. Недостатков у такого метода 2:
1. надо посчитать base64 для авторизации (обычная http-авторизация)
2. тут нету ограничения потока, но если у нас «живой» стрим, т.е. с микрофона или другой радиостанции, то ограничивать его нет смысла, как в случае с mp3.

Да, как вы уже догадались, достаточно послать серверу заголовки + сам mp3 и будет вещание! Все крайне просто!

Про кроссплатформенность

Используя команду arecord мы захватывали чистый звук (со звуковой карты, звуки системы), получали чистый wav, нормализовывали его, кодировали через lame и получали простое mp3. К сожалению, даже используя примитивные инструменты, меня обвинили в «linux-only» подходе. Расстроило меня это потому, что я пытался изложить суть происходящего, а не конкретные инструменты, которые на каждой платформе свои и присутствуют в изобилии. Поэтому вы можете использовать ffmpeg (ffmpeg.exe), это хоть и комбайн, но работает практически везде, даже на платформах с вирусами за смс:

ffmpeg.exe -f dshow -i _параметры_устройства_ -acodec libmp3lame -f mp3 - | streamerExample

Fallback, «нон-стоп» и пятиминутка ненависти

Если мы добрались до этого места, то все настроено и всё всем вещается! Но стоит только выключить вещалку, так сразу… Данные на сервер перестают приходить, серверу нечего отдавать и он честно говорит всем слушателям: «все, конец шоу, расходимся», т.е. закрывает соединения у всех пользователей. А что ему еще оставалось делать? Данных то для отправки клиентам больше нету!

Существует одно и очевидное решение этой проблемы: круглосуточное вещание, при котором никто и никогда не завершает вещалку. Но живем мы в реальном мире, где сервера падают, крупные облака подвисают, каналы проседают, софт крешится, а пьяные электрики выключают свет. И хорошо, если здесь не замешаны уборщицы, которым некуда подключить свой пылесос! Вот и выходит, что даже круглосуточное вещание чем-то похоже на диджея-одиночку, который повещав пару часиков уходит в тёплую постельку — разрывы неизбежны. Но есть решение — это резервный стрим!

Резерв в разных руководствах называется по разному: fallback, redundant (server, encoder), backup stream, в рунете же он известен под именем «нонстоп», благодаря какой-то кривой статье про айскаст. Словом «нонстоп» засраны все русскоязычные ресурсы, где хоть как-то упоминается айскаст. За пределами рунета — нет. Нон-стоп чего? Кого? Кто вообще придумал этим словом обозначать резервный стрим? Но я с завидной регулярностью нахожу людей, владеющих данной «терминологией», подразумевающих что так оно и должно быть. Загляните в документацию — ГДЕ ОНО? Но меня заставляют общаться на данном новоязе! Мне уже охота расстреливать каждого такого нонстопщика, особенно когда меня спрашивают «а вы можете настроить нон-стоп?». Так и охота ответить: «да, буду настраивать ваш сервер, попутно исполняя частушки».

В случае айскаста, обычно «нонстопщики» следуют по своей кривой методике и ставят Ices0, заводят его на маунт /non-stop и там постоянно играет поток с музыкой, который затем прописывают в fall-back основному маунту. Если вы увидите вебморду айскаста с таким маунтом — вы знаете кто это настраивал. Чем плохо такое решение:
1. опасно использовать низкие битрейты, так как при подключении сервер выдает «буст» в размере 64кб (настраиваемо, но 64кб — это обычно размер буфера кеширования для большинства плееров), а 64кб в 32 килобита и в 256 килобит — это разное количество секунд. Задержка между включением диджея и его появлением в эфире может составлять длительное время.
2. Ices0 занимается постоянным кодированием потока, нагружая сервер, хотя необходимости в постоянном транскоде нету.
3. Сами авторы Icecast не рекомендуют кодировать поток на сервере из-за часов на дешевых VPS. Я сам наблюдал, как часы на VPS убегали и с радио происходили ужасы.

А теперь скажите, почему бы не сделать:
while true; do cat sorry-my-radio-crashed.mp3;done | ./streamerExample
Данная конструкция будет в цикле крутить запись вида «извините, мое радио отвалилось, приносим свои извинения», практически не тратить на это ресурсов сервера

Обратите внимание, живое вещание и fallback должны быть не только в одном формате, но и параметры кодирования должны совпадать. Как мы уже узнали из прошлой части, над mp3 можно безболезненно издеваться в довольно широких пределах, но многие плееры не расчитывают, что в середине проигрывания звук может превратиться из stereo в mono (и обратно), поэтому в колонках будет нечто ускоренное/замедленное в 2 раза. А какие эффекты возникают при изменении семплрейта! А это можно часто встретить, когда настраивается резерв между выходами диджея (основное назначение «нонстопов»)

А можно с телефона слушать?

Конечно можно! Но как правило, в этой стране телефонный трафик еще стоит денег и прослушивание радиостанции может хорошо разорить слушателя. Поэтому битрейт вещания из 256 килобит можно понизить до 64, качество конечно резко упадет, зато такое будет и дешевле, и стабильность мобильного канала позволит слушать радио без разрывов. В принципе, если вещать в AAC+, то можно вообще сжать до 12 килобит, вот пример такой записи: dump.bitcheese.net/files/iborady/live-hinted.aac (200кб на почти 2 минуты, вполне слушабельно)

Если говорить о моде, то можно вещать в модном webm, к примеру:

radio.kanaria.ru:8000/radio.webm

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

Вот собственно и все.

«Как все? А как же автоматизация?» — спросит пытливый читатель? Ах, ладно, сейчас еще подумаем. По идее, мы можем взять RadioBoss (опционально поставить его на сервер), залить в него музыки, войти в генератор-плейлистов и сделать радио по своему вкусу!

Но готовые решения не для нас, мы изобретем свой велосипед! Что же нам предлагают обычно? Далеко ходить не будем, можно в качестве отправной точки взять habrahabr.ru/post/131116/ — здесь автор сделал ротацию на основе cron, разложил музыку по директориям и обмазался плейлистами. С джинглами тут конечно косяк, но можно же допилить! В некоторых решениях есть опция «взвешенного» плейлиста, к примеру в Darwin Streaming Server можно не только составлять плейлисты, но и задавать вес того или иного трека, что повлияет на выбор рандома. Но мы его не используем, у нас Icecast. Есть замечательное решение от авторов LiquidSoap:

Как работает интернет радиовещание, часть 2

radio = random(weights = [1, 4],[jingles, radio])

Есть еще одно решение для автоматизации:
Как работает интернет радиовещание, часть 2

В основе всех этих решений лежат плейлисты. Кто-то генерирует плейлисты просто с помощью find + sort, кто-то с учетом bpm и жанра, кто-то вводит правил ротации на 3 страницы, с учетом утра/вечера/зимы/лета/рабочих/выходных/праздников. Сложно сказать, что будет приятно услышать конечному слушателю. А если у нас коммерческое вещание, то нам надо очень тонко запрограммировать эфир, дабы насрать рекламой в уши слушателю, пока он не убежал. Так как же подобрать треки? Единого мнения быть не может. Можно к примеру стырить логи вещания других радиостанций, распарсить и построить что-то вроде цепей Маркова. Решение плохо тем, что база музыкальных треков должна быть близка к тем радиостанциям, с которых эти самые цепи Маркова и генерировались, тем самым можно получить клон радиостанции, который никто слушать не будет. Можно зайти с другой стороны, подумать что будет хуже всего и составить набор правил вида:
1. один трек повторяется несколько раз
2. один автор повторяется несколько раз
3. эмбиент сменяется металлом
4. транс сменяется сопливой попсой
5. один трек играет в одинаковое время
6. один трек играет в один день недели
7. всегда один трек сменяет другой трек
Правил может быть очень много, ведь всегда проще сказать что будет хуже, чем то что будет лучше. И уже на основе такого списка составлять правила ротации, вставляя «НЕ» в каждый пункт, дабы не допустить подобных ситуаций. Качество конечно вырастет, но я вот люблю порой послушать 5 каверов на 1 песню подряд, люблю послушать Киркорова вместе с песнями которые он стырил, люблю симиляры из ластика и поэтому перекосы в сторону того или иного коллектива случаются регулярно. Что же делать? Опять, единого решения быть не может, поэтому рекомендую зайти на google://музыкальное+программирование+радио
К примеру: www.soundclip.ru/articles/musicprogramming/musicprogramming.doc

У данного подхода есть большой недостаток, если надо отойти от созданного плейлиста, то у нас начинаются проблемы. К примеру, надо вставить трек по заказу — надо найти ближайшее место, где он будет лучше всего согласован с треками, фактически надо пересчитать весь плейлист. Можно конечно отделаться джинглами-отбивками, но что будет если реквесты посыпались и их очень много? В коммерческой радиостанции таких проблем нет, а вот в интернет-станциях, созданных для людей, такая проблема возникает достаточно часто. Так как же вставить свой трек в законченный плейлист? А если надо добавить голосовой трек поверх музыкального? А если сделать небольшую скайп-конференцию?

К примеру, если нужно вывести в эфир скайп-конференцию, то достаточно поднять такую конструкцию:
Как работает интернет радиовещание, часть 2

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

К примеру, мы сделали небольшое флеш-приложение, которое позволяет общаться голосом, играть на пианинке, все это микшировать на сервере и выводить это в эфир:
Как работает интернет радиовещание, часть 2
Но об этом поговорим в следующий раз. Главное, мы теперь знаем как работает Icecast, как и куда идут данные, в следовательно создание таких вещей не будет для нас проблемой!

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

Автор: iFrolov


  1. Дмитрий:

    Поможете настроить ретранслятор с isecast в hls ?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js