Se comenta todo el código

This commit is contained in:
sebastian 2026-08-05 14:07:38 -03:00
parent 1e5cf8f22a
commit 7532e7b330
9 changed files with 328 additions and 168 deletions

View file

@ -13,14 +13,16 @@ GLWidget::GLWidget(QWidget *parent)
, m_cudaResourceUV(nullptr)
, m_allocatedWidth(0)
, m_allocatedHeight(0)
{
// Almacenamos el contenedor local del frame decodificado
m_localFrame = av_frame_alloc();
}
GLWidget::~GLWidget() {
// Nos aseguramos de tener el contexto de OpenGL activo antes de borrar recursos de la GPU
makeCurrent();
cleanupCudaGL();
if (m_texY) {
glDeleteTextures(1, &m_texY);
}
@ -30,6 +32,8 @@ GLWidget::~GLWidget() {
if (m_vbo) {
glDeleteBuffers(1, &m_vbo);
}
// Liberamos la estructura local de FFmpeg
if (m_localFrame) {
av_frame_free(&m_localFrame);
m_localFrame = nullptr;
@ -38,30 +42,34 @@ GLWidget::~GLWidget() {
void GLWidget::setVideoDecoder(VideoDecoder *decoder) {
m_videoDecoder = decoder;
// Conectamos la señal frameReady del decodificador con la repintada de este Widget
connect(m_videoDecoder, &VideoDecoder::frameReady, this, &GLWidget::onFrameReady);
}
void GLWidget::onFrameReady() {
// Solicita a Qt redibujar el widget (desencadena una llamada interna a paintGL())
update();
}
void GLWidget::clearVideo() {
// Libera los recursos de interoperabilidad y actualiza el render
cleanupCudaGL();
update();
}
void GLWidget::initializeGL() {
// Inicializa la tabla de funciones de OpenGL para poder llamarlas directamente
initializeOpenGLFunctions();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// Compilar e inicializar shaders
// Compilar e inicializar los shaders desde los archivos fuente
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex, "shader.vert");
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment, "shader.frag");
m_shaderProgram.link();
// Crear VBO para un quad de pantalla completa (espejado verticalmente para corregir la diferencia de origen OpenGL vs CUDA)
// Crear VBO para un quad de pantalla completa (dos triángulos coordinados en 2D)
// Nota: Espejamos el eje vertical (coordenada V) de textura para compensar la diferencia
// de origen de coordenadas: OpenGL asume esquina inferior-izquierda como (0,0) y CUDA asume superior-izquierda.
float vertices[] = {
// Posiciones // Coordenadas UV
-1.0f, -1.0f, 0.0f, 1.0f,
@ -78,6 +86,7 @@ void GLWidget::initializeGL() {
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Generar texturas de OpenGL
// Textura Y (Luminancia)
glGenTextures(1, &m_texY);
glBindTexture(GL_TEXTURE_2D, m_texY);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@ -85,6 +94,7 @@ void GLWidget::initializeGL() {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Textura UV (Crominancia entrelazada)
glGenTextures(1, &m_texUV);
glBindTexture(GL_TEXTURE_2D, m_texUV);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@ -94,6 +104,7 @@ void GLWidget::initializeGL() {
}
void GLWidget::resizeGL(int w, int h) {
// Ajusta la ventana gráfica de OpenGL al tamaño del widget físico
glViewport(0, 0, w, h);
}
@ -102,27 +113,29 @@ void GLWidget::paintGL() {
if (!m_videoDecoder) return;
// Obtener la referencia segura del fotograma CUDA decodificado en GPU
// 1. Obtener la referencia segura del fotograma decodificado por hardware (residiendo en VRAM)
if (!m_videoDecoder->getCurrentFrameRef(m_localFrame)) {
return; // No hay frame listo todavía
return; // No hay frame listo todavía para dibujar
}
int w = m_localFrame->width;
int h = m_localFrame->height;
// Si la resolución del video cambió o no se ha asignado aún
// 2. Si la resolución del video cambió o no se ha asignado aún espacio en VRAM
if (w != m_allocatedWidth || h != m_allocatedHeight) {
cleanupCudaGL();
// 1. Reservar espacio en VRAM para la textura Y (Luminancia - 1 canal de 8 bits)
// 2.1 Reservar espacio en VRAM para la textura Y (Luminancia - formato GL_RED, tamaño completo)
glBindTexture(GL_TEXTURE_2D, m_texY);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
// 2. Reservar espacio en VRAM para la textura UV (Crominancia entrelazada - 2 canales de 8 bits)
// 2.2 Reservar espacio en VRAM para la textura UV (Crominancia - formato GL_RG, tamaño a la mitad)
glBindTexture(GL_TEXTURE_2D, m_texUV);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
// Registrar las texturas en la API de interoperabilidad de CUDA
// 2.3 Registrar los identificadores de textura de OpenGL en la API de interoperabilidad de CUDA.
// La bandera 'cudaGraphicsRegisterFlagsWriteDiscard' le dice a CUDA que sobrescribiremos la textura
// por completo y que descarte el contenido anterior para mejorar el rendimiento del driver.
cudaError_t errY = cudaGraphicsGLRegisterImage(&m_cudaResourceY, m_texY, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
cudaError_t errUV = cudaGraphicsGLRegisterImage(&m_cudaResourceUV, m_texUV, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
@ -137,7 +150,7 @@ void GLWidget::paintGL() {
m_allocatedHeight = h;
}
// 3. Mapear las texturas de OpenGL hacia CUDA, realizar la copia VRAM-to-VRAM, y desmapear
// 3. Mapear las texturas de OpenGL hacia CUDA para poder escribir en ellas de manera exclusiva
if (m_cudaResourceY && m_cudaResourceUV) {
cudaError_t mapY = cudaGraphicsMapResources(1, &m_cudaResourceY, 0);
cudaError_t mapUV = cudaGraphicsMapResources(1, &m_cudaResourceUV, 0);
@ -146,10 +159,12 @@ void GLWidget::paintGL() {
cudaArray_t arrayY = nullptr;
cudaArray_t arrayUV = nullptr;
// Extraemos los manejadores de arrays internos de CUDA asociados a nuestras texturas de OpenGL
cudaGraphicsSubResourceGetMappedArray(&arrayY, m_cudaResourceY, 0, 0);
cudaGraphicsSubResourceGetMappedArray(&arrayUV, m_cudaResourceUV, 0, 0);
// Copia Directa en GPU de la textura Y (Luminancia)
// Copia Directa interna de GPU (VRAM-to-VRAM) de la textura Y (Luminancia)
// Usamos linesize[0] como el pitch (ancho de fila físico en memoria) del plano.
cudaError_t cpyY = cudaMemcpy2DToArray(
arrayY, 0, 0,
(void*)m_localFrame->data[0], m_localFrame->linesize[0],
@ -157,7 +172,8 @@ void GLWidget::paintGL() {
cudaMemcpyDeviceToDevice
);
// Copia Directa en GPU de la textura UV (Crominancia entrelazada)
// Copia Directa interna de GPU (VRAM-to-VRAM) de la textura UV (Crominancia entrelazada)
// Se le pasa el ancho físico en bytes ('w' bytes) debido a que tiene 2 componentes de 1 byte (U y V) por píxel.
cudaError_t cpyUV = cudaMemcpy2DToArray(
arrayUV, 0, 0,
(void*)m_localFrame->data[1], m_localFrame->linesize[1],
@ -172,25 +188,27 @@ void GLWidget::paintGL() {
qWarning() << "Fallo al mapear texturas en CUDA:" << cudaGetErrorString(mapY) << "/" << cudaGetErrorString(mapUV);
}
// Importante: desmapear para que OpenGL recupere el control de renderizado
// Importante: desmapear los recursos para que CUDA libere el bloqueo y OpenGL vuelva a tomar el control del pintado
cudaGraphicsUnmapResources(1, &m_cudaResourceY, 0);
cudaGraphicsUnmapResources(1, &m_cudaResourceUV, 0);
}
// 4. Dibujar la escena en pantalla usando el Shader CRT
// 4. Dibujar la escena en pantalla usando el Shader
m_shaderProgram.bind();
// Enlazar textura Y a la unidad de textura 0
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texY);
m_shaderProgram.setUniformValue("u_texY", 0);
// Enlazar textura UV a la unidad de textura 1
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_texUV);
m_shaderProgram.setUniformValue("u_texUV", 1);
// Activar el VBO de vértices y configurar atributos para a_position y a_texCoord
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
int posLocation = m_shaderProgram.attributeLocation("a_position");
m_shaderProgram.enableAttributeArray(posLocation);
glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr);
@ -199,18 +217,21 @@ void GLWidget::paintGL() {
m_shaderProgram.enableAttributeArray(texLocation);
glVertexAttribPointer(texLocation, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(2 * sizeof(float)));
// Dibujar los 6 vértices (2 triángulos que forman el quad)
glDrawArrays(GL_TRIANGLES, 0, 6);
// Desactivar y liberar
m_shaderProgram.disableAttributeArray(posLocation);
m_shaderProgram.disableAttributeArray(texLocation);
m_shaderProgram.release();
// Liberar la referencia local del fotograma
// Liberar nuestra referencia local del fotograma (decrementa el conteo de referencias del AVFrame de FFmpeg)
av_frame_unref(m_localFrame);
}
void GLWidget::cleanupCudaGL() {
makeCurrent();
// Desregistra los recursos de CUDA asociados a las texturas de OpenGL
if (m_cudaResourceY) {
cudaGraphicsUnregisterResource(m_cudaResourceY);
m_cudaResourceY = nullptr;

View file

@ -6,59 +6,75 @@
#include <QOpenGLShaderProgram>
#include <QMutex>
// Cabeceras del Runtime de CUDA e Interoperabilidad CUDA-OpenGL
#include <cuda_runtime.h>
#include <cuda_gl_interop.h>
extern "C" {
// Estructuras de datos multimedia de FFmpeg (necesarias para el manejo del AVFrame)
#include <libavcodec/avcodec.h>
}
class VideoDecoder;
/**
* @brief Clase Widget OpenGL dedicada a renderizar los fotogramas decodificados en la GPU.
* Utiliza interoperabilidad CUDA-GL para realizar copias directas de memoria VRAM a VRAM
* sin intervención de la memoria RAM del sistema (Zero-Copy).
*/
class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT
public:
explicit GLWidget(QWidget *parent = nullptr);
~GLWidget() override;
// Vincula el decodificador de video y conecta las señales de reproducción
void setVideoDecoder(VideoDecoder *decoder);
// Limpia y desregistra los recursos de CUDA y OpenGL de la tarjeta gráfica
void clearVideo();
public slots:
// Slot encargado de forzar la repintada del widget al recibir un nuevo fotograma
void onFrameReady();
protected:
// Configuración inicial de OpenGL (compilación de shaders, buffers y texturas)
void initializeGL() override;
// Ajuste de proyección y Viewport al cambiar de tamaño la ventana
void resizeGL(int w, int h) override;
// Renderizado principal: realiza la copia CUDA VRAM-to-VRAM y dibuja el quad en pantalla
void paintGL() override;
private:
// Desregistra las texturas de OpenGL del contexto de CUDA para liberar recursos de la GPU
void cleanupCudaGL();
// Puntero al decodificador de video (para leer los fotogramas compartidos)
VideoDecoder *m_videoDecoder;
// Programa de Sombreado de OpenGL (Vertex + Fragment Shader compilados)
QOpenGLShaderProgram m_shaderProgram;
// Texturas de OpenGL para NV12
// Identificadores de texturas OpenGL: m_texY (Luminancia) y m_texUV (Crominancia)
GLuint m_texY;
GLuint m_texUV;
// Vertex Buffer Object (VBO) para el quad plano (triángulos) que llena la pantalla
GLuint m_vbo;
// Recursos gráficos registrados de CUDA
// Punteros de control para registrar y mapear las texturas de OpenGL en la API de CUDA
cudaGraphicsResource_t m_cudaResourceY;
cudaGraphicsResource_t m_cudaResourceUV;
// Fotograma local reutilizable
// Contenedor local para clonar la referencia del fotograma decodificado
AVFrame *m_localFrame;
// Dimensiones de texturas registradas en CUDA
// Registra la resolución para la cual están alojadas las texturas actuales (detecta cambios en el video)
int m_allocatedWidth;
int m_allocatedHeight;
};
#endif // GLWIDGET_H

View file

@ -2,13 +2,18 @@
#include <QApplication>
int main(int argc, char *argv[]) {
// 1. Inicializa la infraestructura y el ciclo de eventos de la interfaz de usuario de Qt (QApplication)
QApplication a(argc, argv);
// 2. Instancia y despliega la ventana de control principal del reproductor
MainWindow w;
w.show();
// 3. Si se pasa un argumento por línea de comandos (ej. un archivo de video), lo reproducimos de inmediato
if (argc > 1) {
w.openVideoFileDirect(QString::fromLocal8Bit(argv[1]));
}
// 4. Inicia el procesamiento y bucle principal de eventos del motor de Qt (bloquea la ejecución hasta el cierre de la app)
return a.exec();
}

View file

@ -21,17 +21,18 @@ MainWindow::MainWindow(QWidget *parent)
setupUI();
// Conectar las señales de posición y finalización del hilo descodificador
// Conectar las señales de posición y finalización del hilo decodificador
connect(m_videoDecoder, &VideoDecoder::positionChanged, this, &MainWindow::onPositionChanged);
connect(m_videoDecoder, &VideoDecoder::playbackFinished, this, &MainWindow::onPlaybackFinished);
}
MainWindow::~MainWindow() {
// Detiene de forma segura el hilo decodificador antes de destruir la ventana principal
m_videoDecoder->stopThread();
}
void MainWindow::setupUI() {
// Aplicar hoja de estilos (QSS) para un look oscuro premium y retro-futurista
// Aplicar hoja de estilos (QSS) para un look oscuro premium
this->setStyleSheet(
"QMainWindow { background-color: #08080f; }"
"QWidget { color: #f0f0f5; font-family: 'Outfit', 'Segoe UI', sans-serif; font-size: 13px; }"
@ -49,35 +50,41 @@ void MainWindow::setupUI() {
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
// Layout principal en columna vertical (Pintado OpenGL arriba, controles de reproducción abajo)
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(10);
mainLayout->addWidget(m_glWidget, 1);
// Controles Multimedia
// --- CONTROLES MULTIMEDIA ---
QHBoxLayout *controlsLayout = new QHBoxLayout();
// Botón para buscar y abrir archivos de video
QPushButton *openBtn = new QPushButton("Abrir Video", this);
openBtn->setStyleSheet("background-color: #00bfff;");
connect(openBtn, &QPushButton::clicked, this, &MainWindow::openVideoFile);
controlsLayout->addWidget(openBtn);
// Botón de Play / Pausa
m_playBtn = new QPushButton("▶", this);
connect(m_playBtn, &QPushButton::clicked, this, &MainWindow::togglePlayPause);
controlsLayout->addWidget(m_playBtn);
// Botón alternador de Silencio / Mute
m_muteBtn = new QPushButton("🔊", this);
m_muteBtn->setStyleSheet("background-color: #4a4e69;");
connect(m_muteBtn, &QPushButton::clicked, this, &MainWindow::toggleMute);
controlsLayout->addWidget(m_muteBtn);
// Barra de progreso horizontal (Seekbar)
m_seekBar = new QSlider(Qt::Horizontal, this);
m_seekBar->setRange(0, 1000);
m_seekBar->setRange(0, 1000); // 1000 pasos de precisión
m_seekBar->setEnabled(false);
connect(m_seekBar, &QSlider::sliderMoved, this, &MainWindow::setVideoPosition);
controlsLayout->addWidget(m_seekBar, 1);
controlsLayout->addWidget(m_seekBar, 1); // El factor de estiramiento '1' hace que ocupe todo el espacio sobrante
// Etiqueta indicadora de tiempo transcurrido / tiempo total
m_timeLabel = new QLabel("00:00 / 00:00", this);
m_timeLabel->setMinimumWidth(90);
m_timeLabel->setAlignment(Qt::AlignCenter);
@ -94,14 +101,18 @@ void MainWindow::openVideoFile() {
}
void MainWindow::openVideoFileDirect(const QString &fileName) {
// 1. Limpiamos cualquier textura y detenemos hilos anteriores
m_glWidget->clearVideo();
m_videoDecoder->stopThread();
// 2. Abrimos el nuevo archivo
if (m_videoDecoder->openFile(fileName)) {
// Habilitamos la barra de búsqueda si la duración es válida
m_seekBar->setEnabled(m_videoDecoder->getDuration() > 0.0);
m_playBtn->setText("⏸");
m_videoDecoder->start(); // Arranca el hilo de decodificación
m_videoDecoder->play(); // Inicia la reproducción
m_videoDecoder->start(); // Arranca el hilo secundario de decodificación (invoca a run())
m_videoDecoder->play(); // Inicia la reproducción del temporizador maestro
}
}
@ -119,19 +130,27 @@ void MainWindow::toggleMute() {
static bool muted = false;
muted = !muted;
m_muteBtn->setText(muted ? "🔇" : "🔊");
// Nota: El decodificador no procesa pistas de audio en este ejemplo de interoperabilidad GPU-GPU,
// pero el botón de mute demuestra cómo interactuar con el control de reproducción.
}
void MainWindow::setVideoPosition(int position) {
if (m_videoDecoder->getDuration() > 0.0) {
// Activamos la bandera para que la señal del decodificador no mueva la seekbar mientras el usuario la arrastra
m_updatingSeek = true;
// Calculamos la posición en segundos en base al porcentaje (0 - 1000)
double targetSecs = (static_cast<double>(position) / 1000.0) * m_videoDecoder->getDuration();
m_videoDecoder->seek(targetSecs);
m_updatingSeek = false;
}
}
void MainWindow::onPositionChanged(qint64 positionMs) {
double durationSecs = m_videoDecoder->getDuration();
// Actualizamos la barra de progreso basándonos en la posición actual del PTS
if (durationSecs > 0.0 && !m_updatingSeek) {
m_updatingSeek = true;
double progress = static_cast<double>(positionMs) / (durationSecs * 1000.0);
@ -139,13 +158,15 @@ void MainWindow::onPositionChanged(qint64 positionMs) {
m_updatingSeek = false;
}
// Formatear tiempos
// Lambda para dar formato MM:SS a los milisegundos
auto format = [](qint64 ms) -> QString {
qint64 totalSecs = ms / 1000;
int mins = totalSecs / 60;
int secs = totalSecs % 60;
return QString("%1:%2").arg(mins, 2, 10, QChar('0')).arg(secs, 2, 10, QChar('0'));
};
// Mostramos la marca de tiempo actualizada
m_timeLabel->setText(format(positionMs) + " / " + format(static_cast<qint64>(durationSecs * 1000.0)));
}
@ -153,5 +174,3 @@ void MainWindow::onPlaybackFinished() {
m_playBtn->setText("▶");
m_seekBar->setValue(0);
}

View file

@ -9,37 +9,53 @@
#include "glwidget.h"
#include "videodecoder.h"
/**
* @brief Ventana principal de la aplicación.
* Crea los controles de la interfaz de usuario de reproducción y maneja las conexiones
* y sincronización con el decodificador de video y el renderizador OpenGL.
*/
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
// Método auxiliar para abrir directamente un archivo de video (ej. desde argumentos de terminal)
void openVideoFileDirect(const QString &fileName);
private slots:
// Muestra un selector de archivos nativo del sistema operativo para buscar videos
void openVideoFile();
// Conmutador Play/Pausa de la reproducción de video
void togglePlayPause();
// Alternador para enmudecer (silenciar) el video
void toggleMute();
// Método gatillado cuando el usuario arrastra manualmente la barra de progreso (Seek)
void setVideoPosition(int position);
// Actualiza la posición de la barra de progreso y el texto de tiempo mientras avanza el video
void onPositionChanged(qint64 positionMs);
// Restablece la UI al estado inicial cuando finaliza la reproducción
void onPlaybackFinished();
private:
// Construcción y personalización estilística de la UI
void setupUI();
GLWidget *m_glWidget;
VideoDecoder *m_videoDecoder;
bool m_updatingSeek;
// Elementos de la interfaz multimedia
QPushButton *m_playBtn;
QPushButton *m_muteBtn;
QSlider *m_seekBar;
QLabel *m_timeLabel;
GLWidget *m_glWidget; // El Widget de pintado OpenGL/CUDA
VideoDecoder *m_videoDecoder; // El hilo decodificador de video FFmpeg/CUDA
bool m_updatingSeek; // Bandera lógica para evitar bucles de actualización entre el reproductor y la barra de progreso
// Elementos de la interfaz de usuario multimedia
QPushButton *m_playBtn; // Botón conmutador Play/Pause
QPushButton *m_muteBtn; // Botón Mute
QSlider *m_seekBar; // Barra de progreso interactiva (slider horizontal)
QLabel *m_timeLabel; // Etiqueta de texto de tiempo (Ej: 00:30 / 02:45)
};
#endif // MAINWINDOW_H

View file

@ -1,23 +1,34 @@
#version 150
// Coordenadas de textura interpoladas provenientes del Vertex Shader
in vec2 v_texCoord;
// Color final del píxel de salida que se pintará en la pantalla
out vec4 fragColor;
// Sampler para el canal de Luminancia (Y). Contiene la información de brillo a escala completa (Grises).
uniform sampler2D u_texY;
// Sampler para el canal de Crominancia (UV) entrelazado. Contiene los datos de color a la mitad de resolución.
uniform sampler2D u_texUV;
void main() {
// 1. Muestreamos el brillo (escala de grises) del plano Y. El valor de luminancia se almacena en el canal Rojo (.r).
float y = texture(u_texY, v_texCoord).r;
// 2. Muestreamos la crominancia (color) del plano UV. Al estar entrelazado, el canal Rojo (.r) contiene el componente U
// y el canal Verde (.g) contiene el componente V.
vec2 uvVal = texture(u_texUV, v_texCoord).rg;
// El formato NV12 almacena U y V entrelazados. Restamos 0.5 para centrar la crominancia.
// 3. Restamos 0.5 a las componentes de color para centrar la crominancia en el rango [-0.5, 0.5] (su rango original en YUV).
float u = uvVal.r - 0.5;
float v = uvVal.g - 0.5;
// Matriz estándar de conversión YUV (NV12) a RGB
// 4. Aplicamos la matriz de conversión matemática estándar de YUV (NV12) a RGB para reconstruir los canales de color.
float r = y + 1.402 * v;
float g = y - 0.344136 * u - 0.714136 * v;
float b = y + 1.772 * u;
// 5. Asignamos el color RGB final al fragmento de salida con opacidad completa (alfa = 1.0)
fragColor = vec4(r, g, b, 1.0);
}

View file

@ -1,10 +1,18 @@
#version 150
// Posiciones en pantalla de los vértices (en coordenadas de dispositivo normalizadas 2D: [-1, 1])
in vec2 a_position;
// Coordenadas UV del vértice correspondientes al mapeo de la textura del video
in vec2 a_texCoord;
// Canal de salida para transmitir las coordenadas UV interpoladas hacia el Fragment Shader
out vec2 v_texCoord;
void main() {
// Transmitimos las coordenadas de textura sin alterar
v_texCoord = a_texCoord;
// Establecemos la posición espacial final del vértice en coordenadas homogéneas de clip 4D
gl_Position = vec4(a_position, 0.0, 1.0);
}

View file

@ -1,17 +1,20 @@
#include "videodecoder.h"
#include <QDebug>
enum AVPixelFormat VideoDecoder::m_hwPixFmt = AV_PIX_FMT_NONE;
// 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) {
Q_UNUSED(ctx);
(void)ctx;
const enum AVPixelFormat *p;
for (p = pix_fmts; *p != -1; p++) {
if (*p == m_hwPixFmt) {
return *p;
}
}
qWarning() << "No se pudo acordar el formato de hardware pix format CUDA.";
qWarning() << "No se encontró soporte para formato de hardware (CUDA).";
return AV_PIX_FMT_NONE;
}
@ -30,11 +33,11 @@ VideoDecoder::VideoDecoder(QObject *parent)
, m_codecContext(nullptr)
, m_hwDeviceCtx(nullptr)
{
// Reservamos espacio en memoria para el fotograma compartido de video
m_currentFrame = av_frame_alloc();
}
VideoDecoder::~VideoDecoder() {
stopThread();
closeFile();
if (m_currentFrame) {
av_frame_free(&m_currentFrame);
@ -46,90 +49,86 @@ bool VideoDecoder::openFile(const QString &fileName) {
closeFile();
m_fileName = fileName;
// Abrir archivo de entrada
if (avformat_open_input(&m_formatContext, fileName.toLocal8Bit().constData(), nullptr, nullptr) < 0) {
qWarning() << "No se pudo abrir el archivo de entrada:" << 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;
}
// Buscar información de streams
// 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 streams.";
qWarning() << "No se pudo encontrar información de flujos.";
closeFile();
return false;
}
// Buscar stream de video
m_videoStreamIndex = -1;
for (unsigned int i = 0; i < m_formatContext->nb_streams; ++i) {
if (m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_videoStreamIndex = i;
break;
}
}
if (m_videoStreamIndex == -1) {
qWarning() << "No se encontró un stream de video en el archivo.";
// 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;
}
// Buscar y abrir decodificador
AVCodecParameters *codecParams = m_formatContext->streams[m_videoStreamIndex]->codecpar;
const AVCodec *codec = avcodec_find_decoder(codecParams->codec_id);
if (!codec) {
qWarning() << "No se encontró un decodificador compatible.";
return false;
}
m_codecContext = avcodec_alloc_context3(codec);
if (!m_codecContext) {
qWarning() << "No se pudo asignar memoria para el contexto del decodificador.";
return false;
}
if (avcodec_parameters_to_context(m_codecContext, codecParams) < 0) {
qWarning() << "No se pudieron copiar los parámetros del códec al contexto.";
return false;
}
m_codecContext->thread_count = 0; // auto-detectar hilos
// Forzar decodificación por hardware nativa mediante CUDA
AVHWDeviceType devType = AV_HWDEVICE_TYPE_CUDA;
m_hwPixFmt = AV_PIX_FMT_CUDA;
if (av_hwdevice_ctx_create(&m_hwDeviceCtx, devType, nullptr, nullptr, 0) < 0) {
qWarning() << "Error: No se pudo crear el dispositivo de hardware CUDA.";
m_hwDeviceCtx = nullptr;
m_hwPixFmt = AV_PIX_FMT_NONE;
return false;
} else {
qDebug() << "Dispositivo de decodificación CUDA creado con éxito.";
m_codecContext->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
m_codecContext->get_format = get_hw_format;
}
if (avcodec_open2(m_codecContext, codec, nullptr) < 0) {
qWarning() << "No se pudo abrir el códec de video.";
return false;
}
m_videoWidth = m_codecContext->width;
m_videoHeight = m_codecContext->height;
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;
}
m_isPaused = true;
// 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_playbackTimer.invalidate();
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;
@ -142,6 +141,8 @@ void VideoDecoder::closeFile() {
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) {
@ -158,8 +159,8 @@ void VideoDecoder::play() {
QMutexLocker locker(&m_mutex);
if (m_isPaused) {
m_isPaused = false;
m_playbackTimer.restart();
m_waitCond.wakeAll();
m_playbackTimer.restart(); // Inicia el reloj maestro
m_waitCond.wakeAll(); // Despierta al hilo de decodificación si estaba bloqueado
}
}
@ -167,6 +168,7 @@ 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();
}
@ -176,7 +178,7 @@ void VideoDecoder::seek(double seconds) {
QMutexLocker locker(&m_mutex);
m_seekTimeSecs = seconds;
m_seekRequested = true;
m_waitCond.wakeAll();
m_waitCond.wakeAll(); // Despierta al hilo para procesar el salto inmediatamente
}
void VideoDecoder::stopThread() {
@ -185,7 +187,7 @@ void VideoDecoder::stopThread() {
m_running = false;
m_waitCond.wakeAll();
}
wait();
wait(); // Espera síncrona a que termine el hilo secundario
}
qint64 VideoDecoder::getPlaybackTimeMs() const {
@ -196,11 +198,15 @@ qint64 VideoDecoder::getPlaybackTimeMs() const {
}
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;
}
@ -211,43 +217,62 @@ void VideoDecoder::run() {
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
if (!packet || !frame) {
qWarning() << "No se pudo asignar memoria para packet/frame.";
m_running = false;
return;
}
m_playbackTimer.restart();
m_pausedElapsedMs = 0;
while (true) {
// --- CONTROL DE ESTADO (PAUSA / PARADA) ---
{
QMutexLocker locker(&m_mutex);
if (!m_running) break;
while (m_isPaused && m_running && !m_seekRequested) {
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;
if (m_seekRequested) {
int64_t targetTimestamp = static_cast<int64_t>(m_seekTimeSecs / av_q2d(m_formatContext->streams[m_videoStreamIndex]->time_base));
av_seek_frame(m_formatContext, m_videoStreamIndex, targetTimestamp, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(m_codecContext);
m_pausedElapsedMs = static_cast<qint64>(m_seekTimeSecs * 1000.0);
if (!m_isPaused) {
m_playbackTimer.restart();
}
}
// --- 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;
}
}
int readResult = av_read_frame(m_formatContext, packet);
if (readResult < 0) {
if (readResult == AVERROR_EOF) {
emit playbackFinished();
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;
@ -256,25 +281,33 @@ void VideoDecoder::run() {
}
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) {
if (delay < 2000) { // Margen de seguridad razonable
QThread::msleep(delay);
}
}
// Guardar el fotograma CUDA en VRAM y avisar que está listo
// 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);
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);
}

View file

@ -7,70 +7,101 @@
#include <QElapsedTimer>
extern "C" {
// Cabeceras multimedia de FFmpeg
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libavutil/hwcontext.h>
}
/**
* @brief Hilo de decodificación encargado de leer y decodificar video por hardware (CUDA).
* Realiza la sincronización temporal basada en el reloj maestro del reproductor (PTS).
*/
class VideoDecoder : public QThread {
Q_OBJECT
public:
explicit VideoDecoder(QObject *parent = nullptr);
~VideoDecoder() override;
// Abre el archivo de video e inicializa el contexto de decodificación de hardware
bool openFile(const QString &fileName);
// Cierra el archivo y libera todos los contextos asignados
void closeFile();
// Reanuda la reproducción de video
void play();
// Pausa la reproducción de video
void pause();
// Salta a una posición de tiempo específica en segundos
void seek(double seconds);
// Detiene de forma segura el bucle del hilo y espera a que finalice
void stopThread();
// Métodos de acceso a propiedades del video (Thread-Safe mediante lectura simple)
double getDuration() const { return m_duration; }
int getVideoWidth() const { return m_videoWidth; }
int getVideoHeight() const { return m_videoHeight; }
bool isPaused() const { return m_isPaused; }
// Retorna el tiempo actual de reproducción en milisegundos de acuerdo al reloj maestro
qint64 getPlaybackTimeMs() const;
// Obtener una referencia al fotograma CUDA actual de forma segura
// Método Thread-Safe para que OpenGL tome una referencia rápida al fotograma decodificado actual
bool getCurrentFrameRef(AVFrame *destFrame);
signals:
// Señal emitida cuando hay un nuevo fotograma decodificado listo en la GPU
void frameReady();
// Señal para actualizar la posición en tiempo real de la barra de progreso
void positionChanged(qint64 positionMs);
// Señal emitida al llegar al final del video
void playbackFinished();
protected:
// Bucle de decodificación principal que se ejecuta en el hilo secundario
void run() override;
private:
QString m_fileName;
bool m_running;
bool m_isPaused;
bool m_seekRequested;
double m_seekTimeSecs;
QMutex m_mutex;
QWaitCondition m_waitCond;
QString m_fileName; // Ruta del archivo de video abierto
bool m_running; // Controla si el bucle principal del hilo debe seguir ejecutándose
bool m_isPaused; // Bandera de pausa
bool m_seekRequested; // Bandera de solicitud de salto de tiempo (Seek)
double m_seekTimeSecs; // Tiempo al que saltar en segundos
double m_duration;
int m_videoWidth;
int m_videoHeight;
int m_videoStreamIndex;
QMutex m_mutex; // Mutex para proteger variables de control (pausa, running, seek)
QWaitCondition m_waitCond; // Condición de espera para no consumir CPU cuando el video está en pausa
// Relojes de sincronización
QElapsedTimer m_playbackTimer;
qint64 m_pausedElapsedMs;
double m_duration; // Duración total del video en segundos
int m_videoWidth; // Ancho del video en píxeles
int m_videoHeight; // Alto del video en píxeles
int m_videoStreamIndex; // Índice del flujo de video dentro del contenedor (mp4, mkv, etc.)
AVFormatContext *m_formatContext;
AVCodecContext *m_codecContext;
AVBufferRef *m_hwDeviceCtx;
// Relojes de sincronización para reproducir al framerate correcto
QElapsedTimer m_playbackTimer; // Reloj de alta resolución que mide el tiempo real transcurrido
qint64 m_pausedElapsedMs; // Acumulador de tiempo transcurrido antes de la última pausa
// Fotograma CUDA actual y su mutex de protección
// Estructuras principales de FFmpeg
AVFormatContext *m_formatContext; // Contexto de apertura y lectura del contenedor del archivo
AVCodecContext *m_codecContext; // Contexto del decodificador (NVDEC / CUDA)
AVBufferRef *m_hwDeviceCtx; // Contexto de hardware que asocia FFmpeg con el driver de CUDA
// Fotograma de video en VRAM que contiene el frame actual listo para renderizar
AVFrame *m_currentFrame;
// Mutex de exclusión mutua para proteger el acceso a m_currentFrame entre hilos
QMutex m_frameMutex;
// Formato de pixel de hardware que FFmpeg debe solicitar (CUDA)
static enum AVPixelFormat m_hwPixFmt;
// Callback estático requerido por FFmpeg para negociar el formato de decodificación
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts);
};