Files
2026-09-04 15:22:20 +08:00

119 lines
4.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 【Demo】音频播放(AudioPlayerCard 控制条组件)
> 源文件:`lib/pages/demo/audio_player_demo.dart` | 分类:组件封装
> 一句话:传一个音频地址,得到一个完整控制条 —— 播放/暂停、进度拖动、倍速、循环。
---
## 一、这个 Demo 解决什么问题
课程详情页里常有"音频小节"。本页演示 `AudioPlayerCard`(`lib/core/components/audio_player_card.dart`)的用法:
```dart
const AudioPlayerCard(
url: _audioUrl, // 网络源(也可以传 asset 本地源)
title: '网络测试音频(约 6 分钟)',
autoPlay: true, // 进页自动播放
)
```
核心思想:**AudioPlayerCard 只是"控制条",不是整页** —— 它设计成可嵌进任意列表/详情页(列表项、课程小节卡片都能放)。
> 对照 Vue:相当于"封装好的 `<audio-player url="...">` 组件",内部用 just_audio 插件实现。
---
## 二、怎么运行 / 演示内容
```dart
runApp(const MaterialApp(home: AudioPlayerDemoPage()));
```
- 进页自动播放(网络流约 6 分钟);
- 体验:播放/暂停、拖动进度(网络流拖动后出现"缓冲中 X%")、倍速切换、循环;
- 切后台自动暂停(组件内部处理,页面无需操心)。
> 代码里注释掉的本地源 `assets/audio/sample-30s.mp3`:30 秒本地音频,seek 秒响应(本地文件跳播无需重新缓冲)。
---
## 三、核心知识点(新手向)
### 1. 网络音频能否"拖动进度",取决于服务器是否支持 HTTP Range
音频拖到中间播放,播放器会向服务器请求**字节范围**(Range 请求)。支持则秒跳,不支持则跳不了:
```
curl -s -D - -o NUL -H "Range: bytes=0-1023" <音频url>
```
| 响应头 | 含义 |
|---|---|
| `HTTP/1.1 206 Partial Content` | ✅ 支持,可拖动 |
| `HTTP/1.1 200 OK` | ❌ 不支持,seek 会被重置回起点 |
> 本项目踩过的坑:samplelib.com 实测**不支持 Range**(返回 200),点进度条后位置被重置回起点;
> 而且第二次打开有系统缓存 → 看似正常(会误导排查方向!)。现用 SoundHelix(curl 实测 206)。
> **接真实课程音频前,务必先 curl 验收服务器的 Range 支持。**
### 2. 网络流 seek 后的"缓冲中 X%"
进度条上的**浅色区域 = 已缓冲部分**;网络流 seek 后会有一次"缓冲中 X%"——这是 ExoPlayer 的正常机制,不是卡死。
本地文件没有这个问题,所以本地 demo 拖动"秒响应"。
### 3. 切后台自动暂停
组件内部监听应用生命周期(AppLifecycleListener),切后台自动暂停——用户听一半切走,回来继续,不用业务页管。
---
## 四、关键代码剖析
```dart
class AudioPlayerDemoPage extends StatelessWidget {
const AudioPlayerDemoPage({super.key});
// 【临时调试】网络源:观察 seek 后进度条的"缓冲区域 + 缓冲中 X%"
static const String _audioUrl =
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('音频播放'), centerTitle: true),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
const AudioPlayerCard( // ← 一行引入,全部能力由组件提供
url: _audioUrl,
title: '网络测试音频(约 6 分钟)',
autoPlay: true,
),
...
],
),
),
);
}
}
```
页面本身是 `StatelessWidget`——**音频状态全部在组件内部**(状态封装是组件化的精髓)。
---
## 五、踩坑记录
1. **服务器不支持 HTTP Range 时 seek 会被重置**,且第二次打开有系统缓存会"假装正常"——验收必须用 curl 看 `206`;
2. AudioPlayerCard 是**控制条组件,不是整页**——别拿它当页面用,它设计来嵌进列表/详情;
3. 网络流 seek 后有"缓冲中 X%"是正常机制(ExoPlayer),浅色区域 = 已缓冲;
4. 本地 asset 源与网络 url 源二选一传参(`asset:` 或 `url:`),别两个都传;
5. 切后台自动暂停是组件内部处理的,业务页不要重复实现。
---
## 六、扩展思考
- 组件内部用 just_audio 插件:想了解播放器原理(音源切换、事件流)直接读 `lib/core/components/audio_player_card.dart`;
- 富文本里的 `<audio>` 标签会自动替换成 AudioPlayerCard,见 [rich_text_demo.md](rich_text_demo.md);
- 进阶"列表连续播放":在 AudioPlayerCard 上加 `setAudioSources` + 上一首/下一首(just_audio 原生支持);
- 真实项目:把 `_audioUrl` 换成课程接口返回的音频地址即可,组件层不用动。