android原始數據
A. Android MediaCodec surface模式下如何讀取原始視頻數據
Android 用MediaCodec實現視頻硬解碼
本文向你講述如何用android標準的API (MediaCodec)實現視頻的硬體編解碼。常式將從攝像頭採集視頻開始,然後進行H264編碼,再解碼,然後顯示。我將盡量講得簡短而清晰,不展示那些不相關的代碼。但是,我不建議你讀這篇文章,也不建議你開發這類應用,而應該轉而開發一些戳魚、打鳥、其樂融融的程序。好吧,下面的內容是寫給那些執迷不悟的人的,看完之後也許你會同意我的說法:Android只是一個玩具,很難指望它來做靠譜的應用。
1、從攝像頭採集視頻
可以通過攝像頭Preview的回調,來獲取視頻數據。
首先創建攝像頭,並設置參數:
[java] view plain
cam = Camera.open();
cam.setPreviewDisplay(holder);
Camera.Parameters parameters = cam.getParameters();
parameters.setFlashMode("off"); // 無閃光燈
parameters.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
parameters.setPreviewFormat(ImageFormat.YV12);
parameters.setPictureSize(camWidth, camHeight);
parameters.setPreviewSize(camWidth, camHeight);
//這兩個屬性 如果這兩個屬性設置的和真實手機的不一樣時,就會報錯
cam.setParameters(parameters);
寬度和高度必須是攝像頭支持的尺寸,否則會報錯。要獲得所有支持的尺寸,可用getSupportedPreviewSizes,這里不再累述。據說所有的參數必須設全,漏掉一個就可能報錯,不過只是據說,我只設了幾個屬性也沒出錯。 然後就開始Preview了:
[java] view plain
buf = new byte[camWidth * camHeight * 3 / 2];
cam.addCallbackBuffer(buf);
cam.setPreviewCallbackWithBuffer(this);
cam.startPreview();
setPreviewCallbackWithBuffer是很有必要的,不然每次回調系統都重新分配緩沖區,效率會很低。
在onPreviewFrame中就可以獲得原始的圖片了(當然,this 肯定要 implements PreviewCallback了)。這里我們是把它傳給編碼器:
[java] view plain
public void onPreviewFrame(byte[] data, Camera camera) {
if (frameListener != null) {
frameListener.onFrame(data, 0, data.length, 0);
}
cam.addCallbackBuffer(buf);
}
2、編碼
首先要初始化編碼器:
[java] view plain
mediaCodec = MediaCodec.createEncoderByType("Video/AVC");
MediaFormat mediaFormat = MediaFormat.createVideoFormat(type, width, height);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodec.start();
然後就是給他喂數據了,這里的數據是來自攝像頭的:
[java] view plain
public void onFrame(byte[] buf, int offset, int length, int flag) {
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0)
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(buf, offset, length);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, 0, 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
if (frameListener != null)
frameListener.onFrame(outputBuffer, 0, length, flag);
mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
先把來自攝像頭的數據喂給它,然後從它裡面取壓縮好的數據喂給解碼器。
3、解碼和顯示
首先初始化解碼器:
[java] view plain
mediaCodec = MediaCodec.createDecoderByType("Video/AVC");
MediaFormat mediaFormat = MediaFormat.createVideoFormat(mime, width, height);
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodec.start();
這里通過給解碼器一個surface,解碼器就能直接顯示畫面。
然後就是處理數據了:
[java] view plain
public void onFrame(byte[] buf, int offset, int length, int flag) {
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(buf, offset, length);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, length, mCount * 1000000 / FRAME_RATE, 0);
mCount++;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
while (outputBufferIndex >= 0) {
mediaCodec.releaseOutputBuffer(outputBufferIndex, true);
outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
}
}
queueInputBuffer第三個參數是時間戳,其實怎麼寫都無所謂,只要是按時間線性增加的就可以,這里就隨便弄一個了。後面一段的代碼就是把緩沖區給釋放掉,因為我們直接讓解碼器顯示,就不需要解碼出來的數據了,但是必須要這么釋放一下,否則解碼器始終給你留著,內存就該不夠用了。
好了,到現在,基本上就可以了。如果你運氣夠好,現在就能看到視頻了,比如在我的三星手機上這樣就可以了。但是,我試過幾個其他平台,多數都不可以,總是有各種各樣的問題,如果要開發一個不依賴平台的應用,還有很多的問題要解決。說說我遇到的一些情況:
1、視頻尺寸
一般都能支持176X144/352X288這種尺寸,但是大一些的,640X480就有很多機子不行了,至於為什麼,我也不知道。當然,這個尺寸必須和攝像頭預覽的尺寸一致,預覽的尺寸可以枚舉一下。
2、顏色空間
根據ANdroid SDK文檔,確保所有硬體平台都支持的顏色,在攝像頭預覽輸出是YUV12,在編碼器輸入是COLOR_FormatYUV420Planar,也就是前面代碼中設置的那樣。 不過,文檔終究是文檔,否則安卓就不是安卓。
在有的平台上,這兩個顏色格式是一樣的,攝像頭的輸出可以直接作為編碼器的輸入。也有的平台,兩個是不一樣的,前者就是YUV12,後者等於I420,需要把前者的UV分量顛倒一下。下面的代碼效率不高,可供參考。
[java] view plain
byte[] i420bytes = null;
private byte[] swapYV12toI420(byte[] yv12bytes, int width, int height) {
if (i420bytes == null)
i420bytes = new byte[yv12bytes.length];
for (int i = 0; i < width*height; i++)
i420bytes[i] = yv12bytes[i];
for (int i = width*height; i < width*height + (width/2*height/2); i++)
i420bytes[i] = yv12bytes[i + (width/2*height/2)];
for (int i = width*height + (width/2*height/2); i < width*height + 2*(width/2*height/2); i++)
i420bytes[i] = yv12bytes[i - (width/2*height/2)];
return i420bytes;
}
這里的困難是,我不知道怎樣去判斷是否需要這個轉換。據說,Android 4.3不用再從攝像頭的PreView裡面取圖像,避開了這個問題。這里有個例子,雖然我沒讀,但看起來挺厲害的樣子,應該不會有錯吧(覺厲應然)。http://bigflake.com/mediacodec/CameraToMpegTest.java.txt
3、輸入輸出緩沖區的格式
SDK里並沒有規定格式,但是,這種情況H264的格式基本上就是附錄B。但是,也有比較有特色的,它就是不帶那個StartCode,就是那個0x000001,搞得把他編碼器編出來的東西送給他的解碼器,他自己都解不出來。還好,我們可以自己加。
[java] view plain
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size + 3];
outputBuffer.get(outData, 3, bufferInfo.size);
if (frameListener != null) {
if ((outData[3]==0 && outData[4]==0 && outData[5]==1)
|| (outData[3]==0 && outData[4]==0 && outData[5]==0 && outData[6]==1))
{
frameListener.onFrame(outData, 3, outData.length-3, bufferInfo.flags);
}
else
{
outData[0] = 0;
outData[1] = 0;
outData[2] = 1;
frameListener.onFrame(outData, 0, outData.length, bufferInfo.flags);
}
}
4、有時候會死在dequeueInputBuffer(-1)上面
根據SDK文檔,dequeueInputBuffer 的參數表示等待的時間(毫秒),-1表示一直等,0表示不等。按常理傳-1就行,但實際上在很多機子上會掛掉,沒辦法,還是傳0吧,丟幀總比掛掉好。當然也可以傳一個具體的毫秒數,不過沒什麼大意思吧。
B. android手機刷機怎麼保存原有的程序數據
進行手機數據備份以保存手機數據:
材料工具:手機
1、打開手機「設置」功能。
C. Android 怎樣穩定的獲取原生GPS數據
定位了就有經緯度,沒定位,則一直閃的;
一般是4個星的時候 會定位。
所以可以根據是否獲取有效的經緯度信息來判斷是否定位。
衛星的個數 sv_status也是可以通過API獲取的。LOCATION類
給一段咱的代碼,看看就知道了。
locationManager.addGpsStatusListener(statusListener);//偵聽GPS狀態
private GpsStatus.Listener statusListener = new GpsStatus.Listener()
{
public void onGpsStatusChanged(int event)
{
// TODO Auto-generated method stub
GpsStatus gpsStatus= locationManager.getGpsStatus(null);
Log.v(TAG,"GPS status listener ");
//Utils.DisplayToastShort(GPSService.this, "GPS status listener ");
switch(event)
{
case GpsStatus.GPS_EVENT_FIRST_FIX:{
//第一次定位時間UTC gps可用
//Log.v(TAG,"GPS is usable");
int i=gpsStatus.getTimeToFirstFix();
Utils.DisplayToastShort(GPSService.this, "GPS 第一次可用 "+i);
Utils.setGPSStatus(Utils.GPS_STATUS.START);
break;
}
case GpsStatus.GPS_EVENT_SATELLITE_STATUS:{//周期的報告衛星狀態
//得到所有收到的衛星的信息,包括 衛星的高度角、方位角、信噪比、和偽隨機號(及衛星編號)
Iterable<GpsSatellite> allSatellites;
allSatellites = gpsStatus.getSatellites();
Iterator<GpsSatellite>iterator = allSatellites.iterator();
int numOfSatellites = 0;
int maxSatellites=gpsStatus.getMaxSatellites();
while(iterator.hasNext() && numOfSatellites<maxSatellites){
numOfSatellites++;
iterator.next();
}
Log.v(TAG,"GPS is **unusable** "+ numOfSatellites +" "+ maxSatellites);
if( numOfSatellites < 3){
// Utils.DisplayToastShort(GPSService.this, "***衛星少於3顆***");
Utils.setGPSStatus(Utils.GPS_STATUS.STOP);
} else if(numOfSatellites > 7){
Utils.setGPSStatus(Utils.GPS_STATUS.START);
}
break;
}
case GpsStatus.GPS_EVENT_STARTED:{
//Utils.DisplayToastShort(GPSService.this, "GPS start Event");
break;
}
case GpsStatus.GPS_EVENT_STOPPED:{
//Utils.DisplayToastShort(GPSService.this, "GPS **stop*** Event");
Utils.setGPSStatus(Utils.GPS_STATUS.STOP);
break;
}
default :
break;
}
}
};
D. Android MediaCodec
MediaCodec 類為開發者提供了能訪問到Android底層媒體 Codec (Encoder/Decoder)的能力,它是Android底層多媒體基礎架構的一部分(通常和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack一起使用)。
Codec 對三種類型類型的數據起作用: 編碼後的壓縮數據 , 原始視頻數據 , 原始音頻數據 。這三種類型的數據都可以通過 ByteBuffer 來傳遞給 Codec ,但是對於 原始視頻數據 我們建議使用 Surface 來傳遞,這樣可以提高 Codec 的性能, Surface 使用的是 native video buffer ,不用映射或者拷貝成 ByteBuffer ,因此這樣的方式更高效。當你使用 Surface 來傳遞 原始視頻數據 時,也就無法獲取到了 原始視頻數據 ,Android 提供了 ImageReader 幫助你獲取到解碼後的 原始視頻數據 。這種方式可能仍然有要比 ByteBuffer 的方式更加高效,因為某些 native video buffer 會直接映射成 byteBuffer 。當然如果你 ByteBuffer 的模式,你可以使用 Image 類提供的 getInput/OutputImage(int) 來獲取 原始視頻數據 。
給 Decoder 輸入的 InputBuffer 或者 Encoder 輸出的 outputBuffer 包含的都是編碼後的壓縮數據,數據的壓縮類型由 MediaFormat#KEY_MIME 指明。對於視頻類型而言,這個數據通常是一個壓縮後的視頻幀。對於音頻數據而言,通常是一個訪問單元(一個編碼的音頻段,通常包含幾毫秒的音頻數據,數據類型format type 指定),有時候,一個音頻單元對於一個 buffer 而言可能有點寬松,所以一個 buffer 里可能包含多個編碼後的音頻數據單元。無論 Buffer 包含的是視頻數據還是音頻數據, Buffer 都不會再任意位元組邊界上開始或者結束,而是在幀(視頻)或者單元(音頻)的邊界上開始或者結束。除非它們被BUFFER_FLAG_PARTIAL_FRAME標記。
原始音頻Buffer包含PCM音頻數據的整個幀,是每一個通道按著通道順序的采樣數據。每一個采樣按16Bit量化。
在 ByteBuffer 模式下,視頻數據的排布由 MediaFormat#KEY_COLOR_FORMAT 指定,我們可以通過 getCodecInfo().MediaCodecInfo#getCapabilitiesForType.CodecCapabilities#colorFormats 獲取到一個設備支持的 color format 數組。視頻 Codec 可能支持三種類型的Color Format:
從 Build.VERSION_CODES.LOLLIPOP_MR1 開始所有的視頻 Codec 都支持 flexible YUV 4:2:0
對於 Build.VERSION_CODES.LOLLIPOP 之前並且支持 Image 類時,我們需要使用 MediaFormat#KEY_STRIDE 和 MediaFormat#KEY_SLICE_HEIGHT 的值去理解輸出的原始視頻數據的布局。
鍵值 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 指明了視頻Frame的size。然而,對於大多數用於編碼的視頻圖像,他們只佔用了video Frame的一部分。這部分用一個 'crop rectangle 來表示。
我們需要用下面的一些 keys 從獲取原始視頻數據的 crop rectangle ,如果 out format 中沒有包含這些 keys ,則表示視頻占據了整個 video Frame ,這個 crop rectangle 的解釋應該立足於應用任何 MediaFormat#KEY_ROTATION 之前。
下面是在旋轉之前計算視頻的尺寸的案例:
從概念上講Codec的聲明周期存在三種狀態: Stoped , Executing , Released 。 Stoped 狀態是一個集合狀態,它聚合了三種狀態: Uninitialized , Configured ,和 Error ,同時 Executing 狀態的處理也是通過三個子狀態來完成: Flushed , Running , End-of-Stream 。
Executing 狀態有三個子狀態:Flushed,Running,和End-of-Stream,當我們調用玩 Start() 函數後, Codec 就立刻進入 Flushed 子狀態,這個狀態下,它持有全部的buffer,只要第一個Input buffer被dequeued,Codec就轉變成 Running 子狀態,這個狀態占據了 Codec 的生命周期的絕大部分。當入隊一個帶有 end-of-stream標志的InputBuffer後, Codec 將轉換成 End of Stream 子狀態,在這個狀態下, Codec 將不會再接收任何輸入的數據,但是仍然會產生output buffer ,直到end-of-Stream標記的buffer被輸出。我們可以在 Executing 狀態的任何時候,使用 flush() 函數,將 Codec 切換成 Flushed 狀態。
調用 stop() 函數會將 Codec 返回到 Uninitialized 狀態,這樣我們就可以對 Codec 進行重新配置,當你用完了 Codec 後,你必須要調用 release() 函數去釋放這個 Codec 。
在極少數情況下, Codec 可能也會遇到錯誤,此時 Codec 將會切換到 Error 狀態,我們可以通過queuing操作獲取到一個無效的返回值,或者有時會通過異常來的得知 Codec 發生了錯誤。通過調用 reset() 函數,將 Codec 進行重置,這樣 Codec 將切換成 Uninitalized 狀態,我們可以在任何狀態下調用 rest() 函數將Codec 將切換成 Uninitalized`狀態。
使用 MediaCodecList 創建一個指定 MediaFormat 的MediaCodec。當我們解碼一個文件或者一個流時,我們可以通過 MediaExtractor#getTrackFormat 獲取期望的Fromat,同時我們可以通過 MediaFormat#setFeatureEnabled 為 Codec 注入任何我們想要的特性。然後調用 MediaCodecList#findDecoderForFormat 獲取能夠處理對應format數據 Codec 的name,最後我們使用 createByCodecName(String) 創建出這個 Codec 。
我們也可以使用 createDecoder/EncoderByType(java.lang.String) 函數來創建指定的 MIME 類型的 Codec ,但是這樣我們無法向其中注入一些指定的特性,這樣創建的 Codec 可能不能處理我們期望的媒體類型數據。
E. Android -- 音視頻基礎知識
幀,是視頻的一個基本概念,表示一張畫面,如上面的翻頁動畫書中的一頁,就是一幀。一個視頻就是由許許多多幀組成的。
幀率,即單位時間內幀的數量,單位為:幀/秒 或fps(frames per second)。一秒內包含多少張圖片,圖片越多,畫面越順滑,過渡越自然。 幀率的一般以下幾個典型值:
24/25 fps:1秒 24/25 幀,一般的電影幀率。
30/60 fps:1秒 30/60 幀,游戲的幀率,30幀可以接受,60幀會感覺更加流暢逼真。
85 fps以上人眼基本無法察覺出來了,所以更高的幀率在視頻里沒有太大意義。
這里我們只講常用到的兩種色彩空間。
RGB的顏色模式應該是我們最熟悉的一種,在現在的電子設備中應用廣泛。通過R G B三種基礎色,可以混合出所有的顏色。
這里著重講一下YUV,這種色彩空間並不是我們熟悉的。這是一種亮度與色度分離的色彩格式。
早期的電視都是黑白的,即只有亮度值,即Y。有了彩色電視以後,加入了UV兩種色度,形成現在的YUV,也叫YCbCr。
Y:亮度,就是灰度值。除了表示亮度信號外,還含有較多的綠色通道量。
U:藍色通道與亮度的差值。
V:紅色通道與亮度的差值。
音頻數據的承載方式最常用的是 脈沖編碼調制 ,即 PCM 。
在自然界中,聲音是連續不斷的,是一種模擬信號,那怎樣才能把聲音保存下來呢?那就是把聲音數字化,即轉換為數字信號。
我們知道聲音是一種波,有自己的振幅和頻率,那麼要保存聲音,就要保存聲音在各個時間點上的振幅。
而數字信號並不能連續保存所有時間點的振幅,事實上,並不需要保存連續的信號,就可以還原到人耳可接受的聲音。
根據奈奎斯特采樣定理:為了不失真地恢復模擬信號,采樣頻率應該不小於模擬信號頻譜中最高頻率的2倍。
根據以上分析,PCM的採集步驟分為以下步驟:
采樣率,即采樣的頻率。
上面提到,采樣率要大於原聲波頻率的2倍,人耳能聽到的最高頻率為20kHz,所以為了滿足人耳的聽覺要求,采樣率至少為40kHz,通常為44.1kHz,更高的通常為48kHz。
采樣位數,涉及到上面提到的振幅量化。波形振幅在模擬信號上也是連續的樣本值,而在數字信號中,信號一般是不連續的,所以模擬信號量化以後,只能取一個近似的整數值,為了記錄這些振幅值,采樣器會採用一個固定的位數來記錄這些振幅值,通常有8位、16位、32位。
位數越多,記錄的值越准確,還原度越高。
最後就是編碼了。由於數字信號是由0,1組成的,因此,需要將幅度值轉換為一系列0和1進行存儲,也就是編碼,最後得到的數據就是數字信號:一串0和1組成的數據。
整個過程如下:
聲道數,是指支持能不同發聲(注意是不同聲音)的音響的個數。 單聲道:1個聲道
雙聲道:2個聲道
立體聲道:默認為2個聲道
立體聲道(4聲道):4個聲道
碼率,是指一個數據流中每秒鍾能通過的信息量,單位bps(bit per second)
碼率 = 采樣率 * 采樣位數 * 聲道數
這里的編碼和上面音頻中提到的編碼不是同個概念,而是指壓縮編碼。
我們知道,在計算機的世界中,一切都是0和1組成的,音頻和視頻數據也不例外。由於音視頻的數據量龐大,如果按照裸流數據存儲的話,那將需要耗費非常大的存儲空間,也不利於傳送。而音視頻中,其實包含了大量0和1的重復數據,因此可以通過一定的演算法來壓縮這些0和1的數據。
特別在視頻中,由於畫面是逐漸過渡的,因此整個視頻中,包含了大量畫面/像素的重復,這正好提供了非常大的壓縮空間。
因此,編碼可以大大減小音視頻數據的大小,讓音視頻更容易存儲和傳送。
視頻編碼格式有很多,比如H26x系列和MPEG系列的編碼,這些編碼格式都是為了適應時代發展而出現的。
其中,H26x(1/2/3/4/5)系列由ITU(International Telecommunication Union)國際電傳視訊聯盟主導
MPEG(1/2/3/4)系列由MPEG(Moving Picture Experts Group, ISO旗下的組織)主導。
當然,他們也有聯合制定的編碼標准,那就是現在主流的編碼格式H264,當然還有下一代更先進的壓縮編碼標准H265。
H264是目前最主流的視頻編碼標准,所以我們後續的文章中主要以該編碼格式為基準。
H264由ITU和MPEG共同定製,屬於MPEG-4第十部分內容。
我們已經知道,視頻是由一幀一幀畫面構成的,但是在視頻的數據中,並不是真正按照一幀一幀原始數據保存下來的(如果這樣,壓縮編碼就沒有意義了)。
H264會根據一段時間內,畫面的變化情況,選取一幀畫面作為完整編碼,下一幀只記錄與上一幀完整數據的差別,是一個動態壓縮的過程。
在H264中,三種類型的幀數據分別為
I幀:幀內編碼幀。就是一個完整幀。
P幀:前向預測編碼幀。是一個非完整幀,通過參考前面的I幀或P幀生成。
B幀:雙向預測內插編碼幀。參考前後圖像幀編碼生成。B幀依賴其前最近的一個I幀或P幀及其後最近的一個P幀。
全稱:Group of picture。指一組變化不大的視頻幀。
GOP的第一幀成為關鍵幀:IDR
IDR都是I幀,可以防止一幀解碼出錯,導致後面所有幀解碼出錯的問題。當解碼器在解碼到IDR的時候,會將之前的參考幀清空,重新開始一個新的序列,這樣,即便前面一幀解碼出現重大錯誤,也不會蔓延到後面的數據中。
DTS全稱:Decoding Time Stamp。標示讀入內存中數據流在什麼時候開始送入解碼器中進行解碼。也就是解碼順序的時間戳。
PTS全稱:Presentation Time Stamp。用於標示解碼後的視頻幀什麼時候被顯示出來。
前面我們介紹了RGB和YUV兩種圖像色彩空間。H264採用的是YUV。
YUV存儲方式分為兩大類:planar 和 packed。
planar如下:
packed如下:
上面說過,由於人眼對色度敏感度低,所以可以通過省略一些色度信息,即亮度共用一些色度信息,進而節省存儲空間。因此,planar又區分了以下幾種格式:YUV444、 YUV422、YUV420。
YUV 4:4:4采樣,每一個Y對應一組UV分量。
YUV 4:2:2采樣,每兩個Y共用一組UV分量。
YUV 4:2:0采樣,每四個Y共用一組UV分量。
其中,最常用的就是YUV420。
YUV420屬於planar存儲方式,但是又分兩種類型:
YUV420P:三平面存儲。數據組成為YYYYYYYYUUVV(如I420)或YYYYYYYYVVUU(如YV12)。
YUV420SP:兩平面存儲。分為兩種類型YYYYYYYYUVUV(如NV12)或YYYYYYYYVUVU(如NV21)
原始的PCM音頻數據也是非常大的數據量,因此也需要對其進行壓縮編碼。
和視頻編碼一樣,音頻也有許多的編碼格式,如:WAV、MP3、WMA、APE、FLAC等等,音樂發燒友應該對這些格式非常熟悉,特別是後兩種無損壓縮格式。
但是,我們今天的主角不是他們,而是另外一個叫AAC的壓縮格式。
AAC是新一代的音頻有損壓縮技術,一種高壓縮比的音頻壓縮演算法。在MP4視頻中的音頻數據,大多數時候都是採用AAC壓縮格式。
AAC格式主要分為兩種:ADIF、ADTS。
ADIF:Audio Data Interchange Format。音頻數據交換格式。這種格式的特徵是可以確定的找到這個音頻數據的開始,不需進行在音頻數據流中間開始的解碼,即它的解碼必須在明確定義的開始處進行。這種格式常用在磁碟文件中。
ADTS:Audio Data Transport Stream。音頻數據傳輸流。這種格式的特徵是它是一個有同步字的比特流,解碼可以在這個流中任何位置開始。它的特徵類似於mp3數據流格式。
ADIF數據格式:
ADTS 一幀 數據格式(中間部分,左右省略號為前後數據幀):
AAC內部結構也不再贅述,可以參考AAC 文件解析及解碼流程
細心的讀者可能已經發現,前面我們介紹的各種音視頻的編碼格式,沒有一種是我們平時使用到的視頻格式,比如:mp4、rmvb、avi、mkv、mov...
沒錯,這些我們熟悉的視頻格式,其實是包裹了音視頻編碼數據的容器,用來把以特定編碼標准編碼的視頻流和音頻流混在一起,成為一個文件。
例如:mp4支持H264、H265等視頻編碼和AAC、MP3等音頻編碼。
我們在一些播放器中會看到,有硬解碼和軟解碼兩種播放形式給我們選擇,但是我們大部分時候並不能感覺出他們的區別,對於普通用戶來說,只要能播放就行了。
那麼他們內部究竟有什麼區別呢?
在手機或者PC上,都會有CPU、GPU或者解碼器等硬體。通常,我們的計算都是在CPU上進行的,也就是我們軟體的執行晶元,而GPU主要負責畫面的顯示(是一種硬體加速)。
所謂軟解碼,就是指利用CPU的計算能力來解碼,通常如果CPU的能力不是很強的時候,一則解碼速度會比較慢,二則手機可能出現發熱現象。但是,由於使用統一的演算法,兼容性會很好。
硬解碼,指的是利用手機上專門的解碼晶元來加速解碼。通常硬解碼的解碼速度會快很多,但是由於硬解碼由各個廠家實現,質量參差不齊,非常容易出現兼容性問題。
MediaCodec 是Android 4.1(api 16)版本引入的編解碼介面,是所有想在Android上開發音視頻的開發人員繞不開的坑。
由於Android碎片化嚴重,雖然經過多年的發展,Android硬解已經有了很大改觀,但實際上各個廠家實現不同, 還是會有一些意想不到的坑。
相對於FFmpeg,Android原生硬解碼還是相對容易入門一些,所以接下來,我將會從MediaCodec入手,講解如何實現視頻的編解碼,以及引入OpenGL實現對視頻的編輯,最後才引入FFmpeg來實現軟解,算是一個比較常規的音視頻開發入門流程吧。