**在 VS 2022 和 Qt 环境下利用 FFmpeg 实现一个基础视频播放器,需要完成以下几个步骤:
准备工作:
下载并配置 FFmpeg。确保 FFmpeg 的库和头文件可供 VS 2022 项目使用。
配置 Qt 项目,并导入 FFmpeg 库。
项目结构:
创建一个 Qt 项目,可以选择 Qt Widgets Application 类型,用于构建基本的 GUI 界面。
设计 GUI 界面:
添加视频显示区域(可以使用 QLabel 来显示视频帧)。
添加播放控制按钮(如播放、暂停按钮)。
添加进度条(QSlider)和全屏切换按钮。
初始化 FFmpeg:
使用 FFmpeg 的 API,初始化解码器并打开视频文件。
从视频流中提取音频和视频流。
视频解码和显示:
使用 FFmpeg 解码视频帧,然后将解码后的 YUV 数据转换为 RGB 格式,以便使用 Qt 显示。
将 RGB 格式的视频帧显示在 QLabel 上,可以使用 QImage 和 QPixmap 来显示帧图像。
音频解码和播放:
解码音频流并播放,可以使用 SDL 库来处理音频播放。
需要确保音频和视频的同步,通常通过音视频时间戳(PTS)来同步。
播放控制:
为播放、暂停、全屏等功能创建槽函数,控制视频解码和显示。
使用 QSlider 控制进度条的拖动,并同步视频播放进度。
在开始之前,确保已经安装并配置了以下库:
FFmpeg:用于视频和音频解码。
SDL2:用于音频播放。
代码示例如下:
#include <QMainWindow>
#include <QTimer>
#include <QSlider>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QWidget>
#include <QImage>
#include <QPixmap>
#include <QFileDialog>
// FFmpeg 头文件
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
}
// SDL2 头文件
#include <SDL2/SDL.h>
class VideoPlayer : public QMainWindow {
Q_OBJECT
public:
VideoPlayer(QWidget *parent = nullptr);
~VideoPlayer();
private slots:
void play();
void pause();
void updateFrame();
private:
void initializeFFmpeg();
void initializeSDL();
void decodeVideo();
void decodeAudio();
void displayFrame(AVFrame *frame);
static void audioCallback(void *userdata, Uint8 *stream, int len);
AVFormatContext *formatContext;
AVCodecContext *videoCodecContext;
AVCodecContext *audioCodecContext;
SwsContext *swsContext;
SwrContext *swrContext;
int videoStreamIndex;
int audioStreamIndex;
QLabel *videoLabel;
QPushButton *playButton;
QPushButton *pauseButton;
QSlider *progressSlider;
QTimer *timer;
// SDL相关
SDL_AudioSpec wantedSpec, obtainedSpec;
uint8_t *audioBuffer;
int audioBufferSize;
int audioBufferIndex;
bool isPlaying;
};
VideoPlayer::VideoPlayer(QWidget *parent) : QMainWindow(parent), isPlaying(false) {
// GUI 部件初始化
videoLabel = new QLabel(this);
playButton = new QPushButton("Play", this);
pauseButton = new QPushButton("Pause", this);
progressSlider = new QSlider(Qt::Horizontal, this);
timer = new QTimer(this);
// 布局
QHBoxLayout *controlLayout = new QHBoxLayout;
controlLayout->addWidget(playButton);
controlLayout->addWidget(pauseButton);
controlLayout->addWidget(progressSlider);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(videoLabel);
mainLayout->addLayout(controlLayout);
QWidget *centralWidget = new QWidget(this);
centralWidget->setLayout(mainLayout);
setCentralWidget(centralWidget);
// 初始化 FFmpeg 和 SDL
initializeFFmpeg();
initializeSDL();
// 连接信号和槽
connect(playButton, &QPushButton::clicked, this, &VideoPlayer::play);
connect(pauseButton, &QPushButton::clicked, this, &VideoPlayer::pause);
connect(timer, &QTimer::timeout, this, &VideoPlayer::updateFrame);
}
void VideoPlayer::initializeFFmpeg() {
av_register_all();
formatContext = avformat_alloc_context();
// 打开视频文件并找到流
avformat_open_input(&formatContext, "path/to/video.mp4", nullptr, nullptr);
avformat_find_stream_info(formatContext, nullptr);
// 找到视频流和音频流
for (unsigned int i = 0; i < formatContext->nb_streams; i++) {
AVCodecParameters *codecParams = formatContext->streams[i]->codecpar;
AVCodec *codec = avcodec_find_decoder(codecParams->codec_id);
if (codecParams->codec_type == AVMEDIA_TYPE_VIDEO) {
videoCodecContext = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(videoCodecContext, codecParams);
avcodec_open2(videoCodecContext, codec, nullptr);
videoStreamIndex = i;
} else if (codecParams->codec_type == AVMEDIA_TYPE_AUDIO) {
audioCodecContext = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(audioCodecContext, codecParams);
avcodec_open2(audioCodecContext, codec, nullptr);
audioStreamIndex = i;
swrContext = swr_alloc();
swr_alloc_set_opts(swrContext, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16,
audioCodecContext->sample_rate, audioCodecContext->channel_layout,
audioCodecContext->sample_fmt, audioCodecContext->sample_rate, 0, nullptr);
swr_init(swrContext);
}
}
// 视频缩放上下文
swsContext = sws_getContext(videoCodecContext->width, videoCodecContext->height,
videoCodecContext->pix_fmt, videoCodecContext->width,
videoCodecContext->height, AV_PIX_FMT_RGB24, SWS_BILINEAR,
nullptr, nullptr, nullptr);
}
void VideoPlayer::initializeSDL() {
SDL_Init(SDL_INIT_AUDIO);
wantedSpec.freq = audioCodecContext->sample_rate;
wantedSpec.format = AUDIO_S16SYS;
wantedSpec.channels = 2;
wantedSpec.silence = 0;
wantedSpec.samples = 1024;
wantedSpec.callback = audioCallback;
wantedSpec.userdata = this;
SDL_OpenAudio(&wantedSpec, &obtainedSpec);
}
void VideoPlayer::play() {
isPlaying = true;
SDL_PauseAudio(0); // 开始播放音频
timer->start(30); // 30ms刷新一次
}
void VideoPlayer::pause() {
isPlaying = false;
SDL_PauseAudio(1); // 暂停音频
timer->stop();
}
void VideoPlayer::updateFrame() {
decodeVideo();
}
void VideoPlayer::decodeVideo() {
AVPacket packet;
while (av_read_frame(formatContext, &packet) >= 0) {
if (packet.stream_index == videoStreamIndex) {
avcodec_send_packet(videoCodecContext, &packet);
AVFrame *frame = av_frame_alloc();
if (avcodec_receive_frame(videoCodecContext, frame) == 0) {
displayFrame(frame);
}
av_frame_free(&frame);
} else if (packet.stream_index == audioStreamIndex) {
decodeAudio();
}
av_packet_unref(&packet);
}
}
void VideoPlayer::decodeAudio() {
AVPacket packet;
while (av_read_frame(formatContext, &packet) >= 0) {
if (packet.stream_index == audioStreamIndex) {
avcodec_send_packet(audioCodecContext, &packet);
AVFrame *frame = av_frame_alloc();
if (avcodec_receive_frame(audioCodecContext, frame) == 0) {
int dataSize = av_samples_get_buffer_size(nullptr, obtainedSpec.channels,
frame->nb_samples, AV_SAMPLE_FMT_S16, 1);
audioBuffer = (uint8_t *)av_malloc(dataSize);
swr_convert(swrContext, &audioBuffer, frame->nb_samples,
(const uint8_t **)frame->data, frame->nb_samples);
audioBufferSize = dataSize;
audioBufferIndex = 0;
}
av_frame_free(&frame);
}
av_packet_unref(&packet);
}
}
void VideoPlayer::displayFrame(AVFrame *frame) {
// 转换YUV为RGB
AVFrame *rgbFrame = av_frame_alloc();
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, videoCodecContext->width,
videoCodecContext->height, 1);
uint8_t *buffer = (uint8_t *)av_malloc(numBytes);
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer, AV_PIX_FMT_RGB24,
videoCodecContext->width, videoCodecContext->height, 1);
sws_scale(swsContext, frame->data, frame->linesize, 0, videoCodecContext->height,
rgbFrame->data, rgbFrame->linesize);
// 将RGB帧转化为QImage并显示
QImage img(rgbFrame->data[0], videoCodecContext->width, videoCodecContext->height,
QImage::Format_RGB888);
videoLabel->setPixmap(QPixmap::fromImage(img));
av_free(buffer);
av_frame_free(&rgbFrame);
}
void VideoPlayer::audioCallback(void *userdata, Uint8 *stream, int len) {
VideoPlayer *player = (VideoPlayer *)userdata;
if (player->audioBufferSize - player->audioBufferIndex <= 0) {
return;
}
len = (len > player->audioBufferSize - player->audioBufferIndex) ?
player->audioBufferSize - player->audioBufferIndex : len;
SDL_memcpy(stream, player->audioBuffer + player->audioBufferIndex, len);
player->audioBufferIndex += len;
}
VideoPlayer::~VideoPlayer() {
SDL_CloseAudio();
SDL_Quit();
avcodec_free_context(&videoCodecContext);
avcodec_free_context(&audioCodecContext);
avformat_close_input(&formatContext);
sws_freeContext(swsContext);
swr_free(&swrContext);
}
#include "main.moc"
说明
视频解码与显示:通过 sws_scale 函数将解码的 YUV 视频帧转换为 RGB 格式并显示在 QLabel 上。
音频解码与播放:使用 SDL_OpenAudio 打开音频设备,通过 audioCallback 回调函数播放解码后的音频数据。
音视频同步:基本通过 timer 和 audioCallback 函数保持音视频同步。