|
|
@@ -6,6 +6,9 @@ import { setFindPlaylist } from '../common/util/FindPlaylistStore'
|
|
|
import { PlaybackCoordinator } from './PlaybackCoordinator'
|
|
|
import { VideoItem } from '../viewmodel/VideoItem'
|
|
|
|
|
|
+/**
|
|
|
+ * 面向 UI 暴露的播放控制动作集合。
|
|
|
+ */
|
|
|
export interface MusicPlaybackControllerActions {
|
|
|
playOrPause: () => void | Promise<void>
|
|
|
playNext: () => void | Promise<void>
|
|
|
@@ -14,6 +17,9 @@ export interface MusicPlaybackControllerActions {
|
|
|
seekTo: (value: string, source?: string) => void | Promise<void>
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 内存歌单直接播放时所需的参数集合。
|
|
|
+ */
|
|
|
export interface InMemoryPlaylistPlaybackOptions {
|
|
|
playlistId: string
|
|
|
playlistName: string
|
|
|
@@ -23,17 +29,26 @@ export interface InMemoryPlaylistPlaybackOptions {
|
|
|
playType?: number
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 播放模式切换后的解析结果。
|
|
|
+ */
|
|
|
export interface LoopModeResolution {
|
|
|
playType: number
|
|
|
toastText: string
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * AVSession 循环模式与应用内模式互转时的解析结果。
|
|
|
+ */
|
|
|
export interface SessionLoopModeResolution {
|
|
|
playType: number
|
|
|
toastText: string
|
|
|
reportedLoopMode: number
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 进度跳转所需的输入参数。
|
|
|
+ */
|
|
|
export interface SeekResolutionOptions {
|
|
|
requestedValue: string
|
|
|
activeDuration: number
|
|
|
@@ -42,21 +57,33 @@ export interface SeekResolutionOptions {
|
|
|
isRemoteSong?: boolean
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 进度跳转解析结果。
|
|
|
+ */
|
|
|
export interface SeekResolution {
|
|
|
canSeek: boolean
|
|
|
seekPos: number
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 队列索引移动结果。
|
|
|
+ */
|
|
|
export interface QueueIndexResolution {
|
|
|
nextIndex: number
|
|
|
reachedBoundary: boolean
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 目标歌曲在队列中的定位结果。
|
|
|
+ */
|
|
|
export interface QueueSongIndexResolution {
|
|
|
index: number
|
|
|
shouldAppend: boolean
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 播放完成时下一步动作的解析结果。
|
|
|
+ */
|
|
|
export interface CompletionActionResolution {
|
|
|
action: 'play_next' | 'replay_current' | 'stop_current' | 'random_next'
|
|
|
showBoundaryToast: boolean
|
|
|
@@ -67,11 +94,19 @@ export type PreviousPlaybackAction = 'history_previous' | 'queue_previous'
|
|
|
export type TogglePlaybackAction = 'pause' | 'resume_cast' | 'resume_local'
|
|
|
export type ItemPlaybackToggleAction = 'toggle_current' | 'play_target'
|
|
|
|
|
|
+/**
|
|
|
+ * 统一播放控制入口,负责纯逻辑解析与运行时转发。
|
|
|
+ */
|
|
|
export class MusicPlaybackController {
|
|
|
private static instance?: MusicPlaybackController
|
|
|
private actions?: MusicPlaybackControllerActions
|
|
|
private playbackCoordinator: PlaybackCoordinator = PlaybackCoordinator.getInstance()
|
|
|
|
|
|
+ /**
|
|
|
+ * 获取播放控制器单例。
|
|
|
+ *
|
|
|
+ * @returns 全局唯一的 MusicPlaybackController 实例。
|
|
|
+ */
|
|
|
public static getInstance(): MusicPlaybackController {
|
|
|
if (!MusicPlaybackController.instance) {
|
|
|
MusicPlaybackController.instance = new MusicPlaybackController()
|
|
|
@@ -79,6 +114,12 @@ export class MusicPlaybackController {
|
|
|
return MusicPlaybackController.instance
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 计算下一个应用内循环模式及其提示文案。
|
|
|
+ *
|
|
|
+ * @param playType 当前播放模式。
|
|
|
+ * @returns 下一个播放模式和对应提示文案。
|
|
|
+ */
|
|
|
public static resolveNextLoopMode(playType: number): LoopModeResolution {
|
|
|
const nextType = playType >= 4 || playType < 0 ? 0 : playType + 1
|
|
|
if (nextType === 1) {
|
|
|
@@ -96,6 +137,12 @@ export class MusicPlaybackController {
|
|
|
return { playType: 0, toastText: '连续循环播放' }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将会话层循环模式转换为应用内部播放模式,并给出提示文案。
|
|
|
+ *
|
|
|
+ * @param mode 会话层上报的循环模式。
|
|
|
+ * @returns 应用内播放模式、提示文案和回传给会话层的模式值。
|
|
|
+ */
|
|
|
public static resolveSessionLoopMode(mode: number): SessionLoopModeResolution {
|
|
|
let reportedLoopMode = mode + 1
|
|
|
if (reportedLoopMode >= 4) {
|
|
|
@@ -117,6 +164,12 @@ export class MusicPlaybackController {
|
|
|
return { playType: 4, toastText: '连续播放不循环', reportedLoopMode }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将应用内播放模式映射为 AVSession 可识别的循环模式值。
|
|
|
+ *
|
|
|
+ * @param playType 应用内播放模式。
|
|
|
+ * @returns AVSession 对应的循环模式值。
|
|
|
+ */
|
|
|
public static resolveAvSessionLoopMode(playType: number): number {
|
|
|
if (playType === 1) {
|
|
|
return 1
|
|
|
@@ -133,6 +186,12 @@ export class MusicPlaybackController {
|
|
|
return 2
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据输入参数计算安全的跳转进度,并处理 CUE 偏移与远程歌曲限制。
|
|
|
+ *
|
|
|
+ * @param options 跳转进度解析参数。
|
|
|
+ * @returns 是否允许跳转以及最终跳转位置。
|
|
|
+ */
|
|
|
public static resolveSeekPosition(options: SeekResolutionOptions): SeekResolution {
|
|
|
let seekPos = Number.parseInt(options.requestedValue)
|
|
|
if (Number.isNaN(seekPos)) {
|
|
|
@@ -164,6 +223,13 @@ export class MusicPlaybackController {
|
|
|
return { canSeek: true, seekPos }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将百分比进度转换为实际毫秒值。
|
|
|
+ *
|
|
|
+ * @param percent 百分比进度,取值范围预期为 0 到 100。
|
|
|
+ * @param activeDuration 当前媒体有效时长,单位毫秒。
|
|
|
+ * @returns 对应的毫秒进度。
|
|
|
+ */
|
|
|
public static resolveSeekValueFromPercent(percent: number, activeDuration: number): number {
|
|
|
if (activeDuration <= 0) {
|
|
|
return 0
|
|
|
@@ -172,6 +238,13 @@ export class MusicPlaybackController {
|
|
|
return clampedPercent * (activeDuration / 100)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析下一首的队列索引,并标记是否触达边界。
|
|
|
+ *
|
|
|
+ * @param currentIndex 当前播放索引。
|
|
|
+ * @param queueLength 队列总长度。
|
|
|
+ * @returns 下一首索引和边界信息。
|
|
|
+ */
|
|
|
public static resolveNextQueueIndex(currentIndex: number, queueLength: number): QueueIndexResolution {
|
|
|
if (queueLength <= 0) {
|
|
|
return { nextIndex: -1, reachedBoundary: true }
|
|
|
@@ -182,6 +255,13 @@ export class MusicPlaybackController {
|
|
|
return { nextIndex: currentIndex + 1, reachedBoundary: false }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析上一首的队列索引,并标记是否触达边界。
|
|
|
+ *
|
|
|
+ * @param currentIndex 当前播放索引。
|
|
|
+ * @param queueLength 队列总长度。
|
|
|
+ * @returns 上一首索引和边界信息。
|
|
|
+ */
|
|
|
public static resolvePreviousQueueIndex(currentIndex: number, queueLength: number): QueueIndexResolution {
|
|
|
if (queueLength <= 0) {
|
|
|
return { nextIndex: -1, reachedBoundary: true }
|
|
|
@@ -192,6 +272,13 @@ export class MusicPlaybackController {
|
|
|
return { nextIndex: currentIndex - 1, reachedBoundary: false }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 在现有队列中定位目标歌曲;若不存在则给出追加位置。
|
|
|
+ *
|
|
|
+ * @param queueFilePaths 当前队列内的文件路径列表。
|
|
|
+ * @param targetFilePath 目标歌曲文件路径。
|
|
|
+ * @returns 目标索引以及是否需要追加到队列尾部。
|
|
|
+ */
|
|
|
public static resolveQueueSongIndex(queueFilePaths: string[], targetFilePath: string): QueueSongIndexResolution {
|
|
|
const existingIndex = queueFilePaths.findIndex((filePath: string): boolean => filePath === targetFilePath)
|
|
|
if (existingIndex >= 0) {
|
|
|
@@ -200,6 +287,14 @@ export class MusicPlaybackController {
|
|
|
return { index: queueFilePaths.length, shouldAppend: true }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 判断当前是否应在队列尾部停止播放。
|
|
|
+ *
|
|
|
+ * @param playType 当前播放模式。
|
|
|
+ * @param currentIndex 当前播放索引。
|
|
|
+ * @param queueLength 队列总长度。
|
|
|
+ * @returns 是否应该在末尾停止。
|
|
|
+ */
|
|
|
public static shouldStopAtQueueEnd(playType: number, currentIndex: number, queueLength: number): boolean {
|
|
|
if (queueLength <= 0) {
|
|
|
return false
|
|
|
@@ -207,6 +302,14 @@ export class MusicPlaybackController {
|
|
|
return playType === 4 && currentIndex >= queueLength - 1
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析单曲播放完成后控制层应执行的动作。
|
|
|
+ *
|
|
|
+ * @param playType 当前播放模式。
|
|
|
+ * @param currentIndex 当前播放索引。
|
|
|
+ * @param queueLength 队列总长度。
|
|
|
+ * @returns 完成后的动作以及是否提示边界。
|
|
|
+ */
|
|
|
public static resolveCompletionAction(playType: number, currentIndex: number,
|
|
|
queueLength: number): CompletionActionResolution {
|
|
|
if (playType === 1) {
|
|
|
@@ -224,6 +327,14 @@ export class MusicPlaybackController {
|
|
|
return { action: 'play_next', showBoundaryToast: false }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析“下一首”按钮应采用的动作。
|
|
|
+ *
|
|
|
+ * @param playType 当前播放模式。
|
|
|
+ * @param currentIndex 当前播放索引。
|
|
|
+ * @param queueLength 队列总长度。
|
|
|
+ * @returns 下一首动作类型。
|
|
|
+ */
|
|
|
public static resolveNextPlaybackAction(playType: number, currentIndex: number, queueLength: number): NextPlaybackAction {
|
|
|
if (playType === 3) {
|
|
|
return 'random_play'
|
|
|
@@ -234,6 +345,12 @@ export class MusicPlaybackController {
|
|
|
return 'advance_queue'
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析“上一首”按钮应采用的动作。
|
|
|
+ *
|
|
|
+ * @param playType 当前播放模式。
|
|
|
+ * @returns 上一首动作类型。
|
|
|
+ */
|
|
|
public static resolvePreviousPlaybackAction(playType: number): PreviousPlaybackAction {
|
|
|
if (playType === 3) {
|
|
|
return 'history_previous'
|
|
|
@@ -241,6 +358,13 @@ export class MusicPlaybackController {
|
|
|
return 'queue_previous'
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析播放/暂停切换时应执行的动作。
|
|
|
+ *
|
|
|
+ * @param isPlaying 当前本地播放器是否正在播放。
|
|
|
+ * @param isCastPlaying 当前投屏状态是否正在播放。
|
|
|
+ * @returns 切换动作类型。
|
|
|
+ */
|
|
|
public static resolveTogglePlaybackAction(isPlaying: boolean, isCastPlaying: boolean): TogglePlaybackAction {
|
|
|
if (isPlaying) {
|
|
|
return 'pause'
|
|
|
@@ -251,6 +375,15 @@ export class MusicPlaybackController {
|
|
|
return 'resume_local'
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析点击某个条目时,是切换当前歌曲状态还是直接播放目标歌曲。
|
|
|
+ *
|
|
|
+ * @param currentVideoUrl 当前播放文件路径。
|
|
|
+ * @param targetFilePath 目标歌曲文件路径。
|
|
|
+ * @param isPlaying 当前是否处于播放状态。
|
|
|
+ * @param allowAnyCurrentPlaying 是否允许“任意正在播放”时直接切换当前状态。
|
|
|
+ * @returns 条目点击后应执行的动作。
|
|
|
+ */
|
|
|
public static resolveItemPlaybackToggleAction(currentVideoUrl: string, targetFilePath: string,
|
|
|
isPlaying: boolean, allowAnyCurrentPlaying: boolean = false): ItemPlaybackToggleAction {
|
|
|
if (currentVideoUrl === targetFilePath || (allowAnyCurrentPlaying && isPlaying)) {
|
|
|
@@ -259,6 +392,12 @@ export class MusicPlaybackController {
|
|
|
return 'play_target'
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 构建发现页等内存歌单的统一播放请求对象。
|
|
|
+ *
|
|
|
+ * @param options 内存歌单播放参数。
|
|
|
+ * @returns 可落入待播仓库和事件总线的播放请求对象。
|
|
|
+ */
|
|
|
public static buildInMemoryPlaylistPlayRequest(options: InMemoryPlaylistPlaybackOptions): PlaylistPlayRequest {
|
|
|
const songs = options.songs ?? []
|
|
|
const playlistId = options.playlistId.indexOf('find-') === 0 ? options.playlistId : `find-${options.playlistId}`
|
|
|
@@ -276,37 +415,84 @@ export class MusicPlaybackController {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 注册 UI 层自定义动作实现,优先覆盖协调器默认行为。
|
|
|
+ *
|
|
|
+ * @param actions 播放动作实现集合。
|
|
|
+ */
|
|
|
public setActions(actions: MusicPlaybackControllerActions): void {
|
|
|
this.actions = actions
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 清理已注册的动作实现;传入参数时仅在匹配当前实例时才清理。
|
|
|
+ *
|
|
|
+ * @param actions 可选的动作实现引用,用于避免误清理。
|
|
|
+ */
|
|
|
public clearActions(actions?: MusicPlaybackControllerActions): void {
|
|
|
if (!actions || this.actions === actions) {
|
|
|
this.actions = undefined
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接请求协调器播放单曲,不依赖页面事件宿主。
|
|
|
+ *
|
|
|
+ * @param song 待播放歌曲。
|
|
|
+ * @param source 调用来源,默认标记为 controller。
|
|
|
+ * @param playType 可选的播放模式。
|
|
|
+ * @returns 播放请求完成后的 Promise。
|
|
|
+ */
|
|
|
public async playSong(song: VideoItem, source: string = 'controller', playType?: number): Promise<void> {
|
|
|
await this.playbackCoordinator.playSong(song, source, playType)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接请求协调器播放完整队列,不依赖页面事件宿主。
|
|
|
+ *
|
|
|
+ * @param queue 待播放队列。
|
|
|
+ * @param startIndex 起播索引。
|
|
|
+ * @param source 调用来源,默认标记为 controller。
|
|
|
+ * @param playType 可选的播放模式。
|
|
|
+ * @returns 播放请求完成后的 Promise。
|
|
|
+ */
|
|
|
public async playQueue(queue: VideoItem[], startIndex: number, source: string = 'controller',
|
|
|
playType?: number): Promise<void> {
|
|
|
await this.playbackCoordinator.playQueue(queue, startIndex, source, playType)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接调用协调器切换播放/暂停,绕过 UI 动作覆写。
|
|
|
+ *
|
|
|
+ * @returns 切换动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playOrPauseDirect(): Promise<void> {
|
|
|
await this.playbackCoordinator.playOrPause()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接调用协调器播放下一首,绕过 UI 动作覆写。
|
|
|
+ *
|
|
|
+ * @returns 切歌动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playNextDirect(): Promise<void> {
|
|
|
await this.playbackCoordinator.playNext()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 直接调用协调器播放上一首,绕过 UI 动作覆写。
|
|
|
+ *
|
|
|
+ * @returns 切歌动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playPreviousDirect(): Promise<void> {
|
|
|
await this.playbackCoordinator.playPrevious()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换播放/暂停;若 UI 层注册了动作实现,则优先走自定义实现。
|
|
|
+ *
|
|
|
+ * @returns 切换动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playOrPause(): Promise<void> {
|
|
|
if (this.actions) {
|
|
|
await this.actions.playOrPause()
|
|
|
@@ -315,6 +501,11 @@ export class MusicPlaybackController {
|
|
|
await this.playbackCoordinator.playOrPause()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 播放下一首;优先使用 UI 层注入的动作实现。
|
|
|
+ *
|
|
|
+ * @returns 切歌动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playNext(): Promise<void> {
|
|
|
if (this.actions) {
|
|
|
await this.actions.playNext?.()
|
|
|
@@ -323,6 +514,11 @@ export class MusicPlaybackController {
|
|
|
await this.playNextDirect()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 播放上一首;优先使用 UI 层注入的动作实现。
|
|
|
+ *
|
|
|
+ * @returns 切歌动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async playPrevious(): Promise<void> {
|
|
|
if (this.actions) {
|
|
|
await this.actions.playPrevious?.()
|
|
|
@@ -331,10 +527,22 @@ export class MusicPlaybackController {
|
|
|
await this.playPreviousDirect()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换循环模式;该动作仅由 UI 层注入实现。
|
|
|
+ *
|
|
|
+ * @returns 模式切换完成后的 Promise。
|
|
|
+ */
|
|
|
public async setLoopMode(): Promise<void> {
|
|
|
await this.actions?.setLoopMode?.()
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 跳转到指定进度;优先使用 UI 层注入的动作实现。
|
|
|
+ *
|
|
|
+ * @param value 目标进度值,通常为毫秒字符串。
|
|
|
+ * @param source 可选的调用来源标识。
|
|
|
+ * @returns 跳转动作完成后的 Promise。
|
|
|
+ */
|
|
|
public async seekTo(value: string, source?: string): Promise<void> {
|
|
|
if (this.actions) {
|
|
|
await this.actions.seekTo?.(value, source)
|
|
|
@@ -343,6 +551,11 @@ export class MusicPlaybackController {
|
|
|
await this.playbackCoordinator.seekTo(value, source)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 通过事件总线发起一组内存歌曲的统一播放,并预先写入歌单上下文。
|
|
|
+ *
|
|
|
+ * @param options 内存歌单播放参数。
|
|
|
+ */
|
|
|
public playInMemoryPlaylist(options: InMemoryPlaylistPlaybackOptions): void {
|
|
|
if (!options.songs || options.songs.length <= 0) {
|
|
|
return
|