音视频基础学习之【06.播放跳转】

本文介绍音视频播放中跳转功能的实现方法,包括如何使用FFMPEG的av_seek_frame函数实现跳转,解决跳转延迟及花屏现象等问题,并提供UI界面上实现跳转的具体步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

 

播放跳转

首先添加状态位

跳转的三个问题

跳转延迟

花屏现象

关键帧跳转

UI界面控制

优化


播放跳转

在解码线程类中添加跳转的控制标志,记录是否需要跳转

在主线程的循环中判断此变量,当需要跳转的时候就执行跳转操作

跳转操作可以直接使用 FFMPEG 的跳转函数 av_seek_frame 来实现

函数原型如下: int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp,int flags);

  • 首先添加状态位

//==========================跳转控制==========================
int     seek_req;               //跳转标志 -- 读线程
int64_t seek_pos;               //跳转位置 -- 微秒
int     seek_flag_audio;        //跳转标志 -- 用于音频线程
int     seek_flag_video;        //跳转标志 -- 用于视频线程
double  seek_time;              //跳转时间(秒) 值和 seek_pos相同
  • 跳转的三个问题

跳转延迟

跳转时,音视频队列中未解码的数据包是可以供解码播放几秒钟的,所以每次跳转时,需要将其清空

花屏现象

除了音频队列,解码器中也保留了上一帧视频的信息,而视频发生跳转时,解码器中保留的信息会对解码当前帧产生影响

例如:从3s处跳转到15min处,则3s处的包还残留在队列和解码器中,与15min处的结合会产生"花屏"

因此,清空队列的时候我们也要同时清空解码器的数据

这里的操作是,往队列中放入一个特殊的 packet,当解码线程取到这个 packet 的时候,就执行清除解码器数据的操作

在解码器类头文件中定义“清空”队列包的宏

#define FLUSH_DATA "FLUSH"

然后在主线程循环解码中,退出线程与暂停之间加入跳转的判断,因为在逻辑上,暂停时也可以跳转

此处需要注意一个细节:

当发生跳转时,由于前述的音视频同步操作是采用视频同步到音频上的方式进行,向前跳转音频时钟会变小,一定要重新将视频时钟归零,否则会一直SDL_Delay等待音频

        // 跳转要放在暂停前面 暂停的时候也可以跳转
        // 当跳转标志位 seek_req --> 1 清除队列里的缓存
        if( m_videoState.seek_req )
        {
            int stream_index = -1;
            int64_t seek_target = m_videoState.seek_pos;//微秒
            if (m_videoState.videoStream >= 0)
                stream_index = m_videoState.videoStream;
            else if (m_videoState.audioStream >= 0)
                stream_index = m_videoState.audioStream;
            //时间基
            AVRational aVRational = {1, AV_TIME_BASE};
            if (stream_index >= 0)
            {
                //计算跳转到的位置 单位为微秒
                seek_target = av_rescale_q(seek_target, aVRational,m_videoState.pFormatCtx->streams[stream_index]->time_base);
            }
            if (av_seek_frame(m_videoState.pFormatCtx, stream_index, seek_target,AVSEEK_FLAG_BACKWARD) < 0)
            {
                //跳转失败处理
                fprintf(stderr, "%s: error while seeking\n",m_videoState.pFormatCtx->filename);
            }else
            {
                //跳转成功
                //处理音频队列
                if (m_videoState.audioStream >= 0)
                {
                    //分配一个 packet
                    AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));
                    av_new_packet(packet, 10);
                    //将FLUSH_DATA 清空标志 写入到包里
                    strcpy((char*)packet->data,FLUSH_DATA);
                    //清除队列
                    m_videoState.audioq->packet_queue_flush();
                    //往队列中存入用来清除的包
                    m_videoState.audioq->packet_queue_put(packet);
                }
                //处理视频队列 操作同上
                if (m_videoState.videoStream >= 0)
                {
                    AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));
                    av_new_packet(packet, 10);
                    strcpy((char*)packet->data,FLUSH_DATA);
                    m_videoState.videoq->packet_queue_flush();
                    m_videoState.videoq->packet_queue_put(packet);
                    //考虑到向左跳转 避免卡死
                    //向左跳转 音频时钟会变小 重新将视频时钟归零 否则会一直SDL_Delay等待音频
                    m_videoState.video_clock = 0;
                }
            }
            //主线程的 跳转标志重新置为0 主线程继续进行处理
            m_videoState.seek_req = 0;
            //seek_time 精确到微秒
            m_videoState.seek_time = m_videoState.seek_pos;
            //音频、视频解码线程 跳转处理标志
            m_videoState.seek_flag_audio = 1;
            m_videoState.seek_flag_video = 1;
        }

 然后在视频解码线程中添加以下操作

//读取包后 比对包内容 如果是 清空队列的包
if(strcmp((char*)packet->data,FLUSH_DATA) == 0)
{
     //调用函数清空解码器缓存 释放当前包
     avcodec_flush_buffers(is->video_st->codec);
     av_free_packet(packet);
     //!!!视频时钟归零 否则 向左跳转时会一直等待音频时钟走到原来的位置才继续解码播放
     is->video_clock = 0;
     continue;
}

在音频解码中添加以下操作

//比对当前音频包 是不是清空队列包
if(strcmp((char*)pkt.data,FLUSH_DATA) == 0)
{
     //如果是 清除解码器缓存
     avcodec_flush_buffers(is->audio_st->codec);
     av_free_packet(&pkt);
     continue;
}

关键帧跳转

解码器解码时必须有关键帧的信息才能进行解码,因此跳转功能只能通过关键帧跳转

例如:关键帧在10s 和 15s处,则要跳转到13s位置,正确的做法是:跳转到10s位置,将10-13s的包全部丢弃,等待3s后继续解码播放

在计算视频帧时间戳后,添加判断

if (is->seek_flag_video)
{
     //如果跳转的关键帧在需要跳转位置之前
     if (video_pts < is->seek_time)
     {
          //丢弃当前帧 重新循环
          av_free_packet(packet);
          continue;
     }else
     {
          is->seek_flag_video = 0;
     }
}

在计算音频帧时间戳后,添加判断

if( is->seek_flag_audio)
{
      //没有到目的时间
      if( is ->audio_clock < is->seek_time)
      {
            if( pkt.pts != AV_NOPTS_VALUE)
            {
                  //重新计算音频时钟
                  is->audio_clock = av_q2d( is->audio_st->time_base )*pkt.pts *1000000 ;
            }
            break;
      }
      else
      {
            if( pkt.pts != AV_NOPTS_VALUE)
            {
                  is->audio_clock = av_q2d( is->audio_st->time_base )*pkt.pts *1000000 ;
            }

            is->seek_flag_audio = 0 ;
      }
}

 要注意这里一个细节,由于计算音频时间戳是采用:上一帧的音频时间戳 + 一帧音频时间的长度  的方式

所以在向前跳转时,音频时钟不能减小,需要重新给音频时钟赋值

  • UI界面控制

Qt自带的进度条默认只能拖动实现跳转,在这里我们想做到点击位置直接跳转的方式,就需要对Slider类的鼠标按下事件进行重写

添加一个C++类,继承QSlider,添加进度条值改变的信号 和 鼠标按下事件的重写

#ifndef VIDEOSLIDER_H
#define VIDEOSLIDER_H

#include <QSlider>

class VideoSlider : public QSlider
{
    Q_OBJECT
public:
    explicit VideoSlider(QWidget *parent = 0);

signals:
    void SIG_valueChanged(int);
protected:
    void mousePressEvent(QMouseEvent *event);
    
public slots:
};

#endif // VIDEOSLIDER_H

重写mousePressEvent

void VideoSlider::mousePressEvent(QMouseEvent *event)
{
    int value = QStyle::sliderValueFromPosition(minimum(), maximum(), event->pos().x(), width());
    setValue(value);
    emit SIG_valueChanged(value);
}

将进度条提升为自己定义的VideoSlider 

在UI类中添加connect处理信号SIG_valueChanged

    //进度条改变
    connect( ui->slider_progress , SIGNAL(SIG_valueChanged(int)),
             this,SLOT(slot_videoSliderValueChanged(int)) );

 添加处理槽函数

//视频进度通过手动点击改变
void Youku::slot_videoSliderValueChanged(int value)
{ 
    if( QObject::sender() == ui->slider_progress )
    {
        //精确到毫秒
        decode->seek((qint64)value*1000000); 
    }
}

优化

在测试跳转时,发现播放到末尾时,进度条没有刷新,且停留在最后一帧卡死,考虑可以通过更改状态位设置UI界面解决

在主线程末尾添加状态位设置

m_playerState = PlayerState::Stop;

在定时器响应控制函数中添加,如果进度条到达末尾,切换状态

if( ui->slider_progress->value() == ui->slider_progress->maximum()
                && decode->m_playerState == PlayerState::Stop )
{
     slot_PlayerStateChanged( PlayerState::Stop );
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值