播放器OpenGL渲染yuv420p,nv12,支持自适应等比例拉伸
1 项目概述
本项目实现了一个基于OpenGL的YUV视频渲染器,支持YUV420P和NV12两种主流格式。项目的核心功能包括:
- 硬件加速渲染:利用GPU着色器进行YUV到RGB颜色空间转换
- 等比例缩放:保持视频原始宽高比,避免画面变形
- 实时播放:支持可配置帧率的流畅播放
- 跨平台支持:基于Qt框架,支持Windows/Linux/macOS
视频讲解与工程代码领取:播放器OpenGL渲染yuv420p,nv12,支持自适应等比例拉伸
2 技术架构
┌─────────────────────────────────────┐ │ 应用层 (Qt Application) │ ├─────────────────────────────────────┤ │ main.cpp │ │ └── 命令行参数解析 │ │ └── Widget初始化和显示 │ └─────────────────────────────────────┘ ┌─────────────────────────────────────┐ │ 渲染层 (Widget Class) │ ├─────────────────────────────────────┤ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ YUV数据管理 │ │ OpenGL渲染 │ │ 等比例缩放 │ │ │ │ - 格式解析 │ │ - 着色器编译 │ │ - 顶点计算 │ │ │ │ - 内存分配 │ │ - 纹理管理 │ │ - 窗口适配 │ │ │ │ - 数据加载 │ │ - 绘制调用 │ │ - 宽高比保持 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────┘ ┌─────────────────────────────────────┐ │ OpenGL图形管道 (GPU) │ ├─────────────────────────────────────┤ │ 顶点着色器 → 图元装配 → 光栅化 → 片段着色器 → 帧缓冲 │ │ ↓ ↓ ↓ ↓ ↓ │ │ 坐标变换 几何处理 像素化 YUV→RGB 显示输出 │ └─────────────────────────────────────┘
3 YUV格式基础
YUV颜色空间原理
YUV是一种颜色编码方式,将图像信息分为:
- Y分量:亮度信息 (Luminance),决定图像的明暗
- U分量:色度信息 (Chrominance),蓝色-亮度的差值
- V分量:色度信息 (Chrominance),红色-亮度的差值
YUV420P格式详解
YUV420P是一种平面格式 (Planar),数据存储结构:
内存布局: ┌─────────────────────────────────────────┐ │ Y 平面 │ ← 分辨率: W × H │ (亮度信息) │ ├─────────────────────────────────────────┤ │ U 平面 │ │ ← 分辨率: W/2 × H/2 │ (色度信息) │ │ ├─────────────────────────────────────────┤ │ V 平面 │ │ ← 分辨率: W/2 × H/2 │ (色度信息) │ │ └─────────────────────────────────────────┘ 像素采样比例:4:2:0 - Y分量:每个像素都有亮度信息 - U/V分量:每4个像素共享一个色度值 (2×2区域)
存储特点:
- Y平面大小:width × height 字节
- U平面大小:(width/2) × (height/2) 字节
- V平面大小:(width/2) × (height/2) 字节
- 总大小:width × height × 1.5 字节
NV12格式详解
NV12是一种半平面格式 (Semi-Planar),数据存储结构:
内存布局: ┌─────────────────────┐ │ Y 平面 │ ← 分辨率: W × H │ (亮度信息) │ ├─────────────────────┤ │ UV 交错平面 │ ← 分辨率: W/2 × H/2 │ U0V0 U1V1 U2V2 ... │ ← UV数据交错存储 │ U4V4 U5V5 U6V6 ... │ └─────────────────────┘ 像素采样比例:4:2:0 (与YUV420P相同) - Y分量:每个像素都有亮度信息 - UV分量:U和V交错存储,每4个像素共享
存储特点:
- Y平面大小:width × height 字节
- UV平面大小:(width/2) × (height/2) × 2 字节
- 总大小:width × height × 1.5 字节
格式对比
4 OpenGL渲染原理
OpenGL生命周期和三大核心函数
在Qt的QOpenGLWidget中,OpenGL渲染遵循特定的生命周期,由三个核心函数控制:
应用程序启动 ↓ ┌─────────────────────┐ │ initializeGL() │ ← 只调用一次 │ OpenGL上下文初始化 │ │ - 编译着色器程序 │ │ - 创建纹理对象 │ │ - 设置OpenGL状态 │ │ - 初始化顶点缓冲区 │ └─────────────────────┘ ↓ ┌─────────────────────┐ │ resizeGL() │ ← 窗口大小变化时调用 │ 视口和投影设置 │ │ - 设置OpenGL视口大小 │ │ - 重新计算等比例缩放参数 │ │ - 更新顶点坐标 │ └─────────────────────┘ ↓ ┌─────────────────────┐ │ paintGL() │ ← 每帧渲染时调用 │ 实际绘制渲染 │ │ - 上传YUV纹理数据 │ │ - 执行绘制命令 │ │ - 输出到帧缓冲区 │ └─────────────────────┘ ↑ ↓ └───── 循环调用 ───────┘ (定时器触发/update()调用)
函数调用关系和时机
1. initializeGL() - 一次性初始化
调用时机:
- QOpenGLWidget首次显示时
- OpenGL上下文成功创建后
- 整个应用程序生命周期中只调用一次
主要职责:
void Widget::initializeGL() { // 1. 初始化OpenGL函数指针 initializeOpenGLFunctions(); // 2. 设置OpenGL全局状态 glDisable(GL_DEPTH_TEST); // 禁用深度测试(2D渲染不需要) // 3. 编译和链接着色器程序 this->initShader(); // 根据YUV格式生成着色器 // 4. 创建纹理对象 this->initTextures(); // 创建Y、U、V或UV纹理 // 5. 设置清除颜色 this->initColor(); // 设置黑色背景 // 6. 初始化顶点缓冲区(NV12格式) if (format_ == YUVFormat::NV12) { vbo.create(); } // 7. 计算初始顶点坐标 updateVertexBuffers(); // 设置等比例缩放坐标 }
2. resizeGL() - 窗口变化响应
调用时机:
- 窗口大小改变时自动调用
- 包括初始显示时的第一次调用
- 用户拖拽窗口边缘时的每次变化
主要职责:
void Widget::resizeGL(int width, int height) { // 1. 更新OpenGL视口 glViewport(0, 0, width, height); // 设置渲染区域为整个窗口 // 2. 重新计算等比例缩放 if (width_ > 0 && height_ > 0) { // 确保有有效的视频数据 updateVertexBuffers(); // 根据新窗口尺寸重新计算顶点 // → 调用calculateAspectRatioVertices() // → 更新aspectRatioVertices[]数组 // → 应用到顶点属性或VBO } // 注意:此函数不进行实际绘制,只更新渲染参数 }
3. paintGL() - 每帧渲染
调用时机:
- QTimer定时器触发时 (播放新帧)
- 手动调用update()时
- 窗口需要重绘时 (如被其他窗口遮挡后重新显示)
主要职责:
void Widget::paintGL() { // 1. 数据有效性检查 if (!dataY || width_ == 0 || height_ == 0) { this->initColor(); // 显示黑色背景 return; } // 2. 根据格式选择渲染函数 if(format_ == YUVFormat::YUV420P) { yuv420pPaintGL(); // YUV420P渲染流程 } else if (format_ == YUVFormat::NV12) { nv12PaintGL(); // NV12渲染流程 } // 3. 实际渲染流程 (以YUV420P为例) yuv420pPaintGL() { // a. 上传Y纹理数据 glActiveTexture(GL_TEXTURE0); glTexImage2D(..., dataY); // b. 上传U纹理数据 glActiveTexture(GL_TEXTURE1); glTexImage2D(..., dataU); // c. 上传V纹理数据 glActiveTexture(GL_TEXTURE2); glTexImage2D(..., dataV); // d. 执行绘制命令 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); } }
完整的渲染流程图
应用程序启动 ↓ Widget构造函数 ↓ widget.init(format, width, height) ← 设置格式和尺寸 ↓ widget.show() ← 显示窗口,触发OpenGL初始化 ↓ ┌─────────────────────────────────────────┐ │ initializeGL() │ ← Qt自动调用 │ - initShader() 编译着色器 │ │ - initTextures() 创建纹理 │ │ - updateVertexBuffers() 初始顶点 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ resizeGL(w, h) │ ← Qt自动调用 │ - glViewport(0,0,w,h) 设置视口 │ │ - updateVertexBuffers() 重算顶点 │ └─────────────────────────────────────────┘ ↓ widget.play(fileName, fps) ← 开始播放 ↓ QTimer.start(1000/fps) ← 启动定时器 ↓ ┌─────────────────────────────────────────┐ │ 定时器循环 (每1/fps秒) │ │ ↓ │ │ read() 读取YUV数据 │ │ ↓ │ │ update() 触发重绘 │ │ ↓ │ │ paintGL() 渲染一帧 │ ← Qt自动调用 │ - yuv420pPaintGL() 上传纹理 │ │ - glDrawArrays() 绘制 │ │ ↓ │ │ 显示到屏幕 │ └─────────────────────────────────────────┘ ↑ │ └────────────────────┘ (循环直到播放结束) 用户调整窗口大小时: ↓ ┌─────────────────────────────────────────┐ │ resizeGL(newW, newH) │ ← Qt自动调用 │ - 重新计算等比例缩放参数 │ │ - 更新顶点坐标 │ └─────────────────────────────────────────┘ ↓ 下次paintGL()时使用新的顶点坐标
函数间的数据流动
1. initializeGL() 阶段: 设置格式 → 编译着色器 → 创建纹理 → 计算初始顶点 2. resizeGL() 阶段: 窗口尺寸 → 计算宽高比 → 更新顶点坐标 → 存储到数组 3. paintGL() 阶段: YUV数据 → 上传到纹理 → 使用存储的顶点 → 绘制输出 关键数据传递: - aspectRatioVertices[] : resizeGL()计算 → paintGL()使用 - 纹理对象IDs : initializeGL()创建 → paintGL()使用 - 着色器程序 : initializeGL()编译 → paintGL()使用
性能考虑
initializeGL()优化:
- 一次性操作,可以进行复杂的初始化
- 着色器编译、纹理创建等耗时操作放这里
resizeGL()优化:
- 频率较低,但要快速响应
- 只做必要的参数重新计算
- 避免重复分配内存
paintGL()优化:
- 高频调用,性能关键
- 避免重复的状态设置
- 纹理数据上传是主要开销
三大函数关系总结表
数据流动关系
initializeGL() 创建: ├── textureY, textureU, textureV (纹理对象ID) ├── QOpenGLShaderProgram (着色器程序) └── 初始 aspectRatioVertices[] (默认顶点坐标) ↓ (传递给) resizeGL() 更新: ├── glViewport() (视口大小) └── aspectRatioVertices[] (重新计算的顶点坐标) ↓ (传递给) paintGL() 使用: ├── 纹理对象 (上传YUV数据) ├── 着色器程序 (渲染管线) └── 顶点坐标 (几何变换)
着色器详解
YUV420P顶点着色器
attribute vec4 vertexIn; // 输入:顶点位置 (x,y,z,w) attribute vec2 textureIn; // 输入:纹理坐标 (u,v) varying vec2 textureOut; // 输出:传递给片段着色器的纹理坐标 void main(void) { gl_Position = vertexIn; // 设置最终顶点位置 textureOut = textureIn; // 传递纹理坐标 }
功能说明:
- 接收CPU传入的顶点坐标 (经过等比例缩放计算)
- 接收纹理坐标 (固定为[0,1]范围)
- 将纹理坐标传递给片段着色器进行插值
YUV420P片段着色器
varying mediump vec2 textureOut; // 插值后的纹理坐标 uniform sampler2D textureY; // Y分量纹理采样器 uniform sampler2D textureU; // U分量纹理采样器 uniform sampler2D textureV; // V分量纹理采样器 void main(void) { vec3 yuv; vec3 rgb; // 步骤1:从三个纹理中采样YUV分量 yuv.r = texture2D(textureY, textureOut).r; // Y: [0,1] yuv.g = texture2D(textureU, textureOut).r - 0.5; // U: [-0.5,0.5] yuv.b = texture2D(textureV, textureOut).r - 0.5; // V: [-0.5,0.5] // 步骤2:YUV到RGB颜色空间转换 // 使用ITU-R BT.601转换矩阵 rgb = mat3( 1.0, 1.0, 1.0, // R = Y + 0*U + 1.540*V 0.0, -0.138, 1.816, // G = Y - 0.138*U + 1.816*V 1.540, -0.459, 0.0 // B = Y + 1.540*U + 0*V ) * yuv; // 步骤3:输出最终RGB颜色 gl_FragColor = vec4(rgb, 1.0); }
NV12片段着色器
varying mediump vec4 textureOut; // 插值后的纹理坐标 uniform sampler2D textureY; // Y分量纹理采样器 uniform sampler2D textureUV; // UV交错纹理采样器 void main(void) { vec3 yuv; vec3 rgb; // 步骤1:从两个纹理中采样YUV分量 yuv.r = texture2D(textureY, textureOut.st).r; // Y: [0,1] yuv.g = texture2D(textureUV, textureOut.st).r - 0.5; // U: [-0.5,0.5] (R通道) yuv.b = texture2D(textureUV, textureOut.st).g - 0.5; // V: [-0.5,0.5] (G通道) // 步骤2:YUV到RGB颜色空间转换 (使用NV12优化系数) rgb = mat3( 1.0, 1.0, 1.0, 0.0, -0.3455, 1.779, 1.4075,-0.7169, 0.0 ) * yuv; // 步骤3:输出最终RGB颜色 gl_FragColor = vec4(rgb, 1.0); }
YUV到RGB转换矩阵原理
YUV到RGB的转换基于以下数学公式:
标准公式 (ITU-R BT.601): R = Y + 1.402 * (V - 128) G = Y - 0.344 * (U - 128) - 0.714 * (V - 128) B = Y + 1.772 * (U - 128) 归一化后 (0-1范围): R = Y + 1.402 * (V - 0.5) G = Y - 0.344 * (U - 0.5) - 0.714 * (V - 0.5) B = Y + 1.772 * (U - 0.5) 矩阵形式: [R] [1.0 0.0 1.402] [Y ] [G] = [1.0 -0.344 -0.714] [U-0.5] [B] [1.0 1.772 0.0 ] [V-0.5]
5 等比例缩放算法
算法核心思想
等比例缩放的目标是:保持视频原始宽高比不变,避免画面拉伸变形
实现步骤
1. 计算宽高比
// 视频原始宽高比 float videoAspect = (float)videoWidth / (float)videoHeight; // 窗口当前宽高比 float windowAspect = (float)windowWidth / (float)windowHeight;
2. 决定缩放策略
float scaleX, scaleY; if (videoAspect > windowAspect) { // 视频比窗口更"宽" → 按宽度适配,上下留黑边 scaleX = 1.0f; // 水平填满 scaleY = windowAspect / videoAspect; // 垂直缩放 } else { // 视频比窗口更"高" → 按高度适配,左右留黑边 scaleX = videoAspect / windowAspect; // 水平缩放 scaleY = 1.0f; // 垂直填满 }
3. 计算顶点坐标
OpenGL标准化设备坐标 (NDC) 范围是 [-1, 1]
// 计算等比例缩放后的顶点坐标 GLfloat vertices[8] = { -scaleX, -scaleY, // 左下角 scaleX, -scaleY, // 右下角 -scaleX, scaleY, // 左上角 scaleX, scaleY // 右上角 };
缩放效果示例
示例1:16:9视频在4:3窗口中显示
视频: 1920×1080 (16:9 ≈ 1.78) 窗口: 1024×768 (4:3 ≈ 1.33) 计算: videoAspect = 1920/1080 = 1.78 windowAspect = 1024/768 = 1.33 videoAspect > windowAspect → 按宽度适配 scaleX = 1.0 scaleY = 1.33/1.78 = 0.75 结果: ┌─────────────────────────┐ │ ██████黑边██████ │ ← 上黑边 │ ████████████████ │ │ ████视频内容████ │ ← 视频区域 (75%高度) │ ████████████████ │ │ ██████黑边██████ │ ← 下黑边 └─────────────────────────┘
示例2:4:3视频在16:9窗口中显示
视频: 1024×768 (4:3 ≈ 1.33) 窗口: 1920×1080 (16:9 ≈ 1.78) 计算: videoAspect = 1024/768 = 1.33 windowAspect = 1920/1080 = 1.78 videoAspect < windowAspect → 按高度适配 scaleX = 1.33/1.78 = 0.75 scaleY = 1.0 结果: ┌─────────────────────────┐ │ █黑█████视频内容██████黑 │ ← 视频区域 (75%宽度) │ █边██████████████████边 │ ← 左右黑边 │ █ █████████████████ █ │ └─────────────────────────┘
关键代码实现
void Widget::calculateAspectRatioVertices() { // 安全检查 if (width_ == 0 || height_ == 0) { // 无效尺寸时使用全屏显示 setFullScreenVertices(); return; } // 获取当前窗口尺寸 int windowWidth = this->width(); int windowHeight = this->height(); if (windowWidth == 0 || windowHeight == 0) { setFullScreenVertices(); return; } // 计算宽高比 float videoAspect = (float)width_ / (float)height_; float windowAspect = (float)windowWidth / (float)windowHeight; // 计算缩放因子 float scaleX, scaleY; if (videoAspect > windowAspect) { // 视频更宽:按宽度适配,垂直方向留黑边 scaleX = 1.0f; scaleY = windowAspect / videoAspect; } else { // 视频更高:按高度适配,水平方向留黑边 scaleX = videoAspect / windowAspect; scaleY = 1.0f; } // 生成顶点坐标 (NDC坐标系) aspectRatioVertices[0] = -scaleX; aspectRatioVertices[1] = -scaleY; // 左下 aspectRatioVertices[2] = scaleX; aspectRatioVertices[3] = -scaleY; // 右下 aspectRatioVertices[4] = -scaleX; aspectRatioVertices[5] = scaleY; // 左上 aspectRatioVertices[6] = scaleX; aspectRatioVertices[7] = scaleY; // 右上 }
6 关键技术实现
1. 纹理管理
YUV420P纹理创建
void Widget::initTextures() { if(format_ == YUVFormat::YUV420P) { // 创建Y分量纹理 glGenTextures(1, &textureY); glBindTexture(GL_TEXTURE_2D, textureY); setTextureParameters(); // 创建U分量纹理 glGenTextures(1, &textureU); glBindTexture(GL_TEXTURE_2D, textureU); setTextureParameters(); // 创建V分量纹理 glGenTextures(1, &textureV); glBindTexture(GL_TEXTURE_2D, textureV); setTextureParameters(); } }
纹理参数设置
void Widget::initParamete() { // 纹理过滤方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // 放大过滤 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // 缩小过滤 // 纹理环绕方式 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); // S方向截取 glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); // T方向截取 }
2. 数据上传
YUV420P数据上传
void Widget::yuv420pPaintGL() { // Y分量纹理上传 glActiveTexture(GL_TEXTURE0); // 激活纹理单元0 glBindTexture(GL_TEXTURE_2D, textureY); // 绑定Y纹理 glPixelStorei(GL_UNPACK_ROW_LENGTH, lineSizeY); // 设置行对齐 glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width_, height_, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, dataY); // 上传Y数据 glUniform1i(textureUniformY, 0); // 绑定到着色器uniform变量 // U分量纹理上传 (尺寸为原始的1/4) glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, textureU); glPixelStorei(GL_UNPACK_ROW_LENGTH, lineSizeU); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width_ >> 1, height_ >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, dataU); glUniform1i(textureUniformU, 1); // V分量纹理上传 (尺寸为原始的1/4) glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, textureV); glPixelStorei(GL_UNPACK_ROW_LENGTH, lineSizeV); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, width_ >> 1, height_ >> 1, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, dataV); glUniform1i(textureUniformV, 2); // 绘制四边形 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
NV12数据上传
void Widget::nv12PaintGL() { // Y分量纹理上传 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, textureY); glPixelStorei(GL_UNPACK_ROW_LENGTH, lineSizeY); glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, // 注意:使用GL_RED格式 width_, height_, 0, GL_RED, GL_UNSIGNED_BYTE, dataY); // UV交错纹理上传 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureUV); glPixelStorei(GL_UNPACK_ROW_LENGTH, lineSizeUV >> 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, // 注意:使用GL_RG格式 width_ >> 1, height_ >> 1, 0, GL_RG, GL_UNSIGNED_BYTE, dataUV); // 设置uniform变量 program.setUniformValue("textureY", 1); program.setUniformValue("textureUV", 0); // 绘制四边形 glDrawArrays(GL_QUADS, 0, 4); }
3. 内存管理
YUV数据分配
bool Widget::play(const QString &fileName, int frameRate) { // 计算一帧YUV数据的大小 qint64 frameSize = (width_ * height_ * 3) >> 1; // YUV420/NV12都是1.5倍像素数 // 分配内存缓冲区 dataY = new quint8[frameSize]; if(format_ == YUVFormat::YUV420P) { // YUV420P: 分别指向Y、U、V数据区域 dataU = dataY + (width_ * height_); // U数据起始位置 dataV = dataU + ((width_ * height_) >> 2); // V数据起始位置 } else if(format_ == YUVFormat::NV12) { // NV12: UV交错数据起始位置 dataUV = dataY + (width_ * height_); } // 启动播放定时器 timer.start(1000 / frameRate); return true; }
7 性能优化要点
1. GPU内存优化
纹理格式选择:
- YUV420P使用GL_LUMINANCE格式 (单通道8位)
- NV12使用GL_RED和GL_RG格式 (更高效的内存布局)
纹理过滤:
- 使用GL_NEAREST避免不必要的插值计算
- 对于视频播放,线性插值的视觉提升有限
2. CPU优化
内存对齐:
- 使用glPixelStorei(GL_UNPACK_ROW_LENGTH)处理行对齐
- 避免GPU访问非对齐内存造成的性能损失
数据复用:
- 纹理对象复用,避免频繁创建/销毁
- 着色器程序编译一次,多次使用
3. 渲染优化
状态切换最小化:
- 批量设置OpenGL状态
- 减少纹理绑定切换次数
顶点缓冲优化:
- 预计算等比例缩放顶点坐标
- 只在窗口大小改变时重新计算
8 常见问题解决
1. 花屏问题
症状:显示内容出现彩色噪点或错乱
原因:
- 行对齐设置错误
- YUV数据读取不完整
- 纹理尺寸计算错误
解决方案:
// 确保正确设置行对齐 glPixelStorei(GL_UNPACK_ROW_LENGTH, actualLineSize); // 检查UV平面尺寸计算 int uvWidth = width_ >> 1; // 除以2 int uvHeight = height_ >> 1; // 除以2
2 绿屏问题
症状:显示为绿色画面
原因:
- YUV数据未正确加载 (全零数据)
- 颜色转换矩阵错误
- 纹理采样问题
解决方案:
// 检查数据是否为空 if (!dataY || width_ == 0 || height_ == 0) { // 显示黑色背景而不是绿色 glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT); return; }
3. 性能问题
症状:播放卡顿、帧率不稳定
原因:
- 文件I/O阻塞主线程
- GPU纹理上传过于频繁
- 顶点坐标重复计算
解决方案:
// 1. 优化文件读取 if (!file.read((char*)dataY, frameSize)) { // 读取失败时的处理 timer.stop(); return; } // 2. 避免重复计算顶点坐标 void Widget::resizeGL(int width, int height) { glViewport(0, 0, width, height); // 只在窗口大小改变时重新计算 if (width_ > 0 && height_ > 0) { updateVertexBuffers(); } }
4. 内存泄漏
症状:长时间运行后内存占用不断增长
原因:
- OpenGL资源未正确释放
- YUV数据缓冲区重复分配
解决方案:
Widget::~Widget() { makeCurrent(); // 清理纹理资源 if (textureY != 0) glDeleteTextures(1, &textureY); if (textureU != 0) glDeleteTextures(1, &textureU); if (textureV != 0) glDeleteTextures(1, &textureV); if (textureUV != 0) glDeleteTextures(1, &textureUV); // 清理VBO资源 if (format_ == YUVFormat::NV12) { vbo.destroy(); } doneCurrent(); // 清理数据缓冲区 delete[] dataY; dataY = nullptr; }#实习##秋招##校招##c++##牛客创作赏金赛#