• ffplay源码分析:播放控制


    1. 播放控制

    1.1. 暂停/继续

    暂停/继续状态的切换是由用户按空格键实现的,每按一次空格键,暂停/继续的状态翻转一次。

    1.1.1 暂停/继续状态切换

    函数调用关系如下:

    1. main() -->
    2. event_loop() -->
    3. toggle_pause() -->
    4. stream_toggle_pause()

    stream_toggle_pause()实现状态翻转:

    1. /* pause or resume the video */
    2. static void stream_toggle_pause(VideoState *is)
    3. {
    4. if (is->paused) {
    5. // 这里表示当前是暂停状态,将切换到继续播放状态。在继续播放之前,先将暂停期间流逝的时间加到frame_timer中
    6. is->frame_timer += av_gettime_relative() / 1000000.0 - is->vidclk.last_updated;
    7. if (is->read_pause_return != AVERROR(ENOSYS)) {
    8. is->vidclk.paused = 0;
    9. }
    10. set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);
    11. }
    12. set_clock(&is->extclk, get_clock(&is->extclk), is->extclk.serial);
    13. is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
    14. }

    1.1.2 暂停状态下的视频播放

    在video_refresh()函数中有如下代码:

    1. /* called to display each frame */
    2. static void video_refresh(void *opaque, double *remaining_time)
    3. {
    4. ......
    5. // 视频播放
    6. if (is->video_st) {
    7. ......
    8. // 暂停处理:不停播放上一帧图像
    9. if (is->paused)
    10. goto display;
    11. ......
    12. }
    13. ......
    14. }

    在暂停状态下,实际就是不停播放上一帧(最后一帧)图像。画面不更新。

    本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓

     

    1.2 逐帧播放

    逐帧播放是用户每按一次s键,播放器播放一帧画现。 逐帧播放实现的方法是:每次按了s键,就将状态切换为播放,播放一帧画面后,将状态切换为暂停。 函数调用关系如下:

    1. main() -->
    2. event_loop() -->
    3. step_to_next_frame() -->
    4. stream_toggle_pause()

    实现代码比较简单,如下:

    1. static void step_to_next_frame(VideoState *is)
    2. {
    3. /* if the stream is paused unpause it, then step */
    4. if (is->paused)
    5. stream_toggle_pause(is); // 确保切换到播放状态,播放一帧画面
    6. is->step = 1;
    7. }
    1. /* called to display each frame */
    2. static void video_refresh(void *opaque, double *remaining_time)
    3. {
    4. ......
    5. // 视频播放
    6. if (is->video_st) {
    7. ......
    8. if (is->step && !is->paused)
    9. stream_toggle_pause(is); // 逐帧播放模式下,播放一帧画面后暂停
    10. ......
    11. }
    12. ......
    13. }

    1.3 播放速度控制

    待补充

    1.4. SEEK操作

    SEEK操作就是由用户干预而改变播放进度的实现方式,比如鼠标拖动播放进度条。

    1.4.1 数据结构及SEEK标志

    相关数据变量定义如下:

    1. typedef struct VideoState {
    2. ......
    3. int seek_req; // 标识一次SEEK请求
    4. int seek_flags; // SEEK标志,诸如AVSEEK_FLAG_BYTE等
    5. int64_t seek_pos; // SEEK的目标位置(当前位置+增量)
    6. int64_t seek_rel; // 本次SEEK的位置增量
    7. ......
    8. } VideoState;

    “VideoState.seek_flags”表示SEEK标志。SEEK标志的类型定义如下:

    1. #define AVSEEK_FLAG_BACKWARD 1 ///< seek backward
    2. #define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
    3. #define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
    4. #define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number

    SEEK目标播放点(后文简称SEEK点)的确定,根据SEEK标志的不同,分为如下几种情况: [1]. AVSEEK_FLAG_BYTE:SEEK点对应文件中的位置(字节表示)。有些解复用器可能不支持这种情况。 [2]. AVSEEK_FLAG_FRAME:SEEK点对应stream中frame序号(?frame序号还是frame 的PTS?),stream由stream_index指定。有些解复用器可能不支持这种情况。 [3]. 如果不含上述两种标志且stream_index有效:SEEK点对应时间戳,单位是stream中的timebase,stream由stream_index指定。SEEK点的值由“目标frame中的pts(秒) × stream中的timebase”得到。 [4]. 如果不含上述两种标志且stream_index是-1:SEEK点对应时间戳,单位是AV_TIME_BASE。SEEK点的值由“目标frame中的pts(秒) × AV_TIME_BASE”得到。 [5]. AVSEEK_FLAG_ANY:SEEK点对应帧序号(待确定),播放点可停留在任意帧(包括非关键帧)。有些解复用器可能不支持这种情况。 [6]. AVSEEK_FLAG_BACKWARD:忽略。

    其中AV_TIME_BASE是FFmpeg内部使用的时间基,定义如下:

    1. /**
    2. * Internal time base represented as integer
    3. */
    4. #define AV_TIME_BASE 1000000

    AV_TIME_BASE表示1000000us。

    1.4.2 SEEK的触发方式

    当用户按下“PAGEUP”,“PAGEDOWN”,“UP”,“DOWN”,“LEFT”,“RHIGHT”按键以及用鼠标拖动进度条时,引起播放进度变化,会触发SEEK操作。 在event_loop()函数进行的SDL消息处理中有如下代码片段:

    1. case SDLK_LEFT:
    2. incr = seek_interval ? -seek_interval : -10.0;
    3. goto do_seek;
    4. case SDLK_RIGHT:
    5. incr = seek_interval ? seek_interval : 10.0;
    6. goto do_seek;
    7. case SDLK_UP:
    8. incr = 60.0;
    9. goto do_seek;
    10. case SDLK_DOWN:
    11. incr = -60.0;
    12. do_seek:
    13. if (seek_by_bytes) {
    14. pos = -1;
    15. if (pos < 0 && cur_stream->video_stream >= 0)
    16. pos = frame_queue_last_pos(&cur_stream->pictq);
    17. if (pos < 0 && cur_stream->audio_stream >= 0)
    18. pos = frame_queue_last_pos(&cur_stream->sampq);
    19. if (pos < 0)
    20. pos = avio_tell(cur_stream->ic->pb);
    21. if (cur_stream->ic->bit_rate)
    22. incr *= cur_stream->ic->bit_rate / 8.0;
    23. else
    24. incr *= 180000.0;
    25. pos += incr;
    26. stream_seek(cur_stream, pos, incr, 1);
    27. } else {
    28. pos = get_master_clock(cur_stream);
    29. if (isnan(pos))
    30. pos = (double)cur_stream->seek_pos / AV_TIME_BASE;
    31. pos += incr;
    32. if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)
    33. pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;
    34. stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);
    35. }
    36. break;

    seek_by_bytes生效(对应AVSEEK_FLAG_BYTE标志)时,SEEK点对应文件中的位置,上述代码中设置了对应1秒数据量的播放增量;不生效时,SEEK点对应于播放时刻。我们暂不考虑seek_by_bytes生效这种情况。
    此函数实现如下功能:
    [1]. 首先确定SEEK操作的播放进度增量(SEEK增量)和目标播放点(SEEK点),seek_by_bytes不生效时,将增量设为选定值,如10.0秒(用户按“RHIGHT”键的情况)。
    [2]. 将同步主时钟加上进度增量,即可得到SEEK点。先将相关数值记录下来,供后续SEEK操作时使用。stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);就是记录目标播放点和播放进度增量两个参数的,精确到微秒。调用这个函数的前提是,我们只考虑8.1节中的第[4]种情况。

    再看一下stream_seak()函数的实现,仅仅是变量赋值:

    1. /* seek in the stream */
    2. static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
    3. {
    4. if (!is->seek_req) {
    5. is->seek_pos = pos;
    6. is->seek_rel = rel;
    7. is->seek_flags &= ~AVSEEK_FLAG_BYTE;
    8. if (seek_by_bytes)
    9. is->seek_flags |= AVSEEK_FLAG_BYTE;
    10. is->seek_req = 1;
    11. SDL_CondSignal(is->continue_read_thread);
    12. }
    13. }

    1.4.3 SEEK操作的实现

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

    1. static int read_thread(void *arg)
    2. {
    3. ......
    4. for (;;) {
    5. if (is->seek_req) {
    6. int64_t seek_target = is->seek_pos;
    7. int64_t seek_min = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
    8. int64_t seek_max = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
    9. // FIXME the +-2 is due to rounding being not done in the correct direction in generation
    10. // of the seek_pos/seek_rel variables
    11. ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
    12. if (ret < 0) {
    13. av_log(NULL, AV_LOG_ERROR,
    14. "%s: error while seeking\n", is->ic->url);
    15. } else {
    16. if (is->audio_stream >= 0) {
    17. packet_queue_flush(&is->audioq);
    18. packet_queue_put(&is->audioq, &flush_pkt);
    19. }
    20. if (is->subtitle_stream >= 0) {
    21. packet_queue_flush(&is->subtitleq);
    22. packet_queue_put(&is->subtitleq, &flush_pkt);
    23. }
    24. if (is->video_stream >= 0) {
    25. packet_queue_flush(&is->videoq);
    26. packet_queue_put(&is->videoq, &flush_pkt);
    27. }
    28. if (is->seek_flags & AVSEEK_FLAG_BYTE) {
    29. set_clock(&is->extclk, NAN, 0);
    30. } else {
    31. set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
    32. }
    33. }
    34. is->seek_req = 0;
    35. is->queue_attachments_req = 1;
    36. is->eof = 0;
    37. if (is->paused)
    38. step_to_next_frame(is);
    39. }
    40. }
    41. ......
    42. }

    上述代码中的SEEK操作执行如下步骤:
    [1]. 调用avformat_seek_file()完成解复用器中的SEEK点切换操作

    1. // 函数原型
    2. int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);
    3. // 调用代码
    4. ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);

    这个函数会等待SEEK操作完成才返回。实际的播放点力求最接近参数ts,并确保在[min_ts, max_ts]区间内,之所以播放点不一定在ts位置,是因为ts位置未必能正常播放。
    函数与SEEK点相关的三个参数(实参“seek_min”,“seek_target”,“seek_max”)取值方式与SEEK标志有关(实参“is->seek_flags”),此处“is->seek_flags”值为0,对应7.4.1节中的第[4]中情况。
    [2]. 冲洗各解码器缓存帧,使当前播放序列中的帧播放完成,然后再开始新的播放序列(播放序列由各数据结构中的“serial”变量标志,此处不展开)。代码如下:

    1. if (is->video_stream >= 0) {
    2. packet_queue_flush(&is->videoq);
    3. packet_queue_put(&is->videoq, &flush_pkt);
    4. }

    [3]. 清除本次SEEK请求标志is->seek_req = 0;

    本文福利, C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg webRTC rtmp hls rtsp ffplay srs↓↓↓↓↓↓见下面↓↓文章底部点击领取↓↓

  • 相关阅读:
    应对数据安全典型薄弱点,这家医院“外防内控”筑牢屏障
    没有上司的舞会 - 树形DP
    stable diffusion webui安装部署教程
    [算法日志]图论: 广度优先搜索(BFS)
    springboot项目使用拦截器和注解方式验证token
    TCPIP网络编程 学习笔记_1 --网络编程入门
    navicate安装教程
    QGIS安装(以Windows系统为例)
    每日一题《leetcode-- LCR 025.两数相加||》
    【单元测试】SpirngBoot测试Controller,Service,Dao
  • 原文地址:https://blog.csdn.net/m0_60259116/article/details/126160118