Skip to content

Player System

gxxk-dev edited this page Jan 31, 2026 · 2 revisions

播放器系统 (Player System)

本文档详细介绍 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
Loading

文件结构

src/player/
├── PlayerAdapter.js          # 抽象基类
├── adapters/
│   ├── APlayerAdapter.js     # APlayer 适配器
│   └── SpotifyAdapter.js     # Spotify 适配器
├── mediaSessionBridge.js     # Media Session 桥接
├── constants.js              # 常量定义
└── sources/                  # 音乐源相关(可选)

PlayerAdapter 抽象基类

核心设计

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'
}

APlayerAdapter 实现

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  // 播放列表由外部管理
}

SpotifyAdapter 实现

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 Composable

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)
}

导出 API

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 桥接

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
Loading

实现代码

// 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'
  }
}

如何添加新播放器适配器

步骤 1: 创建适配器类

// 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()
  }

  // ... 实现其他必需方法
}

步骤 2: 注册适配器类型

// src/player/constants.js

export const AdapterType = {
  APLAYER: 'aplayer',
  SPOTIFY: 'spotify',
  MYPLAYER: 'myplayer'  // 新增
}

步骤 3: 在应用中使用

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)

步骤 4: 声明能力

根据播放器实际能力实现查询方法:

supportsLyrics() {
  return true  // 如果支持歌词
}

supportsSeek() {
  return true  // 如果支持跳转
}

hasBuiltInUI() {
  return false  // 如果需要自定义 UI
}

hasInternalPlaylist() {
  return false  // 如果播放列表由外部管理
}

Clone this wiki locally