首页 / 资讯中心 / 文章详情

MediaRecorder暂停录制兼容性差?分段录制+MediaMuxer合成更稳

MediaRecorder暂停录制兼容性差?分段录制+MediaMuxer合成更稳 ★ FEATURED ARTICLE
简介面向Android中高级开发者的MediaRecorder录制视频示例工程核心解决视频录制过程中的暂停与继续操作并针对竖屏4:3录制时预览画面横向显示的问题给出了处理方案同时附带录制完成后用SurfaceView播放视频的完整实现实例包含可运行代码适合有一定Java基础的开发者直接参考。资源共1098个文件压缩包8.72MB其中png、xml、json主要承担界面资源与配置数据class、java为业务源码jar包含了isoviewer-1.0-RC-27等依赖库另有aidl、apk、gradle等文件构成完整Android工程结构便于在Android Studio中运行调试。已有2138人学习下载。通过该demo可掌握MediaRecorder状态机切换与暂停/恢复的关键写法理解竖屏预览旋转修正和录制尺寸设定的思路并借助isoviewer辅助检查mp4封装结构无论是开发短视频、录像功能还是自定义相机该工程都能作为高效的起点。 直接说结论Android 的 MediaRecorder 从 API 24Android 7.0开始提供了 pause() 和 resume() 方法但从我实际项目的经验来看这两个方法在真机上的表现远没有文档里写的那么理想。尤其是在中低端国产机型上很容易出现录制出来的视频在暂停点附近花屏、音画不同步甚至 resume 之后录制时间统计错乱的问题。这篇文章我就把最近一次做“录制视频可暂停/继续”功能时踩过的坑、验证过的方案和最终的落地代码一起复盘一遍。我会先讲 MediaRecorder 自带方案的代码怎么写再讲它为什么会在部分设备上翻车最后给出我实际采用的分段录制 MediaMuxer 合成方案。整个文章的核心目标是让你看完之后不用再走一遍我走过的弯路。1. 为什么不直接上 Camera2 MediaCodec在说 MediaRecorder 之前先讲清楚一个选型问题。很多新手一上来就想用 Camera2 MediaCodec 做录制因为这套方案最灵活能拿到原始帧数据能做滤镜、美颜、任意裁剪。但代价是你需要自己处理 Camera 会话生命周期、MediaCodec 的输入输出 buffer、音视频同步时间戳、旋转角度、文件封装格式……这一套组合拳下来少说也要上千行代码中间任何一环出问题表现在用户那边就是“录出来的视频打不开”或者“声音对不上”。如果你不需要实时滤镜、不需要水印、不需要后台录屏仅仅就是“打开相机点按钮开始暂停继续最后保存一个 MP4”那么 MediaRecorder 是性价比最高的选择。它的内部已经封装好了采集、编码、封装这一整条链路你只需要配置好音视频源、编码格式、输出文件和旋转角度然后调用 start / stop / pause / resume 就行。对于大多数商业项目比如小视频工具、教学录像、电商拍商品来说已经足够使用而且稳定性要远好于自己手写 MediaCodec。2. MediaRecorder 自带 pause/resume 的正确写法先给出一个可以在 Android 7.0 及以上版本直接跑的 MediaRecorder 基础实现后面再逐行解释为什么这么写。2.1 核心配置代码private MediaRecorder mediaRecorder; private boolean isRecording false; private boolean isPaused false; private void initMediaRecorder() { mediaRecorder new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setVideoEncodingBitRate(8 * 1024 * 1024); mediaRecorder.setVideoFrameRate(30); mediaRecorder.setVideoSize(1920, 1080); mediaRecorder.setOrientationHint(90); // 竖屏录制时旋转90度 mediaRecorder.setOutputFile(getOutputFile().getAbsolutePath()); try { mediaRecorder.prepare(); } catch (IOException e) { e.printStackTrace(); } }注意这里的setVideoSource(MediaRecorder.VideoSource.SURFACE)是关键。从 API 21 开始 MediaRecorder 支持从 Surface 采集视频源你再把mediaRecorder.getSurface()传给 Camera2 的 CaptureSession就能把相机的预览流直接送进编码器。这样做的好处有三个录制画面和预览画面完全一致不走文件回读延迟极低、不需要额外申请CAMERA权限之外的权限、暂停和恢复时不会闪黑屏。如果你还在用旧版的setVideoSource(CAMERA)我建议尽早迁移到 SURFACE 方案因为 CAMERA 源在高版本 Android 上已经逐渐被厂商废弃支持很多机型上会出现录制画面卡死一帧或者预览和录制不同步的情况。2.2 暂停和继续的调用时机private void togglePause() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { // 低版本没有 pause 接口只能走分段录制方案后面讲 return; } if (!isPaused) { try { mediaRecorder.pause(); isPaused true; } catch (RuntimeException e) { // 部分机型在未开始录制时调用 pause 会抛异常 e.printStackTrace(); } } else { try { mediaRecorder.resume(); isPaused false; } catch (RuntimeException e) { e.printStackTrace(); } } }从代码本身看非常干净利落但这里有几个隐藏的雷点需要注意第一pause()和resume()必须在start()之后调用否则会抛RuntimeException。不是所有设备都会抛但你最好自己用 try-catch 兜底。第二pause()之后马上调用stop()是可以的但resume()之后立刻stop()在某些机型上会导致文件损坏建议在 resume 和 stop 之间留至少 500ms 的缓冲区。第三pause()不会通知CamcorderProfile或媒体库录制时长统计会出问题。比如你录制了 60 秒中间暂停了 30 秒系统记录的文件时长可能依然是 60 秒但实际上画面只有 30 秒内容。这在播放器里表现就是后半段反复黑屏或者音画不同步。2.3 一个容易忽略的参数CamcorderProfile上面我手动设置了 bitrate、fps、resolution。如果你不确定这些值怎么填更稳妥的写法是直接用CamcorderProfile获取设备的默认配置CamcorderProfile profile CamcorderProfile.get(Camera.CameraInfo.CAMERA_FACING_BACK, CamcorderProfile.QUALITY_1080P); mediaRecorder.setVideoEncodingBitRate(profile.videoBitRate); mediaRecorder.setVideoFrameRate(profile.videoFrameRate); mediaRecorder.setVideoSize(profile.videoFrameWidth, profile.videoFrameHeight); mediaRecorder.setAudioEncodingBitRate(profile.audioBitRate); mediaRecorder.setAudioSamplingRate(profile.audioSampleRate);这样做的优势是设备自己上报的参数是最适配它的编码能力的你手填的 8M bitrate 在某些机型上可能无法支持 1080P30 的编码录制时会以异常速度丢帧甚至中途自动停止。我实测过一台骁龙 680 的千元机手设 4K 分辨率录制结果 start() 刚调用不到两秒就报start failed: -19换上 CamcorderProfile 的 1080P 配置之后就稳定了。所以建议参数优先从 profile 拿只有特殊需求比如强制低码率上传服务器才手动改。3. 自带 pause/resume 的坑我踩过的都有哪些这一节没有理论全是真金白银踩出来的雷。3.1 部分设备 resume 后视频时间戳错乱这个问题我在小米、OPPO、vivo 的低端机上都复现过。现象是录制 30 秒暂停 10 秒再继续录制 30 秒最终文件用系统播放器播放时进度条走 70 秒但画面内容只有前 30 秒是正常播放后面 40 秒要不就是定格黑屏要不就是音频在继续但视频卡死。排查了很久最终用MediaMetadataRetriever读取文件发现MediaRecorder 在这些机型的 pause() 实现里并不会真正“暂停写入”而是继续往容器里写空数据帧导致时间戳中间被插入了一段无效区间。播放器按时间戳顺序解码时就会卡壳。3.2 resume 后首个关键帧缺失画面首帧花屏另一个高频问题是 resume 之后新片段的首个 I 帧没有正确写入容器导致从暂停点往后拖动进度条时画面会出现马赛克块持续几秒后才恢复正常。这在编码器侧其实是正常行为关键帧间隔没到但 MediaRecorder 内部没有做关键帧请求导致 resume 后的首个可独立解码帧迟迟不来。你很难在应用层解决这个问题因为 MediaRecorder 不给你暴露编码器参数调整的入口。唯一的规避方式是尽量缩短单次录制的时长减少 pause/resume 的次数分多次分段录制比长时间暂停继续要稳妥。3.3 不同设备对 pause 后的行为不一致我整理了一个简单的对比表是我在四台真机上测出来的结果机型pause 后调用 stop 是否正常resume 后音画同步文件时长统计Pixel 6 (Android 13)正常正常正常小米 11 (MIUI 14)偶发文件损坏基本正常异常OPPO Reno 8 (ColorOS 13)正常偶发花屏异常荣耀 80 (MagicOS 7)偶发文件损坏异常异常需要注意这不是严格意义上的“bug”而是 MediaRecorder 的 pause/resume 本身是“尽力而为”的接口厂商底层实现参差不齐Google 也一直没有强制约束这些行为。3.4 低版本 Android 根本没有 pauseAPI 24 以下的系统Android 6.0 及更早MediaRecorder压根没有 pause() 方法。虽然现在还在用 Android 6.0 的用户已经很少但如果你的 app 仍然设置minSdkVersion 24就必须为这套老设备准备降级方案。我的建议很直接不要单独为低版本写一套带暂停功能的逻辑直接让老设备走分段录制模式一套代码搞定所有版本。4. 更可靠的替代方案分段录制 MediaMuxer 合成正因为上面的坑太多我在最终交付版本里换成了“每段单独录制最后合成一个文件”的方案。原理非常简单每次暂停就 stop 掉当前 MediaRecorder存一个临时 MP4 片段每次继续就新建一个 MediaRecorder 重新 start再存一个新片段最后用 MediaMuxer 把所有片段按顺序合成一个完整 MP4。这样做的好处有三个一是绕开了整个 pause/resume 的厂商兼容性问题每一段录制都是完整的 start 到 stop 流程底层表现非常稳定。二是最终的合成阶段你可以完全掌控时间戳如果你要每段之间做转场特效、加滤镜、做变速都有机会在合成时实现。三是如果录制过程突然崩溃至少前面已经完整落盘的分段文件还是可以保住的不至于整个录制全部丢失。当然代价是要写一个合成器或者引入 FFmpeg / MediaMuxer 工具来处理多段合并。Android 原生 MediaMuxer 就能干这个事只是 API 稍微繁琐一点。4.1 分段录制的核心逻辑private File currentSegmentFile; private int segmentIndex 0; private long totalDurationUs 0; private void startNewSegment() { currentSegmentFile new File(cacheDir, segment_ segmentIndex .mp4); mediaRecorder new MediaRecorder(); // 配置方式同 initMediaRecorder() mediaRecorder.setOutputFile(currentSegmentFile.getAbsolutePath()); try { mediaRecorder.prepare(); } catch (IOException e) { e.printStackTrace(); return; } mediaRecorder.start(); } private void stopCurrentSegment() { if (mediaRecorder null) return; try { mediaRecorder.stop(); } catch (RuntimeException e) { // stop 失败时直接 release避免白占资源 e.printStackTrace(); } mediaRecorder.release(); mediaRecorder null; segmentIndex; }注意stop()在录制时间小于 1 秒时会抛RuntimeException原因是写入的 MP4 文件没有任何可播放的帧。处理方式是捕获异常后直接删除该文件不参与后续合成。4.2 MediaMuxer 合成多段 MP4合成这一步是很多人容易卡住的点。我直接给一个可用的实现思路private void mergeSegments(ListFile segments, String outputPath) throws IOException { MediaMuxer muxer new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); int videoTrackIndex -1; int audioTrackIndex -1; long videoOffsetUs 0; long audioOffsetUs 0; for (File segment : segments) { MediaExtractor extractor new MediaExtractor(); extractor.setDataSource(segment.getAbsolutePath()); int srcVideoTrack findTrack(extractor, video/); int srcAudioTrack findTrack(extractor, audio/); if (srcVideoTrack 0 srcAudioTrack 0) { extractor.release(); continue; } // 第一个片段拿到轨信息后添加并用 MediaCodec 辅助读取实际帧数据 if (videoTrackIndex 0 srcVideoTrack 0) { videoTrackIndex muxer.addTrack(extractor.getTrackFormat(srcVideoTrack)); } if (audioTrackIndex 0 srcAudioTrack 0) { audioTrackIndex muxer.addTrack(extractor.getTrackFormat(srcAudioTrack)); } if (videoTrackIndex 0 audioTrackIndex 0) { muxer.start(); } // 读取 sample 并写入 muxer视频和音频时间戳分别加上各自的 offset // 具体读取循环这里简写重点在于每个片段的 sample 时间戳需要加上之前片段的总时长 videoOffsetUs segmentDuration(segment); } muxer.stop(); muxer.release(); }这段代码我故意省略了完整的 sample 读取循环因为完整的实现比较长而且不同项目的需求差别比较大。实际上做多段合成时最大的坑在于不同片段的音视频轨参数必须一致否则muxer.addTrack第二次添加时格式不同会导致合成失败或者音画不同步。这也是为什么分段录制必须保证每一段的编码参数完全一样最好都来自同一个CamcorderProfile配置。如果你的项目能够接受引入 FFmpeg这里强烈建议直接用 FFmpeg 的 concat demuxer 来做合成命令简单到令人发指ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4其中 filelist.txt 里每行写file segment_0.mp4 file segment_1.mp4前提是各段编码参数完全一致。这样做性能开销极低纯推流不重编码稳定性极高唯一的门槛是你需要在 app 里集成一个 FFmpeg 库比如 ffmpeg-kit 或自己编译 so包体积会增加不少。我的建议是产品对包体积不敏感就直接上 FFmpeg省心产品对包体积敏感就老老实实用 MediaMuxer 手写合成。5. 常见问题排查与避坑速查把这段时间遇到的问题整理成一张表方便你直接在开发时对照排查问题现象可能原因解决方案start() 后立刻报 start failed: -19分辨率/码率超出设备编码能力改用 CamcorderProfile 获取设备默认参数stop() 报 RuntimeException录制时间太短文件没有有效帧捕获异常后删除无效片段文件分段合成后音画不同步每段使用了不同音频采样率或视频帧率锁定每段的编码参数完全一致合成分片太多导致内存不足MediaExtractor 未及时释放每个片段处理完后立即 release暂停后继续最终文件时长比实际录制长MediaRecorder pause() 写入空数据帧改用分段录制方案resume 后第一帧花屏/黑屏编码器关键帧间隔问题无法在应用层解决建议分段录制录制出的视频在部分播放器无法打开视频角度信息未设置确保调用 setOrientationHint麦克风没有声音未正确处理录音权限申请或 audio source 设置错误检查权限 尝试 AUDIO_UNPROCESSED 源还有两个细节值得单独拿出来说。第一个是权限问题。Android 6.0 以上录制视频需要同时申请CAMERA和RECORD_AUDIO权限而且在 Android 13API 33上RECORD_AUDIO属于敏感权限必须在录制前动态申请并等待用户授权确认。不要试图在 onPause 里申请权限那样会打乱 Activity 生命周期导致权限回调丢失。第二个是 Surface 生命周期和 MediaRecorder 的绑定顺序。Camera2 预览的 Surface 需要先创建再传给 MediaRecorder而 MediaRecorder 的getSurface()必须在prepare()之前拿到。如果你是先 start 再拿 surface会直接拿到 null。具体规范流程是先创建 MediaRecorder - 配置参数 - prepare() -recorder.getSurface()- 创建 CaptureSession - start()。提示Android 12API 31开始CameraManager 要求传入具体的摄像头 id 来创建 CaptureSession并且摄像头回调必须在HandlerThread里执行不能直接用主线程 Looper否则会报IllegalArgumentException: No looper。6. 写在最后的经验总结项目收尾的时候我在测试机上完整跑了一遍三段暂停、两段继续、最后合成的流程最终生成的文件在系统相册里可以正常播放拖动进度条也能准确定位到暂停点附近。相比直接用 MediaRecorder.pause() 的方案分段录制的最终产物在兼容性上让我安心很多。如果让我给一个最终建议如果你的 app 最低系统版本在 Android 7.0 以上并且你希望以最快的速度出一个能跑的版本可以先用MediaRecorder.pause()/resume()顶着但一定要在产品层面做好机型适配测试。一旦发现线上反馈有异常果断切到分段录制 MediaMuxer/FFmpeg 合成方案这个方案虽然代码量多一些但它把录制这样一个多阶段、多编码状态的复杂过程拆分成了多个“开始-结束”的原子操作最大程度降低了状态机的复杂度。我自己以后做类似功能应该会直接走分段方案不再依赖 MediaRecorder 的 pause 能力了。原因很简单在移动端做功能稳定永远要比代码量少更重要。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站