关于鸣潮启动器450张图片杂谈—从代码分析为何使用帧动画
前言
在鸣潮启动器的目录下
Wuthering Waves\kr_game_cache\animate_bg\99de27ae82e3c370286fba14c4fcb699
打开该目录发现有450张图片,不难看出启动器的背景动画是由这450张图片不断切换实现的
qt框架
从动态库能很明显的看出启动器是用qt5写的,而使用qt实现动态背景图的方式主要有以下几种:1.帧动画,也是官方启动器选择的方式 2.使用ffmpeg等开源音视频解码库对视频文件进行解码,3.使用外部解码软件,4.使用gif动图
帧动画
首先来看第一个解决方案,也是最简单,效果也不错的方案,以下是代码,非常简单一共也就十几行,直接一个定时器不断切换背景图片路径就行了
static int index = 0;
AnimatedBackground::AnimatedBackground(QWidget *parent)
: QWidget{parent}
{
this->setFixedSize(1280,760);
m_timer = new QTimer(this);
connect(m_timer,&QTimer::timeout,this,[&]()mutable{
index%=450;
index++;
QString path = "D:\\Wuthering Waves\\kr_game_cache\\animate_bg\\99de27ae82e3c370286fba14c4fcb699\\home_"+QString::number(index)+".jpg";
m_currentBackground.load(path);
update();
});
m_timer->start(1000/33);
}
void AnimatedBackground::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.drawPixmap(rect(),m_currentBackground);
}
来看效果:
使用ffmpeg软解码视频
qt框架自己并没有附带解码器,要实现播放视频需要解码库或者解码软件,这里使用开源的ffmpeg对视频进行解码
首先要将ffmpeg添加到自己的项目:
- 下载源码并编译(略)
- 将编译好的库文件和头文件添加到项目(略)
- 编写Cmake/qmake 文件将库链接到项目(略)
实现方案:
由于解码视频是需要时间的,如果等视频流所有的帧都解码完才显示会有几秒左右的延迟,要想实现第一个方案打开就有动画效果,需要解码和显示同时进行
需要一个队列对帧数据进行缓存,所以选择基于生产者-消费者的设计模式的线程模型 ,以下是原理图:
涉及多线程的的话,当然需要锁啦 先封装个锁:
semaphore.h
#ifndef SEMAPHORE_H
#define SEMAPHORE_H
#include <atomic>
#include <condition_variable>
#include <mutex>
class Semaphore
{
public:
explicit Semaphore(int i = 0) {
m_semaphore.store(i < 0 ? 0 : i);
}
Semaphore(const Semaphore &) = delete;
Semaphore& operator=(const Semaphore &) = delete;
void acquire(int i = 1) {
if (i <= 0) return;
std::unique_lock<std::mutex> lock(m_mutex);
if (m_semaphore.load() < i) {
m_conditionVar.wait(lock);
}
m_semaphore.fetch_sub(i);
}
bool tryAcquire(int i = 1) {
if (i <= 0) return false;
if (m_semaphore.load() >= i) {
m_semaphore.fetch_sub(i);
return true;
} else return false;
}
void release(int i = 1) {
if (i <= 0) return;
m_semaphore.fetch_add(i);
m_conditionVar.notify_one();
}
int available() const {
return m_semaphore.load();
}
private:
std::condition_variable m_conditionVar;
std::atomic_int m_semaphore;
std::mutex m_mutex;
};
#endif
实现缓存队列bufferqueue.h
#ifndef BUFFERQUEUE_H
#define BUFFERQUEUE_H
#ifdef DEBUG_OUTPUT
#include <iostream>
#endif
#include "semaphore.h"
#include <vector>
template <class T> class BufferQueue
{
public:
BufferQueue(int bufferSize = 100) {
setBufferSize(bufferSize);
}
~BufferQueue() {
init();
std::vector<T>().swap(m_bufferQueue);
}
void setBufferSize(int bufferSize) {
m_bufferSize = bufferSize;
m_bufferQueue = std::vector<T>(bufferSize);
m_useableSpace.acquire(m_useableSpace.available());
m_freeSpace.release(m_bufferSize - m_freeSpace.available());
m_front = m_rear = 0;
}
void enqueue(const T &element) {
#ifdef DEBUG_OUTPUT
std::cout << "[freespace " << m_freeSpace.available()
<< "] --- [useablespace " << m_useableSpace.available() << "]" << std::endl;
#endif
m_freeSpace.acquire();
m_bufferQueue[m_front++ % m_bufferSize] = element;
m_useableSpace.release();
}
T dequeue() {
#ifdef DEBUG_OUTPUT
std::cout << "[freespace " << m_freeSpace.available()
<< "] --- [useablespace " << m_useableSpace.available() << "]" << std::endl;
#endif
m_useableSpace.acquire();
T element = m_bufferQueue[m_rear++ % m_bufferSize];
m_freeSpace.release();
return element;
}
/**
* @brief tryDequeue
* @note 尝试获取一个元素,并且在失败时不会阻塞调用线程
* @return 成功返回对应T元素,失败返回默认构造的T元素
*/
T tryDequeue() {
T element;
bool success = m_useableSpace.tryAcquire();
if (success) {
element = m_bufferQueue[m_rear++ % m_bufferSize];
m_freeSpace.release();
}
return element;
}
void init() {
m_useableSpace.acquire(m_useableSpace.available());
m_freeSpace.release(m_bufferSize - m_freeSpace.available());
m_front.store(0);
m_rear.store(0);
}
private:
// -1 +1
// [free space] -> [useable space]
Semaphore m_freeSpace;
Semaphore m_useableSpace;
std::atomic_int m_rear;
std::atomic_int m_front;
std::vector<T> m_bufferQueue;
int m_bufferSize;
};
#endif
封装视频解码类:
videodecoder.h
class VideoDecoder : public QThread
{
Q_OBJECT
public:
VideoDecoder(QObject *parent = nullptr);
~VideoDecoder();
void stop();
void open(const QString &filename);
int fps() const { return m_fps; }
int width() const { return m_width; }
int height() const { return m_height; }
QImage currentFrame();
signals:
void resolved();
void finish();
protected:
void run();
private:
void demuxing_decoding();
bool m_runnable = true;
QMutex m_mutex;
QString m_filename;
BufferQueue<QImage> m_frameQueue;
int m_fps, m_width, m_height;
};
videodecoder.cpp
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#include <QApplication>
#include <QHBoxLayout>
#include <QMimeData>
#include <QPushButton>
#include <QPainter>
#include <QTimer>
#include <QDebug>
VideoDecoder::VideoDecoder(QObject *parent)
: QThread (parent)
{
}
VideoDecoder::~VideoDecoder()
{
stop();
}
void VideoDecoder::stop()
{
//必须先重置信号量
m_frameQueue.init();
m_runnable = false;
wait();
}
void VideoDecoder::open(const QString &filename)
{
stop();
m_mutex.lock();
m_filename = filename;
m_runnable = true;
m_mutex.unlock();
start();
}
QImage VideoDecoder::currentFrame()
{
static QImage image = QImage();
image = m_frameQueue.tryDequeue();
return image;
}
void VideoDecoder::run()
{
demuxing_decoding();
}
void VideoDecoder::demuxing_decoding()
{
AVFormatContext *formatContext = nullptr;
AVCodecContext *codecContext = nullptr;
AVCodec *videoDecoder = nullptr;
AVStream *videoStream = nullptr;
int videoIndex = -1;
//打开输入文件,并分配格式上下文
avformat_open_input(&formatContext, m_filename.toStdString().c_str(), nullptr, nullptr);
avformat_find_stream_info(formatContext, nullptr);
//找到视频流的索引
videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (videoIndex < 0) {
qDebug() << "Has Error: line =" << __LINE__;
return;
}
videoStream = formatContext->streams[videoIndex];
if (!videoStream) {
qDebug() << "Has Error: line =" << __LINE__;
return;
}
videoDecoder = avcodec_find_decoder(videoStream->codecpar->codec_id);
if (!videoDecoder) {
qDebug() << "Has Error: line =" << __LINE__;
return;
}
codecContext = avcodec_alloc_context3(videoDecoder);
if (!codecContext) {
qDebug() << "Has Error: line =" << __LINE__;
return;
}
avcodec_parameters_to_context(codecContext, videoStream->codecpar);
if (!codecContext) {
qDebug() << "Has Error: line =" << __LINE__;
return;
}
avcodec_open2(codecContext, videoDecoder, nullptr);
//打印相关信息
av_dump_format(formatContext, 0, "format", 0);
fflush(stderr);
m_fps = videoStream->avg_frame_rate.num / videoStream->avg_frame_rate.den;
m_width = codecContext->width;
m_height = codecContext->height;
emit resolved();
SwsContext *swsContext = sws_getContext(m_width, m_height, codecContext->pix_fmt, m_width, m_height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);
//分配并初始化一个临时的帧和包
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
packet->data = nullptr;
packet->size = 0;
//读取下一帧
while (m_runnable && av_read_frame(formatContext, packet) >= 0) {
if (packet->stream_index == videoIndex) {
//发送给解码器
int ret = avcodec_send_packet(codecContext, packet);
while (ret >= 0) {
//从解码器接收解码后的帧
ret = avcodec_receive_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
else if (ret < 0) goto Run_End;
int dst_linesize[4];
uint8_t *dst_data[4];
av_image_alloc(dst_data, dst_linesize, m_width, m_height, AV_PIX_FMT_RGB24, 1);
sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, dst_data, dst_linesize);
QImage image = QImage(dst_data[0], m_width, m_height, QImage::Format_RGB888).copy();
av_freep(&dst_data[0]);
m_frameQueue.enqueue(image);
av_frame_unref(frame);
}
}
av_packet_unref(packet);
}
Run_End:
m_fps = m_width = m_height = 0;
if (frame) av_frame_free(&frame);
if (packet) av_packet_free(&packet);
if (swsContext) sws_freeContext(swsContext);
if (codecContext) avcodec_free_context(&codecContext);
if (formatContext) avformat_close_input(&formatContext);
}
显示:
AnimatedBackground::AnimatedBackground(QWidget *parent)
: QWidget(parent)
{
this->setFixedSize(1280,760);
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, [this](){
m_currentFrame = m_decoder->currentFrame();
update();
});
m_decoder = new VideoDecoder(this);
connect(m_decoder, &VideoDecoder::resolved, this, [this]() {
m_timer->start(1000 / m_decoder->fps());
});
m_decoder->open(":/video/1.mp4");
}
void AnimatedBackground::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
if (!m_currentFrame.isNull())
painter.drawImage(rect(), m_currentFrame);
}
效果和第一个方案是一模一样的这里就不做展示了,总之,软解码视频方案比帧动画多了几百行代码不说,ffmpeg这个库也是比较难写的,由于是c语言风格,写之前还需要一定的时间费脑子去阅读文档,实现效果和帧动画没什么区别,内存上也没减少多少虽然图片经压缩视频后大小减小,但难蹦的是库文件的大小比450张图片还大,总之就是十分吃力不讨好。
使用外部解码软件
qt的QMediaPlayer可以使用外部的解码器进行解码,从而实现视频播放,但是不能保证用户是否下载了解码器,要绑定安装的话是十分流氓的行为,而且不开源的软件商用也是要钱的,直接用也是会有商业纠纷,这种方案不必多说
播放gif
效果差,糊的一批的同时帧率还低
总之
综合看下来,帧动画是最优的解决方案,简单且高效,软解码不说前期可能遇到的环境问题不说,代码也是多了几百行,给自己多加了一两天的工作量,内存空间上不但没有因为图片压缩成视频减小空间,反而因为添加动态库比原先还大,是十分吃力且不讨好的行为。代码的最终目的是为了服务于产品的,不管哪种代码,你只要能达到最终的效果,那就是好代码
另外分享一个有趣的:windows的开机动画也是用图标字体一帧一帧拼起来的