321 lines
11 KiB
C++
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);
|
|
}
|