You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android缓存目录音频无法播放:MediaPlayer与ExoPlayer报错问题

问题分析与解决方案

核心问题

录音文件可手动播放、成功上传,但本地播放失败,根源在于三个关键问题:

  • Android 7.0+ 对file:// URI的权限限制
  • MediaRecorder未正确释放导致文件被占用
  • ExoPlayer实例管理不当引发资源泄漏

解决方案

1. 用FileProvider生成合法Content URI

Android 7.0及以上禁止直接使用file:// URI访问应用私有目录文件,需通过FileProvider生成content://格式的URI:

步骤1:注册FileProvider到AndroidManifest.xml

<application>
    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

步骤2:创建路径配置文件res/xml/file_paths.xml

指定缓存目录的访问路径:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path name="audio_cache" path="/" />
</paths>

步骤3:替换Uri.fromFile为Content URI

修改processRecording()中的URI生成逻辑:

private fun processRecording() {
    val audioFile = File(audioFilePath!!)
    Log.d("Path>>", audioFilePath!!)
    if (audioFile.exists()) {
        val audioBytes = audioFile.readBytes()
        // 生成Content URI并授予读取权限
        val audioUri = FileProvider.getUriForFile(
            applicationContext,
            "${packageName}.fileprovider",
            audioFile
        )
        grantUriPermission(
            packageName,
            audioUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION
        )
        playRecording(audioUri)
        // 上传逻辑不变
        FireRef.storageRef.child("ChatAudios/").child(audioFile.name).putBytes(audioBytes)
            .addOnSuccessListener {
                val url = it.storage.downloadUrl
                url.addOnCompleteListener { link ->
                    if (link.isSuccessful) {
                        Log.d("URL>>", link.result.toString())
                    }
                }
            }
    }
}

2. 确保MediaRecorder完全释放

录音结束后必须调用stop()和release(),否则文件会被进程占用,无法读取:

private fun stopRecording() {
    recorder?.apply {
        try {
            stop()
        } catch (e: IllegalStateException) {
            // 处理录音未启动的异常
        } finally {
            release()
            recorder = null
        }
    }
}

注意:必须在调用processRecording()前执行stopRecording(),保证文件写入完成并释放。

3. 优化ExoPlayer实例管理

每次播放新建实例会引发资源泄漏,建议复用单例或完善释放逻辑:

方案1:复用单例实例

private var exoPlayer: ExoPlayer? = null

private fun initExoPlayer() {
    if (exoPlayer == null) {
        exoPlayer = ExoPlayer.Builder(applicationContext).build()
    }
}

private fun playRecording(uri: Uri) {
    initExoPlayer()
    exoPlayer?.apply {
        stop()
        clearMediaItems()
        setMediaItem(MediaItem.fromUri(uri))
        prepare()
        play()
        addListener(object : Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_ENDED) {
                    stop()
                }
            }
            
            override fun onPlayerError(error: PlaybackException) {
                super.onPlayerError(error)
                stop()
            }
        })
    }
}

// 在Activity/Fragment销毁时释放
override fun onDestroy() {
    super.onDestroy()
    exoPlayer?.release()
    exoPlayer = null
}

方案2:多实例场景下完善释放逻辑

若需每次新建实例,需在错误或中途停止时也释放资源:

private fun playRecording(uri: Uri) {
    val exoPlayer = ExoPlayer.Builder(applicationContext).build()
    val mediaItem = MediaItem.fromUri(uri)
    exoPlayer.apply {
        setMediaItem(mediaItem)
        prepare()
        play()
        addListener(object : Player.Listener {
            override fun onPlaybackStateChanged(playbackState: Int) {
                if (playbackState == Player.STATE_ENDED || playbackState == Player.STATE_IDLE) {
                    release()
                }
            }

            override fun onPlayerError(error: PlaybackException) {
                super.onPlayerError(error)
                release()
                // 错误提示逻辑
            }
        })
    }
}

4. 修复MediaPlayer的播放逻辑

同步prepare()易阻塞线程,改用异步方法并完善错误处理:

private fun playRecording(uri: Uri) {
    MediaPlayer().apply {
        setAudioAttributes(
            AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) // 录音为语音,适配该类型
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .build()
        )
        setDataSource(applicationContext, uri)
        setOnPreparedListener { start() }
        setOnErrorListener { _, _, _ ->
            release()
            true
        }
        prepareAsync()
    }
}

验证步骤

  1. 录音结束后优先调用stopRecording()
  2. 确认使用FileProvider生成的content:// URI
  3. 检查MediaRecorder是否已完全释放
  4. 播放时监听错误信息,排查剩余问题

内容的提问来源于stack exchange,提问作者coDeR

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 20:43:19