杰理 SDK 文档中心
首页
首页
  • 快速开始

    • 环境要求与权限配置
    • 集成SDK依赖
    • 运行示例应用
  • 核心架构与协议

    • RCSP协议与数据通道
    • 蓝牙连接与设备管理
    • TWS双耳功能
    • 基础功能接口与自定义命令
  • 设备功能控制

    • 设备音乐控制与ID3信息
    • 文件浏览与传输
    • FM收音与发射
    • 灯光控制
    • 闹钟与时间管理
    • 查找设备与防丢
    • ANC与噪声处理
    • 按键功能设置
    • 彩屏仓控制
    • AI翻译
  • 音效与音频处理

    • 均衡器音效调节
    • 录音与语音控制
    • Line-in、SPDIF与声卡功能
    • 音频编解码库
  • 扩展功能库

    • OTA固件升级
    • 数据加密与解密
    • 图片与动图格式转换
  • 示例应用 btsmart

    • 应用架构与界面导航
    • 设备功能适配与数据层
    • 设备配置JSON与资源文件
  • 参考与版本

    • 错误码参考
    • 版本历史与更新日志
    • 开发文档中心导航

设备音乐控制与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 是这一能力的核心协调器,它同时协调三个子系统:

  1. RCSP 指令通道(RCSPController):负责打开/关闭 ID3 通知流、查询 ID3 信息、下发播放控制指令;
  2. 本地播放控制(PlayControlImpl / MusicPlayControlImpl):负责手机本地播放器的状态与进度;
  3. 音频焦点管理(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_NONE0未知/未确定,尚未发生播放
PLAYER_FLAG_LOCAL1本地播放器正在播放(手机媒体)
PLAYER_FLAG_OTHER2设备(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

流程要点:

  1. 模式预判:先判断当前 UI 是否已是设备播放器模式(PLAYER_FLAG_OTHER);如果不是,则根据 isID3Play() 判定是否应切换到设备模式,或保持本地模式并关闭 ID3 流;
  2. 数据合法性校验:title 为空或 totalTime == 0 视为非法 ID3 数据,转入 getID3AllInfo() 重试计数——首次触发 getID3MusicInfo() 主动查询,连续 11 次非法则 closeID3InfoStream() 关闭推送(L182-L192);
  3. 流与状态对齐:若设备在播但流未开则补开流;若本地在播但流已开则关流;
  4. 状态归一化:currentTime 合法(== 0 或 <= totalTime)时才把 playStatus 强制同步为本地推算的 isID3Play() 结果并发布 LiveData——设备端上报的播放状态以本地判定为准,避免设备与 App 状态不一致;
  5. 指令确认: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_NONEint0播放器标志:未知/未确定
PLAYER_FLAG_LOCALint1播放器标志:本地播放器
PLAYER_FLAG_OTHERint2播放器标志:设备(ID3)播放器
testCount 阈值int11连续非法 ID3 上报达到该值后自动关闭通知流
MusicPlayControlImpl.MIN_STEPint3000设备模式快进/快退最小步长(毫秒)
进度上报周期long1000MusicPlayControlImpl 自走时器的心跳间隔(毫秒)
SConstant.KEY_ID3_INFOString"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 盘歌曲)能力请参见对应目录页。
Next
文件浏览与传输