ffplay.c源码阅读之暂停、重播、快进、快退实现细节(五)

前言

1、播放器如何实现暂停?
2、暂停之后在从暂停之处开始播放?
3、播放中快进、后退这些操作实现细节?
以上功能是作为播放器最重要也是非常基础的功能,本文就是仔细学习一下ffplay.c是如何实现这些功能的,希望能够学以致用。

播放暂停和重播

  • 自我分析

前面我们知道ffplay.c有拉流、解码、渲染供6个线程(这里假设视频包含音频和字幕流)。暂停意味着只是暂停播放,所以这些线程不会销毁,所以暂停的时候让它们处于休眠状态,这样就节约了cpu资源,同时各种音视频缓冲区也保留着,待重新开始播放时直接从之前的位置开始。

关键变量paused代表是否暂停,当用户按下暂停后会将该变量设置为1,重新开始播放后又会将该变量设置为0

typedef struct VideoState {
    /// 省略。。。。
    int paused;
    int last_paused;
    // 省略。。。。
}

paused 代表了目前是否暂停状态,last_paused代表了上一次是否暂停状态

下面看一下暂停或者重播的实现逻辑

  • 主线程更新paused变量的值
static void stream_toggle_pause(VideoState *is)
{
    if (is->paused) {
        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);
    is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
}

1、当处从播放状态处于暂停状态后,通过代码set_clock(&is->vidclk, get_clock(&is->vidclk), is->vidclk.serial);更新视频时钟
2、更新外部时钟
3、更新变量paused的值

  • 拉流线程的处理逻辑

之前的文章有提到,拉流线程的代码都在read_thread里面,这里只贴出当暂停或者重新播放的代码逻辑

static int read_thread(void *arg)
{
        // 省略代码......
       // 主要用于处理rtsp等等实时流拉流时的逻辑。当重播或者暂停时要分别调用av_read_play()函数或者av_read_pause()函数
        if (is->paused != is->last_paused) {
            is->last_paused = is->paused;
            if (is->paused)
                is->read_pause_return = av_read_pause(ic);
            else
                av_read_play(ic);
        }
        
        // 当用户暂停后,先将paused变量置为1,此时拉流线程并未结束工作,它依然会继续通过av_read_frame()函数读取未压缩数据包放入未压缩数据队列,直到该队列满让线程休眠10ms(如果一直暂停,则一直不停休眠10ms的状态),这样不会让拉流线程空转
        /* if the queue are full, no need to read more */
        if (infinite_buffer<1 &&
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
            || (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
                stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
                stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }
      
}

1、当用户按下暂停或者重播后,对于实时流rtsp等等,这里要分别调用av_read_play()函数或者av_read_pause()函数
2、当用户暂停后,先将paused变量置为1,此时拉流线程并未结束工作,它依然会继续通过av_read_frame()函数读取未压缩数据包放入未压缩数据队列,直到该队列满让线程休眠10ms(如果一直暂停,则一直不停休眠10ms的状态),这样不会让拉流线程空转

  • 解码线程的处理逻辑

音视频字幕解码线程的处理逻辑基本都一致,这里以视频为例,主要在函数video_thread()(视频)中,如下为省略了无关紧要的代码

static int video_thread(void *arg)
{
// 省略代码.....
for (;;) {
        // 该函数主要用来获取解码器中的解码结果,当返回<0时,代表解码出现不可描述错误或者解码遇到结束标记了(即到了文件末尾了),小于0时则直接关闭解码线程,否则进入下一步
        ret = get_video_frame(is, frame);
        if (ret < 0)
            goto the_end;
            // 省略代码.......
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 到了这一步代表解码成功,则将解码视频帧插入视频FrameQueue队列,该函数工作原理为,当队列满后,该函数让解码线程处于休眠状态。所以对于暂停后,由于不再继续渲染,即视频FrameQueue不消耗视频帧,故队列肯定会满,即一定会处于休眠状态
            ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame);

        if (ret < 0)
            goto the_end;
    }
 the_end:
    av_frame_free(&frame);
}

1、暂停后,解码线程继续工作。通过get_video_frame()函数持续获取解码结果,当返回<0时,代表解码出现不可描述错误或者到达文件末尾,那么此时直接结束解码线程。否则进入下一步
2、通过queue_picture()函数将解码视频帧插入视频FrameQueue队列,该函数工作原理为,当队列满后,该函数让解码线程处于休眠状态。所以对于暂停后,由于不再继续渲染,即视频FrameQueue不消耗视频帧,故队列肯定会满,即一定会处于休眠状态

  • 渲染线程

渲染视频和字幕在一个线程,处理逻辑差不多,在函数video_refresh()中。音频是在sdl_audio_callback()中。接下来分别看他们的处理逻辑

暂停后视频和字幕的处理逻辑,先看如下代码:

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
    SDL_PumpEvents();
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(is, &remaining_time);
        SDL_PumpEvents();
    }
}

当暂停播放后,视频渲染线程依然继续工作直到视频队列FrameQueue中没有数据为止,当此队列空了之后,这里的remaining_time>0会成立,即它会一直循环的进行休眠以免cpu浪费。

接下来是音频渲染的处理逻辑

/* prepare a new audio buffer */
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    VideoState *is = opaque;
    int audio_size, len1;

    audio_callback_time = av_gettime_relative();
    
    while (len > 0) {
        if (is->audio_buf_index >= is->audio_buf_size) {
           audio_size = audio_decode_frame(is);
           if (audio_size < 0) {
                /* if error, just output silence */
               is->audio_buf = NULL;
               is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
           } else {
               if (is->show_mode != SHOW_MODE_VIDEO)
                   update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
               is->audio_buf_size = audio_size;
           }
           is->audio_buf_index = 0;
        }
        len1 = is->audio_buf_size - is->audio_buf_index;
        if (len1 > len)
            len1 = 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);
            if (!is->muted && is->audio_buf)
                SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
        }
        len -= len1;
        stream += len1;
        is->audio_buf_index += len1;
    }
    is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
    /* Let's assume the audio driver that is used by SDL has two periods. */
    if (!isnan(is->audio_clock)) {
        set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
        sync_clock_to_slave(&is->extclk, &is->audclk);
    }
}

当暂停时audio_decode_frame(is)返回-1,那么此时is->audio_buf_size等于512,但is->audio_buf = NULL;又为nil,即这里的while循环会空转知道len<=0
下次音频渲染线程时又是如此逻辑

疑问?这里既然暂停了不直接return?而是依然让while循环继续执行呢?

快进和快退

快进和快退:将当前播放时间指定到某个指定的时间点,这个时间点可以在当前时间之前也可以之后。快进或者快退要解决两个问题:
1、重新从指定的时间戳开始拉流
2、缓冲区的之前的数据要先清空

以上基本就是实现思路了,接下来就是ffplay.c如何实现了,首先是快进或者快退事件检测

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

可以看到上面逻辑非常清晰,当快进或者快退时首先获取当前主时钟的时间,然后基于该时间得到最终的时间点pos。然后通过stream_seek()函数从指定的时间点获取数据

/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{
    if (!is->seek_req) {
        is->seek_pos = pos;
        is->seek_rel = rel;
        is->seek_flags &= ~AVSEEK_FLAG_BYTE;
        if (seek_by_bytes)
            is->seek_flags |= AVSEEK_FLAG_BYTE;
        is->seek_req = 1;
        SDL_CondSignal(is->continue_read_thread);
    }
}

seek_req:为1代表当前处于快进或者快退状态
seek_pos:代表快进或者快退到的时间点
seek_rel:代表快进或者快退时间点到当前时间点的时间差
seek_flags:快进搜索的方式(ogg格式支持按字节搜索)

当seek_req = 1后,代表当前处于快进快退状态,当拉流线程检测到目前处于快进或者快退状态则会做相应的处理,具体代码如下:

static int read_thread(void *arg)
{
  //省略代码...
  if (is->seek_req) {
            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;
// FIXME the +-2 is due to rounding being not done in the correct direction in generation
//      of the seek_pos/seek_rel variables

            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 {
                if (is->audio_stream >= 0) {
                    packet_queue_flush(&is->audioq);
                    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);
                }
                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;
            is->queue_attachments_req = 1;
            is->eof = 0;
            if (is->paused)
                step_to_next_frame(is);
        }
        if (is->queue_attachments_req) {
            if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
                AVPacket copy = { 0 };
                if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
                    goto fail;
                packet_queue_put(&is->videoq, &copy);
                packet_queue_put_nullpacket(&is->videoq, is->video_stream);
            }
            is->queue_attachments_req = 0;
        }
  // 省略代码....
}

这段代码主要做了两件事:
1、通过ffmpeg的avformat_seek_file()函数将读取指针移动到指定时间点,接下来的拉流都将从这个时间点之后读取数据(快进和快退操作不支持实时流)
2、将音视频字幕压缩数据PacketQueue清空,并放置一个空数据包,之所以要放置这个空数据包是为了清空解码器
3、同步外部时钟,便于音视频同步

以上就是快进和快退的处理逻辑,可以看到还是比较简单和清晰的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容