赞
踩
通过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 << "initialized" << std::endl; } extern "C" JNIEXPORT void JNICALL Java_easing_android_media_RtmpPlayer_RtmpPlayer_native_1prepare(JNIEnv *env, jobject interface, jstring jUrl) { //jstring地址转char*地址 const char *constUrl = JNI::toConstChar(jUrl); url = JNI::toChar(constUrl); //准备播放环境 playing = false; bool ret = _prepare(); if (!ret) { std::cout << "prepare fail" << std::endl; return; } playing = true; //创建推流线程 pthread_create(&pidPush, nullptr, [](void *context) -> void * { _read(); return nullptr; }, nullptr); } extern "C" JNIEXPORT void JNICALL Java_easing_android_media_RtmpPlayer_RtmpPlayer_native_1release(JNIEnv *env, jobject interface) { playing = false; //等待推流结束 pthread_join(pidPush, nullptr); //销毁RTMP if (rtmp) { RTMP_Close(rtmp); RTMP_Free(rtmp); } //销毁URL delete (url); } //连接服务器 bool _prepare() { //连接RTMP rtmp = RTMP_Alloc(); RTMP_Init(rtmp); rtmp->Link.timeout = 30; RTMP_SetupURL(rtmp, url); rtmp->Link.lFlags |= RTMP_LF_LIVE; RTMP_SetBufferMS(rtmp, 1 * 1000); RTMP_Connect(rtmp, nullptr); RTMP_ConnectStream(rtmp, 0); return true; } //读取流数据,并保存为H264和AAC bool _read() { bool ret = true; RTMPPacket packet = {}; //写FLV文件头 const char flvHeader[] = { 'F', 'L', 'V', //固定签名 0x01, //FLV版本号 0x05, //数据类型,0x01表示视频,0x04表示音频,0x05表示音视频都有 0x00, 0x00, 0x00, 0x09, //FLV头长度 0x00, 0x00, 0x00, 0x00 //首个TAG前的固定Size,永远为0 }; fwrite(flvHeader, 13, 1, flvFile); fflush(flvFile); //循环读取流数据 while (playing) { ret = RTMP_ReadPacket(rtmp, &packet); if (!ret) continue; ret = RTMPPacket_IsReady(&packet); if (!ret) continue; if (!packet.m_nBodySize) continue; //读取包信息 uint8_t headerType = packet.m_headerType; uint8_t tagType = packet.m_packetType; uint32_t bodySize = packet.m_nBodySize; uint32_t tagSize = bodySize + 11; uint32_t timestamp = packet.m_nTimeStamp; char *body = packet.m_body; //处理脚本包 if (packet.m_packetType == RTMP_PACKET_TYPE_INFO && RTMP_ClientPacket(rtmp, &packet)) { //写入FLV文件 writeFlvTag(0x12, tagSize, bodySize, timestamp, body); //解析媒体参数 MediaMetadata metadata = {}; parseScriptTag(body, bodySize, metadata); std::string strMetadata; metadata.toString(strMetadata); std::cout << "Media Metadata" << std::endl << strMetadata << std::endl; } //处理音频包 if (packet.m_packetType == RTMP_PACKET_TYPE_AUDIO && RTMP_ClientPacket(rtmp, &packet)) { //写入FLV文件 writeFlvTag(0x08, tagSize, bodySize, timestamp, body); //解析参数 unsigned short header1 = (body[0] & 0xF0) >> 4; //前4位表示音频格式,1表示PCM,2表示MP3,10表示AAC unsigned short header2 = body[1]; //0x00表示AAC解码配置,0x01表示AAC裸数据 bool isAAC = header1 == 0x0A; bool isConfigPacket = header2 == 0x00; //不是AAC if (!isAAC) { std::cout << "audio format is not aac " << header1 << std::endl; continue; } //解包AAC数据,存入AAC文件 if (isConfigPacket) { //从RTMPPacketBody中解析出音频参数 AudioParam audioParam = {}; audioParam.format = (body[0] & 0xF0) >> 4; //音频格式,1表示PCM,2表示MP3,10表示AAC audioParam.sampleRate = (body[0] & 0x0C) >> 2; //音频采样率,1表示11KHz,2表示22kHz,3表示44kHz audioParam.bitDepth = (body[0] & 0x02) >> 1; //音频采样精度,0表示8位,1表示16位 audioParam.channels = body[0] & 0x01; //0表示单声道,1表示多声道 audioParam.packetType = body[1]; //0表示音频配置包,1表示音频裸数据 audioParam.audioSpecificConfig = ((body[2] & 0xFF) << 8) + (0x00FF & body[3]); //解析自定义部分数据 int aotSize = packet.m_nBodySize - 4; uint8_t *aotSpecificConfig = (uint8_t *) malloc(aotSize); memcpy(aotSpecificConfig, body + 4, aotSize); audioParam.aotSpecificConfig = aotSpecificConfig; //提取AudioSpecificConfig,用于生成ADTS(AAC头信息) //想要理解此部分代码,可以找个标准的FLV文件 //然后用FLVAnalyzer软件解析下包结构,对照解析结果逐个字节理解即可 audioSpecificConfig.audioObjectType = (audioParam.audioSpecificConfig & 0xF800) >> 11; //AudioObjectType占5位 audioSpecificConfig.sampleFrequencyIndex = (audioParam.audioSpecificConfig & 0x0780) >> 7; //SampleFrequencyIndex占4位 audioSpecificConfig.channelConfiguration = (audioParam.audioSpecificConfig & 0x78) >> 3; //ChannelConfiguration占4位 audioSpecificConfig.frameLengthFlag = (audioParam.audioSpecificConfig & 0x04) >> 2; //FrameLengthFlag占1位 audioSpecificConfig.dependOnCoreCoder = (audioParam.audioSpecificConfig & 0x02) >> 1; //DependOnCoreCoder占1位 audioSpecificConfig.extensionFlag = audioParam.audioSpecificConfig & 0x01; //ExtensionFlag占1位 } else { //创建ADTS char adts[7] = {}; createADTS(audioSpecificConfig, packet.m_nBodySize - 2 + 7, adts); //除掉AudioBody的前两个参数字节,再加上ADTS的7个字节 //写入ADTS到AAC文件 fwrite(adts, 7, 1, aacFile); fflush(aacFile); //写入AAC裸数据到AAC文件 fwrite(packet.m_body + 2, packet.m_nBodySize - 2, 1, aacFile); fflush(aacFile); } } //处理视频包 if (packet.m_packetType == RTMP_PACKET_TYPE_VIDEO && RTMP_ClientPacket(rtmp, &packet)) { //写入FLV文件 writeFlvTag(0x09, tagSize, bodySize, timestamp, body); //解析参数 unsigned short header1 = body[0]; //高4位表示帧类型,低4位表示解码器,0x10表示关键帧,0x20表示参照帧,0x07表示AVC unsigned short header2 = body[1]; //0x00表示AVC解码配置,0x01表示AVC裸数据 bool isAvc = (header1 & 0x0F) == 0x07; bool isConfigPacket = header2 == 0x00; //不是AVC if (!isAvc) { std::cout << "video format is not avc " << header1 << std::endl; continue; } //H264固定起始码 const char start_code[4] = {0x00, 0x00, 0x00, 0x01}; //解包H264数据,存入H264文件 if (isConfigPacket) { //AVC-Header //从RTMPPacket中解析SPS int sps_num = body[10] & 0x1F; int sps_index = 11; while (sps_index <= 10 + sps_num) { int sps_len = (body[sps_index] & 0xFF) << 8 | (body[sps_index + 1] & 0xFF); sps_index += 2; //写入H264文件 fwrite(start_code, 1, 4, h264File); fwrite(body + sps_index, 1, sps_len, h264File); fflush(h264File); sps_index += sps_len; } //从RTMPPacket中解析PPS int pps_num = body[sps_index] & 0x1F; int pps_index = sps_index + 1; while (pps_index <= sps_index + pps_num) { int pps_len = (body[pps_index] & 0xFF) << 8 | body[pps_index + 1] & 0xFF; pps_index += 2; //写入H264文件 fwrite(start_code, 1, 4, h264File); fwrite(body + pps_index, 1, pps_len, h264File); fflush(h264File); pps_index += pps_len; } } else { //AVC-NALU int nalu_index = 5; while (nalu_index < packet.m_nBodySize) { int nalu_len = (body[nalu_index] & 0x000000FF) << 24 | (body[nalu_index + 1] & 0x000000FF) << 16 | (body[nalu_index + 2] & 0x000000FF) << 8 | body[nalu_index + 3] & 0x000000FF; nalu_index = nalu_index + 4; //写入H264文件 fwrite(start_code, 1, 4, h264File); fwrite(body + nalu_index, 1, nalu_len, h264File); fflush(h264File); nalu_index += nalu_len; } } } //释放Packet RTMPPacket_Free(&packet); } return ret; }
//RtmpConfigParser.h #include <iostream> #include <string.h> #include <stdio.h> #include <sstream> #include "rtmp/rtmp_sys.h" #include "rtmp/log.h" struct AudioParam { uint8_t format; uint8_t sampleRate; uint8_t bitDepth; uint8_t channels; uint8_t packetType; uint16_t audioSpecificConfig; uint8_t * aotSpecificConfig; //可变部分,用于存储自定义数据,长度为FlvTagSize-固定部分长度 }; struct AudioSpecificConfig { uint8_t audioObjectType; uint8_t sampleFrequencyIndex; uint8_t channelConfiguration; uint8_t frameLengthFlag; uint8_t dependOnCoreCoder; uint8_t extensionFlag; uint8_t *AOTSpecificConfig = nullptr; //长度可变,DataSize-固定部分长度,就是这部分的长度 }; struct MediaMetadata { public: int audioCodecId = 0; //音频解码器 int audioSampleRate = 44100; //音频采样率 int audioSampleSize = 16; //音频采样精度 int audioChannelStereo = 1; //音频声道,0单声道,1多声道 int videoCodecId = 0; //视频解码器 int videoWidth = 800; //视频宽度 int videoHeight = 600; //视频高度 int videoFrameRate = 30; //视频帧率 int fileSize = 0; //文件大小 void toString(std::string &str) { std::ostringstream os; os << "audioCodecId" << ":" << audioCodecId << std::endl; os << "audioSampleRate" << ":" << audioSampleRate << std::endl; os << "audioSampleSize" << ":" << audioSampleSize << std::endl; os << "audioChannelStereo" << ":" << audioChannelStereo << std::endl; os << "videoCodecId" << ":" << videoCodecId << std::endl; os << "videoWidth" << ":" << videoWidth << std::endl; os << "videoHeight" << ":" << videoHeight << std::endl; os << "videoFrameRate" << ":" << videoFrameRate << std::endl; os << "fileSize" << ":" << fileSize << std::endl; str = os.str(); } }; void copyProperty(MediaMetadata &metadata, AMFObjectProperty *property); //根据AudioSpecificConfig生成ADTS //FrameSize=ADTS+AACRaw,即Header+Body,整帧长度 void createADTS(AudioSpecificConfig audioSpecificConfig, unsigned short frameSize, char *dst) { uint8_t adts[7] = {}; //Syncword,12位,固定值0xFFF adts[0] = 0xFF; adts[1] = 0xF0; //ID,1位,0表示MPEG4,1表示MPEG2 adts[1] |= 0x00; //Layer,2位,永远是0 adts[1] |= 0x00; //ProtectionAbsent,1位,是否CRC校验,0表示不校验,1表示校验 adts[1] |= 0x01; //Profile,2位,等于audioObjectType-1 adts[2] = (audioSpecificConfig.audioObjectType - 1) << 6; //SampleFrequencyIndex,4位,采样率,4表示44kHz adts[2] |= (audioSpecificConfig.sampleFrequencyIndex & 0x0F) << 2; //PrivateBit,1位,私有位 adts[2] |= (0 << 1); //ChannelConfiguration,3位,声道数 adts[2] |= (audioSpecificConfig.channelConfiguration & 0B0100) >> 2; adts[3] = (audioSpecificConfig.channelConfiguration & 0B0011) << 6; //OriginalCopy,1位 adts[3] |= (0 << 5); //Home,1位 adts[3] |= (0 << 4); //CopyrightedIdBit,1位,永远是0 adts[3] |= (0 << 3); //CopyrightedIdStart,1位,永远是0 adts[3] |= (0 << 2); //AACFrameLength,13位,ADTS和裸数据总长度 adts[3] |= (frameSize & 0x1800) >> 11; adts[4] = (frameSize & 0x07F8) >> 3; adts[5] = (frameSize & 0x07) << 5; //ADTSBufferFullness,11位,永远是0x7FF,表示码率可变 adts[5] |= 0x1F; adts[6] = 0xFC; //RawDataBlockNumber,2位,表示该包中包含多少个原始帧,填0即可 adts[6] |= 0B0000; memcpy(dst, adts, 7); } //解析ScriptTag void parseScriptTag(char *body, int bodySize, MediaMetadata &metadata) { //解析AMFObject AMFObject obj; AMF_Decode(&obj, body, bodySize, false); AMF_Dump(&obj); //遍历所有属性 for (int i = 0; i < obj.o_num; i++) { AMFObjectProperty *property = AMF_GetProp(&obj, NULL, i); if (!property) continue; //普通属性 if (property->p_type != AMF_OBJECT) { copyProperty(metadata, property); continue; } //嵌套属性 AMFObject subObject; AMFProp_GetObject(property, &subObject); for (int m = 0; m < subObject.o_num; m++) { AMFObjectProperty *subProperty = AMF_GetProp(&subObject, NULL, m); copyProperty(metadata, subProperty); } } } //拷贝Script属性到Metadata void copyProperty(MediaMetadata &metadata, AMFObjectProperty *property) { if (!property || !property->p_name.av_val) return; //去除属性名后面的特殊字符 AMFDataType propertyType = property->p_type; std::string strPropertyName = property->p_name.av_val; strPropertyName.erase(remove_if(strPropertyName.begin(), strPropertyName.end(), [](char c) -> bool { return c == '\U00000002'; }), strPropertyName.end()); const char *propertyName = strPropertyName.c_str(); std::cout << "get property: " << propertyName << std::endl; //获取属性值 if (strcmp("stereo", propertyName) == 0) { bool value = AMFProp_GetBoolean(property); metadata.audioChannelStereo = value; } else if (strcmp("width", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.videoWidth = value; } else if (strcmp("height", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.videoHeight = value; } else if (strcmp("framerate", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.videoFrameRate = value; } else if (strcmp("videocodecid", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.videoCodecId = value; } else if (strcmp("audiosamplerate", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.audioSampleRate = value; } else if (strcmp("audiosamplesize", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.audioSampleSize = value; } else if (strcmp("audiocodecid", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.audioCodecId = value; } else if (strcmp("filesize", propertyName) == 0) { double value = AMFProp_GetNumber(property); metadata.fileSize = value; } }
Demo下载
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。