使用ffmpeg将内存中的裸流打包成可播放的MP4文件,并输出到内存中
前两天项目上有个需求,要求大概是这样的,输入端是一帧一帧的h264裸流(本示例只支持h264裸流,h265可基于本示例自己开发,在此我就不过多阐述了)和一个时间,要求输出根据这个时间来产生一个前后各延伸一段时间的视频(伴随录像),且伴随录像是可直接播放的MP4文件。但是产生的视频文件不是直接存储在本地的某个文件夹下,而是通过网络,向外部的某个ftp服务器发送,在存储到ftp服务器的路径下。
基于上述的需求,我将过程大致分成了两步,第一步是取内存中的h264裸流,经过ffmpeg打包成可播放的MP4文件,但是输出到内存中,第二步是将内存中的MP4文件通过ftp发送到服务器。本文所要阐述的是第一步。第二步后续有时间我会继续更新(如果你急需可留言联系我)。
网上关于ffmpeg的开发,多多少少都有参考大神雷霄骅(在此致敬,感谢雷大大做的贡献)的资料,我同样也不例外,他的资料中,读取本地h264裸流文件后打包并输出到MP4文件、和输入rtsp裸流打包并输出到MP4文件的相关资料很全,但是关于从内存中获取h264裸流并输出到内存中的资料确不多,我开发的过程中,只找到一篇雷大大的伪代码,只提供了思路和少数的几行代码,基于这篇伪代码,我在开发过程中遇到了不少问题,所以在此分享,希望能在雷大大的那面“墙”上添上一块转,对广大码友有所帮助。
本示例基于ffmpeg2.8.8版本 (库链接顺序 -lavformat -lavcodec -lavutil -lswscale -lswresample -lm -lpthread -ldl)
HI_ES2MP4.c
#include <stdio.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "pack_h264.h"
extern ITS_ALONG_EVENT_INFO * g_along_event;
static AVFormatContext * g_OutFmt_Ctx;
static int vi = -1;
//static HI_BOOL b_First_IDR_Find = HI_FALSE;
static int ptsInc = 0;
/* Add an output stream */
static int HI_PDT_Add_Stream(AVFormatContext *poutFmtCtx, int h264_rate, int h264_w, int h264_h)
{
AVOutputFormat *pOutFmt = NULL;
AVCodecContext *PAVCodecCtx = NULL;
AVStream *pAVStream = NULL;
AVCodec *pcodec = NULL;
pOutFmt = poutFmtCtx->oformat;
/* find the encoder */
pcodec = avcodec_find_encoder(pOutFmt->video_codec);
if (NULL == pcodec)
{
printf("ERROR: could not find encoder for '%s' \n", avcodec_get_name(pOutFmt->video_codec));
return -1;
}
pAVStream = avformat_new_stream(poutFmtCtx, pcodec);
if (NULL == pAVStream)
{
printf("ERROR: could not allocate stream \n");
return -2;
}
pAVStream->id = poutFmtCtx->nb_streams-1;
PAVCodecCtx = pAVStream->codec;
vi = pAVStream->index;
PAVCodecCtx->codec_tag = 0;
switch ((pcodec)->type)
{
case AVMEDIA_TYPE_AUDIO:
PAVCodecCtx->sample_fmt = (pcodec)->sample_fmts ? (pcodec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
PAVCodecCtx->bit_rate = 64000;
PAVCodecCtx->sample_rate = 44100;
PAVCodecCtx->channels = 2;
break;
case AVMEDIA_TYPE_VIDEO:
PAVCodecCtx->codec_id = AV_CODEC_ID_H264;
PAVCodecCtx->bit_rate = 0;
PAVCodecCtx->width = h264_w;
PAVCodecCtx->height = h264_h;
PAVCodecCtx->time_base.den = h264_rate*1000;
PAVCodecCtx->time_base.num = 1000;
PAVCodecCtx->gop_size = 1;
PAVCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
if (PAVCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
{
PAVCodecCtx->max_b_frames = 2;
}
if (PAVCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO)
{
PAVCodecCtx->mb_decision = 2;
}
break;
default:
break;
}
if (poutFmtCtx->oformat->flags & AVFMT_GLOBALHEADER)
{
PAVCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
return 0;
}
int write_packet(void * opaque, unsigned char * buf, int buf_size)
{
if(g_along_event != NULL && g_along_event->synthesis_video != NULL)
{
if(g_along_event->synthesis_video_max - g_along_event->synthesis_video_size >= buf_size)
{
memcpy(g_along_event->synthesis_video + g_along_event->synthesis_video_size, buf, buf_size);
g_along_event->synthesis_video_size += buf_size;
}
else
{
printf("write_packet error, %d, %d\n", g_along_event->synthesis_video_max, g_along_event->synthesis_video_size);
}
}
if(buf_size == 4) // 写文件结尾时,会写四个字节的数据,是数据的长度,需要把改数据加4写进开头第40~44个字节的位置
{
g_along_event->synthesis_video[40] = buf[0];
g_along_event->synthesis_video[41] = buf[1];
g_along_event->synthesis_video[42] = buf[2];
g_along_event->synthesis_video[43] = buf[3] + 4;
if(g_along_event->synthesis_video[43] < 4) // 说明buf[3] + 4大于0XFF,循环了,需要向高位进一
{
g_along_event->synthesis_video[42] += 1;
if(g_along_event->synthesis_video[42] == 0) // 需要向高位进一
{
g_along_event->synthesis_video[41] += 1;
if(g_along_event->synthesis_video[41] == 0) // 需要向高位进一
{
g_along_event->synthesis_video[40] += 1;
}
}
}
}
return 0;
}
static int64_t _seek(void *opaque, int64_t offset, int whence)
{
return 0;
}
int HI_ESCreatMP4(char* p_File_Name, int h264_rate, int h264_w, int h264_h, unsigned char * buf, int b_size)
{
int ret = 0;
char pszFileName[256] = {0};
AVOutputFormat *pOutFmt = NULL;
if(p_File_Name == NULL || strlen(p_File_Name) <= 0)
{
return -1;
}
sprintf(pszFileName, "%s", p_File_Name);
avcodec_register_all();
av_register_all();
avformat_network_init();
//ret = avformat_alloc_output_context2(&g_OutFmt_Ctx, NULL, NULL, pszFileName);
ret = avformat_alloc_output_context2(&g_OutFmt_Ctx, NULL, "mp4", NULL);
if(ret < 0)
{
printf("avformat_alloc_output_context2 error.\n");
return -2;
}
if (NULL == g_OutFmt_Ctx)
{ //try default
printf("ERROR: Could not deduce output format from file extension: using mp4. \n");
ret = avformat_alloc_output_context2(&g_OutFmt_Ctx, NULL, "mp4", pszFileName);
if (NULL == g_OutFmt_Ctx || ret < 0)
{
printf("avformat_alloc_output_context2 error!, %d\n", ret);
return -3;
}
}
pOutFmt = g_OutFmt_Ctx->oformat;
if (pOutFmt->video_codec == AV_CODEC_ID_NONE)
{
printf("ERROR: add_stream ID =%d \n",pOutFmt->video_codec);
goto exit_outFmt_failed;
}
ret = HI_PDT_Add_Stream(g_OutFmt_Ctx, h264_rate, h264_w, h264_h);
if(ret < 0)
{
printf("HI_PDT_Add_Stream error!\n");
goto exit_outFmt_failed;
}
av_dump_format(g_OutFmt_Ctx, 0, pszFileName, 1);
#if 1
// 输出到MP4文件
if (!(pOutFmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&g_OutFmt_Ctx->pb, pszFileName, AVIO_FLAG_WRITE);
if (ret < 0)
{
printf("ERROR: could not open %s\n", pszFileName);
goto exit_avio_open_failed;
}
}
#else
// 输出到内存
#else
if(buf != NULL && b_size > 0)
{
g_OutFmt_Ctx->pb = avio_alloc_context(buf, b_size, 1, NULL, NULL, write_packet, _seek);//注意:第3个参数赋值为1,否则write_packet回调将不能被成功调用
g_OutFmt_Ctx->flags |= AVFMT_FLAG_CUSTOM_IO;
}
#endif
/* Write the stream header, if any */
ret = avformat_write_header(g_OutFmt_Ctx, NULL);
if (ret < 0)
{
printf("Error occurred when opening output file, ret = %d\n", ret);
goto exit_writeheader_failed;
}
return 0;
exit_writeheader_failed:
//exit_avio_open_failed:
//if (g_OutFmt_Ctx && !(g_OutFmt_Ctx->flags & AVFMT_NOFILE))
// avio_close(g_OutFmt_Ctx->pb);
exit_outFmt_failed:
if(NULL != g_OutFmt_Ctx)
avformat_free_context(g_OutFmt_Ctx);
return -4;
}
#define SDC_H264 0
#define SDC_H265 1
int HI_WriteVideo(unsigned char *pstDat, unsigned int DatLen, int enType, int IsKey)
{
//unsigned int i=0;
//unsigned char* pPackVirtAddr = NULL;
//unsigned int u32PackLen = 0;
//unsigned int u32PackOffset = 0;
//H264E_NALU_TYPE_E enH264EType;
//H265E_NALU_TYPE_E enH265EType;
int ret = 0;
AVStream * pst = NULL;
AVPacket pkt;
//int isIDR = 0;
if(vi<0)
{
printf("HI_WriteVideo vidx %d error!\n");
return -1;
}
if(NULL == pstDat || DatLen <= 0 || enType > 1 || enType < 0)
{
printf("HI_WriteVideo ARG Err %p %d %d error!\n", pstDat, DatLen, enType);
return -2;
}
pst = g_OutFmt_Ctx->streams[vi];
av_init_packet(&pkt);
pkt.flags |= (IsKey==1) ? AV_PKT_FLAG_KEY : 0;
pkt.stream_index = pst->index;
pkt.data = pstDat;
pkt.size = DatLen;
pkt.pts = av_rescale_q((ptsInc++), pst->codec->time_base, pst->time_base);
pkt.dts = av_rescale_q_rnd(pkt.dts, pst->time_base,pst->time_base,(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration,pst->time_base, pst->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(g_OutFmt_Ctx, &pkt);
if (ret < 0)
{
printf("av_interleaved_write_frame error, ret: %d\n", ret);
return -3;
}
return 0;
}
void HI_CloseMP4(void)
{
int ret = 0;
if(g_OutFmt_Ctx)
{
ret = av_write_trailer(g_OutFmt_Ctx);
if(ret != 0)
{
printf("av_write_trailer error, ret = %d\n", ret);
}
}
if(g_OutFmt_Ctx && !(g_OutFmt_Ctx->oformat->flags & AVFMT_NOFILE))
{
//ret = avio_close(g_OutFmt_Ctx->pb);
//if(ret < 0)
//{
// printf("avio_close error, ret = %d\n", ret);
//}
}
if(g_OutFmt_Ctx)
{
avformat_free_context(g_OutFmt_Ctx);
g_OutFmt_Ctx = NULL;
}
vi = -1;
ptsInc = 0;
}
pack_h264.h
#ifndef PACK_H264_H
#define PACK_H264_H
#define EVENT_PRE_VIDEO_S 5
#define EVENT_LATTER_VIDEO_S 3
#define BUF_SIZE_MAX_1080 200 * 1024
#define BUF_SIZE_MAX_2160 1024 * 1024
typedef enum
{
CAMERA_VENC_FRAME_TYPE_I = 0,
CAMERA_VENC_FRAME_TYPE_P = 1,
CAMERA_VENC_FRAME_TYPE_B = 2,
}CAMERA_VENC_FRAME_TYPE;
typedef struct
{
int m_Frame_len; // 帧长度
int m_Frame_type; // 帧类型 I/P/B 帧
int m_Rate; // 帧率
int m_Frame_w; // 帧宽
int m_Frame_h; // 帧高
int m_Format; // (1:H264) or (2:H265)
int m_Frame_idx; // 引用次数
void * p_Frame_ptr; // 帧数据
unsigned int time_s; // 获取该帧时的系统时间秒
unsigned int time_ms; // 获取该帧时的系统时间毫秒
int m_Rev[2];
void * p_Rev[2];
}TS_VENC_FRAME;
typedef struct ITS_ALONG_EVENT_INFO
{
unsigned int event_time_s;
unsigned int event_time_ms;
char along_video_path[128]; // 伴随录像路径 输入 用于ftp上传
void * venc_frame[1024]; // 转成TS_VENC_FRAME指针使用 输入
int venc_num; // 实际的venc帧数 输入
unsigned char * synthesis_video; // 编程MP4文件后的视频 输出
int synthesis_video_size; // synthesis_video使用的大小 输出
int synthesis_video_max; // synthesis_video实际的大小 输入
char reserve[8];
}ITS_ALONG_EVENT_INFO;
#endif
pack_h264.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pack_h264.h"
int HI_ESCreatMP4(char* p_File_Name, int h264_rate, int h264_w, int h264_h, unsigned char * buf, int b_size);
int HI_WriteVideo(unsigned char *pstDat, unsigned int DatLen, int enType, int IsKey);
void HI_CloseMP4(void);
ITS_ALONG_EVENT_INFO * g_along_event; // 用来存储输入参数
static int synthetic_alloc_buf(ITS_ALONG_EVENT_INFO * p_event_info)
{
int i;
int size = 0;
TS_VENC_FRAME * p_Venc_frame = NULL;
#define VIDEO_BUF_MAX 64 * 1024 // 编码后比编码前大的最大值
if(p_event_info == NULL)
{
return -1;
}
for(i = 0; i < p_event_info->venc_num; i++)
{
if(p_event_info->venc_frame[i] != NULL)
{
p_Venc_frame = (TS_VENC_FRAME *)p_event_info->venc_frame[i];
size += p_Venc_frame->m_Frame_len;
}
}
if(size <= 0)
{
return -2;
}
size += VIDEO_BUF_MAX;
p_event_info->synthesis_video = (unsigned char *)malloc(size);
if(p_event_info->synthesis_video == NULL)
{
return -3;
}
memset(p_event_info->synthesis_video, 0, size);
p_event_info->synthesis_video_max = size;
return 0;
}
int VencFrame_Free(void * venc_frame)
{
TS_VENC_FRAME * p_Venc_frame = NULL;
if(venc_frame == NULL)
{
return -1;
}
p_Venc_frame = (TS_VENC_FRAME *)venc_frame;
if(p_Venc_frame->m_Frame_idx > 1)
{
p_Venc_frame->m_Frame_idx--;
}
else if(p_Venc_frame->m_Frame_idx == 1)
{
if(p_Venc_frame->p_Frame_ptr != NULL)
{
free(p_Venc_frame->p_Frame_ptr);
p_Venc_frame->p_Frame_ptr = NULL;
}
free(p_Venc_frame);
p_Venc_frame = NULL;
}
else
{
printf("m_Frame_idx is error(%d)\n", p_Venc_frame->m_Frame_idx);
}
return 0;
}
int main(int argc, char** argv)
{
int ret = 0,i;
ITS_ALONG_EVENT_INFO * p_event_info = g_along_event;
TS_VENC_FRAME * p_Venc_frame = NULL;
int m_Get_I_Frame = 0;
unsigned char * enco_buf = NULL;
int enco_buf_size = 0;
//假设已经通过另外一个线程,通过时间把需要的裸流h264帧写到了全局变量g_along_event里面
ret = synthetic_alloc_buf(p_event_info);
#if 1
// 输出到MP4文件
ret = HI_ESCreatMP4("./123.mp4", 30, 768, 576, NULL, 0);
#else
// 输出到内存
char buf[1024 * 10] = {'\0'};
ret = HI_ESCreatMP4("./123.mp4", 30, 768, 576, buf, sizeof(buf));
#endif
if(ret != 0)
{
printf("HI_ESCreatMP4 error!\n");
}
p_Venc_frame = (TS_VENC_FRAME *)p_event_info->venc_frame[0];
if(p_Venc_frame->m_Frame_h <= 1080)
{
enco_buf_size = BUF_SIZE_MAX_1080;
}
else if(p_Venc_frame->m_Frame_h > 1080 && p_Venc_frame->m_Frame_h <= 2160)
{
enco_buf_size = BUF_SIZE_MAX_1080;
}
enco_buf = (unsigned char *)malloc(enco_buf_size);
if(enco_buf == NULL)
{
printf("ERROR: could not open.\n");
}
if(ret != 0)
{
for(i = 0; i < p_event_info->venc_num; i++)
{
VencFrame_Free(p_event_info->venc_frame[i]);
p_event_info->venc_frame[i] = NULL;
}
if(p_event_info->synthesis_video != NULL)
{
free(p_event_info->synthesis_video);
p_event_info->synthesis_video = NULL;
}
free(enco_buf);
}
for(i = 0; i < p_event_info->venc_num; i++)
{
p_Venc_frame = (TS_VENC_FRAME *)p_event_info->venc_frame[i];
if(p_Venc_frame != NULL)
{
if(CAMERA_VENC_FRAME_TYPE_I == p_Venc_frame->m_Frame_type || m_Get_I_Frame == 1)
{
m_Get_I_Frame = 1;
ret = HI_WriteVideo((unsigned char *)p_Venc_frame->p_Frame_ptr, p_Venc_frame->m_Frame_len, p_Venc_frame->m_Format - 1, 1);
if(ret)
{
printf("%s WriteVideo[%d] Key Frame error, ret: %d\n", __func__, i, ret);
HI_CloseMP4();
for(; i < p_event_info->venc_num; i++)
{
VencFrame_Free(p_event_info->venc_frame[i]);
p_event_info->venc_frame[i] = NULL;
}
p_event_info->venc_num = 0;
break;
}
}
VencFrame_Free(p_Venc_frame);
p_event_info->venc_frame[i] = NULL;
}
}
HI_CloseMP4();
m_Get_I_Frame = 0;
return 0;
}
因为数据输入的缘故,该代码还是不能直接运行,需要自己完成数据输入的部分,最终输出的文件在全局变量g_along_event的synthesis_video成员里面,大小为synthesis_video_size。