1. rtmp 消息解析:
1.1 、RTMP Message格式概述
RTMP协议面向上层用户定义了一种Message数据结构用于封装音视频的帧数据和协议控制命令,具体格式如下:
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Type | Payload length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Stream ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Message Type(1 Byte):消息类型ID,主要分为三类:协议控制消息、音视频数据帧、命令消息。
Payload Length(3-Bytes):报文净荷的字节数。以大端格式保存。
Timestamp(4-Bytes):当前消息的时间戳。以大端格式保存
StreamID(4-Bytes):消息流ID,其实从抓包看,一般只有0、1两种值,0:信令,1:音视频数据
Message Payload:Message报文净荷
1.2、RTMP Chunk格式概述
RTMP协议的一个Message数据包可能会非常大(尤其是传输一个视频帧时,最大可能16M字节)。
为了保证TCP高效传输(超大数据包在网络传输中受MTU限制会自动切片,超大数据包在网络传输过程中丢失一个数据片,都需要重传整个报文,导致传输效率降低)。
所以,RTMP协议的设计者决定在RTMP协议内部对Message数据包分片,这里分片的单位被称为Chunk(数据块)。
Chunk报文长度默认128字节,同时用户也可以修改本端发送的Chunk报文长度,并通过RTMP协议通知接收端。所以,网络中实际传输的RTMP报文,总是如下Chunk封装格式:
1~3字节
|<-Chunk Header->| 0/3/7/11字节 0或4字节
+----------------+----------------+-------------------+-----------+
| Basic header | Msg Header | Extended Timestamp|Chunk Data |
+----------------+----------------+-------------------+-----------+
注:因为Msg Header里有一个3字节的timestamp,当timestamp 被设置为0xffffff时,才会出现Extended Timestamp字段
1.2.1 Basic Header
Basic Header也被称为Chunk header,它的实际格式一共有3种(1~3个字节),目的是为了满足不同的长度的CSID(Chunk Stream ID),所以,在足够存储CSID字段的前提下应该用尽量少的字节格式从而减少由于引入Basic Header而增加的数据量。
+-----------------------+------------------+
| format [2个bit] | CSID [6bit] | 结构1:1字节
+-----------------------+------------------+
+-----------------------+------------------+--------------------+
| format [2个bit] | 0 [6bit] | 扩展 CSID [8bit] | 结构2:2字节
+-----------------------+------------------+--------------------+
+-----------------------+------------------+--------------------------------+
| format [2个bit] | 1 [6bit] | 扩展 CSID [16bit] | 结构3:3字节
+-----------------------+------------------+--------------------------------+
实际上为了处理方便,RTMP协议软件在具体实现时总是先读取一个字节的Basic Header,并根据前面2bit的format信息和后面6bit的CSID信息,判断报文的实际结构。
首先判断第一个字节的低6bit的CSID
当低6bit的CSID为0时,Basic Header占用2个字节,扩展CSID在[64+0,255+64=319]之间。
当低6bit的CSID为1时,Basic Header占用3个字节,扩展CSID在[64+0,65535+64=65599]之间。
当低6bit的CSID为2~63时,Basic Header占用1个字节,这种情况适用于绝大多数的RTMP Chunk报文。
CSID=2时,Message Type为1,2,3,5,6对应Chunk层控制协议,4对应用户控制命令
CSID=3~8,用于connect、createStream、releaseStream 、publish、metaData、音视频数据
(不同厂家之间,如FFMPEG、OBS在这里使用的CSID差异很大)
显然,接下来更多的CSID已经意义不大了,毕竟有Message Type也够了。当然,用户可以使用更大的CSID做一些私有协议的扩展。
实际上,真实环境中需要的CSID并不多,所以一般情况下,Basic Header长度总是一个字节。
1.2.2 Msg Header
接下来通过2bit的format字段,判断后面Msg Header格式
format =00,Msg Header占用11个字节,这种结构最浪费,一般用于流开始发送的第一个chunk报文,且只有这种情况下,报文中的timestamp才是一个绝对时间, 后续chunk报文的Msg Header中要么是没有timestamp,有timestamp也只是相对前一个chunk报文的时间增量。
|<--Basic Header->|<------------------------Msg Header----------------------->|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| format | CSID | timestamp | message length | message type | msg stream id |
| 00 | | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2 bits 6 bits 3 bytes 3 bytes 1 bytes 4 bytes
format =01,Msg Header占用7个字节,省去了4个字节的msg stream id,表示当前chunk和上一次发送的chunk所在的流相同
|<--Basic Header->|<---------------Msg Header---------------->|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| format | CSID | timestamp | message length | message type |
| 01 | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2 bits 6 bits 3 bytes 3 bytes 1 bytes
format =10,Msg Header占用3个字节,相对于format=01格式又省去了表示消息长度的3个字节和表示消息类型的1个字节。
|<--Basic Header->|<--Msg Header--->|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| format | CSID | timestamp |
| 10 | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2 bits 6 bits 3 bytes
format =11,Msg Header占用0个字节,它表示这个chunk的Msg Header和上一个chunk是完全相同的。
|<--Basic Header->|
+-+-+-+-+-+-+-+-+-+
| format | CSID |
| 11 | |
+-+-+-+-+-+-+-+-+-+
2 bits 6 bits
1.3 音视频数据帧
RTMP客户端与服务器之间连接建立后,开始推拉流时,总是先传输和音视频编码信息相关的MetaData(元数据),再传输音视频数据帧:
1)音视频数据总是采用FLV封装格式:tagHeader(1字节) + tagData(编码帧)
2)对于H.264视频,第一个视频帧必须是SPS和PPS,后面才是I帧和P帧。
3)对于AAC音频,第一个音频帧必须是AAC sequence header,后面才是AAC编码音频数据。
所谓元数据MetaData,其实就是一些用字符串描述的音视频编码器ID和宽高信息,接下来以H264和ACC为例初步了解一下音视频数据在RTMP中的Chunk封装格式:
Message Type =8;Audio message
协议层 封装层
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|RTMP Chunk Header | FLV AudioTagHeader | FLV AudioTagBody |
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
FLV AudioTagHeader
++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++
|SoundFormat | SoundRate | SoundSize | SoundType | OR + |AACPacketType|
++++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++
4bits 2bits 1bit 1bit
编码格式 采样率 采样精度8或16位 声道数(单/双)
如果音频格式是AAC(0x0A),上面的AudioTagHeader中会多出1个字节的AACPacketType描述后续的ACC包类型:
AACPacketType = 0x00 表示后续数据是AAC sequence header,
AACPacketType = 0x01 表示后续数据是AAC音频数据
最终,AAC sequence header由AudioSpecificConfig定义,简化的AudioSpecificConfig信息包括2字节如下:
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|AAC Profile 5bits | 采样率 4bits | 声道数 4bits | 其他 3bits |
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
对于RTMP推拉流,在发送第一个音频数据包前必须要发送这个AAC sequence header包。
Message Type =9;Video message
协议层 封装层
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|RTMP Chunk Header | FLV VideoTagHeader | FLV VideoTagBody |
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
FLV VideoTagHeader
+++++++++++++++++++++++ ++++++++++++++++ ++++++++++++++++++
|Frame Type | CodecID | + |AVCPacketType | + |CompositionTime |
+++++++++++++++++++++++ ++++++++++++++++ ++++++++++++++++++
4 bits 4 bits 1 byte 3 byte
视频帧类型 编码器ID
Frame Type = 1表示H264的关键帧,包括IDR;Frame Type=2表示H264的非关键帧
codecID = 7表示AVC
当采用AVC编码(即H264)时,会增加1个字节的AVCPacketType字段(描述后续的AVC包类型)和3个字节的CompositionTime :
AVCPacketType=0时,表示后续数据AVC sequence header,此时3字节CompositionTime内容为0
AVCPacketType=1时,表示后续数据AVC NALU
AVCPacketType=2时,表示后续数据AVC end of sequence (一般不需要)
FLV VideoTagBody
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
| Size | AVCDecoderConfigurationRecord or ( one or more NALUs ) |
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 bytes
2. ffmpeg 实现rtmp 协议
const URLProtocol ff_##flavor##_protocol = { \
.name = #flavor, \
.url_open2 = rtmp_open, \
.url_read = rtmp_read, \
.url_read_seek = rtmp_seek, \
.url_read_pause = rtmp_pause, \
.url_write = rtmp_write, \
.url_close = rtmp_close, \
.priv_data_size = sizeof(RTMPContext), \
.flags = URL_PROTOCOL_FLAG_NETWORK, \
.priv_data_class= &flavor##_class, \
};
2.1 rtmp_open 分析;
1. tcp 连接
2.rtmp 握手
3.rtmp 连接
4.rtmp 连接后处理:
设置chunk_size
设置WINDOW_ACK_SIZE
invoke 操作等等.......
static int rtmp_open(URLContext *s, const char *uri, int flags, AVDictionary **opts)
{
RTMPContext *rt = s->priv_data;
char proto[8], hostname[256], path[1024], auth[100], *fname;
char *old_app, *qmark, *n, fname_buffer[1024];
uint8_t buf[2048];
int port;
int ret;
if (rt->listen_timeout > 0)
rt->listen = 1;
rt->is_input = !(flags & AVIO_FLAG_WRITE);
av_url_split(proto, sizeof(proto), auth, sizeof(auth),
hostname, sizeof(hostname), &port,
path, sizeof(path), s->filename);
n = strchr(path, ' ');
if (n) {
av_log(s, AV_LOG_WARNING,
"Detected librtmp style URL parameters, these aren't supported "
"by the libavformat internal RTMP handler currently enabled. "
"See the documentation for the correct way to pass parameters.\n");
*n = '\0'; // Trim not supported part
}
if (auth[0]) {
char *ptr = strchr(auth, ':');
if (ptr) {
*ptr = '\0';
av_strlcpy(rt->username, auth, sizeof(rt->username));
av_strlcpy(rt->password, ptr + 1, sizeof(rt->password));
}
}
if (rt->listen && strcmp(proto, "rtmp")) {
av_log(s, AV_LOG_ERROR, "rtmp_listen not available for %s\n",
proto);
return AVERROR(EINVAL);
}
if (!strcmp(proto, "rtmpt") || !strcmp(proto, "rtmpts")) {
if (!strcmp(proto, "rtmpts"))
av_dict_set(opts, "ffrtmphttp_tls", "1", 1);
/* open the http tunneling connection */
ff_url_join(buf, sizeof(buf), "ffrtmphttp", NULL, hostname, port, NULL);
} else if (!strcmp(proto, "rtmps")) {
/* open the tls connection */
if (port < 0)
port = RTMPS_DEFAULT_PORT;
ff_url_join(buf, sizeof(buf), "tls", NULL, hostname, port, NULL);
} else if (!strcmp(proto, "rtmpe") || (!strcmp(proto, "rtmpte"))) {
if (!strcmp(proto, "rtmpte"))
av_dict_set(opts, "ffrtmpcrypt_tunneling", "1", 1);
/* open the encrypted connection */
ff_url_join(buf, sizeof(buf), "ffrtmpcrypt", NULL, hostname, port, NULL);
rt->encrypted = 1;
} else {
/* open the tcp connection */
if (port < 0)
port = RTMP_DEFAULT_PORT;
if (rt->listen)
ff_url_join(buf, sizeof(buf), "tcp", NULL, hostname, port,
"?listen&listen_timeout=%d",
rt->listen_timeout * 1000);
else
ff_url_join(buf, sizeof(buf), "tcp", NULL, hostname, port, NULL);
}
reconnect:
// tcp 连接
if ((ret = ffurl_open_whitelist(&rt->stream, buf, AVIO_FLAG_READ_WRITE,
&s->interrupt_callback, opts,
s->protocol_whitelist, s->protocol_blacklist, s)) < 0) {
av_log(s , AV_LOG_ERROR, "Cannot open connection %s\n", buf);
goto fail;
}
if (rt->swfverify) {
if ((ret = rtmp_calc_swfhash(s)) < 0)
goto fail;
}
rt->state = STATE_START;
// rtmp hand shake 发送C0 C1 C2 接受S0 S1 S2
if (!rt->listen && (ret = rtmp_handshake(s, rt)) < 0)
goto fail;
if (rt->listen && (ret = rtmp_server_handshake(s, rt)) < 0)
goto fail;
rt->out_chunk_size = 128;
rt->in_chunk_size = 128; // Probably overwritten later
rt->state = STATE_HANDSHAKED;
// Keep the application name when it has been defined by the user.
old_app = rt->app;
rt->app = av_malloc(APP_MAX_LENGTH);
if (!rt->app) {
ret = AVERROR(ENOMEM);
goto fail;
}
//extract "app" part from path
qmark = strchr(path, '?');
if (qmark && strstr(qmark, "slist=")) {
char* amp;
// After slist we have the playpath, the full path is used as app
av_strlcpy(rt->app, path + 1, APP_MAX_LENGTH);
fname = strstr(path, "slist=") + 6;
// Strip any further query parameters from fname
amp = strchr(fname, '&');
if (amp) {
av_strlcpy(fname_buffer, fname, FFMIN(amp - fname + 1,
sizeof(fname_buffer)));
fname = fname_buffer;
}
} else if (!strncmp(path, "/ondemand/", 10)) {
fname = path + 10;
memcpy(rt->app, "ondemand", 9);
} else {
char *next = *path ? path + 1 : path;
char *p = strchr(next, '/');
if (!p) {
if (old_app) {
// If name of application has been defined by the user, assume that
// playpath is provided in the URL
fname = next;
} else {
fname = NULL;
av_strlcpy(rt->app, next, APP_MAX_LENGTH);
}
} else {
// make sure we do not mismatch a playpath for an application instance
char *c = strchr(p + 1, ':');
fname = strchr(p + 1, '/');
if (!fname || (c && c < fname)) {
fname = p + 1;
av_strlcpy(rt->app, path + 1, FFMIN(p - path, APP_MAX_LENGTH));
} else {
fname++;
av_strlcpy(rt->app, path + 1, FFMIN(fname - path - 1, APP_MAX_LENGTH));
}
}
}
if (old_app) {
// The name of application has been defined by the user, override it.
if (strlen(old_app) >= APP_MAX_LENGTH) {
ret = AVERROR(EINVAL);
goto fail;
}
av_free(rt->app);
rt->app = old_app;
}
if (!rt->playpath) {
int max_len = 1;
if (fname)
max_len = strlen(fname) + 5; // add prefix "mp4:"
rt->playpath = av_malloc(max_len);
if (!rt->playpath) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (fname) {
int len = strlen(fname);
if (!strchr(fname, ':') && len >= 4 &&
(!strcmp(fname + len - 4, ".f4v") ||
!strcmp(fname + len - 4, ".mp4"))) {
memcpy(rt->playpath, "mp4:", 5);
} else {
if (len >= 4 && !strcmp(fname + len - 4, ".flv"))
fname[len - 4] = '\0';
rt->playpath[0] = 0;
}
av_strlcat(rt->playpath, fname, max_len);
} else {
rt->playpath[0] = '\0';
}
}
if (!rt->tcurl) {
rt->tcurl = av_malloc(TCURL_MAX_LENGTH);
if (!rt->tcurl) {
ret = AVERROR(ENOMEM);
goto fail;
}
ff_url_join(rt->tcurl, TCURL_MAX_LENGTH, proto, NULL, hostname,
port, "/%s", rt->app);
}
if (!rt->flashver) {
rt->flashver = av_malloc(FLASHVER_MAX_LENGTH);
if (!rt->flashver) {
ret = AVERROR(ENOMEM);
goto fail;
}
if (rt->is_input) {
snprintf(rt->flashver, FLASHVER_MAX_LENGTH, "%s %d,%d,%d,%d",
RTMP_CLIENT_PLATFORM, RTMP_CLIENT_VER1, RTMP_CLIENT_VER2,
RTMP_CLIENT_VER3, RTMP_CLIENT_VER4);
} else {
snprintf(rt->flashver, FLASHVER_MAX_LENGTH,
"FMLE/3.0 (compatible; %s)", LIBAVFORMAT_IDENT);
}
}
rt->receive_report_size = 1048576;
rt->bytes_read = 0;
rt->has_audio = 0;
rt->has_video = 0;
rt->received_metadata = 0;
rt->last_bytes_read = 0;
rt->max_sent_unacked = 2500000;
rt->duration = 0;
av_log(s, AV_LOG_DEBUG, "Proto = %s, path = %s, app = %s, fname = %s\n",
proto, path, rt->app, rt->playpath);
if (!rt->listen) {
//rtmp 连接服务器
if ((ret = gen_connect(s, rt)) < 0)
goto fail;
} else {
if ((ret = read_connect(s, s->priv_data)) < 0)
goto fail;
}
do {
//连接rtmp服务 后处理
ret = get_packet(s, 1);
} while (ret == AVERROR(EAGAIN));
if (ret < 0)
goto fail;
if (rt->do_reconnect) {
int i;
ffurl_closep(&rt->stream);
rt->do_reconnect = 0;
rt->nb_invokes = 0;
for (i = 0; i < 2; i++)
memset(rt->prev_pkt[i], 0,
sizeof(**rt->prev_pkt) * rt->nb_prev_pkt[i]);
free_tracked_methods(rt);
goto reconnect;
}
if (rt->is_input) {
// generate FLV header for demuxer
rt->flv_size = 13;
if ((ret = av_reallocp(&rt->flv_data, rt->flv_size)) < 0)
goto fail;
rt->flv_off = 0;
memcpy(rt->flv_data, "FLV\1\0\0\0\0\011\0\0\0\0", rt->flv_size);
// Read packets until we reach the first A/V packet or read metadata.
// If there was a metadata package in front of the A/V packets, we can
// build the FLV header from this. If we do not receive any metadata,
// the FLV decoder will allocate the needed streams when their first
// audio or video packet arrives.
while (!rt->has_audio && !rt->has_video && !rt->received_metadata) {
if ((ret = get_packet(s, 0)) < 0)
goto fail;
}
// Either after we have read the metadata or (if there is none) the
// first packet of an A/V stream, we have a better knowledge about the
// streams, so set the FLV header accordingly.
if (rt->has_audio) {
rt->flv_data[4] |= FLV_HEADER_FLAG_HASAUDIO;
}
if (rt->has_video) {
rt->flv_data[4] |= FLV_HEADER_FLAG_HASVIDEO;
}
// If we received the first packet of an A/V stream and no metadata but
// the server returned a valid duration, create a fake metadata packet
// to inform the FLV decoder about the duration.
if (!rt->received_metadata && rt->duration > 0) {
if ((ret = inject_fake_duration_metadata(rt)) < 0)
goto fail;
}
} else {
rt->flv_size = 0;
rt->flv_data = NULL;
rt->flv_off = 0;
rt->skip_bytes = 13;
}
s->max_packet_size = rt->stream->max_packet_size;
s->is_streamed = 1;
return 0;
fail:
av_freep(&rt->playpath);
av_freep(&rt->tcurl);
av_freep(&rt->flashver);
av_dict_free(opts);
rtmp_close(s);
return ret;
}
2.2 rtmp_write 分析
主要去掉flv tag header,调用rtmp_send_packet
static int rtmp_write(URLContext *s, const uint8_t *buf, int size)
{
RTMPContext *rt = s->priv_data;
int size_temp = size;
int pktsize, pkttype, copy;
uint32_t ts;
const uint8_t *buf_temp = buf;
uint8_t c;
int ret;
int i = 0;
for(i = 0; i < 20; i++)
{
printf("%2x ",buf[i]);
}
printf("\n");
do {
if (rt->skip_bytes) {
int skip = FFMIN(rt->skip_bytes, size_temp);
buf_temp += skip;
size_temp -= skip;
rt->skip_bytes -= skip;
continue;
}
if (rt->flv_header_bytes < RTMP_HEADER) {
const uint8_t *header = rt->flv_header;
int channel = RTMP_AUDIO_CHANNEL;
copy = FFMIN(RTMP_HEADER - rt->flv_header_bytes, size_temp);
bytestream_get_buffer(&buf_temp, rt->flv_header + rt->flv_header_bytes, copy);
rt->flv_header_bytes += copy;
size_temp -= copy;
if (rt->flv_header_bytes < RTMP_HEADER)
break;
pkttype = bytestream_get_byte(&header);
pktsize = bytestream_get_be24(&header);
ts = bytestream_get_be24(&header);
ts |= bytestream_get_byte(&header) << 24;
bytestream_get_be24(&header);
rt->flv_size = pktsize;
if (pkttype == RTMP_PT_VIDEO)
channel = RTMP_VIDEO_CHANNEL;
if (((pkttype == RTMP_PT_VIDEO || pkttype == RTMP_PT_AUDIO) && ts == 0) ||
pkttype == RTMP_PT_NOTIFY) {
if ((ret = ff_rtmp_check_alloc_array(&rt->prev_pkt[1],
&rt->nb_prev_pkt[1],
channel)) < 0)
return ret;
// Force sending a full 12 bytes header by clearing the
// channel id, to make it not match a potential earlier
// packet in the same channel.
rt->prev_pkt[1][channel].channel_id = 0;
}
//this can be a big packet, it's better to send it right here
if ((ret = ff_rtmp_packet_create(&rt->out_pkt, channel,
pkttype, ts, pktsize)) < 0)
return ret;
rt->out_pkt.extra = rt->stream_id;
rt->flv_data = rt->out_pkt.data;
}
copy = FFMIN(rt->flv_size - rt->flv_off, size_temp);
bytestream_get_buffer(&buf_temp, rt->flv_data + rt->flv_off, copy);
rt->flv_off += copy;
size_temp -= copy;
if (rt->flv_off == rt->flv_size) {
rt->skip_bytes = 4;
if (rt->out_pkt.type == RTMP_PT_NOTIFY) {
// For onMetaData and |RtmpSampleAccess packets, we want
// @setDataFrame prepended to the packet before it gets sent.
// However, not all RTMP_PT_NOTIFY packets (e.g., onTextData
// and onCuePoint).
uint8_t commandbuffer[64];
int stringlen = 0;
GetByteContext gbc;
bytestream2_init(&gbc, rt->flv_data, rt->flv_size);
if (!ff_amf_read_string(&gbc, commandbuffer, sizeof(commandbuffer),
&stringlen)) {
if (!strcmp(commandbuffer, "onMetaData") ||
!strcmp(commandbuffer, "|RtmpSampleAccess")) {
uint8_t *ptr;
if ((ret = av_reallocp(&rt->out_pkt.data, rt->out_pkt.size + 16)) < 0) {
rt->flv_size = rt->flv_off = rt->flv_header_bytes = 0;
return ret;
}
memmove(rt->out_pkt.data + 16, rt->out_pkt.data, rt->out_pkt.size);
rt->out_pkt.size += 16;
ptr = rt->out_pkt.data;
ff_amf_write_string(&ptr, "@setDataFrame");
}
}
}
printf("\n");
uint8_t *ptr;
ptr = rt->out_pkt.data;
for(i = 0; i < 20; i++)
{
printf("%2x ",ptr[i]);
}
printf("\n");
if ((ret = rtmp_send_packet(rt, &rt->out_pkt, 0)) < 0)
return ret;
rt->flv_size = 0;
rt->flv_off = 0;
rt->flv_header_bytes = 0;
rt->flv_nb_packets++;
}
} while (buf_temp - buf < size);
if (rt->flv_nb_packets < rt->flush_interval)
return size;
rt->flv_nb_packets = 0;
/* set stream into nonblocking mode */
rt->stream->flags |= AVIO_FLAG_NONBLOCK;
/* try to read one byte from the stream */
ret = ffurl_read(rt->stream, &c, 1);
/* switch the stream back into blocking mode */
rt->stream->flags &= ~AVIO_FLAG_NONBLOCK;
if (ret == AVERROR(EAGAIN)) {
/* no incoming data to handle */
return size;
} else if (ret < 0) {
return ret;
} else if (ret == 1) {
RTMPPacket rpkt = { 0 };
if ((ret = ff_rtmp_packet_read_internal(rt->stream, &rpkt,
rt->in_chunk_size,
&rt->prev_pkt[0],
&rt->nb_prev_pkt[0], c)) <= 0)
return ret;
if ((ret = rtmp_parse_result(s, rt, &rpkt)) < 0)
return ret;
ff_rtmp_packet_destroy(&rpkt);
}
return size;
}
rtmp_send_packet
ff_rtmp_packet_write
ff_rtmp_packet_write
将rtmp message 拆分chunk size发送
int ff_rtmp_packet_write(URLContext *h, RTMPPacket *pkt,
int chunk_size, RTMPPacket **prev_pkt_ptr,
int *nb_prev_pkt)
{
uint8_t pkt_hdr[16], *p = pkt_hdr;
int mode = RTMP_PS_TWELVEBYTES;
int off = 0;
int written = 0;
int ret;
RTMPPacket *prev_pkt;
int use_delta; // flag if using timestamp delta, not RTMP_PS_TWELVEBYTES
uint32_t timestamp; // full 32-bit timestamp or delta value
if ((ret = ff_rtmp_check_alloc_array(prev_pkt_ptr, nb_prev_pkt,
pkt->channel_id)) < 0)
return ret;
prev_pkt = *prev_pkt_ptr;
//if channel_id = 0, this is first presentation of prev_pkt, send full hdr.
use_delta = prev_pkt[pkt->channel_id].channel_id &&
pkt->extra == prev_pkt[pkt->channel_id].extra &&
pkt->timestamp >= prev_pkt[pkt->channel_id].timestamp;
timestamp = pkt->timestamp;
if (use_delta) {
timestamp -= prev_pkt[pkt->channel_id].timestamp;
}
if (timestamp >= 0xFFFFFF) {
pkt->ts_field = 0xFFFFFF;
} else {
pkt->ts_field = timestamp;
}
if (use_delta) {
if (pkt->type == prev_pkt[pkt->channel_id].type &&
pkt->size == prev_pkt[pkt->channel_id].size) {
mode = RTMP_PS_FOURBYTES;
if (pkt->ts_field == prev_pkt[pkt->channel_id].ts_field)
mode = RTMP_PS_ONEBYTE;
} else {
mode = RTMP_PS_EIGHTBYTES;
}
}
if (pkt->channel_id < 64) {
bytestream_put_byte(&p, pkt->channel_id | (mode << 6));
} else if (pkt->channel_id < 64 + 256) {
bytestream_put_byte(&p, 0 | (mode << 6));
bytestream_put_byte(&p, pkt->channel_id - 64);
} else {
bytestream_put_byte(&p, 1 | (mode << 6));
bytestream_put_le16(&p, pkt->channel_id - 64);
}
if (mode != RTMP_PS_ONEBYTE) {
bytestream_put_be24(&p, pkt->ts_field);
if (mode != RTMP_PS_FOURBYTES) {
bytestream_put_be24(&p, pkt->size);
bytestream_put_byte(&p, pkt->type);
if (mode == RTMP_PS_TWELVEBYTES)
bytestream_put_le32(&p, pkt->extra);
}
}
if (pkt->ts_field == 0xFFFFFF)
bytestream_put_be32(&p, timestamp);
// save history
prev_pkt[pkt->channel_id].channel_id = pkt->channel_id;
prev_pkt[pkt->channel_id].type = pkt->type;
prev_pkt[pkt->channel_id].size = pkt->size;
prev_pkt[pkt->channel_id].timestamp = pkt->timestamp;
prev_pkt[pkt->channel_id].ts_field = pkt->ts_field;
prev_pkt[pkt->channel_id].extra = pkt->extra;
if ((ret = ffurl_write(h, pkt_hdr, p - pkt_hdr)) < 0)
return ret;
written = p - pkt_hdr + pkt->size;
while (off < pkt->size) {
int towrite = FFMIN(chunk_size, pkt->size - off);
if ((ret = ffurl_write(h, pkt->data + off, towrite)) < 0)
return ret;
off += towrite;
if (off < pkt->size) {
uint8_t marker = 0xC0 | pkt->channel_id;
if ((ret = ffurl_write(h, &marker, 1)) < 0)
return ret;
written++;
if (pkt->ts_field == 0xFFFFFF) {
uint8_t ts_header[4];
AV_WB32(ts_header, timestamp);
if ((ret = ffurl_write(h, ts_header, 4)) < 0)
return ret;
written += 4;
}
}
}
return written;
}