您当前的位置: 首页 >  音视频

命运之手

暂无认证

  • 1浏览

    0关注

    747博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【Android音视频开发】【031】RTMP写入FLV,H264,AAC文件

命运之手 发布时间:2021-09-12 23:37:18 ,浏览量:1

通过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             
关注
打赏
1654938663
查看更多评论
0.0434s