diff --git a/glwidget.cpp b/glwidget.cpp index 8cea4b9..28f4e4d 100644 --- a/glwidget.cpp +++ b/glwidget.cpp @@ -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(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; diff --git a/glwidget.h b/glwidget.h index b521120..14e3ba7 100644 --- a/glwidget.h +++ b/glwidget.h @@ -6,59 +6,75 @@ #include #include +// Cabeceras del Runtime de CUDA e Interoperabilidad CUDA-OpenGL #include #include extern "C" { +// Estructuras de datos multimedia de FFmpeg (necesarias para el manejo del AVFrame) #include } 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 diff --git a/main.cpp b/main.cpp index 25daa26..f8d51fc 100644 --- a/main.cpp +++ b/main.cpp @@ -2,13 +2,18 @@ #include 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(); } diff --git a/mainwindow.cpp b/mainwindow.cpp index 719df25..1666421 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -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(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(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(durationSecs * 1000.0))); } @@ -153,5 +174,3 @@ void MainWindow::onPlaybackFinished() { m_playBtn->setText("▶"); m_seekBar->setValue(0); } - - diff --git a/mainwindow.h b/mainwindow.h index 98f7512..0bd56e9 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -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(); - void togglePlayPause(); - void toggleMute(); - void setVideoPosition(int position); - void onPositionChanged(qint64 positionMs); - void onPlaybackFinished(); - + // 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 diff --git a/shader.frag b/shader.frag index cc03f65..1183980 100644 --- a/shader.frag +++ b/shader.frag @@ -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); } diff --git a/shader.vert b/shader.vert index 4cf8610..ab73800 100644 --- a/shader.vert +++ b/shader.vert @@ -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); } diff --git a/videodecoder.cpp b/videodecoder.cpp index 0fd971d..f276d5b 100644 --- a/videodecoder.cpp +++ b/videodecoder.cpp @@ -1,17 +1,20 @@ #include "videodecoder.h" #include -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(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_running) break; + } + // --- CONTROL DE SALTO DE TIEMPO (SEEK) --- + bool seekNeeded = false; + double targetSeekSecs = 0.0; + { + QMutexLocker locker(&m_mutex); if (m_seekRequested) { - int64_t targetTimestamp = static_cast(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(m_seekTimeSecs * 1000.0); - if (!m_isPaused) { - m_playbackTimer.restart(); - } + 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(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; @@ -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(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); } diff --git a/videodecoder.h b/videodecoder.h index 77956cb..95c1c93 100644 --- a/videodecoder.h +++ b/videodecoder.h @@ -7,70 +7,101 @@ #include extern "C" { +// Cabeceras multimedia de FFmpeg #include #include #include #include } +/** + * @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 + + 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 - double m_duration; - int m_videoWidth; - int m_videoHeight; - int m_videoStreamIndex; + 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.) - // Relojes de sincronización - QElapsedTimer m_playbackTimer; - qint64 m_pausedElapsedMs; + // 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 - AVFormatContext *m_formatContext; - AVCodecContext *m_codecContext; - AVBufferRef *m_hwDeviceCtx; + // 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 CUDA actual y su mutex de protección + // 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); };