|
|
@@ -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'));
|
|
|
+}
|