icecast 和 libshout

最近, 由于业务原因, 使用了下 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
// connection.c
int connection_complete_source(source_t *source, int response)
{

// 通过 http header 'content-type' 找对应的 流decoder 用于decode metadata,
// 如果没有则兼容老版本,默认认为是 mp3, 以mp3 的格式解析 ...
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;
}
// 找对应的 format plugin 进行 format
if (format_get_plugin (format_type, source) < 0) {
// ...
}
}

// format.c
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;
// 默认用 以 mp3 格式解析
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;
}

// format_mp3.c
int format_mp3_get_plugin(source_t *source)
{

format_plugin_t *plugin;
// 这里将所有的format mp3 的实现以函数指针的形式保存到 format_plugin_t 中
// ...
// 将 mp3 write 的函数指针赋给 plugin
plugin->write_buf_to_client = format_mp3_write_buf_to_client
// 然后 将其作为 format 的属性
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
// source.c
void source_main (source_t *source)
{

while (global.running == ICECAST_RUNNING && source->running) {
// 取出一块 source发来的buffer
refbuf = get_next_buffer (source);
if (refbuf)
{
// listener 在内部以平衡二叉树维护。 相当于 将 buffer foreach 发给每个listener
client_node = avl_get_first(source->client_tree);
while (client_node) {
client = (client_t *) client_node->key;
// 发给 listener
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)
{
// ...
// 将数据写到 client , 这里的 write_to_client 是 src/format_mp3.c 的 format_mp3_write_buf_to_client 函数指针
bytes = client->write_to_client(client);
// ...
}
}

// 可见 每一次发送 buf 都会带上 metadata
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
{
/* send any unwritten metadata to the client */
if (client_mp3->in_metadata)
{
refbuf_t *associated = refbuf->associated;
ret = send_stream_metadata(client, associated);

written += ret;
}
/* see if we need to send the current metadata to the client */
if (remaining)
{
ret = client_send_bytes (client, buf, remaining);
written += ret;
}
ret = send_stream_metadata (client, refbuf->associated);
/* write any mp3, maybe after the metadata block */
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 再也不会挂了。

avatar

lelouchcr's blog