Se comenta todo el código
This commit is contained in:
parent
1e5cf8f22a
commit
7532e7b330
9 changed files with 328 additions and 168 deletions
59
glwidget.cpp
59
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<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;
|
||||
|
|
|
|||
32
glwidget.h
32
glwidget.h
|
|
@ -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
|
||||
|
|
|
|||
5
main.cpp
5
main.cpp
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
46
mainwindow.h
46
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
|
||||
|
|
|
|||
15
shader.frag
15
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
223
videodecoder.cpp
223
videodecoder.cpp
|
|
@ -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_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<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();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue