流媒体之RTMP——librtmp推流测试
文章目录
作者:一步(Reser)
日期:2019.10.9
一:LibRTMP推流测试
测试使用 FFMPEG
从MP4文件中解析出H264裸流,之后按照固定帧率将视频流推送到RTMP服务器。
- H264流支持从MP4文件中解析,或者从H264文件中读取(未测试);
- 暂未加入音频测试。
新建项目,加入FFMPEG和LibRTMP相关依赖。
二:时间控制
编写简单的时间控制类 CTimeStatistics
,这个类主要负责RTMP发送时候的帧控制。
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
typedef long long tick_t;
#define GET_TIME(T,S,F) ((double)((T)-(S))/(double)(F/1000))
class CTimeStatistics
{
public:
CTimeStatistics()
{
_start = 0;
_stop = 0;
}
virtual ~CTimeStatistics() {};
inline void reset()
{
_start = 0;
_stop = 0;
}
inline void start()
{
_start = _get_tick();
}
inline void stop()
{
_stop = _get_tick();
}
inline double get_delta()
{
return GET_TIME(_get_tick(), _start, _get_frequency());
}
inline double get_total()
{
return GET_TIME(_stop, _start, _get_frequency());
}
protected:
tick_t _get_tick()
{
LARGE_INTEGER t1;
QueryPerformanceCounter(&t1);
return t1.QuadPart;
}
tick_t _get_frequency()
{
LARGE_INTEGER t1;
QueryPerformanceFrequency(&t1);
return t1.QuadPart;
}
private:
tick_t _start;
tick_t _stop;
};
包含简单的Start/Stop/Reset和获得时间间隔接口。
三:FFMPEG从MP4文件解析出H264
这里假定对FFMPEG的使用有些基本了解。
主要结构体定义:
// For ffmpeg demux
typedef enum _stream_type
{
STREAM_FILE_MP4 = 0,
STREAM_H264_RAW
} stream_type_t;
typedef struct _fmt_manage
{
AVFormatContext *context;
AVStream *vstream;
AVStream *astream;
} fmt_manage_t;
从文件中解析出流信息:
// Ffmpeg demux, parse streams from file
av_register_all();
if (avformat_open_input(&_manage.context, file.c_str(), 0, 0) < 0)
break;
if (avformat_find_stream_info(_manage.context, 0) < 0)
break;
if (!_parse_streams(meta, type))
break;
其中主要函数 _parse_stream
:
bool CTestLibRTMPPusher::_parse_streams(metadata_t &meta, stream_type_t type)
{
for (int i = 0; i < _manage.context->nb_streams; i++) {
// Video stream
if (AVMEDIA_TYPE_VIDEO == _manage.context->streams[i]->codecpar->codec_type) {
_manage.vstream = _manage.context->streams[i];
// Parse sps/pps from extradata
// If MP4,extradata stores 'avcCfg'; or stores 'sps/pps'
if (_manage.context->streams[i]->codecpar->extradata_size > 0) {
uint32_t size = _manage.context->streams[i]->codecpar->extradata_size;
uint8_t *ptr = _manage.context->streams[i]->codecpar->extradata;
switch (type)
{
case STREAM_FILE_MP4:
{
// Parse SPS/PPS from avcCfg
uint32_t offset = 5;
uint32_t num_sps = ptr[offset++] & 0x1f;
for (uint32_t j = 0; j < num_sps; j++) {
meta.param.size_sps = (ptr[offset++] << 8);
meta.param.size_sps |= ptr[offset++];
memcpy(meta.param.data_sps, ptr + offset, meta.param.size_sps);
offset += meta.param.size_sps;
}
uint32_t num_pps = ptr[offset++];
for (uint32_t j = 0; j < num_pps; j++) {
meta.param.size_pps = (ptr[offset++] << 8);
meta.param.size_pps |= ptr[offset++];
memcpy(meta.param.data_pps, ptr + offset, meta.param.size_pps);
offset += meta.param.size_pps;
}
}
break;
case STREAM_H264_RAW:
{
// Parse SPS/PPS from 'sps/pps'
uint32_t offset = 0;
if (ptr[offset] != 0x00 || ptr[offset + 1] != 0x00 || ptr[offset + 2] != 0x00 || ptr[offset + 3] != 0x01) {
// No valid data...
}
else {
// Find next pos
offset++;
while ((ptr[offset] != 0x00 || ptr[offset + 1] != 0x00 || ptr[offset + 2] != 0x00 || ptr[offset + 3] != 0x01) && (offset < size - 3))
offset++;
if ((ptr[4] & 0x1f) == 7) { // SPS first
meta.param.size_sps = offset - 4;
memcpy(meta.param.data_sps, ptr + 4, meta.param.size_sps);
meta.param.size_pps = size - offset - 4;
memcpy(meta.param.data_pps, ptr + offset + 4, meta.param.size_pps);
}
else if ((ptr[4] & 0x1f) == 8) { // PPS first
meta.param.size_pps = offset - 4;
memcpy(meta.param.data_pps, ptr + 4, meta.param.size_pps);
meta.param.size_sps = size - offset - 4;
memcpy(meta.param.data_sps, ptr + offset + 4, meta.param.size_sps);
}
}
}
break;
default:
break;
}
}
}
// Audio stream
else if (AVMEDIA_TYPE_AUDIO == _manage.context->streams[i]->codecpar->codec_type) {
_manage.astream = _manage.context->streams[i];
// parse esds from extra data
if (_manage.context->streams[i]->codecpar->extradata_size > 0) {
uint32_t size = _manage.context->streams[i]->codecpar->extradata_size;
uint8_t *ptr = _manage.context->streams[i]->codecpar->extradata;
// Ignore audio
// ...
}
}
}
return true;
}
- 根据
_manage.context->streams[i]->codecpar->codec_type
判断流类型; - 视频流的extradata保存视频配置数据,当为MP4文件时,保存的是
avcCfg
结构体,需要从中解析出SPS和PPS;当为H264裸流文件时,保存的是以0x00,0x00,0x00,0x01
开头的SPS和PPS,需要从中解析出SPS和PPS;
循环解析和发送数据:
// Read frames from file by ffmpeg
AVPacket pkt = { 0 };
if (av_read_frame(_manage.context, &pkt) < 0)
break;
// Video frame
if (pkt.stream_index == _manage.vstream->index) {
uint64_t pts = period_ms * video_frame_count;
bool keyframe = pkt.flags & AV_PKT_FLAG_KEY;
// Replace size-4-bytes with 0x00,0x00,0x00,0x01
pkt.data[0] = 0x00;
pkt.data[1] = 0x00;
pkt.data[2] = 0x00;
pkt.data[3] = 0x01;
_send_video(pkt.size, pkt.data, pts, keyframe);
video_frame_count++;
if (video_frame_count % 100 == 0) {
printf("Send video frames: %d\n", video_frame_count);
}
}
// Audio frame
else if (pkt.stream_index == _manage.astream->index) {
// Ignore audio
// ...
audio_frame_count++;
}
- 读取的帧类型可由
pkt.stream_index
判断; - 视频关键帧由
pkt.flags & AV_PKT_FLAG_KEY
判断; - 解析出的视频帧并没有以
0x00,0x00,x00,0x01
分割,而是以4个字节的size打头,因此需要将4个字节的size替换为0x00,0x00,x00,0x01
起始分隔符; - 解析出的pts可能为0,因此使用自定义的pts。对于某些RTMP服务器,需要pts从0开始。
四:LibRTMP的使用
使用的结构体定义:
// For rtmp structure
#define H264_PARAM_LEN 512
typedef struct _h264_param
{
uint32_t size_sps;
uint8_t data_sps[H264_PARAM_LEN];
uint32_t size_pps;
uint8_t data_pps[H264_PARAM_LEN];
} h264_param_t;
typedef struct _metadata
{
// Video
uint32_t width;
uint32_t height;
uint32_t fps;
uint32_t bitrate_kpbs;
h264_param_t param;
// Audio
bool has_audio;
uint32_t channels;
uint32_t samplerate;
uint32_t samplesperframe;
uint32_t datarate;
} metadata_t;
LibRTMP在Windows下的使用前和使用后要进行Socket的初始化和反初始化:
bool CTestLibRTMPPusher::_init_sockets()
{
WORD version;
WSADATA wsaData;
version = MAKEWORD(2, 2);
return (0 == WSAStartup(version, &wsaData));
}
void CTestLibRTMPPusher::_cleanup_sockets()
{
WSACleanup();
}
RTMP资源初始化:
...
_rtmp_ptr = RTMP_Alloc();
if (NULL == _rtmp_ptr)
break;
RTMP_Init(_rtmp_ptr);
...
// Parse rtmp url
_rtmp_ptr->Link.timeout = timeout_secs;
_rtmp_ptr->Link.lFlags |= RTMP_LF_LIVE;
if (RTMP_SetupURL(_rtmp_ptr, (char *)url.c_str()) < 0)
break;
// Pusher mode
RTMP_EnableWrite(_rtmp_ptr);
// Socket connection
// Handshakes and connect command
if (RTMP_Connect(_rtmp_ptr, NULL) < 0)
break;
// Setup stream and stream settings
if (RTMP_ConnectStream(_rtmp_ptr, 0) < 0)
break;
// Send metadata(video and audio settings)
if (!_send_metadata(_metadata))
break;
RTMP为Adobe公司开发,其上层传输封装主要基于 FLV 格式,这样可以使用Flash插件直接播放。因此,在发送RTMP的包时必须要属性FLV的封装规范。
FLV由Header和Body组成;Body由多组Tag组成;Tag又由TagHeader和TagData组成。由于RTMP协议的包已经包含了TagHeader的信息,因此,在推流时没必要附加上TagHeader,即实际发送的RTMP packet:
A/V data
|
添加上FLV的TagData头部分数据
|
添加包信息,用于底层分包使用
|
底层拆包发送
4.1 发送Metadata
onMetaData
为FLV的第一个Tag。在RTMP的网络和流通道建立完毕后,需要上层发送的第一个包就是Metadata包。Metadata包主要是键值对形式,指明音视频的格式和解码信息。详细可参见文末 FLV文件的第一个Tag: onMetaData 。
代码:
RTMPPacket packet;
RTMPPacket_Alloc(&packet, RTMP_METADATA_SIZE);
RTMPPacket_Reset(&packet);
packet.m_packetType = RTMP_PACKET_TYPE_INFO;
packet.m_nChannel = 0x04;
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = 0;
packet.m_nInfoField2 = _rtmp_ptr->m_stream_id;
/////////////////////////////////////////////
// Send media info
char *ptr = packet.m_body;
ptr = _put_byte(ptr, AMF_STRING);
ptr = _put_amf_string(ptr, "@setDataFrame");
ptr = _put_byte(ptr, AMF_STRING);
ptr = _put_amf_string(ptr, "onMetaData");
ptr = _put_byte(ptr, AMF_OBJECT);
ptr = _put_amf_string(ptr, "copyright");
ptr = _put_byte(ptr, AMF_STRING);
ptr = _put_amf_string(ptr, "firehood");
ptr = _put_amf_string(ptr, "width");
ptr = _put_amf_double(ptr, meta.width);
ptr = _put_amf_string(ptr, "height");
ptr = _put_amf_double(ptr, meta.height);
ptr = _put_amf_string(ptr, "framerate");
ptr = _put_amf_double(ptr, meta.fps);
ptr = _put_amf_string(ptr, "videodatarate");
ptr = _put_amf_double(ptr, meta.bitrate_kpbs);
double vcodec_ID = 7;
ptr = _put_amf_string(ptr, "videocodecid");
ptr = _put_amf_double(ptr, vcodec_ID);
if (meta.has_audio) {
ptr = _put_amf_string(ptr, "audiodatarate");
ptr = _put_amf_double(ptr, meta.datarate);
ptr = _put_amf_string(ptr, "audiosamplerate");
ptr = _put_amf_double(ptr, meta.samplerate);
ptr = _put_amf_string(ptr, "audiosamplesize");
ptr = _put_amf_double(ptr, meta.samplesperframe);
ptr = _put_amf_string(ptr, "stereo");
ptr = _put_amf_double(ptr, meta.channels);
double acodec_ID = 10;
ptr = _put_amf_string(ptr, "audiocodecid");
ptr = _put_amf_double(ptr, acodec_ID);
}
ptr = _put_amf_string(ptr, "");
ptr = _put_byte(ptr, AMF_OBJECT_END);
packet.m_nBodySize = ptr - packet.m_body;
if (RTMP_SendPacket(_rtmp_ptr, &packet, 0) < 0) {
RTMPPacket_Free(&packet);
return false;
}
- RTMP发送的参数设置使用
AMF/AMF3
编码方式;主要以键值对方式; - 由官方文档:H264编码ID为7,AAC编码ID为10;
- 音频可选。
4.2 发送视频
视频帧数据需要打上FLV的TagData头数据。详细可参见文末 librtmp获取视频流和音频流1 。视频包有两种,一种为视频同步数据 AVCDecoderConfigurationRecord
(解码信息包),一种为H264帧数据 One or more NALUs
(内容视频包)。
Video TagData:
Field | Type | Comment |
---|---|---|
Frame Type | UB [4] | Type of video frame. The following values are defined: 1 = key frame (for AVC, a seekable frame) 2 = inter frame (for AVC, a non-seekable frame) 3 = disposable inter frame (H.263 only) 4 = generated key frame (reserved for server use only) 5 = video info/command frame |
CodecID | UB [4] | Codec Identifier. The following values are defined: 2 = Sorenson H.263 3 = Screen video 4 = On2 VP6 5 = On2 VP6 with alpha channel 6 = Screen video version 2 7 = AVC |
AVCPacketType | IF CodecID == 7 UI8 |
The following values are defined: 0 = AVC sequence header 1 = AVC NALU 2 = AVC end of sequence (lower level NALU sequence ender is not required or supported) |
CompositionTime | IF CodecID == 7 SI24 |
IF AVCPacketType == 1 Composition time offset ELSE 0 See ISO 14496-12, 8.15.3 for an explanation of compositiontimes. The offset in an FLV file is always in milliseconds. |
F AVCPacketType == 0 AVCDecoderConfigurationRecord(AVC sequence header)
IF AVCPacketType == 1 One or more NALUs (Full frames are required)
4.2.1 发送视频信息包
/////////////////////////////////////////////
// Send decode info
// FLV video sequence format:
// Frame type(4 bits) + codecID(4 bits) + AVCPacketType(1 bytes) + CompositionTime + AVCDecoderConfiguration
//
uint32_t offset = 0;
packet.m_body[offset++] = 0x17;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = 0x00;
// AVCDecoderConfiguration
packet.m_body[offset++] = 0x01;
packet.m_body[offset++] = meta.param.data_sps[1];
packet.m_body[offset++] = meta.param.data_sps[2];
packet.m_body[offset++] = meta.param.data_sps[3];
packet.m_body[offset++] = 0xff;
// SPS
packet.m_body[offset++] = 0xE1;
packet.m_body[offset++] = meta.param.size_sps >> 8;
packet.m_body[offset++] = meta.param.size_sps & 0xff;
memcpy(&packet.m_body[offset], meta.param.data_sps, meta.param.size_sps);
offset += meta.param.size_sps;
// PPS
packet.m_body[offset++] = 0x01;
packet.m_body[offset++] = meta.param.size_pps >> 8;
packet.m_body[offset++] = meta.param.size_pps & 0xff;
memcpy(&packet.m_body[offset], meta.param.data_pps, meta.param.size_pps);
offset += meta.param.size_pps;
packet.m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet.m_nBodySize = offset;
if (RTMP_SendPacket(_rtmp_ptr, &packet, 0) < 0) {
RTMPPacket_Free(&packet);
return false;
}
- 包数据部分按
AVCDecoderConfiguration
格式即可。
4.2.2 发送视频数据包
RTMPPacket packet;
RTMPPacket_Alloc(&packet, size + RTMP_RESERVED_HEAD_SIZE * 2);
RTMPPacket_Reset(&packet);
packet.m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet.m_nChannel = 0x04;
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = pts;
packet.m_nInfoField2 = _rtmp_ptr->m_stream_id;
uint32_t offset = 0;
if (keyframe)
packet.m_body[offset++] = 0x17;
else
packet.m_body[offset++] = 0x27;
packet.m_body[offset++] = 0x01;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = 0x00;
packet.m_body[offset++] = size >> 24;
packet.m_body[offset++] = size >> 16;
packet.m_body[offset++] = size >> 8;
packet.m_body[offset++] = size & 0xff;
memcpy(packet.m_body + offset, data_ptr, size);
packet.m_nBodySize = offset + size;
if (RTMP_SendPacket(_rtmp_ptr, &packet, 0) < 0) {
RTMPPacket_Free(&packet);
return false;
}
RTMPPacket_Free(&packet);
return true;
4.3 发送音频
同样,音频也分为音频信息包和音频数据包。
Audio TagData:
名称 | 二进制值 | 介绍 |
---|---|---|
音频格式 | 4 bits[AAC]1010 | 0 = Linear PCM, platform endian 1 = ADPCM 2 = MP3 3 = Linear PCM, little endian 4 = Nellymoser 16-kHz mono 5 = Nellymoser 8-kHz mono 6 = Nellymoser 7 = G.711 A-law logarithmic PCM 8 = G.711 mu-law logarithmic PCM 9 = reserved 10 = AAC 11 = Speex 14 = MP3 8-Khz 15 = Device-specific sound |
采样率 | 2 bits[44kHZ]11 | 0 = 5.5-kHz 1 = 11-kHz 2 = 22-kHz 3 = 44-kHz 对于AAC总是3 |
采样的长度 | 1 bit | 0 = snd8Bit 1 = snd16Bit 压缩过的音频都是16bit |
音频类型 | 1 bit | 0 = sndMono,单声道 1 = sndStereo,立体声 对于AAC总是1 |
ACCPacketType | 8bit 00000000 只有SoundFormat == 10时才有此8bi的字段 | 0x00,表示音频同步包;0x01,表示音频raw数据。 |
AudioObjectType | 5bits [AAC LC]00010 | |
SampleRateIndex | 4bits [44100]0100 | |
ChannelConfig | 4bits [Stereo]0010 | |
FrameLengthFlag | 1bit | |
dependOnCoreCoder | 1bit 0 | |
extensionFlag | 1bit 0 |
4.3.1 发送音频信息包
音频信息包不额外发送也可以,如需发送需要根据上表生成,一般共4个字节(2bytes AACDecoderSpecificInfo和2bytesAudioSpecificConfig)。
4.3.2 发送音频数据包
RTMPPacket packet;
RTMPPacket_Alloc(&packet, size + RTMP_RESERVED_HEAD_SIZE * 2);
RTMPPacket_Reset(&packet);
packet.m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet.m_nChannel = 0x04;
packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
packet.m_nTimeStamp = pts;
packet.m_hasAbsTimestamp = 0;
packet.m_nInfoField2 = _rtmp_ptr->m_stream_id;
packet.m_body[0] = 0xAF;
packet.m_body[1] = 0x01;
memcpy(packet.m_body + 2, data_ptr, size);
packet.m_nBodySize = size + 2;
if (RTMP_SendPacket(_rtmp_ptr, &packet, FALSE) < 0) {
RTMPPacket_Free(&packet);
return false;
}
RTMPPacket_Free(&packet);
return true;
- RTMP包信息很多都是固定字段,使用时注意填充;
- 为了减少内存拷贝,
RTMPPacket
内存在分配时预留了RTMP_RESERVED_HEAD_SIZE
大小;这样,在底层拆包后填充包头时之间在预留内存部分填充就可以了。
references:
FLV文件的第一个Tag: onMetaData
librtmp获取视频流和音频流1
推荐阅读
-
如何搭建RTMP视频流媒体推流服务器进行视频推流?
-
流媒体之RTMP——librtmp推流测试
-
流媒体服务器RTMP推流EasyDSS遇到easydss: [emerg] getpwnam(“nobody”) failed错误的的排查方案
-
RTMP推流协议视频流媒体直播点播平台EasyDSS视频广场分类滚动样式优化示例
-
使用树莓派搭建Nginx-RTMP流媒体服务器实现ffmpeg推流
-
视频流媒体RTSP专用播放器RTSP拉流、RTMP推流方案之EasyPlayer-RTSP-Win抓图代码重构流程介绍
-
nginx + rtmp 搭建流媒体服务器,手机推流,vlc拉流
-
使用Nginx+nginx-rtmp-module+OBS推流搭建流媒体服务器
-
基于Nginx实现RTMP流媒体的推流和拉流方案
-
RTMP推流协议视频流媒体直播点播平台EasyDSS视频广场分类滚动样式优化示例