前言:前幾天工作任務,要把JPEG流封裝為AVI視頻,就找了些AVI文件結構資料和示例代碼研究了下,現將學習總結及最終完成的可用代碼分享出來,由於本人也是現學現用,如有不恰當或錯誤之處,歡迎提出!
1 AVI文件結構
AVI采用RIFF文件結構方式,RIFF是微軟定義的一種用於管理windows環境中多媒體數據的文件格式,波形音頻wave、MIDI和數字視頻AVI都采用這種格式存儲,構造RIFF文件的基本單元叫做數據塊(Chunk),每個數據塊包含3個部分:
(1)4字節的數據塊標記(Chunk ID)
(2)4字節的數據塊大小
(3)數據
整個RIFF文件可以看成一個ID為RIFF的數據塊,RIFF塊包含一系列子塊,其中有一種子塊的ID為LIST,稱為LIST塊,LIST塊中可以再包含一系列子塊,但除了LIST塊的其他所有子塊都不能再包含子塊。
RIFF和LIST塊分別比普通的數據塊多一個被稱為形式類型(Form Type)和列表類型(List Type)的數據域,其組成如下:
(1)4字節的數據塊標記(Chunk ID)
(2)4字節的數據塊大小
(3)4字節的形式類型(對於RIFF塊)或列表類型(對於LIST塊)
(4)數據
AVI文件是最復雜的RIFF文件,它能夠同時存儲音頻和視頻數據(注:本文檔不涉及音頻相關內容,只針對視頻數據進行介紹),AVI文件RIFF塊的形式類型是AVI ,它包含以下3個子塊:
(1)信息塊,ID為hdrl的LIST塊,用於定義AVI文件的數據格式
(2)數據塊,ID為movi的LIST塊,用於存儲音視頻數據
(3)索引塊,ID為idxl的數據塊,用於定義音視頻數據的索引,是可選塊
AVI文件結構如圖1所示
圖1 AVI文件結構
1.1 信息塊
信息塊包含兩個子塊:一個ID為avih的子塊和一個ID為strl的LIST塊。
1.1.1 avih塊
圖2 avih塊結構
avih塊可用如圖2所示的struct avi_avih_chunk結構體定義,圖中已對結構體各變量的含義進行了解釋,以下是對其中幾個變量的補充說明:
(1)max_bytes_per_sec
max_bytes_per_sec用於控制視頻的最大碼率,即每秒傳輸的最大數據量。但實際上,給這個變量賦值並不能影響視頻的碼率,原因如下:設JPEG流圖像總幀數為nframes,視頻幀率為fps,各幀圖像平均大小為len,則封裝的AVI視頻時長、文件大小和視頻碼率分別為
time = nframes / fps
video_size = nframes * len(實際大小還要加上文件頭和文件尾的數據)
rate = video_size / time = fps * len
由此可見,在固定的幀率fps下,視頻碼率完全取決於JPEG各幀圖像的大小,和max_bytes_per_sec的值沒有關系,所以這個變量設為0即可。
(2)flags
flags表示AVI文件的全局屬性,如是否含有索引塊、是否即有音頻數據又有視頻數據等,不進行任何標記時flags值為0,若含有索引塊,則flags值為0x00000010。
(3)init_frames
AVI文件若同時存儲了音頻和視頻數據,則音頻數據和視頻數據是交叉存儲的,init_frames僅在這種情況下使用,對於只有視頻流的情況,該變量的值為0。
(4)width、height
這里的width和height不是JPEG圖像的寬和高,而是用播放器打開AVI文件時視頻主窗口的寬和高,舉個例子,JPEG圖像大小為1920*1080,width和height分別設為960和540,用QQ影音打開AVI文件,則QQ影音會以960*540的窗口大小進行播放。
1.1.2 strl塊
strl塊由圖3所示的結構體定義,它包含strh和strf兩個子塊。
圖3 strl塊結構
1、strh塊結構
圖4 strh塊結構
圖4所示為strh塊結構定義,下面是對結構體內一些變量含義的補充說明:
(1)codec
codec是一個長度為4的字符數組,用於指定數據流的編碼格式,也就是播放器播放這個流時需要的解碼器,對於JPEG編碼的視頻流,codec數組內容就是'J', 'P', 'E', 'G',而不能隨意指定,否則播放器播放時會無法解碼。
(2)scale、rate
對於視頻流,rate除以scale等於視頻幀率,因此這兩個變量可賦值為scale = 1、rate = fps。
2、strf塊結構
strf塊結構根據strh塊中stream_type是視頻流還是音頻流而有所不同,對於視頻流,strf塊結構如圖5所示,其中bitcount表示每個圖像像素占的位數,其值根據視頻流的實際情況而定,但只能是1、4、8、16、24和32之一,常用的有1(黑白二值化圖像)、8(256階灰度圖)和24(RGB圖像)。
圖5 針對視頻流的strf塊結構
1.2 數據塊
由圖1可知,數據塊是一個ID為movi的LIST列表,也稱為movi塊,在僅有視頻流時,該部分存儲的就是一幀一幀的圖像數據,圖6展示了視頻流movi塊的詳細結構。
圖6 僅有視頻流的movi塊結構
可以看到,movi塊首先是一個固定結構的LIST列表頭,包括塊ID、塊大小和塊類型,其中塊ID固定為LIST,塊類型固定為movi,塊大小為movi塊去掉開頭8字節后的大小。
然后是movi塊數據,也就是各幀視頻圖像對應的數據塊,每一幀圖像的數據塊都包含三部分:
(1)4字節ID:可以為00dc或00db,00dc表示壓縮的視頻數據,00db表示未壓縮的視頻數據,根據視頻流的實際情況來選擇賦值。
(2)4字節frame length:圖像數據長度(單位:字節),該長度必須是4的整數倍,如果不是,則需要將其修正到4的整數倍,比如frame length原始數據為99,則需將其加到100。
(3)frame data:真正的圖像數據。
1.3 索引塊
索引塊是AVI文件結構的可選部分,它是一個ID等於idxl的數據塊,索引塊提供了movi塊中存儲各幀圖像的數據塊在AVI文件中的位置索引,作用是提高AVI文件的讀寫速度,提高視頻播放時的體驗效果。
圖7 索引塊結構
如圖7所示為索引塊結構,包括塊ID、塊大小和塊數據三部分,其中塊ID固定為idxl,塊大小等於索引塊數據的大小。
索引塊數據是movi塊中存儲各幀圖像數據塊的索引,每一幀圖像的索引都是一個16字節的數據結構,具體如下:
(1)4字節ChunkID:即movi塊各幀圖像數據塊的ID,00dc或00db
(2)4字節ChunkFlag:表示該幀圖像是否是關鍵幀,0x10代表關鍵幀,0x00代表非關鍵幀
(3)4字節ChunkOffset:圖像數據塊相對於“movi”標示符(圖6紅色箭頭所指處)的偏移量,由圖6可得,各幀圖像索引ChunkOffset的值為:
第一幀圖像索引àChunkOffset1 = 4;
第二幀圖像索引àChunkOffset2 = ChunkOffset1+8+第一幀圖像數據長度
第三幀圖像索引àChunkOffset3 = ChunkOffset2+8+第二幀圖像數據長度
…… (后面各幀圖像索引以此類推,其中各幀圖像數據長度指的是修正到4的整數倍后的長度)
(4)4字節ChunkLength:修正到4的整數倍后的各幀圖像數據長度
2 JPEG流封裝AVI步驟
JPEG流封裝AVI視頻的本質是按照AVI結構進行文件讀寫,操作流程大體上可分為三個步驟:
步驟1:創建空白AVI文件,設置文件偏移量到數據塊movi標示符后面
(1)創建AVI文件,以二進制寫方式打開
(2)計算文件偏移量offset,等於RIFF文件頭12字節 + hdrl塊大小 + movi LIST頭12字節
(3)設置AVI文件偏移量為offset
步驟2:從offset偏移量處開始,向AVI文件中逐幀寫入JPEG數據
(1)將當前JPEG圖像數據長度加到4的整數倍,用length表示
(2)JPEG圖像是壓縮過的圖像數據,故寫入'0', '0', 'd', 'c'
(3)寫入當前JPEG圖像數據長度length
(4)寫入當前JPEG圖像數據,寫入長度為length
(5)循環上述過程,完成逐幀圖像數據的寫入
步驟3:JPEG數據寫完后,先繼續向后寫索引塊,再定位到文件頭回填各塊數據
(1)寫索引塊
- 先寫塊ID 'i', 'd', 'x', 'l'
- 再寫塊大小 16 * nframes
- 最后寫各幀圖像的索引
(2)從文件頭開始,回填各塊數據
- 設置文件偏移量為0
- 按照AVI文件結構,寫入步驟1跳過的各塊數據
需要注意的是,步驟3寫索引塊時需要各幀圖像的數據長度和總幀數,回填各塊數據時也需要總幀數和所有幀的總大小,因此步驟2寫入JPEG數據時需要保存它們的值。
3 代碼分享
我完成的代碼,是以若干張JPEG圖片作為JPEG流,先將圖片數據讀入內存,再寫入AVI文件,共包含五個文件:
1、list.h和list.c,雙向循環鏈表,作用是保存各幀圖像大小,用於寫索引塊
2、Jpeg2AVI.h和Jpeg2AVI.c,用於將JPEG流封裝為AVI視頻
3、main.c,測試程序
Jpeg2AVI.h

1 #ifndef _JPEG2AVI_H_ 2 #define _JPEG2AVI_H_ 3 4 #include <stdio.h> 5 6 void jpeg2avi_start(FILE *fp); 7 void jpeg2avi_add_frame(FILE *fp, void *data, unsigned int len); 8 void jpeg2avi_end(FILE *fp, int width, int height, int fps); 9 10 typedef struct avi_riff_head 11 { 12 unsigned char id[4]; 13 unsigned int size; 14 unsigned char type[4]; 15 }AVI_RIFF_HEAD, AVI_LIST_HEAD; 16 17 typedef struct avi_avih_chunk 18 { 19 unsigned char id[4]; //塊ID,固定為avih 20 unsigned int size; //塊大小,等於struct avi_avih_chunk去掉id和size的大小 21 unsigned int us_per_frame; //視頻幀間隔時間(以微秒為單位) 22 unsigned int max_bytes_per_sec; //AVI文件的最大數據率 23 unsigned int padding; //設為0即可 24 unsigned int flags; //AVI文件全局屬性,如是否含有索引塊、音視頻數據是否交叉存儲等 25 unsigned int total_frames; //總幀數 26 unsigned int init_frames; //為交互格式指定初始幀數(非交互格式應該指定為0) 27 unsigned int streams; //文件包含的流的個數,僅有視頻流時為1 28 unsigned int suggest_buff_size; //指定讀取本文件建議使用的緩沖區大小,通常為存儲一楨圖像 //以及同步聲音所需的數據之和,不指定時設為0 29 unsigned int width; //視頻主窗口寬度(單位:像素) 30 unsigned int height; //視頻主窗口高度(單位:像素) 31 unsigned int reserved[4]; //保留段,設為0即可 32 }AVI_AVIH_CHUNK; 33 34 typedef struct avi_rect_frame 35 { 36 short left; 37 short top; 38 short right; 39 short bottom; 40 }AVI_RECT_FRAME; 41 42 typedef struct avi_strh_chunk 43 { 44 unsigned char id[4]; //塊ID,固定為strh 45 unsigned int size; //塊大小,等於struct avi_strh_chunk去掉id和size的大小 46 unsigned char stream_type[4]; //流的類型,vids表示視頻流,auds表示音頻流 47 unsigned char codec[4]; //指定處理這個流需要的解碼器,如JPEG 48 unsigned int flags; //標記,如是否允許這個流輸出、調色板是否變化等,一般設為0即可 49 unsigned short priority; //流的優先級,視頻流設為0即可 50 unsigned short language; //音頻語言代號,視頻流設為0即可 51 unsigned int init_frames; //為交互格式指定初始幀數(非交互格式應該指定為0) 52 unsigned int scale; // 53 unsigned int rate; //對於視頻流,rate / scale = 幀率fps 54 unsigned int start; //對於視頻流,設為0即可 55 unsigned int length; //對於視頻流,length即總幀數 56 unsigned int suggest_buff_size; //讀取這個流數據建議使用的緩沖區大小 57 unsigned int quality; //流數據的質量指標 58 unsigned int sample_size; //音頻采樣大小,視頻流設為0即可 59 AVI_RECT_FRAME rcFrame; //這個流在視頻主窗口中的顯示位置,設為{0,0,width,height}即可 60 }AVI_STRH_CHUNK; 61 62 /*對於視頻流,strf塊結構如下*/ 63 typedef struct avi_strf_chunk 64 { 65 unsigned char id[4]; //塊ID,固定為strf 66 unsigned int size; //塊大小,等於struct avi_strf_chunk去掉id和size的大小 67 unsigned int size1; //size1含義和值同size一樣 68 unsigned int width; //視頻主窗口寬度(單位:像素) 69 unsigned int height; //視頻主窗口高度(單位:像素) 70 unsigned short planes; //始終為1 71 unsigned short bitcount; //每個像素占的位數,只能是1、4、8、16、24和32中的一個 72 unsigned char compression[4]; //視頻流編碼格式,如JPEG、MJPG等 73 unsigned int image_size; //視頻圖像大小,等於width * height * bitcount / 8 74 unsigned int x_pixels_per_meter; //顯示設備的水平分辨率,設為0即可 75 unsigned int y_pixels_per_meter; //顯示設備的垂直分辨率,設為0即可 76 unsigned int num_colors; //含義不清楚,設為0即可 77 unsigned int imp_colors; //含義不清楚,設為0即可 78 }AVI_STRF_CHUNK; 79 80 typedef struct avi_strl_list 81 { 82 unsigned char id[4]; //塊ID,固定為LIST 83 unsigned int size; //塊大小,等於struct avi_strl_list去掉id和size的大小 84 unsigned char type[4]; //塊類型,固定為strl 85 AVI_STRH_CHUNK strh; 86 AVI_STRF_CHUNK strf; 87 }AVI_STRL_LIST; 88 89 typedef struct avi_hdrl_list 90 { 91 unsigned char id[4]; //塊ID,固定為LIST 92 unsigned int size; //塊大小,等於struct avi_hdrl_list去掉id和size的大小 93 unsigned char type[4]; //塊類型,固定為hdrl 94 AVI_AVIH_CHUNK avih; 95 AVI_STRL_LIST strl; 96 }AVI_HDRL_LIST; 97 98 #endif
Jpeg2AVI.c

1 #include "Jpeg2AVI.h" 2 #include "list.h" 3 #include <stdlib.h> 4 #include <string.h> 5 6 static int nframes; //總幀數 7 static int totalsize; //幀的總大小 8 static struct list_head list; //保存各幀圖像大小的鏈表,用於寫索引塊 9 10 /*鏈表宿主結構,用於保存真正的圖像大小數據*/ 11 struct ListNode 12 { 13 int value; 14 struct list_head head; 15 }; 16 17 static void write_index_chunk(FILE *fp) 18 { 19 unsigned char index[4] = {'i', 'd', 'x', '1'}; //索引塊ID 20 unsigned int index_chunk_size = 16 * nframes; //索引塊大小 21 unsigned int offset = 4; 22 struct list_head *slider = NULL; 23 struct list_head *tmpslider = NULL; 24 25 fwrite(index, 4, 1, fp); 26 fwrite(&index_chunk_size, 4, 1, fp); 27 28 list_for_each_safe(slider, tmpslider, &list) 29 { 30 unsigned char tmp[4] = {'0', '0', 'd', 'c'}; //00dc = 壓縮的視頻數據 31 unsigned int keyframe = 0x10; //0x10表示當前幀為關鍵幀 32 struct ListNode *node = list_entry(slider, struct ListNode, head); 33 34 fwrite(tmp, 4, 1, fp); 35 fwrite(&keyframe, 4, 1, fp); 36 fwrite(&offset, 4, 1, fp); 37 fwrite(&node->value, 4, 1, fp); 38 offset = offset + node->value + 8; 39 40 list_del(slider); 41 free(node); 42 } 43 } 44 45 static void back_fill_data(FILE *fp, int width, int height, int fps) 46 { 47 AVI_RIFF_HEAD riff_head = 48 { 49 {'R', 'I', 'F', 'F'}, 50 4 + sizeof(AVI_HDRL_LIST) + sizeof(AVI_LIST_HEAD) + nframes * 8 + totalsize, 51 {'A', 'V', 'I', ' '} 52 }; 53 54 AVI_HDRL_LIST hdrl_list = 55 { 56 {'L', 'I', 'S', 'T'}, 57 sizeof(AVI_HDRL_LIST) - 8, 58 {'h', 'd', 'r', 'l'}, 59 { 60 {'a', 'v', 'i', 'h'}, 61 sizeof(AVI_AVIH_CHUNK) - 8, 62 1000000 / fps, 25000, 0, 0, nframes, 0, 1, 100000, width, height, 63 {0, 0, 0, 0} 64 }, 65 { 66 {'L', 'I', 'S', 'T'}, 67 sizeof(AVI_STRL_LIST) - 8, 68 {'s', 't', 'r', 'l'}, 69 { 70 {'s', 't', 'r', 'h'}, 71 sizeof(AVI_STRH_CHUNK) - 8, 72 {'v', 'i', 'd', 's'}, 73 {'J', 'P', 'E', 'G'}, 74 0, 0, 0, 0, 1, 23, 0, nframes, 100000, 0xFFFFFF, 0, 75 {0, 0, width, height} 76 }, 77 { 78 {'s', 't', 'r', 'f'}, 79 sizeof(AVI_STRF_CHUNK) - 8, 80 sizeof(AVI_STRF_CHUNK) - 8, 81 width, height, 1, 24, 82 {'J', 'P', 'E', 'G'}, 83 width * height * 3, 0, 0, 0, 0 84 } 85 } 86 }; 87 88 AVI_LIST_HEAD movi_list_head = 89 { 90 {'L', 'I', 'S', 'T'}, 91 4 + nframes * 8 + totalsize, 92 {'m', 'o', 'v', 'i'} 93 }; 94 95 //定位到文件頭,回填各塊數據 96 fseek(fp, 0, SEEK_SET); 97 fwrite(&riff_head, sizeof(riff_head), 1, fp); 98 fwrite(&hdrl_list, sizeof(hdrl_list), 1, fp); 99 fwrite(&movi_list_head, sizeof(movi_list_head), 1, fp); 100 } 101 102 void jpeg2avi_start(FILE *fp) 103 { 104 int offset1 = sizeof(AVI_RIFF_HEAD); //riff head大小 105 int offset2 = sizeof(AVI_HDRL_LIST); //hdrl list大小 106 int offset3 = sizeof(AVI_LIST_HEAD); //movi list head大小 107 108 //AVI文件偏移量設置到movi list head后,從該位置向后依次寫入JPEG數據 109 fseek(fp, offset1 + offset2 + offset3, SEEK_SET); 110 111 //初始化鏈表 112 list_head_init(&list); 113 114 nframes = 0; 115 totalsize = 0; 116 } 117 118 void jpeg2avi_add_frame(FILE *fp, void *data, unsigned int len) 119 { 120 unsigned char tmp[4] = {'0', '0', 'd', 'c'}; //00dc = 壓縮的視頻數據 121 struct ListNode *node = (struct ListNode *)malloc(sizeof(struct ListNode)); 122 123 /*JPEG圖像大小4字節對齊*/ 124 while (len % 4) 125 { 126 len++; 127 } 128 129 fwrite(tmp, 4, 1, fp); //寫入是否是壓縮的視頻數據信息 130 fwrite(&len, 4, 1, fp); //寫入4字節對齊后的JPEG圖像大小 131 fwrite(data, len, 1, fp); //寫入真正的JPEG數據 132 133 nframes += 1; 134 totalsize += len; 135 136 /*將4字節對齊后的JPEG圖像大小保存在鏈表中*/ 137 if (node != NULL) 138 { 139 node->value = len; 140 list_add_tail(&node->head, &list); 141 } 142 } 143 144 void jpeg2avi_end(FILE *fp, int width, int height, int fps) 145 { 146 //寫索引塊 147 write_index_chunk(fp); 148 149 //從文件頭開始,回填各塊數據 150 back_fill_data(fp, width, height, fps); 151 }
list.h

1 #ifndef _LIST_H_ 2 #define _LIST_H_ 3 4 struct list_head 5 { 6 struct list_head *next; 7 struct list_head *prev; 8 }; 9 10 void list_head_init(struct list_head *list); 11 void list_add_tail(struct list_head *_new, struct list_head *head); 12 void list_del(struct list_head *entry); 13 14 #ifndef offsetof 15 #define offsetof(TYPE, MEMBER) \ 16 ((size_t) &((TYPE *)0)->MEMBER) 17 #endif 18 19 #ifndef container_of 20 #define container_of(ptr, type, member) \ 21 ((type *)((char *)ptr - offsetof(type,member))) 22 #endif 23 24 /** 25 * list_entry - get the struct for this entry 26 * @ptr: the &struct list_head pointer. 27 * @type: the type of the struct this is embedded in. 28 * @member: the name of the list_struct within the struct. 29 */ 30 #define list_entry(ptr, type, member) \ 31 container_of(ptr, type, member) 32 33 /** 34 * list_for_each_safe - iterate over a list safe against removal of list entry 35 * @pos: the &struct list_head to use as a loop cursor. 36 * @n: another &struct list_head to use as temporary storage 37 * @head: the head for your list. 38 */ 39 #define list_for_each_safe(pos, n, head) \ 40 for (pos = (head)->next, n = pos->next; pos != (head); \ 41 pos = n, n = pos->next) 42 43 #endif //_LIST_H_
list.c

1 #include "list.h" 2 #include <stdio.h> 3 4 static void __list_add(struct list_head *_new, struct list_head *prev, struct list_head *next) 5 { 6 next->prev = _new; 7 _new->next = next; 8 _new->prev = prev; 9 prev->next = _new; 10 } 11 12 static void __list_del(struct list_head *prev, struct list_head *next) 13 { 14 next->prev = prev; 15 prev->next = next; 16 } 17 18 void list_head_init(struct list_head *list) 19 { 20 list->next = list; 21 list->prev = list; 22 } 23 24 /** 25 * list_add_tail - insert a new entry before the specified head 26 * @_new: new entry to be added 27 * @head: list head to add it before 28 */ 29 void list_add_tail(struct list_head *_new, struct list_head *head) 30 { 31 __list_add(_new, head->prev, head); 32 } 33 34 /** 35 * list_del - deletes entry from list. 36 * @entry: the element to delete from the list. 37 */ 38 void list_del(struct list_head *entry) 39 { 40 __list_del(entry->prev, entry->next); 41 entry->next = NULL; 42 entry->prev = NULL; 43 }
main.c

1 #include "Jpeg2AVI.h" 2 #include <string.h> 3 4 #define JPEG_MAX_SIZE 100000 //JPEG圖像最大字節數 5 #define JPEG_NUM 13800 //JPEG圖像數量 6 7 int main() 8 { 9 FILE *fp_jpg; 10 FILE *fp_avi; 11 int filesize; 12 unsigned char jpg_data[JPEG_MAX_SIZE]; 13 char filename[10]; 14 int i = 0; 15 16 fp_avi = fopen("sample.avi","wb"); 17 18 jpeg2avi_start(fp_avi); 19 20 for (i = 0; i < JPEG_NUM; i++) 21 { 22 memset(filename, 0, 10); 23 memset(jpg_data, 0, JPEG_MAX_SIZE); 24 25 sprintf(filename, "%d.jpg", i + 1); 26 fp_jpg = fopen(filename, "rb"); 27 28 if (fp_jpg != NULL) 29 { 30 /*獲取JPEG數據大小*/ 31 fseek(fp_jpg, 0, SEEK_END); 32 filesize = ftell(fp_jpg); 33 fseek(fp_jpg, 0, SEEK_SET); 34 35 /*將JPEG數據讀到緩沖區*/ 36 fread(jpg_data, filesize, 1, fp_jpg); 37 38 /*將JPEG數據寫入AVI文件*/ 39 jpeg2avi_add_frame(fp_avi, jpg_data, filesize); 40 } 41 42 fclose(fp_jpg); 43 } 44 45 jpeg2avi_end(fp_avi, 1920, 1080, 23); 46 47 fclose(fp_avi); 48 printf("end\n"); 49 50 return 0; 51 }