之前使用的FFMPEG基本都是打开指定链接或者文件读取数据进行操作, 此次要实现的功能是在服务器中读取内存中的H264跟AAC裸流进行文件合成保存, 这就有些难度了, 经过了两个周的资料搜索研究终于完成了该功能, 在此记录说明下, DEMO开源地址:
https://gitee.com/careye_open_source_platform_group/MP4MuxerTest
首先要感谢两篇博客, 没有这两篇博客是几乎没法完成这个功能的:
雷神的:https://blog.csdn.net/leixiaohua1020/article/details/39802913
Jons伯恩特: https://blog.csdn.net/huangyifei_1111/article/details/46955021
总体思路是利用avio_alloc_context函数创建一个带回调读取数据的AVIOContext实体类, 这样就可以通过队列的方式将内存数据追加到队列中, 然后在ffmpeg读取媒体帧的时候在回调函数中输出队列中的数据, 这样达成使用FFMPEG读取内存数据的目的.
程序合成流程是追加数据后就进行流的读取, 所以自制了一个简单的队列结构, 定义如下:
// 用于存储流数据的队列项结构
typedef struct _CE_QUEUE_ITEM_T_
{
// 申请的缓冲区指针
uint8_t* Bytes;
// 申请的缓冲区大小
int TotalSize;
// 入队填充的数据字节数
int EnqueueSize;
// 出队读取的数据字节数
int DequeueSize;
}CEQueueItem;
// 一个简单的队列结构, 用于暂存流数据
typedef struct _CE_QUEUE_T_
{
// 队列项指针
CEQueueItem *Items;
// 队列的标识, 使用者可以自定义标识进行队列区分
uint8_t Flag;
// 队列项个数
int Count;
// 出队计数索引
int DequeueIndex;
// 入队索引
int EnqueueIndex;
}CESimpleQueue;
没有用到链表的结构, 使用数组的方式对队列进行管理.
混流器做成了一个实体类, 方便外部调用, 对外接口也非常简单:
/*
* Comments: 混流器初始化 实例化后首先调用该方法进行内部参数的初始化
* Param aFileName: 要保存的文件名
* @Return 是否成功
*/
bool MP4Muxer::Start(std::string aFileName);
/*
* Comments: 追加一帧AAC数据到混流器
* Param aBytes: 要追加的字节数据
* Param aSize: 追加的字节数
* @Return 成功与否
*/
bool MP4Muxer::AppendAudio(uint8_t* aBytes, int aSize);
/*
* Comments: 追加一帧H264视频数据到混流器
* Param aBytes: 要追加的字节数据
* Param aSize: 追加的字节数
* @Return 成功与否
*/
bool MP4Muxer::AppendVideo(uint8_t* aBytes, int aSize);
/*
* Comments: 关闭并释放输出格式上下文
* Param : None
* @Return None
*/
void MP4Muxer::Stop(void);
Start则申请各种资源, 有些参数在方法实现内部固定了, 可根据需要自己进行修改, Stop则释放申请的各种资源并结束文件录制.
Append两个方法分别是追加音视频帧, 输入的数据要是一个完整帧, 另外做了一个该实体类的使用DEMO:
int main()
{
AVFormatContext* fmt_h264 = NULL;
AVFormatContext* fmt_aac = NULL;
AVPacket pkt_v;
int result = 0;
bool video_finished = false, audio_finished = false;
std::cout << "Start muxer..." << std::endl;
av_register_all();
if ((result = avformat_open_input(&fmt_h264, TestH264, 0, 0)) < 0)
{
DEBUG_E("Could not open input file: -%08X.", -result);
goto end;
}
if ((result = avformat_find_stream_info(fmt_h264, 0)) < 0)
{
DEBUG_E("Failed to retrieve input stream information -%08X.", -result);
goto end;
}
if ((result = avformat_open_input(&fmt_aac, TestAAC, 0, 0)) < 0)
{
DEBUG_E("Could not open input file: -%08X.", -result);
goto end;
}
if ((result = avformat_find_stream_info(fmt_aac, 0)) < 0)
{
DEBUG_E("Failed to retrieve input stream information -%08X.", -result);
goto end;
}
// 输出文件信息
av_dump_format(fmt_h264, 0, TestH264, 0);
av_dump_format(fmt_aac, 0, TestAAC, 0);
// 实例化混流器
MP4Muxer *muxer = new MP4Muxer();
if (!muxer->Start("./test.mp4"))
{
delete muxer;
DEBUG_E("Muxer start fail.");
goto end;
}
while (1)
{
// 读取一帧视频数据
if (!video_finished)
{
if ((result = av_read_frame(fmt_h264, &pkt_v)) < 0)
{
DEBUG_W("Read frame fail: -%08X.", -result);
video_finished = true;
if (audio_finished)
{
break;
}
}
// 需要确保传入的是一帧数据
muxer->AppendVideo(pkt_v.data, pkt_v.size);
}
// 读取一帧音频数据
if (!audio_finished)
{
if ((result = av_read_frame(fmt_aac, &pkt_v)) < 0)
{
DEBUG_W("Read frame fail: -%08X.", -result);
audio_finished = true;
if (video_finished)
{
break;
}
}
// 需要确保传入的是一帧数据
muxer->AppendAudio(pkt_v.data, pkt_v.size);
}
}
av_free_packet(&pkt_v);
avformat_close_input(&fmt_h264);
muxer->Stop();
delete muxer;
end:
_DEBUG_I("Muxer finished.\n");
getchar();
}
DEMO就是利用FFMPEG读取一帧帧音视频数据, 然后将字节数组传入到混流器中进行合成, 示例中的音视频测试文件使用了雷神的264跟AAC文件.
话说真的不想写FFMPEG的博客, 每次都绕不过雷神的博客, 看到雷神的博客就伤感, 天妒英才, 哎~! 在此感谢各位开源先驱们~!
car-eye开源官方网址:www.car-eye.cn
car-eye项目地址:https://github.com/Car-eye-team