ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android MediaRecorder视频录制暂停与继续的实现技巧与兼容性避坑指南

Android MediaRecorder视频录制暂停与继续的实现技巧与兼容性避坑指南 简介面向Android开发者的视频录制实现方案核心解决MediaRecorder的暂停与继续录制问题。Demo引入isoviewer-1.0-RC-27.jar库采用竖屏4:3比例录制修复了竖屏状态下预览画面横向显示的典型缺陷并在录制完成后使用SurfaceView进行播放适合需要深度定制录制交互或处理方向适配的开发者参考也适合Android进阶学习者研究系统API的完整调用链。资源包以zip形式打包共1098个文件其中png、xml占据多数覆盖图标、布局与主题资源json、aidl体现Android Studio工程特性java与class展示源码及编译产物jar与apk中包含依赖库和可直接运行安装的demo整体约8.72MB结构完整清晰便于快速导入IDE复现调试。目前已有2138人学习下载具备一定参考热度。学习后可掌握MediaRecorder状态机切换、暂停续录的触发逻辑、竖屏预览矫正方法以及SurfaceView播放本地视频的写法同时工程内还附有构建配置文件与多类型资源能够帮助梳理Android工程组织方式对需要开展视频类功能开发的读者具有直接借鉴意义。 说实话Android 里用MediaRecorder做视频录制本身不难难的是“可暂停、可继续”这六个字。很多人第一次看到MediaRecorder的 API 列表以为有个现成的pause()和resume()方法就能轻松搞定等你真在真机上跑一遍才会发现一堆奇奇怪怪的兼容性问题和状态错乱。这篇文章我就围绕 MediaRecorder 录制视频、暂停、继续这几个核心点把我在实际项目里趟过的坑和沉淀下来的写法完整讲一遍希望能帮你少走点弯路。1. MediaRecorder pause/resume 的边界与真相1.1 内置 API 的演进API 24 前后是两种开发思路先明确一个事实MediaRecorder直到 Android 7.0API 24才正式提供了pause()和resume()方法。API 24 之前开发者想实现暂停/继续只能走“录制一段 - stop() - 再 start() 录制下一段 - 最后合并文件”的路线不仅代码量大而且分段文件拼接容易出现音画不同步、时间戳跳变的问题。API 24 之后官方在MediaRecorder里加入了暂停/继续的原生支持理论上你只需要mediaRecorder.pause(); mediaRecorder.resume();就可以在一段 MP4 文件里真正实现“中间暂停不算录制时长”的效果。底层会帮你把暂停前后两段媒体数据的时间戳衔接好最终输出的是一个连续、可正常播放的文件。这听起来很美但实际使用中有一个容易被忽略的边界pause/resume 是否真正生效取决于设备厂商对 MediaRecorder 的实现程度而不是你调用了方法就一定有预期效果。1.2 实际使用时哪些场景会翻车根据我在多个机型上的实测下面这几类场景最容易出问题部分国产 ROM 上pause() 调用后画面会卡住一帧resume() 后预览画面却迟迟不恢复。某些设备的音频编码器和视频编码器对 pause/resume 支持不一致暂停结束后音量会出现爆音或短暂丢声。如果同时开启了摄像头预览暂停期间摄像头可能被系统回收恢复时 Surface 已经失效。所以在你动手写代码之前我建议先在目标机型上做一次最简单的可行性测试start()后等待 2 秒调用pause()再等 2 秒调用resume()最后stop()并检查生成的视频时长和音画是否正常。别等整个项目写完才发现底层不支持那会非常被动。2. 动手前必须处理好的底层条件2.1 权限申请不是加一行 Manifest 那么简单一个完整的视频录制应用至少需要这三类权限uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /Android 6.0 之后CAMERA 和 RECORD_AUDIO 都属于运行时权限必须在代码里动态申请。这里有个细节部分机型上如果先申请音频权限再申请相机权限用户拒绝过一次之后后续再弹窗会非常困难。我一般建议进入录制页之前统一申请并且把 RECORD_AUDIO 放在 CAMERA 之前因为用户对麦克风权限的警惕性更高先给相机权限再弹麦克风心理上更容易接受。另外Android 10API 29开始强制分区存储你不要再把文件写到公共存储根目录而是优先使用应用专属目录File outputDir getExternalFilesDir(Environment.DIRECTORY_MOVIES); File outputFile new File(outputDir, System.currentTimeMillis() .mp4);这样既不需要额外申请存储权限也避免分区存储适配问题。2.2 相机预览和 MediaRecorder 的绑定顺序我见过不少新手把SurfaceView和MediaRecorder的绑定顺序写反结果预览是黑的录出来的文件也是黑的。标准的顺序应该是这样打开 Camera 并设置参数。把 Camera 的预览画面绑定到 SurfaceHolder。配置 MediaRecorder。调用mediaRecorder.setPreviewDisplay(surfaceHolder.getSurface())。调用mediaRecorder.prepare()再调用camera.unlock()。调用mediaRecorder.start()。这里的核心原则是Camera 必须先持有 Surface 的引用MediaRecorder 再从同一个 Camera 上获取视频流最后才开始写入。如果你只设置 MediaRecorder 的setPreviewDisplay却忘记把 Camera 的预览也指过去那么用户看到的是黑屏但录制文件可能又是正常的这种“黑屏但有数据”的状态调试起来特别迷惑。3. 核心实现一个可暂停/继续的视频录制器3.1 启动录制我通常把 MediaRecorder 的初始化封装成一个独立的setupMediaRecorder()方法方便在异常时重建private void setupMediaRecorder() { mediaRecorder new MediaRecorder(); camera.unlock(); mediaRecorder.setCamera(camera); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mediaRecorder.setVideoSize(1280, 720); mediaRecorder.setVideoFrameRate(30); mediaRecorder.setVideoEncodingBitRate(4 * 1024 * 1024); mediaRecorder.setAudioEncodingBitRate(128 * 1024); mediaRecorder.setAudioSamplingRate(44100); mediaRecorder.setOrientationHint(90); mediaRecorder.setOutputFile(currentFile.getAbsolutePath()); mediaRecorder.setPreviewDisplay(previewHolder.getSurface()); try { mediaRecorder.prepare(); } catch (IOException e) { Log.e(TAG, prepare failed, e); } }这里有个码率选择的经验720p 视频用 4Mbps 左右比较稳妥画面清晰且文件不会太大如果你录制的场景是长时间监控建议把码率降到 2Mbps否则一小时的视频体积会非常可观。setOrientationHint的值取决于你的应用是横屏还是竖屏竖屏一般设 90但要注意不同厂商对前摄后摄的旋转定义不一样最好在真机上验证。启动录制时还要注意prepare()之后立刻start()不是最保险的建议等MediaRecorder真正进入录制状态再更新 UI否则用户点“暂停”时状态还没就绪按钮会无效mediaRecorder.start(); isRecording true; isPaused false; updateRecordButtonState();3.2 暂停、继续与停止的实现暂停和继续的代码看似简单但必须要加状态校验。我见过有人连续点了两次 pause第二次 pause 直接抛 IllegalStateException 导致应用崩溃public void pauseRecording() { if (isRecording !isPaused mediaRecorder ! null) { try { mediaRecorder.pause(); isPaused true; // 记录暂停时的时间点用于UI上的时长统计 pauseStartTime SystemClock.uptimeMillis(); } catch (RuntimeException e) { // 部分机型暂停失败这里需要降级处理 Log.e(TAG, pause failed, e); } } } public void resumeRecording() { if (isRecording isPaused mediaRecorder ! null) { try { mediaRecorder.resume(); isPaused false; // 将暂停期间的时间从总时长里扣掉 totalPausedTime SystemClock.uptimeMillis() - pauseStartTime; } catch (RuntimeException e) { Log.e(TAG, resume failed, e); } } }停止时有一个经典坑如果录制时间太短比如不到 1 秒就直接调用stop()很多设备会抛RuntimeException。这是因为底层还没有写入足够的可索引数据。标准做法是用 try-catch 包住 stop并在异常时走“放弃文件”的路线public void stopRecording() { if (mediaRecorder null) return; try { if (isPaused) { mediaRecorder.resume(); } mediaRecorder.stop(); } catch (RuntimeException e) { Log.e(TAG, stop failed, maybe duration is too short, e); currentFile.delete(); } finally { mediaRecorder.release(); mediaRecorder null; isRecording false; isPaused false; } }3.3 调用状态机的关键分支把上面的代码放到真实页面里还需要一个清晰的状态判断逻辑。我习惯用三个布尔值控制 UIisRecording、isPaused、isPrepared。每次按钮点击都先走状态判断再执行操作不要在 UI 层硬编码“现在应该能暂停”的假设。举个例子当用户按 Home 键切到后台时如果视频正在录制系统不一定会主动帮你暂停 MediaRecorder此时继续在后台录制会导致音频权限被系统撤销甚至录制失败。我建议在onPause()里根据业务需求决定是pause()还是直接stop()并在onResume()时恢复相应的状态而不是依赖 MediaRecorder 的默认行为。4. 暂停/继续背后的三个隐性坑4.1 摄像头预览冻结这是最隐蔽的问题之一。在一些设备上pause()之后底层的 Camera 仍然被 MediaRecorder 占用但预览 Surface 已经停止刷新导致 resume 之后画面会有一到两秒的黑屏或定格。我在测试某款骁龙中端机型时这种情况复现率几乎百分之百。一个有效的缓解方式在pause()时不要把摄像头释放掉也不要重新创建 MediaRecorder而是继续持有它们等待 resume。如果 resume 后预览仍然冻结可以尝试在 resume 之后重新调用一次camera.startPreview()但这要求你之前的 Camera 对象还没有被 lock实际操作时需要预处理异常。4.2 时间统计不准很多开发者会直接用System.currentTimeMillis()算录制时长这在普通录制时没问题但一旦有暂停你的总时长就会多算出暂停间隙。正确做法是记录有效录制时长long startTime SystemClock.uptimeMillis(); long totalPausedTime 0L; long pauseStartTime 0L; // 每次resume时累加暂停时间 long totalRecordTime SystemClock.uptimeMillis() - startTime - totalPausedTime;UI 上的计时器不要用Thread.sleep循环而是用Handler.postDelayed否则暂停期间 UI 很容易卡顿而且线程管理容易泄漏。4.3 停止时的异常抛出MediaRecorder.stop()在不恰当的状态下会抛RuntimeException这是官方文档里都写了的。除了录制时间太短还有两种情况容易触发一是你调用了pause()后立刻stop()二是你在录制尚未开始时就直接stop()。我建议在 stop 方法里统一做状态检查并把异常处理收敛到一处避免让异常上抛到 Activity 导致闪退。5. 当内置能力不好使时备选方案如何选5.1 分段录制 后端合并如果你的目标机型比较杂特别是需要在 Android 7.0 以下的机器上运行那就必须放弃原生 pause/resume改用“每次暂停时 stop()继续时重新 start() 一个文件最后合并所有分片”的方案。这个方案的关键是所有分片的视频参数必须完全一致包括分辨率、码率、帧率、编码格式否则合并后很容易出现时间戳错乱。合并分片可以用 MediaExtractor 把每个文件读取出来再用 MediaMuxer 写入同一个输出文件。必须注意这种方式本质上是流拷贝而不是重新编码如果分片之间的关键帧参数或时间基不一致合并出来的视频可能会在某些播放器上无法拖动进度条。5.2 直接用 CameraX 或 Media3 的路线如果你的产品不是对 MediaRecorder 有特殊依赖我更建议评估一下 CameraX。CameraX 的VideoCapture底层虽然也是 MediaRecorder但它封装了生命周期和 Surface 的复杂逻辑对暂停/继续的支持在大多数机型上比手写更稳定。Media3 的Transformer则更适合做后处理比如把分段文件合成为一个真正连续的视频。不过要注意CameraX 在部分低端机型上的预热时间比原生 Camera 接口长如果你追求极致的开拍速度原生 MediaRecorder 仍然有它的价值。没有银弹只能在项目里根据实际设备池做取舍。5.3 如何判断 pause/resume 是否真正生效最后分享一个调试方法录制完视频后不要只看播放器能不能播还要用MediaMetadataRetriever读取元数据里的时长和你自己统计的有效录制时长对比。如果两者差值很大说明设备并没有真正实现 pause/resume 的时间标记而是通过某种近似方式处理这种情况在后续剪辑流程里会埋下雷。可以用下面的代码快速验证MediaMetadataRetriever retriever new MediaMetadataRetriever(); retriever.setDataSource(filePath); String duration retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); retriever.release();我自己的习惯是把每个型号真机的测试结果记录到表格里包括是否支持 pause、resume 后是否有黑屏、时长是否准确、合并后是否可拖拽进度条这几项。有了这份数据再去决定哪些机型用原生暂停、哪些机型走分段录制心里就有底了。项目上线前这个兼容性矩阵比任何代码优化都更重要。本文还有配套的精品资源点击获取
返回列表