视频与外部纹理
在处理 60fps 的高频内容更新(如视频或直播)时,ImageReader 提供了连接视频源与渲染管线的高速通路。
视频内容的流动性要求显存能够极其高效地重用。
- 零拷贝策略:视频数据直接从硬件解码器进入 GPU 显存,跳过了 CPU 内存的搬运。
- 同步驱动:通过 Poll 模型确保每一帧内容与渲染循环保持一致。
1. ImageReader 核心流程
ImageReader 通过内部的 ImageStream 连接平台原生视频源,驱动从视频流到 GPU 纹理的高速流转:
原生视频源 (Camera/Player) → ImageStream → ImageReader (帧管理) → acquireNextBuffer (ImageBuffer) → Image::MakeFrom (包装) → Canvas (绘制)
1.1 使用步骤
- 获取帧:在渲染循环中调用
reader->acquireNextBuffer()。该操作返回std::shared_ptr<ImageBuffer>,未就绪时返回 nullptr。 - 包装视图:通过
Image::MakeFrom(buffer)创建不可变图像。 - 渲染:GPU 在内部自动完成 YUV 到 RGB 的色彩空间转换。
1.2 完整代码示例
// Android example: Create reader from SurfaceTexture
auto reader = SurfaceTextureReader::Make(videoWidth, videoHeight, frameListener);
auto inputSurface = reader->getInputSurface();
// ... configure video decoder to output to inputSurface ...
// Render loop
void onDrawFrame(Canvas* canvas) {
auto buffer = reader->acquireNextBuffer();
if (buffer == nullptr) {
return; // No new frame available
}
auto image = Image::MakeFrom(buffer);
canvas->drawImage(image, 0, 0);
}
1.3 平台视频源
| 平台 | 子类 | 视频源 | 创建方式 |
|---|---|---|---|
| Android | SurfaceTextureReader | SurfaceTexture | Make(width, height, listener) |
| Web | VideoElementReader | HTMLVideoElement | MakeFrom(video, width, height) |
| iOS / macOS | 通过 HardwareBuffer | CVPixelBuffer | 使用 Image::MakeFrom(HardwareBuffer) |
Android 特殊说明:SurfaceTextureReader 需要传入一个实现了 SurfaceTexture.OnFrameAvailableListener 的 Java 对象作为 listener。当视频帧可用时,listener 的回调需要通过 JNI 调用 notifyFrameAvailable() 通知 Reader,否则此前获取的 ImageBuffer 将无法生成纹理。
2. YUV 数据导入
除了通过 ImageReader 从视频源读取,TGFX 还支持直接从原始 YUV 数据创建图像:
Image::MakeI420(yuvData, colorSpace):从 I420 平面数据创建图像。Image::MakeNV12(yuvData, colorSpace):从 NV12 交织数据创建图像。
2.1 YUVData 创建
YUVData 是一个不可变的多平面像素容器,通过 MakeFrom 工厂方法创建:
static std::shared_ptr<YUVData> MakeFrom(
int width, int height,
const void** data, // Array of base addresses for each plane
const size_t* rowBytes, // Array of bytes-per-row for each plane
size_t planeCount, // I420: 3, NV12: 2
ReleaseProc releaseProc = nullptr, // Called when YUVData is destroyed
void* context = nullptr); // User data for release callback
注意:如果 releaseProc 为 nullptr,调用者必须确保传入的数据指针在 YUVData 的整个生命周期内保持有效。
代码示例:从 I420 数据创建 Image
// Assume we have I420 data from a video decoder
const void* planes[3] = {yPlane, uPlane, vPlane};
size_t rowBytes[3] = {yStride, uStride, vStride};
auto yuvData = YUVData::MakeFrom(width, height, planes, rowBytes,
YUVData::I420_PLANE_COUNT,
releaseCallback, userData);
auto image = Image::MakeI420(yuvData, YUVColorSpace::BT709_LIMITED);
canvas->drawImage(image, 0, 0);
2.2 常见布局
- I420:平面模式,Y/U/V 三个通道分别存储(共 3 个 plane)。
- NV12:交织模式(Y 平面 + UV 交织,共 2 个 plane),移动端系统的默认采集格式。
2.3 YUVColorSpace (转换矩阵)
每种标准都区分 Limited 和 Full 范围:
BT601_LIMITED/BT601_FULL:标清视频(默认值)。BT709_LIMITED/BT709_FULL:高清视频。BT2020_LIMITED/BT2020_FULL:超高清视频。JPEG_FULL:JPEG 专用的全范围模式。
色彩范围差异:
- Limited 范围:Y 分量 [16-235],U/V 分量 [16-240]。这是录制视频的标准范围。
- Full 范围:所有分量 [0-255]。JPEG 图片默认使用此范围。
色彩注意:若视频看起来发灰或颜色偏差,通常是因为 YUVColorSpace 参数与原始流信息不匹配导致。特别注意 Limited 与 Full 的区分——录制的视频通常为 Limited 范围,而 JPEG 图片为 Full 范围。
3. 安全管理:双缓冲与过期规则
显存重用与安全性的平衡。
3.1 自动过期机制 (Expiration)
- 规则:同一个
ImageReader生成的所有ImageBuffer共享同一份内部纹理。当新获取的ImageBuffer被绘制后,此前获取的 ImageBuffer 将自动失效。这意味着同时可访问的有效 ImageBuffer 最多为两个(当前帧 + 下一帧)。 - 约束:严禁在类的成员变量中跨帧持久化持有
ImageBuffer。 - 后果:访问过期帧时
ImageBuffer::expired()返回 true,其无法再创建纹理。
代码示例:安全帧管理
void onDrawFrame(Canvas* canvas) {
auto buffer = reader->acquireNextBuffer();
if (buffer == nullptr || buffer->expired()) {
return; // No valid frame
}
auto image = Image::MakeFrom(buffer);
canvas->drawImage(image, 0, 0);
// Do NOT store 'buffer' as a member variable across frames
}
3.2 线程与上下文绑定
- Context 绑定:由
ImageReader产生的ImageBuffer在首次被绘制时会绑定到对应的 GPU Context,之后无法再在其他 Context 上使用。 - 线程安全:
ImageBuffer的属性访问(width()、height()、expired()等)在所有线程上都是安全的,即使 buffer 已过期。
4. 渲染效果参考
YUV I420 数据渲染
通过 YUVData 和 Image::MakeI420 将 I420 平面数据渲染为 RGB 图像:
int width = 200;
int height = 200;
// Allocate I420 planes: Y (full size), U/V (quarter size each)
std::vector<uint8_t> yPlane(width * height);
std::vector<uint8_t> uPlane(width / 2 * height / 2);
std::vector<uint8_t> vPlane(width / 2 * height / 2);
// Fill Y plane with horizontal brightness gradient
for (int row = 0; row < height; ++row) {
for (int col = 0; col < width; ++col) {
yPlane[row * width + col] = static_cast<uint8_t>(col * 235 / width + 16);
}
}
// Fill U/V planes with vertical color gradient
for (int row = 0; row < height / 2; ++row) {
for (int col = 0; col < width / 2; ++col) {
uPlane[row * (width / 2) + col] = static_cast<uint8_t>(row * 200 / (height / 2) + 28);
vPlane[row * (width / 2) + col] = static_cast<uint8_t>(200 - row * 150 / (height / 2));
}
}
// Create YUVData and Image
const void* planes[3] = {yPlane.data(), uPlane.data(), vPlane.data()};
size_t rowBytes[3] = {(size_t)width, (size_t)(width / 2), (size_t)(width / 2)};
auto yuvData = YUVData::MakeFrom(width, height, planes, rowBytes, YUVData::I420_PLANE_COUNT);
auto yuvImage = Image::MakeI420(yuvData, YUVColorSpace::BT601_FULL);
// Draw: GPU automatically converts YUV to RGB
canvas->drawImage(yuvImage, 0, 0);

5. 协作提示
同步注意:开发者需自行结合平台的帧可用回调(如
onFrameAvailable)来驱动Canvas重新发起绘制指令。
