學習開源播放器代碼解析之參數設置

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_paramsparse_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 = wrh = 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與起始時間戳的差值。其他情況表示正常,返回時間戳posstart_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;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章