MP4V2封装的类库,可将H264和AAC直接打包到MP4容器中,堪称经典

时间:2022-11-16 09:29:52

废话少说,直接上代码:

// MP4Encoder.h
#pragma once

#ifndef _MP4V2_H_
#define _MP4V2_H_
#include "mp4v2/mp4v2.h"
#endif

#define MP4ENCODER_ERROR(err) ((MP4EncoderResult)(-(err)))
#define DEFAULT_RECORD_TIME 0U

typedef enum
{
MP4ENCODER_ENONE = 0,
MP4ENCODER_E_CREATE_FAIL,
MP4ENCODER_E_ADD_VIDEO_TRACK,
MP4ENCODER_E_ADD_AUDIO_TRACK,
MP4ENCODER_WARN_RECORD_OVER,
MP4ENCODER_E_WRITE_VIDEO_DATA,
MP4ENCODER_E_WRITE_AUDIO_DATA,
MP4ENCODER_E_ALLOC_MEMORY_FAILED,
MP4ENCODER_E_UNKONOWN
}MP4EncoderResult;

class MP4Encoder
{
public:
MP4Encoder(void);
~MP4Encoder(void);
MP4EncoderResult MP4CreateFile(const char *sFileName,
unsigned uRecordTime = DEFAULT_RECORD_TIME);
MP4EncoderResult MP4AddH264Track(const uint8_t *sData, int nSize,
int nWidth, int nHeight, int nFrameRate = 25);
MP4EncoderResult MP4AddAACTrack(const uint8_t *sData, int nSize);
MP4EncoderResult MP4WriteH264Data(uint8_t *sData, int nSize, uint64_t u64PTS);
MP4EncoderResult MP4WriteAACData(const uint8_t *sData, int nSize,
uint64_t u64PTS);
void MP4ReleaseFile();
private:
unsigned m_uSecond;
MP4FileHandle m_hFile;
bool m_bFirstAudio, m_bFirstVideo;
MP4TrackId m_videoTrack, m_audioTrack;
uint64_t m_u64AudioPTS, m_u64VideoPTS, m_u64FirstPTS, m_u64LastPTS;
};

实现部分:

// MP4Encoder.cpp
#pragma once
#include "MP4Encoder.h"
#pragma comment(lib, "../lib/libmp4v2.lib")

#define MIN_FRAME_SIZE 32
#define VIDEO_TIME_SCALE 90000
#define AUDIO_TIME_SCALE 8000
#define MOVIE_TIME_SCALE VIDEO_TIME_SCALE
#define PTS2TIME_SCALE(CurPTS, PrevPTS, timeScale) \
((MP4Duration)((CurPTS - PrevPTS) * 1.0 / (double)(1e+6) * timeScale))
#define INVALID_PTS 0xFFFFFFFFFFFFFFFF
/* Warning: Followings are magic data originally */
#define DEFAULT_VIDEO_TRACK_NUM 3
#define DEFAULT_VIDEO_PROFILE_LEVEL 1
#define DEFAULT_AUDIO_PROFILE_LEVEL 2

MP4Encoder::MP4Encoder(void)
: m_hFile(MP4_INVALID_FILE_HANDLE)
, m_bFirstVideo(true)
, m_bFirstAudio(true)
, m_uSecond(DEFAULT_RECORD_TIME)
, m_videoTrack(MP4_INVALID_TRACK_ID)
, m_audioTrack(MP4_INVALID_TRACK_ID)
, m_u64VideoPTS(0)
, m_u64AudioPTS(0)
, m_u64FirstPTS(INVALID_PTS)
, m_u64LastPTS(INVALID_PTS)
{
}


MP4Encoder::~MP4Encoder(void)
{
}

MP4EncoderResult MP4Encoder::MP4CreateFile(const char *sFileName,
unsigned uRecordTime /* = DEFAULT_RECORD_TIME */)
{
m_hFile = MP4Create(sFileName);
if (m_hFile == MP4_INVALID_FILE_HANDLE)
return MP4ENCODER_ERROR(MP4ENCODER_E_CREATE_FAIL);
if (!MP4SetTimeScale(m_hFile, MOVIE_TIME_SCALE))
return MP4ENCODER_ERROR(MP4ENCODER_E_CREATE_FAIL);
m_uSecond = uRecordTime;
return MP4ENCODER_ENONE;
}

MP4EncoderResult MP4Encoder::MP4AddH264Track(const uint8_t *sData, int nSize,
int nWidth, int nHeight, int nFrameRate/* = 25 */)
{
int sps, pps;
for (sps = 0; sps < nSize;)
if (sData[sps++] == 0x00 && sData[sps++] == 0x00 && sData[sps++] == 0x00
&& sData[sps++] == 0x01)
break;
for (pps = sps; pps < nSize;)
if (sData[pps++] == 0x00 && sData[pps++] == 0x00 && sData[pps++] == 0x00
&& sData[pps++] == 0x01)
break;
if (sps >= nSize || pps >= nSize)
return MP4ENCODER_ERROR(MP4ENCODER_E_ADD_VIDEO_TRACK);

m_videoTrack = MP4AddH264VideoTrack(m_hFile, VIDEO_TIME_SCALE,
VIDEO_TIME_SCALE / nFrameRate, nWidth, nHeight,
sData[sps + 1], sData[sps + 2], sData[sps + 3], DEFAULT_VIDEO_TRACK_NUM);
if (MP4_INVALID_TRACK_ID == m_videoTrack)
return MP4ENCODER_ERROR(MP4ENCODER_E_ADD_VIDEO_TRACK);

MP4SetVideoProfileLevel(m_hFile, DEFAULT_VIDEO_PROFILE_LEVEL);
MP4AddH264SequenceParameterSet(m_hFile, m_videoTrack, sData + sps,
pps - sps - 4);
MP4AddH264PictureParameterSet(m_hFile, m_videoTrack, sData + pps,
nSize - pps);

return MP4ENCODER_ENONE;
}

MP4EncoderResult MP4Encoder::MP4AddAACTrack(const uint8_t *sData, int nSize)
{
m_audioTrack = MP4AddAudioTrack(m_hFile, AUDIO_TIME_SCALE,
/**
* In fact, this is not a magic number. A formula might be:
* SampleRate * ChannelNum * 2 / SampleFormat
* 8000 * 1 * 2 / 16 (字节对齐,这里是 AV_SAMPLE_FMT_S16)
*/
AUDIO_TIME_SCALE / 8, MP4_MPEG4_AUDIO_TYPE);
if (MP4_INVALID_TRACK_ID == m_audioTrack)
return MP4ENCODER_ERROR(MP4ENCODER_E_ADD_AUDIO_TRACK);
MP4SetAudioProfileLevel(m_hFile, DEFAULT_AUDIO_PROFILE_LEVEL);
if (!MP4SetTrackESConfiguration(m_hFile, m_audioTrack, sData, nSize))
return MP4ENCODER_ERROR(MP4ENCODER_E_ADD_AUDIO_TRACK);

return MP4ENCODER_ENONE;
}

MP4EncoderResult MP4Encoder::MP4WriteH264Data(uint8_t *sData, int nSize, uint64_t u64PTS)
{
if (nSize < MIN_FRAME_SIZE)
return MP4ENCODER_ENONE;
bool result = false;
sData[0] = (nSize - 4) >> 24;
sData[1] = (nSize - 4) >> 16;
sData[2] = (nSize - 4) >> 8;
sData[3] = nSize - 4;
if (m_bFirstVideo)
{
if (m_u64FirstPTS > u64PTS)
m_u64FirstPTS = u64PTS;
m_u64VideoPTS = u64PTS;
m_bFirstVideo = false;
}
if ((sData[4] & 0x0F) == 5)
result = MP4WriteSample(m_hFile, m_videoTrack, sData, nSize,
PTS2TIME_SCALE(u64PTS, m_u64VideoPTS, VIDEO_TIME_SCALE));
else
result = MP4WriteSample(m_hFile, m_videoTrack, sData, nSize,
PTS2TIME_SCALE(u64PTS, m_u64VideoPTS, VIDEO_TIME_SCALE), 0, false);
if (!result)
return MP4ENCODER_ERROR(MP4ENCODER_E_WRITE_VIDEO_DATA);
m_u64LastPTS = m_u64VideoPTS = u64PTS;
if (m_uSecond && (m_u64LastPTS - m_u64FirstPTS) / (1e+6) >= m_uSecond)
return MP4ENCODER_ERROR(MP4ENCODER_WARN_RECORD_OVER);
return MP4ENCODER_ENONE;
}

MP4EncoderResult MP4Encoder::MP4WriteAACData(const uint8_t *sData, int nSize,
uint64_t u64PTS)
{
if (nSize < MIN_FRAME_SIZE)
return MP4ENCODER_ENONE;
bool result = false;
if (m_bFirstAudio)
{
if (m_u64FirstPTS > u64PTS)
m_u64FirstPTS = u64PTS;
m_u64AudioPTS = u64PTS;
m_bFirstAudio = false;
}
result = MP4WriteSample(m_hFile, m_audioTrack, sData, nSize,
PTS2TIME_SCALE(u64PTS, m_u64AudioPTS, AUDIO_TIME_SCALE));
if (!result)
return MP4ENCODER_ERROR(MP4ENCODER_E_WRITE_AUDIO_DATA);
m_u64LastPTS = m_u64AudioPTS = u64PTS;
if (m_uSecond && (m_u64LastPTS - m_u64FirstPTS) / (1e+6) >= m_uSecond)
return MP4ENCODER_ERROR(MP4ENCODER_WARN_RECORD_OVER);
return MP4ENCODER_ENONE;
}

void MP4Encoder::MP4ReleaseFile()
{
if (m_hFile != MP4_INVALID_FILE_HANDLE)
{
MP4Close(m_hFile);
m_hFile = MP4_INVALID_FILE_HANDLE;
}
}

注意,这个是 PC 版本,Android 版本要修改 MP4WriteSample 中的 render!

但是问题又出现了,官方文档告知我们:如果是写视频数据,那么 MP4WriteSample 中的 render 是是不应该设置为默认值 0 的,因为有可能出现音视频不同步,结果我按照说明传递了参数,在手机上同步的更好,但是文件导出到电脑上播放时就会有音视频不同步的问题,这是不是官方的 bug 呢?期待有人去向官方反馈一下,因此现在我们就设置为默认值了,但是不保证以后是否要更改!