当前位置: 首页 > news >正文

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
}

⚙️ 性能优化与注意事项

  1. 主线程安全:录音与播放逻辑尽量避免阻塞主线程。

  2. 播放进度防抖:更新频率建议控制在 500ms。

  3. 录音文件写入延迟:部分设备需等待文件完全写入。

  4. 录音时间限制:5s 以下、60s 以上的录音均视为无效。

  5. 生命周期安全:可使用 DefaultLifecycleObserver 自动释放资源。


🏁 结语

通过本次封装,我们实现了一个:

  • 支持录音/播放/暂停/继续播放;

  • 带时间更新与状态回调;

  • 支持时长限制与生命周期安全;
    的完整语音管理模块。

这种封装思路可轻松拓展至视频录制、语音评论、语音私聊等功能场景,极大提升代码复用率与项目可维护性。

http://www.dtcms.com/a/490208.html

相关文章:

  • Lustre/Scade 形式化语义基础 —— 同步Kahn网络 (1996)
  • 内核空间与用户空间解读
  • ELK运维之路(Filebeat第一章-7.17.24)
  • 开源接口管理工具深度横评,swagger vs PostIn哪个更适合你?
  • list的使用和模拟实现
  • 群辉怎么做网站服务器专做投放广告网站
  • 【34】MFC入门到精通——MFC 控件 ComboBox 运行点击控件下拉框 “终止“、“重试“、“忽略“、“引发异常”
  • 论文见解:REACT:在语言模型中协同推理和行动
  • Megatron-DeepSpeed 方案
  • 停止检索!刚刚这本期刊被数据库剔除!
  • Flink-Kafka 数据倾斜问题解决方案
  • html 实现鼠标滑动点亮横轴
  • 连接 TDengine 遇到报错 “failed to connect to server, reason: Connection refused” 怎么办?
  • Web自动化(三方库:Selenium)
  • 网站单页在线制作软件长尾关键词是什么意思
  • 生成静态页面网站源码信息门户网站是什么
  • 在阿里云CentOS服务器上使用Certbot为Nginx配置SSL证书
  • 如何优化网站打开速度网站运营和管理
  • 华为数通认证学习难吗?需掌握哪些核心知识点?
  • 【vue】导出excel
  • WiFi模块远程连接APP:wifi模块的应用
  • 关闭电脑的“快速启动”功能
  • 【MongoDB 基本语法】数据库和集合的基本操作--探秘 MongoDB
  • 提示词管理器设计:从需求到用户体验的高效落地逻辑
  • idea 网站开发无远低代码开发平台
  • FFmpeg 基本API avio_open函数内部调用流程分析
  • 27.Redisson基本使用和可重入性
  • 凡客做网站阎良网站建设公司
  • Uniapp微信小程序开发:全局变量的使用
  • 【优先队列介绍】