超輕量級MP4封裝方法介紹

liwen01發表於2023-12-27

liwen01 2023.12.17

前言

MP4是目前非常常用的一種影片封裝格式,關於MP4的介紹資料也非常多。我們常用的封裝庫或工具有:ffmpeglibmp4v2GPACMP4.js,它們的優點是功能基本上都是比較全面,缺點就是它們佔用的資源相對來說也是非常多的。

在嵌入式系統中,不管是RAM還是FLASH空間,一般都是非常小,這個時候,如果要將音影片封裝成MP4,或是解碼MP4格式就會顯得非常困難,因為上面介紹的那些庫都放不下或是因為記憶體不夠執行不起來,只能根據MP4協議自己去解析。

這裡介紹一個輕量級的MP4封裝方法(minimp4),整合MP4編碼,解碼,資訊查詢功能,整體執行檔案大小如下:

biao@ubuntu:~/minimp4_test$ mips-linux-uclibc-gnu-size test
   text    data     bss     dec     hex filename
 354696    1460   13624  369780   5a474 t
 biao@ubuntu:~/minimp4_test$ 

(一)功能需求介紹

在一般嵌入式裝置上,我們一般只需要MP4的一些簡單操作,比如封裝,解封裝及檔案資訊檢視。具體功能要求如下:

  • 支援合入H.264 和H.265 影片格式影片
  • 支援合入AAC音訊
  • 支援MP4格式檔案解封裝
  • 支援獲取MP4檔案資訊

效能上的限制,我們希望:

  • 程式碼空間小於500K
  • 執行記憶體小於100K

要在有限的資源上實現上面這些功能,我們可以在minimp4的基礎再進一步完善。

minimp4 的原始碼可以直接在 github 上下載,官方有個minimp4_test.c,裡面有些使用的demo,但不是非常完善,可以參考使用。

(二)支援H.264與H.265格式

(1)H.264與H.265的區別

圖片

使用工具開啟官方自帶的foreman.264檔案,我們可以看到:

  • 該檔案中包含SPS,PPS,IDR,P幀,在其它檔案中,可能還會有B幀
  • 頭標籤為00 00 00 01,後面一個是幀型別
  • 影片解析度352*288
  • 流型別AVC/H.264
  • 總共300幀

另外開啟一個H.265檔案

圖片

我們可以看到:

  • 該檔案中包含VPS SPS,PPS,IDR,TRAIL_R,TRAIL_N幀
  • 頭標籤為00 00 00 01,後面是幀型別
  • 影片解析度1280*720
  • 流型別HEVC/H.265
  • 總共770幀

從應用的角度看,H.265 是有多一個VPS幀,它主要是用來描述影片中各種型別幀(比如I幀、P幀、B幀)的使用和順序,以及它們之間的相關性.

在實際使用的時候,需要注意VPS、SPS、PPS這些幀的封裝和解析。

(2)手動新增VPS資訊

正常使用minimp4 的時候,使用h.265進行封裝的是沒有問題的,但是在解封裝的時候,它並不會去提取VPS資訊,這裡比較簡單的做法是自己補充上相關的資訊到檔案頭:

if(is_hevc){
        staticunsignedchar h265_vps_sps_pps[84]={
            0x00,0x00,0x00,0x01,0x40,0x01,0x0C,0x01,
            0xFF,0xFF,0x00,0x80,0x00,0x00,0x03,0x00,
            0x00,0x03,0x00,0x00,0x03,0x00,0x00,0x03,
            0x00,0x00,0xB5,0x02,0x40,0x00,0x00,0x00,
            0x01,0x42,0x01,0x01,0x00,0x80,0x00,0x00,
            0x03,0x00,0x00,0x03,0x00,0x00,0x03,0x00,
            0x00,0x03,0x00,0x00,0xA0,0x02,0x80,0x80,
            0x2D,0x1F,0xE5,0xB5,0x92,0x46,0xD0,0xCE,
            0x49,0x24,0xB7,0x24,0xAA,0x49,0xF2,0x92,
            0xC8,0x00,0x00,0x00,0x01,0x44,0x01,0xC1,
            0xA5,0x58,0x1E,0x48,
        };

        if(fwrite(h265_vps_sps_pps, 1, sizeof(h265_vps_sps_pps), foutV)!= sizeof(h265_vps_sps_pps)){
            goto END;
        }
    }

h265_vps_sps_pps 裡面的內容,需要根據自己實際的碼流資訊進行修改

(三)支援AAC音訊格式

AAC與PCM之間的編碼與轉換,可以檢視上一篇文章:嵌入式音訊應用開發介紹

主要需要注意的是AAC的配置,需要根據實際引數進行修改

// unsigned char  conf[] = {0x11, 0x90};  //AAL-LC 48kHz 2 channle
    staticunsignedchar aac_conf[4]={0x11, 0x90, 0x0, 0x0};
    unsignedint length = 2;
    MP4E_set_dsi(mux, audio_track_id, aac_conf,4);
    printf("%s %d \r\n",__FUNCTION__,__LINE__);

(四)獲取mp4檔案資訊

這部分需要對 mp4 協議比較熟悉,具體的協議介紹,可以去查詢MP4標準。這裡介紹幾個簡單概念:

概念 描述
Box MP4中的基本構建單元,也稱為Atom。每個Box都有特定型別的識別符號和長度欄位,用於描述它自己的內容和大小。不同型別的Box包含不同型別的資訊。
Sample 影片和音訊編解碼中的最小可操作單元。對於影片,每個樣本代表一個畫面或影像幀;對於音訊,每個樣本可能代表一小段聲音。它們按特定順序組成媒體檔案。
Track 用於組織媒體資料的單獨通道。MP4檔案可以包含多個軌道,如影片軌道、音訊軌道。每個軌道包含相應型別的媒體資料和描述資訊,如影片軌道包含影片樣本和後設資料。
Chunk 一組樣本的集合。在媒體檔案中,樣本可以組織成不同大小的塊,可以是連續的或分散的。塊可以包含一個或多個樣本,物理上可以儲存在檔案的不同位置。

mp4中包含很多的Box,mp4的基本資訊和索引是在moov box中的:

Box型別 描述
mvhd 影片整體資訊
trak 包含一個或多個軌道的詳細描述
udta 儲存使用者自定義的資料

其中 mvhd Box(Movie Header Box):這個Box包含了影片的整體資訊,比如時長、時間刻度等。它描述了整個媒體檔案的基本屬性。

我們開啟一個使用miniMP4封裝的MP4檔案,可以看到moov box 是後置的,也就是在檔案的後面,而在一些其它的庫中,moov box的偏移位置是在比較靠近檔案開始的位置。

圖片

如果我們要快速的檢視mp4檔案的資訊,比如影片時間長度,對於使用minimp4封裝的Mp4檔案,它的moov box後置,我們可以從後面開始解析。

#define CHUNK_SIZE (10*1024)

typedefstruct
{
	unsignedchar mvhd[4];
	unsignedchar version;
	unsignedchar flags[3];
	unsignedint creation_time;
	unsignedint modification_time;
	unsignedint timescale;
	unsignedint duration;
}MVHD_ST;

MVHD_ST g_stMVDHInfo;

/**
 * @brief 位元組序翻轉
 * @param  val
 * @return unsigned int 
 */
unsigned int videoinfo_flip(unsigned int val) {
	unsignedlongnew = 0;
	new += (val & 0x000000FF) << 24;
	new += (val & 0xFF000000) >> 24;
	new += (val & 0x0000FF00) << 8;
	new += (val & 0x00FF0000) >> 8;
   returnnew;
}

/**
 * @brief 查詢特定ASCII碼字串位置
 * @param  file_name
 * @param  search_str
 * @return long long 
 */
long long find_string_position(const char *file_name, const char *search_str) {
	FILE *file = NULL;
	char *buffer =NULL;
	int read_size = 0;
	int length = 0;
	longlong file_size = 0; 
	longlong search_position =0; 
	longlong position = 0;
	if(file_name==NULL || search_str==NULL){
		printf("%s %d input para error \r\n",__FUNCTION__,__LINE__);
		return-1;
	}
    file = fopen(file_name, "rb");
    if (file == NULL) {
		printf("%s %d file open error\r\n",__FUNCTION__,__LINE__);
        return-2;
    }
	length = strlen(search_str);
    fseek(file, 0, SEEK_END); 
	file_size = ftell(file); 
	search_position = file_size - CHUNK_SIZE; 
    if (search_position < 0) {
        search_position = 0;
    }

    buffer = (char *)malloc(CHUNK_SIZE + 1);
    if (buffer == NULL) {
        fclose(file);
		printf("%s %d malloc error\r\n",__FUNCTION__,__LINE__);
        return-3; 
    }

	int cnt = 0;
    while (search_position >= 0) {
		/**限制檢索最大值,避免輸入檔案損壞一直在檢索**/
		if(cnt ++>30){
			break;
		}
		fseek(file, search_position, SEEK_SET); 
        size_t read_size = fread(buffer, 1, CHUNK_SIZE, file); 
        for (int i = read_size - 1; i >= 0; i--) {
            int j;
            for (j = 0; j < length; j++) {
                if (i - j < 0 || buffer[i - j] != search_str[length - j - 1]) {
                    break;
                }
            }
            if (j == length) {
                position = search_position + i - length + 1;
				break;
            }
        }
		search_position = search_position - CHUNK_SIZE;
		if(search_position<0){
			search_position = 0;
		}
		if(position!=0){
			break;
		}
    }
	printf("read cnt=%d \r\n",cnt);
	if(position>0){
		fseek(file, position, SEEK_SET); 
		read_size = fread(&g_stMVDHInfo, 1, sizeof(g_stMVDHInfo), file);
	}
	fclose(file);
    free(buffer); 
    return position;
}

MP4的詳細格式定義,可以檢視MP4標準: Text of ISO/IEC 14496-12 5th edition

(五)減少記憶體的使用

在minimp4原始碼中,它有個preload函式,它的主要作用是將輸入的檔案全部讀取到記憶體中去,後面資料解析的時候直接去記憶體Buff中獲取,這種方式的優點是處理速度快,缺點就是耗記憶體。

在嵌入式系統裝置中,有些裝置可能整個系統總共才幾十M的記憶體,一次把整個檔案讀取到記憶體中去解析,如果要處理比較大的mp4檔案,顯然是有問題的,會直接導致系統記憶體不夠用。

有一個處理方式是按需讀取資料,需要多少就讀取多少,這樣的處理方式可以以速度換空間。將 preload 改為 preload_filewrite_callback 改 read_file_callback,同時,有對記憶體資料進行操作的地方也需要修改。

static FILE *preload_file(const char *path, ssize_t *data_size)
{
    FILE *file = fopen(path, "rb");
    uint8_t *data;
    *data_size = 0;
    if (!file){
        return0;
    }

    if (fseek(file, 0, SEEK_END)){
        exit(1);
    }

    *data_size = (ssize_t)ftell(file);
    if (*data_size < 0){
        exit(1);
    }

    if (fseek(file, 0, SEEK_SET)){
        exit(1);
    }

    return file;
}

static int read_file_callback(int64_t offset, void *buffer, size_t size, void *token)
{
    staticint total_len = 0;
    INPUT_FILE_BUF *buf = (INPUT_FILE_BUF*)token;
    int read = 0;
    int ret = 0;
    size_t to_copy = MINIMP4_MIN(size, buf->size - offset - size);
    fseek(buf->fin,offset,SEEK_SET);
    read = fread(buffer,1,to_copy,buf->fin);
    return to_copy != size;
}

(六)工程資料下載

如需上面介紹的工程,測試檔案,以及檢視工具,可以在公眾號中回覆 資源 獲取,內容在音影片連線中。工程目錄如下:

biao@ubuntu:~/test/minimp4/minimp4_test$ tree
.
├── adts.c
├── adts.h
├── file
│   ├── decode_audio.aac
│   ├── decode_foreman_.264
│   ├── decode_foreman.264
│   ├── decode_test.h265
│   ├── decode_video.h265
│   ├── faac_adts.aac
│   ├── foreman.264
│   ├── h264_aac.mp4
│   ├── h265_aac.mp4
│   └── surfing_aa.h265
├── Makefile
├── minimp4.h
├── obj
│   ├── adts.o
│   └── write_demux_mp4.o
├── test
└── write_demux_mp4.c

2 directories, 18 files
biao@ubuntu:~/test/minimp4/minimp4_test$ 

結尾

minimp4 適用於記憶體和儲存空間都非常受限的嵌入式裝置,很多功能並不十分完善,需要自己根據實際應用進行適配。如果只是在裝置上進行簡單的MP4封裝和解封裝,它的功能也是足夠了的。

 

---------------------------End---------------------------
如需獲取更多內容
請關注 liwen01 公眾號

相關文章