天天看點

ffmpeg播放器實作詳解 - 音頻播放

作者:音視訊流媒體技術

在上一篇文章中介紹了如果将ffmpeg解碼出的視訊幀進行渲染顯示

本文在上篇文章的基礎上,讨論如何将ffmpeg解碼出的音頻幀進行播放

在開始音頻播放問題正式讨論前,我們先引入一個經典的生産者-消費者線程同步模型,用于描述與音視訊幀隊列,或音視訊編碼資料包隊列相關線程的同步過程

1、生産者-消費者線程模型

本文主要讨論posix标準下的生産者-消費者線程模型,posix标準多用于類linux相關環境

POSIX: The Portable Operating System Interface (POSIX) is a family of standards specified by the IEEE Computer Society for maintaining compatibility between operating systems. POSIX defines the application programming interface (API), along with command line shells and utility interfaces, for software compatibility with variants of Unix and other operating systems

1.1 posix線程模型

生産者-消費者(producer-consumer)問題是一個經典的線程同步問題,它可以描述為兩個或者多個線程共同維護同一個臨界區資源(critical resource),其中,生成者線程負責從網絡接口或本地視訊檔案中抽取資料,并向臨界區注入資料,這裡的資料可以是解碼後的音視訊幀,或者音視訊編碼資料包,消費者線程負責從臨界區抽取資料,并對資料進行處理,例如對解碼後的視訊幀進行渲染,對音頻幀進行播放,或者是從隊列中提取音視訊編碼資料包并解碼。下圖為生産者-消費者線程同步模型示意圖。

ffmpeg播放器實作詳解 - 音頻播放

圖中上面的process_msg為消費者線程,下面的enqueue_msg為生産者線程,從左到右代表了線程執行的時間線。

我們先來看消費者線程,消費者線程率先擷取互斥鎖對象(圖中的紅點表示互斥鎖對象),獲得了對臨界區資源的獨占處理權及cpu資源的優先使用權,然後開始執行自己的線程函數。

在消費者的線程函數中,首先檢查臨界區資源是否滿足執行條件,如隊列是否已經存在待解碼的視訊編碼包,滿足執行條件,則從隊列中取出資料執行自己的邏輯。

如果臨界區資源不滿足執行條件,如隊列為空,此時,消費者線程通過在pthread_cond_wait中臨時釋放互斥鎖,并将自己投入休眠狀态,等待被生産者線程向臨界區注入資料,将自己喚醒并重新獲得互斥鎖,這時消費者線程會阻塞在pthread_cond_wait調用中

消費者線程通過在pthread_cond_wait中臨時釋放互斥鎖後,将自己投入休眠狀态,此時生成者線程将獲得互斥鎖,并獲得了對臨界區資源的獨占處理權及cpu資源的優先使用權,然後開始執行自己的線程函數。生成者線程向臨界區資源注入資料,如向隊列中注入待解碼的資料包,然後,通過pthread_cond_signal喚醒消費者線程(圖中虛線所示),随即通過unlock_mutex釋放互斥鎖。

在生成者線程釋放互斥鎖後,消費者線程已被喚醒,并重新擷取互斥鎖,再次檢查臨界區資源,如果滿足條件,則執行自己的線程函數,然後釋放互斥鎖,等待下一次執行。

這裡需要說明一下,在實際情況下,并不總是消費者線程優先獲得互斥鎖,這是由cpu排程決定的。

下面給出一些示例代碼來描述這個過程。

//1、消息隊列處理函數在處理消息前,先對互斥量進行鎖定,以保護消息隊列中的臨界區資源
//2、若消息隊列為空,則調用pthread_cond_wait對互斥量暫時解鎖,等待其他線程向消息隊列中插入消息資料
//3、待其他線程向消息隊列中插入消息資料後,通過pthread_cond_signal向等待線程發出qready信号
//4、消息隊列處理線程收到qready信号被喚醒,重新獲得對消息隊列臨界區資源的獨占

#include <pthread.h>

struct msg{//消息隊列結構體
    struct msg *m_next;//消息隊列後繼節點
    //more stuff here
}

struct msg *workq;//消息隊列指針
pthread_cond_t qready=PTHREAD_COND_INITIALIZER;//消息隊列就緒條件變量
pthread_mutex_t qlock=PTHREAS_MUTEX_INITIALIZER;//消息隊列互斥量,保護消息隊列資料

//消息隊列處理函數
void process_msg(void){
    struct msg *mp;//消息結構指針
    for(;;){
        pthread_mutex_lock(&qlock);//消息隊列互斥量加鎖,保護消息隊列資料
        while(workq==NULL){//檢查消息隊列是否為空,若為空
            pthread_cond_wait(&qready,&qlock);//等待消息隊列就緒信号qready,并對互斥量暫時解鎖,該函數傳回時,互斥量再次被鎖住
        }
        mp=workq;//線程醒來,從消息隊列中取資料準備處理
        workq=mp->m_next;//更新消息隊列,指針後移清除取出的消息
        pthread_mutex_unlock(&qlock);//釋放鎖
        //now process the message mp
    }
}

//将消息插入消息隊列
void enqueue_msg(struct msg *mp){
    pthread_mutex_lock(&qlock);//消息隊列互斥量加鎖,保護消息隊列資料
    mp->m_next=workq;//将原隊列頭作為插入消息的後繼節點
    workq=mp;//将新消息插入隊列
    pthread_cond_signal(&qready);//給等待線程發出qready消息,通知消息隊列已就緒
    pthread_mutex_unlock(&qlock);//釋放鎖
}            

1.2 SDL線程模型

本文例程中出現的sdl線程模型,它的使用方法與posix線程模型完全相同,可以看作sdl庫對pthread線程元件的封裝,其中,SDL_cond可以看作是sdl對pthread_cond_t的封裝,其他sdl線程元件對應同名的pthread線程元件

  • pthread_mutex_t - SDL_mutex
  • pthread_cond_t - SDL_cond
  • pthread_mutex_lock - SDL_LockMutex
  • pthread_mutex_unlock - SDL_UnlockMutex
  • pthread_cond_wait - SDL_CondWait
  • pthread_cond_signal - SDL_CondSignal

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

ffmpeg播放器實作詳解 - 音頻播放

2、音頻播放

在上篇文章中我們讨論了如何對視訊幀進行渲染顯示,雖然畫面已經有了,但還缺少聲音,本文在上篇文章的基礎上,繼續完善我們的播放器開發,讨論如何播放聲音。

2.1 音頻播放前的準備

在音頻幀播放前,首先要有一個存儲音頻編碼資料包的緩存隊列PacketQueue,用于儲存從網絡接口或本地視訊檔案中抽取的編碼資料,

packet_queue_put負責向緩存隊列中填充編碼資料包,packet_queue_get負責從隊列中提取資料包,

packet_queue_put與packet_queue_get之間構成了生産者與消費者關系,生産者首先檢查緩存隊列是否有足夠的空間,若隊列存在剩餘空間,則向隊列注入資料,然後發送信号喚醒消費者線程,若隊列滿則生産者線程進入休眠

消費者檢查緩存隊列狀态,若隊列為空則進入休眠模式,若隊列滿則從隊列中抽取音視訊編碼資料包交給解碼器處理,當隊列為空時向生産者發送信号請求資料,同時自己進入休眠狀态

例程中生産者-消費者工作原理與1.1節内容完全相同

2.2 音頻輸出回調函數

sdl庫通過SDL_OpenAudio打開音頻裝置,并建立音頻處理背景線程,sdl背景線程通過audio_callback回調函數将解碼後的pcm資料送入聲霸卡播放。

sdl通常一次會準備一組緩存pcm資料,通過該回調送入聲霸卡,聲霸卡根據音頻pts依次播放pcm資料,待送入緩存的pcm資料完成播放後,再載入一組新的pcm緩存資料(每次音頻輸出緩存為空時,sdl就調用此函數填充音頻輸出緩存,并送入聲霸卡播放)

2.3 音頻播放參數設定

通過建立SDL_AudioSpec結構體,設定音頻播放參數

// Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format
    // 建立SDL_AudioSpec結構體,設定音頻播放參數
    wanted_spec.freq = aCodecCtx->sample_rate;//采樣頻率 DSP frequency -- samples per second
    wanted_spec.format = AUDIO_S16SYS;//采樣格式 Audio data format
    wanted_spec.channels = aCodecCtx->channels;//聲道數 Number of channels: 1 mono, 2 stereo
    wanted_spec.silence = 0;//無輸出時是否靜音
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;//預設每次讀音頻緩存的大小,推薦值為 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames
    wanted_spec.callback = audio_callback;//設定取音頻資料的回調接口函數 the function to call when the audio device needs more data
    wanted_spec.userdata = aCodecCtx;//傳遞使用者資料           

2.4 流程圖

例程的整體工作流程如下圖所示。其中,avcodec_decode_audio4函數用于音頻資料的解碼,解碼操作結束後,會再次進入到audio_callback回調函數的邏輯中,audio_callback回調函數工作在sdl背景線程中,流程圖中從avcodec_decode_audio4到audio_callback的連線表示該過程會反複執行。

packet_queue_put與packet_queue_get之前共同維護PacketQueue緩存隊列,二者SDL_cond方式進行同步

ffmpeg播放器實作詳解 - 音頻播放

3、源碼編譯驗證

源碼的編譯方法和之前的例程完全相同,源碼可采用如下Makefile腳本進行編譯

tutorial03: tutorial03.c
    gcc -o tutorial03 -g3 tutorial03.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE}  \
    -L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
    `sdl-config --cflags --libs`

clean:
    rm -rf tutorial03           

執行make指令開始編譯,編譯完成後,可在源碼目錄生成名為[tutorial03]的可執行檔案。

與ffplay的使用方法類似,執行[tutorial03 url]指令,除了有畫面顯示外可以聽到有聲音的輸出,但此時的聲音播放功能還無法正常工作,别着急,後面的内容會在此基礎上繼續完善,直到最終實作一個能夠正常播放視訊的播放器為止

./tutorial03 rtmp://58.200.131.2:1935/livetv/hunantv           

輸入Ctrl+C結束程式運作

4、源碼清單

源碼在上篇的内容基礎上,主要增加音頻緩存隊列處理,音頻解碼,音頻播放等幾個部分,源碼幾乎每行都有注釋,友善大家調試了解

// tutorial03.c
// A pedagogical video player that will stream through every video frame as fast as it can
// and play audio (out of sync).
//
// This tutorial was written by Stephen Dranger ([email protected]).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme ([email protected])
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
//
// Updates tested on:
// Mac OS X 10.11.6
// Apple LLVM version 8.0.0 (clang-800.0.38)
//
// Use 
//
// $ gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale are correctly installed your system).
//
// Run using
//
// $ tutorial03 myvideofile.mpg
//
// to play the stream on your screen with voice.

/*---------------------------
//1、消息隊列處理函數在處理消息前,先對互斥量進行鎖定,以保護消息隊列中的臨界區資源
//2、若消息隊列為空,則調用pthread_cond_wait對互斥量暫時解鎖,等待其他線程向消息隊列中插入消息資料
//3、待其他線程向消息隊列中插入消息資料後,通過pthread_cond_signal像等待線程發出qready信号
//4、消息隊列處理線程收到qready信号被喚醒,重新獲得對消息隊列臨界區資源的獨占

#include <pthread.h>

struct msg{//消息隊列結構體
    struct msg *m_next;//消息隊列後繼節點
    //more stuff here
}

struct msg *workq;//消息隊列指針
pthread_cond_t qready=PTHREAD_COND_INITIALIZER;//消息隊列就緒條件變量
pthread_mutex_t qlock=PTHREAS_MUTEX_INITIALIZER;//消息隊列互斥量,保護消息隊列資料

//消息隊列處理函數
void process_msg(void){
    struct msg *mp;//消息結構指針
    for(;;){
        pthread_mutex_lock(&qlock);//消息隊列互斥量加鎖,保護消息隊列資料
        while(workq==NULL){//檢查消息隊列是否為空,若為空
            pthread_cond_wait(&qready,&qlock);//等待消息隊列就緒信号qready,并對互斥量暫時解鎖,該函數傳回時,互斥量再次被鎖住
        }
        mp=workq;//線程醒來,從消息隊列中取資料準備處理
        workq=mp->m_next;//更新消息隊列,指針後移清除取出的消息
        pthread_mutex_unlock(&qlock);//釋放鎖
        //now process the message mp
    }
}

//将消息插入消息隊列
void enqueue_msg(struct msg *mp){
    pthread_mutex_lock(&qlock);//消息隊列互斥量加鎖,保護消息隊列資料
    mp->m_next=workq;//将原隊列頭作為插入消息的後繼節點
    workq=mp;//将新消息插入隊列
    pthread_cond_signal(&qready);//給等待線程發出qready消息,通知消息隊列已就緒
    pthread_mutex_unlock(&qlock);//釋放鎖
} 
---------------------------*/

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main // Prevents SDL from overriding main().
#endif

#include <stdio.h>

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

int quit = 0;//全局退出程序辨別,在界面上點了退出後,告訴線程退出
/*-------連結清單節點結構體--------
typedef struct AVPacketList {
    AVPacket pkt;//連結清單資料
    struct AVPacketList *next;//連結清單後繼節點
} AVPacketList;
---------------------------*/
//資料包隊列(連結清單)結構體
typedef struct PacketQueue {
    AVPacketList *first_pkt, *last_pkt;//隊列首尾節點指針
    int nb_packets;//隊列長度
    int size;//儲存編碼資料的緩存長度,size=packet->size
    SDL_mutex *qlock;//隊列互斥量,保護隊列資料
    SDL_cond *qready;//隊列就緒條件變量
} PacketQueue;
PacketQueue audioq;//定義全局隊列對象

//隊列初始化函數
void packet_queue_init(PacketQueue *q) {
    memset(q, 0, sizeof(PacketQueue));//全零初始化隊列結構體對象
    q->qlock = SDL_CreateMutex();//建立互斥量對象
    q->qready = SDL_CreateCond();//建立條件變量對象
}

//向隊列中插入資料包
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
/*-------準備隊列(連結清單)節點對象------*/
    AVPacketList *pktlist;//建立連結清單節點對象指針
    pktlist = av_malloc(sizeof(AVPacketList));//在堆上建立連結清單節點對象
    if (!pktlist) {//檢查連結清單節點對象是否建立成功
        return -1;
    }
    pktlist->pkt = *pkt;//将輸入資料包指派給建立連結清單節點對象中的資料包對象
    pktlist->next = NULL;//連結清單後繼指針為空
//  if (av_packet_ref(pkt, pkt)<0) {//增加pkt編碼資料的引用計數(輸入參數中的pkt與建立連結清單節點中的pkt共享同一緩存空間)
//      return -1;
//  }
/*---------将建立節點插入隊列-------*/
    SDL_LockMutex(q->qlock);//隊列互斥量加鎖,保護隊列資料
    
    if (!q->last_pkt) {//檢查隊列尾節點是否存在(檢查隊列是否為空)
        q->first_pkt = pktlist;//若不存在(隊列尾空),則将目前節點作隊列為首節點
    }
    else {
        q->last_pkt->next = pktlist;//若已存在尾節點,則将目前節點挂到尾節點的後繼指針上,并作為新的尾節點
    }
    q->last_pkt = pktlist;//将目前節點作為新的尾節點
    q->nb_packets++;//隊列長度+1
    q->size += pktlist->pkt.size;//更新隊列編碼資料的緩存長度
    
    SDL_CondSignal(q->qready);//給等待線程發出消息,通知隊列已就緒
    
    SDL_UnlockMutex(q->qlock);//釋放互斥量
    return 0;
}

//從隊列中提取資料包,并将提取的資料包出隊列
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
    AVPacketList *pktlist;//臨時連結清單節點對象指針
    int ret;//操作結果
    
    SDL_LockMutex(q->qlock);//隊列互斥量加鎖,保護隊列資料
    for (;;) {
        if (quit) {//檢查退出程序辨別
            ret = -1;//操作失敗
            break;
        }
        
        pktlist = q->first_pkt;//傳遞将隊列首個資料包指針
        if (pktlist) {//檢查資料包是否為空(隊列是否有資料)
            q->first_pkt = pktlist->next;//隊列首節點指針後移
            if (!q->first_pkt) {//檢查首節點的後繼節點是否存在
                q->last_pkt = NULL;//若不存在,則将尾節點指針置空
            }
            q->nb_packets--;//隊列長度-1
            q->size -= pktlist->pkt.size;//更新隊列編碼資料的緩存長度
            *pkt = pktlist->pkt;//将隊列首節點資料傳回
            av_free(pktlist);//清空臨時節點資料(清空首節點資料,首節點出隊列)
            ret = 1;//操作成功
            break;
        } else if (!block) {
            ret = 0;
            break;
        } else {//隊列處于未就緒狀态,此時通過SDL_CondWait函數等待qready就緒信号,并暫時對互斥量解鎖
            /*---------------------
             * 等待隊列就緒信号qready,并對互斥量暫時解鎖
             * 此時線程處于阻塞狀态,并置于等待條件就緒的線程清單上
             * 使得該線程隻在臨界區資源就緒後才被喚醒,而不至于線程被頻繁切換
             * 該函數傳回時,互斥量再次被鎖住,并執行後續操作
             --------------------*/
            SDL_CondWait(q->qready, q->qlock);//暫時解鎖互斥量并将自己阻塞,等待臨界區資源就緒(等待SDL_CondSignal發出臨界區資源就緒的信号)
        }
    }//end for for-loop
    SDL_UnlockMutex(q->qlock);//釋放互斥量
    return ret;
}

/*---------------------------
 * 從緩存隊列中提取資料包、解碼,并傳回解碼後的資料長度(對一個完整的packet解碼,将解碼資料寫入audio_buf緩存,并傳回多幀解碼資料的總長度)
 * aCodecCtx:音頻解碼器上下文
 * audio_buf:儲存解碼一個完整的packe後的原始音頻資料(緩存中可能包含多幀解碼後的音頻資料)
 * buf_size:解碼後的音頻資料長度,未使用
 --------------------------*/
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {
    static AVPacket pkt;//儲存從隊列中提取的資料包
    static AVFrame frame;//儲存從資料包中解碼的音頻資料
    static uint8_t *audio_pkt_data = NULL;//儲存資料包編碼資料緩存指針
    static int audio_pkt_size = 0;//資料包中剩餘的編碼資料長度
    int coded_consumed_size, data_size = 0;//每次消耗的編碼資料長度[input](len1),輸出原始音頻資料的緩存長度[output]
    
    for (;;) {
        while(audio_pkt_size>0) {//檢查緩存中剩餘的編碼資料長度(是否已完成一個完整的pakcet包的解碼,一個資料包中可能包含多個音頻編碼幀)
            int got_frame = 0;//解碼操作成功辨別,成功傳回非零值
            coded_consumed_size=avcodec_decode_audio4(aCodecCtx,&frame,&got_frame,&pkt);//解碼一幀音頻資料,并傳回消耗的編碼資料長度
            if (coded_consumed_size < 0) {//檢查是否執行了解碼操作
                // if error, skip frame.
                audio_pkt_size = 0;//更新編碼資料緩存長度
                break;
            }
            audio_pkt_data += coded_consumed_size;//更新編碼資料緩存指針位置
            audio_pkt_size -= coded_consumed_size;//更新緩存中剩餘的編碼資料長度
            if (got_frame) {//檢查解碼操作是否成功
                //計算解碼後音頻資料長度[output]
                data_size=av_samples_get_buffer_size(NULL,aCodecCtx->channels,frame.nb_samples,aCodecCtx->sample_fmt,1);
                memcpy(audio_buf, frame.data[0], data_size);//将解碼資料複制到輸出緩存
            }
            if (data_size <= 0) {//檢查輸出解碼資料緩存長度
                // No data yet, get more frames.
                continue;
            }
            // We have data, return it and come back for more later.
            return data_size;//傳回解碼資料緩存長度
        }//end for while

        if (pkt.data) {//檢查資料包是否已從隊列中提取
            av_packet_unref(&pkt);//釋放pkt中儲存的編碼資料
        }
        
        if (quit) {//檢查退出程序辨別
            return -1;
        }
        //從隊列中提取資料包到pkt
        if (packet_queue_get(&audioq, &pkt,1)<0) {
            return -1;
        }
        audio_pkt_data = pkt.data;//傳遞編碼資料緩存指針
        audio_pkt_size = pkt.size;//傳遞編碼資料緩存長度
    }//end for for-loop
}

/*------Audio Callback-------
 * 音頻輸出回調函數,sdl通過該回調函數将解碼後的pcm資料送入聲霸卡播放,
 * sdl通常一次會準備一組緩存pcm資料,通過該回調送入聲霸卡,聲霸卡根據音頻pts依次播放pcm資料
 * 待送入緩存的pcm資料完成播放後,再載入一組新的pcm緩存資料(每次音頻輸出緩存為空時,sdl就調用此函數填充音頻輸出緩存,并送入聲霸卡播放)
 * When we begin playing audio, SDL will continually call this callback function 
 * and ask it to fill the audio buffer with a certain number of bytes
 * The audio function callback takes the following parameters: 
 * stream: A pointer to the audio buffer to be filled,輸出音頻資料到聲霸卡緩存
 * len: The length (in bytes) of the audio buffer,緩存長度wanted_spec.samples=SDL_AUDIO_BUFFER_SIZE(1024)
 --------------------------*/ 
void audio_callback(void *userdata, Uint8 *stream, int len) {
    AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;//傳遞使用者資料
    int wt_stream_len, audio_size;//每次寫入stream的資料長度,解碼後的資料長度
    
    static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];//儲存解碼一個packet後的多幀原始音頻資料
    static unsigned int audio_buf_size = 0;//解碼後的多幀音頻資料長度
    static unsigned int audio_buf_index = 0;//累計寫入stream的長度
    
    while (len>0) {//檢查音頻緩存的剩餘長度
        if (audio_buf_index >= audio_buf_size) {//檢查是否需要執行解碼操作
            // We have already sent all our data; get more,從緩存隊列中提取資料包、解碼,并傳回解碼後的資料長度,audio_buf緩存中可能包含多幀解碼後的音頻資料
            audio_size = audio_decode_frame(aCodecCtx, audio_buf, audio_buf_size);
            if (audio_size < 0) {//檢查解碼操作是否成功
                // If error, output silence.
                audio_buf_size = 1024; // arbitrary?
                memset(audio_buf, 0, audio_buf_size);//全零重置緩沖區
            } else {
                audio_buf_size = audio_size;//傳回packet中包含的原始音頻資料長度(多幀)
            }
            audio_buf_index = 0;//初始化累計寫入緩存長度
        }//end for if

        wt_stream_len = audio_buf_size-audio_buf_index;//計算解碼緩存剩餘長度
        if (wt_stream_len > len) {//檢查每次寫入緩存的資料長度是否超過指定長度(1024)
            wt_stream_len = len;//指定長度從解碼的緩存中取資料
        }
        //每次從解碼的緩存資料中以指定長度抽取資料并寫入stream傳遞給聲霸卡
        memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,wt_stream_len);
        len -= wt_stream_len;//更新解碼音頻緩存的剩餘長度
        stream += wt_stream_len;//更新緩存寫入位置
        audio_buf_index += wt_stream_len;//更新累計寫入緩存資料長度
    }//end for while
}

int main(int argc, char *argv[]) {
/*--------------參數定義-------------*/
    AVFormatContext *pFormatCtx = NULL;//儲存檔案容器封裝資訊及碼流參數的結構體
    AVCodecContext *vCodecCtx = NULL;//視訊解碼器上下文對象,解碼器依賴的相關環境、狀态、資源以及參數集的接口指針
    AVCodecContext *aCodecCtx = NULL;//音頻解碼器上下文對象,解碼器依賴的相關環境、狀态、資源以及參數集的接口指針
    AVCodec *vCodec = NULL;//儲存視訊編解碼器資訊的結構體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量
    AVCodec *aCodec = NULL;//儲存音頻編解碼器資訊的結構體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量
    AVPacket packet;//負責儲存壓縮編碼資料相關資訊的結構體,每幀圖像由一到多個packet包組成
    AVFrame *pFrame = NULL;//儲存音視訊解碼後的資料,如狀态資訊、編解碼器資訊、宏塊類型表,QP表,運動矢量表等資料
    struct SwsContext *sws_ctx = NULL;//描述轉換器參數的結構體
    AVDictionary *videoOptionsDict = NULL;
    AVDictionary *audioOptionsDict = NULL;

    SDL_Surface *screen = NULL;//SDL繪圖表面,A structure that contains a collection of pixels used in software blitting
    SDL_Overlay *bmp = NULL;//SDL畫布
    SDL_Rect rect;//SDL矩形對象
    SDL_AudioSpec wanted_spec, spec;//SDL_AudioSpec a structure that contains the audio output format,建立 SDL_AudioSpec 結構體,設定音頻播放資料
    SDL_Event event;//SDL事件對象

    int i, videoStream, audioStream;//循環變量,音視訊流類型标号
    int frameFinished;//解碼操作是否成功辨別

/*-------------參數初始化------------*/  
    if (argc<2) {//檢查輸入參數個數是否正确
        fprintf(stderr, "Usage: test <file>\n");
        exit(1);
    }
    // Register all formats and codecs,注冊所有多媒體格式及編解碼器
    av_register_all();
    
    // Open video file,打開視訊檔案,取得檔案容器的封裝資訊及碼流參數
    if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
        return -1; // Couldn't open file.
    }
    
    // Retrieve stream information,取得檔案中儲存的碼流資訊
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        return -1; // Couldn't find stream information.
    }
    
    // Dump information about file onto standard error,列印pFormatCtx中的碼流資訊
    av_dump_format(pFormatCtx, 0, argv[1], 0);
    
    // Find the first video stream.
    videoStream = -1;//視訊流類型标号初始化為-1
    audioStream = -1;//音頻流類型标号初始化為-1
    for (i = 0; i < pFormatCtx->nb_streams; i++) {//周遊檔案中包含的所有流媒體類型(視訊流、音頻流、字幕流等)
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && videoStream < 0) {//若檔案中包含有視訊流
            videoStream = i;//用視訊流類型的标号修改辨別,使之不為-1
        }
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audioStream < 0) {//若檔案中包含有音頻流
            audioStream = i;//用音頻流類型的标号修改辨別,使之不為-1
        }
    }
    if (videoStream == -1) {//檢查檔案中是否存在視訊流
        return -1; // Didn't find a video stream.
    }
    if (audioStream == -1) {//檢查檔案中是否存在音頻流
        return -1;
    }
    
    // Get a pointer to the codec context for the video stream,根據流類型标号從pFormatCtx->streams中取得視訊流對應的解碼器上下文
    vCodecCtx = pFormatCtx->streams[videoStream]->codec;
    /*-----------------------
     * Find the decoder for the video stream,根據視訊流對應的解碼器上下文查找對應的解碼器,傳回對應的解碼器(資訊結構體)
     * The stream's information about the codec is in what we call the "codec context.
     * This contains all the information about the codec that the stream is using
     -----------------------*/
    vCodec = avcodec_find_decoder(vCodecCtx->codec_id);
    if (vCodec == NULL) {//檢查解碼器是否比對
        fprintf(stderr, "Unsupported codec!\n");
        return -1; // Codec not found.
    }
    if (avcodec_open2(vCodecCtx, vCodec, &videoOptionsDict) < 0)// Open codec,打開視訊解碼器
        return -1; // Could not open codec.

    // Get a pointer to the codec context for the video stream,根據流類型标号從pFormatCtx->streams中取得音頻流對應的解碼器上下文
    aCodecCtx = pFormatCtx->streams[audioStream]->codec;
    // Find the decoder for the video stream,根據視訊流對應的解碼器上下文查找對應的解碼器,傳回對應的解碼器(資訊結構體)
    aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
    if (!aCodec) {//檢查解碼器是否比對
        fprintf(stderr, "Unsupported codec!\n");
        return -1;
    }
    avcodec_open2(aCodecCtx, aCodec, &audioOptionsDict);// Open codec,打開音頻解碼器

    // Allocate video frame,為解碼後的視訊資訊結構體配置設定空間并完成初始化操作(結構體中的圖像緩存按照下面兩步手動安裝)
    pFrame = av_frame_alloc();
    // Initialize SWS context for software scaling,設定圖像轉換像素格式為AV_PIX_FMT_YUV420P
    sws_ctx = sws_getContext(vCodecCtx->width, vCodecCtx->height, vCodecCtx->pix_fmt, vCodecCtx->width, vCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);

    packet_queue_init(&audioq);//緩存隊列初始化

    //SDL_Init initialize the Event Handling, File I/O, and Threading subsystems,初始化SDL 
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {//initialize the video audio & timer subsystem 
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        exit(1);
    }
    // Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已經棄用,改為SDL_CreateWindow及SDL_CreateRenderer建立視窗及着色器
#ifndef __DARWIN__
    screen = SDL_SetVideoMode(vCodecCtx->width, vCodecCtx->height, 0, 0);//建立SDL視窗及繪圖表面,并指定圖像尺寸及像素個數
#else
    screen = SDL_SetVideoMode(vCodecCtx->width, vCodecCtx->height, 24, 0);//建立SDL視窗及繪圖表面,并指定圖像尺寸及像素個數
#endif
    if (!screen) {//檢查SDL(繪圖表面)視窗是否建立成功(SDL用繪圖表面對象操作視窗)
        fprintf(stderr, "SDL: could not set video mode - exiting\n");
        exit(1);
    }
    SDL_WM_SetCaption(argv[1],0);//用輸入檔案名設定SDL視窗标題

    // Allocate a place to put our YUV image on that screen,建立畫布對象
    bmp = SDL_CreateYUVOverlay(vCodecCtx->width, vCodecCtx->height, SDL_YV12_OVERLAY, screen);

    // Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format
    // 建立SDL_AudioSpec結構體,設定音頻播放參數
    wanted_spec.freq = aCodecCtx->sample_rate;//采樣頻率 DSP frequency -- samples per second
    wanted_spec.format = AUDIO_S16SYS;//采樣格式 Audio data format
    wanted_spec.channels = aCodecCtx->channels;//聲道數 Number of channels: 1 mono, 2 stereo
    wanted_spec.silence = 0;//無輸出時是否靜音
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;//預設每次讀音頻緩存的大小,推薦值為 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames
    wanted_spec.callback = audio_callback;//設定取音頻資料的回調接口函數 the function to call when the audio device needs more data
    wanted_spec.userdata = aCodecCtx;//傳遞使用者資料
    
   /*---------------------------
    * 以指定參數打開音頻裝置,并傳回與指定參數最為接近的參數,該參數為裝置實際支援的音頻參數
    * Opens the audio device with the desired parameters(wanted_spec)
    * return another specs we actually be using 
    * and not guaranteed to get what we asked for
    --------------------------*/ 
    if (SDL_OpenAudio(&wanted_spec, &spec)<0) {
        fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
        return -1;
    }
    SDL_PauseAudio(0);//audio callback starts running again,開啟音頻裝置,如果這時候沒有獲得資料那麼它就靜音
/*--------------循環解碼-------------*/ 
    i = 0;// Read frames and save first five frames to disk.
    /*-----------------------
     * read in a packet and store it in the AVPacket struct
     * ffmpeg allocates the internal data for us,which is pointed to by packet.data
     * this is freed by the av_free_packet()
     -----------------------*/
    while (av_read_frame(pFormatCtx, &packet) >= 0) {//從檔案中依次讀取每個圖像編碼資料包,并存儲在AVPacket資料結構中
        // Is this a packet from the video stream,檢查資料包類型
        if (packet.stream_index == videoStream) {//檢查視訊媒體流類型辨別
           /*-----------------------
            * Decode video frame,解碼完整的一幀資料,并将frameFinished設定為true
            * 可能無法通過隻解碼一個packet就獲得一個完整的視訊幀frame,可能需要讀取多個packet才行
            * avcodec_decode_video2()會在解碼到完整的一幀時設定frameFinished為真
            * Technically a packet can contain partial frames or other bits of data
            * ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
            * convert the packet to a frame for us and set frameFinisned for us when we have the next frame
            -----------------------*/
            avcodec_decode_video2(vCodecCtx, pFrame, &frameFinished, &packet);
            
            // Did we get a video frame,檢查是否解碼出完整一幀圖像
            if (frameFinished) {
                SDL_LockYUVOverlay(bmp);//locks the overlay for direct access to pixel data,原子操作,保護像素緩沖區,避免非法修改
                
                AVFrame pict;//儲存轉換為AV_PIX_FMT_YUV420P格式的視訊幀
                pict.data[0] = bmp->pixels[0];//将轉碼後的圖像與畫布的像素緩沖器關聯
                pict.data[1] = bmp->pixels[2];
                pict.data[2] = bmp->pixels[1];
                
                pict.linesize[0] = bmp->pitches[0];//将轉碼後的圖像掃描行長度與畫布像素緩沖區的掃描行長度相關聯
                pict.linesize[1] = bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane
                pict.linesize[2] = bmp->pitches[1];;//For audio, only linesize[0] may be set
                
                // Convert the image into YUV format that SDL uses,将解碼後的圖像轉換為AV_PIX_FMT_YUV420P格式,并指派到pict對象
                sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data, pFrame->linesize, 0, vCodecCtx->height, pict.data, pict.linesize);
                
                SDL_UnlockYUVOverlay(bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
                //設定矩形顯示區域
                rect.x = 0;
                rect.y = 0;
                rect.w = vCodecCtx->width;
                rect.h = vCodecCtx->height;
                SDL_DisplayYUVOverlay(bmp, &rect);//圖像渲染
                av_packet_unref(&packet);//Free the packet that was allocated by av_read_frame,釋放AVPacket資料結構中編碼資料指針
            }
        } else if (packet.stream_index == audioStream) {//檢查音頻媒體流類型辨別
            packet_queue_put(&audioq, &packet);//向緩存隊列中填充編碼資料包
        } else {//字幕流類型辨別
            //Free the packet that was allocated by av_read_frame,釋放AVPacket資料結構中編碼資料指針
            av_packet_unref(&packet);
        }

       /*-------------------------
        * 在每次循環中從SDL背景隊列取事件并填充到SDL_Event對象中
        * SDL的事件系統使得你可以接收使用者的輸入,進而完成一些控制操作
        * SDL_PollEvent() is the favored way of receiving system events 
        * since it can be done from the main loop and does not suspend the main loop
        * while waiting on an event to be posted
        * poll for events right after we finish processing a packet
        ------------------------*/
        SDL_PollEvent(&event);
        switch (event.type) {//檢查SDL事件對象
            case SDL_QUIT://退出事件
                quit = 1;//退出程序辨別置1
                SDL_Quit();//退出操作
                exit(0);//結束程序
                break;
            default:
                break;
        }//end for switch
    }//end for while
/*--------------參數撤銷-------------*/     
    // Free the YUV frame.
    av_free(pFrame);
    
    // Close the codec.
    avcodec_close(vCodecCtx);
    
    // Close the video file.
    avformat_close_input(&pFormatCtx);
    
    return 0;
}           

原文 https://blog.csdn.net/m0_48578207/article/details/107571487