-
Notifications
You must be signed in to change notification settings - Fork 0
Player System
gxxk-dev edited this page Jan 31, 2026
·
2 revisions
本文档详细介绍 StudyWithMiku 播放器系统的内部实现,包括适配器模式架构、具体适配器实现、状态管理和 Media Session 桥接。
播放器系统采用适配器模式 (Adapter Pattern),定义统一接口来适配不同的播放器实现。
flowchart TB
subgraph VueComponents["Vue Components"]
App["App.vue, SpotifyPlayer.vue, etc."]
end
subgraph usePlayerComposable["usePlayer Composable<br/>(状态管理 + 适配器切换)"]
States["playbackState, currentTrack,<br/>currentTime, volume, trackList ..."]
end
subgraph PlayerAdapterBase["PlayerAdapter (抽象基类)"]
Interface["统一接口: play(), pause(), seek(), loadPlaylist(), ..."]
Events["事件系统: on(), emit(), off()"]
Capabilities["能力查询: supportsSeek(), hasBuiltInUI(), ..."]
end
subgraph Adapters["具体适配器"]
APlayerAdapter["APlayerAdapter<br/>(网易云/QQ音乐)<br/>- 完整播放控制<br/>- 歌词支持<br/>- 进度控制"]
SpotifyAdapter["SpotifyAdapter<br/>(Spotify Embed)<br/>- iframe 模式<br/>- 有限控制<br/>- 内部管理播放列表"]
end
VueComponents --> usePlayerComposable
usePlayerComposable -->|"setAdapter(adapter)"| PlayerAdapterBase
PlayerAdapterBase --> APlayerAdapter
PlayerAdapterBase --> SpotifyAdapter
src/player/
├── PlayerAdapter.js # 抽象基类
├── adapters/
│ ├── APlayerAdapter.js # APlayer 适配器
│ └── SpotifyAdapter.js # Spotify 适配器
├── mediaSessionBridge.js # Media Session 桥接
├── constants.js # 常量定义
└── sources/ # 音乐源相关(可选)
PlayerAdapter 定义了所有播放器必须实现的统一接口,并内置简单的事件发射器。
// src/player/PlayerAdapter.js
export class PlayerAdapter {
constructor() {
/** @type {Map<string, Set<Function>>} */
this._listeners = new Map()
/** @type {PlaybackState} */
this._state = PlaybackState.IDLE
/** @type {UnifiedTrack[]} */
this._tracks = []
/** @type {number} */
this._currentIndex = 0
/** @type {RepeatMode} */
this._repeatMode = RepeatMode.ALL
/** @type {boolean} */
this._initialized = false
}内置简单的事件发射器,避免引入额外依赖:
/**
* 注册事件监听器
* @returns {Function} 取消监听的函数
*/
on(event, callback) {
if (!this._listeners.has(event)) {
this._listeners.set(event, new Set())
}
this._listeners.get(event).add(callback)
// 返回取消监听的函数(便于清理)
return () => this.off(event, callback)
}
/**
* 触发事件
*/
emit(event, data) {
const listeners = this._listeners.get(event)
if (listeners) {
listeners.forEach((callback) => {
try {
callback(data)
} catch (error) {
console.error(`[PlayerAdapter] 事件回调错误 (${event}):`, error)
}
})
}
}
/**
* 移除事件监听器
*/
off(event, callback) {
const listeners = this._listeners.get(event)
if (listeners) {
listeners.delete(callback)
}
}// ================== 生命周期方法 ==================
/**
* 初始化播放器
*/
async initialize(container, options = {}) {
throw new Error('子类必须实现 initialize 方法')
}
/**
* 销毁播放器
*/
async destroy() {
this.removeAllListeners()
this._tracks = []
this._currentIndex = 0
this._state = PlaybackState.IDLE
this._initialized = false
}
// ================== 播放控制方法 ==================
async play() { throw new Error('子类必须实现') }
async pause() { throw new Error('子类必须实现') }
async stop() { throw new Error('子类必须实现') }
async seek(time) { throw new Error('子类必须实现') }
// ================== 音量控制 ==================
getVolume() { throw new Error('子类必须实现') }
setVolume(volume) { throw new Error('子类必须实现') }
// ================== 进度查询 ==================
getCurrentTime() { throw new Error('子类必须实现') }
getDuration() { throw new Error('子类必须实现') }
// ================== 曲目列表管理 ==================
async loadPlaylist(tracks) { throw new Error('子类必须实现') }
getCurrentTrack() {
if (this._tracks.length === 0) return null
return this._tracks[this._currentIndex] || null
}
getTrackList() {
return [...this._tracks]
}
// ================== 切歌控制 ==================
async skipNext() { throw new Error('子类必须实现') }
async skipPrevious() { throw new Error('子类必须实现') }
async switchTrack(index) { throw new Error('子类必须实现') }不同播放器有不同的能力,通过查询方法让上层代码做出相应处理:
/**
* 是否支持歌词显示
*/
supportsLyrics() {
return false
}
/**
* 是否支持跳转
*/
supportsSeek() {
return true
}
/**
* 是否有内置 UI
*/
hasBuiltInUI() {
return false
}
/**
* 是否内部管理播放列表(如 Spotify iframe)
*/
hasInternalPlaylist() {
return false
}
/**
* 获取适配器类型标识
*/
getAdapterType() {
throw new Error('子类必须实现 getAdapterType 方法')
}// src/player/constants.js
/**
* 播放状态枚举
*/
export const PlaybackState = {
IDLE: 'idle',
PLAYING: 'playing',
PAUSED: 'paused',
BUFFERING: 'buffering',
ENDED: 'ended',
ERROR: 'error'
}
/**
* 播放器事件枚举
*/
export const PlayerEvent = {
PLAY: 'play',
PAUSE: 'pause',
STOP: 'stop',
ENDED: 'ended',
TIME_UPDATE: 'timeupdate',
VOLUME_CHANGE: 'volumechange',
TRACK_CHANGE: 'trackchange',
ERROR: 'error',
READY: 'ready',
PLAYLIST_LOADED: 'playlistloaded',
STATE_CHANGE: 'statechange'
}
/**
* 循环模式枚举
*/
export const RepeatMode = {
NONE: 'none',
ALL: 'all',
ONE: 'one'
}
/**
* 适配器类型枚举
*/
export const AdapterType = {
APLAYER: 'aplayer',
SPOTIFY: 'spotify'
}APlayer 适配器包装 APlayer 库,提供完整的播放控制。
// src/player/adapters/APlayerAdapter.js
export class APlayerAdapter extends PlayerAdapter {
constructor() {
super()
this._aplayer = null
this._aplayerListeners = new Map()
this._container = null
}
getAdapterType() {
return AdapterType.APLAYER
}
async initialize(container, options = {}) {
if (this._initialized && this._aplayer) {
await this.destroy()
}
this._container = container
const defaultOptions = {
container,
fixed: true,
autoplay: false,
audio: [],
lrcType: 0,
theme: '#2980b9',
loop: 'all',
order: 'list',
preload: 'auto',
volume: getConfig('AUDIO_CONFIG', 'DEFAULT_VOLUME'),
mutex: true,
listFolded: false,
listMaxHeight: '200px',
width: '300px'
}
this._aplayer = new APlayer({
...defaultOptions,
...options
})
this._bindAPlayerEvents()
this._initialized = true
this.emit(PlayerEvent.READY)
}将 APlayer 原生事件映射到统一事件:
_bindAPlayerEvents() {
if (!this._aplayer) return
const handlers = {
// APlayer 事件 → 适配器事件
play: () => {
this._setPlaybackState(PlaybackState.PLAYING)
this.emit(PlayerEvent.PLAY)
},
pause: () => {
this._setPlaybackState(PlaybackState.PAUSED)
this.emit(PlayerEvent.PAUSE)
},
ended: () => {
this._setPlaybackState(PlaybackState.ENDED)
this.emit(PlayerEvent.ENDED)
},
error: () => {
this._setPlaybackState(PlaybackState.ERROR)
this.emit(PlayerEvent.ERROR)
},
canplay: () => {
if (this._state === PlaybackState.BUFFERING) {
this._setPlaybackState(PlaybackState.PAUSED)
}
},
waiting: () => {
this._setPlaybackState(PlaybackState.BUFFERING)
},
timeupdate: () => {
this.emit(PlayerEvent.TIME_UPDATE, {
currentTime: this.getCurrentTime(),
duration: this.getDuration()
})
},
volumechange: () => {
this.emit(PlayerEvent.VOLUME_CHANGE, this.getVolume())
},
listswitch: (event) => {
this._currentIndex = event.index
const track = this._tracks[event.index]
this.emit(PlayerEvent.TRACK_CHANGE, {
index: event.index,
track
})
}
}
// 绑定并保存引用(用于销毁时清理)
Object.entries(handlers).forEach(([event, handler]) => {
this._aplayer.on(event, handler)
this._aplayerListeners.set(event, handler)
})
}async play() {
if (!this._aplayer) return
this._aplayer.play()
}
async pause() {
if (!this._aplayer) return
this._aplayer.pause()
}
async stop() {
if (!this._aplayer) return
this._aplayer.pause()
this._aplayer.seek(0)
this._setPlaybackState(PlaybackState.IDLE)
this.emit(PlayerEvent.STOP)
}
async seek(time) {
if (!this._aplayer) return
const clampedTime = Math.max(0, Math.min(time, this.getDuration()))
this._aplayer.seek(clampedTime)
}
// 音量控制(直接操作 audio 元素)
getVolume() {
if (!this._aplayer?.audio) return 0
return this._aplayer.audio.volume
}
setVolume(volume) {
if (!this._aplayer?.audio) return
this._aplayer.audio.volume = Math.max(0, Math.min(1, volume))
}
// 进度查询
getCurrentTime() {
if (!this._aplayer?.audio) return 0
return this._aplayer.audio.currentTime || 0
}
getDuration() {
if (!this._aplayer?.audio) return 0
const duration = this._aplayer.audio.duration
return isFinite(duration) ? duration : 0
}/**
* 加载播放列表
* @param {UnifiedTrack[]} tracks - 统一格式曲目列表
*/
async loadPlaylist(tracks) {
if (!this._aplayer) return
// 保存 UnifiedTrack 列表
this._tracks = [...tracks]
// 转换为 APlayer 格式
const aplayerSongs = tracks.map(toAPlayerFormat)
// 清空现有列表并添加新曲目
this._aplayer.list.clear()
this._aplayer.list.add(aplayerSongs)
this._currentIndex = 0
this.emit(PlayerEvent.PLAYLIST_LOADED, { tracks: this._tracks })
}
async skipNext() {
if (!this._aplayer) return
this._aplayer.skipForward()
}
async skipPrevious() {
if (!this._aplayer) return
this._aplayer.skipBack()
}
async switchTrack(index) {
if (!this._aplayer) return
if (index < 0 || index >= this._tracks.length) return
this._aplayer.list.switch(index)
}supportsLyrics() {
return true // APlayer 支持歌词
}
supportsSeek() {
return true // 支持跳转
}
hasBuiltInUI() {
return true // APlayer 有内置 UI
}
hasInternalPlaylist() {
return false // 播放列表由外部管理
}Spotify 适配器目前采用 iframe 嵌入模式,大部分控制方法为 no-op。
由于 Spotify Embed API 的限制,iframe 模式下无法:
- 程序化控制播放/暂停
- 获取播放进度
- 控制音量
- 切换曲目
用户需要在 Spotify embed 界面内直接操作。
// src/player/adapters/SpotifyAdapter.js
export class SpotifyAdapter extends PlayerAdapter {
constructor() {
super()
this._playlistId = null
this._volume = 0.7 // 模拟值
}
getAdapterType() {
return AdapterType.SPOTIFY
}
async initialize(_container, options = {}) {
this._playlistId = options.playlistId || null
this._initialized = true
this._setPlaybackState(PlaybackState.IDLE)
if (this._playlistId) {
this._tracks = [this._createPlaceholderTrack()]
}
this.emit(PlayerEvent.READY)
}
/**
* 创建占位曲目(用于 Media Session 显示)
*/
_createPlaceholderTrack() {
return createUnifiedTrack({
id: `spotify_playlist_${this._playlistId}`,
name: 'Spotify Playlist',
artist: 'Spotify',
meta: { source: 'spotify', sourceId: this._playlistId }
})
}
// ================== 播放控制(no-op) ==================
async play() {
console.debug('[SpotifyAdapter] play() - iframe 模式无法控制')
}
async pause() {
console.debug('[SpotifyAdapter] pause() - iframe 模式无法控制')
}
async seek(_time) {
console.debug('[SpotifyAdapter] seek() - iframe 模式无法控制')
}
// ================== 音量(模拟值) ==================
getVolume() {
return this._volume
}
setVolume(volume) {
this._volume = Math.max(0, Math.min(1, volume))
this.emit(PlayerEvent.VOLUME_CHANGE, this._volume)
}
// ================== 进度(无法获取) ==================
getCurrentTime() {
return 0
}
getDuration() {
return 0
}
// ================== 能力声明 ==================
supportsLyrics() {
return false // Spotify embed 有自己的歌词
}
supportsSeek() {
return false // iframe 无法控制跳转
}
hasBuiltInUI() {
return true
}
hasInternalPlaylist() {
return true // Spotify 完全内部管理播放列表
}
}usePlayer 是播放器系统的状态管理层,提供响应式状态和适配器切换。
// src/composables/usePlayer.js
// 模块级单例状态
const adapter = shallowRef(null)
const playbackState = ref(PlaybackState.IDLE)
const currentTrack = ref(null)
const currentTime = ref(0)
const duration = ref(0)
const volume = ref(getConfig('AUDIO_CONFIG', 'DEFAULT_VOLUME'))
const trackIndex = ref(0)
const trackList = ref([])
const adapterType = ref('')
// 事件取消订阅函数列表
let eventUnsubscribers = []/**
* 绑定适配器事件到响应式状态
*/
function bindAdapterEvents(adapterInstance) {
unbindAdapterEvents() // 清除旧订阅
// 状态变化
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.STATE_CHANGE, (state) => {
playbackState.value = state
})
)
// 时间更新
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.TIME_UPDATE, (data) => {
currentTime.value = data.currentTime
duration.value = data.duration
})
)
// 音量变化
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.VOLUME_CHANGE, (vol) => {
volume.value = vol
})
)
// 曲目切换
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.TRACK_CHANGE, (data) => {
trackIndex.value = data.index
currentTrack.value = data.track
})
)
// 播放列表加载
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.PLAYLIST_LOADED, (data) => {
trackList.value = data.tracks || []
if (trackList.value.length > 0) {
currentTrack.value = trackList.value[0]
trackIndex.value = 0
}
})
)
// 错误/结束
eventUnsubscribers.push(
adapterInstance.on(PlayerEvent.ERROR, () => {
playbackState.value = PlaybackState.ERROR
}),
adapterInstance.on(PlayerEvent.ENDED, () => {
playbackState.value = PlaybackState.ENDED
})
)
}/**
* 设置适配器
*/
async function setAdapter(newAdapter) {
// 销毁旧适配器
if (adapter.value) {
unbindAdapterEvents()
await adapter.value.destroy()
}
adapter.value = newAdapter
adapterType.value = newAdapter.getAdapterType()
// 绑定新适配器事件
bindAdapterEvents(newAdapter)
// 同步初始状态
if (newAdapter.isInitialized()) {
playbackState.value = newAdapter.getPlaybackState()
volume.value = newAdapter.getVolume()
currentTrack.value = newAdapter.getCurrentTrack()
trackIndex.value = newAdapter.getCurrentTrackIndex()
trackList.value = newAdapter.getTrackList()
}
}通知播放时自动降低音量,通知结束后恢复:
let volumeRestoreTimer = null
let originalVolume = getConfig('AUDIO_CONFIG', 'DEFAULT_VOLUME')
/**
* 平滑音量过渡
*/
function fadeVolume(targetVolume, durationMs) {
const fadeDuration = durationMs ?? getConfig('AUDIO_CONFIG', 'DEFAULT_FADE_DURATION')
const fadeSteps = getConfig('AUDIO_CONFIG', 'VOLUME_FADE_STEPS')
return new Promise((resolve) => {
const startVolume = getVolume()
const volumeDiff = targetVolume - startVolume
const stepDuration = fadeDuration / fadeSteps
let currentStep = 0
const interval = setInterval(() => {
currentStep++
const progress = currentStep / fadeSteps
// ease-out cubic 缓动
const easeProgress = 1 - Math.pow(1 - progress, 3)
const newVolume = startVolume + volumeDiff * easeProgress
setVolume(Math.max(0, Math.min(1, newVolume)))
if (currentStep >= fadeSteps) {
clearInterval(interval)
setVolume(targetVolume)
resolve()
}
}, stepDuration)
})
}
/**
* 通知音量闪避
*/
async function duckForNotification(notificationDuration) {
const duration = notificationDuration ?? getConfig('AUDIO_CONFIG', 'NOTIFICATION_DURATION')
if (volumeRestoreTimer) {
clearTimeout(volumeRestoreTimer)
}
// 保存原始音量,降低到 20%
originalVolume = getVolume()
const duckedVolume = originalVolume * getConfig('AUDIO_CONFIG', 'VOLUME_DUCK_RATIO')
const fadeDuration = getConfig('AUDIO_CONFIG', 'VOLUME_FADE_DURATION')
await fadeVolume(duckedVolume, fadeDuration)
// 设置恢复定时器
volumeRestoreTimer = setTimeout(() => {
fadeVolume(originalVolume, fadeDuration)
volumeRestoreTimer = null
}, duration)
}export function usePlayer() {
return {
// 响应式状态(只读)
playbackState: readonly(playbackState),
currentTrack: readonly(currentTrack),
currentTime: readonly(currentTime),
duration: readonly(duration),
volume: readonly(volume),
trackIndex: readonly(trackIndex),
trackList: readonly(trackList),
adapterType: readonly(adapterType),
// Computed
isPlaying,
isPaused,
isBuffering,
progress,
// 适配器管理
setAdapter,
getAdapter,
initialize,
destroy,
// 播放控制
play, pause, stop, seek,
// 音量控制
getVolume, setVolume, fadeVolume, duckForNotification,
// 切歌控制
skipNext, skipPrevious, switchTrack,
// 播放列表
loadPlaylist,
// 能力查询
hasCapability
}
}Media Session API 允许系统媒体控制(锁屏、通知栏)与应用交互。
flowchart TB
subgraph usePlayerState["usePlayer"]
VueRefs["Vue Refs: currentTrack, playbackState,<br/>currentTime, duration, ..."]
end
subgraph MediaSessionAPI["navigator.mediaSession"]
Metadata["metadata (曲目信息)"]
PlaybackState["playbackState (播放状态)"]
PositionState["positionState (播放进度)"]
end
usePlayerMethods["usePlayer 方法<br/>(play, pause, skipNext, skipPrevious, seek)"]
VueRefs -->|"watch() - 播放器状态变化时更新"| MediaSessionAPI
MediaSessionAPI -->|"setActionHandler() - 系统控制时调用"| usePlayerMethods
// src/player/mediaSessionBridge.js
export function setupMediaSession(player) {
if (!('mediaSession' in navigator)) {
return () => {}
}
const unwatchers = []
// ===== 1. 元数据更新(播放器 → 系统) =====
unwatchers.push(
watch(
() => player.currentTrack.value,
(track) => {
if (!track) {
navigator.mediaSession.metadata = null
return
}
navigator.mediaSession.metadata = new MediaMetadata({
title: track.name || 'Unknown',
artist: track.artist || 'Unknown Artist',
album: track.album || '',
artwork: track.cover
? [
{ src: track.cover, sizes: '96x96', type: 'image/jpeg' },
{ src: track.cover, sizes: '192x192', type: 'image/jpeg' },
{ src: track.cover, sizes: '512x512', type: 'image/jpeg' }
]
: []
})
},
{ immediate: true }
)
)
// ===== 2. 播放状态更新 =====
unwatchers.push(
watch(
() => player.playbackState.value,
(state) => {
switch (state) {
case PlaybackState.PLAYING:
navigator.mediaSession.playbackState = 'playing'
break
case PlaybackState.PAUSED:
case PlaybackState.IDLE:
case PlaybackState.ENDED:
navigator.mediaSession.playbackState = 'paused'
break
default:
navigator.mediaSession.playbackState = 'none'
}
},
{ immediate: true }
)
)
// ===== 3. 进度位置更新(节流) =====
let lastPositionUpdate = 0
unwatchers.push(
watch(
() => player.currentTime.value,
() => {
const now = Date.now()
const interval = getConfig('UI_CONFIG', 'MEDIA_POSITION_UPDATE_INTERVAL')
if (now - lastPositionUpdate < interval) return
lastPositionUpdate = now
const durationValue = player.duration.value
const position = player.currentTime.value
if (!isFinite(durationValue) || durationValue <= 0) return
try {
navigator.mediaSession.setPositionState({
duration: durationValue,
playbackRate: 1.0,
position: Math.min(position, durationValue)
})
} catch {}
}
)
)
// ===== 4. Action Handlers(系统 → 播放器) =====
const actions = {
play: () => player.play(),
pause: () => player.pause(),
previoustrack: () => player.skipPrevious(),
nexttrack: () => player.skipNext(),
seekforward: (details) => {
const offset = details?.seekOffset || getConfig('UI_CONFIG', 'MEDIA_SEEK_OFFSET')
player.seek(player.currentTime.value + offset)
},
seekbackward: (details) => {
const offset = details?.seekOffset || getConfig('UI_CONFIG', 'MEDIA_SEEK_OFFSET')
player.seek(Math.max(0, player.currentTime.value - offset))
},
seekto: (details) => {
if (details?.seekTime != null) {
player.seek(details.seekTime)
}
}
}
for (const [action, handler] of Object.entries(actions)) {
try {
navigator.mediaSession.setActionHandler(action, handler)
} catch {}
}
// ===== 5. Cleanup 函数 =====
return () => {
unwatchers.forEach(unwatch => unwatch())
for (const action of Object.keys(actions)) {
try {
navigator.mediaSession.setActionHandler(action, null)
} catch {}
}
navigator.mediaSession.metadata = null
navigator.mediaSession.playbackState = 'none'
}
}// src/player/adapters/MyPlayerAdapter.js
import { PlayerAdapter } from '../PlayerAdapter.js'
import { PlaybackState, PlayerEvent, AdapterType } from '../constants.js'
export class MyPlayerAdapter extends PlayerAdapter {
constructor() {
super()
this._player = null
}
getAdapterType() {
return 'myplayer' // 添加到 AdapterType 枚举
}
async initialize(container, options = {}) {
// 初始化播放器
this._player = new MyPlayer(container, options)
// 绑定事件
this._player.on('play', () => {
this._setPlaybackState(PlaybackState.PLAYING)
this.emit(PlayerEvent.PLAY)
})
// ... 其他事件
this._initialized = true
this.emit(PlayerEvent.READY)
}
async play() {
this._player?.play()
}
async pause() {
this._player?.pause()
}
// ... 实现其他必需方法
}// src/player/constants.js
export const AdapterType = {
APLAYER: 'aplayer',
SPOTIFY: 'spotify',
MYPLAYER: 'myplayer' // 新增
}import { MyPlayerAdapter } from '@/player/adapters/MyPlayerAdapter.js'
import { usePlayer } from '@/composables/usePlayer.js'
const player = usePlayer()
const adapter = new MyPlayerAdapter()
await adapter.initialize(containerElement, options)
await player.setAdapter(adapter)根据播放器实际能力实现查询方法:
supportsLyrics() {
return true // 如果支持歌词
}
supportsSeek() {
return true // 如果支持跳转
}
hasBuiltInUI() {
return false // 如果需要自定义 UI
}
hasInternalPlaylist() {
return false // 如果播放列表由外部管理
}