通过librtmp将RTMP流转换为FLV文件,H264文件,AAC文件
网上很多RTMP写FLV,都是通过RTMP_Read方法循环读来完成的
但这样得到的FLV文件实际是不规范的,因为它没有FLV Tag Header,只是依靠播放器的自动识别功能来完成播放的
如果把这样的数据,交给一些不够强大的播放器,或者交给解析工具,或程序员编写的代码去使用,大概率是会报错的
传统的FLV大多使用AMFArray来表示AMF2中的属性,但现在也有一些服务器,会使用AMFObject来表示多个属性
一些FLV分析工具,只支持AMFArray格式,用这些工具来解析AMFObject格式的Script Packet就会报错,这是工具问题
接下来我们来讲解正规的处理办法
引用的第三方库
librtmp,用于读取和解析RTMP流数据
RTMP流结构分析
服务器先发过来一个ScriptPacket,然后再发送AudioPacket和VideoPacket,ScriptPacket一般只有一个,用于存储音视频参数
在所有的AudioPacket当中,首个Packet是AudioConfigPacket,用于存储音频参数,AudioConfigPacket一般只有一个,剩下的都是AudioRawDataPacket
在所有的VideoPacket当中,首个Packet是VideoConfigPacket,用于存储视频参数,VideoConfigPacket一般只有一个,剩下的都是VideoRawDataPacket
RTMPPacket.Body中的数据,和FLVTag.Body中的数据是完全一样的,可以直接写入FLV文件,如果想要写入H264和AAC,则需要进一步拆解
FLV文件写入流程
FLV文件是由FLVHeader + TagSize0 + Tag1 + TagSize1 + … + TagN + TagSizeN这些部分组成的,我们逐个写入这些部分,就能生成FLV文件
FLVHeader由Signature+Version+DataTye+HeaderLength等字节组成,可以手动逐个字节拼接
TagSize0占4个字节,所有字节都是0
TagN就是第N个RTMPPacket的Body部分,直接拷贝即可
TagSizeN是第N个RTMPPacket的BodySize,占4个字节
以上所有部分拼接完成,直接写入文件,即可得到一个可播放的FLV文件
H264文件写入流程
H264文件的格式一般为SPS帧 + PPS帧 + I帧 + 若干P帧 + … + I帧 + 若干P帧,SPS+PPS一般出现在I帧前面,可能一次,也可能多次
VideoConfigPacket的Body格式一般为CodecId + FrameType + DataType + 若干SPS + 若干PPS
VideoRawDataPacket的Body格式一般为CodecId + FrameType + DataType + H264RawData(IFrame/PFrame)
SPS部分的格式为SpsNum + SpsLength + SpsData + SpsLength + SpsData + …,PPS同理
VideoPacket的第1个字节,可以判断出编码类型(PCM/H264等)和帧类型(关键帧/参照帧),通过第2个字节,可以判断出数据类型(视频参数/视频裸数据)
如果是Packet中存储的是VideoConfig,则按照VideoConfigPacket的格式,解析出SPSData和PPSData数据,写入H264文件
在H264文件中,SPS帧+PPS帧的格式为,StartCode + SPS + StartCode + PPS 如果是Packet中存储的是VideoRawData,则按照VideoRawDataPacket的格式,解析出IFrame/PFrame数据,写入H264文件
在H264文件中,IFrame/PFrame的格式为,StartCode + H264RawData
以上字节按顺序写入文件,就能得到一个可播放的H264文件
注意,H264存的只是视频裸数据,不是用于发布的文件格式,它是没有时间戳的,只有像FLV这样容器类的文件,才会有时间戳
AAC文件写入流程
AAC文件的格式一般为AAC头 + AAC裸数据 + AAC头 + AAC裸数据 + …
AAC头有两种格式,一种是ADTS(每个裸数据前都有Header,适合流传输),一种是ADIF(只有一个统一的AAC头,在文件最前面,适合文件传输),我们这里用的是ADTS
AudioPacket的Body格式为AudioInfo + PacketType + ConfigData/RawData
通过AudioConfigPacket中的ConfigData,就可以计算出ADTS,具体请看代码,字段较多,但逻辑很简单
AudioRawDataPacket中的RawData,就是AAC文件中的裸数据,直接拷贝即可
所有裸数据单元的AAC头都是一样的,按顺序写入文件,就能得到一个可播放的FLV文件
实现代码
//RtmpPlayer.cpp
#include "base/Bases.h"
#include "RtmpConfigParser.h"
#include "rtmp.h"
RTMP *rtmp = nullptr;
char *url = nullptr;
pthread_t pidPush = 0;
bool playing = false;
FILE *h264File;
FILE *aacFile;
FILE *flvFile;
AudioSpecificConfig audioSpecificConfig = {};
bool _prepare();
bool _read();
//传统方法保存RTMP为FLV文件
//这种方法是不规范的,本质上是直接保存RTMP流,而不是FLV文件
//因为一些专业的播放器,能够智能识别格式,所以才能正常播放
//如果交给分析工具,或者开发者自己写的播放客户端,大概率是会出问题的
void writeToFlv() {
//写FLV
char *buffer = (char *) malloc(1024 * 1024);
while (playing) {
int len = RTMP_Read(rtmp, buffer, 1024 * 1024);
if (len > 0)
fwrite(buffer, len, 1, flvFile);
}
fflush(flvFile);
}
//写FLV-TAG
void writeFlvTag(uint8_t tagType, uint32_t tagSize, uint32_t bodySize, uint32_t timestamp, char *body) {
unsigned char *pBodySize = (unsigned char *) &bodySize;
unsigned char *pTimestamp = (unsigned char *) ×tamp;
unsigned char *pTagSize = (unsigned char *) &tagSize;
const unsigned char tagHeader[] = {
tagType, //TAG Type,0x08表示音频,0x09表示视频,0x12表示脚本
*(pBodySize + 2), *(pBodySize + 1), *pBodySize, //Body Size,按字节拷贝bodySize
*(pTimestamp + 2), *(pTimestamp + 1), *pTimestamp, //Timestamp,按字节拷贝timestamp
0x00, //Timestamp Extended,时间戳扩展
0x00, 0x00, 0x00 //StreamID,永远为0
};
const unsigned char tagSizeBytes[] = {
*(pTagSize + 3), *(pTagSize + 2), *(pTagSize + 1), *pTagSize
};
fwrite(tagHeader, 11, 1, flvFile);
fwrite(body, bodySize, 1, flvFile);
fwrite(tagSizeBytes, 4, 1, flvFile);
fflush(flvFile);
}
//JNI接口加载完毕
//System.loadLibrary函数被调用时,会触发此方法
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNI::jvm = vm;
//取流的同时,将流媒体数据同时写入三个文件
//H264视频文件,AAC音频文件,FLV混合文件
h264File = fopen("sdcard/1.h264", "wb");
aacFile = fopen("sdcard/1.aac", "wb");
flvFile = fopen("sdcard/1.flv", "wb");
return JNI_VERSION_1_6;
}
//JNI接口被JVM回收时,会触发此方法
extern "C" JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *reserved) {
//删除Java回调
delete Pusher::java;
Pusher::java = nullptr;
}
extern "C"
JNIEXPORT void JNICALL
Java_easing_android_media_RtmpPlayer_RtmpPlayer_native_1initialize(JNIEnv *env, jobject interface, jstring tag) {
//设置日志标签
JNIPrivate::TAG = JNI::toConstChar(tag);
//记录JNI环境
JNI::env = env;
JNI::interface = env->NewGlobalRef(interface);
//创建JavaCaller,用于回调Java层方法
Pusher::java = new JavaCaller();
//stdio重定向到logcat
JNI::stdioToLogcat();
std::cout
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?