如何使用mp4v2将H264+AAC裸流录制成mp4文件,并保持音视频同步【源码】【mp4】【录像】

前言:

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

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

    上一篇文章已经使用mp4v2从mp4文件中抽取音视频数据帧(文章),本篇文章讲述如何将音视频数据帧(AAC+H264)保存成mp4文件,并保持音视频同步。

源码下载:

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

源码说明:

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

    总体思路:从标准mp4文件,使用mp4v2提供的标准API,解析出音视频格式信息,视频需要获取关键的PPS和SPS,音频需要获取采样率、声道、扩展信息等等。将这些格式信息重组之后用来创建音视频轨道。

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

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

关键代码:(详见Csample_save_mp4Dlg)

1、mp4v2提供的调试回调演示:详见上一篇文章

2、有关mp4路径问题:详见上一篇文章

3、解析音视频格式信息头:详见上一篇文章

4、循环抽取并打印音视频数据帧:详见上一篇文章

5、创建视频轨道:

bool Csample_save_mp4Dlg::BuildVideoTrack(string & inSPS, string & inPPS)
{
	if( m_lpRecMP4 == NULL || inSPS.size() <= 0 || inPPS.size() <= 0 )
		return false;
	// 获取 width 和 height...
	int	nPicWidth = 0;
	int	nPicHeight = 0;
	if( inSPS.size() >  0 ) {
		CSPSReader _spsreader;
		bs_t    s = {0};
		s.p		  = (uint8_t *)inSPS.c_str();
		s.p_start = (uint8_t *)inSPS.c_str();
		s.p_end	  = (uint8_t *)inSPS.c_str() + inSPS.size();
		s.i_left  = 8; // 这个是固定的,对齐长度...
		_spsreader.Do_Read_SPS(&s, &nPicWidth, &nPicHeight);
	}
	// 正在创建视频轨道...
	TRACE("=== 正在创建视频录制轨道,视频高:%lu,视频宽:%lu ===\n", nPicWidth, nPicHeight);
	// 调用封装接口,直接创建视频轨道...
	return m_lpRecMP4->CreateVideoTrack(m_strUTF8Rec.c_str(), VIDEO_TIME_SCALE, nPicWidth, nPicHeight, inSPS, inPPS);
}
bool LibMP4::CreateVideoTrack(const char* lpUTF8Name, int nVideoTimeScale, int width, int height, string & strSPS, string & strPPS)
{
	if( strSPS.size() <= 0 || strPPS.size() <= 0 || lpUTF8Name == NULL )
		return false;
	// 如果句柄为空,则创建MP4对象...
	if( m_hFileHandle == MP4_INVALID_FILE_HANDLE ) {
		m_hFileHandle = MP4Create(lpUTF8Name);
	}
	// 再次判断文件句柄...
	if( m_hFileHandle == MP4_INVALID_FILE_HANDLE )
		return false;
	// 设置TimeScale,时间刻度,通常为任意的固定数值...
	BYTE * lpSPS = (BYTE*)strSPS.c_str();
	uint32_t theTimeScale = nVideoTimeScale;
	MP4SetTimeScale(m_hFileHandle, theTimeScale);
	// 添加视频轨道, 不要用固定时间间隔,用每帧的时间间隔...
	// 这里的是视频时间间隔,一定要用 MP4_INVALID_DURATION,计算两帧之间的时间差的方案...
	// 这种方案可以满足各种信号来源的数据,用 fps 方式不精确...
	m_videoID = MP4AddH264VideoTrack(m_hFileHandle, 
									theTimeScale, 
									MP4_INVALID_DURATION, //theTimeScale/fps,
									width, height, 
									lpSPS[1],
									lpSPS[2],
									lpSPS[3],
									3);
	if( m_videoID == MP4_INVALID_TRACK_ID ) {
		MP4Close(m_hFileHandle);
		m_hFileHandle = MP4_INVALID_FILE_HANDLE;
		return false;
	}
	// 设置视频level...
	MP4SetVideoProfileLevel(m_hFileHandle, 0x7F);
	// 设置SPS/PPS...
	MP4AddH264SequenceParameterSet(m_hFileHandle, m_videoID, (BYTE*)strSPS.c_str(), strSPS.size());
	MP4AddH264PictureParameterSet(m_hFileHandle, m_videoID, (BYTE*)strPPS.c_str(), strPPS.size());
	m_nVideoTimeScale = nVideoTimeScale;
	return true;
}

注意:视频时间间隔,一定要用 MP4_INVALID_DURATION,实际在写入单帧时间时需要计算两帧之间的时间差,只有这样写入mp4文件才会是同步的。

6、创建音频轨道:

bool Csample_save_mp4Dlg::BuildAudioTrack(int nAudioRate, string & inAES)
{
	if( m_lpRecMP4 == NULL || nAudioRate <= 0 || inAES.size() <= 0 )
		return false;
	// 正在创建音频轨道...
	TRACE("=== 正在创建音频录制轨道,采样率:%lu ===\n", nAudioRate);
	// 调用封装接口,直接创建音频轨道...
	return m_lpRecMP4->CreateAudioTrack(m_strUTF8Rec.c_str(), nAudioRate, inAES);
}
bool LibMP4::CreateAudioTrack(const char* lpUTF8Name, int nAudioSampleRate, string & strAES)
{
	if( strAES.size() <= 0 || lpUTF8Name == NULL )
		return false;
	// 如果句柄为空,则创建MP4对象...
	if( m_hFileHandle == MP4_INVALID_FILE_HANDLE ) {
		m_hFileHandle = MP4Create(lpUTF8Name);
	}
	// 再次判断文件句柄...
	if( m_hFileHandle == MP4_INVALID_FILE_HANDLE )
		return false;
	// 添加音频轨道,音频时间刻度固定为 1024,否则会出现问题...
	// AAC 都是采用固定的刻度运转的...
	m_audioID = MP4AddAudioTrack(m_hFileHandle, nAudioSampleRate, 1024, MP4_MPEG4_AUDIO_TYPE);
	if( m_audioID == MP4_INVALID_TRACK_ID ) {
		MP4Close(m_hFileHandle);
		m_hFileHandle = MP4_INVALID_FILE_HANDLE;
		return false;
	}	
	// 设置音频 level 和 AES...
	MP4SetAudioProfileLevel(m_hFileHandle, 0x02);//0xFF);
	MP4SetTrackESConfiguration(m_hFileHandle, m_audioID, (BYTE*)strAES.c_str(), strAES.size());
	return true;
}

注意:AAC音频都是统一的固定时间刻度1024。

7、数据帧抽取方式:

// 循环抽取音视频数据帧...
bool Csample_save_mp4Dlg::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_save_mp4Dlg::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;

	// 返回发送时间(毫秒) => 已将刻度时间转换成了毫秒...
	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);

	// 直接对获取到音视频数据帧进行录像操作...
	if( m_lpRecMP4 != NULL ) {
		if( !m_lpRecMP4->WriteSample(bIsVideo, pSampleData, nSampleSize, outSendTime, nOffset, bIsKeyFrame) ) {
			TRACE("=== 录制MP4数据帧失败! ===\n");
		}
	}

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

	return true;
}

8、数据帧写入方式:

bool LibMP4::WriteSample(bool bIsVideo, BYTE * lpFrame, int nSize, uint32_t inTimeStamp, uint32_t inRenderOffset, bool bIsKeyFrame)
{
	if( m_hFileHandle == MP4_INVALID_FILE_HANDLE || lpFrame == NULL || nSize <= 0 )
		return false;
	if( bIsVideo && m_videoID == MP4_INVALID_TRACK_ID )
		return false;
	if( !bIsVideo && m_audioID == MP4_INVALID_TRACK_ID )
		return false;
	// 从文件中获取已写入秒数...
	m_dwWriteSec = this->GetDurationSecond();
	// 第一帧数据必须是视频关键帧...
	if( m_bFirstFrame ) {
		// 如果是音频,将数据帧缓存起来...
		// 注意:这里不能直接存音频,如果有视频,则会出现音视频不同步,因此,需要先缓存,如果确实没有视频再存盘。
		if( !bIsVideo ) {
			RTMPFrame theFrame;
			theFrame.m_bKeyFrame = bIsKeyFrame;
			theFrame.m_strData.assign((char*)lpFrame, nSize);
			theFrame.m_nTimeStamp = inTimeStamp;
			theFrame.m_nRenderOffset = inRenderOffset;
			m_deqAudio.push_back(theFrame);
			// 如果缓存了500帧之后,还没有视频,则认为只有音频,直接存盘...
			if( m_deqAudio.size() >= 500 ) {
				// 设置非第一帧标志...
				m_bFirstFrame = false;
				KH_DeqFrame::iterator itor;
				// 开始存储缓存的音频数据帧,使用固定的帧时间间隔...
				for(itor = m_deqAudio.begin(); itor != m_deqAudio.end(); ++itor) {
					RTMPFrame & myFrame = (*itor);
					if( itor == m_deqAudio.begin() ) {
						m_dwFirstStamp = myFrame.m_nTimeStamp;
					}
					MP4WriteSample(m_hFileHandle, m_audioID, (BYTE*)myFrame.m_strData.c_str(), myFrame.m_strData.size(), MP4_INVALID_DURATION, 0, myFrame.m_bKeyFrame);
					m_dwWriteSize += myFrame.m_strData.size();
					//m_dwWriteRecMS = myFrame.m_nTimeStamp - m_dwFirstStamp;
				}
				// 存盘之后,释放音频缓存...
				TRACE("[No Video] Audio-Deque = %d\n", m_deqAudio.size());
				m_deqAudio.clear();
			}
			return true;
		}
		// 视频非关键帧,直接丢弃...
		if( !bIsKeyFrame )
			return true;
		// 设置非第一帧标志...
		m_bFirstFrame = false;
		m_dwFirstStamp = inTimeStamp;
		// 之前缓存的音频数据,直接丢弃,否则会出现音视频不同步...
		TRACE("[Has Video] Audio-Deque = %d\n", m_deqAudio.size());
		m_deqAudio.clear();
	}
	// 准备一些共同的数据...
	MP4TrackId  theTrackID = bIsVideo ? m_videoID : m_audioID;
	bool		bWriteFlag = false;
	if( bIsVideo ) { 
		// 判断是否需要保存第一帧...
		if( m_VLastFrame.m_strData.size() <= 0 ) {
			m_VLastFrame.m_bKeyFrame = bIsKeyFrame;
			m_VLastFrame.m_strData.assign((char*)lpFrame, nSize);
			m_VLastFrame.m_nTimeStamp = inTimeStamp;
			m_VLastFrame.m_nRenderOffset = inRenderOffset;
			return true;
		}
		// 准备需要的数据内容...
		int			uFrameMS = inTimeStamp - m_VLastFrame.m_nTimeStamp;
		MP4Duration uDuration = uFrameMS * m_nVideoTimeScale / 1000;
		MP4Duration uOffset = m_VLastFrame.m_nRenderOffset * m_nVideoTimeScale / 1000;
		if( uFrameMS <= 0 ) uDuration = 1;
		//TRACE("Video-Duration = %I64d, offset = %I64d, size = %lu, keyFrame = %d\n", uDuration, uOffset, m_VLastFrame.m_strData.size(), m_VLastFrame.m_bKeyFrame);
		
		// 调用写入帧的接口函数,视频需要计算帧间隔...
		bWriteFlag = MP4WriteSample(m_hFileHandle, theTrackID, (BYTE*)m_VLastFrame.m_strData.c_str(), m_VLastFrame.m_strData.size(), uDuration, uOffset, m_VLastFrame.m_bKeyFrame);
		// 计算写盘量和总时间...
		m_dwWriteSize += m_VLastFrame.m_strData.size();
		//m_dwWriteRecMS = inTimeStamp - m_dwFirstStamp;
		
		// 保存这一帧的数据,下次使用...
		m_VLastFrame.m_bKeyFrame = bIsKeyFrame;
		m_VLastFrame.m_strData.assign((char*)lpFrame, nSize);
		m_VLastFrame.m_nTimeStamp = inTimeStamp;
		m_VLastFrame.m_nRenderOffset = inRenderOffset;
	} else {
		// 音频数据,直接调用接口写盘,音频不用计算帧间隔时间,采用固定的帧间隔...
		bWriteFlag = MP4WriteSample(m_hFileHandle, theTrackID, lpFrame, nSize, MP4_INVALID_DURATION, 0, bIsKeyFrame);
		// 计算写盘量和总时间...
		m_dwWriteSize += nSize;
		//m_dwWriteRecMS = ((inTimeStamp >= m_dwFirstStamp) ? (inTimeStamp - m_dwFirstStamp) : 0);
	}
	// 返回存盘结果...
	return bWriteFlag;
}

注意:第一帧优先写入视频数据帧,不要直接存音频,如果有视频,则会出现音视频不同步,因此,需要先缓存,如果确实没有视频再存盘。

更多信息:

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

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


你可能感兴趣的:(MP4)