Kotlin 实现社交 App 音视频模块:语音录制、播放、暂停与进度控制全流程封装
🚀 本文将带你从 0 到 1 实现一个完整可复用的语音录制与播放模块,适用于社交类 App 场景。
使用 Kotlin 封装网易云信的AudioRecorder
,并结合MediaPlayer
实现播放、暂停、进度监听、时间显示与生命周期管理。
🧭 目录
-
前言
-
功能设计目标
-
模块结构设计
-
核心类 SyncRecordManager
-
使用方式
-
实现思路详解
-
状态管理
-
录音定时器
-
播放控制与进度监听
-
资源释放与重置机制
-
-
性能优化与注意事项
-
结语
🧩 前言
在社交类应用中,语音聊天、语音评论等功能是非常常见的。
而在 Android 中实现一套录音 + 播放 + 暂停 + 恢复播放 + 时间显示的完整方案,往往涉及:
-
录音管理(启动、停止、超时、失败处理)
-
播放管理(暂停、恢复、播放完成)
-
时间更新与 UI 同步
-
生命周期内的资源释放
如果逻辑分散在各个界面中,会造成维护困难。
因此我们需要一个高内聚、低耦合的封装类。
🎯 功能设计目标
本模块需要满足以下需求:
功能 | 说明 |
---|---|
🎤 录音控制 | 点击按钮开始录音,再次点击停止录音 |
⏱️ 时间显示 | 实时显示录音/播放时间(格式:00:06) |
🎧 播放控制 | 支持播放、暂停、继续播放 |
🔁 状态切换 | 录音完成后自动进入播放状态 |
🧹 重置机制 | 播放完成或删除后重置为初始状态 |
🔒 限时录音 | 录音时长限制为 5~60 秒 |
🧩 生命周期安全 | 页面销毁时自动释放资源 |
🧱 模块结构设计
采用单一职责 + 状态驱动设计:
VoiceRecordManager├── AudioRecorder (网易云信)├── MediaPlayer (系统播放)├── Handler/Timer (时间进度)├── State Enum (状态管理)└── Callbacks (状态、进度监听)
💡 核心类 SyncRecordManager
该类负责录音、播放、暂停、继续播放的完整逻辑:
/*** @class SyncRecordManager* @desc 音频录制与播放管理类* 封装了语音的录制、播放、暂停、继续播放、计时、状态管理等逻辑。* 使用网易云信的 AudioRecorder 进行录制,不使用系统 MediaRecorder。** 功能点:* - 支持录音时长限制(最短5秒,最长60秒)* - 自动回调录音/播放时间* - 支持暂停与恢复播放* - 状态统一管理(IDLE / RECORDING / RECORDED / PLAYING / PAUSED)* - 播放完成、录制过短/过长、状态变化等回调通知*/
class NimRecordManager(private val context: Context) {/** 当前状态 */enum class State {IDLE, // 空闲状态RECORDING, // 正在录音RECORDED, // 已录音完成PLAYING, // 正在播放PAUSED // 播放暂停}/** ============= 对外回调接口 ============= */var onStateChanged: ((State) -> Unit)? = null // 状态变化回调var onRecordTimeUpdate: ((Int) -> Unit)? = null // 录音时长更新(秒)var onPlayTimeUpdate: ((Int, Int) -> Unit)? = null // 播放进度更新(当前秒,总秒)var onPlayComplete: (() -> Unit)? = null // 播放完成var onRecordTooShortOrLong: (() -> Unit)? = null // 录音过短或过长/** ============= 内部成员变量 ============= */private var state: State = State.IDLEset(value) {field = valueonStateChanged?.invoke(value)}private var recorder: AudioRecorder? = nullprivate var mediaPlayer: MediaPlayer? = nullprivate var recordFilePath: String? = nullprivate var recordFile: File? = nullprivate var recordDuration = 0 // 录音时长(秒)private val mainHandler = Handler(Looper.getMainLooper())private var recordTimerRunnable: Runnable? = nullprivate var playTimerRunnable: Runnable? = null/** 录音时长限制 */private val maxRecordSeconds = 60private val minRecordSeconds = 5/** 网易云信录音回调 */private val recordCallback = object : IAudioRecordCallback {override fun onRecordStart(audioFile: File?, recordType: RecordType?) {// 开始录音时的回调,可用于更新 UI}override fun onRecordReady() {// 录音准备完成,可在此更新UI状态(可选)}override fun onRecordSuccess(audioFile: File?, audioLength: Long, recordType: RecordType?) {// 录音成功回调audioFile?.let { file ->if (!file.exists()) {onRecordFail()return}// 某些机型需等待文件写入完成val finalSize = waitForFileWrite(file)if (finalSize == 0L) {onRecordFail()return}recordFile = filerecordFilePath = file.absolutePathrecordDuration = (audioLength / 1000).toInt()// 校验录音时长合法性if (recordDuration in minRecordSeconds..maxRecordSeconds) {state = State.RECORDED} else {file.delete()onRecordTooShortOrLong?.invoke()reset(deleteFile = false)}} ?: run {onRecordFail()}}override fun onRecordFail() {// 录音失败处理stopRecordTimer()reset(deleteFile = true)}override fun onRecordCancel() {// 主动取消录音stopRecordTimer()reset(deleteFile = true)}override fun onRecordReachedMaxTime(duration: Int) {// 达到最大录音时长,自动停止stopRecord()}}//===================== 录音相关 =====================///*** toggleAction:一键控制录音与播放状态转换* 根据当前状态自动切换对应操作*/fun toggleAction() {when (state) {State.IDLE -> startRecord()State.RECORDING -> stopRecord()State.RECORDED -> startPlay()State.PLAYING -> pausePlay()State.PAUSED -> resumePlay()}}/** 开始录音 */private fun startRecord() {if (state != State.IDLE) returnrecordDuration = 0recordFile = nullrecordFilePath = nullrecorder = AudioRecorder(context, RecordType.AAC, maxRecordSeconds, recordCallback)recorder?.startRecord()state = State.RECORDINGstartRecordTimer()}/** 停止录音 */private fun stopRecord() {if (state != State.RECORDING) returnrecorder?.completeRecord(false)stopRecordTimer()}/** 启动录音计时器(每秒更新) */private fun startRecordTimer() {recordTimerRunnable = object : Runnable {override fun run() {recordDuration++onRecordTimeUpdate?.invoke(recordDuration)if (recordDuration >= maxRecordSeconds) {stopRecord()} else {mainHandler.postDelayed(this, 1000)}}}mainHandler.postDelayed(recordTimerRunnable!!, 1000)}/** 停止录音计时器 */private fun stopRecordTimer() {recordTimerRunnable?.let { mainHandler.removeCallbacks(it) }recordTimerRunnable = null}//===================== 播放相关 =====================///** 开始播放录音文件 */private fun startPlay() {if (state != State.RECORDED) returnrecordFilePath?.let { path ->try {mediaPlayer = MediaPlayer().apply {setDataSource(path)prepare()start()setOnCompletionListener {stopPlay()onPlayComplete?.invoke()}}state = State.PLAYINGstartPlayTimer()} catch (e: Exception) {e.printStackTrace()reset()}}}/** 暂停播放 */private fun pausePlay() {if (state != State.PLAYING) returnmediaPlayer?.pause()state = State.PAUSEDstopPlayTimer()}/** 继续播放 */private fun resumePlay() {if (state != State.PAUSED) returnmediaPlayer?.start()state = State.PLAYINGstartPlayTimer()}/** 停止播放并释放资源 */private fun stopPlay() {mediaPlayer?.runCatching {stop()release()}mediaPlayer = nullstopPlayTimer()state = State.RECORDED}/** 启动播放进度计时(每500ms更新一次UI) */private fun startPlayTimer() {playTimerRunnable = object : Runnable {override fun run() {mediaPlayer?.let {if (it.isPlaying) {val posSec = it.currentPosition / 1000val durSec = it.duration / 1000onPlayTimeUpdate?.invoke(posSec, durSec)mainHandler.postDelayed(this, 500)}}}}mainHandler.post(playTimerRunnable!!)}/** 停止播放计时器 */private fun stopPlayTimer() {playTimerRunnable?.let { mainHandler.removeCallbacks(it) }playTimerRunnable = null}//===================== 重置 & 工具方法 =====================///*** 重置状态并释放资源* @param deleteFile 是否删除录音文件*/fun reset(deleteFile: Boolean = true) {stopRecordTimer()stopPlayTimer()recorder?.completeRecord(true)recorder = nullmediaPlayer?.runCatching {stop()release()}mediaPlayer = nullif (deleteFile) {recordFilePath?.let { File(it).delete() }recordFile = nullrecordFilePath = null}recordDuration = 0state = State.IDLE}/*** 等待文件写入完成,确保录音文件可用*/private fun waitForFileWrite(file: File, maxWaitMs: Long = 300): Long {val start = System.currentTimeMillis()var size = file.length()while (size == 0L && System.currentTimeMillis() - start < maxWaitMs) {Thread.sleep(20)size = file.length()}return size}/** ============= 对外工具方法 ============= */fun getFilePath(): String? = recordFilePathfun getFile(): File? = recordFilefun getDuration(): Int = recordDuration
}
🧩 使用方式
private val voiceManager by lazy { VoiceRecordManager(requireContext()) }binding.btnVoice.setOnClickListener {when (voiceManager.state) {VoiceRecordManager.State.IDLE -> {// 开始录音voiceManager.startRecord()}VoiceRecordManager.State.RECORDING -> {// 停止录音voiceManager.stopRecord()}VoiceRecordManager.State.RECORDED -> {// 播放录音voiceManager.startPlay()}VoiceRecordManager.State.PLAYING -> {// 暂停播放voiceManager.pausePlay()}VoiceRecordManager.State.PAUSED -> {// 继续播放voiceManager.resumePlay()}}
}
🧠 实现思路详解
状态管理
定义 State
枚举,保证流程转换清晰:
enum class State { IDLE, RECORDING, RECORDED, PLAYING, PAUSED }
录音定时器
通过 Handler
定时更新录音时间:
private fun startRecordTimer() {mainHandler.postDelayed({recordDuration++onRecordTimeUpdate?.invoke(recordDuration)}, 1000)
}
播放控制与进度监听
MediaPlayer
播放状态定期回调进度:
val pos = it.currentPosition / 1000
val dur = it.duration / 1000
onPlayTimeUpdate?.invoke(pos, dur)
资源释放与重置机制
在异常、取消或销毁时释放资源:
fun reset(deleteFile: Boolean = true) {recorder?.completeRecord(true)mediaPlayer?.release()state = State.IDLE
}
⚙️ 性能优化与注意事项
-
主线程安全:录音与播放逻辑尽量避免阻塞主线程。
-
播放进度防抖:更新频率建议控制在 500ms。
-
录音文件写入延迟:部分设备需等待文件完全写入。
-
录音时间限制:5s 以下、60s 以上的录音均视为无效。
-
生命周期安全:可使用
DefaultLifecycleObserver
自动释放资源。
🏁 结语
通过本次封装,我们实现了一个:
-
支持录音/播放/暂停/继续播放;
-
带时间更新与状态回调;
-
支持时长限制与生命周期安全;
的完整语音管理模块。
这种封装思路可轻松拓展至视频录制、语音评论、语音私聊等功能场景,极大提升代码复用率与项目可维护性。