关于鸣潮启动器450张图片杂谈—从代码分析为何使用帧动画

关于鸣潮启动器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的开机动画也是用图标字体一帧一帧拼起来的

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值