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)
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过程的细节,级别越高越详细。
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;
}
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个每秒刻度数,文件每秒刻度数,视频每秒刻度数,音频每秒刻度数(采样率),越大说明粒度越系,更精确。这个每秒刻度数用来还原数据帧的时间戳很关键。
// 循环抽取音视频数据帧...
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
更多信息:
************************************************************************************************************************