产品介绍
经过这几天的打磨,ffmpeg+qt的window播放器,开发完成。解决了目测的所有bug。先给大家上几个效果图
这个播放器,是使用ffmpeg的原生API编写,配合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无解