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


ffplay是ffmpeg源碼中一個自帶的開源播放器實例,同時支持本地視頻文件的播放以及在線流媒體播放,功能非常強大。

FFplay: FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs.

ffplay中的代碼充分調用了ffmpeg中的函數庫,因此,想學習ffmpeg的使用,或基於ffmpeg開發一個自己的播放器,ffplay都是一個很好的切入點

由於ffmpeg本身的開發文檔比較少,且ffplay播放器源碼的實現相對復雜,除了基礎的ffmpeg組件調用外,還包含視頻幀的渲染、音頻幀的播放、音視頻同步策略及線程調度等問題。

因此,這里我們以ffmpeg官網推薦的一個ffplay播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視頻播放器的完整邏輯

在上一篇文章中介紹了如果將ffmpeg解碼出的視頻幀進行渲染顯示
本文在上篇文章的基礎上,討論如何將ffmpeg解碼出的音頻幀進行播放
視頻幀顯示見[公眾號:斷點實驗室]的前述文章 [ffmpeg播放器實現詳解 - 視頻顯示]。

公眾號:斷點實驗室 音視頻開發系列文章
ffmpeg源碼編譯環境搭建
ffplay源碼編譯
ffmpeg播放器實現詳解 - 框架搭建
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),其中,生成者線程負責從網絡接口或本地視頻文件中抽取數據,並向臨界區注入數據,這里的數據可以是解碼后的音視頻幀,或者音視頻編碼數據包,消費者線程負責從臨界區抽取數據,並對數據進行處理,例如對解碼后的視頻幀進行渲染,對音頻幀進行播放,或者是從隊列中提取音視頻編碼數據包並解碼。下圖為生產者-消費者線程同步模型示意圖。

圖中上面的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

2、音頻播放

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

視頻幀顯示見[公眾號:斷點實驗室]的前述文章 [ffmpeg播放器實現詳解 - 視頻顯示]。

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方式進行同步

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 (dranger@gmail.com).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// 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;
}

// 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視頻開發、圖像處理、網絡、
// Linux,Windows、Android、嵌入式開發等


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM