播放一個視頻,都需要經(jīng)過解封裝衔瓮、視頻解碼浊猾、音頻解碼、音視頻同步热鞍、視頻輸出顯示和音頻輸出播放等過程葫慎。
先簡單介紹下視頻解碼的流程。
image.png
解協(xié)議:
就是將流媒體協(xié)議的數(shù)據(jù)薇宠,解析為標準的相應(yīng)的封裝格式數(shù)據(jù)偷办。音視頻在?絡(luò)上傳播的時候,常常采?各種流媒體協(xié)議澄港,例如HTTP椒涯,RTMP,或是MMS等等回梧。這些協(xié)議在傳輸視?頻數(shù)據(jù)的同時废岂,也會傳輸?些信令數(shù)據(jù)。這些信令數(shù)據(jù)包括對播放的控制(播放狱意,暫停湖苞,停?),或者對?絡(luò)狀態(tài)的描述等髓涯。解協(xié)議的過程中會去除掉信令數(shù)據(jù)?只保留視?頻數(shù)據(jù)袒啼。例如,采?RTMP協(xié)議傳輸?shù)臄?shù)據(jù)纬纪,經(jīng)過解協(xié)議操作后蚓再,輸出FLV格式的數(shù)據(jù)。
解封裝:
就是將輸?的封裝格式的數(shù)據(jù)包各,分離成為?頻流壓縮編碼數(shù)據(jù)和視頻流壓縮編碼數(shù)據(jù)摘仅。封裝格式種類很多,例如MP4问畅,MKV娃属,RMVB六荒,TS,F(xiàn)LV矾端,AVI等等掏击,它的作?就是將已經(jīng)壓縮編碼的視頻數(shù)據(jù)和?頻數(shù)據(jù)按照?定的格式放到?起。例如秩铆,F(xiàn)LV格式的數(shù)據(jù)砚亭,經(jīng)過解封裝操作后,輸出H.264編碼的視頻碼流和AAC編碼的?頻碼流殴玛。
解碼:
就是將視頻/?頻壓縮編碼數(shù)據(jù)捅膘,解碼成為?壓縮的視頻/?頻原始數(shù)據(jù)。?頻的壓縮編碼標準包含AAC滚粟,MP3寻仗,AC-3等等,視頻的壓縮編碼標準則包含H.264凡壤,MPEG2署尤,VC-1等等。解碼是整個系統(tǒng)中最重要也是最復(fù)雜的?個環(huán)節(jié)鲤遥。通過解碼沐寺,壓縮編碼的視頻數(shù)據(jù)輸出成為?壓縮的顏?數(shù)據(jù),例如YUV420P盖奈,RGB等等混坞;壓縮編碼的?頻數(shù)據(jù)輸出成為?壓縮的?頻抽樣數(shù)據(jù),例如PCM數(shù)據(jù)钢坦。
利用FFmpeg進行視頻解碼
ffmpeg視頻解碼.png
AVFormatContext:解封裝功能的結(jié)構(gòu)體究孕,包含文件名、音視頻流爹凹、時長厨诸、比特率等信息;
AVCodecContext:編解碼器上下文禾酱,編碼和解碼時必須用到的結(jié)構(gòu)體微酬,包含編解碼器類型、視頻寬高颤陶、音頻通道數(shù)和采樣率等信息颗管;
AVCodec:存儲編解碼器信息的結(jié)構(gòu)體;
AVStream:存儲音頻或視頻流信息的結(jié)構(gòu)體滓走;
AVPacket:存儲音頻或視頻編碼數(shù)據(jù)垦江;
AVFrame:存儲音頻或視頻解碼數(shù)據(jù)(原始數(shù)據(jù));
具體代碼實現(xiàn)
創(chuàng)建VideoDecoder類搅方,分別實現(xiàn)init比吭、start绽族、release等3個方法。
init為初始化衩藤,對一些重要的結(jié)構(gòu)體進行復(fù)制或者指向吧慢。
start為開始解碼循環(huán),讀取幀數(shù)據(jù)
release為釋放邏輯慷彤,在代碼結(jié)束時釋放娄蔼。
int videoDecoder::init(const char *url) {
strcpy(m_url, url);
int result = -1;
//1.創(chuàng)建封裝格式上下文
m_AVFormatContext = avformat_alloc_context();
//2.打開文件
if (avformat_open_input(&m_AVFormatContext, m_url, nullptr, nullptr) != 0) {
LOGE("avformat_open_input fail");
}
//3.獲取音視頻流信息
if (avformat_find_stream_info(m_AVFormatContext, nullptr) < 0) {
}
//4.獲取視頻流索引
m_StreamIndex = av_find_best_stream(m_AVFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (m_StreamIndex == -1) {
}
//5.獲取解碼器參數(shù)
AVCodecParameters *codecParameters = m_AVFormatContext->streams[m_StreamIndex]->codecpar;
//6.獲取解碼器
m_AVCodec = avcodec_find_decoder(codecParameters->codec_id);
if (m_AVCodec == nullptr) {
}
//7.創(chuàng)建解碼器上下文
m_AVCodecContext = avcodec_alloc_context3(m_AVCodec);
//將流的參數(shù)(codecParameters)復(fù)制到解碼器中,否則某些流可能無法正常解碼
if (avcodec_parameters_to_context(m_AVCodecContext, codecParameters) != 0) {
}
AVDictionary *pAVDictionary = nullptr;
av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);
//8.打開解碼器
result = avcodec_open2(m_AVCodecContext, m_AVCodec, &pAVDictionary);
if (result < 0) {
}
m_Duration = m_AVFormatContext->duration / AV_TIME_BASE * 1000;//us to ms
//創(chuàng)建 AVPacket 存放編碼數(shù)據(jù)
m_Packet = av_packet_alloc();
//創(chuàng)建 AVFrame 存放解碼后的數(shù)據(jù)
m_Frame = av_frame_alloc();
return result;
}
void videoDecoder::release() {
//釋放資源底哗,解碼完成
if(m_Frame != nullptr) {
av_frame_free(&m_Frame);
m_Frame = nullptr;
}
if(m_Packet != nullptr) {
av_packet_free(&m_Packet);
m_Packet = nullptr;
}
if(m_AVCodecContext != nullptr) {
avcodec_close(m_AVCodecContext);
avcodec_free_context(&m_AVCodecContext);
m_AVCodecContext = nullptr;
m_AVCodec = nullptr;
}
if(m_AVFormatContext != nullptr) {
avformat_close_input(&m_AVFormatContext);
avformat_free_context(m_AVFormatContext);
m_AVFormatContext = nullptr;
}
}
int videoDecoder::start() {
int result = av_read_frame(m_AVFormatContext, m_Packet);
while (result >= 0) {
//過濾數(shù)據(jù),找出對應(yīng)流锚沸,符合才進一步解碼
if (m_Packet->stream_index == m_StreamIndex) {
//解碼視頻跋选,將packet數(shù)據(jù)交給avcodec去處理
if (avcodec_send_packet(m_AVCodecContext,m_Packet)!=0){
return -1;
}
while (avcodec_receive_frame(m_AVCodecContext,m_Frame)==0){
//獲取到 m_Frame 解碼數(shù)據(jù),在這里進行格式轉(zhuǎn)換哗蜈,然后進行渲染并且顯示
LOGD("m_Frame pts is %ld",m_Frame->pts);
}
}
av_packet_unref(m_Packet);
result = av_read_frame(m_AVFormatContext, m_Packet);
}
}
測試代碼
checkPermission(object : RequestPermissionListener {
override fun permissionAllGranted() {
PlayLocalVideo("/sdcard/Download/v1080.mp4")
}
override fun permissionDenied(list: MutableList<String>) {
}
})
private external fun PlayLocalVideo(url: String)
videoDecoder *vDecoder = new videoDecoder();
const char *url = env->GetStringUTFChars(jUrl, nullptr);
vDecoder->init(url);
vDecoder->start();
vDecoder->release();
測試結(jié)果
image.png
首先在布局中增加一個surfaceView
<SurfaceView
android:id="@+id/surfaceVideo"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/sample_text" />
android代碼中綁定窗口和傳遞參數(shù)
binding.surfaceVideo.holder.addCallback(this)
override fun surfaceCreated(holder: SurfaceHolder) {
surface = holder.surface
PlayLocalVideo("/sdcard/Download/v1080.mp4", surface!!)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
接下來需要做的就是洞翩,如何把上面解碼得到的數(shù)據(jù)稽犁,刷到這個SurfaceView中。在實現(xiàn)之前骚亿,需要知道顯示的控件支持什么類型的數(shù)據(jù)已亥。ANativeWindow 僅支持 RGB 類型的圖像數(shù)據(jù),所以我們還需要利用 libswscale 庫將解碼后的 YUV 數(shù)據(jù)轉(zhuǎn)成 RGB 来屠。
首先在初始化的方法里虑椎,加入ANativeWindow的初始化。
//1. 利用 Java 層 SurfaceView 傳下來的 Surface 對象俱笛,獲取 ANativeWindow
m_NativeWindow = ANativeWindow_fromSurface(env, surface);
//顯示窗口初始化
ANativeWindow_setBuffersGeometry(m_NativeWindow, m_RenderWidth, m_RenderHeight,
WINDOW_FORMAT_RGBA_8888);
m_VideoWidth = m_AVCodecContext->width;
m_VideoHeight = m_AVCodecContext->height;
m_RGBAFrame = av_frame_alloc();
//計算 Buffer 的大小
int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_VideoWidth, m_VideoHeight, 1);
//為 m_RGBAFrame 分配空間
m_FrameBuffer = static_cast<uint8_t *>(av_malloc(bufferSize * sizeof(uint8_t)));
av_image_fill_arrays(m_RGBAFrame->data, m_RGBAFrame->linesize, m_FrameBuffer, AV_PIX_FMT_RGBA,
m_VideoWidth, m_VideoHeight, 1);
//獲取轉(zhuǎn)換的上下文
m_SwsContext = sws_getContext(m_VideoWidth, m_VideoHeight, m_AVCodecContext->pix_fmt,
m_VideoWidth, m_VideoHeight, AV_PIX_FMT_RGBA,
SWS_BICUBIC, nullptr, nullptr, nullptr);
然后在上面讀取幀數(shù)據(jù)的地方捆姜,加入轉(zhuǎn)換和顯示刷新的邏輯
sws_scale(m_SwsContext, m_Frame->data, m_Frame->linesize, 0,
m_VideoHeight, m_RGBAFrame->data, m_RGBAFrame->linesize);
if(m_NativeWindow == nullptr){
LOGE("NativeWindow is null");
return;
}
ANativeWindow_lock(m_NativeWindow, &m_NativeWindowBuffer, nullptr);
uint8_t *dstBuffer = static_cast<uint8_t *>(m_NativeWindowBuffer.bits);
int srcLineSize = m_RGBAFrame->linesize[0];//RGBA
int dstLineSize = m_NativeWindowBuffer.stride * 4;
for (int i = 0; i < m_VideoHeight; ++i) {
memcpy(dstBuffer + i * dstLineSize,
m_FrameBuffer + i * srcLineSize,
srcLineSize);
}
ANativeWindow_unlockAndPost(m_NativeWindow);
這樣就可以看到視頻的播放了。但由于是demo代碼嫂粟,很多細節(jié)都沒有處理娇未,例如實際開發(fā)的時候,需要在異線程中進行解碼星虹、還有適當?shù)卦黾覮og提示零抬、指針的處理等镊讼,避免產(chǎn)生bug。