11、播客与有声书:章节标记、变速播放、睡眠定时器

各位同学,今天我们来聊聊车载多媒体里一个非常“人性化”的模块——播客与有声书。说实话,在车上听播客和有声书,跟听音乐完全是两码事。音乐你可以当背景音,但播客和有声书,用户是真的在“听内容”。

我当年在做一个车载项目时,产品经理扔过来一句话:“用户想在车上听完一本《三体》。”我当时心想,这不就是个播放器吗?结果一做才发现,坑全在细节里。章节标记怎么跳?1.5倍速会不会变“唐老鸭”?睡着了谁帮我关掉?

好,今天我们就把这三大核心功能拆开揉碎,讲清楚。

11.1 章节标记:让用户像翻书一样跳转

播客和有声书跟音乐最大的区别是什么?是“结构”。音乐你听完了就完了,但一本书有目录,一集播客有段落。用户需要的是“跳着听”,而不是从头拖进度条。

章节标记的核心逻辑,说白了就是给音频文件打上时间戳标签。每个章节有一个起始时间、一个标题,可能还有一个图片。

在 Android 里,我们通常用 MediaMetadata 来携带章节信息。但注意,车载场景下,我更推荐用自定义的 Chapter 数据类,因为 MediaMetadata 的字段有限,而且跨组件传递时容易丢数据。

核心思路: 解析音频文件中的章节元数据(如 MP4 的 Chapter List,或播客 RSS 中的 标签),构建一个有序的章节列表,然后通过 RecyclerView 展示给用户。

来看一个简单的章节数据模型:

data class Chapter(
    val title: String,          // 章节标题,比如“第一章 宇宙闪烁”
    val startTimeMs: Long,      // 起始时间,单位毫秒
    val endTimeMs: Long,        // 结束时间,方便计算时长
    val artworkUri: String?     // 章节封面,有些播客每章配图不同
)

嗯,这里要注意:章节的 endTimeMs 其实可以省略。为什么?因为下一章的 startTimeMs 就是上一章的 endTimeMs。我习惯在构建列表时,遍历一遍自动补齐,省得数据源里有人漏填。

我曾经在一个项目里遇到过,某个播客平台的章节数据里,最后一章没有 endTimeMs。结果用户跳到最后一章时,进度条直接崩了。后来我加了个兜底逻辑:如果当前章节是最后一个,endTimeMs 就用音频总时长。

11.2 变速播放:1.2倍速还是1.5倍速?这是个问题

变速播放,技术上叫“时间缩放”(Time Stretching)。你想想看,用户为什么要在车上用变速?因为通勤时间有限,想多听点内容。但车载环境跟手机不一样——车里有风噪、胎噪、发动机声,倍速一高,人声就容易糊。

Android 原生的变速方案,是用 MediaPlayer 配合 PlaybackParams。从 API 23 开始支持,用法很简单:

val params = PlaybackParams()
params.setSpeed(1.5f)  // 1.5倍速
params.setPitch(1.0f)  // 保持音调不变,这是关键!
mediaPlayer.playbackParams = params

注意那个 setPitch(1.0f)。如果不设置这个,1.5倍速下声音会变尖,像《鼠来宝》里的花栗鼠。车载音响系统本来中高频就容易刺耳,再一升调,用户直接关应用。

避坑指南: 我曾经在某个车机上测试,发现 PlaybackParams 在 Android 8.0 以下的系统上不支持。如果你的应用要兼容老车机(很多车机还是 Android 7.1),建议用 Sonic 或 SoundTouch 这类第三方库做软件变速。

我个人习惯的做法是:先检测系统 API 版本,如果 >= 23,优先用硬件加速的 PlaybackParams;如果低于 23,回退到 Sonic 库。Sonic 的集成方式也很简单:

// 使用 Sonic 库进行变速
val sonic = Sonic(44100, 1)
sonic.setSpeed(1.5f)
sonic.setPitch(1.0f)
// 将音频数据通过 sonic 处理后再写入 AudioTrack

这里有个性能问题。Sonic 是纯 CPU 计算,在低端车机上可能会卡顿。我建议把变速后的音频数据缓存到内存里,不要每次播放都重新计算。

11.3 睡眠定时器:用户睡着了,车机不能一直响

睡眠定时器,这个功能看起来简单,但做不好会出大事。你想想,用户设定“30分钟后停止播放”,结果30分钟后车还在开,音乐突然停了,用户还以为车坏了。

睡眠定时器的本质,是一个倒计时 + 一个播放停止动作。但车载场景下,有几个特殊要求:

  • 定时器到期后,不能直接 kill 应用。应该先暂停播放,保留界面状态,方便用户一键续播。
  • 定时器要跟车辆电源状态联动。如果车辆熄火,定时器应该自动取消,因为用户已经下车了。
  • 定时器到期前,最好有语音提示。“还有1分钟即将停止播放”,给用户一个反应时间。

来看一个简单的定时器实现:

class SleepTimer {
    private var countDownTimer: CountDownTimer? = null
    private var remainingTimeMs: Long = 0

    fun start(durationMs: Long, onTick: (Long) -> Unit, onFinish: () -> Unit) {
        countDownTimer?.cancel()
        countDownTimer = object : CountDownTimer(durationMs, 1000) {
            override fun onTick(millisUntilFinished: Long) {
                remainingTimeMs = millisUntilFinished
                onTick(millisUntilFinished)
                // 检查是否到达“1分钟预警”阈值
                if (millisUntilFinished in 60_000..61_000) {
                    // 触发语音提示:“还有1分钟停止播放”
                }
            }

            override fun onFinish() {
                onFinish()
                // 暂停播放,但保留播放位置
                mediaPlayer.pause()
                // 保存当前播放进度到 SharedPreferences
            }
        }.start()
    }

    fun cancel() {
        countDownTimer?.cancel()
    }
}

小技巧: 我建议把定时器的剩余时间持久化到 SharedPreferences 里。万一车机重启(比如用户下车买了个东西再回来),定时器还能恢复。当然,如果车辆熄火超过30分钟,就应该清空定时器——用户大概率已经到家了。

11.4 知识体系总览

好,上面三个功能讲完了。我画了一张图,帮你把整个知识体系串起来。你看,这三个功能其实都围绕着“用户听内容”这个核心场景,只是从不同维度做了增强。

播客与有声书播放 章节标记 时间戳 + 标题 + 封面 支持跳转、列表展示 变速播放 PlaybackParams / Sonic 保持音调不变(Pitch=1.0) 睡眠定时器 倒计时 + 暂停播放 车辆电源联动 底层技术支撑 MediaMetadata Chapter 数据类 PlaybackParams Sonic 库 CountDownTimer 电源状态监听

你看,这张图把三个功能模块和它们依赖的底层技术都串起来了。章节标记依赖元数据解析,变速播放依赖音频处理库,睡眠定时器依赖系统定时器和电源管理。三者独立但又共享同一个播放引擎。

11.5 避坑总结

最后,我把自己踩过的坑列一下,你们做项目时直接对照检查:

功能模块 常见坑点 解决方案
章节标记 最后一章缺少结束时间,导致进度条异常 自动用音频总时长兜底
章节标记 章节列表过长时,RecyclerView 滑动卡顿 使用 DiffUtil 做增量更新,避免全量刷新
变速播放 低版本 Android 不支持 PlaybackParams API 版本检测 + Sonic 库回退
变速播放 1.5倍速下人声变糊,尤其车载噪音环境 建议最大倍速限制在 2.0x,并提供“语音增强”选项
睡眠定时器 定时器到期后直接停止播放,用户找不到续播入口 暂停而非停止,保留播放进度和界面状态
睡眠定时器 车辆熄火后定时器还在跑,下次启动时意外停止 监听 ACC 状态,熄火时取消定时器

嗯,差不多就这些。播客和有声书这个模块,说难不难,但细节特别多。你想想看,用户在车上听内容,本来就是为了打发通勤时间。如果因为章节跳转不准、倍速声音奇怪、或者定时器乱停,用户可能就再也不用了。

我个人习惯是,每做一个功能,先问自己三个问题:用户会在什么场景下用?最可能出错的边界情况是什么?如果出错,怎么让用户无感恢复?想清楚这三点,代码写起来就有底了。


公众号:蓝海资料掘金营,微信deep3321