一 基本實現原理
在介紹具體實現過程之前,先簡單說下基本原理和實現步驟,在解決相對比較複雜的問題,我習慣先理清主要原理步驟,不要一開始就被繁瑣細節絆住,待具體實現時再逐個攻破。下面是主要步驟:
1、視頻檔案的讀取:包括錄製和本地檔案讀取
2、將需要轉換的視頻部分解析為 Bitmap 序列
3、將解析好的 Bitmap 序列編碼產生 GIF 檔案
二 視頻檔案的讀取
視頻檔案的讀取比較簡單,沒什麼特別需要說的地方,這裡簡單貼出視頻讀取的核心部分代碼,詳細實現可以Google一下就行了。
private View.OnClickListener clickListener = new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(); intent.setType("video/*"); intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, "Select Video"), SELECT_VIDEO); }}; @Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_SELECT_VIDEO) { if (resultCode == RESULT_OK) { Uri videoUri = data.getData(); filePath = getRealFilePath(videoUri); } }}
三 視頻檔案的解析
視頻檔案讀取成功後,接下來要做的就是解析視頻檔案,選取需要轉換的視頻片段,提取Bitmap序列。下面來看下具體實現,提取 Bitmap 序列就是根據給定的起始時間和結束時間以及幀率從視頻檔案中擷取相應的 Bitmap,本文主要是利用 MediaMetadataRetriever
提供的 API 來實現的,在看代碼前可以先看下 MediaMetadataRetriever
的 API 文檔,該類的核心功能就是擷取視頻的幀和中繼資料,下面是核心實現代碼:
public List<Bitmap> createBitmaps(String path) { MediaMetadataRetriever mmr = new MediaMetadataRetriever(); mmr.setDataSource(path); double inc = 1000 * 1000 / fps; for (double i = begin; i < end; i += inc) { Bitmap frame = mmr.getFrameAtTime((long) i, MediaMetadataRetriever.OPTION_CLOSEST); if (frame != null) { bitmaps.add(scale(frame)); } } return bitmaps;} private Bitmap scale(Bitmap bitmap) { return Bitmap.createScaledBitmap(bitmap, width > 0 ? width : bitmap.getWidth(), height > 0 ? height : bitmap.getHeight(), true);}
四 產生 GIF 檔案
拿到要產生 GIF 的 Bitmap 序列,接下來需要做的就是將 Bitmap 序列中的資料按照 GIF 的檔案格式編碼,產生最終的 GIF 檔案。目標很明確,接下來就看具體實現過程了。
1. GIF 格式簡介
產生 GIF 檔案之前有必要介紹下 GIF 的儲存格式,GIF 格式的相關文章比較多,這裡也沒必要太詳細的介紹,只是簡單說下後面程式中會用到的方面。
GIF 圖象是基於顏色列表的(儲存的資料是該點的顏色對應於顏色列表的索引值),最多隻支援 8 位(256 色)。GIF 檔案內部分成許多儲存塊,用來儲存多幅圖象或者是決定圖象表現行為的控制塊,用以實現動畫和互動式應用。GIF 檔案還通過 LZW 壓縮演算法壓縮圖象資料來減少圖象尺寸。
GIF 檔案內部是按塊劃分的,包括控制塊和資料區塊兩種。控制塊是控制資料區塊行為的,根據不同的控制塊包含一些不同的控制參數;資料區塊只包含一些 8-bit 的字元流,由它前面的控制塊來決定它的功能,每個資料區塊 0 到 255 個位元組,資料區塊的第一個位元組指出這個資料區塊大小(位元組數),計算資料區塊的大小時不包括這個位元組,所以一個空的資料區塊有一個位元組,那就是資料區塊的大小0x00。
2. GIF 檔案寫入
剛開始接觸 GIF 檔案會覺得比較複雜,儲存格式、編碼格式等都比 Bitmap 要複雜的多,但其實可以把問題簡單化理解,產生 GIF 和產生 Bitmap 原理類似,就是按照規定的格式寫檔案就行了,不用太糾結內部細節,否則就會陷入繁瑣的細節(俗稱鑽牛角尖)而忽略了最終目的只是為了產生 GIF 檔案。下面就來看下有哪些檔案部分需要寫入的:
提取 Bitmap 的像素值
首先需要將上面得到的 Bitmap 的像素值提取出來,方便後面把像素值寫入到 GIF 檔案中,在提取像素值的同時,產生 GIF 檔案所需要的顏色表,產生顏色表過程比較複雜,這裡就不貼出源碼,感興趣的可以Google一下顏色量化演算法,不感興趣的直接用現成的就好,下面是提取像素值的具體實現:
protected void getImagePixels() { int w = image.getWidth(); int h = image.getHeight(); pixels = new byte[w*h*3]; for (int i = 0; i < h; i++) { int stride = w * 3 * i; for (int j = 0; j < w; j++) { int p = image.getPixel(j, i); int step = j * 3; int offset = stride + step; // blue pixels[offset+0] = (byte) ((p & 0x0000FF) >> 0); // green pixels[offset+1] = (byte) ((p & 0x00FF00) >> 8); // red pixels[offset+2] = (byte) ((p & 0xFF0000) >> 16); } }}
GIF 檔案頭(Header)
檔案頭部分總共 6 個位元組,包括:GIF 署名和版本號碼,GIF 署名由 3 個字元"GIF"組成,共 3 個位元組,版本號碼也是由 3 個位元組組成,可以為"87a"或"89a"(分別為 1987 年和 1989 年版本),實現代碼如下:
// 寫入檔案頭protected void writeHeader() throws IOException { writeString("GIF89a");} protected void writeString(String s) throws IOException { for (int i = 0; i < s.length(); i++) { out.write((byte) s.charAt(i)); }}
邏輯螢幕標識符(Logical Screen Descriptor)
檔案頭的後面是邏輯螢幕標識符(Logical Screen Descriptor),這一部分由 7 個位元組組成,定義了 GIF 圖象的大小、色彩深度、背景色以及有無全域顏色列表和顏色列表的索引數。實現代碼如下:
// 寫入邏輯螢幕標識符protected void writeLSD() throws IOException { writeShort(width); // 寫入映像寬度 writeShort(height); // 寫入映像高度 out.write((0x80 | // 全域顏色列表標誌置 1 0x70 | // 確定圖象的色彩深度(7+1=8) 0x00 | // 全域顏色列表分類排列置為 0 0x07)); // 顏色列表的索引數(2的7+1次方) out.write(0); // 背景顏色(在全域顏色列表中的索引) out.write(0); // 像素寬高比預設 1:1} protected void writeShort(int value) throws IOException { out.write(value & 0xff); out.write((value >> 8) & 0xff);}
邏輯螢幕標識符部分結構稍微複雜些,如果不知道每一位代表什麼意思可以參考:GIF圖形檔案格式文檔 中的邏輯螢幕標識符部分。
全域顏色列表(Global Color Table)
全域顏色列表必須緊跟在邏輯螢幕標識符後面,每個顏色清單索引條目由三個位元組組成,按R、G、B的順序排列,具體產生顏色表的實現可以看源碼部分,由於產生過程比較複雜,這裡就不貼顏色表產生的程式碼了,下面是寫入顏色表的代碼:
// 寫入顏色表protected void writePalette() throws IOException { out.write(colorTab, 0, colorTab.length); int n = (3 * 256) - colorTab.length; for (int i = 0; i < n; i++) { out.write(0); }}
圖形控制擴充(Graphic Control Extension)
這一部分是可選的,89a 版本才支援,可以放在一個圖象塊(包括圖象標識符、局部顏色列表和圖象資料)或文本擴充塊的前面,用來控制跟在它後面的第一個圖象(或文本)的渲染( Render )形式,下面實現代碼:
protected void writeGraphicCtrlExt() throws IOException { out.write(0x21); // 擴充塊標識,固定值 0x21 out.write(0xf9); // 圖形控制擴充標籤,固定值 0xf9 out.write(4); // 塊大小,固定值 4 out.write(0 | // 1:3 保留位 0 | // 4:6 不使用處置方法 0 | // 7 使用者輸入標誌置 0 0); // 8 透明色標誌置 0 writeShort(delay); // 延遲時間 out.write(0); // 透明色索引值 out.write(0); // 塊終結器,固定值 0}
圖象標識符(Image Descriptor)
一個 GIF 檔案內可以包含多幅圖象,一幅圖象結束之後緊接著下是一幅圖象的標識符,圖象標識符以 0x2C(',')字元開始,定義緊接著它的圖象的性質,包括圖象相對於邏輯螢幕邊界的位移量、圖象大小以及有無局部顏色列表和顏色列表大小,由10個位元組組成,下面是實現代碼:
protected void writeImageDesc() throws IOException { out.write(0x2c); // 圖象標識符開始,固定值為 0x2c writeShort(0); // x 方向位移 writeShort(0); // y 方向位移 writeShort(width); // 映像寬度 writeShort(height); // 映像高度 out.write(( 0x80 | // 局部顏色列表標誌置 1 0x00 | 0x00 | 0x07)); // 局部顏色列表的索引數(2的7+1次方)}
圖象資料(Image Data)
GIF 圖象資料使用了 LZW 壓縮演算法,大大減小了圖象資料的大小,具體的 LZW 壓縮演算法可以Google一下,程式實現部分可以參考文章底部的源碼連結。下面是映像資料的寫入實現:
protected void writePixels() throws IOException { LZWEncoder encoder = new LZWEncoder( width, height, indexedPixels, colorDepth); encoder.encode(out);}
檔案終結器(Trailer)
這一部分只有一個位元組,標識一個GIF檔案結束,固定值為 0x3B,實現代碼:
public void finish() throws IOException { out.write(0x3b); out.flush(); out.close();}
總結
到目前為止,將 MP4 檔案轉換為 GIF 檔案的實現過程基本完成,如果需要對 GIF 檔案進行裁剪、添加浮水印等處理的話,可以在 Bitmap 序列寫入 GIF 之前,對 Bitmap 進行相應的處理即可,如果有什麼問題歡迎交流學習。希望本文的內容對大家的學習工作能有所協助。