三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

从摄像头到屏幕:手把手解析NV12数据在Android FFmpeg中的处理流水线

从摄像头到屏幕:手把手解析NV12数据在Android FFmpeg中的处理流水线

从摄像头到屏幕:手把手解析NV12数据在Android FFmpeg中的处理流水线

在移动端视频处理领域,NV12格式作为Android Camera2 API默认输出的YUV420SP格式,其高效的内存布局和硬件兼容性使其成为开发者必须掌握的核心技术点。本文将构建一个完整的端到端处理流水线,涵盖从Camera2采集到FFmpeg处理再到屏幕渲染的全链路实战方案。

1. 理解NV12格式与Camera2采集机制

NV12属于YUV420SP家族,其内存排列特点为:先存储所有Y分量(亮度),再交错存储UV分量(色度)。与常见的I420(YUV420P)相比,NV12的UV交错排布更适合GPU直接处理,这也是Android系统首选此格式的原因。

Camera2 API获取NV12数据的关键代码结构:

ImageReader reader = ImageReader.newInstance( width, height, ImageFormat.YUV_420_888, // 实际输出NV12 MAX_IMAGES ); reader.setOnImageAvailableListener(reader -> { Image image = reader.acquireLatestImage(); // 获取YUV三个Plane Image.Plane yPlane = image.getPlanes()[0]; Image.Plane uPlane = image.getPlanes()[1]; Image.Plane vPlane = image.getPlanes()[2]; // 注意:stride可能不等于width int yStride = yPlane.getRowStride(); ByteBuffer yBuffer = yPlane.getBuffer(); // 处理NV12数据... }, handler);

关键注意事项

  • ImageFormat.YUV_420_888是Android的通用封装格式,实际输出可能是NV12或NV21
  • Plane[1]的pixelStride值为2表示UV交错存储(NV12/NV21特征)
  • 必须检查rowStride(内存行跨度),摄像头输出常有padding字节

2. FFmpeg中构建NV12处理管道

2.1 初始化FFmpeg上下文

AVFrame* create_avframe_from_nv12(int width, int height, uint8_t* y_data, uint8_t* uv_data, int y_stride, int uv_stride) { AVFrame* frame = av_frame_alloc(); frame->format = AV_PIX_FMT_NV12; frame->width = width; frame->height = height; // 填充Y分量 frame->data[0] = y_data; frame->linesize[0] = y_stride; // 填充UV交错分量 frame->data[1] = uv_data; frame->linesize[1] = uv_stride; return frame; }

2.2 色彩空间转换实战

使用FFmpeg的sws_scale进行格式转换时,需要特别注意内存对齐问题:

SwsContext* sws_ctx = sws_getContext( src_width, src_height, AV_PIX_FMT_NV12, dst_width, dst_height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL ); AVFrame* rgb_frame = av_frame_alloc(); rgb_frame->format = AV_PIX_FMT_RGBA; rgb_frame->width = dst_width; rgb_frame->height = dst_height; av_frame_get_buffer(rgb_frame, 0); sws_scale(sws_ctx, (const uint8_t* const*)nv12_frame->data, nv12_frame->linesize, 0, src_height, rgb_frame->data, rgb_frame->linesize );

性能优化技巧

  • 复用SwsContext避免重复创建开销
  • 使用av_frame_make_writable检查写时复制
  • 对齐目标宽度为32字节可提升SIMD效率

3. 内存布局的陷阱与解决方案

3.1 Stride不对齐问题

Android摄像头输出常有stride > width的情况,例如1080p图像可能实际stride为1920:

参数典型值说明
width1080图像有效宽度
stride1920内存行跨度
sliceHeight1088带padding的高度

处理方案:

// 裁剪padding部分 sws_scale(sws_ctx, &nv12_frame->data[0] + y_offset, // 计算偏移量 &nv12_frame->linesize[0], 0, visible_height, rgb_frame->data, rgb_frame->linesize );

3.2 零拷贝优化方案

避免内存拷贝的两种实现方式:

方案A:直接渲染到Surface

SurfaceTexture surfaceTexture = new SurfaceTexture(textureId); Surface surface = new Surface(surfaceTexture); // 配置Camera2输出到Surface session.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), ...);

方案B:FFmpeg硬件加速

AVBufferRef* hw_ctx = NULL; av_hwdevice_ctx_create(&hw_ctx, AV_HWDEVICE_TYPE_MEDIACODEC, NULL, NULL, 0); frame->hw_frames_ctx = av_hwframe_ctx_alloc(hw_ctx); // 配置硬件帧参数...

4. 全链路性能调优策略

4.1 流水线各阶段耗时分析

使用Android systrace工具捕获的性能数据示例:

阶段平均耗时(ms)优化手段
Camera采集8.2降低分辨率/帧率
NV12传输2.1使用GraphicBuffer
FFmpeg处理12.7启用NEON指令集
渲染显示3.4使用SurfaceView

4.2 多线程处理架构

推荐的任务调度模型:

Camera线程 → 采集NV12数据 ↓ (共享内存) FFmpeg工作线程池 ↓ (回调通知) UI线程 → OpenGL渲染

关键同步代码:

// 双缓冲队列 ConcurrentLinkedQueue<Image> imageQueue = new ConcurrentLinkedQueue<>(); // 生产者(Camera线程) imageQueue.offer(image); // 消费者(FFmpeg线程) while (!Thread.interrupted()) { Image img = imageQueue.poll(); if (img != null) { processImage(img); img.close(); } }

5. 典型问题排查指南

问题现象:转换后的图像出现绿色偏色

排查步骤

  1. 检查源数据格式是否确认为NV12(非NV21)
  2. 验证UV分量内存是否连续且交错排列
  3. 确认sws_scale输出格式设置正确
  4. 检查stride值是否被正确传递

调试技巧

# 导出NV12原始数据 adb shell dumpsys media.camera --dump-nv12=/sdcard/frame.nv12 # 使用ffplay验证 ffplay -f rawvideo -pixel_format nv12 -video_size 1920x1080 frame.nv12

在真实项目实践中,我们发现MediaCodec编码器对NV12的stride有特殊要求,当图像宽度不是64的倍数时,需要手动添加padding字节。这种情况下,直接使用FFmpeg的scale滤镜进行尺寸调整反而比处理stride问题更高效。

← 返回列表