.264視頻文件封裝成.MP4方法
需求:
海康威視輸出的視頻格式為.264格式,而html5端對其不支持,所以需要將其封裝成. mp4格式。
Tips:我們常常提到的. mp4格式視頻,其實指的是一種容器(或者說集合體),包括視頻、音頻,甚至是字幕等。而.264是指一種視頻的編碼方式,起壓縮作用。所以將.264文件轉換成.mp4文件,其實就是一個解碼的過程。
思路:
網上已經提供解決方案,采用ffmpeg庫,先將.264文件解碼,再編碼生成.mp4文件,但這種方式效率較低,10M的視頻可能需要幾秒鍾才能完成。另一種方式根據MP4文件協議直接將H264包封裝成MP4格式,由於是直接基於MP4的封裝,因而效率很高。在Google Code上找到一個開源的MP4編解碼庫Mp4v2(https://code.google.com/p/mp4v2/),通過Mp4v2可以很方便的將H264編碼成MP4格式文件。為了方便使用,基於該庫封裝了一個MP4Encoder類。
但上述解決方案采用的是C++語言編寫,要想在C#代碼中實現對其進行調用,有兩種解決方案:① webservice ,② 動態鏈接庫(dll)。由於C++編寫webservice較為困難(涉及到套接字等),故本文采用了生成dll方式,但方法的缺點是,即只要源代碼做改動,就得重新生成一遍,好在本文涉及的C++程序不需要做變動。
利用Swig工具可以很方便地實現上述生成dll需求。SWIG是個幫助使用C或者C++編寫的軟件能與其它各種高級編程語言進行嵌入聯接的開發工具。SWIG能應用於各種不同類型的語言包括常用腳本編譯語言例如Perl, PHP, Python, Tcl, Ruby and PHP。支持語言列表中也包括非腳本編譯語言,例如C#, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), Java, Modula-3, OCAML以及R,甚至是編譯器或者匯編的計划應用(Guile, MzScheme, Chicken)。SWIG普遍應用於創建高級語言解析或匯編程序環境,用戶接口,作為一種用來測試C/C++或進行原型設計的工具。SWIG還能夠導出XML或Lisp s-expressions格式的解析樹。SWIG可以被自由使用,發布,修改用於商業或非商業中。
實現過程:
- 1. C++實現格式轉換的具體過程:
新建C++ win32空項目DLL,如”videoWrapCPlus”,

選擇“DLL”應用類型:

新建“MP4Encoder”類:

修改MP4Encoder.h文件,代碼如下:
/********************************************************************
filename: MP4Encoder.h
created: 2016-04-14
author: TangSir
purpose: MP4編碼器,基於開源庫mp4v2實現(https://code.google.com/p/mp4v2/)。
*********************************************************************/
#pragma once
#include "mp4v2\mp4v2.h"
// NALU單元
typedef struct _MP4ENC_NaluUnit
{
int type;
int size;
unsigned char *data;
}MP4ENC_NaluUnit;
typedef struct _MP4ENC_Metadata
{
// video, must be h264 type
unsigned int nSpsLen;
unsigned char Sps[1024];
unsigned int nPpsLen;
unsigned char Pps[1024];
} MP4ENC_Metadata,*LPMP4ENC_Metadata;
class MP4Encoder
{
public:
MP4Encoder(void);
~MP4Encoder(void);
public:
// open or creat a mp4 file.
MP4FileHandle CreateMP4File(const char *fileName,int width,int height,int timeScale = 90000,int frameRate = 25);
// wirte 264 metadata in mp4 file.
bool Write264Metadata(MP4FileHandle hMp4File,LPMP4ENC_Metadata lpMetadata);
// wirte 264 data, data can contain multiple frame.
int WriteH264Data(MP4FileHandle hMp4File,const unsigned char* pData,int size);
// close mp4 file.
void CloseMP4File(MP4FileHandle hMp4File);
// convert H264 file to mp4 file.
// no need to call CreateMP4File and CloseMP4File,it will create/close mp4 file automaticly.
bool WriteH264File(const char* pFile264,const char* pFileMp4);
// Prase H264 metamata from H264 data frame
static bool PraseMetadata(const unsigned char* pData,int size,MP4ENC_Metadata &metadata);
private:
// read one nalu from H264 data buffer
static int ReadOneNaluFromBuf(const unsigned char *buffer,unsigned int nBufferSize,unsigned int offSet,MP4ENC_NaluUnit &nalu);
private:
int m_nWidth;
int m_nHeight;
int m_nFrameRate;
int m_nTimeScale;
MP4TrackId m_videoId;
};
修改MP4Encoder.cpp文件,代碼如下:
/********************************************************************
filename: MP4Encoder.cpp
created: 2016-04-14
author: TangSir
purpose: MP4編碼器,基於開源庫mp4v2實現(https://code.google.com/p/mp4v2/)。
*********************************************************************/
#include "stdafx.h"
#include "MP4Encoder.h"
#include <string.h>
#pragma comment(lib,"../lib/libmp4v2.lib")
#define BUFFER_SIZE (1024*1024)
MP4Encoder::MP4Encoder(void):
m_videoId(NULL),
m_nWidth(0),
m_nHeight(0),
m_nTimeScale(0),
m_nFrameRate(0)
{
}
MP4Encoder::~MP4Encoder(void)
{
}
MP4FileHandle MP4Encoder::CreateMP4File(const char *pFileName,int width,int height,int timeScale/* = 90000*/,int frameRate/* = 25*/)
{
if(pFileName == NULL)
{
return false;
}
// create mp4 file
MP4FileHandle hMp4file = MP4Create(pFileName);
if (hMp4file == MP4_INVALID_FILE_HANDLE)
{
printf("ERROR:Open file fialed.\n");
return false;
}
m_nWidth = width;
m_nHeight = height;
m_nTimeScale = 90000;
m_nFrameRate = 25;
MP4SetTimeScale(hMp4file, m_nTimeScale);
return hMp4file;
}
bool MP4Encoder::Write264Metadata(MP4FileHandle hMp4File,LPMP4ENC_Metadata lpMetadata)
{
m_videoId = MP4AddH264VideoTrack
(hMp4File,
m_nTimeScale,
m_nTimeScale / m_nFrameRate,
m_nWidth, // width
m_nHeight,// height
lpMetadata->Sps[1], // sps[1] AVCProfileIndication
lpMetadata->Sps[2], // sps[2] profile_compat
lpMetadata->Sps[3], // sps[3] AVCLevelIndication
3); // 4 bytes length before each NAL unit
if (m_videoId == MP4_INVALID_TRACK_ID)
{
printf("add video track failed.\n");
return false;
}
MP4SetVideoProfileLevel(hMp4File, 0x01); // Simple Profile @ Level 3
// write sps
MP4AddH264SequenceParameterSet(hMp4File,m_videoId,lpMetadata->Sps,lpMetadata->nSpsLen);
// write pps
MP4AddH264PictureParameterSet(hMp4File,m_videoId,lpMetadata->Pps,lpMetadata->nPpsLen);
return true;
}
int MP4Encoder::WriteH264Data(MP4FileHandle hMp4File,const unsigned char* pData,int size)
{
if(hMp4File == NULL)
{
return -1;
}
if(pData == NULL)
{
return -1;
}
MP4ENC_NaluUnit nalu;
int pos = 0, len = 0;
while (len = ReadOneNaluFromBuf(pData,size,pos,nalu))
{
if(nalu.type == 0x07) // sps
{
// 添加h264 track
m_videoId = MP4AddH264VideoTrack
(hMp4File,
m_nTimeScale,
m_nTimeScale / m_nFrameRate,
m_nWidth, // width
m_nHeight, // height
nalu.data[1], // sps[1] AVCProfileIndication
nalu.data[2], // sps[2] profile_compat
nalu.data[3], // sps[3] AVCLevelIndication
3); // 4 bytes length before each NAL unit
if (m_videoId == MP4_INVALID_TRACK_ID)
{
printf("add video track failed.\n");
return 0;
}
MP4SetVideoProfileLevel(hMp4File, 1); // Simple Profile @ Level 3
MP4AddH264SequenceParameterSet(hMp4File,m_videoId,nalu.data,nalu.size);
}
else if(nalu.type == 0x08) // pps
{
MP4AddH264PictureParameterSet(hMp4File,m_videoId,nalu.data,nalu.size);
}
else
{
int datalen = nalu.size+4;
unsigned char *data = new unsigned char[datalen];
// MP4 Nalu前四個字節表示Nalu長度
data[0] = nalu.size>>24;
data[1] = nalu.size>>16;
data[2] = nalu.size>>8;
data[3] = nalu.size&0xff;
memcpy(data+4,nalu.data,nalu.size);
if(!MP4WriteSample(hMp4File, m_videoId, data, datalen,MP4_INVALID_DURATION, 0, 1))
{
return 0;
}
delete[] data;
}
pos += len;
}
return pos;
}
int MP4Encoder::ReadOneNaluFromBuf(const unsigned char *buffer,unsigned int nBufferSize,unsigned int offSet,MP4ENC_NaluUnit &nalu)
{
int i = offSet;
while(i<nBufferSize)
{
if(buffer[i++] == 0x00 &&
buffer[i++] == 0x00 &&
buffer[i++] == 0x00 &&
buffer[i++] == 0x01
)
{
int pos = i;
while (pos<nBufferSize)
{
if(buffer[pos++] == 0x00 &&
buffer[pos++] == 0x00 &&
buffer[pos++] == 0x00 &&
buffer[pos++] == 0x01
)
{
break;
}
}
if(pos == nBufferSize)
{
nalu.size = pos-i;
}
else
{
nalu.size = (pos-4)-i;
}
nalu.type = buffer[i]&0x1f;
nalu.data =(unsigned char*)&buffer[i];
return (nalu.size+i-offSet);
}
}
return 0;
}
void MP4Encoder::CloseMP4File(MP4FileHandle hMp4File)
{
if(hMp4File)
{
MP4Close(hMp4File);
hMp4File = NULL;
}
}
bool MP4Encoder::WriteH264File(const char* pFile264,const char* pFileMp4)
{
if(pFile264 == NULL || pFileMp4 == NULL)
{
return false;
}
MP4FileHandle hMp4File = CreateMP4File(pFileMp4,352,288);
if(hMp4File == NULL)
{
printf("ERROR:Create file failed!");
return false;
}
FILE *fp = fopen(pFile264, "rb");
if(!fp)
{
printf("ERROR:open file failed!");
return false;
}
fseek(fp, 0, SEEK_SET);
unsigned char *buffer = new unsigned char[BUFFER_SIZE];
int pos = 0;
while(1)
{
int readlen = fread(buffer+pos, sizeof(unsigned char), BUFFER_SIZE-pos, fp);
if(readlen<=0)
{
break;
}
readlen += pos;
int writelen = 0;
for(int i = readlen-1; i>=0; i--)
{
if(buffer[i--] == 0x01 &&
buffer[i--] == 0x00 &&
buffer[i--] == 0x00 &&
buffer[i--] == 0x00
)
{
writelen = i+5;
break;
}
}
writelen = WriteH264Data(hMp4File,buffer,writelen);
if(writelen<=0)
{
break;
}
memcpy(buffer,buffer+writelen,readlen-writelen+1);
pos = readlen-writelen+1;
}
fclose(fp);
delete[] buffer;
CloseMP4File(hMp4File);
return true;
}
bool MP4Encoder:: PraseMetadata(const unsigned char* pData,int size,MP4ENC_Metadata &metadata)
{
if(pData == NULL || size<4)
{
return false;
}
MP4ENC_NaluUnit nalu;
int pos = 0;
bool bRet1 = false,bRet2 = false;
while (int len = ReadOneNaluFromBuf(pData,size,pos,nalu))
{
if(nalu.type == 0x07)
{
memcpy(metadata.Sps,nalu.data,nalu.size);
metadata.nSpsLen = nalu.size;
bRet1 = true;
}
else if((nalu.type == 0x08))
{
memcpy(metadata.Pps,nalu.data,nalu.size);
metadata.nPpsLen = nalu.size;
bRet2 = true;
}
pos += len;
}
if(bRet1 && bRet2)
{
return true;
}
return false;
}
注意:代碼中引入了編解碼庫Mp4v2的頭文件和lib文件,可從頁面(可能需要翻牆)https://code.google.com/p/mp4v2/中下載:


分別將下載包中的相關lib鏈接庫和頭文件拷貝到C++工程(具體位置跟C++代碼中引用路徑想關聯)中,其中lib庫在下載包lib文件夾下,頭文件在inc文件夾下:

編譯工程。
- 2. 利用Swig工具進行dll生成
下載Swig文件,Swing的使用方式大概有兩種,一種是配置Vs對外部工具,一種是配置環境變量通過Cmd命令。前一種方法比較簡單,Vs2012配置如圖。

其中Title為外部工具名稱,Command配置為swig.exe的地址,下面兩個按圖中配置。這樣swig即配置完成,接下來是該工具的使用。
在C++工程根路徑上添加.i文件,如test.i,編寫內容如下:
%module videoWrapCPlus
%include <windows.i>
%{
#include "MP4Encoder.h"
%}
%include "MP4Encoder.h"
其中第一行module名需要與C++的dll名稱一致。
此時,在當前頁面的狀態下,點擊工具欄中剛剛配置的swig工具:
會在videoWrapCPlus工程目錄下生成幾個文件:

其中以cs為后綴名的文件為C#類型文件。接着在C++項目中添加test_wrap.cxx,並編譯整個工程,以生成dll動態鏈接庫。整個C++工程結構圖如下:
- 3. C#工程對dll文件的調用
新建C#工程,以控制台為例,並將剛才生成的C#類文件拷貝到工程根目錄下:
另外,需要將生成的dll文件和libmp4v2.dll(可從之前Mp4v2網站上下載)和拷貝到C#工程運行目錄下(若直接添加引用會報錯):
最后,可編寫測試函數,實現C#中對.264文件到.mp4文件的轉換過程:

其中WriteH264File函數的第一個參數為.264文件地址,第二個參數為目標文件地址(如有中文,需要UTF8編碼,建議使用英文路徑)。
--2016/4/14 於創意城
