#include "videodecoder.h" #include // 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(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(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(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(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); }