ffplay播放器 暂停、逐帧、音量、快进快退seek功能分析

理论知识

ffplay常⽤的播放器操作按键:

•播放:程序启动即播放,处于暂停时通过p或空格键

•静⾳:m键

•⾳量+:0键

•⾳量-:9键

•暂停:p或空格键

•快退/快进:左箭头/右箭头 10s pgup/pgon 60s

•逐帧:s键

•退出:q或Esc键

•全屏:f或者⿏标左键双击

接下来我们对以上功能实现进行做出分析!

疑问

1.能一直接收按键命令,并做出对应输出,这是什么技术?

ffplay使用事件循环机制(epoll网络模型底层相似) 监听键盘输入不断取出事件,做出相应处理。

2.播放、暂停 是如何实现?

  1. 画⾯要停⽌
    1. 画⾯停留在最后⼀帧
  2. 声⾳要停⽌
    1. ⾳频回调接⼝请求数据帧时直接填0 即输出静音
  3. 读取数据是否要停⽌?
    1. ⾳视频包缓存队列满时进⼊休眠。
  4. 暂停->继续 :时钟的恢复 线程唤醒。

3. 逐帧、调⾳量、静⾳是如何实现?

  1. 逐帧
    1. 逐帧播放的本质是,播放⼀帧图像,然后暂停。
  2. 调⾳量
    1. ⾳量控制的本质:控制采样点的幅值
    2. 静⾳,将采样点数值置为0
    3. ⾳量+,提升采样点的幅值
    4. ⾳量-,降低采样点的幅值

 4.快进快退,seek是如何实现?

前置知识:

  1. 移动播放分按字节(MP4格式不支持,主要支持有流媒体格式,ts,flv mp3)
    1. 每秒字节数:bitrate/8  如果快进5s则 当前字节位置 + 5 * 比特率 / 8字节数
  2. 按时间移动 时间戳移动 
    1. avformat_seek_file  flag=AVSEEK_FLAG_ANY时,就是强时间移动。比如i帧是200ms 移动到202ms,不设置则跳到200ms i帧所在处 ,设置了则在202 ,i帧不合理那就会造成画面马赛克
  3. 注意:不同的容器(⽐如MP4和FLV)seek的机制是不⼀样的。有些容器seek的时间会快些,有些则相对 耗时。这个和容器的存储结构有关系。

具体操作: SDLK_LEFT:后退10秒

SDLK_RIGHT:前进10秒

SDLK_UP:前进60秒

SDLK_DOWN:后退60秒

SDL_MOUSEMOTION:⿏标右键按下,seek到指定的位置,

最终也是调⽤stream_seek()

快进、快退、seek在ffplay的实现是⼀样的。

  1. 快进和快退的本质是seek到某个点重新开始播放。
    1. 跳转到指定的数据位置avformat_seek_file
    2. 清空packet队列
    3. 清空frame队列(在ffplay⾥⾯是通过serial去控制)
    4. 插⼊flush_pkt以便冲刷解码器
    5. 切换时钟序列(ffplay)
    6. 清空解码器
api
用于在媒体文件中搜索和定位指定时间点的函数。在指定的时间范围内寻找
最接近目标时间戳的位置,并将解码器定位到该位置。

int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);

AVFormatContext *s: 要搜索的 AVFormatContext 上下文。
int stream_index: 要搜索的流的索引,如果设为 -1 表示搜索所有流。
int64_t min_ts: 允许搜索的最小时间戳。
int64_t ts: 目标时间戳。
int64_t max_ts: 允许搜索的最大时间戳。
int flags: 搜索标志位,可以是以下值的组合:
    AVSEEK_FLAG_BACKWARD: 优先向后搜索。
    AVSEEK_FLAG_BYTE: 以字节为单位搜索。
    AVSEEK_FLAG_ANY: 允许在任意关键帧上搜索。
    AVSEEK_FLAG_FRAME: 以帧为单位搜索。
返回值:
成功返回 0。
失败返回负错误码。

5.退出播放是如何实现?

•关闭流          •销毁队列资源         •销毁线程         •退出:do_exit()

源码阅读

有了前面理论指导,查看源码就方便很多了。

1.event_loop

//这里是摘抄代码,为了大家更好理解
static void event_loop(VideoState *cur_stream){
    SDL_Event event;
     for (;;) {
         refresh_loop_wait_event(cur_stream, &event);//注册输入设备监听事件,不断取出事件队列处理。
        switch (event.type) {
            case SDLK_PAGEUP: //up 60
                if (cur_stream->ic->nb_chapters <= 1) {
                    incr = 600.0;
                    goto do_seek;
                }
                seek_chapter(cur_stream, 1);
                break;
            case SDL_QUIT: //退出
            case FF_QUIT_EVENT:	/* ffplay自定义事件 */
                do_exit(cur_stream);
                break;
            default:
                break;
        }
     }
}

2播放、暂停 

调用栈

1 stream_toggle_pause ffplay.c 1680 0x40ad6c   暂停或恢复视频帧播放
2 toggle_pause        ffplay.c 1702 0x40aea0   暂停处理
3 event_loop          ffplay.c 3881 0x41160e 
4 main                ffplay.c 4370 0x412b10 

/// @brief 暂停处理
/// @param is 
static void toggle_pause(VideoState *is)
{
    stream_toggle_pause(is);  //底层调用
    is->step = 0;
    printf("is->step = 0; toggle_pause\n");
}

/// @brief 暂停或恢复视频帧播放
/// @param is 
static void stream_toggle_pause(VideoState *is)
{
    if (is->paused) {
        /* 在恢复播放时,更新视频时钟(vidclk)的状态和时间。
         * 为什么要做这一步,因为默认是以音频为基准做同步,当暂停播放很长时间,视频时钟会增加
         * 虽然它做了动态frame_time 但是暂停时间很长,那动态frame_time值就很大 平均帧就很多
         * compute_target_delay 帧追赶调小,暂停长就能演示出bug,画面不动 音频在动。
 */
        is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
        if (is->read_pause_return != AVERROR(ENOSYS)) {
            is->vidclk.paused = 0;
        }
        set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
    }
    //更新外部时钟
    set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
    // 切换 pause/resume 两种状态
    is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
    printf("is->step = %d; stream_toggle_pause\n", is->step);
}

以上两个api主要是对is->paused标志位设置,真正暂停是在视频渲染和音频输出中。
这里简略的来看看是如何取消掉视频渲染和音频输出。

static int audio_decode_frame(VideoState *is)
     if (is->paused)
        return -1;

在暂停状态下,实际就是不停播放上⼀帧(最后⼀帧)图像。画⾯不更新。
static void video_refresh(void *opaque, double *remaining_time)
        if (is->paused)
        {
           goto display;
           printf("视频暂停is->paused");
        }

3. 逐帧、调⾳量、静⾳

逐帧

eventloop里的case环节

case SDLK_s: // S: Step to next frame
                step_to_next_frame(cur_stream);
                break;

/// @brief 播放逐帧播放逻辑
/// @param is 
static void step_to_next_frame(VideoState *is)
{
    /* 如果流已暂停,则取消暂停,然后执行step */
    if (is->paused)
        stream_toggle_pause(is); 
    is->step = 1;
    printf("is->step = 1; step_to_next_frame\n");
}

这也是将is->step设置flag,
static void video_refresh(void *opaque, double *remaining_time){
          frame_queue_next(&is->pictq);   // 当前vp帧出队列
          is->force_refresh = 1;          /* 说明需要刷新视频帧 */

          if (is->step && !is->paused)
                stream_toggle_pause(is); // 逐帧的时候那继续进入暂停状态
        
display:
        /* display picture 尽管是暂停状态 is->step=1不影响video_display */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is);// 重点是显示
}

调⾳量、静⾳

case SDLK_m: //禁音
                toggle_mute(cur_stream);
                break;

case SDLK_KP_MULTIPLY:

 case SDLK_0:
                update_volume(cur_stream, 1, SDL_VOLUME_STEP);
                break;
case SDLK_KP_DIVIDE:
  case SDLK_9:
                update_volume(cur_stream, -1, SDL_VOLUME_STEP);
                break;

/// @brief 增加或减少音量  
/// @param is 
/// @param sign 
/// @param step 
static void update_volume(VideoState *is, int sign, double step)
{
    double volume_level = is->audio_volume ? (20 * log(is->audio_volume / (double)SDL_MIX_MAXVOLUME) / log(10)) : -1000.0;
    int new_volume = lrint(SDL_MIX_MAXVOLUME * pow(10.0, (volume_level + sign * step) / 20.0));
    //(0,128)区间
    is->audio_volume = av_clip(is->audio_volume == new_volume ? (is->audio_volume + sign) : new_volume, 0, SDL_MIX_MAXVOLUME);
}

/// @brief 静音逻辑
/// @param is 
static void toggle_mute(VideoState *is)
{
    is->muted = !is->muted;
}

以上也是将特定标志位设置
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){
...
/* 判断是否为静音,以及当前音量的大小,如果音量为最大则直接拷贝数据 */
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)//音量为最大
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            // 3.调整音量 
            /* 如果处于mute状态则直接使用stream填0数据, 暂停时is->audio_buf = NULL */
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index,
                                   AUDIO_S16SYS, len1, is->audio_volume);
        }
...
}
    

 4快进快退,seek

seek事件循环

case SDLK_PAGEUP:
                if (cur_stream->ic->nb_chapters <= 1) {
                    incr = 600.0;
                    goto do_seek;
                }
                seek_chapter(cur_stream, 1);
                break;
case SDLK_PAGEDOWN:
                if (cur_stream->ic->nb_chapters <= 1) {
                    incr = -600.0;
                    goto do_seek;
                }
                seek_chapter(cur_stream, -1);
                break;
case SDLK_LEFT:
                incr = seek_interval ? -seek_interval : -10.0;
                goto do_seek;
case SDLK_RIGHT:
                incr = seek_interval ? seek_interval : 10.0;
                goto do_seek;
case SDLK_UP:
                incr = 60.0;
                goto do_seek;
case SDLK_DOWN:
                incr = -60.0;
do_seek:
                if (seek_by_bytes) {//字节移动
                    pos = -1;
                    if (pos < 0 && cur_stream->video_stream >= 0)
                        pos = frame_queue_last_pos(&cur_stream->pictq);//返回最后显示的字节位置
                    if (pos < 0 && cur_stream->audio_stream >= 0)
                        pos = frame_queue_last_pos(&cur_stream->sampq);
                    if (pos < 0)
                        pos = avio_tell(cur_stream->ic->pb);
                    if (cur_stream->ic->bit_rate)
                        incr *= cur_stream->ic->bit_rate / 8.0; //求每秒字节数
                    else
                        incr *= 180000.0;
                    pos += incr; //移动n字节
                    stream_seek(cur_stream, pos, incr, 1);
                } else {//时钟移动
                    pos = get_master_clock(cur_stream);
                    if (isnan(pos))
                        pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
                    pos += incr;
                    if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
                        pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
                    stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
                }
                break;
            default:
                break;
            }
            break;


case SDL_MOUSEMOTION:		/* 鼠标移动事件 */
            if (cursor_hidden) {
                SDL_ShowCursor(1);
                cursor_hidden = 0;
            }
            cursor_last_shown = av_gettime_relative();
            if (event.type == SDL_MOUSEBUTTONDOWN) {
                if (event.button.button != SDL_BUTTON_RIGHT)
                    break;
                x = event.button.x;
            } else {
                if (!(event.motion.state & SDL_BUTTON_RMASK))
                    break;
                x = event.motion.x;
            }
            if (seek_by_bytes || cur_stream->ic->duration <= 0) {
                uint64_t size =  avio_size(cur_stream->ic->pb);
                stream_seek(cur_stream, size*x/cur_stream->width, 0, 1);
            } else {
                int64_t ts;
                int ns, hh, mm, ss;
                int tns, thh, tmm, tss;
                tns  = cur_stream->ic->duration / 1000000LL;
                thh  = tns / 3600;
                tmm  = (tns % 3600) / 60;
                tss  = (tns % 60);
                frac = x / cur_stream->width;
                ns   = frac * tns;
                hh   = ns / 3600;
                mm   = (ns % 3600) / 60;
                ss   = (ns % 60);
                av_log(NULL, AV_LOG_INFO,
                       "Seek to %2.0f%% (%2d:%02d:%02d) of total duration (%2d:%02d:%02d)       \n", frac*100,
                       hh, mm, ss, thh, tmm, tss);
                ts = frac * cur_stream->ic->duration;
                if (cur_stream->ic->start_time != AV_NOPTS_VALUE)
                    ts += cur_stream->ic->start_time;
                stream_seek(cur_stream, ts, 0, 0);
            }
            break;

数据结构及SEEK标志
typedef struct VideoState{
...
    int     seek_req;           // 标识一次seek请求
    int     seek_flags;         // seek标志,诸如AVSEEK_FLAG_BYTE等
    int64_t     seek_pos;       // 请求seek的目标位置(当前位置+增量)
    int64_t     seek_rel;       // 本次seek的位置增量
...

}VideoState;

标志位以及相关变量设置

底层都是调⽤stream_seek()

/**
 * @brief 
 * @param is
 * @param pos  具体seek到的位置
 * @param rel  增量情况
 * @param seek_by_bytes
 */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
    if (!is->seek_req) {
        is->seek_pos = pos; // 按时间微秒,按字节 byte
        is->seek_rel = rel;
        is->seek_flags &= ~AVSEEK_FLAG_BYTE;        // 不按字节的方式去seek
        if (seek_by_bytes)
            is->seek_flags |= AVSEEK_FLAG_BYTE;     // 强制按字节的方式去seek
        is->seek_req = 1;       // 请求seek, 在read_thread线程seek成功才将其置为0
        SDL_CondSignal(is->continue_read_thread);
    }
}

SEEK操作的实现

 在解复⽤线程主循环中处理了SEEK操作。

static int read_thread(void *arg){
...
    /*
     * 二、For循环流程
    */
    for (;;) {
...
        //  3 检测是否seek
        if (is->seek_req) { // 是否有seek请求
//计算 seek 的目标时间点  seek_min 和 seek_max 分别是 seek 操作的最小和最大时间点范围。 主要是为了修复由于四舍五入导致的精度问题
            int64_t seek_target = is->seek_pos;
            int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
            int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
            //执行 seek 操作
            ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR,
                       "%s: error while seeking\n", is->ic->url);
            } else {//冲洗
                /* seek的时候,要把原先的数据情况,并重启解码器,
put flush_pkt的目的是告知解码线程需要reset decoder
                 */
                if (is->audio_stream >= 0) { // 如果有音频流
                    packet_queue_flush(&is->audioq);    // 清空packet队列数据
                    // 放入flush pkt, 用来开起新的一个播放序列, 解码器读取到flush_pkt也清空解码器
                    packet_queue_put(&is->audioq, &flush_pkt);
                }
                if (is->subtitle_stream >= 0) { // 如果有字幕流
                    packet_queue_flush(&is->subtitleq); // 和上同理
                    packet_queue_put(&is->subtitleq, &flush_pkt);
                }
                if (is->video_stream >= 0) {    // 如果有视频流
                    packet_queue_flush(&is->videoq);    // 和上同理
                    packet_queue_put(&is->videoq, &flush_pkt);
                }
//更新pts
                if (is->seek_flags & AVSEEK_FLAG_BYTE) {
                    set_clock(&is->extclk, NAN, 0);
                } else {
                    set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
                }
            }
            //更新播放状态
            is->seek_req = 0; //seek 请求已处理完成 清除本次SEEK请求标志
            is->queue_attachments_req = 1; //表示需要重新请求附件数据
            is->eof = 0;
            if (is->paused)
                step_to_next_frame(is); //暂停时 seek播放一帧
        }

...
}

5.退出播放

 case SDL_QUIT:
 case FF_QUIT_EVENT:    /* ffplay自定义事件 */
            do_exit(cur_stream);
            break;


static void do_exit(VideoState *is)
{
    if (is) {
        stream_close(is); //流关闭
    }
    if (renderer)
        SDL_DestroyRenderer(renderer); //关闭渲染器
    if (window)
        SDL_DestroyWindow(window); //关闭窗口
    uninit_opts(); //关闭AVDictionary
#if CONFIG_AVFILTER
    av_freep(&vfilters_list);  //关闭过滤器
#endif
    avformat_network_deinit();
    if (show_status)
        printf("\n");
    SDL_Quit();
    av_log(NULL, AV_LOG_QUIET, "%s", "");
    exit(0);
}

总结

本文章由理论结合实践阅读ffplay播放器源代码,清晰的将ffplay各种复杂功能解剖分析。

学习资料分享

0voice · GitHub

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值