最近, 由于业务原因, 使用了下 icecast, 顺便研究了下 libshout 和 icecast, 以下做个总结。
icecast 是一个流媒体服务器, 文档上说可以推 mp3, ogg 等流, 然后可以对外直播。
但是, 正好有个需要希望能推 aac 的流, 提高音质, 并且实测试发现他确实可以推 aac 的流, 惊讶之余便研究了下 icecast 的代码。
icecast 总的 流程图 如下
source 首先, 他是一个流媒体服务器, 对于一个媒体流来说, 播放器要播放的话, 首先需要知道他的 metadata, 所以推流的时候, icecast 会给每一个 source 保存它对应的 metadata
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 int connection_complete_source (source_t *source, int response) { if (contenttype != NULL ) { } else if (source->parser->req_type == httpp_req_put) { } else { ICECAST_LOG_ERROR("No content-type header, falling back to backwards compatibility mode " "for icecast 1.x relays. Assuming content is mp3. This behaviour is deprecated " "and the source client will NOT work with future Icecast versions!" ); format_type = FORMAT_TYPE_GENERIC; } if (format_get_plugin (format_type, source) < 0 ) { } } int format_get_plugin (format_type_t type, source_t *source) { int ret = -1 ; switch (type) { case FORMAT_TYPE_OGG: ret = format_ogg_get_plugin(source); break ; case FORMAT_TYPE_EBML: ret = format_ebml_get_plugin(source); break ; case FORMAT_TYPE_GENERIC: ret = format_mp3_get_plugin(source); break ; default : break ; } if (ret < 0 ) stats_event (source->mount, "content-type" , source->format->contenttype); return ret; } int format_mp3_get_plugin (source_t *source) { format_plugin_t *plugin; plugin->write_buf_to_client = format_mp3_write_buf_to_client source->format = plugin; }
listener source 的 main Thread 不断的取 source 发过来的数据, 然后依次执行上面的步骤, 当有 listener 时, 顺序 foreach 发给每一个 listener。
注意, icecast server 干的事情仅仅是将从 source 发来的数据依次发给 listener。 如果 source 发来的数据发的过快,比如 将60秒的数据一次性发过来
那么 icecast 也会 一瞬间将数据推给所有listener
。 当前连着的 listener 是可以听的, 但是没有连上就听不到的。
总结来说, icecast server 自己不对媒体数据做缓冲, 缓冲完全靠 source 自己控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 void source_main (source_t *source) { while (global.running == ICECAST_RUNNING && source->running) { refbuf = get_next_buffer (source); if (refbuf) { client_node = avl_get_first(source->client_tree); while (client_node) { client = (client_t *) client_node->key; send_to_listener(source, client, remove_from_q); client_node = avl_get_next(client_node); } } } } static void send_to_listener (source_t *source, client_t *client, int deletion_expected) { while (1 ) { bytes = client->write_to_client(client); } } static int format_mp3_write_buf_to_client (client_t *client) { char *buf = refbuf->data + client->pos; unsigned int len = refbuf->len - client->pos; do { if (client_mp3->in_metadata) { refbuf_t *associated = refbuf->associated; ret = send_stream_metadata(client, associated); written += ret; } if (remaining) { ret = client_send_bytes (client, buf, remaining); written += ret; } ret = send_stream_metadata (client, refbuf->associated); if (len) { ret = client_send_bytes (client, buf, len); written += ret; } ret = 0 ; } while (0 ); return written; }
并且每一次发送buf 的时候都会带上 mp3 格式的 buf
aac 那么问题来了,为什么 可以推 aac?
查了下资料 , aac 有2种格式
AAC编码的主要扩展名有三种:
aac - 使用MPEG-2 Audio Transport Stream(ADTS,参见MPEG-2)容器,区别于使用MPEG-4容器的MP4/M4A格式,属于传统的AAC编码(FAAC默认的封装,但FAAC亦可输出MPEG-4封装的AAC)。
mp4 - 使用了MPEG-4 Part 14(第14部分)的简化版即3GPP Media Release 6 Basic(3gp6,参见3GP)进行封装的AAC编码(Nero AAC编码器仅能输出MPEG-4封装的AAC)。
m4a - 为了区别纯音频MP4文件和包含视频的MP4文件而由苹果(Apple)公司使用的扩展名,Apple iTunes对纯音频MP4文件采用了”.m4a”命名。M4A的本质和音频MP4相同,故音频MP4文件亦可直接更改扩展名为M4A。
因为我想弄 aac stream 所以选了第一个
而又因为 aac 的 adts 其实是 MPEG-2 Audio Transport Stream
的简称,
而 mp3 其实全称是 MPEG-1 or MPEG-2 Audio Layer III
他们 metadata 格式相同, 所以可以兼容。
也就是说 如果推流推的是 adts, 则可以兼容默认的 mp3。
e.g
1 ffmpeg -re -i xx.xx -vn -c:a libfdk_aac -profile:a aac_he -ac 2 -ab 64k -f adts 'icecast://source:hackme@localhost:8000/jlibshout'
这时, 推的流的 header(contentType) 不是 mp3, 而是 audio/mpeg
可以看到 icecast 的警告日志。
1 [2017-01-01 01:23:41] WARN format/format_get_type Unsupported or legacy stream type: "audio/mpeg". Falling back to generic minimal handler for best effort.
然后用默认的 mp3 格式解析, 由于 adts 正好格式与 mp3 相兼容, 于是。。。。
libshout 由于, 业务上不仅仅使用 ffmpeg 推流, 还希望可以通过代码推流 (节约资源).
所以看了下 libshout, 之前也分析了 icecast 不进行缓冲。
然后找了个 java的库 试了下, 结果JVM 老是崩溃, (ps 因为我的源流并不稳定.)
于是研究了下 libshout。 libshout 是推流的工具包, 他支持 ICY
, Http
, Shoutcast
协议。
Icy
是自己的协议, Shoutcast
是一个 商业版本的类似 icecast 的 live server 的自己的协议。
他的 Http 协议其实就是自己新建了个 Source 的 method
1 2 3 4 5 6 SOURCE /jlibshout HTTP/1.1 Host: localhost:8030 Content-Type: audio/mp3; Authorization: Basic Base64(user:password)" ice-name: xxx ice-description: xxx
由于 libshout 需要自己做buf的时间控制, 原理就是 读取mp3 的metadata, 确定文件总时长, 然后每次发送buf 估算他的时长,发一段 sleep 一段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 src/format_mp3.c 139 : self->senttime += (int64_t )((double )mp3_data->frame_samples / (double )mp3_data->frame_samplerate * 1000000 );void shout_sync (shout_t *self) { int64_t sleep; if (self->senttime == 0 ) return ; sleep = self->senttime / 1000 - (timing_get_time() - self->starttime); if (sleep > 0 ) timing_sleep((uint64_t )sleep); }
知道原理后, 搞了个纯 java 版的 libshout , 只有 IO 操作, JVM 再也不会挂了。