zero_copy_video_player/videodecoder.cpp
2026-08-05 14:07:38 -03:00

321 lines
11 KiB
C++

#include "videodecoder.h"
#include <QDebug>
// Definimos el formato de píxeles por hardware que utilizaremos en el pipeline (CUDA)
enum AVPixelFormat VideoDecoder::m_hwPixFmt = AV_PIX_FMT_CUDA;
// Callback estático requerido por FFmpeg para negociar el formato de renderizado.
// Si el hardware soporta el formato de píxeles deseado (AV_PIX_FMT_CUDA), lo seleccionamos.
enum AVPixelFormat VideoDecoder::get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts) {
(void)ctx;
const enum AVPixelFormat *p;
for (p = pix_fmts; *p != -1; p++) {
if (*p == m_hwPixFmt) {
return *p;
}
}
qWarning() << "No se encontró soporte para formato de hardware (CUDA).";
return AV_PIX_FMT_NONE;
}
VideoDecoder::VideoDecoder(QObject *parent)
: QThread(parent)
, m_running(false)
, m_isPaused(true)
, m_seekRequested(false)
, m_seekTimeSecs(0.0)
, m_duration(0.0)
, m_videoWidth(0)
, m_videoHeight(0)
, m_videoStreamIndex(-1)
, m_pausedElapsedMs(0)
, m_formatContext(nullptr)
, m_codecContext(nullptr)
, m_hwDeviceCtx(nullptr)
{
// Reservamos espacio en memoria para el fotograma compartido de video
m_currentFrame = av_frame_alloc();
}
VideoDecoder::~VideoDecoder() {
closeFile();
if (m_currentFrame) {
av_frame_free(&m_currentFrame);
m_currentFrame = nullptr;
}
}
bool VideoDecoder::openFile(const QString &fileName) {
closeFile();
m_fileName = fileName;
// 1. Abrir el contenedor multimedia (demuxer)
if (avformat_open_input(&m_formatContext, m_fileName.toLocal8Bit().constData(), nullptr, nullptr) < 0) {
qWarning() << "No se pudo abrir el archivo de video:" << fileName;
return false;
}
// 2. Obtener información de los flujos del contenedor
if (avformat_find_stream_info(m_formatContext, nullptr) < 0) {
qWarning() << "No se pudo encontrar información de flujos.";
closeFile();
return false;
}
// 3. Buscar el flujo de video principal
const AVCodec *codec = nullptr;
m_videoStreamIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (m_videoStreamIndex < 0) {
qWarning() << "No se encontró flujo de video en el archivo.";
closeFile();
return false;
}
AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex];
m_videoWidth = videoStream->codecpar->width;
m_videoHeight = videoStream->codecpar->height;
// Convertimos la duración del video de la base de tiempo interna de FFmpeg a segundos reales
if (m_formatContext->duration != AV_NOPTS_VALUE) {
m_duration = static_cast<double>(m_formatContext->duration) / AV_TIME_BASE;
} else {
m_duration = 0.0;
}
// 4. Crear el contexto de decodificación
m_codecContext = avcodec_alloc_context3(codec);
if (!m_codecContext) {
qWarning() << "No se pudo asignar el contexto del codec.";
closeFile();
return false;
}
// Copiar parámetros del flujo al contexto del decodificador
if (avcodec_parameters_to_context(m_codecContext, videoStream->codecpar) < 0) {
qWarning() << "No se pudieron copiar los parámetros del codec.";
closeFile();
return false;
}
// 5. Inicializar la decodificación por hardware (NVDEC / CUDA)
// Crear el contexto de hardware que comunica FFmpeg con la API de CUDA
AVHWDeviceType devType = AV_HWDEVICE_TYPE_CUDA;
if (av_hwdevice_ctx_create(&m_hwDeviceCtx, devType, nullptr, nullptr, 0) < 0) {
qWarning() << "No se pudo inicializar la decodificación por hardware de NVIDIA (CUDA).";
closeFile();
return false;
}
m_codecContext->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
// Configurar callbacks y formatos para la decodificación
m_codecContext->get_format = get_hw_format;
m_codecContext->thread_count = 1; // Un solo hilo para evitar problemas de sincronización de contextos CUDA
// 6. Abrir el decodificador de video
if (avcodec_open2(m_codecContext, codec, nullptr) < 0) {
qWarning() << "No se pudo abrir el codec.";
closeFile();
return false;
}
m_pausedElapsedMs = 0;
m_isPaused = true;
m_seekRequested = false;
return true;
}
void VideoDecoder::closeFile() {
// Esperamos a que finalice el hilo antes de liberar sus recursos
stopThread();
if (m_codecContext) {
avcodec_free_context(&m_codecContext);
m_codecContext = nullptr;
}
if (m_formatContext) {
avformat_close_input(&m_formatContext);
m_formatContext = nullptr;
}
if (m_hwDeviceCtx) {
av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr;
}
// Liberamos la referencia del fotograma actual de forma segura protegiendo el puntero
{
QMutexLocker frameLocker(&m_frameMutex);
if (m_currentFrame) {
av_frame_unref(m_currentFrame);
}
}
m_videoStreamIndex = -1;
m_videoWidth = 0;
m_videoHeight = 0;
m_duration = 0.0;
}
void VideoDecoder::play() {
QMutexLocker locker(&m_mutex);
if (m_isPaused) {
m_isPaused = false;
m_playbackTimer.restart(); // Inicia el reloj maestro
m_waitCond.wakeAll(); // Despierta al hilo de decodificación si estaba bloqueado
}
}
void VideoDecoder::pause() {
QMutexLocker locker(&m_mutex);
if (!m_isPaused) {
m_isPaused = true;
// Acumula el tiempo transcurrido hasta este momento y detiene el temporizador activo
m_pausedElapsedMs += m_playbackTimer.elapsed();
m_playbackTimer.invalidate();
}
}
void VideoDecoder::seek(double seconds) {
QMutexLocker locker(&m_mutex);
m_seekTimeSecs = seconds;
m_seekRequested = true;
m_waitCond.wakeAll(); // Despierta al hilo para procesar el salto inmediatamente
}
void VideoDecoder::stopThread() {
{
QMutexLocker locker(&m_mutex);
m_running = false;
m_waitCond.wakeAll();
}
wait(); // Espera síncrona a que termine el hilo secundario
}
qint64 VideoDecoder::getPlaybackTimeMs() const {
if (m_isPaused || !m_playbackTimer.isValid()) {
return m_pausedElapsedMs;
}
return m_pausedElapsedMs + m_playbackTimer.elapsed();
}
bool VideoDecoder::getCurrentFrameRef(AVFrame *destFrame) {
// Cerramos el mutex para garantizar que el hilo decodificador no reemplace m_currentFrame
// mientras OpenGL clona la referencia.
QMutexLocker frameLocker(&m_frameMutex);
if (!m_currentFrame || m_currentFrame->width == 0) {
return false;
}
av_frame_unref(destFrame);
// Clonamos la referencia rápida de FFmpeg (copia punteros internos del búfer de VRAM de hardware, no los píxeles)
av_frame_ref(destFrame, m_currentFrame);
return true;
}
void VideoDecoder::run() {
m_running = true;
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (true) {
// --- CONTROL DE ESTADO (PAUSA / PARADA) ---
{
QMutexLocker locker(&m_mutex);
if (!m_running) break;
if (m_isPaused && !m_seekRequested) {
// Dormimos el hilo hasta que se llame a play() o seek()
m_waitCond.wait(&m_mutex);
if (!m_running) break;
}
}
// --- CONTROL DE SALTO DE TIEMPO (SEEK) ---
bool seekNeeded = false;
double targetSeekSecs = 0.0;
{
QMutexLocker locker(&m_mutex);
if (m_seekRequested) {
seekNeeded = true;
targetSeekSecs = m_seekTimeSecs;
m_seekRequested = false;
}
}
if (seekNeeded) {
// Convertimos la posición de segundos al formato interno de base de tiempo del flujo
AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex];
int64_t targetTimestamp = static_cast<int64_t>(targetSeekSecs / av_q2d(videoStream->time_base));
// Realizar salto al fotograma clave (keyframe) más cercano
avformat_seek_file(m_formatContext, m_videoStreamIndex, INT64_MIN, targetTimestamp, INT64_MAX, 0);
// Vaciar los buffers del decodificador para no mezclar frames antiguos con nuevos
avcodec_flush_buffers(m_codecContext);
// Ajustar el reloj de referencia a la nueva marca de tiempo elegida
{
QMutexLocker locker(&m_mutex);
m_pausedElapsedMs = static_cast<qint64>(targetSeekSecs * 1000.0);
if (!m_isPaused) {
m_playbackTimer.restart();
}
}
}
// --- LECTURA Y DECODIFICACIÓN ---
if (av_read_frame(m_formatContext, packet) < 0) {
// Llegamos al final del video
{
QMutexLocker frameLocker(&m_frameMutex);
av_frame_unref(m_currentFrame);
}
emit playbackFinished();
// Forzamos la pausa del hilo
QThread::msleep(10);
QMutexLocker locker(&m_mutex);
m_isPaused = true;
m_playbackTimer.invalidate();
continue;
}
if (packet->stream_index == m_videoStreamIndex) {
// Enviamos el paquete comprimido al decodificador físico
if (avcodec_send_packet(m_codecContext, packet) >= 0) {
// Leemos los fotogramas decodificados por el hardware
while (avcodec_receive_frame(m_codecContext, frame) >= 0) {
// 1. Obtener el PTS del frame (en ticks) y convertirlo a milisegundos de tiempo real
double ptsSecs = frame->best_effort_timestamp * av_q2d(m_formatContext->streams[m_videoStreamIndex]->time_base);
qint64 ptsMs = static_cast<qint64>(ptsSecs * 1000.0);
// 2. Comparar contra el Reloj Maestro
qint64 playbackTime = getPlaybackTimeMs();
if (ptsMs > playbackTime) {
// El frame viene del futuro: el decodificador va muy rápido.
// Calculamos la demora y ponemos a dormir al hilo decodificador para sincronizar.
qint64 delay = ptsMs - playbackTime;
if (delay < 2000) { // Margen de seguridad razonable
QThread::msleep(delay);
}
}
// 3. Guardar el fotograma CUDA en VRAM protegiendo el acceso multihilo
{
QMutexLocker frameLocker(&m_frameMutex);
av_frame_unref(m_currentFrame);
av_frame_ref(m_currentFrame, frame); // Guardamos la nueva referencia
}
// Notificar a OpenGL y a la UI que actualicen su posición y pinten
emit frameReady();
emit positionChanged(ptsMs);
}
}
}
av_packet_unref(packet);
}
av_packet_free(&packet);
av_frame_free(&frame);
}