#include "videoplayer_engine.h" #include "sound_manager.h" #include "../../lib/debug_config.h" #include #include #ifdef HAVE_FFMPEG extern "C" { #include #include #include #include #include #include #include #include } #endif static const int kMaxBuffered = 30; /* ── Worker: демюкс + декод в отдельном потоке ── */ class Worker : public QObject { Q_OBJECT public: QString path; QAtomicInt* abort = nullptr; QAtomicInt* paused = nullptr; QAtomicInt* seekReq = nullptr; QAtomicInteger* seekTargetMs = nullptr; QAtomicInt* buffered = nullptr; QAtomicInt* epoch = nullptr; signals: void opened(int width, int height, qint64 durationMs, bool hasAudio); void audioDecoded(std::shared_ptr> pcm, int sampleRate, int channels, qint64 durationMs); void frameDecoded(const QImage& image, qint64 ptsMs, int epoch); void error(const QString& msg); void finished(); public slots: void run(); }; #ifdef HAVE_FFMPEG static void worker_fail(Worker* w, const QString& msg) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: %s", qPrintable(msg)); emit w->error(msg); } void Worker::run() { AVFormatContext* fmt = nullptr; AVCodecContext* vdec = nullptr; AVCodecContext* adec = nullptr; struct SwsContext* sws = nullptr; SwrContext* swr = nullptr; AVFrame* frame = nullptr; AVFrame* aframe = nullptr; AVPacket* pkt = nullptr; int vidx = -1, aidx = -1; auto cleanup = [&]() { if (sws) sws_freeContext(sws); if (swr) swr_free(&swr); if (frame) av_frame_free(&frame); if (aframe) av_frame_free(&aframe); if (pkt) av_packet_free(&pkt); if (vdec) avcodec_free_context(&vdec); if (adec) avcodec_free_context(&adec); if (fmt) avformat_close_input(&fmt); }; if (avformat_open_input(&fmt, path.toUtf8().constData(), nullptr, nullptr) != 0) { worker_fail(this, QString("cannot open: %1").arg(path)); return; } if (avformat_find_stream_info(fmt, nullptr) < 0) { worker_fail(this, QString("no stream info: %1").arg(path)); cleanup(); return; } for (unsigned i = 0; i < fmt->nb_streams; i++) { AVCodecParameters* p = fmt->streams[i]->codecpar; if (p->codec_type == AVMEDIA_TYPE_VIDEO && vidx < 0) vidx = (int)i; else if (p->codec_type == AVMEDIA_TYPE_AUDIO && aidx < 0) aidx = (int)i; } if (vidx < 0) { worker_fail(this, QString("no video stream: %1").arg(path)); cleanup(); return; } AVStream* vst = fmt->streams[vidx]; int vw = vst->codecpar->width, vh = vst->codecpar->height; qint64 durMs = 0; if (fmt->duration != AV_NOPTS_VALUE) durMs = fmt->duration / 1000; else if (vst->duration != AV_NOPTS_VALUE && vst->time_base.num > 0) durMs = av_rescale_q(vst->duration, vst->time_base, AVRational{1, 1000}); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: opened %s %dx%d dur=%lldms audio=%d", qPrintable(path), vw, vh, (long long)durMs, aidx >= 0 ? 1 : 0); emit opened(vw, vh, durMs, aidx >= 0); /* ── 1. декодируем весь аудио-трек в PCM (мастер-часы) ── */ std::shared_ptr> audioPcm; int asr = 0, ach = 0; if (aidx >= 0) { AVStream* ast = fmt->streams[aidx]; const AVCodec* ac = avcodec_find_decoder(ast->codecpar->codec_id); adec = avcodec_alloc_context3(ac); if (!ac || !adec || avcodec_parameters_to_context(adec, ast->codecpar) < 0 || avcodec_open2(adec, ac, nullptr) < 0) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio decoder open failed, video only"); avcodec_free_context(&adec); aidx = -1; } } if (aidx >= 0) { asr = adec->sample_rate; ach = adec->ch_layout.nb_channels; if (asr <= 0 || ach <= 0) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: bad audio fmt"); avcodec_free_context(&adec); aidx = -1; } } if (aidx >= 0) { audioPcm = std::make_shared>(); audioPcm->reserve((size_t)(durMs > 0 ? (durMs * asr * ach / 1000) + 4096 : 4096)); int rc = swr_alloc_set_opts2(&swr, &adec->ch_layout, AV_SAMPLE_FMT_S16, asr, &adec->ch_layout, adec->sample_fmt, adec->sample_rate, 0, nullptr); if (rc < 0 || swr_init(swr) < 0) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: swr init failed, video only"); swr_free(&swr); avcodec_free_context(&adec); aidx = -1; audioPcm.reset(); } } if (aidx >= 0) { aframe = av_frame_alloc(); pkt = av_packet_alloc(); if (!aframe || !pkt) { worker_fail(this, "alloc failed"); cleanup(); return; } while (av_read_frame(fmt, pkt) >= 0) { if (abort->loadRelaxed()) { av_packet_unref(pkt); cleanup(); return; } if (pkt->stream_index == aidx) { if (avcodec_send_packet(adec, pkt) == 0) { while (avcodec_receive_frame(adec, aframe) == 0) { int outSamples = av_rescale_rnd(swr_get_delay(swr, aframe->sample_rate) + aframe->nb_samples, asr, aframe->sample_rate, AV_ROUND_UP); uint8_t* out = nullptr; av_samples_alloc(&out, nullptr, ach, outSamples, AV_SAMPLE_FMT_S16, 0); int got = swr_convert(swr, &out, outSamples, (const uint8_t**)aframe->data, aframe->nb_samples); if (got > 0) { size_t cur = audioPcm->size(); audioPcm->resize(cur + (size_t)got * ach); memcpy(audioPcm->data() + cur, out, (size_t)got * ach * sizeof(int16_t)); } av_freep(&out); } } } av_packet_unref(pkt); } av_packet_free(&pkt); pkt = nullptr; av_frame_free(&aframe); aframe = nullptr; avcodec_free_context(&adec); adec = nullptr; swr_free(&swr); swr = nullptr; qint64 audioDurMs = audioPcm->size() / (size_t)ach * 1000 / asr; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio decoded %zu frames %dHz %dch %.1fs", audioPcm->size() / (size_t)ach, asr, ach, audioDurMs / 1000.0); /* вернуть чтение в начало для видео */ av_seek_frame(fmt, -1, 0, AVSEEK_FLAG_BACKWARD); emit audioDecoded(audioPcm, asr, ach, audioDurMs); } /* ── 2. декодируем видео и шлём кадры ── */ { const AVCodec* vc = avcodec_find_decoder(vst->codecpar->codec_id); vdec = avcodec_alloc_context3(vc); if (!vc || !vdec || avcodec_parameters_to_context(vdec, vst->codecpar) < 0 || avcodec_open2(vdec, vc, nullptr) < 0) { worker_fail(this, "video decoder open failed"); cleanup(); return; } frame = av_frame_alloc(); pkt = av_packet_alloc(); if (!frame || !pkt) { worker_fail(this, "alloc failed"); cleanup(); return; } /* YUVJ* (deprecated full-range) → обычный формат + явный range, чтобы * не ловить warning swscaler и не искажать цвета */ AVPixelFormat srcFmt = vdec->pix_fmt; int srcRange = 0; switch (srcFmt) { case AV_PIX_FMT_YUVJ420P: srcFmt = AV_PIX_FMT_YUV420P; srcRange = 1; break; case AV_PIX_FMT_YUVJ422P: srcFmt = AV_PIX_FMT_YUV422P; srcRange = 1; break; case AV_PIX_FMT_YUVJ444P: srcFmt = AV_PIX_FMT_YUV444P; srcRange = 1; break; default: break; } sws = sws_getContext(vw, vh, srcFmt, vw, vh, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); if (!sws) { worker_fail(this, "sws init failed"); cleanup(); return; } int *invTbl = nullptr, *tbl = nullptr, sr = 0, dr = 0, br = 0, ct = 0, st = 0; if (sws_getColorspaceDetails(sws, &invTbl, &sr, &tbl, &dr, &br, &ct, &st) >= 0) sws_setColorspaceDetails(sws, invTbl, srcRange, tbl, 1 /* RGB full-range */, br, ct, st); int gen = epoch->loadRelaxed(); auto emitFrame = [&](AVFrame* fr) { if (fr->pts == AV_NOPTS_VALUE) return; /* кадр без pts ломает очередь */ QImage img(vw, vh, QImage::Format_RGB888); uint8_t* dst[1] = { img.bits() }; int dstStride[1] = { (int)img.bytesPerLine() }; sws_scale(sws, fr->data, fr->linesize, 0, vh, dst, dstStride); qint64 ptsMs = av_rescale_q(fr->pts, vst->time_base, AVRational{1, 1000}); buffered->fetchAndAddRelaxed(1); emit frameDecoded(img, ptsMs, gen); }; bool eof = false; while (!abort->loadRelaxed()) { if (seekReq->loadRelaxed()) { qint64 targetMs = seekTargetMs->loadRelaxed(); int sret = av_seek_frame(fmt, -1, targetMs * 1000, AVSEEK_FLAG_BACKWARD); if (sret < 0) DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek to %lldms failed ret=%d", (long long)targetMs, sret); else DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek to %lldms", (long long)targetMs); avcodec_flush_buffers(vdec); gen = epoch->loadRelaxed(); eof = false; seekReq->storeRelaxed(0); } if (paused->loadRelaxed()) { QThread::msleep(10); continue; } if (eof) { QThread::msleep(10); continue; } /* ждём seek или abort */ if (buffered->loadRelaxed() >= kMaxBuffered) { QThread::msleep(3); continue; } int ret = av_read_frame(fmt, pkt); if (ret < 0) { avcodec_send_packet(vdec, nullptr); while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame); eof = true; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: decode finished"); emit finished(); continue; } if (pkt->stream_index == vidx) { avcodec_send_packet(vdec, pkt); while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame); } av_packet_unref(pkt); } } cleanup(); } #else /* HAVE_FFMPEG */ void Worker::run() { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: built without FFmpeg"); emit error(QStringLiteral("видео недоступно (сборка без FFmpeg)")); } #endif /* HAVE_FFMPEG */ /* ── VideoPlayerEngine ── */ VideoPlayerEngine::VideoPlayerEngine(QObject* parent) : QObject(parent) { qRegisterMetaType("QImage"); qRegisterMetaType>>(); m_presentTimer.setInterval(16); connect(&m_presentTimer, &QTimer::timeout, this, &VideoPlayerEngine::presentTick); } VideoPlayerEngine::~VideoPlayerEngine() { close(); } void VideoPlayerEngine::open(const QString& path) { close(); m_path = path; m_abort.storeRelaxed(0); m_paused.storeRelaxed(0); m_seekReq.storeRelaxed(0); m_buffered.storeRelaxed(0); clearPending(); m_renderFrame = QImage(); m_durationMs = 0; m_positionMs = 0; m_hasAudio = false; m_videoWidth = m_videoHeight = 0; m_audioSampleRate = 0; m_thread = new QThread(this); m_worker = new Worker(); m_worker->path = path; m_worker->abort = &m_abort; m_worker->paused = &m_paused; m_worker->seekReq = &m_seekReq; m_worker->seekTargetMs = &m_seekTargetMs; m_worker->buffered = &m_buffered; m_worker->epoch = &m_epoch; m_worker->moveToThread(m_thread); connect(m_thread, &QThread::started, m_worker, &Worker::run); uint64_t generation = ++m_openGeneration; connect(m_worker, &Worker::opened, this, [this, generation](int w, int h, qint64 duration, bool audio) { if (generation == m_openGeneration) onWorkerOpened(w, h, duration, audio); }); connect(m_worker, &Worker::audioDecoded, this, [this, generation](auto pcm, int rate, int channels, qint64 duration) { if (generation == m_openGeneration) onAudioDecoded(pcm, rate, channels, duration); }); connect(m_worker, &Worker::frameDecoded, this, [this, generation](const QImage& frame, qint64 pts, int epoch) { if (generation == m_openGeneration) onFrameDecoded(frame, pts, epoch); }); connect(m_worker, &Worker::error, this, [this, generation](const QString& error) { if (generation == m_openGeneration) onWorkerError(error); }); connect(m_worker, &Worker::finished, this, [this, generation]() { if (generation == m_openGeneration) onWorkerFinished(); }); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: open %s", qPrintable(path)); m_thread->start(); } void VideoPlayerEngine::play() { if (m_state != Paused && m_state != Playing) return; m_paused.storeRelaxed(0); if (m_hasAudio) SoundManager::instance()->resumePcm(m_audioPlayback); else { m_elapsedBaseMs = m_positionMs; m_elapsed.restart(); } setState(Playing); m_presentTimer.start(); } void VideoPlayerEngine::pause() { if (m_state != Playing) return; m_paused.storeRelaxed(1); if (m_hasAudio) SoundManager::instance()->pausePcm(m_audioPlayback); setState(Paused); m_presentTimer.stop(); } void VideoPlayerEngine::togglePlayPause() { if (m_state == Playing) pause(); else if (m_state == Paused) play(); else if (m_state == Finished) { seekMs(0); play(); } } void VideoPlayerEngine::seekMs(qint64 ms) { if (ms < 0) ms = 0; if (m_durationMs > 0 && ms > m_durationMs) ms = m_durationMs; m_epoch.fetchAndAddRelaxed(1); m_seekTargetMs.storeRelaxed(ms); m_seekReq.storeRelaxed(1); clearPending(); if (m_hasAudio) { int sr = SoundManager::instance()->pcmSampleRate(m_audioPlayback); if (sr > 0) SoundManager::instance()->seekPcm(m_audioPlayback, (unsigned long long)(ms * sr / 1000)); } else { m_elapsedBaseMs = ms; m_elapsed.restart(); } m_positionMs = ms; if (m_state == Finished) setState(Paused); emit positionChanged(ms); } void VideoPlayerEngine::stop() { ++m_openGeneration; m_abort.storeRelaxed(1); m_presentTimer.stop(); clearPending(); m_renderFrame = QImage(); if (m_hasAudio) SoundManager::instance()->stopPcm(m_audioPlayback); setState(Stopped); m_positionMs = 0; m_durationMs = 0; } void VideoPlayerEngine::close() { stop(); if (m_thread) { m_abort.storeRelaxed(1); m_thread->quit(); m_thread->wait(2000); delete m_worker; m_worker = nullptr; delete m_thread; m_thread = nullptr; } } qint64 VideoPlayerEngine::clockMs() const { if (m_hasAudio && m_audioSampleRate > 0) { if (m_state == Finished && !SoundManager::instance()->isPcmPlaying(m_audioPlayback)) return m_durationMs; /* аудио закончилось — часы в конец */ unsigned long long cur = SoundManager::instance()->pcmCursor(m_audioPlayback); return (qint64)(cur * 1000 / (unsigned long long)m_audioSampleRate); } return m_elapsedBaseMs + m_elapsed.elapsed(); } void VideoPlayerEngine::setState(State s) { if (m_state == s) return; m_state = s; emit stateChanged((int)s); } void VideoPlayerEngine::clearPending() { m_buffered.fetchAndSubRelaxed((int)m_pending.size()); m_pending.clear(); } void VideoPlayerEngine::onWorkerOpened(int width, int height, qint64 durationMs, bool hasAudio) { m_videoWidth = width; m_videoHeight = height; m_durationMs = durationMs; m_hasAudio = hasAudio; emit opened(width, height, durationMs, hasAudio); if (!hasAudio) { m_elapsedBaseMs = 0; m_elapsed.restart(); setState(Playing); m_presentTimer.start(); } } void VideoPlayerEngine::onAudioDecoded(std::shared_ptr> pcm, int sampleRate, int channels, qint64 durationMs) { if (pcm && !pcm->empty() && channels > 0) { m_audioSampleRate = sampleRate; SoundManager::instance()->stopPcm(m_audioPlayback); m_audioPlayback = SoundManager::instance()->playRawPcm(pcm->data(), (int)(pcm->size() / (size_t)channels), sampleRate, channels, pcm); if (!SoundManager::instance()->isPcmPlaying(m_audioPlayback)) { /* аудио-устройство недоступно — переходим на системные часы */ m_hasAudio = false; m_audioSampleRate = 0; DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio not playing, fallback to system clock"); } else { DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started %dHz %dch", sampleRate, channels, pcm); } } else { m_hasAudio = false; DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: no audio data, video only"); } m_elapsedBaseMs = 0; m_elapsed.restart(); setState(Playing); m_presentTimer.start(); } void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, int epoch) { if (epoch != m_epoch.loadRelaxed()) { m_buffered.fetchAndSubRelaxed(1); /* устаревший кадр после seek — не будет показан */ return; } m_pending.push_back({ image, ptsMs }); } void VideoPlayerEngine::onWorkerError(const QString& msg) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: %s", qPrintable(msg)); m_presentTimer.stop(); setState(Stopped); emit error(msg); } void VideoPlayerEngine::onWorkerFinished() { DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: worker finished"); /* дождёмся, пока отрисуются оставшиеся кадры в presentTick, потом Finished */ setState(Finished); emit finished(); } void VideoPlayerEngine::presentTick() { qint64 clk = clockMs(); if (m_durationMs > 0 && clk > m_durationMs) clk = m_durationMs; QImage show; bool have = false; while (!m_pending.empty() && m_pending.front().second <= clk) { show = m_pending.front().first; m_pending.pop_front(); m_buffered.fetchAndSubRelaxed(1); have = true; } if (have) { m_renderFrame = show; emit framePresented(); } m_positionMs = clk; emit positionChanged(m_positionMs); if (m_state == Finished && m_pending.empty()) { m_presentTimer.stop(); if (m_hasAudio) SoundManager::instance()->stopPcm(m_audioPlayback); } } #include "videoplayer_engine.moc"