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;
}
}