1.前言
出於學習音視頻的目的,在
Github
找了個基於FFMPEG
的播放器代碼,代碼量比較小。地址:fflayer。於是乎下載編譯了下,運行結果良好。So,出於學習的目的,寫寫學習筆記,歸納歸納。該開源代碼使用的ffmpeg
函數有些被標記成過時的換成最新的會出現閃屏以及看直播時視頻聲音不同步等各種問題。在後續解析完再慢慢琢磨怎麼解決這些問題以及自己可以嘗試寫個簡易播放器加深理解。
2.源碼分析
鑑於個人的習慣,從點到面學習一個工程,首先分析下該工程中參數設置。工程中的主入口ffplayer.cpp
,那麼我們也從這個文件入手分。參數相關的有三個函數:
//在創建播放器的時候會一同傳入,用於配置播放器的各個參數。
void player_load_params(PLAYER_INIT_PARAMS *params, char *str)
//設置參數
void player_setparam(void *hplayer, int id, void *param)
//獲取參數
void player_getparam(void *hplayer, int id, void *param)
2.1加載參數
先看看代碼:
void player_load_params(PLAYER_INIT_PARAMS *params, char *str)
{
params->video_stream_cur = parse_params(str, "video_stream_cur" );
params->video_thread_count = parse_params(str, "video_thread_count" );
params->video_hwaccel = parse_params(str, "video_hwaccel" );
params->video_deinterlace = parse_params(str, "video_deinterlace" );
params->video_rotate = parse_params(str, "video_rotate" );
params->audio_stream_cur = parse_params(str, "audio_stream_cur" );
params->subtitle_stream_cur = parse_params(str, "subtitle_stream_cur");
params->vdev_render_type = parse_params(str, "vdev_render_type" );
params->adev_render_type = parse_params(str, "adev_render_type" );
params->init_timeout = parse_params(str, "init_timeout" );
params->open_syncmode = parse_params(str, "open_syncmode" );
}
參數str
是傳入的配置字符串,經過函數parse_params
的解析將參數值賦值給params
。那麼接下來看看parse_params
函數:
//標註1 "video_hwaccel=1;video_rotate=0"
static int parse_params(const char *str, const char *key)
{
char t[12];
char *p = (char*)strstr(str, key);
int i;
//標註2 得到"=1;video_rotate=0"
if (!p) return 0;
p += strlen(key);
if (*p == '\0') return 0;
while (1) {
if (*p != ' ' && *p != '=' && *p != ':') break;
else p++;
}
//標註3 "1;video_rotate=0"
for (i=0; i<12; i++) {
if (*p == ',' || *p == ';' || *p == '\n' || *p == '\0') {
t[i] = '\0';
break;
} else {
t[i] = *p++;
}
}
t[11] = '\0';
//標註4 "1\0"
//把字符串轉換成整型數
return atoi(t);
}
首先demo裏傳入的值是"video_hwaccel=1;video_rotate=0"
。
先返回player_load_params
看下,調了11次parse_params
。除了parse_params(str, "video_hwaccel" )
和parse_params(str, "video_rotate" )
,其他在標註2處直接返回。
先拿parse_params(str, "video_hwaccel" )
出來分析。在標註2處經過strstr
函數過濾出"=1;video_rotate=0"
。
在標註3處過濾掉空格、冒號、等號,得到"1;video_rotate=0"
。在標註4出將之前得到的第一個有效數字過濾出來存在t字符中並在最後加上’\0’結束符並利用atoi
轉成整數後返回。
parse_params(str, "video_rotate" )
也是同樣的道理。於是player_load_params
中parse_params(str, "video_hwaccel" )
和parse_params(str, "video_rotate" )
分別得到了1和0並賦值給params
對應的變量。而其他的得到的還是空。
2.1設置參數
先看下該函數代碼:
void player_setparam(void *hplayer, int id, void *param) {
if (!hplayer) return;
PLAYER *player = (PLAYER *) hplayer;
switch (id) {
case PARAM_VIDEO_MODE:
player->vdmode = *(int *) param;
player_setrect(hplayer, 0,
player->vdrect.left, player->vdrect.top,
player->vdrect.right - player->vdrect.left,
player->vdrect.bottom - player->vdrect.top);
break;
default:
render_setparam(player->render, id, param);
break;
}
}
2.1.1設置屏幕尺寸
當id是PARAM_VIDEO_MODE
的時候,主要還是調用player_setrect
來實現。看下player_setrect
代碼:
void player_setrect(void *hplayer, int type, int x, int y, int w, int h) {
if (!hplayer) return;
PLAYER *player = (PLAYER *) hplayer;
***
int vw = player->init_params.video_owidth;
int vh = player->init_params.video_oheight;
int rw = 0, rh = 0;
if (!vw || !vh) return;
***
//標註1
switch (player->vdmode) {
case VIDEO_MODE_LETTERBOX:
if (w * vh < h * vw) {
rw = w;
rh = rw * vh / vw;
}
else {
rh = h;
rw = rh * vw / vh;
}
break;
case VIDEO_MODE_STRETCHED:
rw = w;
rh = h;
break;
}
if (rw <= 0) rw = 1;
if (rh <= 0) rh = 1;
render_setrect(player->render, type, x + (w - rw) / 2, y + (h - rh) / 2, rw, rh);
}
先明確下這幾個變量的定義:
w,h
表示想要設置的寬和長
rw,rh
表示實際要渲染的寬和長
vw,vh
表示想要解碼出視頻的寬和長
再看下代碼,其他部分都是簡單的賦值。重點看下標註1處按我理解應該VIDEO_MODE_LETTERBOX表示按比例伸縮而VIDEO_MODE_STRETCHED是按原尺寸輸出。
-
VIDEO_MODE_STRETCHED比較簡單
rw = w
和rh = h
,則接下來就是render_setrect(player->render, type, x, y , w, h)
。也就是說按照設置的尺寸顯示在屏幕上。 -
VIDEO_MODE_LETTERBOX的話有一點邏輯,比如說
if(w * vh < h * vw)
的情況,換一種寫法更清晰。if(w/h<vw/vh)
,也就是想要設置的寬長比小於視頻的寬長比。那麼將要設置的w賦值給rw
,而高度根據解碼出視頻的寬比進行壓縮。然後接下來的render_setrect
函數中,x + (w - rw) / 2
也就是將渲染的區域居中在想要設置的區域居中。整體流程如下圖所示:
2.1.1設置其他參數
這裏主要介紹一下設置速度和聲音,其他貌似也沒用到。先不管,函數如下:
void render_setparam(void *hrender, int id, void *param)
{
if (!hrender) return;
RENDER *render = (RENDER*)hrender;
switch (id)
{
case PARAM_AUDIO_VOLUME:
adev_setparam(render->adev, id, param);
break;
case PARAM_PLAY_SPEED:
render_setspeed(render, *(int*)param);
break;
***
}
}
設置聲音:adev_setparam
,這個簡單。只是往變量裏填充聲音數值。
設置速度:代碼如下:
// 內部函數實現
static void render_setspeed(RENDER *render, int speed)
{
if (speed > 0) {
// set vdev frame rate
int framerate = (int)((render->frame_rate.num * speed) / (render->frame_rate.den * 100.0) + 0.5);
vdev_setparam(render->vdev, PARAM_VDEV_FRAME_RATE, &framerate);
// set render_speed_new to triger swr_context re-create
render->render_speed_new = speed;
}
}
void vdev_setparam(void *ctxt, int id, void *param)
{
if (!ctxt || !param) return;
VDEV_COMMON_CTXT *c = (VDEV_COMMON_CTXT*)ctxt;
switch (id) {
case PARAM_VDEV_FRAME_RATE:
c->tickframe = 1000 / (*(int*)param > 1 ? *(int*)param : 1);
break;
***
}
if (c->setparam) c->setparam(c, id, param);
}
默認的情況下speed是100,也就是說默認情況下int framerate = (int)(render->frame_rate.num / (render->frame_rate.den + 0.5)
。至於爲什麼是這兩個東西,跟同步有關係放在分析同步的時候再說。接下來調用vdev_setparam
函數。將得到的乘以1000也就是得到毫秒數賦值給c->tickframe
,以供後續同步的時候用。
2.2獲取參數
先看看代碼:
void player_getparam(void *hplayer, int id, void *param) {
if (!hplayer || !param) return;
PLAYER *player = (PLAYER *) hplayer;
switch (id) {
case PARAM_MEDIA_DURATION:
if (!player->avformat_context) *(int64_t *) param = 1;
else *(int64_t *) param = (player->avformat_context->duration * 1000 / AV_TIME_BASE);
if (*(int64_t *) param <= 0) *(int64_t *) param = 1;
break;
case PARAM_MEDIA_POSITION:
if ((player->player_status & PS_F_SEEK) ||
(player->player_status & player->seek_req) == player->seek_req) {
*(int64_t *) param = player->seek_dest - player->start_pts;
} else {
int64_t pos = 0;
render_getparam(player->render, id, &pos);
switch (pos) {
case -1:
*(int64_t *) param = -1;
break;
case AV_NOPTS_VALUE:
*(int64_t *) param = player->seek_dest - player->start_pts;
break;
default:
*(int64_t *) param = pos - player->start_pts;
break;
}
}
break;
case PARAM_VIDEO_WIDTH:
if (!player->vcodec_context) *(int *) param = 0;
else *(int *) param = player->init_params.video_owidth;
break;
case PARAM_VIDEO_HEIGHT:
if (!player->vcodec_context) *(int *) param = 0;
else *(int *) param = player->init_params.video_oheight;
break;
case PARAM_VIDEO_MODE:
*(int *) param = player->vdmode;
break;
case PARAM_RENDER_GET_CONTEXT:
*(void **) param = player->render;
break;
default:
render_getparam(player->render, id, param);
break;
}
}
void render_getparam(void *hrender, int id, void *param)
{
if (!hrender) return;
RENDER *render = (RENDER*)hrender;
VDEV_COMMON_CTXT *vdev = (VDEV_COMMON_CTXT*)render->vdev;
switch (id)
{
case PARAM_MEDIA_POSITION:
if (vdev->status & VDEV_COMPLETED) {
*(int64_t*)param = -1; // means completed
} else {
*(int64_t*)param = vdev->apts > vdev->vpts ? vdev->apts : vdev->vpts;
}
break;
***
}
}
- 獲取播放時間總長度:當
PARAM_MEDIA_DURATION
的時候,player->avformat_context->duration / AV_TIME_BASE
表示總時間是秒,乘以1000將返回的值轉爲毫秒。 - 獲取長、寬以及播放模式:從player獲取參數並直接返回。
- 獲取當前播放位置:當正在拖動的時候或者正在請求拖動的時候,返回
seek_dest
與起始時間戳的差值。當正常播放的時候,先調用render_getparam
函數。播放完成那麼返回-1,還沒完成就返回音頻或視頻時間戳裏比較快的那個。回到player_getparam
方法,pos
如果返回-1,則給param
賦值-1.如果AV_NOPTS_VALUE
,則返回seek_dest
與起始時間戳的差值。其他情況表示正常,返回時間戳pos
與start_pts
的差值。 - 獲取其他的參數直接調用
render_getparam
函數,都是從render中獲取參數。
void render_getparam(void *hrender, int id, void *param)
{
if (!hrender) return;
RENDER *render = (RENDER*)hrender;
VDEV_COMMON_CTXT *vdev = (VDEV_COMMON_CTXT*)render->vdev;
switch (id)
{
***
case PARAM_AUDIO_VOLUME:
adev_getparam(render->adev, id, param);
break;
case PARAM_PLAY_SPEED:
*(int*)param = render->render_speed_cur;
break;
case PARAM_AVSYNC_TIME_DIFF:
case PARAM_VDEV_GET_D3DDEV:
vdev_getparam(render->vdev, id, param);
break;
case PARAM_ADEV_GET_CONTEXT:
*(void**)param = render->adev;
break;
case PARAM_VDEV_GET_CONTEXT:
*(void**)param = render->vdev;
break;
}
}