FFmpeg+QT 视频播放器完整版

产品介绍

经过这几天的打磨,ffmpeg+qt的window播放器,开发完成。解决了目测的所有bug。先给大家上几个效果图

这个播放器,是使用ffmpeg的原生API编写,配合qt的界面,播放器支持暂停,前进,后退,关闭,画面等比缩放,视频显示,打开文件。后期还可以开发比较多功能,比如获取信息,快进,快堆,倍速播放,变声等等。

代码仓库:

ffmpeg+qt播放器

开发环境

  • VS 版本: Visual Studio Professional 2022 (64 位)
  • QT 版本: 5.12.0
  • ffmpeg3.4
  • libyuv库 以上环境如果有确实,可以翻阅前面的博客,和观看我的博客视频讲解。特别是是vs下的qt使用环境可以参考地址vs studio 集成qt环境

主要代码功能介绍

  • MyQtMainWindow.ui

这是qt的ui文件,可以通过编译生成头文件 ui_MainWindow.h 这个是自动过程,也可以右键单独编译。里面保存了视频基本布局

  • MyQtMainWindow.cpp

    这是ui文件的绑定文件 .ui 文件会设置这个为content,这个文件里面的类是集成 QMainWindow的。同时,我将所有的操作,比如前进,停止,关闭,暂停,打开的逻辑写在这里面,

  • FileDecode.cpp 这是播放器的核心文件之一,里面主要是ffmpeg的open,以及ffmpeg的解码音视频,音频的重采样。

  • SwrResample.cpp 这是封装的音频pcm重采样的类

  • ImageYuvRender.cpp 这里封装的是视频yuv的渲染,这里使用的是QImage的重绘制方式,它简单易用,但是效率 不高,可以编写一个支持opengl的yuv渲染器

  • AudioPlayer.cpp 这里封装的是qt组件的pcm播放器

核心代码讲解

1. UI界面

在MyQtMainWindow我创建了所有会用到的布局

scss 复制代码
MyQtMainWindow::MyQtMainWindow(QWidget* parent)
 : QMainWindow(parent), ui(new Ui::MainWindow)
 
{
 ui->setupUi(this);
 
 QMenu* fileMenu = ui->menubar->addMenu("File");
 QAction* openAction = fileMenu->addAction("OpenFile");
 ui->menubar->setFixedHeight(40);


 // 当点击菜单项或按钮时弹出文件对话框
 QObject::connect(openAction, &QAction::triggered,this, &MyQtMainWindow::OpenFileDialog);

 QWidget* bottom = new QWidget();
 bottom->setFixedHeight(50);

 QHBoxLayout* hlayout = new QHBoxLayout(bottom);
 stopButton = new QPushButton();
 stopButton->setText("stop");
 hlayout->addWidget(stopButton);
 QObject::connect(stopButton, &QPushButton::clicked, this, &MyQtMainWindow::ClosePlayer);

 pushButton = new QPushButton();
 pushButton->setText("pause");
 hlayout->addWidget(pushButton);
 QObject::connect(pushButton, &QPushButton::clicked, this, &MyQtMainWindow::ClickPlay);


 progressBar = new QSlider(Qt::Horizontal);
 hlayout->addWidget(progressBar);
 progressBar->setRange(0, 1000); // 设置进度范围
 progressBar->setValue(0); // 设置初始值
 connect(progressBar, &QSlider::valueChanged, this, &MyQtMainWindow::OnSliderValueChanged);
 connect(progressBar, &QSlider::sliderPressed, this, &MyQtMainWindow::OnSliderPressed);
 connect(progressBar, &QSlider::sliderReleased, this, &MyQtMainWindow::OnSliderValueReleased);
 
 info_label = new QLabel();
 info_label->setText("10:34:40");
 hlayout->addWidget(info_label);

 ui->statusbar->setStyleSheet("background-color: gray;");
 bottom->setStyleSheet("background-color: white;");
 // 设置菜单栏样式表
 setStyleSheet("QMenuBar { background-color: gray; color: black; }");

 QVBoxLayout* layout2 = new QVBoxLayout(ui->centralwidget);
 render = new ImageYuvRender(ui->centralwidget);
 ui->centralwidget->setStyleSheet("background-color: black;");
 layout2->addWidget(render);
 layout2->addWidget(bottom);

 //test();

}

在菜单栏里面创建了打开文件的并连接方法OpenFileDialog,创建了两个按钮,一个为停止,一个为暂停/播放,还有一个QSlider 用显示播放进度和前进后退的操作件。QLabel 用于显示视频当前播放时长和总播放时长。同时 ImageYuvRender 作为一个Widght也加入了进来,它是作为视频播放区域

2. 启动播放

启动播放是由菜单的打开文件来触发的:

ini 复制代码
void MyQtMainWindow::OpenFileDialog()
{
	QFileDialog fileDialog(this);
	fileDialog.setNameFilter("视频文件 (*.mp4)");
	int ret = fileDialog.exec();
	if (ret == QDialog::Accepted) {
		QString fileName = fileDialog.selectedFiles().first();
		
		qDebug() << "打开文件: " << fileName;
		

		QObject::connect(&timer, &QTimer::timeout, this, &MyQtMainWindow::UpdatePlayerInfo);
		timer.start(1000); // 每隔1000毫秒(1秒)触发一次定时器事件
		
		fileDecode = new FileDecode();
;		fileDecode->SetMyWindow(this);
		fileDecode->StartRead(fileName.toStdString());

		playFlag = true;
		pushButton->setText("pause");

	}
	fileDialog.close();
}

在这里,每次播放,都会创建一个定时器,它的作用是定时从fildDecode里面获取播放进度,从而显示出来。 每次播放,都会创建一个FileDecode的指针对象,把串口传入进去,因为窗口里面有渲染布局,后续FileDecode解码数据过程中,会将解码出来的信息,包括宽高,视频yuv数据输入到MyQtMainWindow进行渲染播放。StartRead就是启动ffmpeg的各个线程的方法,传入的是本地视频地址。

视频在关闭或者整个窗口关闭的时候 FileDecode都会析构处理

3. 启动播放后的处理过程

调用StartRead后,我会在FileDecode里面创建线程来处理:

ini 复制代码
int FileDecode::StartRead(std::string fildName)
{
    read_frame_flag = true;
    player_thread_ = new std::thread(&FileDecode::RunFFmpeg, this, fildName);

    return 0;
}

使用指针对象的线程,会方便管理一些,可以有判空条件

RunFFmpeg方法,就会开启ffmpeg的代码

c 复制代码
void FileDecode::RunFFmpeg(std::string url)
{
   
    // 注册所有的编解码器、格式和协议
    av_register_all();


    int ret = AVOpenFile(url);
    if (ret != 0)
    {
        std::cout << "AVOpenFile Faild";
    }
    
    ret = OpenVideoDecode();
    ret = OpenAudioDecode();

    ret = InnerStartRead();
    if (ret == -1)
    {
        std::cout << "读到文件尾部了,并且都渲染完了" << std::endl;

        std::ostringstream oss;
        oss << std::this_thread::get_id();
        qDebug() << oss.str().c_str();
        QMetaObject::invokeMethod(qtWin, &MyQtMainWindow::ClosePlayer);
    }
}

首先ffmpeg注册,打开文件

然后就调用了三个方法:

  • OpenVideoDecode 这里是封装了打开视频解码器过程
  • OpenAudioDecode 这里是封装了打开音频解码器的过程

InnerStartRead 里面就是循环读取视频流数据,同时创建了两个解码线程。InnerStartRead这个方法是阻塞的,只有在主动结束,和读取到文件的尾部的时候,才会停止,它的返回值是-1的时候,就是读到文件尾部,也就是视频播放完成的时候,会通过下面的方法,调用到主线程里面,去关闭播放器

QMetaObject::invokeMethod(qtWin, &MyQtMainWindow::ClosePlayer);

4. InnerStartRead里的流程

这个过程比较复杂,有比较多设计思想,理解相对难,要耐心阅读

ini 复制代码
int FileDecode::InnerStartRead()
{
	audio_packet_buffer = std::make_unique<AVJitterBuffer>(10);
	video_packet_buffer = std::make_unique<AVJitterBuffer>(10);

  
    videoDecodeThreadFlag = true;
    videoDecodeThread = new std::thread(&FileDecode::VideoDecodeFun, this);  //启动解码线程

	audioDecodeThreadFlag = true;
	audioDecodeThread = new std::thread(&FileDecode::AudioDecodeFun, this);  //启动解码线程


	AVRational video_pts_base = formatCtx->streams[videoStream]->time_base;
    AVRational audio_pts_base = formatCtx->streams[audioStream]->time_base;
	int64_t audio_pts_begin = formatCtx->streams[audioStream]->start_time;
    int64_t video_pts_begin = formatCtx->streams[videoStream]->start_time;

	/*  int audio_dur_ms = formatCtx->streams[audioStream]->duration * av_q2d(pts_base) * 1000;
	  int video_dur_ms = formatCtx->streams[videoStream]->duration * av_q2d(pts_base) * 1000;*/

    StartSysClockMs();

    int result = 0;
    read_frame_flag = true;
    do {
        std::unique_lock<std::mutex> lock(read_mutex_);
        if (!pause_read_flag)
        {
            Sleep(2);
            lock.unlock();
            continue;
        }
       

        if (position_ms != -1)
        {
            double rr = av_q2d(audio_pts_base);
            int64_t base_position = (double)position_ms / (av_q2d(audio_pts_base) * 1000);
            int seek_flag = (position_ms <= curr_playing_ms) ? AVSEEK_FLAG_BACKWARD : AVSEEK_FLAG_FRAME;
			int ret = av_seek_frame(formatCtx, audioStream, base_position, seek_flag);
            qDebug() << "seek frame ms:" << position_ms;
 

            //清空缓冲区
            ClearJitterBuf();

            qDebug() << "audio queue buffer ms:" << audio_packet_buffer->size();
            qDebug() << "video queue buffer ms:" << video_packet_buffer->size();

            //发生seek的时候,重置时钟到seek的位置
            ClockReset(position_ms);

            //player_start_time_ms = GetNowMs() - position_ms;

            position_ms = -1;

        }

		AVPacket* avpkt = av_packet_alloc();
		av_init_packet(avpkt);

        int expand_size = 0;
       
        int read_ret = av_read_frame(formatCtx, avpkt);
        if (read_ret < 0) {
            char errmsg[AV_ERROR_MAX_STRING_SIZE];
            av_make_error_string(errmsg,AV_ERROR_MAX_STRING_SIZE, read_ret);
            if (read_ret == AVERROR_EOF)
			{
				qDebug() << "Reached end of file" << errmsg;
				read_frame_flag = false;
				av_packet_unref(avpkt);
				result = -1;
				break;

            }
            else
            {
               
				qDebug() << "Error while reading frames" << errmsg;
				continue;
            }
            
        }

        if (avpkt->stream_index == audioStream) {
            int64_t pkt_dur = avpkt->duration * av_q2d(audio_pts_base) * 1000;
            if (pkt_dur != 0 && pkt_dur != audio_frame_dur)
            {
                //jitterbuf 里面存放1秒的数据,这个Resize方法要注意死锁
                expand_size = 1000 / pkt_dur;
                audio_frame_dur = pkt_dur;
            }
            int64_t read_time = (avpkt->pts - audio_pts_begin) * av_q2d(audio_pts_base) * 1000;
            qDebug() << "push read audio frame ms: " << read_time << ":" << audio_packet_buffer->size();;
            audio_packet_buffer->Push(avpkt, expand_size);
            qDebug() << "push read audio frame end ms: " << read_time << ":" << audio_packet_buffer->size();;
            int buffer_time = audio_packet_buffer->size() * pkt_dur;
            //if (buffer_time > 200)
            //{
            //    Sleep(180);
            //}
        }
        else if(avpkt->stream_index == videoStream)
        {
			int64_t pkt_dur = avpkt->duration * av_q2d(video_pts_base) * 1000;
			if (pkt_dur != 0 && pkt_dur != video_frame_dur)
			{
				//jitterbuf 里面存放1秒的数据,这个Resize方法要注意死锁
				expand_size = 1000 / pkt_dur;
                audio_frame_dur = pkt_dur;
			}
			int64_t read_time = (avpkt->pts - video_pts_begin) * av_q2d(video_pts_base) * 1000;
			qDebug() << "push read video frame ms: " << read_time << ":"<< video_packet_buffer->size();
            video_packet_buffer->Push(avpkt, expand_size);
            qDebug() << "push read video frame end ms: " << read_time;
        }

        lock.unlock();
    } while (read_frame_flag);

    return result;
}
  • 首先audio_packet_buffer,video_packet_buffer的创建,这是两个JitterBuffer,实际就是一个fifo结构的环行队列,我使用的模板方法创建的里面,里面的操作都是现成安全,这个数据结构可以用于很多场景。
  • 在这个项目里面,这两个环行队列会存放解码前的AVPacket*,为什么不直接就解码渲染呢,因为我们的视频和音频都在里面,如果直接渲染播放,那么就胡相互阻塞,导致播放的卡顿。所有要另外起两个线程来从队列里面取数据,然后音频独立线程的播放,当然,这样肯定就会涉及音画面同步。后面进一步会讲解。
  • 接下来,就创建了两个线程 videoDecodeThread,audioDecodeThread,这两个线程就是后面的解码渲染线程
  • 然后我们使用下面的代码,获取了音视频流的时间基数,我们需要它来配合AVPakcet的pts来计算每个包的时间撮
ini 复制代码
    AVRational video_pts_base = formatCtx->streams[videoStream]->time_base;
    AVRational audio_pts_base = formatCtx->streams[audioStream]->time_base;
    int64_t audio_pts_begin = formatCtx->streams[audioStream]->start_time;
    int64_t video_pts_begin = formatCtx->streams[videoStream]->start_time;
  • StartSysClockMs(); 方法是启动系统时钟,这里面就是记录了一个初始的系统时间

  • 接下来就是一个while循环,循环读取音视频的AVPacket*,这里面有一些条件:

    • pause_read_flag 是暂停读取的标识,暂停视频,就是用这个控制
    • position_ms 是一个seek值,当我们前进/后退的时候,这个值就是指定的目标位置,在里面,我们使用av_seek_frame来位移到视频流的指定位置。每次发生主动位移的时候,我们的时钟就会 错乱,所以这个时候要统一次时钟 ClockReset(),这个方法,会将当前时间,音视频流时间,当前播放时间统一指向我们seek的目标位置
    • ClearJitterBuf(); 的作用是清空待解码队列,因为seek后,我们不能继续播放缓冲的数据
  • 接下来就是读取AVPacket*,后将他们push到队列里面,后有个参数,这个参数是指定队列新的大小,根据AVPacket一个代表的数据长度(ms),来缓存多少个包,这里注意这个push是阻塞的,当达到最大长度的时候,会阻塞push,这样可以避免数据一致在读取,导致占用内存越来越大

scss 复制代码
 audio_packet_buffer->Push(avpkt, expand_size);
 video_packet_buffer->Push(avpkt, expand_size);

这里有一个lock锁,因为我们在做一些seek的时候,我们不能放任继续读数据和push数据,这会造成错乱,所以将这整个过程加了锁。当我们seek的时候,会暂停这个过程,通过pause_read_flag来改变
read_frame_flag 是整体标识,在需要退出播放的时候,我们可以操作这个变量来退出循环

  • 接下来讲解 VideoDecodeFun这个视频解码线程方法

先记录一下这个视频的pts起点,用于后面计算当前包的时间点。然后一个while循环。 在这个while循环里,我们不停的判断时间,如果记录的视频流时间到达了系统时钟时间,那么就继续执行,否则就sleep continue。然后我们从jiiterbuf里面pop出数据来,这里的pop方法,用的是非阻塞的,为了防止死锁,不好处理。当然这个jitterbuff也是支持阻塞模式的

ini 复制代码
 AVPacket* avpkt = video_packet_buffer->Pop(true);

然后将AVPacket* 送入 DecodeVideo(avpkt);方法

ini 复制代码
int FileDecode::DecodeVideo(AVPacket* originalPacket)
{
    
    int ret = avcodec_send_packet(videoCodecCtx, originalPacket);
    if (ret < 0)
    {
        return -1;
    }
    AVFrame* frame = av_frame_alloc();
    ret = avcodec_receive_frame(videoCodecCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        return -2;
    }
    else if (ret < 0) {
        std::cout << "error decoding";
        return -1;
    }


    qtWin->updateYuv(frame->data[0], frame->data[1], frame->data[2]);

    av_frame_free(&frame);

    return 0;
   
}

这个方法里面,就是我们讲到的ffmpeg解码过程,加码出来的是AVFrame数据帧,我们送入qtWin,最终送入ImageYuvRender里面,这个后面再讲

  • 接下来讲解 AudioDecodeFun

它与视频的过程类似,也是pop出AVPacket* 然后送入DecodeAudio方法

ini 复制代码
int FileDecode::DecodeAudio(AVPacket* originalPacket)
{
    int ret = avcodec_send_packet(audioCodecCtx, originalPacket);
    if (ret < 0)
    {
        return -1;
    }
    AVFrame* frame = av_frame_alloc();
    ret = avcodec_receive_frame(audioCodecCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
        return -2;
    }else if (ret < 0) {
        std::cout << "error decoding";
        return -1;
    }

    int data_size = av_get_bytes_per_sample(audioCodecCtx->sample_fmt);
    if (data_size < 0) {
        /* This should not occur, checking just for paranoia */
        std::cout << "Failed to calculate data size\n";
        return -1;
    }

    ResampleAudio(frame);
   
    av_frame_free(&frame);
    return 0;
}

DecodeAudio也是解码数据,但是是音频数据。后面多了一个过程ResampleAudio(frame),为了兼容性,我们把音频统一采样成一个格式,在采样之后,我们会把pcm数据送入

ini 复制代码
int FileDecode::ResampleAudio(AVFrame* frame)
{
	// 把AVFrame里面的数据拷贝到,预备的src_data里面
	if (!swrResample)
	{
		swrResample = std::make_unique<SwrResample>();

		//创建重采样信息
		int src_ch_layout = audioCodecCtx->channel_layout;
		int src_rate = audioCodecCtx->sample_rate;
		enum AVSampleFormat src_sample_fmt = audioCodecCtx->sample_fmt;

		int dst_ch_layout = AV_CH_LAYOUT_STEREO;
		int dst_rate = 44100;
		enum AVSampleFormat dst_sample_fmt = AV_SAMPLE_FMT_S16;

		//aac编码一般是这个,实际这个值只能从解码后的数据里面获取,所有这个初始化过程可以放在解码出第一帧的时候
		int src_nb_samples = frame->nb_samples;

		swrResample->Init(src_ch_layout, dst_ch_layout,
			src_rate, dst_rate,
			src_sample_fmt, dst_sample_fmt,
			src_nb_samples);
	}

	swrResample->WriteInput(frame);

	int res = swrResample->SwrConvert();

    return res;
}

这里设定了目标采样格式,最终int res = swrResample->SwrConvert(); 会执行重采样

ini 复制代码
int SwrResample::SwrConvert()
{
    int ret = swr_convert(swr_ctx, dst_data_, dst_nb_samples_, (const uint8_t**)src_data_, src_nb_samples_);
    if (ret < 0) {
        fprintf(stderr, "Error while converting\n");
        exit(1);
    }

    int  dst_bufsize = av_samples_get_buffer_size(&dst_linesize, dst_nb_channels,
        ret, dst_sample_fmt_, 1);

   int planar = av_sample_fmt_is_planar(dst_sample_fmt_);
   if (planar)
   {

       int data_size = av_get_bytes_per_sample(dst_sample_fmt_);
#ifdef WRITE_RESAMPLE_PCM_FILE
       for (int i = 0; i < dst_nb_samples_; i++) {
           for (int ch = 0; ch < dst_nb_channels; ch++)
           {
               fwrite(dst_data_[ch] + i * data_size, 1, data_size, outdecodedswffile);
           }
       }
#endif // WRITE_RESAMPLE_PCM_FILE
       

   }
   else {
       //非planr结构,dst_data_[0] 里面存在着全部数据

       audioPlayer.writeData((const char*)(dst_data_[0]), dst_bufsize);
   }

    return dst_bufsize;
}

在里面我们将pcm数据送入了 qt实现的pcm播放器 audioPlayer

6. 视频yuv渲染

arduino 复制代码
void ImageYuvRender::updateYuv(uint8_t* y, uint8_t* u, uint8_t* v)
{
    memcpy(yuvData, y, stride_width_ * height_);
    memcpy(yuvData + stride_width_ * height_, v, stride_width_ * height_ / 4);
    memcpy(yuvData + stride_width_ * height_ * 5 / 4, u, stride_width_ * height_ / 4);

    int size = stride_width_ * height_;
    libyuv::I420ToARGB(yuvData, stride_width_, yuvData + size, stride_width_ / 2,
        yuvData + size * 5 / 4, stride_width_ / 2,
        rgbaData, width_ * 4,
        width_, height_);

    this->update();
}

我们在外面通过updateYuv 传入yuv数据,在这之前,会在打开视频解码器的时候调用ImageYuvRender:initData 它主要是初始化yuv,rgb数组。

因为qt只能得QImage只能渲染rgb数据,所以这个,要先用libyuv将yuv数据转换成rgb数据。对yuv不太了解的可以翻看我的博客和视频。

stride_width_ 是视频的跨距,可以从解码器的codec_width获取,前面讲过,有些编码器为了优化内存存储,stride_width_ 会比 width大,关于它的 作用可以翻看 [视频解码] (juejin.cn/post/736165...) 这篇博客

ini 复制代码
void ImageYuvRender::paintEvent(QPaintEvent* event) {

    if (yuvData == NULL || rgbaData == NULL)
    {
        return;
    }

    QPainter painter(this);

    QImage rgbImage(rgbaData,width_, height_, QImage::Format_RGBA8888);

    int window_with = size().width();
    int window_height = size().height();

    float window_rate = (float)window_with / (float)window_height;
    float imag_rate  = (float)width_ / (float)height_;

    int target_width;
    int target_height;
    int x;
    int y;
    
    if (window_rate > imag_rate)
    {
        target_height = window_height;
        target_width = target_height * imag_rate;
        x = (window_with - target_width)/2;
        y = 0;
    }
    else
    {
        target_width = window_with;
        target_height = target_width * height_ / width_;
        y = (window_height - target_height) / 2;
        x = 0;
    }
    QSize tszie(target_width,target_height);

    // 在窗口中绘制RGB图像
    painter.drawImage(x, y, rgbImage.scaled(tszie, Qt::KeepAspectRatioByExpanding));

}

paintEvent 就是重写的父类方法,在里面渲染yuv,每次调用this.update的时候,就会重绘一次。

这里有很多计算宽高的,这个是为了让视频按照比例显示在QImage里面

7. 音频播放

音频播放实在 AudioPlayer.h 里面处理的。outputDevice是一个数据管道,audioOutput负责播放

ini 复制代码
private:
    QIODevice* outputDevice;
    QAudioOutput* audioOutput;

外部调用writeData方法传入数据

scss 复制代码
    void writeData(const char* data, qint64 len) {
        //audioData.insert(0, data, len);
        int buf_size = audioOutput->bufferSize();
        outputDevice->write(data, len);
    }

SetFormat 设置了播放器的格式,并启动播放,audioOutput里面是有一个缓冲区,这样可以保证音频的数据额连续性,不至于卡。

scss 复制代码
void SetFormat(int dst_nb_samples, int rate, int sample_size, int nch) {
    QAudioFormat format;
    format.setSampleRate(rate); // 采样率
    format.setChannelCount(nch);   // 声道数
    format.setSampleSize(sample_size);    // 采样大小
    format.setCodec("audio/pcm"); // 音频编码格式
    format.setByteOrder(QAudioFormat::LittleEndian); // 字节顺序
    format.setSampleType(QAudioFormat::SignedInt);  // 采样类型

    int len = dst_nb_samples * format.channelCount() * format.sampleSize()/8;
   
     // 创建 QAudioOutput 对象
    audioOutput = new QAudioOutput(format);
    //audioOutput->setBufferSize(len * 100);
    audioOutput->setVolume(1.0); // 设置音量(0.0 - 1.0)

    // 打开音频输出
    outputDevice = audioOutput->start();
}

8. 暂停和恢复

ini 复制代码
void MyQtMainWindow::ClickPlay()
{
	if (!fileDecode) {
		return;
	}

	if (playFlag)
	{
		fileDecode->PauseRender();
		pushButton->setText("resume");
		playFlag = false;
	}
	else
	{
		fileDecode->ResumeRender();
		pushButton->setText("pause");
		playFlag = true;
	}
}

MyQtMainWindow::ClickPlay 负责暂停和恢复播放的逻辑。里面是一个标识切换

ini 复制代码
void FileDecode::PauseRender()
{
    pauseFlag = true;
}

void FileDecode::ResumeRender()
{
    pauseFlag = false;
}

pauseFlag 用在音频和视频的解码线程里面,控制暂停,一旦停止解码,就是停止pop,读数据线程也会阻塞在push。所以av_read_frame线程里面不用处理

9. 前进/后退

scss 复制代码
void MyQtMainWindow::OnSliderPressed() 
{

	if (!fileDecode) {
		return;
	}
	
	fileDecode->PauseRead(); //加锁方式,绝对保证不在有写数据,push 也不有
	fileDecode->ClearJitterBuf(); //然后清空缓存

	ClickPlay();

	qDebug() << "OnSliderPressed"  << ";" << getCurrentTimeAsString().c_str();
}

void MyQtMainWindow::OnSliderValueReleased()
{

	if (!fileDecode) {
		return;
	}

	fileDecode->SetPosition(progressBar->value());
	fileDecode->ResumeRead();

	ClickPlay(); // 回复audio,video线程的执行
	//qDebug() << "OnSliderValueReleased " << ":" <<       getCurrentTimeAsString().c_str();
}
  • OnSliderPressed 是在滑动条按下的时候触发,这个时候我们先暂停av_read_frame,使用PauseRead,里面也是个标志位,但是是枷锁的。

然后清空解码队列,因为这些数据也不用了。 然后暂停播放,也就是停止音视频的加码线程,这里是 暂停 ,里面的循环还是在的。

  • OnSliderValueReleased 是当滑动条移动到指定位置后,松开的时候 ,也是在这个时候,我们处理seek。首先根据移动的位置计算一个目标时间,使用SetPosition。计算方法很简单,就是比例*总时长。

然后我们就恢复读数据,恢复解码渲染线程 然后再 int FileDecode::InnerStartRead()方法里面,我们根据有效的position_ms去做seek。

ini 复制代码
        if (position_ms != -1)
        {
            double rr = av_q2d(audio_pts_base);
            int64_t base_position = (double)position_ms / (av_q2d(audio_pts_base) * 1000);
            int seek_flag = (position_ms <= curr_playing_ms) ? AVSEEK_FLAG_BACKWARD : AVSEEK_FLAG_FRAME;
			int ret = av_seek_frame(formatCtx, audioStream, base_position, seek_flag);
            qDebug() << "seek frame ms:" << position_ms;
 

            //清空缓冲区
            ClearJitterBuf();

            qDebug() << "audio queue buffer ms:" << audio_packet_buffer->size();
            qDebug() << "video queue buffer ms:" << video_packet_buffer->size();

            //发生seek的时候,重置时钟到seek的位置
            ClockReset(position_ms);

            //player_start_time_ms = GetNowMs() - position_ms;

            position_ms = -1;

        }
  • OnSliderValueChanged 比较简单,滑动过程中,时间变,方便我们定位

10. 显示进度

显示进度也比较简单,就是在定时器方法void MyQtMainWindow::UpdatePlayerInfo()里面获取fileDecode里面记录是时间错,然后显示就完事。当然有一个毫秒转换时分秒格式的过程

ini 复制代码
void MyQtMainWindow::UpdatePlayerInfo()
{
	if (!playFlag)
	{
		return;
	}

	int64_t file_len = fileDecode->GetFileLenMs();
	int64_t curr_len = fileDecode->GetPlayingMs();

	QTime time = QTime::fromMSecsSinceStartOfDay(curr_len);
	QTime time2 = QTime::fromMSecsSinceStartOfDay(file_len);
	float rato = (float)curr_len*1000 / (float)file_len;
	progressBar->setValue((int)(rato));
	info_label->setText(time.toString("hh:mm:ss") + "/" + time2.toString("hh:mm:ss"));
}

11. 关闭

void MyQtMainWindow::ClosePlayer() 负责关闭,里面及时close所有的东西,初始化所有的

scss 复制代码
void MyQtMainWindow::ClosePlayer()
{
	std::ostringstream oss;
	oss << std::this_thread::get_id();
	qDebug() << oss.str().c_str();

	timer.stop();

	if (fileDecode){
		fileDecode->Close();

		delete fileDecode;
		fileDecode = nullptr;
	}

	render->Close();

	pushButton->setText("start");
	progressBar->setValue(0); // 设置初始值
}

逻辑简单,唯一就是涉及线程的停止,调试稍微复杂一点

11. 播放结束

在 int FileDecode::InnerStartRead() 线程方法里面 av_read_frame返回小于0的时候,我们根据返回值可以判断是否读到文件尾部。如果是,那么就退出while。并调用

css 复制代码
 QMetaObject::invokeMethod(qtWin, &MyQtMainWindow::ClosePlayer);

调用时间到主线程的关闭。为什么不直接操作。因为它自己也在线程里面,不能自己停自己。

12 难点说明

在这个项目里面,从音视频知识来讲,理解ffmpeg,理解yuv,立即pcm比较难。这些知识可以转么研究一下。 从项目代码设计来讲,那么这里的多线程,以及为什么多线程比较难。

在处理seek的时候,是调试了很多次,就是多线程的时候jitterbuffer里面的数据可能阻塞,或者有就数据保留,到时seek无解

13 制作不易,清帮忙点赞关注

相关推荐
Mr.Q2 小时前
OpenCV和Qt坐标系不一致问题
qt·opencv
lxkj_20243 小时前
使用线程局部存储解决ffmpeg中多实例调用下自定义日志回调问题
ffmpeg
重生之我是数学王子5 小时前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
runing_an_min7 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
ruizhenggang7 小时前
ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!
ffmpeg
runing_an_min9 小时前
ffmpeg视频滤镜:提取缩略图-framestep
ffmpeg·音视频·framestep
----云烟----14 小时前
QT中QString类的各种使用
开发语言·qt
「QT(C++)开发工程师」20 小时前
【qt版本概述】
开发语言·qt
韩曙亮1 天前
【FFmpeg】FFmpeg 内存结构 ③ ( AVPacket 函数简介 | av_packet_ref 函数 | av_packet_clone 函数 )
ffmpeg·音视频·avpacket·av_packet_clone·av_packet_ref·ffmpeg内存结构
一路冰雨1 天前
Qt打开文件对话框选择文件之后弹出两次
开发语言·qt