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播放器簡化版本的開發例程為基礎,在此基礎上循序漸進由淺入深,最終探討實現一個視頻播放器的完整邏輯。
在前幾篇文章中,我們討論了與音視頻同步相關的時間戳概念,如通過video_clock & audio_clock 追蹤當前播放進度的時間戳,在音頻解碼時如何通過插值與丟幀與主時鍾同步,以及如何動態預測下一幀的延遲刷新顯示時間來同步視頻播放等內容。
到目前為止,一個視頻播放器的主要技術原理及功能實現已經介紹完了。在本系列文章的最后,我們將討論如何實現播放器的[快進]/[快退]功能。
公眾號:斷點實驗室 音視頻開發系列文章
ffmpeg源碼編譯環境搭建
ffplay源碼編譯
ffmpeg播放器實現詳解 - 框架搭建
ffmpeg播放器實現詳解 - 視頻顯示
ffmpeg播放器實現詳解 - 音頻播放
ffmpeg播放器實現詳解 - 創建線程
ffmpeg播放器實現詳解 - 視頻同步控制
ffmpeg播放器實現詳解 - 音頻同步控制
ffmpeg播放器實現詳解 - 快進快退控制
1、快進快退事件處理
先來看下[快進]/[快退]事件的觸發與處理機制,這里我們仍然是通過異步事件機制實現[快進]/[快退]控制的。
首先通過上下左右4個方向鍵觸發[快進]/[快退]事件,其中,左右鍵設定[快進]/[快退]10s,上下鍵設定[快進]/[快退]60s。
然后在main函數的事件循環處理邏輯中,通過sdl來監聽捕獲每個按鍵對應的消息,接着通過goto跳轉到do_seek執行具體的事件處理邏輯。
int main(int argc, char *argv[]) {
...
for (;;) {
double incr, pos;
SDL_WaitEvent(&event);//Use this function to wait indefinitely for the next available event,主線程阻塞,等待事件到來
switch (event.type) {//事件到來后喚醒主線程后,檢查事件類型,執行相應操作
case SDL_KEYDOWN://檢查鍵盤操作事件
switch (event.key.keysym.sym) {//檢查鍵盤操作類型,which key get hit
case SDLK_LEFT://[左鍵]
incr = -10.0;//后退10s
goto do_seek;
case SDLK_RIGHT://[右鍵]
incr = 10.0;//快進10s
goto do_seek;
case SDLK_UP://[上鍵]
incr = 60.0;//快進60s
goto do_seek;
case SDLK_DOWN://[下鍵]
incr = -60.0;//后退60s
goto do_seek;
有了[快進]/[快退]事件的監聽,下面我們來看下對具體事件的處理過程。
我們先在VideoState全局狀態參數集中增加幾個字段,用於追蹤與[快進]/[快退]相關的狀態,其他組件在自己的邏輯中判斷這些狀態值,並執行相應的動作。
typedef struct VideoState {
...
int seek_req;//[快進]/[后退]操作開啟標志位
int seek_flags;//[快進]/[后退]操作類型標志位
int64_t seek_pos;//[快進]/[后退]操作后的參考時間戳
...
}VideoState;
接着我們來看事件處理邏輯。
int main(int argc, char *argv[]) {
...
do_seek://處理請求
if (global_video_state) {
pos = get_master_clock(global_video_state);//取得當前主同步源時間戳
pos += incr;//根據鍵盤操作更新主同步源時間戳(AV_TIME_BASE為時間戳基准值)
stream_seek(global_video_state,(int64_t)(pos*AV_TIME_BASE),incr);//根據主同步源時間戳設置查找位置
}
break;
在do_seek中處理按鍵事件。
- 首先通過getMasterClock獲取當前主同步源時鍾。
- 接着將[快進]/[快退]的時差值更新到主同步源時種上,作為新的同步目標時間。
- 最后我們在stream_seek中更新[快進]/[快退]的全局狀態信息。
//設置[快進]/[快退]狀態參數
void stream_seek(VideoState *is, int64_t pos, int rel) {
if (!is->seek_req) {//檢查[快進]/[后退]操作標志位是否開啟
is->seek_pos = pos;//更新[快進]/[后退]后的參考時間戳
is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0;//確定[快進]還是[后退]操作
is->seek_req = 1;//開啟[快進]/[后退]標志位
}
}
這幾個全局狀態參數在stream_seek中更新后,其他組件就可以根據這些狀態信息,在自己的邏輯中執行相應的動作了。
2、快進快退功能實現
我們在stream_seek中更新了[快進]/[快退]的全局狀態參數,在編碼數據包解析線程函數parse_thread中判斷[快進]/[快退]操作標志位是否開啟,當判斷有[快進]/[快退]請求時,執行下列3個動作
- 首先進行時間單位轉換,將seek_target的單位由AV_TIME_BASE_Q轉換為time_base
- 通過av_seek_frame函數根據目標時間戳跳到指定幀
- 清空當前編碼數據緩存隊列
具體實現如下
int parse_thread(void *arg) {
...
// Seek stuff goes here
if (is->seek_req) {//檢查[快進]/[快退]操作標志位是否開啟
int stream_index= -1;//初始化音視頻流類型標號
int64_t seek_target = is->seek_pos;//取得[快進]/[快退]操作后的參考時間戳
if (is->videoStream >= 0) {//檢查是否取得視頻流類型標號
stream_index = is->videoStream;//取得視頻流類型標號
} else if (is->audioStream >= 0) {//檢查是否取得音頻流類型標號
stream_index = is->audioStream;//取得音頻流類型標號
}
if (stream_index >= 0){//檢查是否取得音視頻流類型標號
//時間單位轉換,將seek_target的單位由AV_TIME_BASE_Q轉換為time_base
seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q, pFormatCtx->streams[stream_index]->time_base);
}
//根據[快進]/[快退]操作后的時間戳,跳到指定幀(該函數只能跳到離指定幀最近的關鍵幀)
if (av_seek_frame(is->pFormatCtx, stream_index, seek_target, is->seek_flags) < 0) {
fprintf(stderr, "%s: error while seeking\n", is->pFormatCtx->filename);
} else {//在執行[快進]/[快退]操作后,立刻清空緩存隊列,並重置音視頻解碼器
if (is->audioStream >= 0) {//檢查是否取得音頻流類型標號
packet_queue_flush(&is->audioq);//清除音頻隊列緩存,釋放隊列中所有動態分配的內存
packet_queue_put(&is->audioq, &flush_pkt);//將flush_pkt插入音頻數據包隊列,執行重置音頻解碼器操作avcodec_flush_buffers
}
if (is->videoStream >= 0) {//取得視頻流類型標號
packet_queue_flush(&is->videoq);//清除視頻隊列緩存,釋放隊列中所有動態分配的內存
packet_queue_put(&is->videoq, &flush_pkt);//將flush_pkt插入視頻數據包隊列,執行重置視頻解碼器操作avcodec_flush_buffers
}
}
is->seek_req = 0;//關閉[快進]/[快退]操作標志位
}//end for if (is->seek_req)
這里需要注意的是,av_seek_frame的輸入參數seek_target不是一個具體的時間長度,如多少秒,而是一個基於avcodec內部時基單位的取值,因此這里需要進行時間單位的轉換。
清空隊列原理是這樣的,給隊列插入一個特殊的flush packet,解碼線程檢測到這個packet后,立刻清空當前編碼數據緩存,然后等待下一個packet的到來。
清空編碼數據隊列后,parse_thread會根據av_seek_frame輸入的目標時間戳,從指定幀重新解析編碼數據,開始下一輪解析過程。
3、源碼編譯驗證
源碼的編譯方法和之前的例程完全相同,源碼可采用如下Makefile腳本進行編譯
tutorial07: tutorial07.c
gcc -o tutorial07 -g3 tutorial07.c -I${FFMPEG_INCLUDE} -I${SDL_INCLUDE} \
-L${FFMPEG_LIB} -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm \
`sdl-config --cflags --libs`
clean:
rm -rf tutorial07
執行make命令開始編譯,編譯完成后,可在源碼目錄生成名為[tutorial07]的可執行文件。
與ffplay的使用方法類似,執行[tutorial07 url]命令,url可以選擇本地視頻文件,或媒體流地址。
./tutorial07 ./xxx.mp4
輸入Ctrl+C結束程序運行
4、源碼清單
至此,一個完整的視頻播放器模型就介紹完了。雖然這只是一個播放器的基礎模型,但里面的內容卻並不檢查,涉及方方面面的內容。為了簡化學習的難度,這里將幾乎所有代碼都加上了注釋,大家可以自己動手調試,加深理解。
// tutorial07.c
// A pedagogical video player that really works! Now with seeking features.
//
// 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 tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lswscale -lswresample -lz -lm `sdl-config --cflags --libs`
//
// to build (assuming libavutil/libavformat/libavcodec/libswscale/libswresample are correctly installed your system).
//
// Run using
//
// $ tutorial07 myvideofile.mpg
//
// to play the video.
/*---------------------------
//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_mutex_unlock(&qlock);//釋放鎖
pthread_cond_signal(&qready);//給等待線程發出qready消息,通知消息隊列已就緒
}
---------------------------*/
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
#include <libavutil/avstring.h>
#include <libavutil/opt.h>
#include <libavutil/time.h>
#include <SDL.h>
#include <SDL_thread.h>
#ifdef __MINGW32__
#undef main // Prevents SDL from overriding main().
#endif
#include <stdio.h>
#include <math.h>
#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000
#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)
#define AV_SYNC_THRESHOLD 0.01
#define AV_NOSYNC_THRESHOLD 10.0
#define SAMPLE_CORRECTION_PERCENT_MAX 10
#define AUDIO_DIFF_AVG_NB 20
#define FF_ALLOC_EVENT (SDL_USEREVENT)
#define FF_REFRESH_EVENT (SDL_USEREVENT + 1)
#define FF_QUIT_EVENT (SDL_USEREVENT + 2)
#define VIDEO_PICTURE_QUEUE_SIZE 1
#define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER
SDL_Surface *screen;//SDL繪圖表面,A structure that contains a collection of pixels used in software blitting
//data成員被標記為"FLUSH"的AVPacket類型對象,解碼操作時在數據包隊列中遇到某個包的data成員為"FLUSH"時,執行重置解碼器操作
AVPacket flush_pkt;//在執行[快進]/[快退]操作后,ffmpeg需要執行重置解碼器操作
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;
enum {//同步時鍾源
AV_SYNC_AUDIO_MASTER,//音頻時鍾主同步源
AV_SYNC_VIDEO_MASTER,//視頻時鍾主同步源
AV_SYNC_EXTERNAL_MASTER,//外部時鍾主同步源
};
/*-------取得系統當前時間--------
int64_t av_gettime(void){
#if defined(CONFUG_WINCE)
return timeGetTime()*int64_t_C(1000);
#elif defined(CONFIG_WIN32)
struct _timeb tb;
_ftime(&tb);
return ((int64_t)tb.time*int64_t_C(1000)+(int64_t)tb.millitm)*int64_t_C(1000);
#else
struct timeval tv;
gettimeofday(&tv,NULL);//取得系統當前時間
return (int64_t)tv.tv_sec*1000000+tv.tv_usec;//以1/1000000秒為單位,便於在各個平台移植
#endif
}
---------------------------*/
/*-------鏈表節點結構體--------
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;
//圖像幀結構體
typedef struct VideoPicture {
SDL_Overlay *bmp;//SDL畫布overlay
int width, height;//Source height & width
int allocated;//是否分配內存空間,視頻幀轉換為SDL overlay標識
double pts;//當前圖像幀的絕對顯示時間戳
} VideoPicture;
//全局維護視頻播放狀態結構體
typedef struct VideoState {
AVFormatContext *pFormatCtx;//保存文件容器封裝信息及碼流參數的結構體
AVPacket audio_pkt;//保存從隊列中提取的編碼數據包
AVFrame audio_frame;//保存從數據包中解碼的音頻數據
AVStream *video_st;//視頻流信息結構體
AVStream *audio_st;//音頻流信息結構體
struct SwsContext *sws_ctx;//描述轉換器參數的結構體
struct SwsContext *sws_ctx_audio;
AVIOContext *io_context;
PacketQueue videoq;//視頻編碼數據包隊列(編碼數據隊列,以鏈表方式實現)
//解碼后的圖像幀隊列(解碼數據隊列,以數組方式實現),渲染邏輯就會從pictq獲取數據,同時解碼邏輯又會往pictq寫入數據
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;//隊列長度,讀/寫位置索引
SDL_mutex *pictq_lock;//隊列讀寫鎖對象,保護圖像幀隊列數據
SDL_cond *pictq_ready;//隊列就緒條件變量
PacketQueue audioq;//音頻編碼數據包隊列(編碼數據隊列,以鏈表方式實現)
uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];//保存解碼一個packet后的多幀原始音頻數據(解碼數據隊列,以數組方式實現)
unsigned int audio_buf_size;//解碼后的多幀音頻數據長度
unsigned int audio_buf_index;//累計寫入聲卡緩存長度
uint8_t *audio_pkt_data;//編碼數據緩存指針位置
int audio_pkt_size;//緩存中剩余的編碼數據長度(是否已完成一個完整的pakcet包的解碼,一個數據包中可能包含多個音頻編碼幀)
int audio_hw_buf_size;
int videoStream, audioStream;//音視頻流類型標號
SDL_Thread *parse_tid;//解封裝線程id
SDL_Thread *video_tid;//視頻解碼線程id
char filename[1024];//輸入文件完整路徑名
int quit;//全局退出進程標識,在界面上點了退出后,告訴線程退出
//video/audio_clock save pts of last decoded frame/predicted pts of next decoded frame
//keep track of how much time has passed according to the video/audio
double video_clock;//視頻時鍾,跟蹤視頻顯示時間戳
double audio_clock;//音頻時鍾,跟蹤音頻播放時間戳
double frame_timer;//視頻播放到當前幀時的累計已播放時間,跟蹤視頻實際播放時間,以系統時間為基准,frame_timer is what time it should be when we display the next frame
double frame_last_pts;//上一幀圖像的顯示時間戳,用於在video_refersh_timer中保存上一幀的pts值
double frame_last_delay;//上一幀圖像的動態刷新延遲時間
int av_sync_type;//主同步源類型
double audio_diff_cum;//U音頻時鍾與同步源累計時差,sed for AV difference average computation
double audio_diff_avg_coef;//音頻時鍾與同步源時差均值加權系數
double audio_diff_threshold;//音頻時鍾與同步源時差均值閾值
int audio_diff_avg_count;//音頻不同步計數(音頻時鍾與主同步源存在不同步的次數)
double video_current_pts;//當前幀顯示時間戳,Current displayed pts (different from video_clock if frame fifos are used)
//time (av_gettime) at which we updated video_current_pts - used to have running video pts
int64_t video_current_pts_time;//取得video_current_pts的時刻值(系統絕對時間)
double external_clock;//外部時鍾時間戳,External clock base.
int64_t external_clock_time;//取得外部時鍾的時刻值
int seek_req;//[快進]/[快退]操作開啟標志位
int seek_flags;//[快進]/[快退]操作類型標志位
int64_t seek_pos;//[快進]/[快退]操作后的參考時間戳
} VideoState;// Since we only have one decoding thread, the Big Struct can be global in case we need it.
VideoState *global_video_state;
//編碼數據隊列初始化函數
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= av_malloc(sizeof(AVPacketList));
if (!pktlist) {//檢查鏈表節點對象是否創建成功
return -1;
}
pktlist->pkt = *pkt;//將輸入數據包賦值給新建鏈表節點對象中的數據包對象
pktlist->next = NULL;//鏈表后繼指針為空
// if (pkt != &flush_pkt && av_packet_ref(pkt, pkt)<0) {
// 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 (global_video_state->quit) {//檢查退出進程標識
ret = -1;
break;
}//end for if
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發出臨界區資源就緒的信號)
}
}
SDL_UnlockMutex(q->qlock);//釋放互斥量
return ret;
}
//音頻解碼函數,從緩存隊列中提取數據包、解碼,並返回解碼后的數據長度(對一個完整的packet解碼,將解碼數據寫入audio_buf緩存,並返回多幀解碼數據的總長度)
int audio_decode_frame(VideoState *is, double *pts_ptr) {
int coded_consumed_size,data_size=0,pcm_bytes;//每次消耗的編碼數據長度[input](len1),輸出原始音頻數據的緩存長度[output],每組音頻采樣數據的字節數
AVPacket *pkt = &is->audio_pkt;//保存從隊列中提取的數據包
double pts;//音頻播放時間戳
for (;;) {
/*--2、從數據包pkt中不斷的解碼音頻數據,直到剩余的編碼數據長度<=0---*/
while (is->audio_pkt_size>0) {//檢查緩存中剩余的編碼數據長度(是否已完成一個完整的pakcet包的解碼,一個數據包中可能包含多個音頻編碼幀)
int got_frame = 0;//解碼操作成功標識,成功返回非零值
//解碼一幀音頻數據,並返回消耗的編碼數據長度
coded_consumed_size = avcodec_decode_audio4(is->audio_st->codec, &is->audio_frame, &got_frame, pkt);
if (coded_consumed_size<0) {//檢查是否執行了解碼操作
// If error, skip frame.
is->audio_pkt_size=0;//更新編碼數據緩存長度
break;
}
if (got_frame) {//檢查解碼操作是否成功
if (is->audio_frame.format != AV_SAMPLE_FMT_S16) {//檢查音頻數據格式是否為16位采樣格式
//當音頻數據不為16位采樣格式情況下,采用decode_frame_from_packet計算解碼數據長度
data_size=decode_frame_from_packet(is, is->audio_frame);
} else {//計算解碼后音頻數據長度[output]
data_size=av_samples_get_buffer_size(NULL,is->audio_st->codec->channels,is->audio_frame.nb_samples,is->audio_st->codec->sample_fmt, 1);
memcpy(is->audio_buf,is->audio_frame.data[0],data_size);//將解碼數據復制到輸出緩存
}
}
is->audio_pkt_data += coded_consumed_size;//更新編碼數據緩存指針位置
is->audio_pkt_size -= coded_consumed_size;//更新緩存中剩余的編碼數據長度
if (data_size <= 0) {//檢查輸出解碼數據緩存長度
// No data yet, get more frames.
continue;
}
pts = is->audio_clock;
*pts_ptr=pts;
/*---------------------
* 當一個packet中包含多個音頻幀時
* 通過[解碼后音頻原始數據長度]及[采樣率]來推算一個packet中其他音頻幀的播放時間戳pts
* 采樣頻率44.1kHz,量化位數16位,意味着每秒采集數據44.1k個,每個數據占2字節
--------------------*/
pcm_bytes=2*is->audio_st->codec->channels;//計算每組音頻采樣數據的字節數=每個聲道音頻采樣字節數*聲道數
/*----更新audio_clock---
* 一個pkt包含多個音頻frame,同時一個pkt對應一個pts(pkt->pts)
* 因此,該pkt中包含的多個音頻幀的時間戳由以下公式推斷得出
* bytes_per_sec=pcm_bytes*is->audio_st->codec->sample_rate
* 從pkt中不斷的解碼,推斷(一個pkt中)每幀數據的pts並累加到音頻播放時鍾
--------------------*/
is->audio_clock+=(double)data_size/(double)(pcm_bytes*is->audio_st->codec->sample_rate);
// We have data, return it and come back for more later.
return data_size;//返回解碼數據原始數據長度
}
/*-----------------1、從緩存隊列中提取數據包------------------*/
if (pkt->data) {//檢查數據包是殘留編碼數據
av_packet_unref(pkt);//釋放pkt中保存的編碼數據
}
if (is->quit) {//檢查退出進程標識
return -1;
}
// Next packet,從隊列中提取數據包到pkt
if (packet_queue_get(&is->audioq, pkt, 1) < 0) {
return -1;
}
if (pkt->data == flush_pkt.data) {//檢查是否需要重新解碼
avcodec_flush_buffers(is->audio_st->codec);//重新解碼前需要重置解碼器
continue;
}
is->audio_pkt_data = pkt->data;//傳遞編碼數據緩存指針
is->audio_pkt_size = pkt->size;//傳遞編碼數據緩存長度
// If update, update the audio clock w/pts.
if (pkt->pts != AV_NOPTS_VALUE) {//檢查音頻播放時間戳
//獲得一個新的packet的時候,更新audio_clock,用packet中的pts更新audio_clock(一個pkt對應一個pts)
is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;//更新音頻已經播的時間
}
}
}
/*------Audio Callback-------
* 音頻輸出回調函數,系統通過該回調函數將解碼后的pcm數據送入聲卡播放
* 系統通常一次會准備一組緩存pcm數據(減少低速系統i/o次數),通過該回調送入聲卡,聲卡根據音頻pts依次播放pcm數據
* 待送入緩存的pcm數據完成播放后,再載入一組新的pcm緩存數據(每次音頻輸出緩存為空時,系統就調用此函數填充音頻輸出緩存,並送入聲卡播放)
* 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) {
VideoState *is = (VideoState *)userdata;//傳遞用戶數據
int wt_stream_len, audio_size;//每次送入聲卡的數據長度,解碼后的數據長度
double pts;//音頻時間戳
while (len > 0) {//檢查音頻緩存的剩余長度
if (is->audio_buf_index >= is->audio_buf_size) {//檢查是否需要執行解碼操作
//從緩存隊列中提取數據包、解碼,並返回解碼后的數據長度,audio_buf緩存中可能包含多幀解碼后的音頻數據,We have already sent all our data; get more
audio_size = audio_decode_frame(is, &pts);
if (audio_size < 0) {//檢查解碼操作是否成功
// If error, output silence.
is->audio_buf_size = 1024;
memset(is->audio_buf, 0, is->audio_buf_size);//全零重置緩沖區
} else {//在回調函數中增加音頻同步過程,即對音頻數據緩存進行丟幀(或插值),以起到降低音頻時鍾與主同步源時差的目的
audio_size=synchronize_audio(is,(int16_t*)is->audio_buf,audio_size,pts);//返回音頻同步后的緩存長度
is->audio_buf_size=audio_size;//返回packet中包含的原始音頻數據長度(多幀)
}
is->audio_buf_index = 0;//初始化累計寫入緩存長度
}
wt_stream_len = is->audio_buf_size - is->audio_buf_index;//計算解碼緩存剩余長度
if (wt_stream_len > len) {//檢查每次寫入緩存的數據長度是否超過指定長度(1024)
wt_stream_len = len;//指定長度從解碼的緩存中取數據
}
//每次從解碼的緩存數據中以指定長度抽取數據並送入聲卡
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, wt_stream_len);
len -= wt_stream_len;//更新解碼音頻緩存的剩余長度
stream += wt_stream_len;//更新緩存寫入位置
is->audio_buf_index += wt_stream_len;//更新累計寫入緩存數據長度
}
}
//視頻(圖像)幀渲染
void video_display(VideoState *is) {
SDL_Rect rect;//SDL矩形對象
VideoPicture *vp;//圖像幀結構體指針
float aspect_ratio;//寬度/高度比
int w, h, x, y;//窗口尺寸及起始位置
vp = &is->pictq[is->pictq_rindex];//從圖像幀隊列(數組)中提取圖像幀結構對象
if (vp->bmp) {//檢查像素數據指針是否有效
if (is->video_st->codec->sample_aspect_ratio.num == 0) {
aspect_ratio = 0;
} else {
aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) * is->video_st->codec->width / is->video_st->codec->height;
}
if (aspect_ratio <= 0.0) {
aspect_ratio = (float) is->video_st->codec->width / (float) is->video_st->codec->height;
}
h = screen->h;
w = ((int) rint(h * aspect_ratio)) & -3;
if (w > screen->w) {
w = screen->w;
h = ((int) rint(w / aspect_ratio)) & -3;
}
x = (screen->w - w) / 2;
y = (screen->h - h) / 2;
//設置矩形顯示區域
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_DisplayYUVOverlay(vp->bmp, &rect);//圖像渲染
}
}
//創建/重置圖像幀,為圖像幀分配內存空間
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *)userdata;//傳遞用戶數據
VideoPicture *vp= &is->pictq[is->pictq_windex];//從圖像幀隊列(數組)中提取圖像幀結構對象
if (vp->bmp) {//檢查圖像幀是否已存在
// We already have one make another, bigger/smaller.
SDL_FreeYUVOverlay(vp->bmp);//釋放當前overlay緩存
}
// Allocate a place to put our YUV image on that screen,根據指定尺寸及像素格式重新創建像素緩存區
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width, is->video_st->codec->height, SDL_YV12_OVERLAY, screen);
vp->width = is->video_st->codec->width;//設置圖像幀寬度
vp->height = is->video_st->codec->height;//設置圖像幀高度
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護畫布的像素數據
vp->allocated = 1;//圖像幀像素緩沖區已分配內存
SDL_CondSignal(is->pictq_ready);//給等待線程發出消息,通知隊列已就緒
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
}
/*---------------------------
* queue_picture:圖像幀插入隊列等待渲染
* @is:全局狀態參數集
* @pFrame:保存圖像解碼數據的結構體
* @pts:當前圖像幀的絕對顯示時間戳
* 1、首先檢查圖像幀隊列(數組)是否存在空間插入新的圖像,若沒有足夠的空間插入圖像則使當前線程休眠等待
* 2、在初始化的條件下,隊列(數組)中VideoPicture的bmp對象(YUV overlay)尚未分配空間,通過FF_ALLOC_EVENT事件的方法調用alloc_picture分配空間
* 3、當隊列(數組)中所有VideoPicture的bmp對象(YUV overlay)均已分配空間的情況下,直接跳過步驟2向bmp對象拷貝像素數據,像素數據在進行格式轉換后執行拷貝操作
---------------------------*/
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
/*-------1、檢查隊列是否有插入空間------*/
// Wait until we have space for a new pic.
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護圖像幀隊列
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit) {//檢查隊列當前長度
SDL_CondWait(is->pictq_ready, is->pictq_lock);//線程休眠等待pictq_ready信號
}
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
if (is->quit) {//檢查進程退出標識
return -1;
}
/*------2、初始化/重置YUV overlay------*/
// windex is set to 0 initially.
VideoPicture *vp = &is->pictq[is->pictq_windex];//從圖像幀隊列中抽取圖像幀對象
// Allocate or resize the buffer,檢查YUV overlay是否已存在,否則初始化YUV overlay,分配像素緩存空間
if (!vp->bmp || vp->width != is->video_st->codec->width || vp->height != is->video_st->codec->height) {
SDL_Event event;//SDL事件對象
vp->allocated = 0;//圖像幀未分配空間
// We have to do it in the main thread.
event.type = FF_ALLOC_EVENT;//指定分配圖像幀內存事件
event.user.data1 = is;//傳遞用戶數據
SDL_PushEvent(&event);//發送SDL事件
// Wait until we have a picture allocated.
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護圖像幀隊列
while (!vp->allocated && !is->quit) {//檢查當前圖像幀是否已初始化(為SDL overlay)
SDL_CondWait(is->pictq_ready, is->pictq_lock);//線程休眠等待alloc_picture發送pictq_ready信號喚醒當前線程
}
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
if (is->quit) {//檢查進程退出標識
return -1;
}
}
/*-------3、拷貝視頻幀到YUV overlay------*/
// We have a place to put our picture on the queue.
// If we are skipping a frame, do we set this to null but still return vp->allocated = 1?
AVFrame pict;//臨時保存轉換后的圖像幀像素,與隊列中的元素相關聯
if (vp->bmp) {//檢查像素數據指針是否有效
SDL_LockYUVOverlay(vp->bmp);//locks the overlay for direct access to pixel data,原子操作,保護像素緩沖區,避免非法修改
// Point pict at the queue.
pict.data[0] = vp->bmp->pixels[0];//將轉碼后的圖像與畫布的像素緩沖器關聯
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];//將轉碼后的圖像掃描行長度與畫布像素緩沖區的掃描行長度相關聯
pict.linesize[1] = vp->bmp->pitches[2];//linesize-Size, in bytes, of the data for each picture/channel plane
pict.linesize[2] = vp->bmp->pitches[1];//For audio, only linesize[0] may be set
// Convert the image into YUV format that SDL uses,將解碼后的圖像幀轉換為AV_PIX_FMT_YUV420P格式,並拷貝到圖像幀隊列
sws_scale(is->sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, is->video_st->codec->height, pict.data, pict.linesize);
SDL_UnlockYUVOverlay(vp->bmp);//Unlocks a previously locked overlay. An overlay must be unlocked before it can be displayed
vp->pts = pts;//傳遞當前圖像幀的絕對顯示時間戳
// Now we inform our display thread that we have a pic ready.
if (++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {//更新並檢查當前圖像幀隊列寫入位置
is->pictq_windex = 0;//重置圖像幀隊列寫入位置
}
SDL_LockMutex(is->pictq_lock);//鎖定隊列讀寫鎖,保護隊列數據
is->pictq_size++;//更新圖像幀隊列長度
SDL_UnlockMutex(is->pictq_lock);//釋放隊列讀寫鎖
}
return 0;
}
//修改FFmpeg內部退出回調對應的函數
int decode_interrupt_cb(void *opaque) {
return (global_video_state && global_video_state->quit);
}
//清除隊列緩存,釋放隊列中所有動態分配的內存
static void packet_queue_flush(PacketQueue *q) {
AVPacketList *pkt, *pkttmp;//隊列當前節點,臨時節點
SDL_LockMutex(q->qlock);//鎖定互斥量
for (pkt = q->first_pkt; pkt != NULL; pkt = pkttmp) {//遍歷隊列所有節點
pkttmp = pkt->next;//隊列頭節點后移
av_packet_unref(&pkt->pkt);//當前節點引用計數-1
av_freep(&pkt);//釋放當前節點緩存
}
q->last_pkt = NULL;//隊列尾節點指針置零
q->first_pkt = NULL;//隊列頭節點指針置零
q->nb_packets = 0;//隊列長度置零
q->size = 0;//隊列編碼數據的緩存長度置零
SDL_UnlockMutex(q->qlock);//互斥量解鎖
}
//編碼數據包解析線程函數(從視頻文件中解析出音視頻編碼數據單元,一個AVPacket的data通常對應一個NAL)
int parse_thread(void *arg) {
VideoState *is = (VideoState *)arg;//傳遞用戶參數
global_video_state = is;//傳遞全局狀態參量結構體
AVFormatContext *pFormatCtx = NULL;//保存文件容器封裝信息及碼流參數的結構體
AVPacket pkt, *packet = &pkt;//在棧上創建臨時數據包對象並關聯指針
// Find the first video/audio stream.
is->videoStream = -1;//視頻流類型標號初始化為-1
is->audioStream = -1;//音頻流類型標號初始化為-1s
int video_index = -1;//視頻流類型標號初始化為-1
int audio_index = -1;//音頻流類型標號初始化為-1
int i;//循環變量
AVDictionary *io_dict = NULL;
AVIOInterruptCB callback;
// Will interrupt blocking functions if we quit!.
callback.callback = decode_interrupt_cb;
callback.opaque = is;
//if (avio_open2(&is->io_context, is->filename, 0, &callback, &io_dict)) {
// fprintf(stderr, "Unable to open I/O for %s\n", is->filename);
// return -1;
//}
// Open video file,打開視頻文件,取得文件容器的封裝信息及碼流參數
if (avformat_open_input(&pFormatCtx, is->filename, NULL, NULL) != 0) {
return -1; // Couldn't open file.
}
is->pFormatCtx = pFormatCtx;//傳遞文件容器封裝信息及碼流參數
// 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, is->filename, 0);
// Find the first video stream.
for (i=0; i<pFormatCtx->nb_streams; i++) {//遍歷文件中包含的所有流媒體類型(視頻流、音頻流、字幕流等)
if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO && video_index < 0) {//若文件中包含有視頻流
video_index=i;//用視頻流類型的標號修改標識,使之不為-1
}
if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO && audio_index < 0) {//若文件中包含有音頻流
audio_index=i;//用音頻流類型的標號修改標識,使之不為-1
}
}
if (audio_index >= 0) {//檢查文件中是否存在音頻流
stream_component_open(is, audio_index);//根據指定類型打開音頻流
}
if (video_index >= 0) {//檢查文件中是否存在視頻流
stream_component_open(is, video_index);
}
if (is->videoStream < 0 || is->audioStream < 0) {//檢查文件中是否存在音視頻流
fprintf(stderr, "%s: could not open codecs\n", is->filename);
goto fail;
}
// Main decode loop.
for (;;) {
if (is->quit) {//檢查退出進程標識
break;
}
// Seek stuff goes here
if (is->seek_req) {//檢查[快進]/[快退]操作標志位是否開啟
int stream_index= -1;//初始化音視頻流類型標號
int64_t seek_target = is->seek_pos;//取得[快進]/[快退]操作后的參考時間戳
if (is->videoStream >= 0) {//檢查是否取得視頻流類型標號
stream_index = is->videoStream;//取得視頻流類型標號
} else if (is->audioStream >= 0) {//檢查是否取得音頻流類型標號
stream_index = is->audioStream;//取得音頻流類型標號
}
if (stream_index >= 0){//檢查是否取得音視頻流類型標號
//時間單位轉換,將seek_target的單位由AV_TIME_BASE_Q轉換為time_base
seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q, pFormatCtx->streams[stream_index]->time_base);
}
//根據[快進]/[快退]操作后的時間戳,跳到指定幀(該函數只能跳到離指定幀最近的關鍵幀)
if (av_seek_frame(is->pFormatCtx, stream_index, seek_target, is->seek_flags) < 0) {
fprintf(stderr, "%s: error while seeking\n", is->pFormatCtx->filename);
} else {//在執行[快進]/[快退]操作后,立刻清空緩存隊列,並重置音視頻解碼器
if (is->audioStream >= 0) {//檢查是否取得音頻流類型標號
packet_queue_flush(&is->audioq);//清除音頻隊列緩存,釋放隊列中所有動態分配的內存
packet_queue_put(&is->audioq, &flush_pkt);//將flush_pkt插入音頻數據包隊列,執行重置音頻解碼器操作avcodec_flush_buffers
}
if (is->videoStream >= 0) {//取得視頻流類型標號
packet_queue_flush(&is->videoq);//清除視頻隊列緩存,釋放隊列中所有動態分配的內存
packet_queue_put(&is->videoq, &flush_pkt);//將flush_pkt插入視頻數據包隊列,執行重置視頻解碼器操作avcodec_flush_buffers
}
}
is->seek_req = 0;//關閉[快進]/[快退]操作標志位
}//end for if (is->seek_req)
// Seek stuff goes here,檢查音視頻編碼數據包隊列長度是否溢出
if (is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
//從文件中依次讀取每個圖像編碼數據包,並存儲在AVPacket數據結構中
if (av_read_frame(is->pFormatCtx, packet) < 0) {
if (is->pFormatCtx->pb->error == 0) {
SDL_Delay(100); // No error; wait for user input.
continue;
} else {
break;
}
}
// Is this a packet from the video stream?.
if (packet->stream_index == is->videoStream) {//檢查數據包是否為視頻類型
packet_queue_put(&is->videoq, packet);//向隊列中插入數據包
} else if (packet->stream_index == is->audioStream) {//檢查數據包是否為音頻類型
packet_queue_put(&is->audioq, packet);//向隊列中插入數據包
} else {//檢查數據包是否為字幕類型
av_packet_unref(packet);//釋放packet中保存的(字幕)編碼數據
}
}
// All done - wait for it.
while (!is->quit) {
SDL_Delay(100);
}
fail:
{
SDL_Event event;//SDL事件對象
event.type = FF_QUIT_EVENT;//指定退出事件類型
event.user.data1 = is;//傳遞用戶數據
SDL_PushEvent(&event);//將該事件對象壓入SDL后台事件隊列
}
return 0;
}
//根據指定類型打開流,找到對應的解碼器、創建對應的音頻配置、保存關鍵信息到 VideoState、啟動音頻和視頻線程
int stream_component_open(VideoState *is, int stream_index) {
AVFormatContext *pFormatCtx = is->pFormatCtx;//傳遞文件容器的封裝信息及碼流參數
AVCodecContext *codecCtx = NULL;//解碼器上下文對象,解碼器依賴的相關環境、狀態、資源以及參數集的接口指針
AVCodec *codec = NULL;//保存編解碼器信息的結構體,提供編碼與解碼的公共接口,可以看作是編碼器與解碼器的一個全局變量
AVDictionary *optionsDict = NULL;//SDL_AudioSpec a structure that contains the audio output format,創建 SDL_AudioSpec 結構體,設置音頻播放數據
SDL_AudioSpec wanted_spec, spec;
//檢查輸入的流類型是否在合理范圍內
if (stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
return -1;
}
// Get a pointer to the codec context for the video stream.
codecCtx = pFormatCtx->streams[stream_index]->codec;//取得解碼器上下文
if (codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {//檢查解碼器類型是否為音頻解碼器
// Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format
// 創建SDL_AudioSpec結構體,設置音頻播放參數
wanted_spec.freq = codecCtx->sample_rate;//采樣頻率 DSP frequency -- samples per second
wanted_spec.format = AUDIO_S16SYS;//采樣格式 Audio data format
wanted_spec.channels = codecCtx->channels;//聲道數 Number of channels: 1 mono, 2 stereo
wanted_spec.silence = 0;//無輸出時是否靜音
//默認每次讀音頻緩存的大小,推薦值為 512~8192,ffplay使用的是1024 specifies a unit of audio data refers to the size of the audio buffer in sample frames
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;//設置讀取音頻數據的回調接口函數 the function to call when the audio device needs more data
wanted_spec.userdata = is;//傳遞用戶數據
//Opens the audio device with the desired parameters(wanted_spec),以指定參數打開音頻設備
if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
is->audio_hw_buf_size = spec.size;
}
// Find the decoder for the video stream,根據視頻流對應的解碼器上下文查找對應的解碼器,返回對應的解碼器(信息結構體)
codec = avcodec_find_decoder(codecCtx->codec_id);
if (!codec || (avcodec_open2(codecCtx, codec, &optionsDict) < 0)) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
//檢查解碼器類型
switch (codecCtx->codec_type) {
case AVMEDIA_TYPE_AUDIO://音頻解碼器
is->audioStream = stream_index;//音頻流類型標號初始化
is->audio_st = pFormatCtx->streams[stream_index];
is->audio_buf_size = 0;//解碼后的多幀音頻數據長度
is->audio_buf_index = 0;//累計寫入聲卡緩存長度
// Averaging filter for audio sync.
is->audio_diff_avg_coef = exp(log(0.01 / AUDIO_DIFF_AVG_NB));//音頻時鍾與同步源時差均值加權系數初始化
is->audio_diff_avg_count = 0;//初始化音頻時鍾與主同步源存在不同步的次數
// Correct audio only if larger error than this. 初始化音頻時鍾與同步源時差均值閾值
is->audio_diff_threshold = 2.0 * SDL_AUDIO_BUFFER_SIZE / codecCtx->sample_rate;
is->sws_ctx_audio = (struct SwsContext *) swr_alloc();
if (!is->sws_ctx_audio) {
fprintf(stderr, "Could not allocate resampler context\n");
return -1;
}
memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
packet_queue_init(&is->audioq);//音頻數據包隊列初始化
SDL_PauseAudio(0);//audio callback starts running again,開啟音頻設備,如果這時候沒有獲得數據那么它就靜音
break;
case AVMEDIA_TYPE_VIDEO://視頻解碼器
is->videoStream = stream_index;//視頻流類型標號初始化
is->video_st = pFormatCtx->streams[stream_index];//取得視頻流信息
//以系統時間為基准,初始化播放到當前幀的已播放時間值,該值為真實時間值、動態時間值、絕對時間值
is->frame_timer = (double)av_gettime() / 1000000.0;
is->frame_last_delay = 40e-3;//初始化上一幀圖像的動態刷新延遲時間
is->video_current_pts_time = av_gettime();//取得系統當前時間
packet_queue_init(&is->videoq);//視頻數據包隊列初始化
is->video_tid = SDL_CreateThread(decode_thread,is);//創建解碼線程
// Initialize SWS context for software scaling,設置圖像轉換像素格式為AV_PIX_FMT_YUV420P
is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height,
is->video_st->codec->pix_fmt, is->video_st->codec->width, is->video_st->codec->height,
AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);
codecCtx->get_buffer2 = our_get_buffer;
break;
default:
break;
}
return 0;
}
//視頻解碼線程函數
int decode_thread(void *arg) {
VideoState *is = (VideoState *)arg;//傳遞用戶數據
AVPacket pkt, *packet = &pkt;//在棧上創建臨時數據包對象並關聯指針
int frameFinished;//解碼操作是否成功標識,解碼幀結束標志frameFinished
// Allocate video frame,為解碼后的視頻信息結構體分配空間並完成初始化操作(結構體中的圖像緩存按照下面兩步手動安裝)
AVFrame *pFrame= av_frame_alloc();
double pts;//當前幀在整個視頻中的(絕對)時間位置
for (;;) {
if (packet_queue_get(&is->videoq,packet,1)<0){//從隊列中提取數據包到packet,並將提取的數據包出隊列
// Means we quit getting packets.
break;
}
if (packet->data == flush_pkt.data) {//檢查是否需要重新解碼
avcodec_flush_buffers(is->video_st->codec);//重新解碼前需要重置解碼器
continue;
}
pts = 0;//(絕對)顯示時間戳初始化
// Save global pts to be stored in pFrame in first call.
global_video_pkt_pts = packet->pts;
/*-------------------------
* Decode video frame,解碼完整的一幀數據,並將frameFinished設置為true
* 可能無法通過只解碼一個packet就獲得一個完整的視頻幀frame,可能需要讀取多個packet才行,avcodec_decode_video2()會在解碼到完整的一幀時設置frameFinished為真
* avcodec_decode_video2會按照dts指定的順序解碼,然后按照正確的顯示順序輸出圖像幀,並附帶圖像幀的pts
* 若解碼順序與顯示順序存在不一致,則avcodec_decode_video2會先對當前幀進行緩存(此時frameFinished=0),然后按照正確的順序輸出圖像幀
* The decoder bufferers a few frames for multithreaded efficiency
* It is also absolutely required to delay decoding in the case of B frames
* where the decode order may not be the same as the display order.
* Takes input raw video data from frame and writes the next output packet,
* if available, to avpkt. The output packet does not necessarily contain data for the most recent frame,
* as encoders can delay and reorder input frames internally as needed.
-------------------------*/
avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);
// if (packet->dts == AV_NOPTS_VALUE && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) {
// pts = *(uint64_t *)pFrame->opaque;
// } else if (packet->dts != AV_NOPTS_VALUE) {
// pts = packet->dts;
// } else {
// pts = 0;
// }
pts=av_frame_get_best_effort_timestamp(pFrame);//取得編碼數據包中的圖像幀顯示序號PTS(int64_t),並暫時保存在pts(double)中
/*-------------------------
* 在解碼線程函數中計算當前圖像幀的顯示時間戳
* 1、取得編碼數據包中的圖像幀顯示序號PTS(int64_t),並暫時保存在pts(double)中
* 2、根據PTS*time_base來計算當前幀在整個視頻中的顯示時間戳,即PTS*(1/framerate)
* av_q2d把AVRatioal結構轉換成double的函數,
* 用於計算視頻源每個圖像幀的顯示時間(1/framerate),即返回(time_base->num/time_base->den)
-------------------------*/
//根據pts=PTS*time_base={numerator=1,denominator=25}計算當前幀在整個視頻中的顯示時間戳
pts *= av_q2d(is->video_st->time_base);
// Did we get a video frame?
if (frameFinished) {
pts = synchronize_video(is, pFrame, pts);//檢查當前幀的顯示時間戳pts並更新內部視頻播放計時器(記錄視頻已經播時間(video_clock))
if (queue_picture(is, pFrame, pts) < 0) {//將解碼完成的圖像幀添加到圖像幀隊列,並記錄圖像幀的絕對顯示時間戳pts
break;
}
}
av_packet_unref(packet);//釋放pkt中保存的編碼數據
}
av_free(pFrame);//清除pFrame中的內存空間
return 0;
}
/*---------------------------
* 更新內部視頻播放計時器(記錄視頻已經播時間(video_clock))
* @is:全局狀態參數集
* @src_frame:當前(輸入的)(待更新的)圖像幀對象
* @pts:當前圖像幀的顯示時間戳
---------------------------*/
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {
/*----------檢查顯示時間戳----------*/
if (pts != 0) {//檢查顯示時間戳是否有效
// If we have pts, set video clock to it.
is->video_clock = pts;//用顯示時間戳更新已播放時間
} else {//若獲取不到顯示時間戳
// If we aren't given a pts, set it to the clock.
pts = is->video_clock;//用已播放時間更新顯示時間戳
}
/*--------更新視頻已經播時間--------*/
// Update the video clock,若該幀要重復顯示(取決於repeat_pict),則全局視頻播放時序video_clock應加上重復顯示的數量*幀率
double frame_delay=av_q2d(is->video_st->codec->time_base);//該幀顯示完將要花費的時間
// If we are repeating a frame, adjust clock accordingly,若存在重復幀,則在正常播放的前后兩幀圖像間安排渲染重復幀
frame_delay+=src_frame->repeat_pict*(frame_delay*0.5);//計算渲染重復幀的時值(類似於音符時值)
is->video_clock+=frame_delay;//更新視頻播放時序
return pts;//此時返回的值即為下一幀將要開始顯示的時間戳
}
/*---------------------------
* return the wanted number of samples to get better sync if sync_type is video or external master clock
* 通常情況下會以音頻或系統時鍾為主同步源,只有在音頻或系統時鍾失效的情況下才以視頻為主同步源
* 該函數比對音頻時鍾與主同步源的時差,通過動態丟幀(或插值)部分音頻數據,以起到減少(或增加)音頻播放時長,減少與主同步源時差的作用
* 該函數對音頻緩存數據進行丟幀(或插值),返回丟幀(或插值)后的音頻數據長度
* 因為音頻同步可能帶來輸出聲音不連續等副作用,該函數通過音頻不同步次數(audio_diff_avg_count)及時差均值(avg_diff)來約束音頻的同步過程
---------------------------*/
int synchronize_audio(VideoState *is, short *samples, int samples_size, double pts) {
double ref_clock;//主同步源(基准時鍾)
int pcm_bytes=is->audio_st->codec->channels*2;//每組音頻數據字節數=聲道數*每聲道數據字節數
/* if not master, then we try to remove or add samples to correct the clock */
if (is->av_sync_type != AV_SYNC_AUDIO_MASTER) {//檢查主同步源,若同步源不是音頻時鍾的情況下,執行以下代碼
double diff, avg_diff;//diff-音頻幀播放間與主同步源的時差,avg_diff-采樣不同步的平均值
int wanted_size, min_size, max_size;//經過丟幀(或插值)后的緩存長度,緩存長度最大/最小值
ref_clock = get_master_clock(is);//取得當前主同步源,以主同步源為基准時間(與get_video_clock實現相結合)
diff = get_audio_clock(is) - ref_clock;//計算音頻時鍾與當前主同步源的時差
if (diff<AV_NOSYNC_THRESHOLD) {//檢查音頻是否處於不同步狀態(通過AV_NOSYNC_THRESHOLD限制丟棄的音頻數據長度,避免出現聲音不連續)
//Accumulate the diffs,對時差加權累加(離當前播放時間近的時差權值系數大)
is->audio_diff_cum=diff+is->audio_diff_avg_coef*is->audio_diff_cum;
if (is->audio_diff_avg_count<AUDIO_DIFF_AVG_NB) {//將音頻不同步計數與閾值進行比對
//not enough measures to have a correct estimate
is->audio_diff_avg_count++;//音頻不同步計數更新
} else {//當音頻不同步次數超過閾值限定后,觸發音頻同步操作
avg_diff=is->audio_diff_cum*(1.0-is->audio_diff_avg_coef);//計算時差均值(等比級數幾何平均數)
if (fabs(avg_diff)>=is->audio_diff_threshold) {//比對時差均值與時差閾值
wanted_size=samples_size+((int)(diff*is->audio_st->codec->sample_rate)*pcm_bytes);//根據時差換算同步后的緩存長度
min_size=samples_size*((100-SAMPLE_CORRECTION_PERCENT_MAX)/100);//同步后的緩存長度最小值
max_size=samples_size*((100+SAMPLE_CORRECTION_PERCENT_MAX)/100);//同步后的緩存長度最大值
if (wanted_size<min_size) {//若同步后緩存長度<最小緩存長度
wanted_size=min_size;//用最小緩存長度作為同步后的緩存長度
} else if (wanted_size>max_size) {//若同步后緩存長度>最小緩存長度
wanted_size=max_size;//用最大緩存長度作為同步后的緩存長度
}
if (wanted_size<samples_size) {//比對同步后的音頻緩存數據長度與原始緩存長度
samples_size=wanted_size;//Remove samples,用丟幀后的音頻緩存長度更新原始緩存長度
} else if (wanted_size>samples_size) {//若同步后緩存長度大於當前緩存長度
//Add samples by copying final sample.
//int nb= (samples_size - wanted_size);
int nb=wanted_size-samples_size;//計算插值后緩存長度與原始緩存長度間的差值(需要插值的音頻數據組數)
uint8_t *samples_end=(uint8_t*)samples+samples_size-pcm_bytes;//取得緩存末端數據指針
uint8_t *q=samples_end+pcm_bytes;//初始插值位置|<-----samples----->||q|
while (nb>0) {//檢查插值音頻組數(每組包括兩個聲道的pcm數據)
memcpy(q, samples_end, pcm_bytes);//在samples原始緩存后追加插值
q += pcm_bytes;//更新插值位置
nb -= pcm_bytes;//更新插值組數
}
samples_size = wanted_size;//返回音頻同步后的緩存長度
}
}
}
} else {
// Difference is too big, reset diff stuff,時差過大,重置時差累計值
is->audio_diff_avg_count = 0;//音頻不同步計數重置
is->audio_diff_cum = 0;//音頻累計時差重置
}
}//end for if (is->av_sync_type != AV_SYNC_AUDIO_MASTER)
return samples_size;//返回音頻同步后的緩存長度
}
/*-----------取得音頻時鍾-----------
* 即取得當前播放音頻數據的pts,以音頻時鍾作為音視頻同步基准
* 音視頻同步的原理是根據音頻的pts來控制視頻的播放
* 也就是說在視頻解碼一幀后,是否顯示以及顯示多長時間,是通過該幀的PTS與同時正在播放的音頻的PTS比較而來的
* 如果音頻的PTS較大,則視頻顯示完畢准備下一幀的解碼顯示,否則等待
*
* 因為pcm數據采用audio_callback回調方式進行播放
* 對於音頻播放我們只能得到寫入回調函數前緩存音頻幀的pts,而無法得到當前播放幀的pts(需要采用當前播放音頻幀的pts作為參考時鍾)
* 考慮到音頻的大小與播放時間成正比(相同采樣率),那么當前時刻正在播放的音頻幀pts(位於回調函數緩存中)
* 就可以根據已送入聲卡的pcm數據長度、緩存中剩余pcm數據長度,緩存長度及采樣率進行推算了
--------------------------------*/
double get_audio_clock(VideoState *is) {
double pts= is->audio_clock;//Maintained in the audio thread,取得解碼操作完成時的時間戳
//還未(送入聲卡)播放的剩余原始音頻數據長度,等於解碼后的多幀原始音頻數據長度-累計寫入聲卡緩存(送入聲卡)的長度
int hw_buf_size=is->audio_buf_size - is->audio_buf_index;
int bytes_per_sec=0;//每秒的原始音頻字節數
int pcm_bytes=is->audio_st->codec->channels*2;//每組原始音頻數據字節書=聲道數*每聲道數據字節數
if (is->audio_st) {
bytes_per_sec=is->audio_st->codec->sample_rate*pcm_bytes;//計算每秒的原始音頻字節數
}
if (bytes_per_sec) {//檢查每秒的原始音頻字節數是否有效
pts-=(double)hw_buf_size/bytes_per_sec;//根據送入聲卡緩存的索引位置,往前倒推計算當前時刻的音頻播放時間戳pts
}
return pts;//返回音頻播放時間戳
}
/*-----------取得視頻時鍾-----------
* 即取得當前播放視頻幀的pts,以視頻時鍾pts作為音視頻同步基准,return the current time offset of the video currently being played
* 該值為當前幀時間戳pts+一個微小的修正值delta
* 因為在ms的級別上,在毫秒級別上,若取得視頻時鍾(即當前幀pts)的時刻,與調用視頻時鍾的時刻(如將音頻同步到該視頻pts時刻)存在延遲
* 那么,視頻時鍾需要在被調用時進行修正,修正值delta為
* delta=[取得視頻時鍾的時刻值video_current_pts_time] 到 [調用get_video_clock時刻值] 的間隔時間
* 通常情況下,都會選擇以外部時鍾或音頻時鍾作為主同步源,以視頻同步到音頻或外部時鍾為首選同步方案
* 以視頻時鍾作為主同步源的同步方案,屬於3種基本的同步方案(同步到音頻、同步到視頻、同步到外部時鍾)
* 本利僅為展示同步到視頻時鍾的方法,一般情況下同步到視頻時鍾僅作為輔助的同步方案
--------------------------------*/
double get_video_clock(VideoState *is) {
double delta=(av_gettime()-is->video_current_pts_time)/1000000.0;
return is->video_current_pts+delta;//pts_of_last_frame+(Current_time-time_elapsed_since_pts_value_was_set)
}
//取得系統時間,以系統時鍾作為同步基准
double get_external_clock(VideoState *is) {
return av_gettime()/1000000.0;//取得系統當前時間,以1/1000000秒為單位,便於在各個平台移植
}
//取得主時鍾(基准時鍾)
double get_master_clock(VideoState *is) {
if (is->av_sync_type == AV_SYNC_VIDEO_MASTER) {//檢查主同步源類型
return get_video_clock(is);//返回視頻時鍾
} else if (is->av_sync_type == AV_SYNC_AUDIO_MASTER) {
return get_audio_clock(is);//返回音頻時鍾
} else {
return get_external_clock(is);//返回系統時鍾
}
}
//定時器觸發的回調函數
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;//SDL事件對象
event.type = FF_REFRESH_EVENT;//視頻顯示刷新事件
event.user.data1 = opaque;//傳遞用戶數據
SDL_PushEvent(&event);//發送事件
return 0; // 0 means stop timer.
}
/*---------------------------
* Schedule a video refresh in 'delay' ms.
* 設置一下幀播放的延遲
* 告訴系統在指定的延時后來推送一個FF_REFRESH_EVENT事件,起到類似於節拍器的作用
* 這個事件將在事件隊列里觸發sdl_refresh_timer_cb函數的調用
* @delay用於在圖像幀的解碼順序與渲染順序不一致的情況下,調節下一幀的渲染時機
* 從而盡可能的使所有圖像幀按照固定的幀率渲染刷新
--------------------------*/
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);//在指定的時間(ms)后回調用戶指定的函數
}
/*---------------------------
* 顯示刷新函數(FF_REFRESH_EVENT響應函數)
* 將視頻同步到主時鍾上,計算下一幀的延遲時間
* 使用當前幀的PTS和上一幀的PTS差來估計播放下一幀的延遲時間,並根據video的播放速度來調整這個延遲時間
---------------------------*/
void video_refresh_timer(void *userdata) {
VideoState *is = (VideoState *)userdata;//傳遞用戶數據
VideoPicture *vp;//圖像幀對象
//delay-前后兩幀顯示時間間隔([畫面-畫面]時差),diff-圖像幀顯示與音頻幀播放間的時間差
//sync_threshold-[畫面-畫面]最小時間差,actual_delay-當前幀-下已幀的顯示時間間隔(動態時間、真實時間、絕對時間)
double delay,diff,sync_threshold,actual_delay,ref_clock;
if (is->video_st) {//檢查全局狀態參數集中的視頻流信息結構體是否有效(是否已加載視頻文件)
if (is->pictq_size == 0) {//檢查圖像幀隊列中是否有等待顯示刷新的圖像
schedule_refresh(is, 1);//若隊列為空,則發送顯示刷新事件並再次進入video_refresh_timer函數
} else {
vp = &is->pictq[is->pictq_rindex];//從顯示隊列中取得等待顯示的圖像幀
is->video_current_pts = vp->pts;//取得當前幀的顯示時間戳
is->video_current_pts_time = av_gettime();//取得系統時間,作為當前幀播放的時間基准
//計算當前幀和前一幀顯示(pts)的間隔時間(顯示時間戳的差值)
delay = vp->pts - is->frame_last_pts;//The pts from last time,[畫面-畫面]時差
if (delay <= 0 || delay >= 1.0) {//檢查時間間隔是否在合理范圍
// If incorrect delay, use previous one.
delay = is->frame_last_delay;//沿用之前的動態刷新間隔時間
}
// Save for next time.
is->frame_last_delay = delay;//保存[上一幀圖像]的動態刷新延遲時間
is->frame_last_pts = vp->pts;//保存[上一幀圖像]的顯示時間戳
// Update delay to sync to audio,取得聲音播放時間戳(作為視頻同步的參考時間)
if (is->av_sync_type != AV_SYNC_VIDEO_MASTER) {//檢查主同步時鍾源
ref_clock = get_master_clock(is);//根據主時鍾來判斷Video播放的快慢,以主時鍾為基准時間
diff = vp->pts - ref_clock;//計算圖像幀顯示與主時鍾的時間差
//根據時間差調整播放下一幀的延遲時間,以實現同步 Skip or repeat the frame,Take delay into account
sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
//判斷音視頻不同步條件,即[畫面-聲音]時間差&[畫面-畫面]時間差<10ms閾值,若>該閾值則為快進模式,不存在音視頻同步問題
if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
if (diff <= -sync_threshold) {//慢了,delay設為0盡快顯示
//下一幀畫面顯示的時間和當前的聲音很近的話加快顯示下一幀(即后面video_display顯示完當前幀后開啟定時器很快去顯示下一幀
delay=0;
} else if (diff>=sync_threshold) {//快了,加倍delay
delay=2*delay;
}
}//如果diff(明顯)大於AV_NOSYNC_THRESHOLD,即快進的模式了,畫面跳動太大,不存在音視頻同步的問題了
}
//更新視頻播放到當前幀時的已播放時間值(所有圖像幀動態播放累計時間值-真實值),frame_timer一直累加在播放過程中我們計算的延時
is->frame_timer+=delay;
//每次計算frame_timer與系統時間的差值(以系統時間為基准時間),將frame_timer與系統時間(絕對時間)相關聯的目的
actual_delay=is->frame_timer-(av_gettime()/1000000.0);//Computer the REAL delay
if (actual_delay < 0.010) {//檢查絕對時間范圍
actual_delay = 0.010;// Really it should skip the picture instead
}
schedule_refresh(is,(int)(actual_delay*1000+0.5));//用絕對時間開定時器去動態顯示刷新下一幀
video_display(is);//刷新圖像,Show the picture
// update queue for next picture!.
if (++is->pictq_rindex==VIDEO_PICTURE_QUEUE_SIZE) {//更新圖像幀隊列讀索引位置
is->pictq_rindex = 0;//若讀索引抵達隊列尾,則重置讀索引位置
}
SDL_LockMutex(is->pictq_lock);//鎖定互斥量,保護畫布的像素數據
is->pictq_size--;//更新圖像幀隊列長度
SDL_CondSignal(is->pictq_ready);//發送隊列就緒信號
SDL_UnlockMutex(is->pictq_lock);//釋放互斥量
}
} else {//若視頻信息獲取失敗,則經過指定延時(100ms)后重新嘗試刷新視圖
schedule_refresh(is,100);
}
}
//設置[快進]/[快退]狀態參數
void stream_seek(VideoState *is, int64_t pos, int rel) {
if (!is->seek_req) {//檢查[快進]/[快退]操作標志位是否開啟
is->seek_pos = pos;//更新[快進]/[快退]后的參考時間戳
is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0;//確定[快進]還是[快退]操作
is->seek_req = 1;//開啟[快進]/[快退]標志位
}
}
//當音頻數據不為16位采樣格式情況下,采用decode_frame_from_packet計算解碼數據長度
int decode_frame_from_packet(VideoState *is, AVFrame decoded_frame) {
if (decoded_frame.channel_layout == 0) {
decoded_frame.channel_layout = av_get_default_channel_layout(decoded_frame.channels);
}
int src_nb_samples = decoded_frame.nb_samples;//一幀數據包含的pcm個數
int src_linesize = (int) decoded_frame.linesize;//掃描行數據長度
uint8_t **src_data = decoded_frame.data;//解碼后原始數據緩存指針
int src_rate = decoded_frame.sample_rate;//采樣率
int dst_rate = decoded_frame.sample_rate;
int64_t src_ch_layout = decoded_frame.channel_layout;
int64_t dst_ch_layout = decoded_frame.channel_layout;
enum AVSampleFormat src_sample_fmt = decoded_frame.format;
enum AVSampleFormat dst_sample_fmt = AV_SAMPLE_FMT_S16;
av_opt_set_int(is->sws_ctx_audio, "in_channel_layout", src_ch_layout, 0);
av_opt_set_int(is->sws_ctx_audio, "out_channel_layout", dst_ch_layout, 0);
av_opt_set_int(is->sws_ctx_audio, "in_sample_rate", src_rate, 0);
av_opt_set_int(is->sws_ctx_audio, "out_sample_rate", dst_rate, 0);
av_opt_set_sample_fmt(is->sws_ctx_audio, "in_sample_fmt", src_sample_fmt, 0);
av_opt_set_sample_fmt(is->sws_ctx_audio, "out_sample_fmt", dst_sample_fmt, 0);
int ret;//返回結果
// Initialize the resampling context.
if ((ret = swr_init((struct SwrContext *) is->sws_ctx_audio)) < 0) {
fprintf(stderr, "Failed to initialize the resampling context\n");
return -1;
}
// Allocate source and destination samples buffers.
int src_nb_channels=av_get_channel_layout_nb_channels(src_ch_layout);
ret=av_samples_alloc_array_and_samples(&src_data,&src_linesize,src_nb_channels,src_nb_samples,src_sample_fmt,0);
if (ret < 0) {
fprintf(stderr, "Could not allocate source samples\n");
return -1;
}
//Compute the number of converted samples: buffering is avoided ensuring that the output buffer will contain at least all the converted input samples.
int dst_nb_samples = av_rescale_rnd(src_nb_samples, dst_rate, src_rate, AV_ROUND_UP);
int max_dst_nb_samples = dst_nb_samples;
int dst_linesize;
uint8_t **dst_data = NULL;
// Buffer is going to be directly written to a rawaudio file, no alignment.
int dst_nb_channels = av_get_channel_layout_nb_channels(dst_ch_layout);
ret=av_samples_alloc_array_and_samples(&dst_data,&dst_linesize,dst_nb_channels,dst_nb_samples,dst_sample_fmt,0);
if (ret < 0) {
fprintf(stderr, "Could not allocate destination samples\n");
return -1;
}
//Compute destination number of samples.
dst_nb_samples=av_rescale_rnd(swr_get_delay((struct SwrContext*)is->sws_ctx_audio,src_rate)+src_nb_samples,dst_rate,src_rate,AV_ROUND_UP);
//Convert to destination format.
ret=swr_convert((struct SwrContext*)is->sws_ctx_audio,dst_data,dst_nb_samples,(const uint8_t **)decoded_frame.data,src_nb_samples);
if (ret<0) {
fprintf(stderr, "Error while converting\n");
return -1;
}
int dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels, ret, dst_sample_fmt, 1);
if (dst_bufsize < 0) {
fprintf(stderr, "Could not get sample buffer size\n");
return -1;
}
memcpy(is->audio_buf, dst_data[0], dst_bufsize);
if (src_data) {
av_freep(&src_data[0]);
}
av_freep(&src_data);
if (dst_data) {
av_freep(&dst_data[0]);
}
av_freep(&dst_data);
return dst_bufsize;
}
// These are called whenever we allocate a frame buffer. We use this to store the global_pts in a frame at the time it is allocated.
int our_get_buffer(struct AVCodecContext *c, AVFrame *pic, int flags) {
int ret = avcodec_default_get_buffer2(c, pic, 0);
uint64_t *pts = av_malloc(sizeof(uint64_t));
*pts = global_video_pkt_pts;
pic->opaque = pts;
return ret;
}
int main(int argc, char *argv[]) {
if (argc < 2) {//檢查輸入參數個數是否正確
fprintf(stderr, "Usage: test <file>\n");
exit(1);
}
av_register_all();// Register all formats and codecs,注冊所有多媒體格式及編解碼器
VideoState *is=av_mallocz(sizeof(VideoState));//創建全局狀態對象
av_strlcpy(is->filename, argv[1], 1024);//復制視頻文件路徑名
is->pictq_lock = SDL_CreateMutex();//創建編碼數據包隊列互斥鎖對象
is->pictq_ready = SDL_CreateCond();//創建編碼數據包隊列就緒條件對象
//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)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());//initialize the video audio & timer subsystem
exit(1);
}
// Make a screen to put our video,在SDL2.0中SDL_SetVideoMode及SDL_Overlay已經棄用,改為SDL_CreateWindow及SDL_CreateRenderer創建窗口及着色器
#ifndef __DARWIN__
screen = SDL_SetVideoMode(640, 480, 0, 0);//創建SDL窗口及繪圖表面,並指定圖像尺寸及像素格式
#else
screen = SDL_SetVideoMode(640, 480, 24, 0);//創建SDL窗口及繪圖表面,並指定圖像尺寸及像素格式
#endif
if (!screen) {//檢查SDL(繪圖表面)窗口是否創建成功(SDL用繪圖表面對象操作窗口)
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
schedule_refresh(is, 40);//在指定的時間(40ms)后回調用戶指定的函數,進行圖像幀的顯示更新
is->av_sync_type = DEFAULT_AV_SYNC_TYPE;//指定主同步源
is->parse_tid = SDL_CreateThread(parse_thread ,is);//創建編碼數據包解析線程
if (!is->parse_tid) {//檢查線程是否創建成功
av_free(is);
return -1;
}
av_init_packet(&flush_pkt);//初始化flush_pkt
//將flush_pkt的data成員指定為"FLUSH",當數據包隊列中某個包的data成員取值為"FLUSH",執行重置解碼器操作
flush_pkt.data = (unsigned char *) "FLUSH";
/*-----------------------
* SDL事件(消息)循環
* 播放器通過消息循環機制,不斷循環往復的觸發(驅動)圖像幀渲染操作,完成整個視頻文件的渲染
* 整個過程類似於按照指定節拍彈奏鋼琴,消息循環機制保證了視頻按照固定的節拍(6/8)播放
* 因為存在解碼順序與渲染順序不一致的情況(解碼B幀的情況),視頻同步機制保證了在圖像在解碼后都盡量按照固定節拍播放
* 在每次的消息響應函數video_refresh_timer中,重新計算下一幀的顯示時間
* 並通過schedule_refresh指定時間(類似於節拍器作用),觸發下一輪的圖像幀顯示
-----------------------*/
SDL_Event event;//SDL事件(消息)對象
for (;;) {
double incr, pos;
SDL_WaitEvent(&event);//Use this function to wait indefinitely for the next available event,主線程阻塞,等待事件到來
switch (event.type) {//事件到來后喚醒主線程后,檢查事件類型,執行相應操作
case SDL_KEYDOWN://檢查鍵盤操作事件
switch (event.key.keysym.sym) {//檢查鍵盤操作類型,which key get hit
case SDLK_LEFT://[左鍵]
incr = -10.0;//快退10s
goto do_seek;
case SDLK_RIGHT://[右鍵]
incr = 10.0;//快進10s
goto do_seek;
case SDLK_UP://[上鍵]
incr = 60.0;//快進60s
goto do_seek;
case SDLK_DOWN://[下鍵]
incr = -60.0;//快退60s
goto do_seek;
do_seek://處理請求
if (global_video_state) {
pos = get_master_clock(global_video_state);//取得當前主同步源時間戳
pos += incr;//根據鍵盤操作更新主同步源時間戳(AV_TIME_BASE為時間戳基准值)
stream_seek(global_video_state,(int64_t)(pos*AV_TIME_BASE),incr);//根據主同步源時間戳設置查找位置
}
break;
default:
break;
}
break;
case FF_QUIT_EVENT:
case SDL_QUIT://退出進程事件
is->quit = 1;
// If the video has finished playing, then both the picture and audio queues are waiting for more data. Make them stop waiting and terminate normally.
SDL_CondSignal(is->audioq.qready);//發出隊列就緒信號避免死鎖
SDL_CondSignal(is->videoq.qready);
SDL_Quit();
exit(0);
break;
case FF_ALLOC_EVENT://分配overlay事件
alloc_picture(event.user.data1);//分配overlay事件響應函數
break;
case FF_REFRESH_EVENT://視頻顯示刷新事件
video_refresh_timer(event.user.data1);//視頻顯示刷新事件響應函數
break;
default:
break;
}
}
return 0;
}
// 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
// 公眾號:斷點實驗室
// 掃描二維碼,關注更多優質原創,內容包括:音視頻開發、圖像處理、網絡、
// Linux,Windows、Android、嵌入式開發等
