设备音乐控制与ID3信息
本文档介绍 Android-JL_Bluetooth 应用中"设备音乐控制与 ID3 信息"能力的完整实现:包括 ID3 音乐信息流的开启/关闭与解析、上一曲/下一曲/播放暂停指令的下发、本地播放器与设备播放器(ID3 播放)之间的切换仲裁,以及设备模式(TF 卡/U 盘)下的播放控制逻辑。
Purpose and Scope
本页面覆盖以下内容:
ID3ControlViewModel:ID3 信息流的生命周期管理、播放指令的发送与回包仲裁、播放器标志(本地/设备)切换逻辑;ID3MusicInfo、MusicStatusInfo、MusicNameInfo:ID3 元数据与音乐状态的数据模型;MusicPlayControlImpl:设备模式下的播放控制实现(播放、暂停、切歌、快进、播放模式设置、时间进度上报)。
以下相关主题属于其他页面,不在本文展开:
- 本地音乐(手机存储)的播放管理,参见"本地音乐播放"相关页面;
- RCSP 蓝牙协议底层的指令封装(
RCSPController的 open/close/get 系列方法),属于 SDK 协议层能力; - 蓝牙连接/断连状态管理,参见"蓝牙连接"相关页面。
Overview
在杰理(Jieli)蓝牙音频生态中,手机 App 通过 RCSP(Remote Control & Status Protocol)协议与耳机/音箱设备通信。当设备侧正在播放音乐时(例如设备内置 TF 卡、U 盘或经典蓝牙 A2DP 播放),设备会把当前曲目的 ID3 信息(歌名、歌手、专辑、时长、播放进度、播放状态等)通过通知流推送给 App,App 则可以下发播放/暂停/上一曲/下一曲等控制指令。
ID3ControlViewModel 是这一能力的核心协调器,它同时协调三个子系统:
- RCSP 指令通道(
RCSPController):负责打开/关闭 ID3 通知流、查询 ID3 信息、下发播放控制指令; - 本地播放控制(
PlayControlImpl/MusicPlayControlImpl):负责手机本地播放器的状态与进度; - 音频焦点管理(
AudioFocusManager):用于判断"当前到底是谁在播音乐",从而决定 UI 应该展示本地播放器还是设备播放器。
关键设计意图:App 需要在不与本地播放器冲突的前提下,实时反映设备端的播放状态。通过 showPlayerFlag(PLAYER_FLAG_NONE/LOCAL/OTHER)与音频焦点、本地播放状态的组合判断,系统能在"本地播放"与"设备(ID3)播放"两种模式之间自动切换 UI 与指令目标,避免两边同时播放或指令互相干扰。
Architecture
flowchart TD
subgraph sg_UI["UI 层"]
MusicControlFragment["MusicControlFragment"]
BlankMusicControlFragment["BlankMusicControlFragment"]
end
subgraph sg_VM["ViewModel 层"]
ID3ControlVM["ID3ControlViewModel"]
BtBasicVM["BtBasicVM (基类)"]
end
subgraph sg_Control["播放控制层"]
PlayControlImpl["PlayControlImpl (单例)"]
MusicPlayControlImpl["MusicPlayControlImpl (设备模式)"]
LocalPlayer["本地播放器"]
end
subgraph sg_SDK["JL SDK / 系统服务"]
RCSPController["RCSPController (单例)"]
AudioFocusManager["AudioFocusManager (单例)"]
BTRcspEventCallback["BTRcspEventCallback"]
BLE_Device["蓝牙设备 (耳机/音箱)"]
end
MusicControlFragment -->|"观察 LiveData"| ID3ControlVM
BlankMusicControlFragment -->|"观察 LiveData"| ID3ControlVM
ID3ControlVM -->|"open/close/get/play 指令"| RCSPController
ID3ControlVM -->|"注册回调"| BTRcspEventCallback
RCSPController --> BTRcspEventCallback
BTRcspEventCallback -->|"onID3MusicInfo 事件"| ID3ControlVM
ID3ControlVM -->|"查询本地播放状态"| PlayControlImpl
ID3ControlVM -->|"音频焦点判断"| AudioFocusManager
PlayControlImpl -->|"设备模式实现"| MusicPlayControlImpl
MusicPlayControlImpl -->|"musicPlayNext/Prev/Pause 等"| RCSPController
RCSPController <-->|"BLE 指令通道"| BLE_Device
AudioFocusManager -->|"onAudioFocusChange 回调"| ID3ControlVM
ID3ControlVM --> BtBasicVM
架构说明:
ID3ControlViewModel继承自BtBasicVM,构造时向RCSPController注册BTRcspEventCallback、向PlayControlImpl注册播放控制监听、向AudioFocusManager注册音频焦点回调(见 ID3ControlViewModel.java#L38-L43),release()时逐一反注册(L122-L128),生命周期管理完整、无泄漏。MusicPlayControlImpl是设备模式下PlayControl接口的实现类(包内可见class),持有RCSPController单例并通过它下发设备指令;同时注册FileBrowseManager文件观察者用于歌曲浏览联动。- UI 层(
MusicControlFragment等)通过MutableLiveData<ID3MusicInfo>、MutableLiveData<Integer> mChangePlayerFlag、MutableLiveData<BaseError> mID3CmdError三个 LiveData 订阅状态变化,实现数据驱动刷新。
核心组件与实现细节
ID3ControlViewModel:ID3 信息流的协调中枢
ID3ControlViewModel(ID3ControlViewModel.java)承担三类职责:ID3 流管理、播放指令下发、播放器切换仲裁。
ID3 信息流生命周期
设备端 ID3 信息通过"通知流"(notification stream)持续推送。ViewModel 提供一对开关方法:
public void openID3InfoStream() {
mRCSPController.openID3MusicNotification(getConnectedDevice(), new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
setOpeningID3Stream(true);
testCount = 1;
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
mID3CmdError.setValue(error);
testCount = 1;
}
});
}
public void closeID3InfoStream() {
JL_Log.i("zzc_id3", "closeID3InfoStream >>>>>>> ");
mRCSPController.closeID3MusicNotification(getConnectedDevice(), new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
isOpeningID3Stream = false;
testCount = 0;
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
mID3CmdError.setValue(error);
}
});
}
Source: ID3ControlViewModel.java#L49-L79
设计意图:isOpeningID3Stream 布尔标志 + testCount 计数共同构成"流状态机"。开启成功置 testCount = 1,关闭成功置 testCount = 0;该计数在 getID3AllInfo() 中用于统计连续无效 ID3 上报的次数,达到阈值(11 次)自动关闭通知流,防止设备持续推送垃圾数据造成无效功耗与流量。
播放指令与回包仲裁
playOrPauseID3 展示了指令防抖与状态回滚机制:
public void playOrPauseID3(boolean srcPlayState) {
Log.e("zzc_id3", " isID3Play(): " + isID3Play() + " tryToPlayState: " + tryToPlayState + " srcPlayState: " + srcPlayState);
if (!playCmdIsResponse) return;
playCmdIsResponse = false;
mRCSPController.iD3MusicPlayOrPause(getConnectedDevice(), new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
tryToPlayState = !srcPlayState;
playCmdIsResponse = true;
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
tryToPlayState = srcPlayState;
playCmdIsResponse = true;
}
});
}
Source: ID3ControlViewModel.java#L103-L120
要点:
playCmdIsResponse作为指令互斥锁:上一次指令未收到回包前,忽略新的播放/暂停请求,避免连点导致指令风暴;tryToPlayState记录"用户期望的目标播放状态",成功回包时置为取反后的源状态;失败时回滚为源状态。后续onID3MusicInfo中用isID3Play() == tryToPlayState判断指令是否真正生效,从而复位互斥锁(见 L245-L253)。
上一曲/下一曲/查询指令则直接透传,不回传 UI 状态(null 回调):
public void getID3MusicInfo() {
JL_Log.e("zzc_id3", "getID3MusicInfo >>>>>>> ");
mRCSPController.getID3MusicInfo(getConnectedDevice(), null);
}
public void playID3Prev() {
mRCSPController.iD3MusicPlayPrev(getConnectedDevice(), null);
}
public void playID3Next() {
mRCSPController.iD3MusicPlayNext(getConnectedDevice(), null);
}
Source: ID3ControlViewModel.java#L90-L101
播放器切换仲裁(PLAYER_FLAG 状态机)
showPlayerFlag 表示当前 UI 应展示哪种播放器:
| 标志 | 值 | 含义 |
|---|---|---|
PLAYER_FLAG_NONE | 0 | 未知/未确定,尚未发生播放 |
PLAYER_FLAG_LOCAL | 1 | 本地播放器正在播放(手机媒体) |
PLAYER_FLAG_OTHER | 2 | 设备(ID3)播放器正在播放 |
判定逻辑的核心是 isID3Play():
private boolean isID3Play() {
return !getPlayControl().isPlay() && !getAudioFocusManager().isHasAudioFocus() && getAudioFocusManager().isMusicPlay();
}
Source: ID3ControlViewModel.java#L145-L158
设计意图:isID3Play() 通过"排除法"判定——本地播放器没在播 + App 没有音频焦点 + 系统检测到有音乐在播 三条件同时成立时,说明正在出声的一定是蓝牙设备侧,因此 UI 应切换为设备(ID3)播放器。注释中保留了早期基于 isOtherPlayerPlay 的多分支判断,最终简化为当前一行式判断,可读性与确定性更好。
切换动作由 onChangePlayerFlag 触发:
private void onChangePlayerFlag(int flag) {
setPlayerFlag(flag);
JL_Log.e("zzc_id3", "onChangePlayerFlag : " + flag);
mChangePlayerFlag.setValue(flag);
if (flag == PLAYER_FLAG_OTHER && !isOpeningID3Stream) {
openID3InfoStream();
}
}
Source: ID3ControlViewModel.java#L164-L172
即:一旦判定为设备播放(PLAYER_FLAG_OTHER)且 ID3 流尚未打开,自动 openID3InfoStream(),保证 UI 能立刻收到设备推送的曲目信息。
ID3 数据模型
ID3MusicInfo(ID3MusicInfo.java)实现了 Parcelable,用于在 ViewModel/UI 之间传递曲目信息:
public class ID3MusicInfo implements Parcelable {
// 歌曲名
private String title;
// 作曲家
private String artist;
// 专辑
private String album;
// 序号
private int number = -1;
// 歌单总长度
private int total;
// 总时长
private int totalTime;
// 类型
private String genre;
// 当前时间
private int currentTime = -1;
// 播放状态
private boolean playStatus;
}
Source: ID3MusicInfo.java#L8-L26
字段说明:
title/artist/album/genre:曲目元数据(ID3 标签的核心内容);number/total:当前曲目序号与歌单总长度(用于显示"3/20"这类进度);totalTime/currentTime:总时长与当前播放位置(毫秒),驱动进度条;playStatus:设备端播放/暂停状态。
该模型还提供了 cloneMySelf() 深拷贝工厂与 Parcel 序列化支持(L44-L68)。同一包下还有 MusicStatusInfo(播放状态)、MusicNameInfo(歌曲名/文件名)等模型,分别服务于设备音乐状态查询与歌曲浏览功能。此外,SConstant 中定义了 KEY_ID3_INFO = "key_id3_info"(SConstant.java#L43),用于 Intent/事件中携带 ID3 信息。
MusicPlayControlImpl:设备模式播放控制
MusicPlayControlImpl(MusicPlayControlImpl.java)是 PlayControl 接口在设备模式下的实现,负责对设备内音乐(TF 卡/U 盘)的播放控制。其构造时即注册 RCSP 事件回调、文件浏览观察者,并在已连接时主动拉取设备音乐信息:
public MusicPlayControlImpl() {
mRCSPController.addBTRcspEventCallback(callback);
JL_Log.e(tag, "music play bluetoothEventCallback=" + callback);
FileBrowseManager.getInstance().addFileObserver(fileObserver);
if (mRCSPController.isDeviceConnected()) {
mRCSPController.getDeviceMusicInfo(mRCSPController.getUsingDevice(), null);
}
}
Source: MusicPlayControlImpl.java#L86-L94
进度上报采用 自走时器(self-ticker):前台时每 1000ms 依据 beginCountTime 与当前系统时间的差值累加 startTime,通过 PlayControlCallback.onTimeChange(startTime, duration) 通知 UI,避免依赖设备端低频的进度推送:
private final Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (!onFrontdesk) {
HandlerManager.getInstance().getMainHandler().removeCallbacks(mRunnable);
return;
}
HandlerManager.getInstance().getMainHandler().removeCallbacks(mRunnable);
HandlerManager.getInstance().getMainHandler().postDelayed(mRunnable, 1000);
long delay = System.currentTimeMillis() - beginCountTime;
startTime += delay;
if (mPlayControlCallback != null) {
mPlayControlCallback.onTimeChange(startTime, duration);
}
setStartTime(startTime);
}
};
Source: MusicPlayControlImpl.java#L61-L79
播放/切歌指令通过 RCSPController 直接下发,例如 playNext() 调 mRCSPController.musicPlayNext(...)(L96-L99)。MIN_STEP = 3000 常量用于快进/快退的最小步长控制(快进至少跳 3 秒),保证用户可感知的进度变化。
核心流程
连接 → 开启 ID3 流 → 实时刷新
sequenceDiagram
participant UI as MusicControlFragment
participant VM as ID3ControlViewModel
participant RCSP as RCSPController
participant DEV as 蓝牙设备
UI->>VM: onCreate 观察 LiveData
Note over VM: 构造时注册事件回调/播放监听/音频焦点回调
DEV-->>RCSP: BLE 连接成功
RCSP-->>VM: onConnection(CONNECTION_OK)
VM->>VM: setPlayerFlag(PLAYER_FLAG_NONE)
VM->>RCSP: openID3MusicNotification(device)
RCSP-->>VM: onSuccess → isOpeningID3Stream = true
DEV-->>RCSP: 推送 ID3 信息 (周期上报)
RCSP-->>VM: onID3MusicInfo(id3MusicInfo)
VM->>VM: 校验 title/totalTime 合法性、同步 playStatus
VM-->>UI: mID3MusicInfo.setValue(info)
UI->>UI: 刷新歌名/歌手/进度/播放状态
Note over UI,VM: 用户点击暂停
UI->>VM: playOrPauseID3(srcPlayState)
VM->>VM: playCmdIsResponse = false (互斥)
VM->>RCSP: iD3MusicPlayOrPause(device)
RCSP-->>VM: onSuccess → tryToPlayState = !srcPlayState
DEV-->>RCSP: 推送新播放状态
RCSP-->>VM: onID3MusicInfo(playStatus 已变化)
VM->>VM: isID3Play() == tryToPlayState → playCmdIsResponse = true
VM-->>UI: mID3MusicInfo.setValue(更新后)
onID3MusicInfo 事件处理决策树
onID3MusicInfo 是整个 ID3 能力的核心入口(ID3ControlViewModel.java#L212-L262),其处理逻辑可概括为:
flowchart TD
Start["onID3MusicInfo(id3MusicInfo)"] --> CheckFlag{"showPlayerFlag == PLAYER_FLAG_OTHER?"}
CheckFlag -->|"否"| Branch1{"isID3Play()?"}
Branch1 -->|"是"| SetOther["setPlayerFlag(PLAYER_FLAG_OTHER)<br/>isNeedCallback = true"]
Branch1 -->|"否"| Branch2{"showPlayerFlag == PLAYER_FLAG_NONE?"}
Branch2 -->|"是"| ToLocal["onChangePlayerFlag(PLAYER_FLAG_LOCAL)"]
Branch2 -->|"否"| Branch3{"本地播放器有焦点且正在播?"}
Branch3 -->|"是"| CloseStream["isOpeningID3Stream=false<br/>closeID3InfoStream()"]
Branch3 -->|"否"| Return["return(忽略)"]
SetOther --> Valid1{"title 为空?"}
Valid1 -->|"是"| Retry1["getID3AllInfo() 计数+1"]
Valid1 -->|"否"| Valid2{"totalTime == 0?"}
Valid2 -->|"是"| Retry1
Valid2 -->|"否"| StreamSync{"流状态与播放状态同步?"}
StreamSync -->|"ID3 在播但流未开"| OpenStream["setOpeningID3Stream(true)<br/>openID3InfoStream()"]
StreamSync -->|"本地在播但流已开"| CloseStream2["setOpeningID3Stream(false)<br/>closeID3InfoStream()"]
StreamSync -->|"正常"| TimeValid{"currentTime 合法?"}
TimeValid -->|"是"| SyncStatus["同步 playStatus 为 isID3Play()<br/>校验 playCmdIsResponse"]
TimeValid -->|"否"| Retry1
SyncStatus --> Publish["mID3MusicInfo.setValue(info)"]
Retry1 --> CountCheck{"testCount == 11?"}
CountCheck -->|"是"| AutoClose["closeID3InfoStream()"]
CountCheck -->|"否"| Return
流程要点:
- 模式预判:先判断当前 UI 是否已是设备播放器模式(
PLAYER_FLAG_OTHER);如果不是,则根据isID3Play()判定是否应切换到设备模式,或保持本地模式并关闭 ID3 流; - 数据合法性校验:
title为空或totalTime == 0视为非法 ID3 数据,转入getID3AllInfo()重试计数——首次触发getID3MusicInfo()主动查询,连续 11 次非法则closeID3InfoStream()关闭推送(L182-L192); - 流与状态对齐:若设备在播但流未开则补开流;若本地在播但流已开则关流;
- 状态归一化:
currentTime合法(== 0或<= totalTime)时才把playStatus强制同步为本地推算的isID3Play()结果并发布 LiveData——设备端上报的播放状态以本地判定为准,避免设备与 App 状态不一致; - 指令确认:
isID3Play() == tryToPlayState说明刚才的播放/暂停指令已生效,复位playCmdIsResponse,允许下一次指令。
连接断开与异常恢复
onConnection 回调(L198-L209)负责状态复位:
- 断连/连接失败(
CONNECTION_DISCONNECT/CONNECTION_FAILED):isOpeningID3Stream = false、testCount = 0、PLAYER_FLAG_NONE,全部回到初始态; - 连接成功(
CONNECTION_OK):标志置PLAYER_FLAG_NONE,若流未开则自动openID3InfoStream(),等待设备推送。
这一设计保证了设备重连后 ID3 能力无需用户手动干预即可自动恢复。
配置选项与常量
本能力没有外部配置文件,所有"可调参数"均为代码内常量:
| 常量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
PLAYER_FLAG_NONE | int | 0 | 播放器标志:未知/未确定 |
PLAYER_FLAG_LOCAL | int | 1 | 播放器标志:本地播放器 |
PLAYER_FLAG_OTHER | int | 2 | 播放器标志:设备(ID3)播放器 |
testCount 阈值 | int | 11 | 连续非法 ID3 上报达到该值后自动关闭通知流 |
MusicPlayControlImpl.MIN_STEP | int | 3000 | 设备模式快进/快退最小步长(毫秒) |
| 进度上报周期 | long | 1000 | MusicPlayControlImpl 自走时器的心跳间隔(毫秒) |
SConstant.KEY_ID3_INFO | String | "key_id3_info" | 事件/Intent 中携带 ID3 信息的键名 |
testCount 与阈值 11 的关系定义在 getID3AllInfo()(ID3ControlViewModel.java#L182-L192):第 1 次触发主动查询,之后每来一条非法数据 +1,达到 11 次即判定设备异常并关闭流。
API 参考
ID3ControlViewModel(对外能力)
| 方法 | 参数 | 说明 | 回调/副作用 |
|---|---|---|---|
openID3InfoStream() | 无 | 请求设备开启 ID3 通知流 | 成功:isOpeningID3Stream=true、testCount=1;失败:mID3CmdError |
closeID3InfoStream() | 无 | 请求设备关闭 ID3 通知流 | 成功:isOpeningID3Stream=false、testCount=0;失败:mID3CmdError |
getID3MusicInfo() | 无 | 主动查询一次当前 ID3 信息 | 透传,无 UI 回调 |
playID3Prev() | 无 | 设备播放上一曲 | 透传 iD3MusicPlayPrev |
playID3Next() | 无 | 设备播放下一曲 | 透传 iD3MusicPlayNext |
playOrPauseID3(boolean srcPlayState) | srcPlayState:当前已知播放状态 | 设备播放/暂停切换 | 受 playCmdIsResponse 互斥;成功置 tryToPlayState=!srcPlayState,失败回滚 |
getMusicInfo() | 无 | 返回 getDeviceInfo().getiD3MusicInfo() | 设备未连接时返回 null |
isOpeningID3InfoStream() | 无 | 查询 ID3 流是否已开启 | 返回 boolean |
showPlayerFlag() | 无 | 查询当前播放器标志 | 返回 PLAYER_FLAG_* |
release() | 无 | 反注册全部回调并释放资源 | 生命周期方法 |
对外暴露的 LiveData(UI 订阅入口):
mID3MusicInfo(MutableLiveData<ID3MusicInfo>):设备曲目信息,每次有效上报更新;mChangePlayerFlag(MutableLiveData<Integer>):播放器模式切换通知;mID3CmdError(MutableLiveData<BaseError>):ID3 指令失败错误对象。
MusicPlayControlImpl(设备模式播放控制)
| 方法 | 说明 |
|---|---|
playNext() | 发送下一曲指令(musicPlayNext) |
playPrev() | 发送上一曲指令(musicPlayPrev) |
playOrPause() | 播放/暂停切换(musicPlayOrPause) |
seekTo(int) | 快进/快退,步长受 MIN_STEP=3000 约束 |
setPlayMode(JL_PlayMode) / getPlayMode() | 设置/查询播放模式(单曲循环、列表循环、随机等) |
isPlay() | 查询当前是否播放中 |
getDuration() / getCurrentTime() | 获取总时长/当前进度 |
进度通过 PlayControlCallback.onTimeChange(current, total) 每 1 秒回调一次;设备模式下由自走时器驱动,onFrontdesk=false 时自动停止心跳,避免后台空转耗电。
失败模式、边界情况与并发
指令并发与防抖
playCmdIsResponse互斥:playOrPauseID3在上一条指令未回包时直接return丢弃新指令(L105)。若设备回包丢失,互斥锁可能长期占用——缓解机制是onID3MusicInfo中通过isID3Play() == tryToPlayState判断状态已达成并复位锁,即"状态驱动解锁"而非仅依赖指令回包。
非法/异常 ID3 数据
title为空或totalTime == 0视为非法数据,不发布给 UI;currentTime越界(> totalTime)视为非法,转入重试计数;- 连续 11 次非法 →
closeID3InfoStream()自动关流,防止异常设备拖垮 App; - 若设备在播但流未开、或本地在播但流已开,事件处理器会动态对齐流状态(补开/关闭)。
播放器冲突仲裁
isID3Play()用"排除法"判定设备播放:本地未播 + 无音频焦点 + 系统有音乐在播。边界情形是本地播放器暂停但设备也在播——此时isID3Play()为 true,UI 切换到设备模式,符合"谁在出声谁显示"的产品预期;- 本地播放器获得音频焦点且
mPlayControl.isPlay()时,主动关闭 ID3 流,避免设备推送数据与本地播放 UI 抢状态。
断连恢复
- 断连/失败:
isOpeningID3Stream、testCount、showPlayerFlag全部复位; - 重连成功:自动重新
openID3InfoStream()。
后台/前台切换
MusicPlayControlImpl的进度心跳仅在onFrontdesk为 true 时运行,App 退后台即停表,避免无用唤醒与电量消耗。
性能与运维注意事项
- BLE 指令最小化:ID3 信息依赖设备周期推送,App 仅在切换为设备模式时开流、切回本地模式时关流,避免长期占用通知通道带宽;
- 自走时器替代高频轮询:进度条不依赖逐帧的设备上报,而是本地每秒推算,显著降低 BLE 流量;
- 主动查询兜底:
getID3MusicInfo()在非法数据首次出现时触发单次主动查询,用于"催更"设备重推; - 日志埋点:
JL_Log.e("zzc_id3", ...)系列日志覆盖开流/关流/查询/播放/事件处理全链路,线上排障可按zzc_id3标签过滤。
扩展点
- 新增 ID3 字段展示:扩展
ID3MusicInfo(注意同步更新Parcel读写与cloneMySelf()),并在onID3MusicInfo的合法校验后发布; - 更换播放器判定策略:
isID3Play()是唯一判定点,可在此替换为基于getAudioFocusManager()与PlayControlImpl的任意组合策略; - 新增设备指令:参照
playID3Prev/Next模式,在ID3ControlViewModel中封装RCSPController的新指令方法并暴露 LiveData; - 播放控制扩展:
MusicPlayControlImpl实现PlayControl接口,可通过新增实现类(如本地模式)在PlayControlImpl.getInstance()的工厂逻辑中切换。
测试
仓库内提供了 MusicDemo.java(code/.../test/java/com/jieli/btsmart/demo/MusicDemo.java)作为音乐相关能力的示例用法,展示如何组合 RCSPController 与播放控制接口进行设备音乐操作。UI 侧 MusicControlFragment 与 BlankMusicControlFragment(空态占位页)演示了本 ViewModel 在界面层的两种消费方式。
使用示例
在 ViewModel 中开启 ID3 流并驱动 UI
以下代码展示 ID3ControlViewModel 与设备建立 ID3 能力协作的完整入口——构造函数完成三类回调注册,保证事件到达时无需额外初始化:
public ID3ControlViewModel() {
super();
mRCSPController.addBTRcspEventCallback(mEventCallback);
getPlayControl().registerPlayControlListener(mControlCallback);
getAudioFocusManager().registerOnAudioFocusChangeCallback(mOnAudioFocusChangeCallback);
}
Source: ID3ControlViewModel.java#L38-L43
处理设备推送的 ID3 信息
事件回调中对上报数据进行合法性过滤与状态归一化,最终通过 mID3MusicInfo 发布给 UI:
@Override
public void onID3MusicInfo(BluetoothDevice device, ID3MusicInfo id3MusicInfo) {
JL_Log.d("zzc_id3", "onID3MusicInfo : " + id3MusicInfo + ", showPlayerFlag : " + showPlayerFlag + ", isID3Play : " + isID3Play());
if (showPlayerFlag != PLAYER_FLAG_OTHER) {
if (isID3Play()) {
setPlayerFlag(PLAYER_FLAG_OTHER);
isNeedCallback = true;
} else if (showPlayerFlag == PLAYER_FLAG_NONE) {
if (!isOpeningID3Stream) setOpeningID3Stream(true);
onChangePlayerFlag(PLAYER_FLAG_LOCAL);
}
return;
}
if (TextUtils.isEmpty(id3MusicInfo.getTitle())) {//不合法ID3信息
getID3AllInfo();
return;
}
if (id3MusicInfo.getTotalTime() == 0) {//不合法ID3信息
getID3AllInfo();
return;
}
// ... 流状态对齐、currentTime 校验、playStatus 归一化、LiveData 发布
}
Source: ID3ControlViewModel.java#L212-L237
设备模式下驱动进度条
MusicPlayControlImpl 通过 1 秒心跳推进进度,UI 层只需监听 PlayControlCallback.onTimeChange:
private final Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (!onFrontdesk) {
HandlerManager.getInstance().getMainHandler().removeCallbacks(mRunnable);
return;
}
HandlerManager.getInstance().getMainHandler().removeCallbacks(mRunnable);
HandlerManager.getInstance().getMainHandler().postDelayed(mRunnable, 1000);
long delay = System.currentTimeMillis() - beginCountTime;
startTime += delay;
if (mPlayControlCallback != null) {
mPlayControlCallback.onTimeChange(startTime, duration);
}
setStartTime(startTime);
}
};
Source: MusicPlayControlImpl.java#L61-L79
深拷贝 ID3 信息用于跨组件传递
ID3MusicInfo 提供 cloneMySelf 工厂方法,避免共享可变对象在多处被意外修改:
public static ID3MusicInfo cloneMySelf(ID3MusicInfo info){
ID3MusicInfo clone = new ID3MusicInfo();
clone.setTotal(info.getTotal());
clone.setCurrentTime(info.getCurrentTime());
clone.setPlayStatus(info.isPlayStatus());
clone.setAlbum(info.getAlbum());
clone.setArtist(info.getArtist());
clone.setTitle(info.getTitle());
clone.setGenre(info.getGenre());
clone.setNumber(info.getNumber());
clone.setTotalTime(info.getTotalTime());
return clone;
}
Source: ID3MusicInfo.java#L44-L56
相关链接
- ID3ControlViewModel.java — ID3 信息流协调中枢(本文核心)
- MusicPlayControlImpl.java — 设备模式播放控制实现
- ID3MusicInfo.java — ID3 曲目信息数据模型
- MusicControlFragment.java — 设备音乐控制 UI(消费 ID3 LiveData)
- MusicDemo.java — 音乐能力示例代码
- SConstant.java — 常量定义(含
KEY_ID3_INFO) - 相关页面:蓝牙连接管理、本地音乐播放、文件浏览(TF 卡/U 盘歌曲)能力请参见对应目录页。