音視頻基礎知識
視頻播放原理
我們先從一個簡單的視頻播放器的原理開始講述,下圖是一個最簡單的視頻播放的過程(不包括視頻加密等等過程):
這是一個視頻播放的最基本的原理流程圖,從這個圖可以很整體得看到視頻處理的一些主要步驟,后面我們會詳細介紹一些這里提到的基本概念。
注意:我們利用FFmpeg進行編程的時候幾乎就是基于這個流程圖來進行。比如說,編程的時候我們會拿到解碼器,解碼讀取數據,繪制到屏幕上面的時候可能還需要把YUV數據轉換為RGB等等。
我們常見的封裝視頻的格式有:flv(音視頻分開)、mp4、avi等等。后面我們會詳細說明。
為什么視頻需要經過封裝處理呢?
因為攝像頭采集到的畫面、以及麥克風采集到的音頻數據是經過壓縮的處理,不然視頻文件就會很大。
也就是說:
- 錄像、錄音,實質是一個壓縮采集的圖像或者聲音的過程。這個過程就是視頻編碼壓縮的過程。
- 播放視頻、音頻文件實質上就是解壓縮的過程,這個過程又稱為解碼。
視頻的封裝格式介紹
封裝格式的作用是:視頻碼流和音頻碼流按照一定的格式存儲在一個文件中。
封裝格式分析工具:Elecard Format Analyzer
為什么要音視頻分開存儲呢?因為音視頻的編碼格式各種各樣,同時編碼必然會造成混亂。
常見的視頻封裝格式有:
以兩個格式為例子,介紹一下原理:
- MPEG2-TS格式是由一個一個數據大小固定的TS-Packet組成,因此可以支持快進。
- FLY格式由FLV HEADER以及一個一個大小不固定的Tag組成。因為FLV格式直接能夠用flash(瀏覽器)播放,因此常用于視頻直播鄰域。我們在做RTMP推流的時候,一開始就需要發送頭信息。因為數據單元大小不固定,因此原生的視頻播放器不支持FLV視頻的快進(有些播放器進行了處理可以快進)。
視頻編解碼常見格式介紹
視頻的壓縮算法很多,因此編碼格式就會有很多種,下面介紹一些常見的編解碼格式:
視頻編解碼格式:
- 常見的視頻編碼格式有:H.264、MPEG2、VP8等(谷歌收購的WebRTC視頻通話就是用VP8)。
- 視頻解碼得到的像素數據YUV、RGB。YUV格式中,Y代表亮度,UV代表色度,人眼對亮度比較敏感,兩者比例為4:1,與生物學的理論有關。
原理分析:
以H264為例,H264是由大小不固定的NALU構成。(NALU實質是一種數據結構)。H264里面有很多子壓縮算法,原理比較復雜,包括了熵編碼,環路濾波,幀內檢測,幀間檢測等知識。H264編碼原理比較復雜,因此H264的壓縮效率是幾百到幾千倍。
我們需要學會FFmpeg即可,因為這個庫封裝了H264等格式的處理。
視頻解碼(攝像機獲取)得到的是視頻像素數據,保存了屏幕上每個像素點的像素值。常見的像素數據格式有RGB24, RGB32, YUV420P,YUV422P,YUV444P等。壓縮編碼中一般使用的是YUV格式的像素數據,最為常見的格式為YUV420P。
YUV視頻格式是沒有經過壓縮的,很大。早期在電視上面用得比較多,比如古老的黑白、彩電。彩電播放早期的黑白視頻實質上是只播放了Y(亮度)的數據,因為黑白視頻只有Y的數據嘛。
RGB也有很多種,比如RGB24,不同的RGB編碼色彩豐富度不同。
音視頻編解碼格式:
- 常見的音頻編碼格式有:AAC、MP3。
- 音頻解碼得到的是音頻采樣數據,然后喇叭才能播放。常見格式是PCM,實質是一個一個的采樣值。單位時間內震動的數據,包括振幅和頻率。常用采樣率44100,人耳朵能夠擦覺到的最高采樣率。
在做視頻直播的時候:音頻常用AAC來進行編碼,用FAAC庫來處理;視頻用H264編碼。
音頻采樣數據PCM:保存了音頻中每個采樣點的值,音頻采樣數據體積很大,一般需要進過壓縮,我們平常說的“無損”實質上是沒有損失的壓縮的意思。
相關播放(編輯)工具
- YUV:YUV Player
- PCM:Adobe Audition
- 查看視頻信息:MediaInfo
- 視頻編碼數據:Elecard Format Analyzer
- 視頻編碼分析工具:Elecard Stream Eye
有興趣可以下載玩玩。
FFmpeg介紹
FFmpeg是開源的C/C++音視頻處理的類庫,這個庫十分優秀,以至于很多大公司都在用。主流的視頻播放器幾乎都使用了FFmpeg。
FFmpeg的八個函數庫的基本介紹
如下圖所示:
Visual Studio下FFmpeg的項目配置
前言
我們一般是在VS中寫好代碼然后放到Android中的,因此有必要搭建VS的開發環境。
FFmpeg資源獲取
首先我們需要去FFmpeg的官網去獲取源碼,因為獲取的步驟比較麻煩,固下個筆記記錄下來,我們打開http://ffmpeg.org/:
點擊官網中大大的Download按鈕,跳轉到下面的界面:
選擇對應的系統,這里我們先介紹Windows版本的,點擊下面的Windows Builds,跳轉到下面的界面:
我們推薦使用舊版的FFmpeg庫,因為如果使用新版的話,除了問題很難去百度。筆者的電腦是64位的,于是就點擊All 64-bit Downloads。然后我們會跳轉到下面這個倉庫頁面:
其中,我們需要下載的FFmpeg版本是2.8系列的,我們推薦使用2.8或者以下的版本。其中,dev是開發版本的庫,shared是一些動態鏈接庫,static是一些已經編譯好的exe(Windows版本)可執行文件。這三個我們都需要下載下來。
如果你嫌麻煩的話,我下面直接給出下載地址:
https://ffmpeg.zeranoe.com/builds/win64/dev/2015/ffmpeg-20151105-git-c878082-win64-dev.7z
https://ffmpeg.zeranoe.com/builds/win64/shared/2015/ffmpeg-20151105-git-c878082-win64-shared.7z
https://ffmpeg.zeranoe.com/builds/win64/static/2015/ffmpeg-20151105-git-c878082-win64-static.7z
下面并解壓的效果如下:
注意:下面分別用dev、static、shared來代表這三個文件夾。
在命令行玩一玩static中的可執行文件
我們打開static文件夾,里面有個bin目錄,有三個exe文件。這就是我們即將要玩的東西:
為了簡化操作,我們不妨把bin目錄添加到環境變量path中。
然后我們準備一個測試用的視頻,例如筆者準備了一個test.flv視頻文件。
打開命令行,輸入:
ffmpeg -i test.flv test.avi
然后這就完成了一次簡單的視頻格式轉換。相信細心的你也會發現,FFmpeg的官網上面有這么一幅圖:
其實這就是一個最簡單的例子。
下面我們再來搞一個是視頻轉GIF,在命令行輸入下面的語句:
ffmpeg -ss 0 -t 11 -i test.flv -s 1366x768 -b:v 1500k test.gif
意思就是把test.flv轉換為test.gif文件,其中需要指定轉換的時間范圍,分辨率大小,比特率。
碼率(比特率),單位時間每一幀畫面以及音頻的大小,也叫作比特每秒,單位時間內播放連續的媒體例如壓縮后的音頻或者視頻的比特數量。碼率越高,音視頻越清晰。
把視頻轉gif是很有意義的,例如微信中的小視頻,我們沒有點開的時候,其實播放的是一個gif文件(可能使用的是libgif這個NDK庫),用戶點擊打開的時候,才會從服務器下載真正的視頻文件。這樣做大大降低了服務器的壓力。
最后我們看一個播放器的例子,輸入下面的命令,就會打開一個播放器,所以說如果你想研究一個Android平臺的播放器,那么需要研究ffplay相關的代碼:
ffplay test.flv
FFmpeg的VS項目配置
我們首先創建一個空項目,然后把dev中的include、lib兩個目錄拷貝到項目的根路徑下的源代碼目錄中(默認是與項目名一樣)。
然后在項目屬性中配置附加庫目錄:
然后配置附加庫目錄:
然后配置有哪些附加庫(附加依賴項),如下面所示:
avcodec.lib
avdevice.lib
avfilter.lib
avformat.lib
avutil.lib
postproc.lib
swresample.lib
swscale.lib
最后我們創建一個測試用的CPP文件:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
using namespace std;
//由于FFmpeg是C和C++混編的,因此使用extern是為了解決兼容問題
extern "C"{
#include "libavcodec/avcodec.h"
}
void main(){
//輸出FFmpeg的配置信息,檢查是否配置好
cout << avcodec_configuration() << endl;
system("pause");
}
然后你會發現編譯不過,因為我們用的FFmpeg庫是64位的,因此需要把我們的平臺改為64位的:
然后編譯通過,輸出的結果如下:
題外話——關于extern關鍵字的基本解釋
extern可以置于變量或者函數前,以標示變量或者函數的定義在別的文件中,提示編譯器遇到此變量和函數時在其他模塊中尋找其定義。此外extern也可用來進行鏈接指定。也就是說extern有兩個作用:
- 第一個,當它與"C"一起連用時,如: extern "C" void fun(int a, int b);則告訴編譯器在編譯fun這個函數名時按著C的規則去翻譯相應的函數名而不是C++的,C++的規則在翻譯這個函數名時會把fun這個名字變得面目全非,可能是fun@aBc_int_int#%$也可能是別的,這要看編譯器的"脾氣"了(不同的編譯器采用的方法不一樣),為什么這么做呢,因為C++支持函數的重載啊,在這里不去過多的論述這個問題,如果你有興趣可以去網上搜索,相信你可以得到滿意的解釋!
- 第二,當extern不與"C"在一起修飾變量或函數時,如在頭文件中: extern int g_Int; 它的作用就是聲明函數或全局變量的作用范圍的關鍵字,其聲明的函數和變量可以在本模塊活其他模塊中使用,記住它是一個聲明不是定義!也就是說B模塊(編譯單元)要是引用模塊(編譯單元)A中定義的全局變量或函數時,它只要包含A模塊的頭文件即可,在編譯階段,模塊B雖然找不到該函數或變量,但它不會報錯,它會在連接時從模塊A生成的目標代碼中找到此函數。
如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:
我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)。