音视频解封装demo:使用libmp4v2解封装(demux)出mp4文件中的h264视频数据和aac语音数据

发布于:2024-07-11 ⋅ 阅读:(20) ⋅ 点赞:(0)

1、README

前言

本demo是使用的mp4v2来将mp4文件解封装得到h264、aac的,目前demo提供的.a静态库文件是在x86_64架构的Ubuntu16.04编译得到的,如果想在其他环境下测试demo,可以自行编译mp4v2并替换相应的库文件(libmp4v2.a)。

a. 编译
$ make # 或者`make DEBUG=1`打开调试打印信息

如果想编译mp4v2,则可以参考以下步骤:

mp4v2源码下载地址:https://github.com/TechSmith/mp4v2

$ tar xjf mp4v2-2.0.0.tar.bz2
$ cd mp4v2-2.0.0/
$ ./configure --prefix=$PWD/_install # 交叉编译可参考加上选项: --host=arm-linux-gnueabihf
$ make -j96
$ make install
b. 使用

注:示例2中的音视频测试源文件是不同步的,不影响本demo的解封装。

$ ./mp4v2_unpack_demo 
Usage: 
   ./mp4v2_unpack_demo ./avfile/test1.mp4 ./test1_out.h264 ./test1_out.aac
   ./mp4v2_unpack_demo ./avfile/test2.mp4 ./test2_out.h264 ./test2_out.aac
c. 参考文章
d. demo目录结构
.
├── avfile
│   ├── test1.mp4
│   ├── test1_out.aac
│   ├── test1_out.h264
│   ├── test2.mp4
│   ├── test2_out.aac
│   └── test2_out.h264
├── docs
│   ├── 01.mp4v2应用—mp4转h264 - wade_linux - 博客园.mhtml
│   ├── mp4文件格式解析 - 简书.mhtml
│   ├── MP4格式详解_DONGHONGBAI的专栏-CSDN博客.mhtml
│   └── 使用mp4v2解码mp4转成h264码流和aac码流_lq496387202的博客-CSDN博客_mp4v2解码.mhtml
├── include
│   └── mp4v2
│       ├── chapter.h
│       ├── file.h
│       ├── file_prop.h
│       ├── general.h
│       ├── isma.h
│       ├── itmf_generic.h
│       ├── itmf_tags.h
│       ├── mp4v2.h
│       ├── platform.h
│       ├── project.h
│       ├── sample.h
│       ├── streaming.h
│       ├── track.h
│       └── track_prop.h
├── lib
│   └── libmp4v2.a
├── main.c
├── Makefile
└── README.md

2、主要代码片段

main.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "mp4v2/mp4v2.h"

// 编译时Makefile里控制
#ifdef ENABLE_DEBUG
	#define DEBUG(fmt, args...) 	printf(fmt, ##args)
#else
	#define DEBUG(fmt, args...)
#endif


int unpackMp4File(char *mp4FileName, char *videoFileName, char *audioFileName);


unsigned char g_sps[64] = {0};
unsigned char g_pps[64] = {0};
unsigned int  g_spslen  = 0;
unsigned int  g_ppslen  = 0;


int main(int argc, char **argv)
{
	if(argc < 2)
	{
		printf("Usage: \n"
			   "   %s ./avfile/test1.mp4 ./test1_out.h264 ./test1_out.aac\n"
			   "   %s ./avfile/test2.mp4 ./test2_out.h264 ./test2_out.aac\n",
				argv[0], argv[0]);
		return -1;
	}

    int ret = unpackMp4File(argv[1], argv[2], argv[3]);
	if(ret == 0)
	{
		printf("\033[32mSuccess!\033[0m\n");
	}
	else
	{
		printf("\033[31mFailed!\033[0m\n");
	}

    return 0;
}

int getH264Stream(MP4FileHandle mp4Handler, int videoTrackId, int totalSamples, char *saveFileName)
{
	// 调用的接口要传的参数
	uint32_t curFrameIndex = 1; // `MP4ReadSample`函数的参数要求是从1开始,但我们打印帧下标还是从0开始
    uint8_t *pData = NULL;
    uint32_t nSize = 0;
    MP4Timestamp pStartTime;
    MP4Duration pDuration;
    MP4Duration pRenderingOffset;
    bool pIsSyncSample = 0;

	// 写文件要用的参数
    char naluHeader[4] = {0x00, 0x00, 0x00, 0x01};
    FILE *fpVideo = NULL;

    if(!mp4Handler)
		return -1;

    fpVideo = fopen(saveFileName, "wb"); 
	if (fpVideo == NULL)
	{
		printf("open file(%s) error!\n", saveFileName);
		return -1;
	}
 
    while(curFrameIndex <= totalSamples)
    {   
        // 如果传入MP4ReadSample的视频pData是null,它内部就会new 一个内存
        // 如果传入的是已知的内存区域,则需要保证空间bigger then max frames size.
        MP4ReadSample(mp4Handler, videoTrackId, curFrameIndex, &pData, &nSize, &pStartTime, &pDuration, &pRenderingOffset, &pIsSyncSample);

		DEBUG("[\033[35mvideo\033[0m] ");

		if(pIsSyncSample)
		{
			DEBUG("IDR\t");

			fwrite(naluHeader, 4, 1, fpVideo);
			fwrite(g_sps, g_spslen, 1, fpVideo);

			fwrite(naluHeader, 4, 1, fpVideo);
			fwrite(g_pps, g_ppslen, 1, fpVideo);
		}
		else
		{
			DEBUG("SLICE\t");
		}

        if(pData && nSize > 4)
		{
			// `MP4ReadSample`函数的参数要求是从1开始,但我们打印帧下标还是从0开始;而大小已经包含了4字节的start code长度
			DEBUG("frame index: %d\t size: %d\n", curFrameIndex - 1, nSize);
            fwrite(naluHeader, 4, 1, fpVideo);
			fwrite(pData + 4, nSize - 4, 1, fpVideo); // pData+4了,那nSize就要-4
        }
        
        free(pData);
        pData = NULL;
		
        curFrameIndex++;
    }       
    fflush(fpVideo);
    fclose(fpVideo);  
 
    return 0;
}

int getAACStream(MP4FileHandle mp4Handler, int audioTrackId, int totalSamples, char *saveFileName)
{
	// 调用的接口要传的参数
	uint32_t curFrameIndex = 1; // `MP4ReadSample`函数的参数要求是从1开始,但我们打印帧下标还是从0开始
    uint8_t *pData = NULL;
    uint32_t nSize = 0;

	// 写文件要用的参数
	FILE *fpAudio = NULL;

	if(!mp4Handler)
		return -1;

	fpAudio = fopen(saveFileName, "wb");
	if (fpAudio == NULL)
	{
		printf("open file(%s) error!\n", saveFileName);
		return -1;
	}
 
	while(curFrameIndex <= totalSamples)
	{
		// 如果传入MP4ReadSample的音频pData是null,它内部就会new 一个内存
		// 如果传入的是已知的内存区域,则需要保证空间bigger then max frames size.
		MP4ReadSample(mp4Handler, audioTrackId, curFrameIndex, &pData, &nSize, NULL, NULL, NULL, NULL);

		DEBUG("[\033[36maudio\033[0m] ");

		if(pData)
		{			
			DEBUG("frame index: %d\t size: %d\n", curFrameIndex - 1, nSize);
			fwrite(pData, nSize, 1, fpAudio);
		}
		
		free(pData);
		pData = NULL;
		
		curFrameIndex++;
	}		
	fflush(fpAudio);
	fclose(fpAudio);  
 
	return 0;
}


int unpackMp4File(char *mp4FileName, char *videoFileName, char *audioFileName)
{
	MP4FileHandle mp4Handler = 0;
	uint32_t trackCnt = 0;	
	int videoTrackId = -1;
	int audioTrackId = -1;
	unsigned int videoSampleCnt = 0;
	unsigned int audioSampleCnt = 0;


	mp4Handler = MP4Read(mp4FileName);
	if (mp4Handler <= 0)
	{
		printf("MP4Read(%s) error!\n", mp4FileName);
		return -1;
	}
 
	trackCnt = MP4GetNumberOfTracks(mp4Handler, NULL, 0); //获取音视频轨道数
	printf("****************************\n");
	printf("trackCnt: %d\n", trackCnt);
 
    for (int i = 0; i < trackCnt; i++)
    {
		// 获取trackId,判断获取数据类型: 1-获取视频数据,2-获取音频数据
		MP4TrackId trackId = MP4FindTrackId(mp4Handler, i, NULL, 0);
		const char* trackType = MP4GetTrackType(mp4Handler, trackId);

		if (MP4_IS_VIDEO_TRACK_TYPE(trackType))
		{
			// 不关心,只是打印出来看看
			MP4Duration duration = 0;
			uint32_t timescale = 0;

			videoTrackId = trackId;

			duration = MP4GetTrackDuration(mp4Handler, videoTrackId);
			timescale = MP4GetTrackTimeScale(mp4Handler, videoTrackId);
			videoSampleCnt = MP4GetTrackNumberOfSamples(mp4Handler, videoTrackId);
			
			printf("video params: \n"
				   " - trackId: %d\n"
				   " - duration: %lu\n"
				   " - timescale: %d\n"
				   " - samples count: %d\n",
				   videoTrackId, duration, timescale, videoSampleCnt);

			// 读取 sps/pps 
			uint8_t **seqheader;			
			uint32_t *seqheadersize;
			uint8_t **pictheader;
			uint32_t *pictheadersize;

			MP4GetTrackH264SeqPictHeaders(mp4Handler, videoTrackId, &seqheader, &seqheadersize, &pictheader, &pictheadersize);

			// 获取sps
            for (int ix = 0; seqheadersize[ix] != 0; ix++)
            {
				memcpy(g_sps, seqheader[ix], seqheadersize[ix]);
				g_spslen = seqheadersize[ix];
				free(seqheader[ix]);
			}
			free(seqheader);
			free(seqheadersize);

			// 获取pps
			for (int ix = 0; pictheader[ix] != 0; ix++)
			{
				memcpy(g_pps, pictheader[ix], pictheadersize[ix]);
				g_ppslen = pictheadersize[ix];
				free(pictheader[ix]);
			}
			free(pictheader);
			free(pictheadersize);
        }
		else if (MP4_IS_AUDIO_TRACK_TYPE(trackType))
		{
			audioTrackId = trackId;
			audioSampleCnt = MP4GetTrackNumberOfSamples(mp4Handler, audioTrackId);

			printf("audio params: \n"
				   " - trackId: %d\n"
				   " - samples count: %d\n",
				   audioTrackId, audioSampleCnt);
        }
    }
	printf("****************************\n");

    // 解析完了mp4,主要是为了获取sps pps 还有video的trackID
    if(videoTrackId >= 0)
	{
        getH264Stream(mp4Handler, videoTrackId, videoSampleCnt, videoFileName);  
    }

	if(audioTrackId >= 0)
	{
        getAACStream(mp4Handler, audioTrackId, audioSampleCnt, audioFileName);
    }
 
    // 需要mp4close 否则在嵌入式设备打开mp4上多了会内存泄露挂掉.
    MP4Close(mp4Handler, 0);
	
    return 0;
}

3、demo下载地址(任选一个)