当前位置: 首页 > news >正文

利用QT和FFmpeg实现一个简单的视频播放器

          在当今的多媒体世界中,视频播放已成为不可或缺的一部分。从简单的媒体播放器到复杂的视频编辑软件,视频解码和显示技术无处不在。本示例使用Qt和FFmpeg构建一个简单的视频播放器。利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。特点: 采用软件解码(CPU)、只解码图像数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。视频解码采用独立子线程,解码后将得到的图像数据通过信号槽发方式传递给UI界面进行渲染。

一、 环境介绍    

1、QT版本: QT5.12.6

2、编译器:  MSVC2017 64

3、ffmpeg版本: 6.1.1

4、SDL2 音频播放所需

5、完整工程下载地址(下载即可编译运行): https://download.csdn.net/download/u012959478/89626950

二、实现功能
  • 使用ffmpeg音视频库软解码实现视频播放器
  • 支持打开多种本地视频文件(如mp4,mov,avi等)
  • 支持视频匀速播放
  • 采用QPainter进行图像显示,支持自适应窗口缩放
  • 视频播放支持实时开始,暂停,继续播放
  • 采用模块化编程,视频解码,线程控制,图像显示各功能分离,低耦合
  • 多线程编程
三、实现思路  

该视频播放器的主要运行三条线程,需要两条队列:

线程1(音视频数据分离):使用FFMPEG分解视频文件,将视频数据存入到视频队列中,将音频数据存入到音频队列中。

线程2(视频解码):从视频队列中获取一包视频数据,通过FFMPEG解码该包视频数据,解码后再将视频转换为RGB数据,最后通过QT的画图显示将视频画面显示出来。

线程3(音频解码):实际该线程由SDL新建,它是通过回调的方式来从音频队列中获取音频数据,由SDL解码后再进行声音的播放。

四、示例代码  
 condmutex.h
#ifndef CONDMUTEX_H
#define CONDMUTEX_H#include "SDL.h"class CondMutex {
public:CondMutex();~CondMutex();void lock();void unlock();void signal();void broadcast();void wait();private:/** 互斥锁 */SDL_mutex *_mutex = nullptr;/** 条件变量 */SDL_cond *_cond = nullptr;
};#endif // CONDMUTEX_H
condmutex.cpp 
#include "condmutex.h"CondMutex::CondMutex() {// 创建互斥锁_mutex = SDL_CreateMutex();// 创建条件变量_cond = SDL_CreateCond();
}CondMutex::~CondMutex() {SDL_DestroyMutex(_mutex);SDL_DestroyCond(_cond);
}void CondMutex::lock() {SDL_LockMutex(_mutex);
}void CondMutex::unlock() {SDL_UnlockMutex(_mutex);
}void CondMutex::signal() {SDL_CondSignal(_cond);
}void CondMutex::broadcast() {SDL_CondBroadcast(_cond);
}void CondMutex::wait() {SDL_CondWait(_cond, _mutex);
}
videoslider.h 
#ifndef VIDEOSLIDER_H
#define VIDEOSLIDER_H#include <QSlider>class VideoSlider : public QSlider {Q_OBJECT
public:explicit VideoSlider(QWidget *parent = nullptr);signals:void clicked(VideoSlider *slider);private:void mousePressEvent(QMouseEvent *ev) override;
};#endif // VIDEOSLIDER_H
videoslider.cpp 
#include "videoslider.h"
#include <QMouseEvent>
#include <QStyle>VideoSlider::VideoSlider(QWidget *parent) : QSlider(parent) {}void VideoSlider::mousePressEvent(QMouseEvent *ev) {// 根据点击位置的x值,计算出对应的valueint value = QStyle::sliderValueFromPosition(minimum(),maximum(),ev->pos().x(),width());setValue(value);QSlider::mousePressEvent(ev);// 发出信号emit clicked(this);
}
videowidget.h
#ifndef VIDEOWIDGET_H
#define VIDEOWIDGET_H#include <QWidget>
#include <QImage>
#include "videoplayer.h"/*** 显示(渲染)视频*/
class VideoWidget : public QWidget {Q_OBJECT
public:explicit VideoWidget(QWidget *parent = nullptr);~VideoWidget();public slots:void onPlayerFrameDecoded(VideoPlayer *player, uint8_t *data, VideoPlayer::VideoSwsSpec &spec);void onPlayerStateChanged(VideoPlayer *player);private:QImage *_image = nullptr;QRect _rect;void paintEvent(QPaintEvent *event) override;void freeImage();
};#endif // VIDEOWIDGET_H
videowidget.cpp 
#include "videowidget.h"
#include <QPainter>VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {// 设置背景色setAttribute(Qt::WA_StyledBackground);setStyleSheet("background: black");
}VideoWidget::~VideoWidget() {freeImage();
}void VideoWidget::onPlayerStateChanged(VideoPlayer *player) {if (player->getState() != VideoPlayer::Stopped) return;freeImage();update();
}void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player,uint8_t *data, VideoPlayer::VideoSwsSpec &spec) {if (player->getState() == VideoPlayer::Stopped) return;// 释放之前的图片freeImage();// 创建新的图片if (data != nullptr) {_image = new QImage((uchar *) data,spec.width, spec.height,QImage::Format_RGB888);// 计算最终的尺寸// 组件的尺寸int w = width();int h = height();// 计算rectint dx = 0;int dy = 0;int dw = spec.width;int dh = spec.height;// 计算目标尺寸if (dw > w || dh > h) { // 缩放if (dw * h > w * dh) { // 视频的宽高比 > 播放器的宽高比dh = w * dh / dw;dw = w;} else {dw = h * dw / dh;dh = h;}}// 居中dx = (w - dw) >> 1;dy = (h - dh) >> 1;_rect = QRect(dx, dy, dw, dh);}update();//触发paintEvent方法
}void VideoWidget::paintEvent(QPaintEvent *event) {if (!_image) return;// 将图片绘制到当前组件上QPainter(this).drawImage(_rect, *_image);
}void VideoWidget::freeImage() {if (_image) {av_free(_image->bits());delete _image;_image = nullptr;}
}
 videoplayer.h
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H#include <QObject>
#include <QDebug>
#include <list>
#include "condmutex.h"extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}#define ERROR_BUF \char errbuf[1024]; \av_strerror(ret, errbuf, sizeof (errbuf));#define CODE(func,code) \if (ret < 0) { \ERROR_BUF; \qDebug() << #func << "error" << errbuf; \code; \}#define END(func) CODE(func,fataError(); return;)
#define RET(func) CODE(func, return ret;)
#define CONTINUE(func) CODE(func, continue;)
#define BREAK(func) CODE(func, break;)/*** 预处理视频数据(不负责显示、渲染视频)*/
class VideoPlayer : public QObject {Q_OBJECT
public:// 状态typedef enum {Stopped = 0,Playing,Paused} State;// 音量typedef enum {Min = 0,Max = 100} Volumn;// 视频frame参数typedef struct {int width;int height;AVPixelFormat pixFmt;int size;} VideoSwsSpec;explicit VideoPlayer(QObject *parent = nullptr);~VideoPlayer();/** 播放 */void play();/** 暂停 */void pause();/** 停止 */void stop();/** 是否正在播放中 */bool isPlaying();/** 获取当前的状态 */State getState();/** 设置文件名 */void setFilename(QString &filename);/** 获取总时长(单位是妙,1秒=1000毫秒=1000000微妙)*/int getDuration();/** 当前的播放时刻(单位是秒) */int getTime();/** 设置当前的播放时刻(单位是秒) */void setTime(int seekTime);/** 设置音量 */void setVolumn(int volumn);int getVolumn();/** 设置静音 */void setMute(bool mute);bool isMute();signals:void stateChanged(VideoPlayer *player);void timeChanged(VideoPlayer *player);void initFinished(VideoPlayer *player);void playFailed(VideoPlayer *player);void frameDecoded(VideoPlayer *player,uint8_t *data,VideoSwsSpec &spec);private:/******** 音频相关 ********/typedef struct {int sampleRate;AVSampleFormat sampleFmt;int chLayout;int chs;int bytesPerSampleFrame;} AudioSwrSpec;/** 解码上下文 */AVCodecContext *_aDecodeCtx = nullptr;/** 流 */AVStream *_aStream = nullptr;/** 存放音频包的列表 */std::list<AVPacket> _aPktList;/** 音频包列表的锁 */CondMutex _aMutex;/** 音频重采样上下文 */SwrContext *_aSwrCtx = nullptr;/** 音频重采样输入\输出参数 */AudioSwrSpec _aSwrInSpec;AudioSwrSpec _aSwrOutSpec;/** 音频重采样输入\输出frame */AVFrame *_aSwrInFrame = nullptr;AVFrame *_aSwrOutFrame = nullptr;/** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */int _aSwrOutIdx = 0;/** 音频重采样输出PCM的大小 */int _aSwrOutSize = 0;/** 音量 */int _volumn = Max;/** 静音 */bool _mute = false;/** 音频时钟,当前音频包对应的时间值 */double _aTime = 0;/** 是否有音频流 */bool _hasAudio = false;/** 音频资源是否可以释放 */bool _aCanFree = false;/** 外面设置的当前播放时刻(用于完成seek功能) */int _aSeekTime = -1;/** 初始化音频信息 */int initAudioInfo();/** 初始化SDL */int initSDL();/** 添加数据包到音频包列表中 */void addAudioPkt(AVPacket &pkt);/** 清空音频包列表 */void clearAudioPktList();/** SDL填充缓冲区的回调函数 */static void sdlAudioCallbackFunc(void *userdata, Uint8 *stream, int len);/** SDL填充缓冲区的回调函数 */void sdlAudioCallback(Uint8 *stream, int len);/** 音频解码 */int decodeAudio();/** 初始化音频重采样 */int initSwr();/******** 视频相关 ********//** 解码上下文 */AVCodecContext *_vDecodeCtx = nullptr;/** 流 */AVStream *_vStream = nullptr;/** 像素格式转换的输入\输出frame */AVFrame *_vSwsInFrame = nullptr, *_vSwsOutFrame = nullptr;/** 像素格式转换的上下文 */SwsContext *_vSwsCtx = nullptr;/** 像素格式转换的输出frame的参数 */VideoSwsSpec _vSwsOutSpec;/** 存放视频包的列表 */std::list<AVPacket> _vPktList;/** 视频包列表的锁 */CondMutex _vMutex;/** 视频时钟,当前视频包对应的时间值 */double _vTime = 0;/** 是否有视频流 */bool _hasVideo = false;/** 视频资源是否可以释放 */bool _vCanFree = false;/** 外面设置的当前播放时刻(用于完成seek功能) */int _vSeekTime = -1;/** 初始化视频信息 */int initVideoInfo();/** 初始化视频像素格式转换 */int initSws();/** 添加数据包到视频包列表中 */void addVideoPkt(AVPacket &pkt);/** 清空视频包列表 */void clearVideoPktList();/** 解码视频 */void decodeVideo();/******** 其他 ********//** 当前的状态 */State _state = Stopped;/** fmtCtx是否可以释放 */bool _fmtCtxCanFree = false;/** 文件名 */QString _filename;// 解封装上下文AVFormatContext *_fmtCtx = nullptr;/** 外面设置的当前播放时刻(用于完成seek功能) */int _seekTime = -1;/** 初始化解码器和解码上下文 */int initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type);/** 改变状态 */void setState(State state);/** 读取文件数据 */void readFile();/** 释放资源 */void free();void freeAudio();void freeVideo();/** 严重错误 */void fataError();
};#endif // VIDEOPLAYER_H
videoplayer.cpp
#include "videoplayer.h"
#include <thread>#define AUDIO_MAX_PKT_SIZE 1000
#define VIDEO_MAX_PKT_SIZE 500VideoPlayer::VideoPlayer(QObject *parent) : QObject(parent) {// 初始化Audio子系统if (SDL_Init(SDL_INIT_AUDIO)) {// 返回值不是0,就代表失败qDebug() << "SDL_Init error" << SDL_GetError();emit playFailed(this);return;}
}VideoPlayer::~VideoPlayer() {// 不再对外发送消息disconnect();stop();SDL_Quit();
}void VideoPlayer::play() {if (_state == Playing) return;// 状态可能是:暂停、停止、正常完毕if(_state == Stopped){// 开始线程:读取文件std::thread([this](){readFile();}).detach();// detach 等到readFile方法执行完,这个线程就会销毁}else{setState(Playing);}
}void VideoPlayer::pause() {if (_state != Playing) return;// 状态可能是:正在播放setState(Paused);
}void VideoPlayer::stop() {if (_state == Stopped) return;// 状态可能是:正在播放、暂停、正常完毕// 改变状态_state = Stopped;// 释放资源free();// 通知外界emit stateChanged(this);
}bool VideoPlayer::isPlaying() {return _state == Playing;
}VideoPlayer::State VideoPlayer::getState() {return _state;
}void VideoPlayer::setFilename(QString &filename) {_filename = filename;
}int VideoPlayer::getDuration(){return _fmtCtx ? round(_fmtCtx->duration * av_q2d(AV_TIME_BASE_Q)) : 0;
}int VideoPlayer::getTime(){return round(_aTime);
}void VideoPlayer::setVolumn(int volumn){_volumn = volumn;
}void VideoPlayer::setTime(int seekTime){_seekTime = seekTime;
}int VideoPlayer::getVolumn(){return _volumn;
}void VideoPlayer::setMute(bool mute) {_mute = mute;
}bool VideoPlayer::isMute() {return _mute;
}void VideoPlayer::readFile(){   int ret = 0;// 创建解封装上下文、打开文件ret = avformat_open_input(&_fmtCtx,_filename.toUtf8().data(),nullptr,nullptr);END(avformat_open_input);// 检索流信息ret = avformat_find_stream_info(_fmtCtx,nullptr);END(avformat_find_stream_info);// 打印流信息到控制台av_dump_format(_fmtCtx,0,_filename.toUtf8().data(),0);fflush(stderr);// 初始化音频信息_hasAudio = initAudioInfo() >= 0;// 初始化视频信息_hasVideo = initVideoInfo() >= 0;if (!_hasAudio && !_hasVideo) {emit playFailed(this);free();return;}// 到此为止,初始化完毕emit initFinished(this);// 改变状态setState(Playing);// 音频解码子线程:开始工作SDL_PauseAudio(0);// 开启新的线程去解码视频数据std::thread([this](){decodeVideo();}).detach();// 从输入文件中读取数据AVPacket pkt;while (_state != Stopped) {// 处理seek操作if (_seekTime >= 0) {int streamIdx;if (_hasAudio) { // 优先使用音频流索引streamIdx = _aStream->index;} else {streamIdx = _vStream->index;}// 现实时间 -> 时间戳AVRational timeBase = _fmtCtx->streams[streamIdx]->time_base;int64_t ts = _seekTime / av_q2d(timeBase);//           ret = av_seek_frame(_fmtCtx, streamIdx, ts, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME);ret = avformat_seek_file(_fmtCtx, streamIdx, INT64_MIN, ts, INT64_MAX, 0);if(ret < 0){// seek失败qDebug() << "seek失败" << _seekTime << ts << streamIdx;_seekTime = -1;}else{// seek成功qDebug() << "seek成功" << _seekTime << ts << streamIdx;// 清空之前读取的数据包clearAudioPktList();clearVideoPktList();_vSeekTime = _seekTime;_aSeekTime = _seekTime;_seekTime = -1;// 恢复时钟_aTime = 0;_vTime = 0;}}int vSize = _vPktList.size();int aSize = _aPktList.size();if (vSize >= VIDEO_MAX_PKT_SIZE || aSize >= AUDIO_MAX_PKT_SIZE) {SDL_Delay(1);continue;}ret = av_read_frame(_fmtCtx, &pkt);if (ret == 0) {if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据addAudioPkt(pkt);} else if (pkt.stream_index == _vStream->index) { // 读取到的是视频数据addVideoPkt(pkt);}else{// 如果不是音频、视频流,直接释放av_packet_unref(&pkt);}} else if (ret == AVERROR_EOF) { // 读到了文件的尾部//           break;// seek的时候不能用breakif(vSize == 0 && aSize ==0){// 说明文件正常播放完毕_fmtCtxCanFree = true;break;}} else {ERROR_BUF;qDebug() << "av_read_frame error" << errbuf;continue;}}if (_fmtCtxCanFree) { // 文件正常播放完毕stop();} else {// 标记一下:_fmtCtx可以释放了_fmtCtxCanFree = true;}
}int VideoPlayer::initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type) {// 根据type寻找最合适的流信息// 返回值是流索引int ret = av_find_best_stream(_fmtCtx, type, -1, -1, nullptr, 0);RET(av_find_best_stream);// 检验流int streamIdx = ret;*stream = _fmtCtx->streams[streamIdx];if (!*stream) {qDebug() << "stream is empty";return -1;}// 为当前流找到合适的解码器const AVCodec *decoder = avcodec_find_decoder((*stream)->codecpar->codec_id);if (!decoder) {qDebug() << "decoder not found" << (*stream)->codecpar->codec_id;return -1;}// 初始化解码上下文*decodeCtx = avcodec_alloc_context3(decoder);if (!decodeCtx) {qDebug() << "avcodec_alloc_context3 error";return -1;}// 从流中拷贝参数到解码上下文中ret = avcodec_parameters_to_context(*decodeCtx, (*stream)->codecpar);RET(avcodec_parameters_to_context);// 打开解码器ret = avcodec_open2(*decodeCtx, decoder, nullptr);RET(avcodec_open2);return 0;
}void VideoPlayer::setState(State state) {if (state == _state) return;_state = state;emit stateChanged(this);
}void VideoPlayer::free(){while (_hasAudio && !_aCanFree);while (_hasVideo && !_vCanFree);while (!_fmtCtxCanFree);avformat_close_input(&_fmtCtx);_fmtCtxCanFree = false;_seekTime = -1;freeAudio();freeVideo();
}void VideoPlayer::fataError(){setState(Stopped);free();emit playFailed(this);
}
 videoplayer_audio.cpp
#include "videoplayer.h"// 初始化音频信息
int VideoPlayer::initAudioInfo() {int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);RET(initDecoder);// 初始化音频重采样ret = initSwr();RET(initSwr);// 初始化SDLret = initSDL();RET(initSDL);return 0;
}int VideoPlayer::initSwr() {// 重采样输入参数_aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;_aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;_aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;_aSwrInSpec.chs = _aDecodeCtx->channels;// 重采样输出参数_aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;_aSwrOutSpec.sampleRate = 44100;_aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;_aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);_aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs * av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt);// 创建重采样上下文_aSwrCtx = swr_alloc_set_opts(nullptr,// 输出参数_aSwrOutSpec.chLayout,_aSwrOutSpec.sampleFmt,_aSwrOutSpec.sampleRate,// 输入参数_aSwrInSpec.chLayout,_aSwrInSpec.sampleFmt,_aSwrInSpec.sampleRate,0, nullptr);if (!_aSwrCtx) {qDebug() << "swr_alloc_set_opts error";return -1;}// 初始化重采样上下文int ret = swr_init(_aSwrCtx);RET(swr_init);// 初始化重采样的输入frame_aSwrInFrame = av_frame_alloc();if (!_aSwrInFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化重采样的输出frame_aSwrOutFrame = av_frame_alloc();if (!_aSwrOutFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化重采样的输出frame的data[0]空间ret = av_samples_alloc(_aSwrOutFrame->data,_aSwrOutFrame->linesize,_aSwrOutSpec.chs,4096, _aSwrOutSpec.sampleFmt, 1);RET(av_samples_alloc);return 0;
}void VideoPlayer::freeAudio(){_aSwrOutIdx = 0;_aSwrOutSize =0;_aTime = 0;_aCanFree = false;_aSeekTime = -1;clearAudioPktList();avcodec_free_context(&_aDecodeCtx);swr_free(&_aSwrCtx);av_frame_free(&_aSwrInFrame);if(_aSwrOutFrame){av_freep(&_aSwrOutFrame->data[0]);// 因手动创建了data[0]的空间av_frame_free(&_aSwrOutFrame);}// 停止播放SDL_PauseAudio(1);SDL_CloseAudio();
}void VideoPlayer::sdlAudioCallbackFunc(void *userdata, uint8_t *stream, int len){VideoPlayer *player = (VideoPlayer *)userdata;player->sdlAudioCallback(stream,len);
}int VideoPlayer::initSDL(){// 音频参数SDL_AudioSpec spec;// 采样率spec.freq = _aSwrOutSpec.sampleRate;// 采样格式(s16le)spec.format = AUDIO_S16LSB;// 声道数spec.channels = _aSwrOutSpec.chs;// 音频缓冲区的样本数量(这个值必须是2的幂)spec.samples = 512;// 回调spec.callback = sdlAudioCallbackFunc;// 传递给回调的参数spec.userdata = this;// 打开音频设备if (SDL_OpenAudio(&spec, nullptr)) {qDebug() << "SDL_OpenAudio error" << SDL_GetError();return -1;}return 0;
}void VideoPlayer::addAudioPkt(AVPacket &pkt){_aMutex.lock();_aPktList.push_back(pkt);_aMutex.signal();_aMutex.unlock();
}void VideoPlayer::clearAudioPktList(){_aMutex.lock();for(AVPacket &pkt : _aPktList){av_packet_unref(&pkt);}_aPktList.clear();_aMutex.unlock();
}void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){// 清零(静音)SDL_memset(stream, 0, len);// len:SDL音频缓冲区剩余的大小(还未填充的大小)while (len > 0) {if (_state == Paused) break;if (_state == Stopped) {_aCanFree = true;break;}// 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了// 需要解码下一个pkt,获取新的PCM数据if (_aSwrOutIdx >= _aSwrOutSize) {// 全新PCM的大小_aSwrOutSize = decodeAudio();// 索引清0_aSwrOutIdx = 0;// 没有解码出PCM数据,那就静音处理if (_aSwrOutSize <= 0) {// 假定PCM的大小_aSwrOutSize = 1024;// 给PCM填充0(静音)memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);}}// 本次需要填充到stream中的PCM数据大小int fillLen = _aSwrOutSize - _aSwrOutIdx;fillLen = std::min(fillLen, len);// 获取当前音量int volumn = _mute ? 0 : ((_volumn * 1.0 / Max) * SDL_MIX_MAXVOLUME);// 填充SDL缓冲区SDL_MixAudio(stream,_aSwrOutFrame->data[0] + _aSwrOutIdx,fillLen, volumn);// 移动偏移量len -= fillLen;stream += fillLen;_aSwrOutIdx += fillLen;}
}/*** @brief VideoPlayer::decodeAudio* @return 解码出来的pcm大小*/
int VideoPlayer::decodeAudio(){// 加锁_aMutex.lock();if (_aPktList.empty() || _state == Stopped) {_aMutex.unlock();return 0;}// 取出头部的数据包AVPacket pkt = _aPktList.front();// 从头部中删除_aPktList.pop_front();// 解锁_aMutex.unlock();// 保存音频时钟if (pkt.pts != AV_NOPTS_VALUE) {_aTime = av_q2d(_aStream->time_base) *pkt.pts;// 通知外界:播放时间点发生了改变emit timeChanged(this);}// 如果是视频,不能在这个位置判断(不能提前释放pkt,不然会导致B帧、P帧解码失败,画面撕裂)// 发现音频的时间是早于seekTime的,直接丢弃if (_aSeekTime >= 0) {if (_aTime < _aSeekTime) {// 释放pktav_packet_unref(&pkt);return 0;} else {_aSeekTime = -1;}}// 发送压缩数据到解码器int ret = avcodec_send_packet(_aDecodeCtx, &pkt);// 释放pktav_packet_unref(&pkt);RET(avcodec_send_packet);// 获取解码后的数据ret = avcodec_receive_frame(_aDecodeCtx, _aSwrInFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;} else RET(avcodec_receive_frame);// 重采样输出的样本数int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,_aSwrInFrame->nb_samples,_aSwrInSpec.sampleRate, AV_ROUND_UP);// 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致,需要进行重采样ret = swr_convert(_aSwrCtx,_aSwrOutFrame->data,outSamples,(const uint8_t **) _aSwrInFrame->data,_aSwrInFrame->nb_samples);RET(swr_convert);return ret * _aSwrOutSpec.bytesPerSampleFrame;
}
videoplayer_video.cpp
#include "videoplayer.h"
#include <thread>
extern "C" {
#include <libavutil/imgutils.h>
}// 初始化视频信息
int VideoPlayer::initVideoInfo() {int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);RET(initDecoder);// 初始化像素格式转换ret = initSws();RET(initSws);return 0;
}int VideoPlayer::initSws(){int inW = _vDecodeCtx->width;int inH = _vDecodeCtx->height;// 输出frame的参数_vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保证是16的倍数_vSwsOutSpec.height = inH >> 4 << 4;_vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;_vSwsOutSpec.size = av_image_get_buffer_size(_vSwsOutSpec.pixFmt,_vSwsOutSpec.width,_vSwsOutSpec.height, 1);// 初始化像素格式转换的上下文_vSwsCtx = sws_getContext(inW,inH,_vDecodeCtx->pix_fmt,_vSwsOutSpec.width,_vSwsOutSpec.height,_vSwsOutSpec.pixFmt,SWS_BILINEAR, nullptr, nullptr, nullptr);if (!_vSwsCtx) {qDebug() << "sws_getContext error";return -1;}// 初始化像素格式转换的输入frame_vSwsInFrame = av_frame_alloc();if (!_vSwsInFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化像素格式转换的输出frame_vSwsOutFrame = av_frame_alloc();if (!_vSwsOutFrame) {qDebug() << "av_frame_alloc error";return -1;}// _vSwsOutFrame的data[0]指向的内存空间int ret = av_image_alloc(_vSwsOutFrame->data,_vSwsOutFrame->linesize,_vSwsOutSpec.width,_vSwsOutSpec.height,_vSwsOutSpec.pixFmt,1);RET(av_image_alloc);return 0;
}void VideoPlayer::addVideoPkt(AVPacket &pkt){_vMutex.lock();_vPktList.push_back(pkt);_vMutex.signal();_vMutex.unlock();
}void VideoPlayer::clearVideoPktList(){_vMutex.lock();for(AVPacket &pkt : _vPktList){av_packet_unref(&pkt);}_vPktList.clear();_vMutex.unlock();
}void VideoPlayer::freeVideo(){clearVideoPktList();avcodec_free_context(&_vDecodeCtx);av_frame_free(&_vSwsInFrame);if (_vSwsOutFrame) {av_freep(&_vSwsOutFrame->data[0]);av_frame_free(&_vSwsOutFrame);}sws_freeContext(_vSwsCtx);_vSwsCtx = nullptr;_vStream = nullptr;_vTime = 0;_vCanFree = false;_vSeekTime = -1;
}void VideoPlayer::decodeVideo(){while (true) {// 如果是暂停,并且没有Seek操作if (_state == Paused && _vSeekTime == -1) {continue;}if (_state == Stopped) {_vCanFree = true;break;}_vMutex.lock();if(_vPktList.empty()){_vMutex.unlock();continue;}// 取出头部的视频包AVPacket pkt = _vPktList.front();_vPktList.pop_front();_vMutex.unlock();// 视频时钟if (pkt.dts != AV_NOPTS_VALUE) {_vTime = av_q2d(_vStream->time_base) * pkt.dts;}// 发送压缩数据到解码器int ret = avcodec_send_packet(_vDecodeCtx, &pkt);// 释放pktav_packet_unref(&pkt);CONTINUE(avcodec_send_packet);while (true) {ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;} else BREAK(avcodec_receive_frame);// 一定要在解码成功后,再进行下面的判断// 发现视频的时间是早于seekTime的,直接丢弃if(_vSeekTime >= 0){if (_vTime < _vSeekTime) {continue;// 丢掉} else {_vSeekTime = -1;}}// 像素格式的转换sws_scale(_vSwsCtx,_vSwsInFrame->data, _vSwsInFrame->linesize,0, _vDecodeCtx->height,_vSwsOutFrame->data, _vSwsOutFrame->linesize);if(_hasAudio){// 有音频// 如果视频包过早被解码出来,那就需要等待对应的音频时钟到达while (_vTime > _aTime && _state == Playing) {SDL_Delay(1);}}uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size);memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size);// 发出信号emit frameDecoded(this,data,_vSwsOutSpec);qDebug()<< "渲染了一帧"<< _vTime << _aTime;}}
}
 界面设计mainwindow.ui

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include "videoplayer.h"
#include "videoslider.h"QT_BEGIN_NAMESPACE
namespace Ui {class MainWindow;
}
QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void onPlayerStateChanged(VideoPlayer *player);void onPlayerTimeChanged(VideoPlayer *player);void onPlayerInitFinished(VideoPlayer *player);void onPlayerPlayFailed(VideoPlayer *player);void onSliderClicked(VideoSlider *slider);void on_stopBtn_clicked();void on_openFileBtn_clicked();void on_currentSlider_valueChanged(int value);void on_volumnSlider_valueChanged(int value);void on_playBtn_clicked();void on_muteBtn_clicked();private:Ui::MainWindow *ui;VideoPlayer *_player;QString getTimeText(int value);
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>#define FILEPATH "../test/"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);// 注册信号的参数类型,保证能够发出信号qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");// 创建播放器_player = new VideoPlayer();connect(_player, &VideoPlayer::stateChanged,this, &MainWindow::onPlayerStateChanged);connect(_player, &VideoPlayer::timeChanged,this, &MainWindow::onPlayerTimeChanged);connect(_player, &VideoPlayer::initFinished,this, &MainWindow::onPlayerInitFinished);connect(_player, &VideoPlayer::playFailed,this, &MainWindow::onPlayerPlayFailed);connect(_player, &VideoPlayer::frameDecoded,ui->videoWidget, &VideoWidget::onPlayerFrameDecoded);connect(_player, &VideoPlayer::stateChanged,ui->videoWidget, &VideoWidget::onPlayerStateChanged);// 监听时间滑块的点击connect(ui->currentSlider, &VideoSlider::clicked,this, &MainWindow::onSliderClicked);// 设置音量滑块的范围ui->volumnSlider->setRange(VideoPlayer::Volumn::Min,VideoPlayer::Volumn::Max);ui->volumnSlider->setValue(ui->volumnSlider->maximum() >> 2);
}MainWindow::~MainWindow() {delete ui;delete _player;
}void MainWindow::onSliderClicked(VideoSlider *slider) {_player->setTime(slider->value());
}void MainWindow::onPlayerPlayFailed(VideoPlayer *player) {QMessageBox::critical(nullptr,"提示","播放失败");
}void MainWindow::onPlayerTimeChanged(VideoPlayer *player) {ui->currentSlider->setValue(player->getTime());
}void MainWindow::onPlayerInitFinished(VideoPlayer *player) {int duration = player->getDuration();qDebug()<< duration;// 设置一些slider的范围ui->currentSlider->setRange(0,duration);// 设置label的文字ui->durationLabel->setText(getTimeText(duration));
}/*** onPlayerStateChanged方法的发射虽然在子线程中执行(VideoPlayer::readFile()),* 但是此方法是在主线程执行,因为它的connect是在主线程执行的*/
void MainWindow::onPlayerStateChanged(VideoPlayer *player) {VideoPlayer::State state = player->getState();if (state == VideoPlayer::Playing) {ui->playBtn->setText("暂停");} else {ui->playBtn->setText("播放");}if (state == VideoPlayer::Stopped) {ui->playBtn->setEnabled(false);ui->stopBtn->setEnabled(false);ui->currentSlider->setEnabled(false);ui->volumnSlider->setEnabled(false);ui->muteBtn->setEnabled(false);ui->durationLabel->setText(getTimeText(0));ui->currentSlider->setValue(0);// 显示打开文件的页面ui->playWidget->setCurrentWidget(ui->openFilePage);} else {ui->playBtn->setEnabled(true);ui->stopBtn->setEnabled(true);ui->currentSlider->setEnabled(true);ui->volumnSlider->setEnabled(true);ui->muteBtn->setEnabled(true);// 显示播放视频的页面ui->playWidget->setCurrentWidget(ui->videoPage);}
}void MainWindow::on_stopBtn_clicked() {_player->stop();
}void MainWindow::on_openFileBtn_clicked() {QString filename = QFileDialog::getOpenFileName(nullptr,"选择多媒体文件",FILEPATH,"多媒体文件 (*.mp4 *.avi *.mkv *.mp3 *.aac)");qDebug() << "打开文件" << filename;if (filename.isEmpty()) return;// 开始播放打开的文件_player->setFilename(filename);_player->play();
}void MainWindow::on_currentSlider_valueChanged(int value) {ui->currentLabel->setText(getTimeText(value));
}void MainWindow::on_volumnSlider_valueChanged(int value) {ui->volumnLabel->setText(QString("%1").arg(value));_player->setVolumn(value);
}void MainWindow::on_playBtn_clicked() {VideoPlayer::State state = _player->getState();if (state == VideoPlayer::Playing) {_player->pause();} else {_player->play();}
}QString MainWindow::getTimeText(int value){QString h = QString("0%1").arg(value / 3600).right(2);QString m = QString("0%1").arg((value / 60) % 60).right(2);QString s = QString("0%1").arg(value % 60).right(2);return  QString("%1:%2:%3").arg(h).arg(m).arg(s);
}void MainWindow::on_muteBtn_clicked()
{if (_player->isMute()) {_player->setMute(false);ui->muteBtn->setText("静音");} else {_player->setMute(true);ui->muteBtn->setText("开音");}
}

        通过以上的实现,我们就可以得到一个简单的录音软件,它可以利用QT实现录音,使用ffmpeg进行音频重采样,并使用fdk-aac进行编码。这个录音软件不仅简单易用,可以帮助我们记录和存储语音信息,是一个非常实用的工具。

五、运行效果

​​​​​​​

        谢谢您的阅读。希望本文能对您有所帮助,并且给您带来了一些新的观点和思考。如果您有任何问题或意见,请随时与我联系。再次感谢您的支持!

 六、相关文章

Windosw下Visual Studio2022编译FFmpeg(支持x264、x265、fdk-acc)-CSDN博客

相关文章:

利用QT和FFmpeg实现一个简单的视频播放器

在当今的多媒体世界中&#xff0c;视频播放已成为不可或缺的一部分。从简单的媒体播放器到复杂的视频编辑软件&#xff0c;视频解码和显示技术无处不在。本示例使用Qt和FFmpeg构建一个简单的视频播放器。利用ffmpeg解码视频&#xff0c;通过QWidget渲染解码后的图像&#xff0c…...

怎么用云手机进行TikTok矩阵运营

TikTok作为炙手可热的社交媒体巨头&#xff0c;已经吸引了亿万用户的目光。随着科技的飞速发展&#xff0c;云手机的出现为TikTok矩阵运营注入了新的活力。本文将深入探讨云手机在TikTok矩阵运营中的实际应用&#xff0c;并分享一系列高效策略与技巧。 &#xff08;1&#xff0…...

TCP/IP 协议及其协议号

协议号十六进制协议号协议介绍10x1ICMP (Internet Control Message Protocol)20x2IGMP (Internet Group Management Protocol) 30x3GGP (Gateway-to-Gateway Protocol) 40x4IPv4 (encapsulation) 50x5ST (Stream Protocol) 60x6TCP (Transm…...

【传知代码】机器情绪及抑郁症算法 四(论文复现)

在现代心理健康研究中&#xff0c;抑郁症一直是一个备受关注的课题。随着科学的进步&#xff0c;研究人员逐渐认识到&#xff0c;抑郁症的成因远不止单一因素&#xff0c;而是由复杂的生物学、心理学和社会环境因素交织而成的。最近&#xff0c;MSA&#xff08;综合性综合性模型…...

C#开启和关闭UAC功能

在开发软件或制作安装包时&#xff0c;有时会需要管理员权限 &#xff0c;但是又不想弹出UAC对话框。 可以编写一个小工具&#xff0c;检测UAC是否关闭。如果没有关闭&#xff0c;就自动关闭UAC。 实现比较简单&#xff0c; 找到注册表 计算机\HKEY_LOCAL_MACHINE\SOFTWARE…...

LVS的简单配置及对Mysql主从复制的补充

Day 22 LVS的配置 环境准备 DSN() 用来解析各主机的域名和ip地址&#xff0c;配置域名解析huajuan&#xff0c;负责管理其他主机 web1--->web1.tangpin.huajuan web2--->web2.tangpin.huajuan dns--->dns.tangpin.huajuan web1(192.168.2.200) 用nginx…...

七夕情人节特辑:程序员的浪漫惊喜,9个表白源码,甜蜜编程陪你过节

大家好呀&#x1f44b;&#xff0c;今天是中国的七夕情人节&#xff0c;一个充满浪漫与爱的日子。为了庆祝这个特别的节日&#xff0c;我为大家精心准备了9个表白专用的前端小项目。这些项目涵盖了“我爱你”网站、爱情表白网站和心形动画等&#xff0c;通过HTML、CSS和一点点J…...

Mask-Rcnn

一 、FPN层 FPN层的基本作用 基本网络架构 基本思想 将多个阶段特征图融合在一起&#xff0c;这就相当于既有了高层的语义特征&#xff0c;也有了低层的轮廓特征 二、RPN层 三、ROI Align层...

Python图像背景去除

目录 &#x1f381;库的导入 &#x1f380;库的安装 &#x1f381;rembg库去除背景 &#x1f381;效果 &#x1f381;文末彩蛋 今天来介绍一个特别有趣的python库&#xff0c;rembg库&#xff0c;全称是“Remove Background”的缩写&#xff0c;意为“去除背景”&#xff…...

【C语言篇】C语言常考及易错题整理DAY1

文章目录 C语言常考及易错题整理选择题全局、局部和静态变量#define与typedef转义字符操作符循环其他 编程题计算日期到天数转换柯尼希定理旋转数组的最小数字描述错误的集合整数转换密码检查 C语言常考及易错题整理 选择题 全局、局部和静态变量 执行下面程序&#xff0c;正…...

MySQL5.7之源码安装

文章目录 下载编译&打包初始化数据目录启动服务器更改/设置root密码 下载 下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 推荐下载 All Operating Systems (Generic) (Architecture Independent), Compressed TAR ArchiveIncludes Boost Headers …...

【Linux学习 | 第3篇】Linux系统安装 jdk+Tomcat+MySQL+lrzsz

文章目录 Linux—day31. 软件安装方式2. 安装jdk3. 安装Tomcat3.1 安装步骤&#xff1a;3.2 防火墙操作3.3 停止Tomcat服务的方式 4. 安装MySQL5. 安装lrzsz5.1 操作步骤 Linux—day3 Linux系统中软件安装 1. 软件安装方式 二进制发布包安装&#xff1a;软件已经针对具体平台…...

python语言day5 MD5 json

md5&#xff1a; python提供了内置的md5加密功能&#xff0c;使用md5模拟一个小项目&#xff1a; 注册&#xff1a; 启动py程序&#xff0c;在控制台界面提示用户输入用户名及密码&#xff1b; 使用md5加密 密码&#xff1b; 创建txt文件记录输入的用户名 和密文。 登录&…...

【Python学习手册(第四版)】学习笔记19-函数的高级话题

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文主要介绍函数相关的高级概念&#xff1a;递归函数、函数注解、lambda表达式函数&#xff0c;常用函数工具如map、filter、reduce&#xff0c;以及通用的函数设…...

Selenium + Python 自动化测试11(unittest组织用例)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以独立完成自动化测试的任务。 上一篇我们讨论了unittest基本使用方法。 本篇文章我们接着讲。一些概念和一些常用的构造测试集的方法。 1、基本概念 1&#xff09;Test Case 一个Test Case的实例就是一个测…...

【唐氏题目 nt题】与众不同

# 与众不同 ## 题目描述 A是某公司的CEO&#xff0c;每个月都会有员工把公司的盈利数据送给A&#xff0c;A是个与众不同的怪人&#xff0c;A不注重盈利还是亏本&#xff0c;而是喜欢研究「完美序列」&#xff1a;一段连续的序列满足序列中的数互不相同。 A想知道区间[L,R]之…...

2000块的活嫌低?这个 6 位数的项目,你可不能错过哟!

2000块钱嫌低&#xff1f;这个6位数的项目&#xff0c;你可不能错过&#xff0c;关注有好礼。 最近写了一篇“接了一个2000块钱的活&#xff0c;大家看看值不值”的文章&#xff0c;发现流量和大家互动的热情出奇的高&#xff0c;可能是跟有钱有关的缘故&#xff0c;大家不是奔…...

【Postman工具】

一.接口扫盲 1.什么是接口&#xff1f; 接口是系统之间数据交互的通道。拿小红到沙县点餐为例&#xff1a;小红想吃鸭腿饭。她要用什么语言来表达&#xff1f;跟谁表达&#xff1f;通过什么表达&#xff1f;按照生活习惯应该是&#xff1a;小红根据菜单对服务员用中文表达她想要…...

全网超详细攻略-从入门到精通haproxy七层代理

目录 一.haproxy概述 1.1 haproxy简介 1.2 haproxy的主要特性 1.3 haproxy的优缺点 二.负载均衡介绍 2.1 什么是负载均衡 2.2 为什么用负载均衡 2.3 负载均衡类型 2.3.1 四层负载均衡 2.3.2 七层负载均衡 2.3.3 四层和七层的区别 三.haproxy的安装及服务 3.1 实验环…...

AI编程辅助工具:CodeGeeX 插件使用

CodeGeeX 插件使用 前言1.支持的平台2.安装步骤3.启用插件4.代码生成5.代码优化 前言 CodeGeeX 是一款基于 AI 技术的编程助手插件&#xff0c;旨在帮助开发者提高编程效率和代码质量。它能够智能生成代码、优化现有代码、自动生成文档以及回答编程相关的问题。无论您是初学者…...

sql注入实战——thinkPHP

sql注入实战——thinkPHP sql注入实战——thinkPHPthinkPHP前期环境搭建创建数据库开始寻找漏洞点输入SQL注入语句漏洞分析 实验错误 sql注入实战——thinkPHP thinkPHP前期环境搭建 下载thinkPHP文件 解压&#xff0c;将framework关键文件放到think-5.0.15中&#xff0c;改…...

MySQL 迁移 OceanBase 的 Oracle模式中,实现自增主键的方法

本文作者&#xff1a;赵黎明&#xff0c;爱可生 MySQL DBA 团队成员&#xff0c;熟练掌握Oracle、MySQL等数据库系统&#xff0c;擅长对数据库性能问题的诊断&#xff0c;以及事务与锁机制的分析等。负责解决客户在MySQL及爱可生自主研发的DMP平台日常运维中所遇到的各种问题&a…...

【C++ 面试 - 基础题】每日 3 题(十一)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/fYaBd &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏&…...

ESP8266在线升级OTA固件

OTA的基本实现方式&#xff1a; ESP8266 的 OTA 实现有几种方式&#xff0c;常用的方式包括&#xff1a; 1、Arduino OTA&#xff1a;使用Arduino IDE提供的OTA功能&#xff0c;可以直接通过Arduino IDE上传固件到ESP8266。 2、Web OTA&#xff1a;ESP8266运行一个简易的Web服…...

精通C++ STL(六):list的模拟实现

目录 类及其成员函数接口总览 结点类的模拟实现 构造函数 迭代器类的模拟实现 迭代器类存在的意义 迭代器类的模板参数说明 构造函数 运算符的重载 --运算符的重载 运算符的重载 !运算符的重载 *运算符的重载 ->运算符的重载 list的模拟实现 默认成员函数 构造函数 拷贝…...

《雅思口语真经总纲1.0》话题实战训练笔记part1——6. Music

《雅思口语真经总纲1.0》笔记——第四章&#xff1a;口语素材大全&#xff08;part1、part2、part3回答准则及练习方法&#xff0c;不包括范例答案&#xff09;★★★★★ 文章目录 MusicWhen do you listen to music?20240804答评价注意事项1、在说到“no music”时&#xff…...

Python之赋值语句(多重赋值和交换赋值)

这是《Python入门经典以解决计算问题为导向的Python编程实践》73-74页关于赋值的内容。讲了Python中几种赋值方式。 赋值语句 1、最简单的赋值&#xff1a;ab2、多重赋值&#xff1a;a,b,c1,2,33、交换&#xff1a;a,bb,a 1、最简单的赋值&#xff1a;ab b可以是数字、字符串…...

网络协议七 应用层 HTTP 协议

应用层常见的协议 HTTP协议 1. 如何查看我们的http 协议全部的内容有哪些呢&#xff1f; 一种合理的方法是 通过 wireshark 软件&#xff0c;找到想要查看的HTTP --->追踪流--->HTTP流 来查看 结果如下&#xff1a;红色部分 为 发送给服务器的&#xff0c;蓝色部分为服务…...

uniapp vue 在适配百度小程序平台动态:style

uniapp vue 在适配百度小程序平台动态:style踩坑报错Unexpected string concatenation of literals 抖快平台动态style写法基本是 <view :style"{width: 686rpx, height: (setHeight 96) rpx}"> </view>这种写法在百度上会又解析报错&#xff1a; Une…...

【最小生成树】(二) Kruskal 算法

题目: 寻宝 题目描述 在世界的某个区域&#xff0c;有一些分散的神秘岛屿&#xff0c;每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路&#xff0c;方便运输。 不同岛屿之间&#xff0c;路途距离不同&#xff0c;国王希望你可以规划建公路的方案&#xf…...