使用librtmp库进行推流与拉流

目前比较主流的直播技术有RTMP、HLS,其中RTMP主要基于TCP协议,HLS主要基于HTTP协议,二者在实施成本、延迟性等方面有较大差异。本文主要讲解RTMP的推流与拉流技术的应用。

理论知识:

原则上说,开发RTMP的推拉流应用,除具备基本的语言及工具链外,还需要掌握以下知识:
a) RTMP协议原理
b) 基本的音视频编解码知识
c) FLV等流媒体封装格式技巧

RTMP协议原理:

与大多的协议格式一样,RTMP也遵循TLV范式,以及其远程过程调用的指令构造格式AMF。

RTMP基于TCP,在TCP三次握手完成后,RTMP也定义了自己的六次握手,主要用于版本适配、RTT计算,解析这块需要较大篇幅,感兴趣的朋友请百度搜索“RTMP协议详解”。

完成握手之后,传输的报文都基于RTMP分块格式,如下:

+---------------------+-----------------------------------+----------------------+----------------------+
 |  1~3字节基本块头    |        0~11字节的消息块头            | 可选4字节扩展时间戳 |      变长消息负载        |
+---------------------+-----------------------------------+----------------------+----------------------+

采用分块技术原于分时复用的思想,不同的上层消息交错地在一条TCP链路中传输,通常一个块不会太长,避免因为某一个应用消息过多的占用网络资源而造成其它应用饿死。

基本块头介绍:
基本块头定义了两个关键要素:消息块头格式、块流ID。

先看基本块头的第一字节表示:

+-位7-+-位6-+-位5-+-位4-+-位3-+-位2-+-位1-+-位0-+
 |   块头格式   |             块流ID                                      |
+-------------+------------------------------------------+

块头格式占2个位,有4种表示:
00 —— 11字节完整的消息块头。
01 —— 7字节中等消息块头。
10 —— 3字节小型消息块头。
11 —— 0字节无消息块头。

首字节块流ID占6个位,有0~63种表示,每种表示一个块流通道。为了扩展更多的字节表示块流通道,首字节的块流ID被划出0和1两个值用于表示扩展1或2个字节。
扩展1字节的表示法:

+-------------+--------------------+----------------------+
 |   块头格式   |           0                 |           扩展1字节       |
+-------------+--------------------+----------------------+

扩展2字节的表示法:

+-------------+--------------------+----------------------+----------------------+
 |   块头格式   |           1                 |                          扩展2字节                          |
+-------------+--------------------+----------------------+----------------------+

扩展1字节可以表示64+255=319个块流通道。
扩展1字节可以表示64+65525=65599个块流通道。

消息块头介绍:

11字节完整的消息块头格式:

+------------------+------------------+------------------+------------------+
 |                             3字节绝对时间戳                            |    负载消息长度   |
+------------------+------------------+------------------+------------------+
|                3字节负载消息长度           |    1字节消息类型  |        块流ID         |
+------------------+------------------+------------------+------------------+
|                               4字节块流ID                                 |
+------------------+------------------+------------------+

7字节中等消息块头:

+------------------+------------------+------------------+------------------+
 |                             3字节相对时间戳                            |    负载消息长度   |
+------------------+------------------+------------------+------------------+
|                3字节负载消息长度           |    1字节消息类型  |
+------------------+------------------+------------------+

3字节小型消息块头:

+------------------+------------------+------------------+
 |                             3字节相对时间戳                            | 
+------------------+------------------+------------------+

对于不完整的消息块头,省略的字段表示与上次的相同,这可以起到很好的压缩传输效果。

可选扩展时间戳:
只有当块消息头中的普通时间戳设置为0x00ffffff时,本字段才被传送。

变长消息负载:

在RTMP协议规范中在块协议基础上又进一步定义了消息协议,格式如下:

+------------------+------------------+------------------+------------------+
 |  1字节消息类型   |                          3字节负载消息长度                             |
+------------------+------------------+------------------+------------------+
|                                              4字节时间戳                                              |
+------------------+------------------+------------------+------------------+
|                               3字节消息流ID                              |
+------------------+------------------+------------------+

消息协议仍然遵循TLV范式,这种套娃式的结构在协议设计模式中也是主流,然而令疑惑的是,消息头的字段设计,与块头过于重合,并且还定义了一个消息流ID。官方对此的解释是,消息头旨在规范开发者应用层消息的设计,与块头属于两个不同的维度,各字段既使同名也互不影响。事实上,如果我们深入分析librtmp的实现,或者抓包分析RTMP的拉流过程,这个消息头规范是没有被采纳的。

基本的音视频编解码知识:

流媒体传输的音视频格式一般采用主流的压缩协议,如音频采用AAC、MP3,视频采用H264、MPEG4。对直播推流来说,我们需要从摄像头抓取YUV或者Bayer格式的视频,从声卡抓取PCM格式的音频,这些数据都是没有经过压缩的,数据量非常大,不便于网络传输。使用压缩协议执行压缩与解压缩的过程称为编解码。编解码可以使用当下流行的FFMpeg工具,开发者遵循其接口调用规范,将原始音视频数据打包,交给指定的编码模块,生成压缩后的编码帧。压缩后的数据大小通常不到原始数据的1/10,对视频来说,由于采用了P帧和B帧前后向预测技术,压缩效率更高。编码帧通过网络传输到目的端如播放器,交给解码器解码还原,当然,通常采用的编码算法都是有损的,解码后的音视频数据既使不能还原到100%的程度,由于人脑视觉和听觉系统的特殊性,大多数情况下仍然不会影响我们对消息的解读。

FLV等流媒体封装格式技巧:

编码器输出的裸音视频数据是不能直接用于推拉流的,RTMP直播通常采用FLV格式,FLV格式要求:
对于音频数据,在裸数据之前,必须增加1个字节的元格式,这个格式定义了采样率、采样精度、通道布局,对于AAC格式,还需要增加ADTS头。
对于视频数据,在裸数据之前,也必须增加1个字节的元格式,这个格式定义了编码器、帧格式,对于H264,还需要增加SPS和PPS。

librtmp库接口介绍:

官网下载:http://rtmpdump.mplayerhq.hu/download

结构定义:

与开发者最直接相关结构包括:

RTMP报文格式:

  typedef struct RTMPPacket
  {
    uint8_t m_headerType;   // 块头类型
    uint8_t m_packetType;   // 负载格式
    uint8_t m_hasAbsTimestamp;   // 是否绝对时间戳
    int m_nChannel;   // 块流ID
    uint32_t m_nTimeStamp;    // 时间戳
    int32_t m_nInfoField2;    // 块流ID
    uint32_t m_nBodySize;   // 负载大小
    uint32_t m_nBytesRead;   // 读入负载大小
    RTMPChunk *m_chunk;  // 在RTMP_ReadPacket()调用时,若该字段非NULL,表示关心原始块的信息,通常设为NULL
    char *m_body;   // 负载指针
  } RTMPPacket;

RTMP上下文格式:

  typedef struct RTMP
  {
    int m_inChunkSize;    // 最大接收块大小
    int m_outChunkSize;    // 最大发送块大小
    int m_nBWCheckCounter;    // 带宽检测计数器
    int m_nBytesIn;    // 接收数据计数器
    int m_nBytesInSent;    // 当前数据已回应计数器
    int m_nBufferMS;    // 当前缓冲的时间长度,以MS为单位
    int m_stream_id;    // 当前连接的流ID
    int m_mediaChannel;    // 当前连接媒体使用的块流ID
    uint32_t m_mediaStamp;    // 当前连接媒体最新的时间戳
    uint32_t m_pauseStamp;    // 当前连接媒体暂停时的时间戳
    int m_pausing;    // 是否暂停状态
    int m_nServerBW;    // 服务器带宽
    int m_nClientBW;    // 客户端带宽
    uint8_t m_nClientBW2;    // 客户端带宽调节方式
    uint8_t m_bPlaying;    // 当前是否推流或连接中
    uint8_t m_bSendEncoding;    // 连接服务器时发送编码
    uint8_t m_bSendCounter;    // 设置是否向服务器发送接收字节应答

    int m_numInvokes;    // 0x14命令远程过程调用计数
    int m_numCalls;    // 0x14命令远程过程请求队列数量
    RTMP_METHOD *m_methodCalls;    // 远程过程调用请求队列

    RTMPPacket *m_vecChannelsIn[RTMP_CHANNELS];    // 对应块流ID上一次接收的报文
    RTMPPacket *m_vecChannelsOut[RTMP_CHANNELS];    // 对应块流ID上一次发送的报文
    int m_channelTimestamp[RTMP_CHANNELS];    // 对应块流ID媒体的最新时间戳

    double m_fAudioCodecs;    // 音频编码器代码
    double m_fVideoCodecs;    // 视频编码器代码
    double m_fEncoding;         /* AMF0 or AMF3 */

    double m_fDuration;    // 当前媒体的时长

    int m_msgCounter;    // 使用HTTP协议发送请求的计数器
    int m_polling;    // 使用HTTP协议接收消息主体时的位置
    int m_resplen;    // 使用HTTP协议接收消息主体时的未读消息计数
    int m_unackd;    // 使用HTTP协议处理时无响应的计数
    AVal m_clientID;    // 使用HTTP协议处理时的身份ID

    RTMP_READ m_read;    // RTMP_Read()操作的上下文
    RTMPPacket m_write;    // RTMP_Write()操作使用的可复用报文对象
    RTMPSockBuf m_sb;    // RTMP_ReadPacket()读包操作的上下文
    RTMP_LNK Link;    // RTMP连接上下文
  } RTMP;
函数定义:

关于返回值为int类型的函数,大多数其实是bool语义,即1表示成功,0表示失败,但是仍然也有少部分函数可以返回负值。

RTMP报文操作:

// 重置报文
void RTMPPacket_Reset(RTMPPacket *p);
// 为报文分配负载空间
int RTMPPacket_Alloc(RTMPPacket *p, int nSize);
// 释放负载空间
void RTMPPacket_Free(RTMPPacket *p);

// 检查报文是否可读,当报文被分块,且接收未完成时不可读
#define RTMPPacket_IsReady(a)   ((a)->m_nBytesRead == (a)->m_nBodySize)

地址解析操作:

// 解析流地址
int RTMP_ParseURL(const char *url, int *protocol, AVal *host, unsigned int *port, AVal *playpath, AVal *app);

媒体缓存时长设置操作:

// 连接前,设置服务器发送给客户端的媒体缓存时长
void RTMP_SetBufferMS(RTMP *r, int size);
// 连接后,更新服务器发送给客户端的媒体缓存时长
void RTMP_UpdateBufferMS(RTMP *r);

RTMP播放地址及上下文选项操作:

// 更新RTMP上下文中的相应选项
int RTMP_SetOpt(RTMP *r, const AVal *opt, AVal *arg);
// 设置流地址
int RTMP_SetupURL(RTMP *r, char *url);
// 设置RTMP上下文播放地址和相应选项,不关心的可以设为NULL
void RTMP_SetupStream(RTMP *r, int protocol,
                        AVal *hostname,
                        unsigned int port,
                        AVal *sockshost,
                        AVal *playpath,
                        AVal *tcUrl,
                        AVal *swfUrl,
                        AVal *pageUrl,
                        AVal *app,
                        AVal *auth,
                        AVal *swfSHA256Hash,
                        uint32_t swfSize,
                        AVal *flashVer,
                        AVal *subscribepath,
                        int dStart,
                        int dStop, int bLiveStream, long int timeout);

RTMP连接及握手操作:

// 客户端连接及握手
int RTMP_Connect(RTMP *r, RTMPPacket *cp);
// 服务端握手
int RTMP_Serve(RTMP *r);

收发报文操作:

// 接收一个报文
int RTMP_ReadPacket(RTMP *r, RTMPPacket *packet);
// 发送一个报文,queue为1表示当包类型为0x14时,将加入队列等待响应
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue);
// 直接发送块
int RTMP_SendChunk(RTMP *r, RTMPChunk *chunk);

其他操作,不分类了,解释如下:

// 检查网络是否连接
int RTMP_IsConnected(RTMP *r);
// 返回套接字
int RTMP_Socket(RTMP *r);
// 检查连接是否超时
int RTMP_IsTimedout(RTMP *r);

// 获取当前媒体的时长
double RTMP_GetDuration(RTMP *r);

// 暂停与播放切换控制
int RTMP_ToggleStream(RTMP *r);

// 连接流,并指定开始播放的位置
int RTMP_ConnectStream(RTMP *r, int seekTime);
// 重新创建流
int RTMP_ReconnectStream(RTMP *r, int seekTime);
// 删除当前流
void RTMP_DeleteStream(RTMP *r);

// 获取第一个媒体包
int RTMP_GetNextMediaPacket(RTMP *r, RTMPPacket *packet);

// 处理客户端的报文交互,即处理报文分派逻辑
int RTMP_ClientPacket(RTMP *r, RTMPPacket *packet);

// 分配RTMP上下文
RTMP *RTMP_Alloc(void);
// 初使化RTMP上下文,设默认值
void RTMP_Init(RTMP *r);
// 关闭RTMP上下文
void RTMP_Close(RTMP *r);
// 释放RTMP上下文
void RTMP_Free(RTMP *r)

// 开启客户端的RTMP写开关,用于推流
void RTMP_EnableWrite(RTMP *r);

// 返回RTMP的版本
int RTMP_LibVersion(void);

// 开启RTMP工作中断
void RTMP_UserInterrupt(void);

// 发送0x04号命令的控制消息
int RTMP_SendCtrl(RTMP *r, short nType, unsigned int nObject, unsigned int nTime);

// 发送0x14号远程调用控制暂停
int RTMP_SendPause(RTMP *r, int DoPause, int dTime);
int RTMP_Pause(RTMP *r, int DoPause);

// 递归在一个对象中搜索指定的属性
int RTMP_FindFirstMatchingProperty(AMFObject *obj, const AVal *name, AMFObjectProperty * p);

// 底层套接口的网络读取、发送、关闭连接操作
int RTMPSockBuf_Fill(RTMPSockBuf *sb);
int RTMPSockBuf_Send(RTMPSockBuf *sb, const char *buf, int len);
int RTMPSockBuf_Close(RTMPSockBuf *sb);

// 发送建流操作
int RTMP_SendCreateStream(RTMP *r);

// 发送媒体时间定位操作
int RTMP_SendSeek(RTMP *r, int dTime);

// 发送设置服务器应答窗口大小操作
int RTMP_SendServerBW(RTMP *r);
// 发送设置服务器输出带宽操作
int RTMP_SendClientBW(RTMP *r);

// 删除0x14命令远程调用队列中的请求
void RTMP_DropRequest(RTMP *r, int i, int freeit);

// 读取FLV格式数据
int RTMP_Read(RTMP *r, char *buf, int size);
// 发送FLV格式数据
int RTMP_Write(RTMP *r, const char *buf, int size);

推流用法:

推流流程:

步骤:

  1. 初使化RTMP上下文
  2. 设置推流地址
  3. 开启推流标志
  4. 连接服务器
  5. 连接流地址
  6. 若从文件推流,循环读TAG,组织报文发送
  7. 若发送太快,适当做延迟
  8. 推流完毕,释放资源
简单例子:

下面这个例子演示了使用librtmp库将本地flv文件推流到服务器,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

#include <librtmp/rtmp.h>

// 大小端字节序转换
#define HTON16(x) ( (x >> 8 & 0x00FF) | (x << 8 & 0xFF00) )
#define HTON24(x) ( (x >> 16 & 0x0000FF) | (x & 0x00FF00) | (x << 16 & 0xFF0000) )
#define HTON32(x) ( (x >> 24 & 0x000000FF) | (x >> 8 & 0x0000FF00) | (x << 8 & 0x00FF0000) | (x << 24 & 0xFF000000) )
#define HTONTIME(x) ( (x >> 16 & 0x000000FF) | (x & 0x0000FF00) | (x << 16 & 0x00FF0000) | (x & 0xFF000000) )

// 从文件读取指定字节
bool ReadFP(char* pBuf, int nSize, FILE* pFile)
{
    return (fread(pBuf, 1, nSize, pFile) == nSize);
}

// 从文件读取1个字节整数
bool ReadU8(uint8_t* u8, FILE* fp)
{
    return ReadFP((char*)u8, 1, fp);
}

// 从文件读取2个字节整数
bool ReadU16(uint16_t* u16, FILE* fp)
{
    if (!ReadFP((char*)u16, 2, fp))
        return false;

    *u16 = HTON16(*u16);
    return true;
}

// 从文件读取3个字节整数
bool ReadU24(uint32_t* u24, FILE* fp)
{
    if (!ReadFP((char*)u24, 3, fp))
        return false;

    *u24 = HTON24(*u24);
    return true;
}

// 从文件读取4个字节整数
bool ReadU32(uint32_t* u32, FILE* fp)
{
    if (!ReadFP((char*)u32, 4, fp))
        return false;

    *u32 = HTON32(*u32);
    return true;
}

// 从文件读取4个字节时间戳
bool ReadTime(uint32_t* utime, FILE* fp)
{
    if (!ReadFP((char*)utime, 4, fp))
        return false;

    *utime = HTONTIME(*utime);
    return true;
}

// 从文件预读1个字节整数
bool PeekU8(uint8_t* u8, FILE* fp)
{
    if (!ReadFP((char*)u8, 1, fp))
        return false;

    fseek(fp, -1, SEEK_CUR);
    return true;
}

int main(int argc, char* argv[])
{
    FILE* pFile = fopen("1.flv", "rb");

    // 初使化RTMP上下文
    RTMP* pRTMP = RTMP_Alloc();
    RTMP_Init(pRTMP);

    // 设置推流地址
    pRTMP->Link.timeout = 10;
    RTMP_SetupURL(pRTMP, (char*)"rtmp://127.0.0.1:1935/live/a");

    // 开启推流标志
    RTMP_EnableWrite(pRTMP);

    // 连接服务器
    bool b = RTMP_Connect(pRTMP, NULL);
    if (!b)
    {
        printf("connect failed! \n");
        return -1;
    }
        
    // 连接流地址
    b = RTMP_ConnectStream(pRTMP, 0);
    if (!b)
    {
        printf("connect stream failed! \n");
        return -1;
    }

    // 跳过FLV文件头的13个字节
    fseek(pFile, 9, SEEK_SET);
    fseek(pFile, 4, SEEK_CUR);

    // 初使化RTMP报文
    RTMPPacket packet;
    RTMPPacket_Reset(&packet);
    packet.m_body = NULL;
    packet.m_chunk = NULL;

    packet.m_nInfoField2 = pRTMP->m_stream_id;

    uint32_t starttime = RTMP_GetTime();

    while (true)
    {
        // 读取TAG头

        uint8_t type = 0;
        if (!ReadU8(&type, pFile))
            break;

        uint32_t datalen = 0;
        if (!ReadU24(&datalen, pFile))
            break;

        uint32_t timestamp = 0;
        if (!ReadTime(&timestamp, pFile))
            break;

        uint32_t streamid = 0;
        if (!ReadU24(&streamid, pFile))
            break;

/*
        // 跳过0x12 Script
        if (type != 0x08 && type != 0x09)
        {
            fseek(pFile, datalen + 4, SEEK_CUR);
            continue;
        }
*/

        RTMPPacket_Alloc(&packet, datalen);

        if (fread(packet.m_body, 1, datalen, pFile) != datalen)
            break;

        // 组织报文并发送
        packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
        packet.m_packetType = type;
        packet.m_hasAbsTimestamp = 0;
        packet.m_nChannel = 6;
        packet.m_nTimeStamp = timestamp;
        packet.m_nBodySize = datalen;

        if (!RTMP_SendPacket(pRTMP, &packet, 0))
        {
            printf("Send Error! \n");
            break;
        }

        printf("send type:[%d] timestamp:[%d] datasize:[%d] \n", type, timestamp, datalen);

        // 跳过PreTag
        uint32_t pretagsize = 0;
        if (!ReadU32(&pretagsize, pFile))
            break;

        // 延时,避免发送太快
        uint32_t timeago = (RTMP_GetTime() - starttime);
        if (timestamp > 1000 && timeago < timestamp - 1000)
        {
            printf("sleep...\n");
            usleep(100000);
        }

        RTMPPacket_Free(&packet);
    }

    // 关闭连接,释放RTMP上下文
    RTMP_Close(pRTMP);
    RTMP_Free(pRTMP);

    fclose(pFile);

    return 0;
}

运行输出:

g++ -o testrtmp2 testrtmp2.cpp -lrtmp
./testrtmp2
send type:[18] timestamp:[0] datasize:[371] 
send type:[8] timestamp:[0] datasize:[209] 
send type:[9] timestamp:[25] datasize:[9838] 
send type:[8] timestamp:[26] datasize:[210] 
send type:[8] timestamp:[52] datasize:[210] 
send type:[8] timestamp:[78] datasize:[210] 
send type:[9] timestamp:[88] datasize:[11181] 
send type:[8] timestamp:[104] datasize:[210] 
send type:[8] timestamp:[131] datasize:[210] 
send type:[9] timestamp:[150] datasize:[11820] 
send type:[8] timestamp:[157] datasize:[210] 
send type:[8] timestamp:[183] datasize:[210] 
send type:[8] timestamp:[209] datasize:[210] 
send type:[9] timestamp:[213] datasize:[11995] 
send type:[8] timestamp:[235] datasize:[210] 
send type:[8] timestamp:[261] datasize:[210] 
send type:[9] timestamp:[275] datasize:[11809] 
send type:[8] timestamp:[287] datasize:[210] 
send type:[8] timestamp:[313] datasize:[210] 
send type:[9] timestamp:[338] datasize:[6124] 
send type:[8] timestamp:[340] datasize:[210] 
send type:[8] timestamp:[366] datasize:[210] 
send type:[8] timestamp:[392] datasize:[210] 
send type:[9] timestamp:[400] datasize:[4500] 
send type:[8] timestamp:[418] datasize:[210] 
send type:[8] timestamp:[444] datasize:[210] 
send type:[9] timestamp:[463] datasize:[2850] 
send type:[8] timestamp:[470] datasize:[210] 

拉流用法:

拉流流程:

步骤:

  1. 初使化RTMP上下文
  2. 设置拉流地址
  3. 连接服务器
  4. 连接流地址
  5. 循环拉流,提取媒体数据,保存为文件或者交给解码模块
  6. 拉流完毕,释放资源
简单例子:

下面这个例子演示了使用librtmp库从服务器拉流到本地保存为flv/mp3文件,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include <librtmp/rtmp.h>

int main(int argc, char* argv[])
{
    // 初使化RTMP上下文
    RTMP* pRTMP = RTMP_Alloc();
    RTMP_Init(pRTMP);

    // 设置拉流地址
    RTMP_SetupURL(pRTMP, (char*)"rtmp://127.0.0.1:1935/live/a");

    // 连接服务器
    pRTMP->Link.timeout = 10;
    pRTMP->Link.lFlags |= RTMP_LF_LIVE;
    bool b = RTMP_Connect(pRTMP, NULL);
    if (!b)
    {
        printf("connect failed! \n");
        return -1;
    }

    // 连接流地址
    b = RTMP_ConnectStream(pRTMP, 0);
    if (!b)
    {
        printf("connect stream failed! \n");
        return -1;
    }

    bool bSaveMP3 = true;
    FILE* pFile = fopen(bSaveMP3 ? "testrtmp.mp3" : "testrtmp.flv", "wb");

    while (RTMP_IsConnected(pRTMP))
    {
        if (bSaveMP3)
        {
            RTMPPacket packet;
            RTMPPacket_Reset(&packet);
            packet.m_body = NULL;
            packet.m_chunk = NULL;
            b = RTMP_ReadPacket(pRTMP, &packet);

            if (!b)
                break;

            if (!RTMPPacket_IsReady(&packet))
                continue;

            printf("\t headerType:[%d] \n", packet.m_headerType);
            printf("\t packetType:[%d] \n", packet.m_packetType);
            printf("\t hasAbsTimestamp:[%d] \n", packet.m_hasAbsTimestamp);
            printf("\t nChannel:[%d] \n", packet.m_nChannel);
            printf("\t nTimeStamp:[%d] \n", packet.m_nTimeStamp);
            printf("\t nInfoField2:[%d] \n", packet.m_nInfoField2);
            printf("\t nBodySize:[%d] \n", packet.m_nBodySize);
            printf("\t nBytesRead:[%d] \n", packet.m_nBytesRead);

            //fwrite(packet.m_body, 1, packet.m_nBodySize, pFile);
            //fwrite("AAAAAAAAAAAAAAAA", 1, 16, pFile);

            if (packet.m_packetType == 0x08)
            {
                fwrite(packet.m_body + 1, 1, packet.m_nBodySize - 1, pFile);
            }

            RTMPPacket_Free(&packet);
        }
        else
        {
            char sBuf[4096] = {0};
            int bytes = RTMP_Read(pRTMP, sBuf, sizeof(sBuf));
            printf("RTMP_Read() ret:[%d] \n", bytes);

            if (bytes <= 0)
                break;

            fwrite(sBuf, 1, bytes, pFile);
        }
    }

    fclose(pFile);
  
    RTMP_Close(pRTMP);
    RTMP_Free(pRTMP);

    return 0;
}

运行输出:

g++ -o testrtmp testrtmp.cpp -lrtmp
./testrtmp
RTMP_Read() ret:[640] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[4096] 
RTMP_Read() ret:[4096] 
RTMP_Read() ret:[1304] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[952] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[1705] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[1142] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[225] 
RTMP_Read() ret:[1957] 
RTMP_Read() ret:[225] 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容