如何使用mp4v2解析mp4文件,抽取音视频数据帧【源码】【mp4】【NVR】

前言:

    mp4文件目前已经成为了流媒体音视频行业的通用标准文件格式,它是基于mov格式基础上演变来的,特别适合多平台播放,录制一次,多个平台都可使用。但是,由于mp4格式相对比较复杂,直到mp4v2这个开源工程的出现,解决了这个问题。

    通常,我们在使用mp4文件时,会遇到两个问题:如何从已有的mp4文件中抽取音视频数据帧;如何将音视频数据帧录制成mp4文件,并保持音视频同步。

    首先,我们需要对mp4v2开源库进行编译,源码下载地址如下:

    CSDN:https://download.csdn.net/download/haoyitech/10290353

    mp4v2的开源库提供多种平台的编译,我们要在windows下做演示,选择vs2010编译,vs2010的编译工程又提供多个编译输出,为了避免动态链接库带来的麻烦,我们选择静态lib的方式,调试版:Debug static(MTD),发行版:Release static(MT)。静态lib库的好处是,不用考虑dll加载路径。

    mp4v2\build32\bin\Debug Static (MTd)\libmp4v2D.lib
    mp4v2\build32\bin\Release Static (MT)\libmp4v2.lib

源码下载:

    CSDN:https://download.csdn.net/download/haoyitech/10291430

源码说明:

    开发工具:下载后,请用 VS2010 打开。

    总体思路:准备好标准mp4文件(H264+AAC),使用mp4v2提供的标准API,解析出音视频格式信息,视频需要获取关键的PPS和SPS,音频需要获取采样率、声道、扩展信息等等。

测试需要的mp4文件  => sample_libmp4v2\bin\sample.mp4
mp4v2需要的头文件  => sample_libmp4v2\libmp4v2
mp4v2静态调试库    => sample_libmp4v2\libmp4v2\libmp4v2D.lib
mp4v2静态发行库    => sample_libmp4v2\libmp4v2\libmp4v2.lib
浩一科技代码辅助库 => sample_libmp4v2\common

    注意:在vs2010工程配置当中,需要在预处理定义中加入 MP4V2_EXPORTS,否则,会在编译中出现链接失败的问题。

关键代码:(详见Csample_libmp4v2Dlg)

1、mp4v2提供的调试回调演示:

void myMP4LogCallback( MP4LogLevel loglevel, const char* fmt, va_list ap)
{
	// 组合传递过来的格式...
	CString	strDebug;
	strDebug.FormatV(fmt, ap);
	if( (strDebug.ReverseFind('\r') < 0) && (strDebug.ReverseFind('\n') < 0) ) {
		strDebug.Append("\r\n");
	}
	// 进行格式转换,并打印出来...
	string strANSI = CUtilTool::UTF8_ANSI(strDebug);
	TRACE(strANSI.c_str());
}

// 解析mp4文件...
void Csample_libmp4v2Dlg::OnBnClickedButtonParse()
{
	// 设置MP4调试级别 => 最高最详细级别...
	MP4LogLevel theLevel = MP4LogGetLevel();
	MP4LogSetLevel(MP4_LOG_VERBOSE4);
	MP4SetLogCallback(myMP4LogCallback);
	// 打开MP4文件...
	CString strMP4File;
	MP4FileHandle hMP4Handle = MP4_INVALID_FILE_HANDLE;
	strMP4File.Format("%s\\sample.mp4", CUtilTool::GetExePath((HINSTANCE)NULL).c_str());
	string strUTF8 = CUtilTool::ANSI_UTF8(strMP4File);
	hMP4Handle = MP4Read( strUTF8.c_str() );
	MP4Close(hMP4Handle);
}

    说明:这个调试输出,可以帮助在分析特殊mp4文件时使用,会打印出每一个解析mp4过程的细节,级别越高越详细。

2、有关mp4路径问题:

    libmp4v2在文件路径上,需要使用完整的UTF8格式的全路径,因此,我们借助了辅助库来完成。
    注意:在工程配置上我们选择的是 多字符集(ANSI)。
CString strMP4File;
MP4FileHandle hMP4Handle = MP4_INVALID_FILE_HANDLE;
strMP4File.Format("%s\\sample.mp4", CUtilTool::GetExePath((HINSTANCE)NULL).c_str());
string strUTF8 = CUtilTool::ANSI_UTF8(strMP4File);
hMP4Handle = MP4Read( strUTF8.c_str() );
string CUtilTool::ANSI_UTF8(LPCTSTR lpSValue)
{
	string strUValue;
	USES_CONVERSION;
	if( lpSValue == NULL || strlen(lpSValue) <= 0 )
		return strUValue;	// 验证有效性
	_acp = CP_ACP;                  // 设置code page(默认)
	// A2W 在某些系统下会发生崩溃...
	// LPCWSTR lpWValue = A2W(lpSValue); // ANSI to Unicode Wide char
	int dwSize = MultiByteToWideChar(_acp, 0, lpSValue, -1, NULL, 0);
	wchar_t * lpWValue = new wchar_t[dwSize];
	MultiByteToWideChar(_acp, 0, lpSValue, -1, lpWValue, dwSize);
	_acp = CP_UTF8;                     // 设置code page, 得到编码长度, Unicode Wide char to UTF-8
	int ret = WideCharToMultiByte(_acp, 0, lpWValue, -1, NULL, NULL, NULL, NULL);
	strUValue.resize(ret); ASSERT( ret == strUValue.size() );
	ret = WideCharToMultiByte(_acp, 0, lpWValue, -1, (LPSTR)strUValue.c_str(), strUValue.size(), NULL, NULL);
	ASSERT( ret == strUValue.size() );
	delete [] lpWValue;
	return strUValue;
}

3、解析音视频格式信息头:

bool Csample_libmp4v2Dlg::doMP4ParseAV(MP4FileHandle inFile)
{
	if( inFile == MP4_INVALID_FILE_HANDLE )
		return false;
	ASSERT( inFile != MP4_INVALID_FILE_HANDLE );
	// 首先获取文件的每秒刻度数和总刻度数(不是毫秒数)...
	uint32_t dwFileScale = MP4GetTimeScale(inFile);
	MP4Duration theDuration = MP4GetDuration(inFile);
	TRACE("=== 文件每秒刻度数:%lu ===\n", dwFileScale);
	TRACE("=== 文件总刻度数:%lu ===\n", theDuration);
	// 总毫秒数 = 总刻度数*1000/每秒刻度数 => 先乘法可以降低误差...
	uint32_t		dwMP4Duration = theDuration*1000/dwFileScale;
	TRACE("=== 总毫秒数 = 文件总刻度数*1000/文件每秒刻度数 => %lu ===\n", dwMP4Duration);
	MP4TrackId		tidVideo = MP4_INVALID_TRACK_ID;	// 视频轨道编号
	MP4TrackId		tidAudio = MP4_INVALID_TRACK_ID;	// 音频轨道编号
	string			strSPS, strPPS, strAES;
	int				audio_rate_index = 0;
	int				audio_channel_num = 0;
	int				audio_type = 0;
	uint32_t		audio_time_scale = 0;
	uint32_t		video_time_scale = 0;
	// 获取需要的相关信息...
    uint32_t trackCount = MP4GetNumberOfTracks( inFile );
	TRACE("=== 发现轨道数:%lu ===\n", trackCount);
    for( uint32_t i = 0; i < trackCount; ++i ) {
		MP4TrackId  id = MP4FindTrackId( inFile, i );
		const char* type = MP4GetTrackType( inFile, id );
		if( MP4_IS_VIDEO_TRACK_TYPE( type ) ) {
			// 视频已经有效,检测下一个...
			if( tidVideo > 0 )
				continue;
			// 获取视频信息...
			tidVideo = id;
			TRACE("=== 视频轨道号:%d,标识:%s ===\n", tidVideo, type);
			char * lpVideoInfo = MP4Info(inFile, id);
			TRACE("视频信息:%s \n", lpVideoInfo);
			free(lpVideoInfo);
			// 获取视频时间间隔...
			video_time_scale = MP4GetTrackTimeScale(inFile, id);
			TRACE("=== 视频每秒刻度数:%lu ===\n", video_time_scale);
			const char * video_name = MP4GetTrackMediaDataName(inFile, id);
			TRACE("=== 视频编码:%s ===\n", video_name);
			// 获取视频的PPS/SPS信息...
			uint8_t  ** spsHeader = NULL;
			uint8_t  ** ppsHeader = NULL;
			uint32_t  * spsSize = NULL;
			uint32_t  * ppsSize = NULL;
			uint32_t    ix = 0;
			bool bResult = MP4GetTrackH264SeqPictHeaders(inFile, id, &spsHeader, &spsSize, &ppsHeader, &ppsSize);
			for(ix = 0; spsSize[ix] != 0; ++ix) {
				// SPS指针和长度...
				uint8_t * lpSPS = spsHeader[ix];
				uint32_t  nSize = spsSize[ix];
				// 只存储第一个SPS信息...
				if( strSPS.size() <= 0 && nSize > 0 ) {
					strSPS.assign((char*)lpSPS, nSize);
				}
				free(spsHeader[ix]);
				TRACE("=== 找到第 %d 个SPS格式头,长度:%lu ===\n", ix+1, nSize);
			}
			free(spsHeader);
			free(spsSize);
			for(ix = 0; ppsSize[ix] != 0; ++ix) {
				// PPS指针和长度...
				uint8_t * lpPPS = ppsHeader[ix];
				uint32_t  nSize = ppsSize[ix];
				// 只存储第一个PPS信息...
				if( strPPS.size() <= 0 && nSize > 0 ) {
					strPPS.assign((char*)lpPPS, nSize);
				}
				free(ppsHeader[ix]);
				TRACE("=== 找到第 %d 个PPS格式头,长度:%lu ===\n", ix+1, nSize);
			}
			free(ppsHeader);
			free(ppsSize);
		} else if( MP4_IS_AUDIO_TRACK_TYPE( type ) ) {
			// 音频已经有效,检测下一个...
			if( tidAudio > 0 )
				continue;
			// 获取音频信息...
			tidAudio = id;
			TRACE("=== 音频轨道号:%d,标识:%s ===\n", tidVideo, type);
			char * lpAudioInfo = MP4Info(inFile, id);
			TRACE("音频信息:%s \n", lpAudioInfo);
			free(lpAudioInfo);

			// 获取音频的类型/名称/采样率/声道信息...
			audio_type = MP4GetTrackAudioMpeg4Type(inFile, id);
			TRACE("=== 音频格式类型:%d ===\n", audio_type);
			audio_channel_num = MP4GetTrackAudioChannels(inFile, id);
			TRACE("=== 音频声道数:%d ===\n", audio_channel_num);
			const char * audio_name = MP4GetTrackMediaDataName(inFile, id);
			TRACE("=== 音频编码:%s ===\n", audio_name);
			audio_time_scale = MP4GetTrackTimeScale(inFile, id);
			TRACE("=== 音频每秒刻度数(采样率):%lu ===\n", audio_time_scale);
			if (audio_time_scale == 48000)
				audio_rate_index = 0x03;
			else if (audio_time_scale == 44100)
				audio_rate_index = 0x04;
			else if (audio_time_scale == 32000)
				audio_rate_index = 0x05;
			else if (audio_time_scale == 24000)
				audio_rate_index = 0x06;
			else if (audio_time_scale == 22050)
				audio_rate_index = 0x07;
			else if (audio_time_scale == 16000)
				audio_rate_index = 0x08;
			else if (audio_time_scale == 12000)
				audio_rate_index = 0x09;
			else if (audio_time_scale == 11025)
				audio_rate_index = 0x0a;
			else if (audio_time_scale == 8000)
				audio_rate_index = 0x0b;
			TRACE("=== 音频采样率编号:%lu ===\n", audio_rate_index);
			// 获取音频扩展信息...
            uint8_t  * pAES = NULL;
            uint32_t   nSize = 0;
            bool haveEs = MP4GetTrackESConfiguration(inFile, id, &pAES, &nSize);
			// 存储音频扩展信息...
			if( strAES.size() <= 0 && nSize > 0 ) {
				strAES.assign((char*)pAES, nSize);
			}
			TRACE("=== 音频扩展信息长度:%lu ===\n", nSize);
			// 释放分配的缓存...
			if( pAES != NULL ) {
				free(pAES);
				pAES = NULL;
			}
		}
	}
	// 如果音频和视频都没有,返回失败...
	if((tidVideo == MP4_INVALID_TRACK_ID) && (tidAudio == MP4_INVALID_TRACK_ID)) {
		MsgLogGM(GM_File_Read_Err);
		return false;
	}
	// 一切正常,循环抽取音视频数据帧...
	return this->doMP4ReadFrame(inFile, tidVideo, tidAudio);
}
    注意:这里有3个每秒刻度数,文件每秒刻度数,视频每秒刻度数,音频每秒刻度数(采样率),越大说明粒度越系,更精确。这个每秒刻度数用来还原数据帧的时间戳很关键。

4、循环抽取并打印音视频数据帧:

// 循环抽取音视频数据帧...
bool Csample_libmp4v2Dlg::doMP4ReadFrame(MP4FileHandle inFile, MP4TrackId inVideoID, MP4TrackId inAudioID)
{
	BOOL		bAudioComplete = false;
	BOOL		bVideoComplete = false;
	uint32_t	dwOutVSendTime = 0;
	uint32_t	dwOutASendTime = 0;
	uint32_t	iVSampleInx = 1;
	uint32_t	iASampleInx = 1;
	while( true ) {
		// 读取一帧视频帧...
		if( !bVideoComplete && !this->doMP4ReadOneFrame(inFile, inVideoID, iVSampleInx++, true, dwOutVSendTime) ) {
			bVideoComplete = true;
			continue;
		}
		// 读取一帧音频帧...
		if( !bAudioComplete && !this->doMP4ReadOneFrame(inFile, inAudioID, iASampleInx++, false, dwOutASendTime) ) {
			bAudioComplete = true;
			continue;
		}
		// 如果音频和视频全部结束,退出循环...
		if( bVideoComplete && bAudioComplete ) {
			TRACE("=== MP4文件数据帧读取完毕 ===\n");
			TRACE("=== 已读取视频帧:%lu ===\n", iVSampleInx);
			TRACE("=== 已读取音频帧:%lu ===\n", iASampleInx);
			break;
		}
	}
	return true;
}

// 抽取一帧音频或视频数据帧...
bool Csample_libmp4v2Dlg::doMP4ReadOneFrame(MP4FileHandle inFile, MP4TrackId tid, uint32_t sid, bool bIsVideo, uint32_t & outSendTime)
{
	uint8_t		*	pSampleData = NULL;		// 帧数据指针
	uint32_t		nSampleSize = 0;		// 帧数据长度
	MP4Timestamp	nStartTime = 0;			// 开始时间
	MP4Duration		nDuration = 0;			// 持续时间
	MP4Duration     nOffset = 0;			// 偏移时间
	bool			bIsKeyFrame = false;	// 是否关键帧
	uint32_t		timescale = 0;
	uint64_t		msectime = 0;

	// 读取轨道的采样率 和 当前帧的时间戳...
	timescale = MP4GetTrackTimeScale( inFile, tid );
	msectime = MP4GetSampleTime( inFile, tid, sid );

	// 读取一帧数据帧,失败,直接返回...
	if( false == MP4ReadSample(inFile, tid, sid, &pSampleData, &nSampleSize, &nStartTime, &nDuration, &nOffset, &bIsKeyFrame) )
		return false;

	// 计算当前读取数据帧的时间戳...
	// 计算发送时间 => PTS => 刻度时间转换成毫秒...
	msectime *= UINT64_C( 1000 );
	msectime /= timescale;
	// 计算开始时间 => DTS => 刻度时间转换成毫秒...
	nStartTime *= UINT64_C( 1000 );
	nStartTime /= timescale;
	// 计算偏差时间 => CTTS => 刻度时间转换成毫秒...
	nOffset *= UINT64_C( 1000 );
	nOffset /= timescale;

	// 这里需要释放读取的缓冲区...
	MP4Free(pSampleData);
	pSampleData = NULL;

	// 返回发送时间(毫秒) => 已将刻度时间转换成了毫秒...
	outSendTime = (uint32_t)msectime;
	
	// 打印获取的音视频数据帧内容信息...
	TRACE("[%s] duration = %I64d, offset = %I64d, KeyFrame = %d, SendTime = %lu, StartTime = %I64d, Size = %lu\n", bIsVideo ? "Video" : "Audio", nDuration, nOffset, bIsKeyFrame, outSendTime, nStartTime, nSampleSize);

	return true;
}

    注意:利用mp4v2抽取的一帧(sample)数据,会返回5个时间,都是刻度时间,都要转换成毫秒。

    timescale => 每秒刻度数
    PTS  => 显示时间 => msectime*1000 /= timescale
    DTS  => 解码时间 => nStartTime*1000 /= timescale
    CTTS => 偏差时间 => nOffset*1000 /= timescale
    Duration => 这一帧持续时间 => nDuration*1000 /= timescale

更多信息:

************************************************************
 * 浩一科技,提供云监控、云录播的全平台无插件解决方案。
 * 支持按需直播,多点布控,分布式海量存储,动态扩容;
 * 支持微信扫码登录,全平台帐号统一,关联微信小程序;
 * 支持多种数据输入:摄像头IPC、rtmp、rtsp、MP4文件;
 * 支持全实时、全动态、全网页管理,网页前后台兼容IE8;
 * 支持多终端无插件自适应播放,flvjs/hls/rtmp自动适配;
************************************************************
 * 官方网站 => https://myhaoyi.com
 * 技术博客 => http://blog.csdn.net/haoyitech
 * 开源代码 => https://github.com/HaoYiTech/

************************************************************

你可能感兴趣的:(MP4)