☰
Android音视频开发实战:MediaCodec与Camera2全链路录制与避坑指南
2026/10/15 4:48:17 网站建设 项目流程

简介:这是一份面向Android音视频开发者的PDF学习资料,系统梳理了从视频帧、帧率、码率、分辨率到IPB帧等核心概念,并结合FFmpeg脚本演示抽帧、转GIF、码率控制等实操方法。文件为单个PDF文档,大小6.39MB,内容精炼,适合需要快速建立音视频知识框架的初中级开发者阅读。目前已有1213人学习下载。资料还介绍了Android多媒体框架(MediaCodec、AudioTrack、Surface)与FFmpeg Android版本的使用思路,帮助读者将理论应用到实际编码、解码、播放和录制场景中。学习后可理解CBR/VBR/CVBR/AVBR码率模式差异、I/P/B帧与GOP的意义,为后续优化画质与体积打下基础。

1. 这份PDF最反直觉的地方:音视频技术不是“看懂”的,是“跑通”的

拿到《Android音视频开发与处理.pdf》这类资料,多数人的第一反应是找目录、按章节顺序从头读。我建议先放下这个习惯。做音视频开发有个反直觉的规律:把这份PDF从头到尾读三遍,不如照着它梳理的链路在真机上跑通一条录制管线。原因是音视频每一环都互相咬合——采集分辨率、编码码率、封装时间戳、渲染Surface,任何一个参数错位,程序都不会报错,只是“画面不对”:首帧泛绿、声音比画面快半秒、帧率忽高忽低。这类问题在纸面上推演不出来,只能靠真机验证。这篇笔记就按这份PDF的地图,把采集、编码、封装、渲染的落地路径讲透,包括参数怎么设、坑踩在哪,以及怎么用工程手段确认链路真的通。适合想做实录、直播、视频编辑底座的开发者,也适合被播放器卡了半年想往上层走的人。

2. 从PDF目录反推音视频核心链路:采集到渲染每一环在解决什么问题

不管这份PDF的目录怎么编排,底层都绕不开一条主线:采集 → 编码 → 封装 → 渲染。先建立整体坐标,后面跑工程时才知道自己站在链路的哪一段,报错时也知道该去哪一段找原因。这四个环节对应的Android系统能力其实高度集中,核心就几个类,但每个类的职责边界很容易被混淆。

2.1 采集层:Camera和AudioRecord在链路上的职责边界

采集层负责把物理世界的画面和声音变成原始数据。画面走Camera2,产出的是Image对象,像素格式通常是YUV_420_888;声音走AudioRecord,产出的是PCM裸流。两者在链路上的职责只有一个:产帧,不碰编码。

很多人在这里犯的第一个错,是把采集和编码混在一个回调里做。比如在ImageReader的onImageAvailable回调里直接同步做编码和写文件,结果就是采集帧率被编码拖慢,画面开始一顿一顿地动。采集回调应该只做一件事:拿帧,入队,然后立刻close掉Image。Image不close,缓冲区池会耗尽,预览会莫名其妙卡死,这个问题我在真机上排查过一整天才定位到。

AudioRecord这边容易被忽略的是bufferSize。官方提供了getMinBufferSize(),但那个值只是下限,直接用它做缓冲区,在高负载下很容易出现overrun,声音变成“咔哒咔哒”的断音。我一般会取最小值的三倍左右做实际缓冲区,让采集线程有足够的喘息空间。采样率用44100或48000,声道数、PCM位深,这些参数要和后面编码器期望的输入完全一致,不一致时不会报错,只是声音变调或速度不对,非常难查。

2.2 编码层:MediaCodec为什么是绕不开的中心

Android上做编码,绕不开MediaCodec。它既是硬编的入口,也是软编的统一抽象。很多刚从FFmpeg转过来的人会下意识想自己集成x264,不是说不行,而是兼容性和功耗很难控制:x264跑在CPU上,720p@30fps的实时编码就能把中端手机跑热,发热降频后又反过来拖累采集帧率。硬编虽然画质调校空间小,但胜在低功耗、低延迟,而且Android厂商的系统分发时会带各自的硬件编码器,MediaCodec帮你做了一层统一调度。

MediaCodec是一个典型的状态机:Uninitialized → Configured → Executing。configure之后必须start,start之后才能dequeueInputBuffer和dequeueOutputBuffer;stop之后不能再次start,只能release重建。对这个状态机不熟的人,最常见的问题是多个线程同时在dequeue,触发IllegalStateException,这是一个典型的并发边界:MediaCodec本身不是线程安全的,同步模式下所有dequeue操作必须集中在同一个循环里。

同步模式还是异步模式?我的建议是单路录制场景先用同步模式。API 21之后有异步回调onInputBufferAvailable、onOutputBufferAvailable,看起来很优雅,但它引入了背压问题:编码器输出突然变快时,回调里如果还在做IO写文件,就会卡住编码线程,继而影响采集。同步模式虽然代码啰嗦,但循环里每一步都可以控制超时,排错路径清晰得多。

2.3 渲染层:SurfaceView、TextureView和Surface的三角关系

渲染层是新手最懵的部分,因为牵扯到三个长得像的东西:SurfaceView、TextureView、Surface。

Surface是一块可绘制的内存区域,背后是BufferQueue,生产者和消费者通过它交换图像数据。SurfaceView是自带独立Surface的View,它不在ViewGroup的普通绘制流程里,性能好、延迟低,适合相机预览、视频播放。TextureView是普通View,内部靠SurfaceTexture充当生产者和View绘制之间的桥梁,可以平移缩放旋转,但每帧多一次合成,延迟和功耗略高。

选型上,我一般遵循一个原则:只做上屏显示用SurfaceView,要做动画变换或嵌套进复杂布局再用TextureView。还有一个容易忽略的用法是,Surface可以直接作为MediaCodec的输入和输出——输入Surface模式(input surface)可以让Camera直接通过Surface送帧给编码器,绕开YUV手动拷贝;但这也意味着你要理解整条Surface链路才能调好,新手阶段先用ImageReader加ByteBuffer的方式,等链路通了再优化成Surface输入。

链路环节核心类典型数据格式最容易出错的地方
采集Camera2 / AudioRecordYUV_420_888 / PCM 16bit帧率与分辨率不匹配
编码MediaCodecH.264 / AAC时间戳不单调
封装MediaMuxerMP4addTrack与start顺序
渲染SurfaceView / TextureViewRGB / YUVSurface生命周期管理

3. 把PDF变成可跑工程:用MediaCodec实现一条录制链路的最小闭环

纸上谈兵到此为止。我一般建议先做“预览+录像”这条线,它能同时验证采集、编码、封装三段链路。完整工程要考虑的东西很多,但最小闭环可以拆成三段:Camera2预览、编码循环、MediaMuxer写MP4。下面是能落地的写法,代码基于API 21以上,按同步模式组织。

3.1 建立预览:Camera2 + SurfaceView的最小配置

Camera2的openCamera回调里,要做两件事:创建CameraCaptureSession,并同时绑两个Surface——一个给预览用,一个给ImageReader用。ImageReader是录像的数据源,格式设成YUV_420_888,尺寸必须和编码器输入尺寸一致,我这里统一存储到成员变量width、height。

// 3.1 预览 + 录像:Camera2 打开后的最小会话 private void openCamera() { CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); try { String cameraId = manager.getCameraIdList()[0]; // 实战中要挑后摄 CameraCharacteristics chars = manager.getCameraCharacteristics(cameraId); // 传感器方向,竖屏预览要拿它来旋转画面 int sensorOrientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION); // 录像帧源:YUV_420_888,尺寸按编码器对齐到16的倍数 mImageReader = ImageReader.newInstance( width, height, ImageFormat.YUV_420_888, 3); mImageReader.setOnImageAvailableListener(mOnImageAvailable, mHandler); manager.openCamera(cameraId, mStateCallback, mHandler); } catch (CameraAccessException | SecurityException e) { Log.e("TAG", "openCamera failed", e); } }

这段代码的关键点是ImageReader的maxImages参数。设2是最小值,但编码线程一旦因为GC或写文件抖动,采集侧的Image就会堆积,队列满了之后预览直接“假死”,所以我通常设3。sensorOrientation拿到的值用来计算预览和录像的旋转角度,Camera传感器默认是横置的,不做旋转处理,竖屏拍出来的视频旋转信息会丢,后面避坑章再展开。

3.2 编码循环:MediaCodec的同步模式怎么写才不丢帧

编码器初始化的核心是MediaFormat。mime用video/avc,颜色格式优先选ColorFormatYUV420Flexible,因为Camera2输出到Image里的YUV_420_888是flexible布局,兼容性最好。如果写死COLOR_FormatYUV420SemiPlanar,部分设备的编码器会直接拒绝configure。

// 3.2 编码器初始化 MediaCodec mEncoder = 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, 4_000_000); format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 秒 format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31); mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mEncoder.start();

参数说明:4Mbps码率是720p@30fps的保守起步值,画面复杂度高的场景可以提到6Mbps;I帧间隔2秒适合录播,直播再缩到1秒;Profile用Baseline兜底,兼容性最好,老解码器也不会花屏;Level31对应720p@30fps的规格上限,超出这个规格的设备上configure阶段就会失败。

编码循环分两个方向:入队是往里送原始YUV帧,出队是往外取编码后的H.264流。入队时要特别注意,Image用完必须close,YUV转换必须在入队前完成。

// 3.2 每一帧 YUV(已转 NV12)送入编码器 int inIndex = mEncoder.dequeueInputBuffer(10_000); if (inIndex >= 0) { ByteBuffer buffer = mEncoder.getInputBuffer(inIndex); buffer.clear(); buffer.put(nv12Data); // nv12Data 来自 Image 转换 // pts 必须单调递增,单位是微秒 long ptsUs = (System.nanoTime() - mBaseTimeNs) / 1000; mEncoder.queueInputBuffer(inIndex, 0, nv12Data.length, ptsUs, 0); }

这里用System.nanoTime()减一个起始时间作为时间戳来源,单位是微秒。不要用帧号乘1000/帧率去估算PTS,因为实际采集帧率一直在抖动,估算出来的时间戳必然导致音画不同步。

出队循环要把编码后的数据交给MediaMuxer。关键帧的Buffer前面会带SPS/PPS信息,这部分MediaMuxer在addTrack时已经从outputFormat里取走了,写数据时不要手动拼头,手动拼了反而会花屏。

// 3.2 编码输出循环 MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); int outIndex = mEncoder.dequeueOutputBuffer(info, 10_000); if (outIndex >= 0) { ByteBuffer outData = mEncoder.getOutputBuffer(outIndex); // 关键帧的 csd 信息由 MediaMuxer 在 addTrack 时自动处理 mMuxer.writeSampleData(mVideoTrackIndex, outData, info); mEncoder.releaseOutputBuffer(outIndex, false); }

3.3 MediaMuxer封装:addTrack与时间戳的唯一原则

MediaMuxer负责把编码后的H.264流封装成MP4文件。它有一个严格的顺序要求:addTrack必须先于start,start之后才能writeSampleData。addTrack时传入的是编码器的outputFormat,里面带着SPS/PPS等编解码信息,MediaFormat的csd-0和csd-1字段会被Muxer自动写到文件头里。

// 3.3 MediaMuxer 封装 mMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); mVideoTrackIndex = mMuxer.addTrack(mEncoder.getOutputFormat()); mMuxer.setOrientationHint(rotation); // 竖屏录像必须设置 mMuxer.start();

setOrientationHint要在start之前调用。rotation来自Camera2的sensorOrientation和DisplayRotation换算,不设置的话,竖屏拍出来的视频在播放器里是横着的。这段逻辑本身不复杂,但顺序错了会直接抛RuntimeException,定位起来像玄学。

收尾顺序也容易踩坑:先stop编码器,再stop Muxer,最后release。如果先stop Muxer,后面再往里面写数据就会抛异常;如果编码器还没产出剩余帧就stop Muxer,文件尾部会缺数据,播放时最后几帧丢或花。我一般会先调用mEncoder.stop(),然后mMuxer.stop(),最后mMuxer.release()。

YUV_420_888转NV12是这个闭环里绕不开的转换,直接贴一下核心搬运逻辑。Camera2出来的Image,每个plane的rowStride很可能大于图像宽度,也就是每行末尾有padding,不能直接array()整体拷贝,必须按行拷:

// 3.2 YUV_420_888 -> NV12:逐行搬运,处理 rowStride/pixelStride private byte[] yuv420888ToNV12(Image image) { Image.Plane[] planes = image.getPlanes(); int w = image.getWidth(), h = image.getHeight(); byte[] nv12 = new byte[w * h * 3 / 2]; // Y 平面 ByteBuffer yBuf = planes[0].getBuffer(); int yRowStride = planes[0].getRowStride(); int pos = 0; for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { nv12[pos++] = yBuf.get(row * yRowStride + col); } } // UV 平面:半平面 V、U 交错成 NV12 布局 ByteBuffer uBuf = planes[1].getBuffer(); ByteBuffer vBuf = planes[2].getBuffer(); int uRowStride = planes[1].getRowStride(); int vRowStride = planes[2].getRowStride(); for (int row = 0; row < h / 2; row++) { for (int col = 0; col < w / 2; col++) { nv12[pos++] = vBuf.get(row * vRowStride + col); nv12[pos++] = uBuf.get(row * uRowStride + col); } } return nv12; }

这段代码假设pixelStride为1,很多设备上U/V平面pixelStride是2,实战代码要读planes[1].getPixelStride()做跳步处理,否则颜色会错乱。这份PDF里如果只是讲概念不贴这个细节,真机调试时你一定会卡在这一步。

4. 决定成败的6个编码参数:码率、帧率、GOP、Profile、颜色格式怎么设

编码参数是音视频开发里最像“玄学”的领域,因为改一个值,画质可能没变化,但文件体积变大一倍;或者某台设备上一切正常,换个芯片直接花屏。下面这张表是720p录播场景的起步配置,后面逐项展开为什么这么设、失控时坏在哪。

参数含义720p录播起步值失控后果
码率每秒编码字节数4Mbps低码率高帧率→花屏
帧率每秒编码帧数30运动画面顿挫
GOP两个关键帧间隔2秒拖拽/seek困难
Profile编码特性集BaselineHigh在老设备花屏
Level分辨率和帧率上限31超规格初始化失败
颜色格式YUV排列方式Flexible写错颜色错乱或绿屏

4.1 码率和帧率:清晰度与体积不是唯一约束

码率决定每一秒钟有多少比特分配给画面,帧率决定画面每秒更新多少次。这两个参数放在一起看才有意义:同样4Mbps,30fps下每帧约133Kb,15fps下每帧约266Kb,后者单帧质量更高但运动不流畅;反过来,同样30fps,码率压到1.5Mbps,静态场景勉强能看,镜头一甩就是满屏马赛克和花屏。

动态场景的码率需求会指数上升,因为H.264的帧内压缩在画面剧烈变化时会产生大量残差数据。拍人的场景都算复杂,我一般会比起步值上浮50%。CBR还是VBR也要在此刻定:CBR码率平稳,适合推流和实时传输,但静态场景浪费比特;VBR在静态时省码率、动态时冲高,适合本地录播。MediaCodec里通过KEY_BITRATE_MODE控制,录播我选VBR,直播选CBR,避免上行带宽抖动。

4.2 GOP与Profile/Level:兼容性由关键帧间隔决定

GOP是I帧(关键帧)之间的间隔。I帧是全量编码,体积大,P帧依赖前面的帧才能解码。间隔设2秒意味着播放器最多等2秒就能找到可解码的入口点,seek时也是先跳到最近的关键帧再往后解码。直播场景要缩到1秒,甚至更短,因为丢包后要等关键帧到达才能恢复画面,间隔太长就是长时间黑屏或花屏。本地录播间隔可以适当拉长到3到4秒,文件体积更小,但拖进度条时定位变慢且解码负担大。

Profile和Level解决的是“编码出来的流别人能不能解”。Baseline不支持B帧,解码器负担小,兼容性最好,我一般拿它当兜底。Main和High画质更好,但一些老设备硬件解码器不支持,播放时直接花屏或只有声音没有画面。Level对应规格上限,AVCLevel31是720p@30fps的档位,如果设了1080p却用Level30,编码器初始化就会失败。

4.3 颜色格式:YUV420的三种包装不能混

YUV420的“420”指色度采样,但同一采样格式有不同包装:I420是Y后跟整个U平面再跟整个V平面,NV12是Y后跟UV交错排列,NV21是Y后跟VU交错。三者数据量一样,排列顺序不同。Camera2输出的YUV_420_888是flexible格式,具体底层布局因设备而异,编码器期望的又往往是NV12或I420。

这就带来了一个很隐蔽的坑:设备A上你的转换代码工作正常,因为它的底层恰巧是半平面;设备B上底层布局变了,颜色就出现红蓝对调或整体偏绿。所以不能赌设备的默认布局,必须在运行期读取planes数组的rowStride和pixelStride,动态决定搬运方式。还有颜色范围的问题:部分编码器输出VideoRange,部分输出FullRange,混用时画面会灰蒙蒙的,对比度发灰,这个在MediaFormat里也可以设置KEY_COLOR_RANGE,如果确认播放端支持的色域后仍然发灰,就去查这个字段。

5. 音视频开发踩坑清单:从黑匣子到能定位问题的排查路径

音视频技术最折磨人的不是概念难,而是出错时症状和原因离得太远。下面这些坑是我在真机上一个个踩过的,每条都按现象、原因、解决三个步骤记录,希望能帮你减少几天的黑匣子排查。

5.1 首帧泛绿或花屏

现象:录制文件开头一小段是绿色噪点画面,后面逐渐正常,或者整个文件在某个播放器里全都花屏。

原因:H.264流需要有SPS和PPS参数集才能解码,这部分信息通常由MediaCodec在关键帧输出时以csd-0、csd-1字段带出。MediaMuxer在addTrack时自动把它们写进MP4的文件头和解码器描述里,但如果你在writeSampleData时手动了拼接、或者用了错误的outputFormat做addTrack,解码器拿到的SPS/PPS就是错的,首帧甚至前几秒就解不出来。

解决:addTrack时严格使用mEncoder.getOutputFormat(),不要自己new MediaFormat拼参数。writeSampleData只负责写码流数据,不要手动往sample前头塞SPS/PPS。想确认csd有没有正确写入,可以用MediaExtractor读出视频轨的格式,检查csd-0、csd-1是否非空。

5.2 音画不同步

现象:声音比画面快几百毫秒,或反过来。最糟糕的是随时间推移偏移量越来越大,前10秒感觉不到,1分钟后差了一秒。

原因:时间戳来源不统一。视频帧的PTS来自采集回调里某个单调时钟,音频帧的PTS来自AudioRecord的frame count换算,两个独立时钟本来就有漂移。还有人用System.currentTimeMillis()做时间戳,它受系统时间修改、NTP校时影响会跳变,造成PTS回退,解码器一见到回退就会丢帧或错序。

解决:所有帧的时间戳基于同一个单调时钟源。标准做法是录制开始那一刻取一次System.nanoTime()作为基准,之后每帧的PTS都是当前nanoTime减基准然后除以1000转成微秒,视频和音频共用这一个基准。不要用帧号乘帧间隔去反推,采集帧率从来不稳定。

5.3 MediaCodec buffer泄漏和死锁

现象:录制一段时间后画面卡死,日志里dequeueInputBuffer一直返回-1,或者ANR,问题随机出现,压测时必现。

原因:同步模式里所有dequeue和queue的操作必须集中在同一个线程。如果ImageReader的回调线程和编码循环线程同时访问编码器,或者有人在主线程直接做编码循环,一旦编码输出挤压,主线程就被dequeueOutputBuffer阻塞,整个App无响应。

解决:编码循环放独立线程,ImageReader回调里只做入队或只把YUV发到队列,绝不在回调里直接dequeue。dequeueOutputBuffer传一个有限的超时值,比如10000微秒,返回-1表示超时,此时应该循环继续而不是无限等。还有一点,getInputBuffer拿到ByteBuffer后,即便这一帧不需要编码也要queueInputBuffer或clear,否则buffer池被占满,dequeueInputBuffer永远拿不到可用的输入槽。

5.4 硬编兼容性与厂商差异

现象:同一份代码,A品牌的设备录出来一切正常,B品牌的设备录出来的视频在电脑上播放花屏,或者在C设备上颜色全部偏绿。

原因:硬件编码器是各家芯片厂商提供的,MediaCodec只是统一接口,底层对颜色格式的支持、码率控制精度、Profile/Level的实现细节差异很大。比如某些设备只支持NV12,不认I420;某些设备编码器的VBR实现不严谨,码率会冲到设定值的两倍。

解决:在configure之前先用MediaCodecList跑一次能力探测,确认设备支持的color format并按优先级选择。兼容性优先时,Profile用Baseline,Level不要想当然,按实际分辨率去匹配。颜色偏绿优先怀疑YUV转换代码写死了布局,改成运行期读pixelStride和rowStride。这类问题没有银弹,只能靠设备矩阵测试来兜底。

5.5 旋转信息丢失,竖屏录的横着放

现象:手机竖屏录出来的MP4,在电脑上或某些播放器里是横的,画面像被转了90度。

原因:Camera传感器默认是横置的,Image输出画面本来就是横的。竖屏拍摄时,旋转处理通常靠两件事完成:一是预览渲染时用Surface的变换矩阵把画面转正,二是把最终旋转角度写进视频文件的metadata。录像链路只做了第一个,漏了第二个,文件本身没有记录旋转信息,播放器自然按原样播。

解决:在MediaMuxer.start()之前调用setOrientationHint(rotation),rotation由sensorOrientation加Display.getRotation()计算得出。注意setOrientationHint只影响播放器的旋转展示,如果想在视频编码时就转正画面,需要额外在编码前做旋转处理,那又是另一套矩阵逻辑。最省事的可靠方案是前者:录横的,写旋转metadata,让播放器负责转。

6. 从能录到能放:用工程手段验证你的音视频链路是否真的通

6.1 用PTS检查代替肉眼验证

“能播放”不等于“链路正确”。肉眼只能看出有没有画面,看不出来时间戳是否单调、关键帧间隔是否合理、首帧是否丢了SPS/PPS。我拿到一个录完的文件,第一步是抽PTS检查,而不是双击播放器。用MediaExtractor把每一帧的PTS和关键帧标志打出来,看看有没有回退、间隔是否均匀:

// 用 MediaExtractor 检查 MP4 的时间戳与关键帧分布 MediaExtractor extractor = new MediaExtractor(); extractor.setDataSource(path); MediaFormat format = extractor.getTrackFormat(videoTrackIndex); extractor.selectTrack(videoTrackIndex); long lastPts = -1, lastKeyFramePts = -1; MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); while (true) { info.size = extractor.readSampleData(buffer, 0); if (info.size < 0) break; info.presentationTimeUs = extractor.getSampleTime(); info.flags = extractor.getSampleFlags(); if (info.presentationTimeUs < lastPts) { Log.w("CheckPts", "PTS 回退: " + info.presentationTimeUs + " < " + lastPts); } long interval = info.presentationTimeUs - lastPts; if (lastPts > 0 && (interval > 200_000)) { Log.w("CheckPts", "帧间隔异常: " + interval + "us"); } if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { if (lastKeyFramePts > 0) { long gop = (info.presentationTimeUs - lastKeyFramePts) / 1_000_000; Log.i("CheckPts", "GOP 间隔: " + gop + "s"); } lastKeyFramePts = info.presentationTimeUs; } lastPts = info.presentationTimeUs; extractor.advance(); }

PTS回退、帧间隔突变、GOP间隔和设定值明显不符,三条里任何一条命中,都说明链路里有隐藏问题,这条链路后面的诸如合流、推流都做不稳。

6.2 从录制管线延伸出去的三条进阶路

链路确认没问题后,就可以往三个方向延伸了:一是把ByteBuffer入队改成input surface模式,让Camera通过Surface直接送帧给编码器,省掉YUV手动拷贝,性能明显提升,但要对Surface生命周期非常小心;二是接AudioRecord加AAC编码,把音频轨合进同一个MediaMuxer,验证多轨时间戳对齐;三是把渲染层从SurfaceView升级到TextureView加OpenGL ES,在编码前插入滤镜或特效,那就进入了视频编辑的范畴。

我现在的习惯是,任何一段新写的音视频代码,先跑一个最小的录制用例,跑完立刻抽PTS,查关键帧间隔,再做改动。这套流程帮我过滤掉了大量“看着能跑”的假象。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询