news 2026/6/13 11:10:02

Android摄像头采集→H.264硬编码→UDP推流→SurfaceView硬解播放全流程示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android摄像头采集→H.264硬编码→UDP推流→SurfaceView硬解播放全流程示例

本文还有配套的精品资源,点击获取

简介:直接在Android设备上跑通的端到端视频处理链路:调用系统摄像头实时采集YV12帧,转I420格式后送MediaCodec硬编码为H.264裸流;编码数据支持双路径输出——一路通过UDP协议实时推送到指定IP和端口,可用VLC等播放器直接拉流观看;另一路可读取本地.h264文件进行硬解码,并将解码画面渲染到SurfaceView显示。整套代码已在Android 4.4.2系统(小米机型)完成实测验证,包含完整APK(studyMediacodec.apk)、Java源码(src/com/目录下)、资源文件(res/各密度drawable、layout、values等)、配置清单(AndroidManifest.xml)、构建配置(project.properties、proguard-project.txt)及必要依赖库(android-support-v4.jar)。项目结构清晰,兼容Eclipse开发环境,导入即编译,无需额外配置或修改即可运行调试。适合用于理解Android平台MediaCodec硬编解码工作流程、验证摄像头采集与网络传输协同逻辑、搭建轻量级实时视频传输原型。

1. 项目概述:一条跑在真实手机上的“视频流水线”

你有没有试过,在一台连着USB线的Android手机上,点开一个APK,摄像头画面立刻开始采集,几毫秒后,另一台电脑上的VLC播放器就跳出一个实时流动的画面?不是模拟器,不是截图,不是录屏回放——是真正在小米4(Android 4.4.2)上跑起来的、从光学镜头到网络字节、再到解码渲染的完整视频链路。这就是本项目要解决的核心问题:把教科书里抽象的“采集→编码→传输→解码→显示”五个环节,变成一段能摸得到、测得出、改得动、复现得了的真实代码流。

它不讲大而全的音视频理论,也不堆砌MediaCodec API文档里的每个参数含义,而是聚焦在一个最朴素但最难打通的闭环上:让摄像头帧真正“活”起来,穿过系统硬编解码器,跳过网络协议栈,最终在SurfaceView里稳稳地动起来。关键词“MediaCodec硬编码”“UDP视频推流”“SurfaceView解码显示”,不是并列的三个技术点,而是这条流水线上环环相扣的三道工序——少一道,画面就卡死;错一步,VLC就黑屏;漏一帧,SurfaceView就撕裂。我当年第一次在Android 4.4上跑通这套流程时,盯着VLC窗口里自己晃动的手指,足足愣了半分钟:原来YV12转I420不是memcpy那么简单,原来UDP发包不能一股脑send(),原来SurfaceView的Surface必须和MediaCodec的outputSurface严格绑定生命周期……这些细节,官方Sample里不会写,Stack Overflow上搜到的答案往往缺前少后,而本项目就是把这些“踩坑现场”原样打包,连同APK、源码、配置一起塞给你。

它适合谁?如果你正被MediaCodec的configure()卡住,搞不清inputBuffer和outputBuffer怎么配对;如果你试过用Socket发送H.264裸流,结果VLC提示“invalid NAL unit size”;如果你的SurfaceView明明setSurface()了却一片空白——那这项目就是为你写的。它不要求你懂H.264语法树,但要求你愿意打开Eclipse,导入工程,断点进onPreviewFrame(),看着byte[]数据从Camera回调里一层层流出去。它不是终点,而是你亲手拧开Android音视频黑盒子的第一颗螺丝。

2. 整体架构与设计思路拆解

2.1 为什么是“YV12 → I420 → H.264 → UDP → SurfaceView”这条路径?

先说结论:这不是拍脑袋定的,而是Android 4.4.2系统级硬编码能力与摄像头原始输出格式之间“妥协出来的最优解”。我们来一层层剥开:

第一层:摄像头输出格式的硬约束
Android Camera API(非Camera2)在4.4.2时代,previewCallback默认返回的格式是ImageFormat.NV21ImageFormat.YV12,具体取决于设备厂商实现。小米4实测返回的是YV12——一种Planar YUV格式,其内存布局是:Y平面(宽×高字节)+ V平面(宽/2 × 高/2字节)+ U平面(宽/2 × 高/2字节),且U、V平面顺序与标准I420相反(YV12是Y-V-U,I420是Y-U-V)。而MediaCodec硬编码器(尤其是高通、联发科芯片的OMX.qcom.video.encoder.avc等组件)在4.4.2时期,只接受I420作为输入格式。这是硬性门槛:你不能直接把YV12丢给MediaCodec.configure(),它会抛IllegalStateException。所以第一步转换不是“优化”,而是“生存必需”。

第二层:I420转换的取舍逻辑
有人会问:为什么不直接用OpenGL ES做YUV纹理转换?太重。为什么不等Camera2 API?4.4.2不支持。所以项目采用纯Java层内存拷贝+通道交换:将YV12的Y平面原样复制,再把V平面数据拷贝到I420的U平面位置,U平面数据拷贝到I420的V平面位置。计算量可控(每帧约300KB内存操作),无JNI依赖,适配所有ARMv7设备。关键点在于:拷贝必须在Camera回调线程内完成,且必须用System.arraycopy()而非for循环——实测下来,for循环在低端机上单帧耗时超8ms,直接导致预览掉帧;arraycopy稳定在1.2ms内。

第三层:UDP推流而非RTMP的底层考量
项目没选RTMP,不是因为RTMP不好,而是因为RTMP需要完整的FLV封装、AMF序列化、握手协议栈,对入门者来说,光是理解connect()createStream()的交互就足够劝退。而UDP推流,本质就是“把H.264 Annex B格式的NAL单元,按顺序切成MTU大小(通常1400字节)的UDP包,发出去”。VLC只要收到连续的NALU(SPS/PPS + IDR/P帧),就能自动拼装解码。这里的关键设计是:编码器输出的ByteBuffer,必须按NALU边界切分。MediaCodec在4.4.2中不提供NALU边界标记,所以项目在编码回调里,用0x000001或0x00000001模式扫描buffer,手动提取每个NALU。这个扫描逻辑必须高效(位运算+缓存命中优化),否则会拖慢整个编码线程。

第四层:SurfaceView解码显示的生命周期绑定
SurfaceView的Surface和MediaCodec的outputSurface不是“设置一次就完事”。在4.4.2上,SurfaceView的Surface可能因Activity重建、横竖屏切换而销毁重建。项目通过重写SurfaceHolder.CallbacksurfaceCreated()/surfaceDestroyed(),在surface创建时调用MediaCodec.createInputSurface()获取新Surface,并在configure()前重新绑定;销毁时主动stop()/release()解码器。这个绑定过程如果遗漏,就会出现“解码器在跑,但SurfaceView黑屏”的经典问题。

提示:整个架构刻意规避了AsyncTask、HandlerThread等高级封装,所有核心逻辑(采集、编码、推流、解码)都运行在独立的子线程中,用while(true)+sleep(1)控制帧率,确保你能看清每一帧数据的流转路径。这不是最佳实践,但对学习者最友好——没有隐藏的线程调度,没有异步回调地狱。

2.2 模块职责划分与数据流向图

整个工程被拆成四个物理模块,对应四条清晰的数据流:

  • CameraCaptureModule:负责开启Camera、设置PreviewCallback、接收YV12帧。核心是onPreviewFrame(byte[] data, Camera camera)回调,它把原始字节数组交给转换模块。
  • I420ConverterModule:纯Java类,接收YV12 byte[],输出I420 byte[]。内部维护两个预分配的byte[]缓冲区(避免频繁GC),转换逻辑封装在convertYV12ToI420()方法中。
  • MediaCodecEncoderModule:持有MediaCodec实例,接收I420数据,执行queueInputBuffer()dequeueOutputBuffer()循环。关键点在于:dequeueOutputBuffer()返回的info.offsetinfo.size必须精确指向NALU起始位置,且需检查info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG以分离SPS/PPS。
  • UDPSenderModule / SurfaceViewRendererModule:双出口设计。UDPSender监听Encoder的outputBuffer,按NALU切片后DatagramSocket.send();SurfaceViewRenderer则在Surface创建后,用MediaCodec.createInputSurface()获取Surface,再configure()解码器,最后queueInputBuffer()喂入本地.h264文件读取的NALU。

数据流向不是单向管道,而是带反馈的闭环:Camera帧率(通常30fps)驱动整个流水线节奏;Encoder的dequeueOutputBuffer()阻塞时间反向影响Camera预览帧率;UDP发包失败不重传(流媒体特性),但会记录丢包数供调试;SurfaceView的surfaceChanged()触发解码器重配置。这种紧耦合,正是真实音视频开发的常态。

3. 核心细节解析与实操要点

3.1 YV12到I420转换:不只是字节顺序交换

YV12和I420都是4:2:0采样的Planar格式,但U/V平面存储顺序相反。YV12布局为:[Y][V][U],I420为:[Y][U][V]。转换看似简单,但有三个致命细节:

细节1:Y平面可直接复用,但U/V平面需跨步拷贝
Y平面尺寸为width * height,U/V平面尺寸均为(width/2) * (height/2)。但YV12的V平面起始地址是yOffset + ySize,U平面起始地址是yOffset + ySize + vSize;而I420的U平面起始地址是yOffset + ySize,V平面起始地址是yOffset + ySize + uSize。所以转换不是简单的memcpy(dstU, srcV, size),而是:

// 假设ySize = width * height, uvSize = (width/2) * (height/2) System.arraycopy(yv12Data, yOffset, i420Data, yOffset, ySize); // Y平面直拷 System.arraycopy(yv12Data, yOffset + ySize + uvSize, i420Data, yOffset + ySize, uvSize); // YV12的U → I420的U System.arraycopy(yv12Data, yOffset + ySize, i420Data, yOffset + ySize + uvSize, uvSize); // YV12的V → I420的V

注意:uvSize必须是整数,widthheight需向下对齐到2的倍数(Camera预览尺寸通常是偶数,但需在onPreviewFrame()中校验)。

细节2:字节对齐与padding处理
某些小米机型(如MIUI 5)的Camera输出,会在Y平面末尾添加额外padding字节(用于内存对齐)。若直接按width*height拷贝Y平面,会把padding也拷过去,导致I420数据错位。项目解决方案:在Camera.Parameters中显式设置setPreviewSize(w, h),并在onPreviewFrame()中用camera.getParameters().getPreviewSize()获取真实尺寸,严格按realWidth * realHeight拷贝Y平面,忽略padding

细节3:性能陷阱——避免对象创建
初版代码曾用ByteBuffer.allocateDirect()为每次转换创建新缓冲区,结果GC频繁,预览卡顿。优化后:在Converter类中预分配两个byte[]mI420BuffermYV12Buffer),大小按最大预览尺寸(如1280×720)预留,复用同一块内存。转换方法签名变为void convert(byte[] yv12, byte[] i420, int width, int height),彻底消除GC压力。

注意:I420转换必须在Camera回调线程内完成,且不能做耗时操作(如Log.d)。实测发现,在小米4上,若在onPreviewFrame()中加入Log.d("TAG", "frame"),单帧耗时从12ms飙升至35ms,直接导致预览掉帧。调试时应改用android.util.LogisLoggable()控制开关,或仅在首帧打日志。

3.2 MediaCodec硬编码配置:参数背后的硬件真相

MediaCodec.configure()的参数不是随便填的,每个值都对应芯片级能力:

MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); // 关键! format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // IDR帧间隔1秒 format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31);

参数深挖:
-COLOR_FormatYUV420Flexible:这是4.4.2时代的“万金油”格式。它告诉编码器:“我不在乎你内部用什么YUV变种,只要给我YUV420就行”。相比COLOR_FormatYUV420Planar(强制I420),它兼容性更好,高通芯片实测成功率100%。
-BIT_RATE:2Mbps是平衡画质与网络带宽的经验值。计算依据:1280×720@30fps的H.264 Baseline Profile,理论最小码率为width * height * fps * 0.15 ≈ 1280*720*30*0.15 ≈ 4.1Mbps,但实际芯片编码效率有限,2Mbps已能保证主体清晰。若设为5Mbps,小米4编码器会因缓冲区溢出而崩溃。
-PROFILELEVEL:Baseline Profile是必须的,因为4.4.2的软解码器(如Stagefright)对Main Profile支持不全;LEVEL 3.1对应最高分辨率为1280×720@30fps,完美匹配主流预览尺寸。

编码循环中的生死线:

// 编码主循环 while (mIsEncoding) { int inputIndex = mEncoder.dequeueInputBuffer(10000); // 10ms超时 if (inputIndex >= 0) { ByteBuffer inputBuffer = mEncoder.getInputBuffer(inputIndex); inputBuffer.clear(); inputBuffer.put(mI420Buffer); // 直接put整个I420数组 mEncoder.queueInputBuffer(inputIndex, 0, mI420Buffer.length, System.nanoTime() / 1000, 0); } MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int outputIndex = mEncoder.dequeueOutputBuffer(info, 10000); if (outputIndex >= 0) { ByteBuffer outputBuffer = mEncoder.getOutputBuffer(outputIndex); if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // SPS/PPS,单独处理,不送UDP handleCodecConfig(outputBuffer, info); } else { // 普通NALU,送UDP或存文件 sendNALU(outputBuffer, info); } mEncoder.releaseOutputBuffer(outputIndex, false); } }

关键点:dequeueInputBuffer()的超时设为10ms,而非-1(无限等待),防止Camera帧堆积导致OOM;queueInputBuffer()presentationTimeUs必须用System.nanoTime()计算,否则VLC播放会卡顿;releaseOutputBuffer()的第二个参数为false,因为我们用的是ByteBuffer模式,不是Surface模式。

3.3 UDP推流的健壮性设计:不只是send()那么简单

UDP本身无连接、不保证送达,但推流必须保证基本可用性:

设计1:MTU自适应切片
H.264 NALU长度不定,大帧(如IDR)可达50KB。UDP单包上限通常为1500字节(以太网MTU),实际有效载荷约1400字节。项目采用“NALU优先切片”:先提取完整NALU,再按1400字节切分成多个UDP包,每个包头部加2字节序列号(0,1,2…)和1字节NALU类型(0x01=非IDR,0x05=IDR)。接收端VLC能自动重组。

设计2:发包速率控制
若编码器瞬间输出大量NALU(如场景切换时),UDP发包会堵塞。项目在UDPSender中引入令牌桶算法:每1000ms生成30个令牌(对应30fps),每次send()消耗1个令牌。令牌不足时,Thread.sleep(1)等待,避免网络风暴。

设计3:错误隔离
UDP socket异常(如目标IP不可达)不能导致整个编码线程崩溃。项目将DatagramSocket.send()包裹在try-catch中,捕获IOException后仅记录log,继续处理下一帧。实测发现,当VLC未启动时,小米4会抛java.net.PortUnreachableException,但捕捉后不影响后续推流。

提示:测试UDP推流时,务必关闭手机防火墙(MIUI的“安全中心”常默认拦截UDP)。一个典型问题是:APK能编译运行,Camera预览正常,但VLC收不到任何包——八成是防火墙拦了。临时解决方案:在MIUI中找到“安全中心→授权管理→studyMediacodec→允许后台弹窗和网络访问”。

4. 实操过程与核心环节实现

4.1 环境准备与工程导入(Eclipse时代最后的倔强)

虽然Android Studio已是主流,但本项目为兼容4.4.2旧环境,仍基于Eclipse ADT构建。导入步骤必须严格:

  1. 安装JDK 1.7:Android 4.4.2编译要求JDK 7,JDK 8会导致dx工具报错“unsupported class file version”。
  2. 配置ADT Bundle:下载adt-bundle-windows-x86_64-20140702.zip(含Eclipse 4.3、ADT 23.0.2、SDK Tools 23.0.2),解压后启动eclipse.exe。
  3. 导入工程:File → Import → Existing Android Code into Workspace → 选择项目根目录(含AndroidManifest.xml的文件夹)→ 勾选“Copy projects into workspace” → Finish。
  4. 修复依赖:右键项目 → Properties → Java Build Path → Libraries → Add External JARs → 选择libs/android-support-v4.jar(项目自带)→ OK。
  5. 设置Target SDK:Properties → Android → Project Build Target → 选择“Android 4.4.2 (API 19)” → Apply。

常见问题:导入后R.java报错。解决方案:Project → Clean → Clean all projects → 勾选“Start a build immediately” → OK。若仍有错误,检查res/values/strings.xml是否有中文乱码(用记事本另存为UTF-8无BOM格式)。

4.2 摄像头采集与预览配置实战

核心代码在CameraPreviewActivity.java中:

private void initCamera() { try { mCamera = Camera.open(); // 打开后置摄像头 Camera.Parameters params = mCamera.getParameters(); List<Camera.Size> sizes = params.getSupportedPreviewSizes(); Camera.Size optimalSize = getOptimalPreviewSize(sizes, 1280, 720); // 选最接近1280x720的尺寸 params.setPreviewSize(optimalSize.width, optimalSize.height); params.setPreviewFormat(ImageFormat.YV12); // 强制YV12,避免NV21 mCamera.setParameters(params); // 设置预览回调 mCamera.setPreviewCallback(new Camera.PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { if (mIsEncoding && mI420Converter != null) { mI420Converter.convert(data, mI420Buffer, optimalSize.width, optimalSize.height); mEncoder.feedFrame(mI420Buffer); // 推入编码器 } } }); mCamera.setPreviewDisplay(mSurfaceHolder); // 绑定SurfaceView mCamera.startPreview(); // 启动预览 } catch (Exception e) { Log.e(TAG, "initCamera failed", e); } }

关键实操技巧:
-getOptimalPreviewSize()算法:遍历supportedPreviewSizes,找width*height最接近目标面积(如1280×720=921600)且width/height≈16/9的尺寸。小米4返回的最优解通常是1280×720或960×720。
-setPreviewFormat(ImageFormat.YV12)必须显式调用,否则某些机型默认NV21,导致I420转换失败。
-setPreviewDisplay()必须在startPreview()之前调用,否则预览黑屏。

4.3 MediaCodec编码器初始化与NALU提取

编码器初始化在MediaCodecEncoder.java中:

public void prepare(int width, int height) { try { mMediaCodec = MediaCodec.createEncoderByType("video/avc"); MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31); mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mMediaCodec.start(); } catch (Exception e) { Log.e(TAG, "prepare encoder failed", e); } }

NALU提取的硬核实现:
MediaCodec输出的ByteBuffer是Annex B格式(0x000001或0x00000001起始),但dequeueOutputBuffer()返回的info.offset指向buffer起始,info.size是整个输出长度。项目用以下方法提取每个NALU:

private void extractNALUs(ByteBuffer buffer, MediaCodec.BufferInfo info) { byte[] data = new byte[info.size]; buffer.get(data, info.offset, info.size); // 复制到byte[]便于扫描 int pos = 0; while (pos < data.length - 4) { // 查找0x000001或0x00000001 if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 1) { int nalStart = pos + 3; int nalEnd = findNextStartCode(data, pos + 3); if (nalEnd > nalStart) { byte[] nal = new byte[nalEnd - nalStart]; System.arraycopy(data, nalStart, nal, 0, nal.length); sendNALU(nal); // 发送此NALU } pos = nalEnd; } else if (data[pos] == 0 && data[pos+1] == 0 && data[pos+2] == 0 && data[pos+3] == 1) { int nalStart = pos + 4; int nalEnd = findNextStartCode(data, pos + 4); if (nalEnd > nalStart) { byte[] nal = new byte[nalEnd - nalStart]; System.arraycopy(data, nalStart, nal, 0, nal.length); sendNALU(nal); } pos = nalEnd; } else { pos++; } } } private int findNextStartCode(byte[] data, int start) { for (int i = start; i < data.length - 3; i++) { if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) { return i; } if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 0 && data[i+3] == 1) { return i; } } return data.length; }

此实现虽非最优(可优化为状态机),但胜在清晰可靠,实测在小米4上单帧NALU提取耗时<0.5ms。

4.4 SurfaceView硬解播放全流程

解码播放在VideoPlayerActivity.java中:

private void initPlayer() { mSurfaceView = findViewById(R.id.surface_view); mSurfaceHolder = mSurfaceView.getHolder(); mSurfaceHolder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { initDecoder(holder.getSurface()); // Surface创建时初始化解码器 } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Surface尺寸变化,可重置解码器 } @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseDecoder(); // Surface销毁时释放解码器 } }); } private void initDecoder(Surface surface) { try { mMediaCodec = MediaCodec.createDecoderByType("video/avc"); MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1280, 720); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); mMediaCodec.configure(format, surface, null, 0); mMediaCodec.start(); // 读取本地.h264文件,逐NALU喂入 FileInputStream fis = new FileInputStream("/sdcard/test.h264"); parseAndFeedH264(fis); } catch (Exception e) { Log.e(TAG, "initDecoder failed", e); } }

关键点:
-configure()的第二个参数是Surface(来自SurfaceView),不是null,这是硬解的关键。
-parseAndFeedH264()需按Annex B格式解析.h264文件,提取每个NALU,调用queueInputBuffer()送入解码器。
- 解码循环中,dequeueOutputBuffer()返回后,releaseOutputBuffer(index, true)的第二个参数必须为true,表示渲染到Surface。

5. 常见问题与排查技巧实录

5.1 典型问题速查表

问题现象可能原因排查步骤解决方案
Camera预览黑屏,但无CrashSurfaceView未正确绑定Camera1. 检查mCamera.setPreviewDisplay(mSurfaceHolder)是否在startPreview()前调用
2. 在surfaceCreated()中加Log确认回调触发
确保setPreviewDisplay()startPreview()之前;检查SurfaceHolder是否为空
VLC收不到任何流,显示“no data received”UDP包被防火墙拦截或目标IP错误1. 用adb logcat | grep UDPSender看是否打印“send packet”
2. 在电脑端用Wireshark抓包,过滤UDP端口
关闭MIUI安全中心防火墙;确认VLC打开的地址是udp://@:8080(非rtsp://
SurfaceView解码画面撕裂、卡顿解码器输出帧率与Surface刷新率不匹配1. 在onOutputBufferAvailable()中打印info.presentationTimeUs差值
2. 检查queueInputBuffer()presentationTimeUs是否递增
在解码循环中,用System.nanoTime()计算每帧时间戳,确保严格递增;启用SurfaceView.setZOrderOnTop(true)避免Surface叠加问题
编码器configure()抛IllegalStateExceptionColorFormat不被芯片支持1. Log打印MediaCodecList所有编码器支持的ColorFormat
2. 尝试COLOR_FormatYUV420PlanarCOLOR_FormatYUV420PackedSemiPlanar
改用COLOR_FormatYUV420Flexible;降级到AVCProfileBaselineAVCLevel31
APK安装失败,提示“INSTALL_FAILED_CPU_ABI_INCOMPATIBLE”APK包含x86 so库,但手机是ARM1. 解压APK,查看lib/目录下so库架构
2.adb shell getprop ro.product.cpu.abi查看手机ABI
重新编译APK,只保留armeabi-v7a目录;或在build.gradle中指定ndk.abiFilters 'armeabi-v7a'

5.2 独家避坑技巧

技巧1:用Logcat定位MediaCodec状态机卡死
MediaCodec内部是状态机(Uninitialized → Configured → Running → …),卡在某状态会导致整个流程停滞。在关键节点加Log:

Log.d(TAG, "Encoder state: " + mMediaCodec.getOutputBuffers().length); // Running状态才非空 Log.d(TAG, "Input buffer count: " + mMediaCodec.getInputBuffers().length);

getInputBuffers()返回null,说明configure()失败;若getOutputBuffers()长期为0,说明编码器未start()。

技巧2:H.264文件录制与播放的时序对齐
项目提供recordH264()方法将NALU写入文件,但直接用VLC播放可能卡顿。原因是文件缺少时间戳。解决方案:在.h264文件头部写入4字节魔数0x00000001,然后每个NALU前加4字节长度(大端序),VLC即可按长度解析。项目H264FileWriter.java已实现此格式。

技巧3:小米机型特有的“省电模式”干扰
MIUI的“神隐模式”会杀死后台Service,导致UDP推流中断。实测发现,即使APP在前台,若用户锁屏超过1分钟,推流会停止。解决方案:在AndroidManifest.xml中为CameraPreviewActivity添加android:keepScreenOn="true",并在Activity中调用getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON),强制保持屏幕常亮。

技巧4:快速验证硬编码是否生效
不用VLC,用最原始方法:在onOutputBufferAvailable()中,将每个NALU的info.size累加,计算平均每秒输出字节数。若稳定在200KB/s(2Mbps),说明编码器在工作;若为0或忽高忽低,说明输入数据未正确送入。

最后分享一个小技巧:当你改完代码,APK安装后Camera预览还是黑屏,别急着重装。先adb shell input keyevent KEYCODE_POWER灭屏再亮屏,强制SurfaceView重建Surface——很多黑屏问题,其实是Surface生命周期没处理好,重启Surface就能解决。这是我踩了三次坑后,写在便签贴在显示器边上的第一条。

本文还有配套的精品资源,点击获取

简介:直接在Android设备上跑通的端到端视频处理链路:调用系统摄像头实时采集YV12帧,转I420格式后送MediaCodec硬编码为H.264裸流;编码数据支持双路径输出——一路通过UDP协议实时推送到指定IP和端口,可用VLC等播放器直接拉流观看;另一路可读取本地.h264文件进行硬解码,并将解码画面渲染到SurfaceView显示。整套代码已在Android 4.4.2系统(小米机型)完成实测验证,包含完整APK(studyMediacodec.apk)、Java源码(src/com/目录下)、资源文件(res/各密度drawable、layout、values等)、配置清单(AndroidManifest.xml)、构建配置(project.properties、proguard-project.txt)及必要依赖库(android-support-v4.jar)。项目结构清晰,兼容Eclipse开发环境,导入即编译,无需额外配置或修改即可运行调试。适合用于理解Android平台MediaCodec硬编解码工作流程、验证摄像头采集与网络传输协同逻辑、搭建轻量级实时视频传输原型。


本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/13 11:10:01

水如何打破数学:物理约束下的数值奇点搜索

1. 项目概述&#xff1a;一场用物理直觉重写数学边界的硬核探索 “Does Water Break Math?”——这个标题乍看像一句带着调侃的哲学发问&#xff0c;实则是一次严肃到近乎偏执的科学实践。它不是在质疑数学的逻辑根基&#xff0c;而是在追问&#xff1a;当真实世界的物理约束&…

作者头像 李华
网站建设 2026/6/13 11:06:47

蓝屏后不重装系统也能继续用的小工具(带图形安装向导)

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;Windows突然蓝屏别急着重装&#xff0c;这个轻量小工具专治由驱动冲突、内存访问异常或系统文件损坏引发的典型蓝屏&#xff0c;比如0x0000007E、0x0000003B等错误代码。它不替换系统核心文件&#xff0c;也不修…

作者头像 李华
网站建设 2026/6/13 10:59:06

2026图片去水印工具推荐指南

无论是收藏喜欢的视频片段&#xff0c;还是整理学习素材时遇到画面上的水印&#xff0c;总让人有点头疼。市面上工具虽多&#xff0c;但真正好用、免费、不伤画质的却要花点心思找。今天这篇教程就从个人用户的真实需求出发&#xff0c;分享几款2026年实测好用的去水印工具&…

作者头像 李华