视频直播必备:掌握RTMP推流技术

发表时间: 2024-03-22 14:23

对应RTMP推流,业界有很多开源方案。如使用FFMPEG推流,librtmp(rtmp-dump),gstream推流。由于ffmpeg和gstreamer比较庞大,仅仅用来推流,有大炮打蚊子之嫌。针对客户端特别是瘦客户端,使用librtmp(rtmp-dump)方案更加精简,更加高效。

本方案基本思路:

  1. 下载并编译librtmp。

下载地址:http://rtmpdump.mplayerhq.hu/download/

编译成功后产生一个librtmp.so 库

2.调用librtmp,封装一个视频层Wrapper_RtmpLib.cpp,该类定义如下:

class Wrapper_RtmpLib{public:       Wrapper_RtmpLib(char * url);       ~Wrapper_RtmpLib();      int Open();      int SendData(char * data,int dataLength, unsigned int timeStamp,int debug = -1);      int IsConnect();      int Close();private:     int InitSockets();     void CleanupSockets();     int pushSPSPPS(char *sps, int spsLen, char *pps, int ppsLen, int m_stream_id,unsigned int timeStamp);     int pushVideoData(char *data, int dataLen, bool keyFrame, int m_stream_id,unsigned int timeStamp);     int GetStartPrixLen(char *Pack, int offest);     char * rtmpUrl = NULL;     RTMP * m_pRtmp = NULL;     NALU * CopyNALU(NALU * src);     void FreeNALU(NALU * nalu);};

Wrapper_RtmpLib对外提供RTMP推流接口。

基本使用步骤:

  1. 定义一个Wrapper_RtmpLib对象test
  2. Test.open(),与服务器建立rtmp信令相关连接
  3. int SendData(char * data,int dataLength, unsigned int timeStamp,int debug = -1);发送RTMP数据

注意data,必须是一个完整的NAL单元。所以应用程序调该接口前必须解析出NAL单元。

下面是一个h264裸文件推送RTMP过程。

粉丝福利, 免费领取C++音视频学习资料包+学习路线大纲、技术视频/代码,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs),有需要的可以进企鹅裙927239107领取哦~

#include <cstdio>#include"Wrapper_RtmpLib.hpp"#include <unistd.h>#include<string.h>#include <signal.h>#include<time.h>#define LEN_R 1400//检测启动码,并获取启动码的长度int GetStartCode(char *Pack, int offest){    int iStartPrexLen = 0;    if (Pack[offest] == '\x00' && Pack[offest + 1] == '\x00'&&Pack[offest + 2] == '\x00'&&Pack[offest + 3] == '\x01')    {        iStartPrexLen = 4;        return iStartPrexLen;    }    else if (Pack[offest] == '\x00' && Pack[offest + 1] == '\x00'&&Pack[offest + 2] == '\x01')    {        iStartPrexLen = 3;        return iStartPrexLen;    }    else    {        return iStartPrexLen;    }}#include <time.h>void delaytime(int ms){//    return;    struct timespec tvUec;    clock_gettime(CLOCK_MONOTONIC, &tvUec);    long long pretime = tvUec.tv_nsec / 1000000 + tvUec.tv_sec * 1000;    long long nowtime = tvUec.tv_nsec / 1000000 + tvUec.tv_sec * 1000;    while (1)    {        clock_gettime(CLOCK_MONOTONIC, &tvUec);        nowtime = tvUec.tv_nsec / 1000000 + tvUec.tv_sec * 1000;        if (nowtime - pretime > ms-10) //程序自身耗时,预估耗时10ms。实际网络流不要延时,仅供测试        {            return;        }    }}void help(char *p){    printf("Use:");    printf("%s h264_File RTMP_URL FRate  1111\n",p);}char NALBuff[1080 * 1920 * 8]={0};int NALLen = 0;int lastPos = 0;int pretime = 0;int NALCount = 0;int main(int argc,char*argv[]){    if(argc<4)    {        help(argv[0]);        return 1;    }    signal(SIGPIPE, SIG_IGN);    Wrapper_RtmpLib test(argv[2]);    if (test.Open() < 0)    {        printf("open is failed\n");        return 0;    }    if (test.IsConnect() < 0)    {        printf("connect is failed\n");        return 0;    }    else        printf("connect is ok\n");    char Pack[1500] = { 0 };    int ret = 0;    char *pStart = NULL;    char *pEnd = NULL;    char *pNALbuff = NALBuff;    unsigned int timestamp = 0;    int ioffset = 1000 /atoi(argv[3]);    char *pPack = Pack;    int iCurrentStartLen = 0;    int iPreStartLen = 0;    FILE *fp = NULL;    fp = fopen(argv[1], "rb+");    if (fp == NULL)    {        printf("open file is failed\n");        return -1;    }    while (1)    {        pStart = NULL;        pEnd = NULL;        ret = fread(Pack, LEN_R, 1, fp);        if (ret == 1)        {            //如果头4个字节恰好为 00 00 00 01  或者00 00 01            iCurrentStartLen = GetStartCode(Pack, 0);            if(iCurrentStartLen>0)            {                iPreStartLen = iCurrentStartLen;                pStart=&Pack[0];                pEnd = &Pack[0];                for (int i = 2; i < LEN_R; i++)                {                    iCurrentStartLen = GetStartCode(Pack, i);                    if (iCurrentStartLen > 0)                    {                        printf("##find nal start1\n");                        pEnd = &Pack[i];                        memmove(NALBuff+ NALLen, pStart, pEnd - pStart);//分离NAL拷贝到buffer                        NALLen += pEnd - pStart;                        NALCount++;                    }                    if (NALLen != 0)                    {                            int StartCodeLen = GetStartCode(NALBuff, 0);                            if (StartCodeLen <= 0)                            {                                printf("NAL buffer data error\n");                            }                            if ((NALBuff[StartCodeLen] & 0x1F) == 7) //sps需要等pps一起发                            {                                if (NALCount == 1)                                {                                    iPreStartLen = iCurrentStartLen;                                    i++;                                    pStart = pEnd;                                    continue;                                }                            }                            else if (( NALBuff[StartCodeLen] & 0x1F)== 5|| (NALBuff[StartCodeLen] & 0x1F) == 1)                            {                                timestamp = timestamp + ioffset;                                i++;                            }                            else                            {                                //不是我所关注的NAL类型,可以不往下送                                NALLen = 0;                                NALCount = 0;                                memset(NALBuff, 0, sizeof(NALBuff));                                iPreStartLen = iCurrentStartLen;                                i++;                                pStart = pEnd;                                continue;                            }                            ret =test.SendData(NALBuff, NALLen, timestamp, -1);                            if (ret < 0)                                printf("send is failed\n");                            NALLen = 0;                            NALCount = 0;                            memset(NALBuff, 0, sizeof(NALBuff));                            iPreStartLen = iCurrentStartLen;                            delaytime(ioffset);                    }                     pStart = pEnd;                }                //一个包中遗留半个NAL单元,找不到下一个头                //剩余的不完整NAL单元拷贝到临时buffer,后面凑齐一个NAL单元再发                    memmove(NALBuff + NALLen, pStart, (&Pack[LEN_R - 1] - pStart) + 1);//sps pps idr non-idr,拷贝到buffer                    NALLen += (&Pack[LEN_R - 1] - pStart + 1);            }            else  //如果头4个字节不是启动码            {                for (int i = 1; i < LEN_R; i++) //必须从2开始,因为可能存在00 00 01或00 00 00 01相邻出现                {                    //  pStart = &Pack[0];                    iCurrentStartLen = GetStartCode(Pack, i);                    if (iCurrentStartLen > 0)                    {                        printf("##find nal start2\n");                        pEnd = &Pack[i];                        if (pStart == NULL)                            pStart = &Pack[0];                        memmove(NALBuff + NALLen, pStart, pEnd - pStart);//sps pps idr non-idr,拷贝到buffer                        NALLen += pEnd - pStart;                        NALCount++;                        if (NALLen != 0)                        {                            int StartCodeLen = GetStartCode(NALBuff, 0);                            if (StartCodeLen <= 0)                            {                                printf("NAL buffer data error\n");                            }                            if ((NALBuff[StartCodeLen] & 0x1F) == 7) //sps需要等pps一起发                            {                                if (NALCount == 1)                                {                                    iPreStartLen = iCurrentStartLen;                                    i++;                                    pStart = pEnd;                                    continue;                                }                            }                            else if ((NALBuff[StartCodeLen] & 0x1F) == 5 || (NALBuff[StartCodeLen] & 0x1F) == 1)                            {                                timestamp = timestamp + ioffset;                            }                            else                            {                                //不是我所关注的NAL类型,可以不往下送                                NALLen = 0;                                NALCount = 0;                                memset(NALBuff, 0, sizeof(NALBuff));                                iPreStartLen = iCurrentStartLen;                                i++;                                pStart = pEnd;                                continue;                            }                                ret = test.SendData(NALBuff, NALLen, timestamp, -1);                                if (ret < 0)                                    printf("send data is failed\n");                                NALLen = 0;                                NALCount = 0;                                memset(NALBuff, 0, sizeof(NALBuff));                                delaytime(ioffset);                        }                        i++;                        iPreStartLen = iCurrentStartLen;                  }                    pStart = pEnd;                }                //整个包都不足一个NAL单元                if (pStart == pEnd)                {                    if (pStart == NULL)                    {                        pStart = &Pack[0];                    }                    pEnd = &Pack[LEN_R - 1];                    memmove(NALBuff + NALLen, pStart, pEnd - pStart + 1); //                        //lastPos = NALLen;                    NALLen += (pEnd - pStart + 1);                }            }        }        else        {            printf("read is failed endof stream\n");            break;        }    }    getchar();    printf("hello from rtmp!\n");    return 0;}

基本思路如下:

读文件----解析NAL单元---利用 SendData发送一个完成的NAL单元完成推流

编译main.cpp Wrapper_RtmpLib.cpp 并链接librtmp.so生成可执行文件h2642rtmp.

运行可执行程序推流

./h264tortmp avc.h264 rtmp://192.168.1.226:8085/live/1830562240700540100 25

使用该方案注意:

  1. SendData 必须是一个完整的NAL单元。如果是文件需要解析或网络流必须解析出NAL单元。
  2. 时间戳采用间隔时间。即时间戳按每帧时间间隔递增,可能因为网络抖动或者1000/帧率不是帧率会存在累计误差。该demo因为不存在音视频同步,时间戳影响不大。

3.如果是云主机,在云主机内不能推公网IP,而要推内网IP 192.168.1.226,客户端访问需要外网IP。

客户端播放效果如下:。