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_cudaResourceUV(nullptr)
, m_allocatedWidth(0) , m_allocatedWidth(0)
, m_allocatedHeight(0) , m_allocatedHeight(0)
{ {
// Almacenamos el contenedor local del frame decodificado
m_localFrame = av_frame_alloc(); m_localFrame = av_frame_alloc();
} }
GLWidget::~GLWidget() { GLWidget::~GLWidget() {
// Nos aseguramos de tener el contexto de OpenGL activo antes de borrar recursos de la GPU
makeCurrent(); makeCurrent();
cleanupCudaGL(); cleanupCudaGL();
if (m_texY) { if (m_texY) {
glDeleteTextures(1, &m_texY); glDeleteTextures(1, &m_texY);
} }
@ -30,6 +32,8 @@ GLWidget::~GLWidget() {
if (m_vbo) { if (m_vbo) {
glDeleteBuffers(1, &m_vbo); glDeleteBuffers(1, &m_vbo);
} }
// Liberamos la estructura local de FFmpeg
if (m_localFrame) { if (m_localFrame) {
av_frame_free(&m_localFrame); av_frame_free(&m_localFrame);
m_localFrame = nullptr; m_localFrame = nullptr;
@ -38,30 +42,34 @@ GLWidget::~GLWidget() {
void GLWidget::setVideoDecoder(VideoDecoder *decoder) { void GLWidget::setVideoDecoder(VideoDecoder *decoder) {
m_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); connect(m_videoDecoder, &VideoDecoder::frameReady, this, &GLWidget::onFrameReady);
} }
void GLWidget::onFrameReady() { void GLWidget::onFrameReady() {
// Solicita a Qt redibujar el widget (desencadena una llamada interna a paintGL())
update(); update();
} }
void GLWidget::clearVideo() { void GLWidget::clearVideo() {
// Libera los recursos de interoperabilidad y actualiza el render
cleanupCudaGL(); cleanupCudaGL();
update(); update();
} }
void GLWidget::initializeGL() { void GLWidget::initializeGL() {
// Inicializa la tabla de funciones de OpenGL para poder llamarlas directamente
initializeOpenGLFunctions(); initializeOpenGLFunctions();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); 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::Vertex, "shader.vert");
m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment, "shader.frag"); m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment, "shader.frag");
m_shaderProgram.link(); 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[] = { float vertices[] = {
// Posiciones // Coordenadas UV // Posiciones // Coordenadas UV
-1.0f, -1.0f, 0.0f, 1.0f, -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); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Generar texturas de OpenGL // Generar texturas de OpenGL
// Textura Y (Luminancia)
glGenTextures(1, &m_texY); glGenTextures(1, &m_texY);
glBindTexture(GL_TEXTURE_2D, m_texY); glBindTexture(GL_TEXTURE_2D, m_texY);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 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_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Textura UV (Crominancia entrelazada)
glGenTextures(1, &m_texUV); glGenTextures(1, &m_texUV);
glBindTexture(GL_TEXTURE_2D, m_texUV); glBindTexture(GL_TEXTURE_2D, m_texUV);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@ -94,6 +104,7 @@ void GLWidget::initializeGL() {
} }
void GLWidget::resizeGL(int w, int h) { 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); glViewport(0, 0, w, h);
} }
@ -102,27 +113,29 @@ void GLWidget::paintGL() {
if (!m_videoDecoder) return; 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)) { 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 w = m_localFrame->width;
int h = m_localFrame->height; 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) { if (w != m_allocatedWidth || h != m_allocatedHeight) {
cleanupCudaGL(); 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); glBindTexture(GL_TEXTURE_2D, m_texY);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); 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); glBindTexture(GL_TEXTURE_2D, m_texUV);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); 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 errY = cudaGraphicsGLRegisterImage(&m_cudaResourceY, m_texY, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
cudaError_t errUV = cudaGraphicsGLRegisterImage(&m_cudaResourceUV, m_texUV, 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; 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) { if (m_cudaResourceY && m_cudaResourceUV) {
cudaError_t mapY = cudaGraphicsMapResources(1, &m_cudaResourceY, 0); cudaError_t mapY = cudaGraphicsMapResources(1, &m_cudaResourceY, 0);
cudaError_t mapUV = cudaGraphicsMapResources(1, &m_cudaResourceUV, 0); cudaError_t mapUV = cudaGraphicsMapResources(1, &m_cudaResourceUV, 0);
@ -146,10 +159,12 @@ void GLWidget::paintGL() {
cudaArray_t arrayY = nullptr; cudaArray_t arrayY = nullptr;
cudaArray_t arrayUV = 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(&arrayY, m_cudaResourceY, 0, 0);
cudaGraphicsSubResourceGetMappedArray(&arrayUV, m_cudaResourceUV, 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( cudaError_t cpyY = cudaMemcpy2DToArray(
arrayY, 0, 0, arrayY, 0, 0,
(void*)m_localFrame->data[0], m_localFrame->linesize[0], (void*)m_localFrame->data[0], m_localFrame->linesize[0],
@ -157,7 +172,8 @@ void GLWidget::paintGL() {
cudaMemcpyDeviceToDevice 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( cudaError_t cpyUV = cudaMemcpy2DToArray(
arrayUV, 0, 0, arrayUV, 0, 0,
(void*)m_localFrame->data[1], m_localFrame->linesize[1], (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); 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_cudaResourceY, 0);
cudaGraphicsUnmapResources(1, &m_cudaResourceUV, 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(); m_shaderProgram.bind();
// Enlazar textura Y a la unidad de textura 0
glActiveTexture(GL_TEXTURE0); glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texY); glBindTexture(GL_TEXTURE_2D, m_texY);
m_shaderProgram.setUniformValue("u_texY", 0); m_shaderProgram.setUniformValue("u_texY", 0);
// Enlazar textura UV a la unidad de textura 1
glActiveTexture(GL_TEXTURE1); glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_texUV); glBindTexture(GL_TEXTURE_2D, m_texUV);
m_shaderProgram.setUniformValue("u_texUV", 1); 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); glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
int posLocation = m_shaderProgram.attributeLocation("a_position"); int posLocation = m_shaderProgram.attributeLocation("a_position");
m_shaderProgram.enableAttributeArray(posLocation); m_shaderProgram.enableAttributeArray(posLocation);
glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); glVertexAttribPointer(posLocation, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr);
@ -199,18 +217,21 @@ void GLWidget::paintGL() {
m_shaderProgram.enableAttributeArray(texLocation); m_shaderProgram.enableAttributeArray(texLocation);
glVertexAttribPointer(texLocation, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(2 * sizeof(float))); 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); glDrawArrays(GL_TRIANGLES, 0, 6);
// Desactivar y liberar
m_shaderProgram.disableAttributeArray(posLocation); m_shaderProgram.disableAttributeArray(posLocation);
m_shaderProgram.disableAttributeArray(texLocation); m_shaderProgram.disableAttributeArray(texLocation);
m_shaderProgram.release(); 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); av_frame_unref(m_localFrame);
} }
void GLWidget::cleanupCudaGL() { void GLWidget::cleanupCudaGL() {
makeCurrent(); makeCurrent();
// Desregistra los recursos de CUDA asociados a las texturas de OpenGL
if (m_cudaResourceY) { if (m_cudaResourceY) {
cudaGraphicsUnregisterResource(m_cudaResourceY); cudaGraphicsUnregisterResource(m_cudaResourceY);
m_cudaResourceY = nullptr; m_cudaResourceY = nullptr;

View file

@ -6,59 +6,75 @@
#include <QOpenGLShaderProgram> #include <QOpenGLShaderProgram>
#include <QMutex> #include <QMutex>
// Cabeceras del Runtime de CUDA e Interoperabilidad CUDA-OpenGL
#include <cuda_runtime.h> #include <cuda_runtime.h>
#include <cuda_gl_interop.h> #include <cuda_gl_interop.h>
extern "C" { extern "C" {
// Estructuras de datos multimedia de FFmpeg (necesarias para el manejo del AVFrame)
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
} }
class VideoDecoder; 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 { class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions {
Q_OBJECT Q_OBJECT
public: public:
explicit GLWidget(QWidget *parent = nullptr); explicit GLWidget(QWidget *parent = nullptr);
~GLWidget() override; ~GLWidget() override;
// Vincula el decodificador de video y conecta las señales de reproducción
void setVideoDecoder(VideoDecoder *decoder); void setVideoDecoder(VideoDecoder *decoder);
// Limpia y desregistra los recursos de CUDA y OpenGL de la tarjeta gráfica
void clearVideo(); void clearVideo();
public slots: public slots:
// Slot encargado de forzar la repintada del widget al recibir un nuevo fotograma
void onFrameReady(); void onFrameReady();
protected: protected:
// Configuración inicial de OpenGL (compilación de shaders, buffers y texturas)
void initializeGL() override; void initializeGL() override;
// Ajuste de proyección y Viewport al cambiar de tamaño la ventana
void resizeGL(int w, int h) override; 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; void paintGL() override;
private: private:
// Desregistra las texturas de OpenGL del contexto de CUDA para liberar recursos de la GPU
void cleanupCudaGL(); void cleanupCudaGL();
// Puntero al decodificador de video (para leer los fotogramas compartidos)
VideoDecoder *m_videoDecoder; VideoDecoder *m_videoDecoder;
// Programa de Sombreado de OpenGL (Vertex + Fragment Shader compilados)
QOpenGLShaderProgram m_shaderProgram; QOpenGLShaderProgram m_shaderProgram;
// Texturas de OpenGL para NV12 // Identificadores de texturas OpenGL: m_texY (Luminancia) y m_texUV (Crominancia)
GLuint m_texY; GLuint m_texY;
GLuint m_texUV; GLuint m_texUV;
// Vertex Buffer Object (VBO) para el quad plano (triángulos) que llena la pantalla
GLuint m_vbo; 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_cudaResourceY;
cudaGraphicsResource_t m_cudaResourceUV; cudaGraphicsResource_t m_cudaResourceUV;
// Fotograma local reutilizable // Contenedor local para clonar la referencia del fotograma decodificado
AVFrame *m_localFrame; 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_allocatedWidth;
int m_allocatedHeight; int m_allocatedHeight;
}; };
#endif // GLWIDGET_H #endif // GLWIDGET_H

View file

@ -2,13 +2,18 @@
#include <QApplication> #include <QApplication>
int main(int argc, char *argv[]) { 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); QApplication a(argc, argv);
// 2. Instancia y despliega la ventana de control principal del reproductor
MainWindow w; MainWindow w;
w.show(); 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) { if (argc > 1) {
w.openVideoFileDirect(QString::fromLocal8Bit(argv[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(); return a.exec();
} }

View file

@ -21,17 +21,18 @@ MainWindow::MainWindow(QWidget *parent)
setupUI(); 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::positionChanged, this, &MainWindow::onPositionChanged);
connect(m_videoDecoder, &VideoDecoder::playbackFinished, this, &MainWindow::onPlaybackFinished); connect(m_videoDecoder, &VideoDecoder::playbackFinished, this, &MainWindow::onPlaybackFinished);
} }
MainWindow::~MainWindow() { MainWindow::~MainWindow() {
// Detiene de forma segura el hilo decodificador antes de destruir la ventana principal
m_videoDecoder->stopThread(); m_videoDecoder->stopThread();
} }
void MainWindow::setupUI() { 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( this->setStyleSheet(
"QMainWindow { background-color: #08080f; }" "QMainWindow { background-color: #08080f; }"
"QWidget { color: #f0f0f5; font-family: 'Outfit', 'Segoe UI', sans-serif; font-size: 13px; }" "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); QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget); setCentralWidget(centralWidget);
// Layout principal en columna vertical (Pintado OpenGL arriba, controles de reproducción abajo)
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget); QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(10); mainLayout->setSpacing(10);
mainLayout->addWidget(m_glWidget, 1); mainLayout->addWidget(m_glWidget, 1);
// Controles Multimedia // --- CONTROLES MULTIMEDIA ---
QHBoxLayout *controlsLayout = new QHBoxLayout(); QHBoxLayout *controlsLayout = new QHBoxLayout();
// Botón para buscar y abrir archivos de video
QPushButton *openBtn = new QPushButton("Abrir Video", this); QPushButton *openBtn = new QPushButton("Abrir Video", this);
openBtn->setStyleSheet("background-color: #00bfff;"); openBtn->setStyleSheet("background-color: #00bfff;");
connect(openBtn, &QPushButton::clicked, this, &MainWindow::openVideoFile); connect(openBtn, &QPushButton::clicked, this, &MainWindow::openVideoFile);
controlsLayout->addWidget(openBtn); controlsLayout->addWidget(openBtn);
// Botón de Play / Pausa
m_playBtn = new QPushButton("▶", this); m_playBtn = new QPushButton("▶", this);
connect(m_playBtn, &QPushButton::clicked, this, &MainWindow::togglePlayPause); connect(m_playBtn, &QPushButton::clicked, this, &MainWindow::togglePlayPause);
controlsLayout->addWidget(m_playBtn); controlsLayout->addWidget(m_playBtn);
// Botón alternador de Silencio / Mute
m_muteBtn = new QPushButton("🔊", this); m_muteBtn = new QPushButton("🔊", this);
m_muteBtn->setStyleSheet("background-color: #4a4e69;"); m_muteBtn->setStyleSheet("background-color: #4a4e69;");
connect(m_muteBtn, &QPushButton::clicked, this, &MainWindow::toggleMute); connect(m_muteBtn, &QPushButton::clicked, this, &MainWindow::toggleMute);
controlsLayout->addWidget(m_muteBtn); controlsLayout->addWidget(m_muteBtn);
// Barra de progreso horizontal (Seekbar)
m_seekBar = new QSlider(Qt::Horizontal, this); 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); m_seekBar->setEnabled(false);
connect(m_seekBar, &QSlider::sliderMoved, this, &MainWindow::setVideoPosition); 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 = new QLabel("00:00 / 00:00", this);
m_timeLabel->setMinimumWidth(90); m_timeLabel->setMinimumWidth(90);
m_timeLabel->setAlignment(Qt::AlignCenter); m_timeLabel->setAlignment(Qt::AlignCenter);
@ -94,14 +101,18 @@ void MainWindow::openVideoFile() {
} }
void MainWindow::openVideoFileDirect(const QString &fileName) { void MainWindow::openVideoFileDirect(const QString &fileName) {
// 1. Limpiamos cualquier textura y detenemos hilos anteriores
m_glWidget->clearVideo(); m_glWidget->clearVideo();
m_videoDecoder->stopThread(); m_videoDecoder->stopThread();
// 2. Abrimos el nuevo archivo
if (m_videoDecoder->openFile(fileName)) { 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_seekBar->setEnabled(m_videoDecoder->getDuration() > 0.0);
m_playBtn->setText("⏸"); 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; static bool muted = false;
muted = !muted; muted = !muted;
m_muteBtn->setText(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) { void MainWindow::setVideoPosition(int position) {
if (m_videoDecoder->getDuration() > 0.0) { 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; 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(); double targetSecs = (static_cast<double>(position) / 1000.0) * m_videoDecoder->getDuration();
m_videoDecoder->seek(targetSecs); m_videoDecoder->seek(targetSecs);
m_updatingSeek = false; m_updatingSeek = false;
} }
} }
void MainWindow::onPositionChanged(qint64 positionMs) { void MainWindow::onPositionChanged(qint64 positionMs) {
double durationSecs = m_videoDecoder->getDuration(); 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) { if (durationSecs > 0.0 && !m_updatingSeek) {
m_updatingSeek = true; m_updatingSeek = true;
double progress = static_cast<double>(positionMs) / (durationSecs * 1000.0); double progress = static_cast<double>(positionMs) / (durationSecs * 1000.0);
@ -139,13 +158,15 @@ void MainWindow::onPositionChanged(qint64 positionMs) {
m_updatingSeek = false; m_updatingSeek = false;
} }
// Formatear tiempos // Lambda para dar formato MM:SS a los milisegundos
auto format = [](qint64 ms) -> QString { auto format = [](qint64 ms) -> QString {
qint64 totalSecs = ms / 1000; qint64 totalSecs = ms / 1000;
int mins = totalSecs / 60; int mins = totalSecs / 60;
int secs = totalSecs % 60; int secs = totalSecs % 60;
return QString("%1:%2").arg(mins, 2, 10, QChar('0')).arg(secs, 2, 10, QChar('0')); 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))); m_timeLabel->setText(format(positionMs) + " / " + format(static_cast<qint64>(durationSecs * 1000.0)));
} }
@ -153,5 +174,3 @@ void MainWindow::onPlaybackFinished() {
m_playBtn->setText("▶"); m_playBtn->setText("▶");
m_seekBar->setValue(0); m_seekBar->setValue(0);
} }

View file

@ -9,37 +9,53 @@
#include "glwidget.h" #include "glwidget.h"
#include "videodecoder.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 { class MainWindow : public QMainWindow {
Q_OBJECT Q_OBJECT
public: public:
explicit MainWindow(QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override; ~MainWindow() override;
// Método auxiliar para abrir directamente un archivo de video (ej. desde argumentos de terminal)
void openVideoFileDirect(const QString &fileName); void openVideoFileDirect(const QString &fileName);
private slots: private slots:
// Muestra un selector de archivos nativo del sistema operativo para buscar videos
void openVideoFile(); void openVideoFile();
// Conmutador Play/Pausa de la reproducción de video
void togglePlayPause(); void togglePlayPause();
// Alternador para enmudecer (silenciar) el video
void toggleMute(); void toggleMute();
// Método gatillado cuando el usuario arrastra manualmente la barra de progreso (Seek)
void setVideoPosition(int position); 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); void onPositionChanged(qint64 positionMs);
// Restablece la UI al estado inicial cuando finaliza la reproducción
void onPlaybackFinished(); void onPlaybackFinished();
private: private:
// Construcción y personalización estilística de la UI
void setupUI(); void setupUI();
GLWidget *m_glWidget; GLWidget *m_glWidget; // El Widget de pintado OpenGL/CUDA
VideoDecoder *m_videoDecoder; VideoDecoder *m_videoDecoder; // El hilo decodificador de video FFmpeg/CUDA
bool m_updatingSeek;
// Elementos de la interfaz multimedia
QPushButton *m_playBtn;
QPushButton *m_muteBtn;
QSlider *m_seekBar;
QLabel *m_timeLabel;
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 #endif // MAINWINDOW_H

View file

@ -1,23 +1,34 @@
#version 150 #version 150
// Coordenadas de textura interpoladas provenientes del Vertex Shader
in vec2 v_texCoord; in vec2 v_texCoord;
// Color final del píxel de salida que se pintará en la pantalla
out vec4 fragColor; out vec4 fragColor;
// Sampler para el canal de Luminancia (Y). Contiene la información de brillo a escala completa (Grises).
uniform sampler2D u_texY; 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; uniform sampler2D u_texUV;
void main() { 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; 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; 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 u = uvVal.r - 0.5;
float v = uvVal.g - 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 r = y + 1.402 * v;
float g = y - 0.344136 * u - 0.714136 * v; float g = y - 0.344136 * u - 0.714136 * v;
float b = y + 1.772 * u; 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); fragColor = vec4(r, g, b, 1.0);
} }

View file

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

View file

@ -1,17 +1,20 @@
#include "videodecoder.h" #include "videodecoder.h"
#include <QDebug> #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) { enum AVPixelFormat VideoDecoder::get_hw_format(AVCodecContext *ctx, const enum AVPixelFormat *pix_fmts) {
Q_UNUSED(ctx); (void)ctx;
const enum AVPixelFormat *p; const enum AVPixelFormat *p;
for (p = pix_fmts; *p != -1; p++) { for (p = pix_fmts; *p != -1; p++) {
if (*p == m_hwPixFmt) { if (*p == m_hwPixFmt) {
return *p; 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; return AV_PIX_FMT_NONE;
} }
@ -30,11 +33,11 @@ VideoDecoder::VideoDecoder(QObject *parent)
, m_codecContext(nullptr) , m_codecContext(nullptr)
, m_hwDeviceCtx(nullptr) , m_hwDeviceCtx(nullptr)
{ {
// Reservamos espacio en memoria para el fotograma compartido de video
m_currentFrame = av_frame_alloc(); m_currentFrame = av_frame_alloc();
} }
VideoDecoder::~VideoDecoder() { VideoDecoder::~VideoDecoder() {
stopThread();
closeFile(); closeFile();
if (m_currentFrame) { if (m_currentFrame) {
av_frame_free(&m_currentFrame); av_frame_free(&m_currentFrame);
@ -46,90 +49,86 @@ bool VideoDecoder::openFile(const QString &fileName) {
closeFile(); closeFile();
m_fileName = fileName; m_fileName = fileName;
// Abrir archivo de entrada // 1. Abrir el contenedor multimedia (demuxer)
if (avformat_open_input(&m_formatContext, fileName.toLocal8Bit().constData(), nullptr, nullptr) < 0) { if (avformat_open_input(&m_formatContext, m_fileName.toLocal8Bit().constData(), nullptr, nullptr) < 0) {
qWarning() << "No se pudo abrir el archivo de entrada:" << fileName; qWarning() << "No se pudo abrir el archivo de video:" << fileName;
return false; 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) { 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; return false;
} }
// Buscar stream de video // 3. Buscar el flujo de video principal
m_videoStreamIndex = -1; const AVCodec *codec = nullptr;
for (unsigned int i = 0; i < m_formatContext->nb_streams; ++i) { m_videoStreamIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (m_formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { if (m_videoStreamIndex < 0) {
m_videoStreamIndex = i; qWarning() << "No se encontró flujo de video en el archivo.";
break; closeFile();
}
}
if (m_videoStreamIndex == -1) {
qWarning() << "No se encontró un stream de video en el archivo.";
return false; return false;
} }
// Buscar y abrir decodificador AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex];
AVCodecParameters *codecParams = m_formatContext->streams[m_videoStreamIndex]->codecpar; m_videoWidth = videoStream->codecpar->width;
const AVCodec *codec = avcodec_find_decoder(codecParams->codec_id); m_videoHeight = videoStream->codecpar->height;
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;
// Convertimos la duración del video de la base de tiempo interna de FFmpeg a segundos reales
if (m_formatContext->duration != AV_NOPTS_VALUE) { if (m_formatContext->duration != AV_NOPTS_VALUE) {
m_duration = static_cast<double>(m_formatContext->duration) / AV_TIME_BASE; m_duration = static_cast<double>(m_formatContext->duration) / AV_TIME_BASE;
} else { } else {
m_duration = 0.0; 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_pausedElapsedMs = 0;
m_playbackTimer.invalidate(); m_isPaused = true;
m_seekRequested = false;
return true; return true;
} }
void VideoDecoder::closeFile() { void VideoDecoder::closeFile() {
// Esperamos a que finalice el hilo antes de liberar sus recursos
stopThread();
if (m_codecContext) { if (m_codecContext) {
avcodec_free_context(&m_codecContext); avcodec_free_context(&m_codecContext);
m_codecContext = nullptr; m_codecContext = nullptr;
@ -142,6 +141,8 @@ void VideoDecoder::closeFile() {
av_buffer_unref(&m_hwDeviceCtx); av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr; m_hwDeviceCtx = nullptr;
} }
// Liberamos la referencia del fotograma actual de forma segura protegiendo el puntero
{ {
QMutexLocker frameLocker(&m_frameMutex); QMutexLocker frameLocker(&m_frameMutex);
if (m_currentFrame) { if (m_currentFrame) {
@ -158,8 +159,8 @@ void VideoDecoder::play() {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
if (m_isPaused) { if (m_isPaused) {
m_isPaused = false; m_isPaused = false;
m_playbackTimer.restart(); m_playbackTimer.restart(); // Inicia el reloj maestro
m_waitCond.wakeAll(); m_waitCond.wakeAll(); // Despierta al hilo de decodificación si estaba bloqueado
} }
} }
@ -167,6 +168,7 @@ void VideoDecoder::pause() {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
if (!m_isPaused) { if (!m_isPaused) {
m_isPaused = true; m_isPaused = true;
// Acumula el tiempo transcurrido hasta este momento y detiene el temporizador activo
m_pausedElapsedMs += m_playbackTimer.elapsed(); m_pausedElapsedMs += m_playbackTimer.elapsed();
m_playbackTimer.invalidate(); m_playbackTimer.invalidate();
} }
@ -176,7 +178,7 @@ void VideoDecoder::seek(double seconds) {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
m_seekTimeSecs = seconds; m_seekTimeSecs = seconds;
m_seekRequested = true; m_seekRequested = true;
m_waitCond.wakeAll(); m_waitCond.wakeAll(); // Despierta al hilo para procesar el salto inmediatamente
} }
void VideoDecoder::stopThread() { void VideoDecoder::stopThread() {
@ -185,7 +187,7 @@ void VideoDecoder::stopThread() {
m_running = false; m_running = false;
m_waitCond.wakeAll(); m_waitCond.wakeAll();
} }
wait(); wait(); // Espera síncrona a que termine el hilo secundario
} }
qint64 VideoDecoder::getPlaybackTimeMs() const { qint64 VideoDecoder::getPlaybackTimeMs() const {
@ -196,11 +198,15 @@ qint64 VideoDecoder::getPlaybackTimeMs() const {
} }
bool VideoDecoder::getCurrentFrameRef(AVFrame *destFrame) { 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); QMutexLocker frameLocker(&m_frameMutex);
if (!m_currentFrame || m_currentFrame->width == 0) { if (!m_currentFrame || m_currentFrame->width == 0) {
return false; return false;
} }
av_frame_unref(destFrame); 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); av_frame_ref(destFrame, m_currentFrame);
return true; return true;
} }
@ -211,43 +217,62 @@ void VideoDecoder::run() {
AVPacket *packet = av_packet_alloc(); AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_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) { while (true) {
// --- CONTROL DE ESTADO (PAUSA / PARADA) ---
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
if (!m_running) break; 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); m_waitCond.wait(&m_mutex);
}
if (!m_running) break; 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; m_seekRequested = false;
} }
} }
int readResult = av_read_frame(m_formatContext, packet); if (seekNeeded) {
if (readResult < 0) { // Convertimos la posición de segundos al formato interno de base de tiempo del flujo
if (readResult == AVERROR_EOF) { AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex];
emit playbackFinished(); 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); QThread::msleep(10);
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
m_isPaused = true; m_isPaused = true;
@ -256,25 +281,33 @@ void VideoDecoder::run() {
} }
if (packet->stream_index == m_videoStreamIndex) { if (packet->stream_index == m_videoStreamIndex) {
// Enviamos el paquete comprimido al decodificador físico
if (avcodec_send_packet(m_codecContext, packet) >= 0) { if (avcodec_send_packet(m_codecContext, packet) >= 0) {
// Leemos los fotogramas decodificados por el hardware
while (avcodec_receive_frame(m_codecContext, frame) >= 0) { 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); double ptsSecs = frame->best_effort_timestamp * av_q2d(m_formatContext->streams[m_videoStreamIndex]->time_base);
qint64 ptsMs = static_cast<qint64>(ptsSecs * 1000.0); qint64 ptsMs = static_cast<qint64>(ptsSecs * 1000.0);
// 2. Comparar contra el Reloj Maestro
qint64 playbackTime = getPlaybackTimeMs(); qint64 playbackTime = getPlaybackTimeMs();
if (ptsMs > playbackTime) { 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; qint64 delay = ptsMs - playbackTime;
if (delay < 2000) { if (delay < 2000) { // Margen de seguridad razonable
QThread::msleep(delay); 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); QMutexLocker frameLocker(&m_frameMutex);
av_frame_unref(m_currentFrame); 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 frameReady();
emit positionChanged(ptsMs); emit positionChanged(ptsMs);
} }

View file

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