一彬犯、編碼基礎概念
1.1 為什么要進行視頻編碼
視頻是由一幀幀圖像組成,就如常見的gif圖片查吊,如果打開一張gif圖片谐区,可以發(fā)現里面是由很多張圖片組成。一般視頻為了不讓觀眾感覺到卡頓逻卖,一秒鐘至少需要24幀畫面(一般是30幀)宋列,假如該視頻是一個1280x720分辨率的視頻,那么不經過編碼一秒鐘的大衅酪病:
結果:1280x720x4x24/(1024*1024)≈84.375M
所以不經過編碼的視頻根本沒法保存炼杖,更不用說傳輸了。
1.2 視頻壓縮編碼標準
視頻中存在很多冗余信息盗迟,比如圖像相鄰像素之間有較強的相關性坤邪,視頻序列的相鄰圖像之間內容相似,人的視覺系統(tǒng)對某些細節(jié)不敏感等罚缕,對這部分冗余信息進行處理的過程就是視頻編碼艇纺。
H.26X系列(由ITU[國際電傳視訊聯盟]主導)
H.261:主要在老的視頻會議和視頻電話產品中使用
H.263:主要用在視頻會議、視頻電話和網絡視頻上
H.264:H.264/MPEG-4第十部分邮弹,或稱AVC(Advanced Video Coding黔衡,高級視頻編碼),是一種視頻壓縮標準腌乡,一種被廣泛使用的高精度視頻的錄制盟劫、壓縮和發(fā)布格式。
H.265:高效率視頻編碼(High Efficiency Video Coding与纽,簡稱HEVC)是一種視頻壓縮標準侣签,H.264/MPEG-4 AVC的繼任者塘装。可支持4K分辨率甚至到超高畫質電視硝岗,最高分辨率可達到8192×4320(8K分辨率)氢哮,這是目前發(fā)展的趨勢,尚未有大眾化編碼軟件出現
MPEG系列(由ISO[國際標準組織機構]下屬的MPEG[運動圖象專家組]開發(fā))
MPEG-1第二部分:MPEG-1第二部分主要使用在VCD上型檀,有些在線視頻也使用這種格式
MPEG-2第二部分(MPEG-2第二部分等同于H.262,使用在DVD听盖、SVCD和大多數數字視頻廣播系統(tǒng)中
MPEG-4第二部分(MPEG-4第二部分標準可以使用在網絡傳輸胀溺、廣播和媒體存儲上
1.3 編碼流程
在進行當前信號編碼時,編碼器首先會產生對當前信號做預測的信號皆看,稱作預測信號(predicted signal)
預測的方式
時間上的預測(interprediction)仓坞,亦即使用先前幀的信號做預測
空間上的預測 (intra prediction),亦即使用同一張幀之中相鄰像素的信號做預測
得到預測信號后腰吟,編碼器會將當前信號與預測信號相減得到殘余信號(residual signal)无埃,并只對殘余信號進行編碼,如此一來,可以去除一部份時間上或是空間上的冗余信息毛雇。
編碼器并不會直接對殘余信號進行編碼嫉称,而是先將殘余信號經過變換(通常為離散余弦變換)然后量化以進一步去除空間上和感知上的冗余信息。量化后得到的量化系數會再透過熵編碼灵疮,去除統(tǒng)計上的冗余信息织阅。
二、H.264編碼詳解(AVC)
2.1 H.264是新一代的編碼標準震捣,以高壓縮高質量和支持多種網絡的流媒體傳輸著稱荔棉。
相關理解:
在相鄰幾幅圖像畫面中,一般有差別的像素只有10%以內的點,亮度差值變化不超過2%蒿赢,而色度差值的變化只有1%以內润樱,所以對于一段變化不大圖像畫面,我們可以先編碼出一個完整的圖像幀A羡棵,隨后的B幀就不編碼全部圖像壹若,只寫入與A幀的差別,這樣B幀的大小就只有完整幀的1/10或更小晾腔,B幀之后的C幀如果變化不大舌稀,我們可以繼續(xù)以參考B的方式編碼C幀,這樣循環(huán)下去灼擂。
這段圖像我們稱為一個序列壁查,序列就是有相同特點的一段數據,當某個圖像與之前的圖像變化很大剔应,無法參考前面的幀來生成睡腿,那我們就結束上一個序列语御,開始下一段序列,也就是對這個圖像生成一個完整幀A1席怪,隨后的圖像就參考A1生成应闯,只寫入與A1的差別內容。
主要目標:
高的視頻壓縮比挂捻;
良好的網絡親和性碉纺;
2.2 H.264三種幀
在H.264中定義了三種幀:
????I幀:完整編碼的幀叫I幀
????P幀:參考之前的I幀生成的只包含差異部分編碼的幀叫P幀
????B幀:參考前后的幀編碼的幀叫B幀
H264采用的核心算法是幀內壓縮和幀間壓縮:
????幀內壓縮是生成I幀的算法
????幀間壓縮是生成B幀和P幀的算法
壓縮方法:
????分組:把幾幀圖像分為一組(GOP,也就是一個序列),為防止運動變化,幀數不宜取多
????定義幀:將每組內各幀圖像定義為三種類型,即I幀刻撒、B幀和P幀;
????預測幀:以I幀做為基礎幀,以I幀預測P幀,再由I幀和P幀預測B幀;
????數據傳輸:最后將I幀數據與預測的差值信息進行存儲和傳輸骨田。
GOP序列:
????在H264中圖像以序列為單位進行組織,一個序列是一段圖像編碼后的數據流声怔。
????一個序列的第一個圖像叫做 IDR 圖像(立即刷新圖像)态贤,IDR 圖像都是 I 幀圖像
2.3 編碼YUV為H.26X編碼視頻格式
YUV數據
YUV通過Y,U醋火,V三個分量表示顏色空間悠汽,Y表示亮度,UV表示色度芥驳。RGB顏色空間每個像素點都有獨立的RGB三個顏色分量值柿冲。YUV卻不同:
YUV根據UV采樣數目的不同,分為YUV444晚树,YUV422姻采,YUV420等。
YUV420
表示每個像素點有一個獨立的亮度表示爵憎,即Y慨亲;色度UV分量由每四個像素點共享一個。例如一個4X4的圖片宝鼓,在YUV420格式下刑棵,有16個Y,UV各四個愚铡。
YUV420根據UV色度的存儲順序不同蛉签,又分為不同的格式。它分為兩個YUV420P和YUV420SP兩個大類沥寥,YUV420P的UV順序存儲碍舍,YUV420SP的UV交錯存儲。以4X4的圖片格式為例部分格式如下:
三邑雅、H.264分層設計
3.1 H264算法在概念上分為兩層:
1片橡、VCL:(Video Coding Layer)視頻編碼層,負責高效的內容表示淮野。
2捧书、NAL:(Network Abstraction Layer)網絡提取層吹泡,負責以網絡所要求的恰當的方式對數據進行打包和傳送。
VCL數據即被壓縮編碼后的視頻數據序列经瓷。在VCL數據要封裝到NAL單元中之后爆哑,才可以用來傳輸或存儲。
H.264 的編碼視頻序列包括一系列的NAL 單元舆吮,每個NAL 單元包含一個RBSP揭朝。一個原始的H.264 NALU 單元常由 [StartCode] [NALU Header] [NALU Payload] 三部分組成,其中 Start Code 用于標示這是一個NALU 單元的開始色冀,必須是"00 00 00 01" 或"00 00 01"萝勤。
其中RBPS有分為幾種類型:
3.2 NAL頭
占一個字節(jié)(8bit),由三部分組成forbidden_bit(1bit)呐伞,nal_reference_bit(2bits)(優(yōu)先級),nal_unit_type(5bits)(類型)慎式。
3.2.1 forbidden_bit
禁止位伶氢,H264文檔規(guī)定,這個值應該為0瘪吏,當它不為0時癣防,表示網絡傳輸過程中,當前NALU中可能存在錯誤掌眠,解碼器可以考慮不對這個NALU進行解碼蕾盯。
3.2.2 nal_reference_bit
當前NAL的優(yōu)先級,值越大蓝丙,該NAL越重要级遭。
3.2.3 nal_unit_type
它表示NALU Header后面的RBSP的數據結構的類型。下圖為nal_unit_type所有可能的取值渺尘,和對應的語義:
可以看到挫鸽,nal_unit_type的值為1-5時,表示RBSP里面包含的數據為條帶(片/Slice)數據鸥跟,所以值為1-5的NALU統(tǒng)稱為VCL(視像編碼層)單元丢郊,其他的NALU則稱為非VCL NAL單元。
當nal_unit_type為7時医咨,代表當前NALU為序列參數集枫匾,為8時為圖像參數集。這也是我們打開.h264文件后拟淮,遇到的前兩個NALU干茉,它們位于碼流的最前面。
而且當nal_unit_type為14-31時惩歉,我們可以不用理睬等脂,目前幾乎用不到俏蛮。
PPS(Picture Parameter Sets):圖像參數集
SPS(Sequence Parameter Set):序列參數集
接下來說說SPS、PPS和SEI上遥。
SPS和PPS是用來初始化解碼器的搏屑,沒這些數據,視頻數據是無法解析出來的粉楚。另外如果我們分析單獨的H264文件辣恋,可以發(fā)現有的文件每個IDR幀前面都有PPS和SPS,有的只是開頭才有模软。針對SPS和PPS伟骨,一般來說:
1)、如果是在直播的話燃异,每個IDR幀前面都應該加上SPS和PPS携狭,因為有的觀眾會中途進來觀看。
2)回俐、如果是本地穩(wěn)定文件逛腿,可以在開頭加上SPS和PPS,或者都加上仅颇,這個根據具體需要來单默。
另外說明下SEI,有的H264文件有SEI忘瓦,有的則沒有搁廓,這說明SEI對文件的播放并無太大影響。
SEI(Supplemental Enhancement Information):輔助增強信息耕皮。這里面可以存放一些影片簡介境蜕,版權信息或者作者自己添加的一些信息。
四明场、實例
4.1 MediaCodec解碼本地視頻 H.264文件汽摹,然后在SurfaceView上預覽
4.1.1 從本地H264文件中獲取文件流
public void getFileInputStream() {
try {
File file = new File(H264_FILE);
mInputStream = new DataInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
try {
mInputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
4.1.2 初始化解碼器
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private void initMediaCodec() {
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
//創(chuàng)建編碼器
mCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
} catch (IOException e) {
e.printStackTrace();
}
//初始化編碼器
final MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,holder.getSurfaceFrame().width(),holder.getSurfaceFrame().height());
/*h264常見的幀頭數據為:
00 00 00 01 67 (SPS)
00 00 00 01 68 (PPS)
00 00 00 01 65 (IDR幀)
00 00 00 01 61 (P幀)*/
//獲取H264文件中的pps和sps數據
if (UseSPSandPPS) {
byte[] header_sps = {0, 0, 0, 1, 67, 66, 0, 42, (byte) 149, (byte) 168, 30, 0, (byte) 137, (byte) 249, 102, (byte) 224, 32, 32, 32, 64};
byte[] header_pps = {0, 0, 0, 1, 68, (byte) 206, 60, (byte) 128, 0, 0, 0, 1, 6, (byte) 229, 1, (byte) 151, (byte) 128};
mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
mediaFormat.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
}
//設置幀率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE,40);
mCodec.configure(mediaFormat,holder.getSurface(),null,0);
startDecodingThread();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
4.1.3 開啟新的線程進行解碼
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
private class DecodeThread implements Runnable{
@Override
public void run() {
//循環(huán)解碼
decodeLoop();
}
private void decodeLoop(){
//獲取一組輸入緩存區(qū)
ByteBuffer[] inputBuffers = mCodec.getInputBuffers();
//解碼后的數據,包含每一個buffer的元數據信息
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
long startMs = System.currentTimeMillis();
long timeoutUs = 10000;
//用于檢測文件頭
byte[] maker0 = new byte[]{0,0,0,1};
byte[] dummyFrame = new byte[]{0x00,0x00,0x01,0x20};
byte[] streamBuffer = null;
try {
//返回可用的字節(jié)數組
streamBuffer = getBytes(mInputStream);
} catch (IOException e) {
e.printStackTrace();
}
int bytes_cnt = 0;
while (mStopFlag == false){
//得到可用字節(jié)數組長度
bytes_cnt = streamBuffer.length;
if (bytes_cnt == 0){
streamBuffer = dummyFrame;
}
int startIndex = 0;
//定義記錄剩余字節(jié)的變量
int remaining = bytes_cnt;
while (true) {
//當剩余的字節(jié)=0或者開始的讀取的字節(jié)下標大于可用的字節(jié)數時 不在繼續(xù)讀取
if (remaining == 0 || startIndex >= remaining) {
break;
}
//尋找?guī)^部
int nextFrameStart = KMPMatch(maker0,streamBuffer,startIndex + 2,remaining);
//找不到頭部返回-1
if (nextFrameStart == -1) {
nextFrameStart = remaining;
}
//得到可用的緩存區(qū)
int inputIndex = mCodec.dequeueInputBuffer(timeoutUs);
//有可用緩存區(qū)
if (inputIndex >= 0) {
ByteBuffer byteBuffer = inputBuffers[inputIndex];
byteBuffer.clear();
//將可用的字節(jié)數組苦锨,傳入緩沖區(qū)
byteBuffer.put(streamBuffer, startIndex, nextFrameStart - startIndex);
//把數據傳遞給解碼器
mCodec.queueInputBuffer(inputIndex, 0, nextFrameStart - startIndex, 0, 0);
//指定下一幀的位置
startIndex = nextFrameStart;
} else {
continue;
}
int outputIndex = mCodec.dequeueOutputBuffer(info,timeoutUs);
if (outputIndex >= 0) {
//幀控制是不在這種情況下工作逼泣,因為沒有PTS H264是可用的
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
boolean doRender = (info.size != 0);
//對outputbuffer的處理完后,調用這個函數把buffer重新返回給codec類舟舒。
mCodec.releaseOutputBuffer(outputIndex, doRender);
} else {
}
}
mStopFlag = true;
}
}
}
源碼地址:https://github.com/Xiaoben336/MediaCodecDecodeH264Demo
4.2 收集Camera數據拉庶,并轉碼為H264存儲到文件
4.2.1 實現方式
視頻采集用Camera 2采集YV12數據編碼為H264視頻文件并保存。
1秃励、我們要實現的功能位通過Camera采集到每幀YUV原始數據
2氏仗、錄制視頻后,編碼YUV原始數據為H.264視頻格式
3、保存H.264編碼格式的視頻文件
在使用Camera的時候皆尔,設置預覽的數據格式為NV21
在使用Camera2的時候呐舔,無法支持預覽數據為NV21,我設置的預覽數據格式YV12
4.2.2 編碼實現
4.2.2.1 初始化MediaCodec
public AvcEncoder(int width, int height, int framerate, File outFile,boolean isCamera) {
this.mIsCamera = isCamera;
mWidth = width;
mHeight = height;
mFrameRate = framerate;
mOutFile = outFile;
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);//YUV420SP
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);//*5表示較高的比特率慷蠕,也可以是1珊拼、3
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
try {
mMediaCodec = MediaCodec.createEncoderByType("video/avc");
} catch (IOException e) {
e.printStackTrace();
}
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();
createfile();//創(chuàng)建H264文件的保存位置
}
4.2.2.2 開始編碼
public boolean isRunning = false;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void startEncoderThread(){
Thread encoderThread = new Thread(new Runnable() {
@Override
public void run() {
isRunning = true;
byte[] input = null;
long pts = 0;
long generateIndex = 0;
while (isRunning) {
if (mYuvQueue.size() > 0) {
input = mYuvQueue.poll();
if (mIsCamera) {//Camera NV21
//NV12數據所需空間為如下,所以建立如下緩沖區(qū)
//y=W*h;u=W*H/4;v=W*H/4,so total add is W*H*3/2 (1 + 1/4 + 1/4 = 3/2)
byte[] yuv420sp = new byte[mWidth * mHeight *3 /2];
NV21ToNV12(input, yuv420sp, mWidth, mHeight);
input = yuv420sp;
} else {//Camera 2
byte[] yuv420sp = new byte[mWidth * mHeight *3 /2];
YV12toNV12(input, yuv420sp, mWidth, mHeight);
input = yuv420sp;
}
}
if (input != null) {
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
pts = computePresentationTime(generateIndex);
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(input);
mMediaCodec.queueInputBuffer(inputBufferIndex,0,input.length,pts,0);
generateIndex += 1;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
if (bufferInfo.flags == 2) {
mConfigByte = new byte[bufferInfo.size];
mConfigByte = outData;
} else if (bufferInfo.flags == 1) {
byte[] keyframe = new byte[bufferInfo.size + mConfigByte.length];
System.arraycopy(mConfigByte, 0, keyframe, 0, mConfigByte.length);
System.arraycopy(outData, 0, keyframe, mConfigByte.length, outData.length);
try {
outputStream.write(keyframe, 0, keyframe.length);
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
outputStream.write(outData, 0, outData.length);
} catch (IOException e) {
e.printStackTrace();
}
}
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo,TIMEOUT_USEC);
}
} else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
encoderThread.start();
}
需要把Camera的NV21的預覽數據和Camera2的YV12預覽數據都轉換成了NV12格式的數據流炕。
H.264編碼必須要用NV12澎现,所以我們拿到預覽數據要做格式轉換 .
4.2.3 獲取預覽數據
開啟攝像頭后,獲取預覽數據每辟,參考之前的實現了Camera有回掉很簡單(前面有寫Camera 2的使用)剑辫,Camera2通過ImageRender來獲取。
private static final int STATE_PREVIEW = 0;
private static final int STATE_RECORD = 1;
private int mState = STATE_PREVIEW;
private AvcEncoder mAvcEncoder;
private int mFrameRate = 30;
private void setupImageReader() {
//2代表ImageReader中最多可以獲取兩幀圖像流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(),mPreviewSize.getHeight(), ImageFormat.YV12,1);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Log.e(TAG, "onImageAvailable: "+Thread.currentThread().getName() );
//這里一定要調用reader.acquireNextImage()和img.close方法否則不會一直回掉了
Image img = reader.acquireNextImage();
switch (mState){
case STATE_PREVIEW:
Log.e(TAG, "mState: STATE_PREVIEW");
if (mAvcEncoder != null) {
mAvcEncoder.stopThread();
mAvcEncoder = null;
Toast.makeText(mContext,"停止錄制視頻成功",Toast.LENGTH_SHORT).show();
}
break;
case STATE_RECORD:
Log.e(TAG, "mState: STATE_RECORD");
Image.Plane[] planes = img.getPlanes();
byte[] dataYUV = null;
if (planes.length >= 3) {
ByteBuffer bufferY = planes[0].getBuffer();
ByteBuffer bufferU = planes[1].getBuffer();
ByteBuffer bufferV = planes[2].getBuffer();
int lengthY = bufferY.remaining();
int lengthU = bufferU.remaining();
int lengthV = bufferV.remaining();
dataYUV = new byte[lengthY + lengthU + lengthV];
bufferY.get(dataYUV, 0, lengthY);
bufferU.get(dataYUV, lengthY, lengthU);
bufferV.get(dataYUV, lengthY + lengthU, lengthV);
}
if (mAvcEncoder == null) {
mAvcEncoder = new AvcEncoder(mPreviewSize.getWidth(),
mPreviewSize.getHeight(), mFrameRate,
getOutputMediaFile(MEDIA_TYPE_VIDEO), false);
mAvcEncoder.startEncoderThread();
Toast.makeText(mContext, "開始錄制視頻成功", Toast.LENGTH_SHORT).show();
}
mAvcEncoder.putYUVData(dataYUV);
break;
default:
break;
}
img.close();
}
},mBackgroundHandler);
}