Sfoglia il codice sorgente

添加: 视频播放

zhuizhu 9 mesi fa
parent
commit
0f598e0e92

+ 8 - 0
AvPlayer/AvPlayer.pri

@@ -0,0 +1,8 @@
+HEADERS += \
+    $$PWD/RingBuffer.h \
+    $$PWD/ffmpegvideopuller.h \
+    $$PWD/playerdemowindow.h
+
+SOURCES += \
+    $$PWD/ffmpegvideopuller.cpp \
+    $$PWD/playerdemowindow.cpp

+ 103 - 0
AvPlayer/RingBuffer.h

@@ -0,0 +1,103 @@
+#ifndef RINGBUFFER_H
+#define RINGBUFFER_H
+
+#pragma once
+#include <cmath>
+#include <functional>
+#include <mutex>
+#include <vector>
+
+template<typename T>
+class RingBuffer
+{
+public:
+    RingBuffer(size_t capacity)
+        : m_capacity(capacity)
+        , m_data(capacity)
+        , m_pts(capacity)
+    {}
+
+    void push(const T& value, double pts)
+    {
+        std::unique_lock<std::mutex> lock(m_mutex);
+        if (m_data[m_writeIndex]) {
+            // 释放旧帧
+            if (m_deleter)
+                m_deleter(m_data[m_writeIndex]);
+        }
+        m_data[m_writeIndex] = value;
+        m_pts[m_writeIndex] = pts;
+        m_writeIndex = (m_writeIndex + 1) % m_capacity;
+        if (m_size < m_capacity)
+            ++m_size;
+        else
+            m_readIndex = (m_readIndex + 1) % m_capacity; // 覆盖最旧
+    }
+
+    // 按索引获取
+    T get(size_t index)
+    {
+        std::unique_lock<std::mutex> lock(m_mutex);
+        if (index >= m_size)
+            return nullptr;
+        size_t realIndex = (m_readIndex + index) % m_capacity;
+        return m_data[realIndex];
+    }
+
+    // 按PTS查找最近帧
+    size_t getIndexByPts(double pts)
+    {
+        std::unique_lock<std::mutex> lock(m_mutex);
+        if (m_size == 0)
+            return 0;
+        size_t best = m_readIndex;
+        double minDiff = std::abs(m_pts[best] - pts);
+        for (size_t i = 1; i < m_size; ++i) {
+            size_t idx = (m_readIndex + i) % m_capacity;
+            double diff = std::abs(m_pts[idx] - pts);
+            if (diff < minDiff) {
+                minDiff = diff;
+                best = idx;
+            }
+        }
+        if (best >= m_readIndex)
+            return best - m_readIndex;
+        else
+            return m_capacity - m_readIndex + best;
+    }
+
+    size_t size() const { return m_size; }
+    size_t capacity() const { return m_capacity; }
+
+    double firstPts() const { return m_pts[m_readIndex]; }
+    double lastPts() const { return m_pts[(m_writeIndex + m_capacity - 1) % m_capacity]; }
+
+    void setDeleter(std::function<void(T&)> deleter) { m_deleter = deleter; }
+
+    void clear()
+    {
+        std::unique_lock<std::mutex> lock(m_mutex);
+        if (m_deleter) {
+            for (size_t i = 0; i < m_capacity; ++i) {
+                if (m_data[i])
+                    m_deleter(m_data[i]);
+            }
+        }
+        m_data.assign(m_capacity, nullptr);
+        m_pts.assign(m_capacity, 0.0);
+        m_size = 0;
+        m_readIndex = 0;
+        m_writeIndex = 0;
+    }
+
+private:
+    std::vector<T> m_data;
+    std::vector<double> m_pts;
+    size_t m_capacity;
+    size_t m_size = 0;
+    size_t m_readIndex = 0;
+    size_t m_writeIndex = 0;
+    std::function<void(T&)> m_deleter;
+    mutable std::mutex m_mutex;
+};
+#endif // RINGBUFFER_H

+ 326 - 0
AvPlayer/ffmpegvideopuller.cpp

@@ -0,0 +1,326 @@
+#include "ffmpegvideopuller.h"
+
+#include <QDebug>
+#include <chrono>
+#include <thread>
+
+FFmpegVideoPuller::FFmpegVideoPuller(QObject* parent)
+    : QObject(parent)
+{
+    // av_register_all();
+    avformat_network_init();
+}
+
+FFmpegVideoPuller::~FFmpegVideoPuller()
+{
+    stop();
+}
+
+bool FFmpegVideoPuller::open(const QString& url, int videoBufferSec, int audioBufferSec)
+{
+    m_url = url;
+    // 打开流
+    if (avformat_open_input(&m_fmtCtx, url.toStdString().c_str(), nullptr, nullptr) < 0)
+        return false;
+    if (avformat_find_stream_info(m_fmtCtx, nullptr) < 0)
+        return false;
+
+    // 查找视频流
+    for (unsigned i = 0; i < m_fmtCtx->nb_streams; ++i) {
+        if (m_fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO
+            && m_videoStreamIdx == -1) {
+            m_videoStreamIdx = i;
+        }
+        if (m_fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO
+            && m_audioStreamIdx == -1) {
+            m_audioStreamIdx = i;
+        }
+    }
+    if (m_videoStreamIdx == -1)
+        return false;
+
+    // 打开视频解码器
+    const AVCodec* vcodec = avcodec_find_decoder(
+        m_fmtCtx->streams[m_videoStreamIdx]->codecpar->codec_id);
+    m_videoCodecCtx = avcodec_alloc_context3(vcodec);
+    avcodec_parameters_to_context(m_videoCodecCtx, m_fmtCtx->streams[m_videoStreamIdx]->codecpar);
+    avcodec_open2(m_videoCodecCtx, vcodec, nullptr);
+
+    // 打开音频解码器
+    if (m_audioStreamIdx != -1) {
+        const AVCodec* acodec = avcodec_find_decoder(
+            m_fmtCtx->streams[m_audioStreamIdx]->codecpar->codec_id);
+        m_audioCodecCtx = avcodec_alloc_context3(acodec);
+        avcodec_parameters_to_context(m_audioCodecCtx,
+                                      m_fmtCtx->streams[m_audioStreamIdx]->codecpar);
+        avcodec_open2(m_audioCodecCtx, acodec, nullptr);
+    }
+
+    // 计算缓冲区大小
+    int videoFps = 25;
+    AVRational fr = m_fmtCtx->streams[m_videoStreamIdx]->avg_frame_rate;
+    if (fr.num && fr.den)
+        videoFps = int(av_q2d(fr));
+    int videoBufferSize = videoBufferSec * videoFps;
+    int audioBufferSize = audioBufferSec * 50; // 假设每秒50帧
+
+    // 创建缓冲区
+    m_videoBuffer = new RingBuffer<AVFrame*>(videoBufferSize);
+    m_audioBuffer = new RingBuffer<AVFrame*>(audioBufferSize);
+    m_videoBuffer->setDeleter([](AVFrame*& f) { av_frame_free(&f); });
+    m_audioBuffer->setDeleter([](AVFrame*& f) { av_frame_free(&f); });
+
+    m_videoPlayIndex = 0;
+    m_audioPlayIndex = 0;
+    m_currentPts = 0.0;
+
+    // 获取总时长
+    if (m_fmtCtx->duration > 0) {
+        m_totalDuration = m_fmtCtx->duration / (double)AV_TIME_BASE;
+    } else {
+        m_totalDuration = -1.0;
+    }
+
+    return true;
+}
+
+void FFmpegVideoPuller::setSpeed(float speed)
+{
+    std::lock_guard<std::mutex> lock(m_speedMutex);
+    m_speed = speed;
+}
+
+float FFmpegVideoPuller::getSpeed() const
+{
+    std::lock_guard<std::mutex> lock(m_speedMutex);
+    return m_speed;
+}
+
+void FFmpegVideoPuller::start()
+{
+    if (m_running)
+        return;
+    m_running = true;
+
+    // 解码线程
+    m_decodeThread = QThread::create([this]() { this->decodeLoop(); });
+    m_decodeThread->start();
+
+    // 播放线程
+    m_playThread = QThread::create([this]() { this->playLoop(); });
+    m_playThread->start();
+}
+
+void FFmpegVideoPuller::stop()
+{
+    m_running = false;
+    if (m_decodeThread) {
+        m_decodeThread->quit();
+        m_decodeThread->wait();
+        delete m_decodeThread;
+        m_decodeThread = nullptr;
+    }
+    if (m_playThread) {
+        m_playThread->quit();
+        m_playThread->wait();
+        delete m_playThread;
+        m_playThread = nullptr;
+    }
+    // 释放缓冲区
+    if (m_videoBuffer) {
+        m_videoBuffer->clear();
+        delete m_videoBuffer;
+        m_videoBuffer = nullptr;
+    }
+    if (m_audioBuffer) {
+        m_audioBuffer->clear();
+        delete m_audioBuffer;
+        m_audioBuffer = nullptr;
+    }
+    // 释放FFmpeg资源
+    if (m_videoCodecCtx)
+        avcodec_free_context(&m_videoCodecCtx);
+    if (m_audioCodecCtx)
+        avcodec_free_context(&m_audioCodecCtx);
+    if (m_fmtCtx)
+        avformat_close_input(&m_fmtCtx);
+}
+
+void FFmpegVideoPuller::decodeLoop()
+{
+    AVPacket* pkt = av_packet_alloc();
+    AVFrame* frame = av_frame_alloc();
+
+    while (m_running) {
+        if (av_read_frame(m_fmtCtx, pkt) < 0) {
+            std::this_thread::sleep_for(std::chrono::milliseconds(10));
+            continue;
+        }
+        if (pkt->stream_index == m_videoStreamIdx) {
+            avcodec_send_packet(m_videoCodecCtx, pkt);
+            while (avcodec_receive_frame(m_videoCodecCtx, frame) == 0) {
+                double pts = frame->best_effort_timestamp
+                             * av_q2d(m_fmtCtx->streams[m_videoStreamIdx]->time_base);
+                m_videoBuffer->push(av_frame_clone(frame), pts);
+            }
+        } else if (pkt->stream_index == m_audioStreamIdx) {
+            avcodec_send_packet(m_audioCodecCtx, pkt);
+            while (avcodec_receive_frame(m_audioCodecCtx, frame) == 0) {
+                double pts = frame->best_effort_timestamp
+                             * av_q2d(m_fmtCtx->streams[m_audioStreamIdx]->time_base);
+                //qDebug() << "解码到音频帧, pts=" << pts << " nb_samples=" << frame->nb_samples;
+                m_audioBuffer->push(av_frame_clone(frame), pts);
+            }
+        }
+        av_packet_unref(pkt);
+    }
+    av_frame_free(&frame);
+    av_packet_free(&pkt);
+}
+
+void FFmpegVideoPuller::playLoop()
+{
+    // while (m_running) {
+    //     AVFrame* vFrame = getCurrentVideoFrame();
+    //     if (!vFrame) {
+    //         std::this_thread::sleep_for(std::chrono::milliseconds(5));
+    //         continue;
+    //     }
+    //     double vPts = vFrame->best_effort_timestamp
+    //                   * av_q2d(m_fmtCtx->streams[m_videoStreamIdx]->time_base);
+
+    //     // 这里你可以直接把vFrame送到OpenGL渲染
+    //     // 例如: renderFrame(vFrame);
+
+    //     // 控制倍速
+    //     AVRational fr = m_fmtCtx->streams[m_videoStreamIdx]->avg_frame_rate;
+    //     double frame_delay = 1.0 / (fr.num ? av_q2d(fr) : 25.0);
+    //     float speed = getSpeed();
+    //     int delay = int(frame_delay * 1000 / speed);
+    //     qDebug() << "playLoop, m_speed=" << speed << delay;
+    //     if (delay > 0)
+    //         std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+
+    //     // 前进
+    //     nextVideoFrame();
+
+    //     // 更新当前pts
+    //     {
+    //         std::lock_guard<std::mutex> lock(m_ptsMutex);
+    //         m_currentPts = vPts;
+    //     }
+    // }
+    while (m_running) {
+        AVFrame* vFrame = getCurrentVideoFrame();
+        double vPts = vFrame ? (vFrame->best_effort_timestamp
+                                * av_q2d(m_fmtCtx->streams[m_videoStreamIdx]->time_base))
+                             : m_currentPts.load();
+        if (vFrame && m_videoRenderCallback) {
+            m_videoRenderCallback(vFrame);
+        }
+
+        // 音频同步:只要音频帧PTS小于等于视频帧PTS,就顺序播放
+        while (true) {
+            AVFrame* aFrame = getCurrentAudioFrame();
+            if (!aFrame)
+                break;
+            double aPts = aFrame->best_effort_timestamp
+                          * av_q2d(m_fmtCtx->streams[m_audioStreamIdx]->time_base);
+            if (aPts <= vPts + 0.02) { // 允许微小误差
+                if (m_audioPlayCallback)
+                    m_audioPlayCallback(aFrame);
+                nextAudioFrame();
+            } else {
+                break;
+            }
+        }
+
+        // 控制倍速
+        AVRational fr = m_fmtCtx->streams[m_videoStreamIdx]->avg_frame_rate;
+        double frame_delay = 1.0 / (fr.num ? av_q2d(fr) : 25.0);
+        float speed = getSpeed();
+        int delay = int(frame_delay * 1000 / speed);
+        if (delay > 0)
+            std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+        // 前进
+        nextVideoFrame();
+        // 更新当前pts
+        {
+            std::lock_guard<std::mutex> lock(m_ptsMutex);
+            m_currentPts = vPts;
+        }
+    }
+}
+
+// 进度相关
+double FFmpegVideoPuller::getFirstPts() const
+{
+    return m_videoBuffer ? m_videoBuffer->firstPts() : 0.0;
+}
+double FFmpegVideoPuller::getLastPts() const
+{
+    return m_videoBuffer ? m_videoBuffer->lastPts() : 0.0;
+}
+double FFmpegVideoPuller::getCurrentPts() const
+{
+    std::lock_guard<std::mutex> lock(m_ptsMutex);
+    return m_currentPts;
+}
+void FFmpegVideoPuller::seekToPts(double pts)
+{
+    // 停止线程
+    m_running = false;
+    if (m_decodeThread) { m_decodeThread->quit(); m_decodeThread->wait(); delete m_decodeThread; m_decodeThread = nullptr; }
+    if (m_playThread) { m_playThread->quit(); m_playThread->wait(); delete m_playThread; m_playThread = nullptr; }
+    // 清空缓冲区
+    if (m_videoBuffer) m_videoBuffer->clear();
+    if (m_audioBuffer) m_audioBuffer->clear();
+    // FFmpeg seek
+    int64_t seek_target = pts / av_q2d(m_fmtCtx->streams[m_videoStreamIdx]->time_base);
+    av_seek_frame(m_fmtCtx, m_videoStreamIdx, seek_target, AVSEEK_FLAG_BACKWARD);
+    avcodec_flush_buffers(m_videoCodecCtx);
+    if (m_audioCodecCtx) avcodec_flush_buffers(m_audioCodecCtx);
+    // 重置索引
+    m_videoPlayIndex = 0;
+    m_audioPlayIndex = 0;
+    // 重新启动线程
+    m_running = true;
+    m_decodeThread = QThread::create([this]() { this->decodeLoop(); });
+    m_decodeThread->start();
+    m_playThread = QThread::create([this]() { this->playLoop(); });
+    m_playThread->start();
+}
+
+// 取帧接口
+AVFrame* FFmpegVideoPuller::getCurrentVideoFrame()
+{
+    if (!m_videoBuffer)
+        return nullptr;
+    return m_videoBuffer->get(m_videoPlayIndex);
+}
+AVFrame* FFmpegVideoPuller::getCurrentAudioFrame()
+{
+    if (!m_audioBuffer)
+        return nullptr;
+    return m_audioBuffer->get(m_audioPlayIndex);
+}
+void FFmpegVideoPuller::nextVideoFrame()
+{
+    ++m_videoPlayIndex;
+    if (m_videoBuffer && m_videoPlayIndex >= m_videoBuffer->size())
+        m_videoPlayIndex = m_videoBuffer->size() - 1;
+}
+void FFmpegVideoPuller::nextAudioFrame()
+{
+    ++m_audioPlayIndex;
+    if (m_audioBuffer && m_audioPlayIndex >= m_audioBuffer->size())
+        m_audioPlayIndex = m_audioBuffer->size() - 1;
+}
+size_t FFmpegVideoPuller::videoBufferSize() const
+{
+    return m_videoBuffer ? m_videoBuffer->size() : 0;
+}
+size_t FFmpegVideoPuller::audioBufferSize() const
+{
+    return m_audioBuffer ? m_audioBuffer->size() : 0;
+}

+ 95 - 0
AvPlayer/ffmpegvideopuller.h

@@ -0,0 +1,95 @@
+#ifndef FFMPEGVIDEOPULLER_H
+#define FFMPEGVIDEOPULLER_H
+
+#pragma once
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+}
+
+#include <QObject>
+#include <QString>
+#include <QThread>
+
+#include <atomic>
+#include <functional>
+
+#include "RingBuffer.h"
+
+class FFmpegVideoPuller : public QObject
+{
+    Q_OBJECT
+public:
+    explicit FFmpegVideoPuller(QObject* parent = nullptr);
+    ~FFmpegVideoPuller();
+
+    bool open(const QString& url,
+              int videoBufferSec = 300,
+              int audioBufferSec = 300); // 默认缓存5分钟
+    void setSpeed(float speed);          // 设置倍速
+    float getSpeed() const;
+    void start();
+    void stop();
+
+    // 进度相关
+    double getFirstPts() const;
+    double getLastPts() const;
+    double getCurrentPts() const;
+    void seekToPts(double pts); // 跳转到指定pts
+
+    // 取帧接口
+    AVFrame* getCurrentVideoFrame();
+    AVFrame* getCurrentAudioFrame();
+
+    // 播放指针前进
+    void nextVideoFrame();
+    void nextAudioFrame();
+
+    // 缓冲区大小
+    size_t videoBufferSize() const;
+    size_t audioBufferSize() const;
+
+    void setVideoRenderCallback(std::function<void(AVFrame*)> cb) { m_videoRenderCallback = cb; }
+    void setAudioPlayCallback(std::function<void(AVFrame*)> cb) { m_audioPlayCallback = cb; }
+
+    double getTotalDuration() const { return m_totalDuration; }
+
+private:
+    void decodeLoop();
+    void playLoop();
+
+    QString m_url;
+    std::atomic<bool> m_running{false};
+    mutable std::mutex m_speedMutex;
+    float m_speed = 1.0f;
+
+    // FFmpeg相关
+    AVFormatContext* m_fmtCtx = nullptr;
+    AVCodecContext* m_videoCodecCtx = nullptr;
+    AVCodecContext* m_audioCodecCtx = nullptr;
+    int m_videoStreamIdx = -1;
+    int m_audioStreamIdx = -1;
+
+    // 缓冲区
+    RingBuffer<AVFrame*>* m_videoBuffer = nullptr;
+    RingBuffer<AVFrame*>* m_audioBuffer = nullptr;
+
+    // 线程
+    QThread* m_decodeThread = nullptr;
+    QThread* m_playThread = nullptr;
+
+    // 播放指针
+    std::atomic<size_t> m_videoPlayIndex{0};
+    std::atomic<size_t> m_audioPlayIndex{0};
+    std::atomic<double> m_currentPts{0.0};
+
+    mutable std::mutex m_ptsMutex;
+
+    std::function<void(AVFrame*)> m_videoRenderCallback;
+    std::function<void(AVFrame*)> m_audioPlayCallback;
+
+    double m_totalDuration = -1.0;
+};
+
+#endif // FFMPEGVIDEOPULLER_H

+ 326 - 0
AvPlayer/playerdemowindow.cpp

@@ -0,0 +1,326 @@
+#include "playerdemowindow.h"
+#include <QAudioDeviceInfo>
+#include <QAudioFormat>
+#include <QDebug>
+#include <QHBoxLayout>
+#include <QMessageBox>
+#include <QVBoxLayout>
+
+PlayerDemoWindow::PlayerDemoWindow(QWidget* parent)
+    : QWidget(parent)
+{
+    QVBoxLayout* layout = new QVBoxLayout(this);
+    m_videoWidget = new OpenGLVideoWidget(this);
+    layout->addWidget(m_videoWidget);
+
+    // 进度条和时间
+    QHBoxLayout* progressLayout = new QHBoxLayout();
+    m_progressSlider = new QSlider(Qt::Horizontal, this);
+    m_progressSlider->setRange(0, 1000);
+    m_timeLabel = new QLabel("00:00:00 / 00:00:00", this);
+    progressLayout->addWidget(m_progressSlider, 1);
+    progressLayout->addWidget(m_timeLabel);
+    layout->addLayout(progressLayout);
+
+    // 倍速选择和播放按钮
+    QHBoxLayout* controlLayout = new QHBoxLayout();
+    m_playBtn = new QPushButton("播放", this);
+    m_speedCombo = new QComboBox(this);
+    m_speedCombo->addItem("0.5x", 0.5);
+    m_speedCombo->addItem("1.0x", 1.0);
+    m_speedCombo->addItem("1.5x", 1.5);
+    m_speedCombo->addItem("2.0x", 2.0);
+    controlLayout->addWidget(m_playBtn);
+    controlLayout->addWidget(new QLabel("倍速:", this));
+    controlLayout->addWidget(m_speedCombo);
+    layout->addLayout(controlLayout);
+
+    m_speedCombo->setCurrentText("1.0x");
+
+    connect(m_progressSlider, &QSlider::sliderPressed, [this]() { m_sliderPressed = true; });
+    connect(m_progressSlider,
+            &QSlider::sliderReleased,
+            this,
+            &PlayerDemoWindow::onProgressSliderReleased);
+    connect(m_progressSlider, &QSlider::sliderMoved, this, &PlayerDemoWindow::onProgressSliderMoved);
+    connect(m_speedCombo,
+            QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this,
+            &PlayerDemoWindow::onSpeedChanged);
+    connect(m_playBtn, &QPushButton::clicked, this, &PlayerDemoWindow::onPlayClicked);
+}
+
+PlayerDemoWindow::~PlayerDemoWindow()
+{
+    if (m_puller) {
+        m_puller->stop();
+        delete m_puller;
+    }
+    if (m_audioOutput) {
+        m_audioOutput->stop();
+        delete m_audioOutput;
+    }
+    if (m_swrCtx) {
+        swr_free(&m_swrCtx);
+    }
+    if (m_swrBuffer) {
+        av_free(m_swrBuffer);
+    }
+}
+
+void PlayerDemoWindow::startPlay(const QString& url)
+{
+    if (m_puller) {
+        m_puller->stop();
+        delete m_puller;
+        m_puller = nullptr;
+    }
+    m_puller = new FFmpegVideoPuller();
+    if (!m_puller->open(url, 300, 300)) {
+        QMessageBox::critical(this, "错误", "无法打开流");
+        return;
+    }
+    m_puller->setSpeed(m_speedCombo->currentData().toFloat());
+
+    // 设置回调
+    m_puller->setVideoRenderCallback([this](AVFrame* frame) {
+        m_videoWidget->Render(frame);
+        updateProgress(); // 每次渲染视频帧时刷新进度条
+    });
+    m_puller->setAudioPlayCallback([this](AVFrame* frame) {
+        if (!m_audioOutput) {
+            initAudio(frame);
+        }
+        playAudioFrame(frame);
+    });
+
+    m_puller->start();
+
+    // 进度条初始化
+    m_firstPts = m_puller->getFirstPts();
+    m_lastPts = m_puller->getLastPts();
+    m_duration = m_lastPts - m_firstPts;
+    m_progressSlider->setValue(0);
+    updateProgress();
+}
+
+void PlayerDemoWindow::onPlayClicked()
+{
+    if (!m_puller)
+        return;
+    float curSpeed = m_puller->getSpeed();
+    if (curSpeed > 0.01f) {
+        m_puller->setSpeed(0.0f);
+        m_playBtn->setText("播放");
+    } else {
+        float speed = m_speedCombo->currentData().toFloat();
+        m_puller->setSpeed(speed);
+        m_playBtn->setText("暂停");
+    }
+}
+
+void PlayerDemoWindow::initAudio(const AVFrame* frame)
+{
+    if (m_audioOutput) {
+        m_audioOutput->stop();
+        delete m_audioOutput;
+        m_audioOutput = nullptr;
+    }
+    if (m_swrCtx) {
+        swr_free(&m_swrCtx);
+        m_swrCtx = nullptr;
+    }
+    if (m_swrBuffer) {
+        av_free(m_swrBuffer);
+        m_swrBuffer = nullptr;
+        m_swrBufferSize = 0;
+    }
+
+    QAudioFormat fmt;
+    m_audioSampleRate = frame->sample_rate;
+    m_audioChannels = frame->ch_layout.nb_channels; // FFmpeg 7.x
+    m_audioFormat = (AVSampleFormat) frame->format;
+
+    fmt.setSampleRate(m_audioSampleRate);
+    fmt.setChannelCount(m_audioChannels);
+    fmt.setSampleSize(16); // 目标格式 S16
+    fmt.setCodec("audio/pcm");
+    fmt.setByteOrder(QAudioFormat::LittleEndian);
+    fmt.setSampleType(QAudioFormat::SignedInt);
+
+    QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
+    if (!info.isFormatSupported(fmt)) {
+        qWarning() << "音频格式不支持,尝试最近格式";
+        fmt = info.nearestFormat(fmt);
+    }
+    m_audioOutput = new QAudioOutput(fmt, this);
+    m_audioDevice = m_audioOutput->start();
+
+    qDebug() << "initAudio, m_audioOutput=" << m_audioOutput << " m_audioDevice=" << m_audioDevice;
+    if (m_audioOutput)
+        qDebug() << "QAudioOutput error:" << m_audioOutput->error();
+
+    AVChannelLayout out_ch_layout;
+    av_channel_layout_default(&out_ch_layout, m_audioChannels);
+
+    m_swrCtx = swr_alloc();
+    if (!m_swrCtx) {
+        qWarning() << "swr_alloc 失败";
+        return;
+    }
+    if (swr_alloc_set_opts2(&m_swrCtx,
+                            &out_ch_layout,
+                            AV_SAMPLE_FMT_S16,
+                            m_audioSampleRate,
+                            &frame->ch_layout,
+                            (AVSampleFormat) frame->format,
+                            frame->sample_rate,
+                            0,
+                            nullptr)
+        < 0) {
+        qWarning() << "swr_alloc_set_opts2 失败";
+        swr_free(&m_swrCtx);
+        return;
+    }
+    if (swr_init(m_swrCtx) < 0) {
+        qWarning() << "swr_init 失败";
+        swr_free(&m_swrCtx);
+        m_swrCtx = nullptr;
+    }
+    av_channel_layout_uninit(&out_ch_layout);
+}
+
+void PlayerDemoWindow::playAudioFrame(AVFrame* frame)
+{
+    qDebug() << "--------------" << m_audioOutput << m_audioDevice;
+    if (!m_audioOutput || !m_audioDevice)
+        return;
+
+    int out_channels = m_audioChannels;
+    int out_samples = frame->nb_samples;
+    int out_bytes_per_sample = av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);
+
+    // 目标缓冲区大小
+    int needed_bufsize = av_samples_get_buffer_size(nullptr,
+                                                    out_channels,
+                                                    out_samples,
+                                                    AV_SAMPLE_FMT_S16,
+                                                    1);
+
+    if (!m_swrBuffer || m_swrBufferSize < needed_bufsize) {
+        if (m_swrBuffer)
+            av_free(m_swrBuffer);
+        m_swrBuffer = (uint8_t*) av_malloc(needed_bufsize);
+        m_swrBufferSize = needed_bufsize;
+    }
+
+    uint8_t* out[] = {m_swrBuffer};
+    int converted = 0;
+
+    // qDebug() << "playAudioFrame, m_audioOutput=" << m_audioOutput
+    //          << " m_audioDevice=" << m_audioDevice;
+
+    if (m_swrCtx) {
+        converted = swr_convert(m_swrCtx,
+                                out,
+                                out_samples,
+                                (const uint8_t**) frame->extended_data,
+                                frame->nb_samples);
+        qDebug() << "swr_convert 返回" << converted;
+        if (converted > 0) {
+            int out_size = converted * out_channels * out_bytes_per_sample;
+            qint64 written = m_audioDevice->write((const char*) m_swrBuffer, out_size);
+            (void) written;
+            // qDebug() << "写入音频数据, 大小=" << out_size << " 实际写入=" << written;
+        }
+    } else if (frame->format == AV_SAMPLE_FMT_S16) {
+        // 直接写
+        int dataSize = av_samples_get_buffer_size(nullptr,
+                                                  frame->ch_layout.nb_channels,
+                                                  frame->nb_samples,
+                                                  (AVSampleFormat) frame->format,
+                                                  1);
+        m_audioDevice->write((const char*) frame->data[0], dataSize);
+    }
+}
+
+void PlayerDemoWindow::onProgressSliderMoved(int value)
+{
+    m_sliderPressed = true;
+    if (m_duration <= 0) {
+        m_timeLabel->setText("00:00:00 / 00:00:00");
+        return;
+    }
+    double seekPts = m_firstPts + (m_duration * value / 1000.0);
+    QString cur = formatTime(seekPts - m_firstPts);
+    QString total = formatTime(m_duration);
+    m_timeLabel->setText(cur + " / " + total);
+}
+
+void PlayerDemoWindow::onProgressSliderReleased()
+{
+    if (!m_puller || m_duration <= 0)
+        return;
+    int value = m_progressSlider->value();
+    double seekPts = m_firstPts + (m_duration * value / 1000.0);
+    if (seekPts < m_firstPts)
+        seekPts = m_firstPts;
+    if (seekPts > m_lastPts)
+        seekPts = m_lastPts;
+    m_puller->seekToPts(seekPts);
+    m_sliderPressed = false;
+}
+
+void PlayerDemoWindow::onSpeedChanged(int index)
+{
+    if (!m_puller)
+        return;
+    float speed = m_speedCombo->itemData(index).toFloat();
+    m_puller->setSpeed(speed);
+}
+
+void PlayerDemoWindow::updateProgress()
+{
+    if (!m_puller || m_sliderPressed)
+        return;
+    double duration = m_puller->getTotalDuration();
+    if (duration > 0) {
+        m_duration = duration;
+        m_firstPts = 0;
+        m_lastPts = duration;
+    } else {
+        m_firstPts = m_puller->getFirstPts();
+        m_lastPts = m_puller->getLastPts();
+        m_duration = m_lastPts - m_firstPts;
+    }
+    // 检查有效性
+    if (m_firstPts < 0 || m_lastPts < 0 || m_duration <= 0) {
+        m_progressSlider->setValue(0);
+        m_timeLabel->setText("00:00:00 / 00:00:00");
+        return;
+    }
+    double curPts = m_puller->getCurrentPts();
+    if (curPts < m_firstPts)
+        curPts = m_firstPts;
+    if (curPts > m_lastPts)
+        curPts = m_lastPts;
+    int value = int((curPts - m_firstPts) / m_duration * 1000);
+    m_progressSlider->setValue(value);
+    QString cur = formatTime(curPts - m_firstPts);
+    QString total = formatTime(m_duration);
+    m_timeLabel->setText(cur + " / " + total);
+}
+
+QString PlayerDemoWindow::formatTime(double seconds) const
+{
+    if (seconds < 0)
+        seconds = 0;
+    int sec = int(seconds + 0.5);
+    int h = sec / 3600;
+    int m = (sec % 3600) / 60;
+    int s = sec % 60;
+    return QString("%1:%2:%3")
+        .arg(h, 2, 10, QChar('0'))
+        .arg(m, 2, 10, QChar('0'))
+        .arg(s, 2, 10, QChar('0'));
+}

+ 70 - 0
AvPlayer/playerdemowindow.h

@@ -0,0 +1,70 @@
+#ifndef PLAYERDEMOWINDOW_H
+#define PLAYERDEMOWINDOW_H
+
+#pragma once
+
+#include <QAudioOutput>
+#include <QComboBox>
+#include <QIODevice>
+#include <QLabel>
+#include <QPushButton>
+#include <QSlider>
+#include <QWidget>
+#include "AvRecorder/ui/opengl_video_widget.h"
+#include "Avplayer/FFmpegVideoPuller.h"
+
+extern "C" {
+#include <libswresample/swresample.h>
+}
+class PlayerDemoWindow : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit PlayerDemoWindow(QWidget* parent = nullptr);
+    ~PlayerDemoWindow();
+
+    void startPlay(const QString& url);
+
+private slots:
+    void onProgressSliderMoved(int value);
+    void onProgressSliderReleased();
+    void onSpeedChanged(int index);
+    void onPlayClicked();
+
+private:
+    FFmpegVideoPuller* m_puller = nullptr;
+    OpenGLVideoWidget* m_videoWidget = nullptr;
+
+    // 音频相关
+    QAudioOutput* m_audioOutput = nullptr;
+    QIODevice* m_audioDevice = nullptr;
+    int m_audioSampleRate = 0;
+    int m_audioChannels = 0;
+    AVSampleFormat m_audioFormat = AV_SAMPLE_FMT_NONE;
+
+    // UI
+    QSlider* m_progressSlider = nullptr;
+    QLabel* m_timeLabel = nullptr;
+    QComboBox* m_speedCombo = nullptr;
+    QPushButton* m_playBtn = nullptr;
+
+    // 进度相关
+    bool m_sliderPressed = false;
+    double m_firstPts = 0.0;
+    double m_lastPts = 0.0;
+    double m_duration = 0.0;
+
+    bool m_playing;
+
+    void initAudio(const AVFrame* frame);
+    void playAudioFrame(AVFrame* frame);
+    void updateProgress();
+    QString formatTime(double seconds) const;
+
+private:
+    SwrContext* m_swrCtx = nullptr;
+    uint8_t* m_swrBuffer = nullptr;
+    int m_swrBufferSize = 0;
+};
+
+#endif // PLAYERDEMOWINDOW_H

+ 2 - 0
LearningSmartClient.pro

@@ -81,6 +81,8 @@ msvc {
 
 DEFINES += _SILENCE_CLANG_COROUTINE_MESSAGE
 include($$PWD/AvRecorder/AvRecorder.pri)
+include($$PWD/AvPlayer/AvPlayer.pri)
+
 
 include($$PWD/qwindowkit/qwindowkit.pri)
 include($$PWD/fmt.pri)

+ 6 - 4
api/loginapi.cpp

@@ -4,15 +4,17 @@
 #include <QJsonObject>
 
 namespace AuthApi {
-
 QFuture<HttpResponse> loginApi(const LoginParams& data)
 {
     QJsonObject jsonData;
     jsonData["username"] = data.username;
     jsonData["password"] = data.password;
+    jsonData["captchaId"] = "testtesttesttesttest";
+    jsonData["captcha"] = "testa";
+
     QJsonDocument doc(jsonData);
 
-    return TC::RequestClient::requestClient()->postAsync("/auth/login", doc);
+    return TC::RequestClient::requestClient()->postAsync("/user/login", doc);
 }
 
 QFuture<HttpResponse> refreshTokenApi()
@@ -30,12 +32,12 @@ QFuture<HttpResponse> logoutApi()
     config["withCredentials"] = true;
     QJsonDocument doc(config);
 
-    return TC::RequestClient::baseRequestClient()->postAsync("/auth/logout", doc);
+    return TC::RequestClient::baseRequestClient()->postAsync("/user/logout", doc);
 }
 
 QFuture<HttpResponse> getAccessCodesApi()
 {
-    return TC::RequestClient::requestClient()->getAsync("/auth/codes");
+    return TC::RequestClient::requestClient()->getAsync("/user/perm");
 }
 
 } // namespace AuthApi

+ 1 - 1
api/userapi.cpp

@@ -2,5 +2,5 @@
 
 QFuture<HttpResponse> getUserInfoApi()
 {
-    return TC::RequestClient::requestClient()->getAsync("/users/info");
+    return TC::RequestClient::requestClient()->getAsync("/user/info");
 }

+ 12 - 2
main.cpp

@@ -1,3 +1,4 @@
+#include "AvPlayer/playerdemowindow.h"
 #include "mainwindow.h"
 #include "thememanager.h"
 #include "themesettingswidget.h"
@@ -22,11 +23,20 @@ docker run -itd  --name zlmediakit --restart=always
 -v /data/zlmediakit/media/conf:/opt/media/conf
 zlmediakit/zlmediakit:master
 */
-    MainWindow w;
-    w.show();
+    // MainWindow w;
+    // w.show();
 
     // ThemeSettingsWidget ThemeSettingsWidget;
     // ThemeSettingsWidget.show();
 
+    PlayerDemoWindow w;
+    w.resize(960, 540);
+    w.show();
+
+    // 这里填你的流地址
+    // w.startPlay("http://vd3.bdstatic.com/mda-jennyc5ci1ugrxzi/mda-jennyc5ci1ugrxzi.mp4");
+
+    w.startPlay("rtmp://127.0.0.1:1936/stream/V1");
+
     return a.exec();
 }

+ 9 - 6
mainwindow.cpp

@@ -55,10 +55,11 @@ public:
         }
     }
     void setupUI()
-    { // 创建并显示 AvRecorder
+    {
+        // 创建并显示 AvRecorder
         AvRecorder *avRecorder = new AvRecorder(this);
 
-        // 确保 AvRecorder 不会阻止鼠标事件传递
+        //确保 AvRecorder 不会阻止鼠标事件传递
         avRecorder->setAttribute(Qt::WA_TransparentForMouseEvents, false);
 
         // 创建用户信息显示
@@ -201,16 +202,18 @@ void MainWindow::onLoginSuccess(const QString &username, const QString &password
 
     loginApiPromise
         .then([this](const HttpResponse &response) {
-            if (response.code != 200) {
+            qDebug() << response.code << response.data << response.message;
+            if (response.code != 0) {
                 BubbleTip::showTip(this, response.message, BubbleTip::Top, 3000);
                 return;
             }
             QString token;
             if (response.data.isObject()) {
                 QJsonObject dataObj = response.data.toObject();
-                token = dataObj["accessToken"].toString();
+                qDebug() << dataObj;
+                token = dataObj["token"].toString();
                 AppEvent::instance()->setJwtToken(token);
-                authLogin();
+                authLogin(); // 获取登录信息
             }
 
             if (!token.isEmpty()) {
@@ -245,7 +248,7 @@ void MainWindow::authLogin()
             qDebug() << "访问代码:" << accessCodes.code << accessCodes.data << accessCodes.message;
 
             // 处理成功的逻辑
-            if (userInfo.code == 200 && accessCodes.code == 200) {
+            if (userInfo.code == 0 && accessCodes.code == 0) {
                 qDebug() << "所有请求成功完成";
                 // 这里可以添加后续处理逻辑
             }

+ 29 - 9
network/networkaccessmanager.cpp

@@ -11,7 +11,10 @@
 #include <QUrlQuery>
 
 // 基础URL配置
-static QString base_url("http://127.0.0.1:8888/api");
+static QString base_url("http://127.0.0.1:8200");
+
+static const QString messageVal = "msg";
+// static QString base_url("http://127.0.0.1:8888/api");
 
 static NetworkAccessManager* namInstance = nullptr;
 
@@ -32,7 +35,21 @@ NetworkAccessManager* NetworkAccessManager::instance()
 
 NetworkAccessManager::NetworkAccessManager(QObject* parent)
     : QNetworkAccessManager(parent)
-{}
+{
+    // #ifdef QT_DEBUG
+    //     connect(this, &QNetworkAccessManager::finished, this, [](QNetworkReply* reply) {
+    //         const auto data = reply->readAll();
+    //         reply->seek(0);
+    //         const auto startTimeValue = reply->property("startTime");
+    //         const QDateTime startTime = startTimeValue.value<QDateTime>();
+    //         const QDateTime endTime = QDateTime::currentDateTime();
+    //         qDebug() << QString("[%1] time:[%2]ms --> %3")
+    //                         .arg(endTime.toString("yyyy-MM-dd hh:mm:ss.zzz"))
+    //                         .arg((endTime.msecsTo(startTime)))
+    //                         .arg(QString::fromUtf8(data));
+    //     });
+    // #endif
+}
 
 QNetworkReply* NetworkAccessManager::createRequest(Operation op,
                                                    const QNetworkRequest& request,
@@ -180,6 +197,9 @@ void RequestClient::setBaseUrl(const QString& url)
 
 QString RequestClient::baseUrl() const
 {
+    if (m_baseUrl.isEmpty()) {
+        return base_url;
+    }
     return m_baseUrl;
 }
 
@@ -333,17 +353,17 @@ HttpResponse RequestClient::parseReplyData(QNetworkReply* reply)
         return response;
     }
 
-    QByteArray responseData = reply->readAll();
+    const QByteArray responseData = reply->readAll();
     QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
-
+    qDebug() << responseData << jsonDoc;
     if (jsonDoc.isObject()) {
         const QJsonObject jsonObj = jsonDoc.object();
-
+        qDebug() << jsonObj;
         if (jsonObj.contains("code")) {
             response.code = jsonObj["code"].toInt();
         }
-        if (jsonObj.contains("message")) {
-            response.message = jsonObj["message"].toString();
+        if (jsonObj.contains(messageVal)) {
+            response.message = jsonObj[messageVal].toString();
         }
         if (jsonObj.contains("data")) {
             QJsonValue dataValue = jsonObj["data"];
@@ -624,8 +644,8 @@ void ErrorResponseInterceptor::interceptResponse(QNetworkReply* reply)
 
             if (jsonObject.contains("error")) {
                 errorMessage = jsonObject["error"].toString();
-            } else if (jsonObject.contains("message")) {
-                errorMessage = jsonObject["message"].toString();
+            } else if (jsonObject.contains(messageVal)) {
+                errorMessage = jsonObject[messageVal].toString();
             }
         } catch (...) {
             // 解析失败,使用默认错误信息

+ 4 - 2
network/websocketclient.cpp

@@ -15,7 +15,7 @@ WebSocketClient::WebSocketClient(QObject* parent)
     , m_connected(false)
     , m_autoReconnect(true)
     , m_reconnectAttempts(0)
-    , m_maxReconnectAttempts(5)
+    , m_maxReconnectAttempts(300)
     , m_isReconnecting(false)
 {
     // 连接WebSocket信号
@@ -62,6 +62,8 @@ void WebSocketClient::connectToRoom(const QString& roomId)
         request.setRawHeader("Authorization", "Bearer " + token.toUtf8());
     }
 
+    qDebug() << "---" << token;
+
     request.setRawHeader("Machine-Code", AppEvent::instance()->machineCode().toUtf8());
     request.setRawHeader("Accept-Language", AppEvent::instance()->locale().toUtf8());
 
@@ -218,7 +220,7 @@ QUrl WebSocketClient::buildWebSocketUrl(const QString& roomId)
     }
 
     if (wsUrl.isEmpty()) {
-        wsUrl = "ws://127.0.0.1:8888";
+        wsUrl = "ws://127.0.0.1:8200";
     }
 
     // 构建完整的WebSocket URL

+ 0 - 5
views/loginwindow.cpp

@@ -133,11 +133,6 @@ void LoginWidgetPrivate::setupMainLayout()
     mainLayout->setContentsMargins(0, 0, 0, 0);
     mainLayout->setSpacing(0);
 
-    // QHBoxLayout *controlLayout = new QHBoxLayout();
-    // controlLayout->addStretch();
-    // controlLayout->addWidget(controlPanel);
-    // controlLayout->setContentsMargins(0, 10, 20, 0);
-
     mainContentLayout = new QHBoxLayout();
     mainContentLayout->setContentsMargins(0, 0, 0, 0);
     mainContentLayout->setSpacing(0);

+ 8 - 8
widgets/chatView/chat1/chatmessagedelegate.cpp

@@ -144,14 +144,14 @@ void ChatMessageDelegate::paint(QPainter *painter,
     bool isCurrentMessage = (index == m_currentMessageIndex);
     drawTextWithSelection(painter, textRect, message.text, option.fontMetrics, isCurrentMessage);
 
-    // 绘制时间戳
-    QRectF timestampRect(bubbleRect.left(),
-                         bubbleRect.bottom(),
-                         bubbleRect.width(),
-                         ChatConstants::TIMESTAMP_HEIGHT);
-    painter->setPen(ThemeManager::instance().color("colorTextSecondary"));
-    painter->setFont(QFont(option.font.family(), option.font.pointSize() - 2));
-    painter->drawText(timestampRect, Qt::AlignCenter, message.timestamp.toString("HH:mm"));
+    // // 绘制时间戳
+    // QRectF timestampRect(bubbleRect.left(),
+    //                      bubbleRect.bottom(),
+    //                      bubbleRect.width(),
+    //                      ChatConstants::TIMESTAMP_HEIGHT);
+    // painter->setPen(ThemeManager::instance().color("colorTextSecondary"));
+    // painter->setFont(QFont(option.font.family(), option.font.pointSize() - 2));
+    // painter->drawText(timestampRect, Qt::AlignCenter, message.timestamp.toString("HH:mm"));
 }
 
 QSize ChatMessageDelegate::sizeHint(const QStyleOptionViewItem &option,