废话少说,直接上代码:
// 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 呢?期待有人去向官方反馈一下,因此现在我们就设置为默认值了,但是不保证以后是否要更改!