|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MediaSessionManager { |
|
|
constructor() { |
|
|
this.isSupported = 'mediaSession' in navigator |
|
|
this.currentBlobUrl = null |
|
|
this.artworkCache = new Map() |
|
|
|
|
|
|
|
|
this.defaultArtwork = { |
|
|
src: `${import.meta.env.BASE_URL}icons/icon-512x512.svg`, |
|
|
sizes: '512x512', |
|
|
type: 'image/svg+xml' |
|
|
} |
|
|
|
|
|
if (this.isSupported) { |
|
|
console.log('Media Session API 支持已启用') |
|
|
this.setupActionHandlers() |
|
|
} else { |
|
|
console.log('Media Session API 不支持') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async setMetadata(song, options = {}) { |
|
|
if (!this.isSupported || !song) { |
|
|
return |
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
const artwork = await this.getOptimizedArtwork(song) |
|
|
|
|
|
|
|
|
navigator.mediaSession.metadata = new MediaMetadata({ |
|
|
title: song.name || '未知歌曲', |
|
|
artist: this.formatArtist(song.artist) || '未知艺术家', |
|
|
album: song.album || '未知专辑', |
|
|
artwork: artwork |
|
|
}) |
|
|
|
|
|
|
|
|
if (options.onPlay) this.onPlayHandler = options.onPlay |
|
|
if (options.onPause) this.onPauseHandler = options.onPause |
|
|
if (options.onNext) this.onNextHandler = options.onNext |
|
|
if (options.onPrev) this.onPrevHandler = options.onPrev |
|
|
|
|
|
console.log('Media Session 元数据已更新:', song.name) |
|
|
} catch (error) { |
|
|
console.error('设置 Media Session 元数据失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getOptimizedArtwork(song) { |
|
|
|
|
|
let coverUrl = null |
|
|
|
|
|
if (song.cover) { |
|
|
coverUrl = song.cover |
|
|
} else { |
|
|
|
|
|
try { |
|
|
const { usePlayerStore } = await import('@/stores/player') |
|
|
const playerStore = usePlayerStore() |
|
|
coverUrl = await playerStore.getAlbumCover(song, 300) |
|
|
} catch (error) { |
|
|
console.warn('获取专辑封面失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (coverUrl && coverUrl !== this.defaultArtwork.src) { |
|
|
return await this.createMultiSizeArtwork(coverUrl, song) |
|
|
} |
|
|
|
|
|
|
|
|
return [this.defaultArtwork] |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async createMultiSizeArtwork(imageUrl, song) { |
|
|
const cacheKey = `${song.source}-${song.id || song.pic_id}` |
|
|
|
|
|
|
|
|
if (this.artworkCache.has(cacheKey)) { |
|
|
return this.artworkCache.get(cacheKey) |
|
|
} |
|
|
|
|
|
try { |
|
|
const artwork = await this.processImageForMediaSession(imageUrl) |
|
|
this.artworkCache.set(cacheKey, artwork) |
|
|
return artwork |
|
|
} catch (error) { |
|
|
console.error('处理专辑封面失败:', error) |
|
|
return [this.defaultArtwork] |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async processImageForMediaSession(imageUrl) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const image = new Image() |
|
|
image.crossOrigin = 'anonymous' |
|
|
|
|
|
image.onload = async () => { |
|
|
try { |
|
|
|
|
|
const sizes = [ |
|
|
{ width: 96, height: 96 }, |
|
|
{ width: 128, height: 128 }, |
|
|
{ width: 192, height: 192 }, |
|
|
{ width: 256, height: 256 }, |
|
|
{ width: 512, height: 512 } |
|
|
] |
|
|
|
|
|
const artwork = [] |
|
|
|
|
|
|
|
|
for (const size of sizes) { |
|
|
const canvas = document.createElement('canvas') |
|
|
canvas.width = size.width |
|
|
canvas.height = size.height |
|
|
const ctx = canvas.getContext('2d') |
|
|
|
|
|
|
|
|
ctx.imageSmoothingEnabled = true |
|
|
ctx.imageSmoothingQuality = 'high' |
|
|
|
|
|
|
|
|
ctx.drawImage(image, 0, 0, size.width, size.height) |
|
|
|
|
|
|
|
|
const blob = await new Promise(resolve => { |
|
|
canvas.toBlob(resolve, 'image/png', 0.9) |
|
|
}) |
|
|
|
|
|
if (blob) { |
|
|
const blobUrl = URL.createObjectURL(blob) |
|
|
artwork.push({ |
|
|
src: blobUrl, |
|
|
sizes: `${size.width}x${size.height}`, |
|
|
type: 'image/png' |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
resolve(artwork) |
|
|
} catch (error) { |
|
|
reject(error) |
|
|
} |
|
|
} |
|
|
|
|
|
image.onerror = () => { |
|
|
reject(new Error('图片加载失败')) |
|
|
} |
|
|
|
|
|
image.src = imageUrl |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setPlaybackState(state) { |
|
|
if (!this.isSupported) return |
|
|
|
|
|
try { |
|
|
navigator.mediaSession.playbackState = state |
|
|
} catch (error) { |
|
|
console.error('设置播放状态失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setPositionState(duration, position, playbackRate = 1.0) { |
|
|
if (!this.isSupported) return |
|
|
|
|
|
try { |
|
|
if ('setPositionState' in navigator.mediaSession) { |
|
|
navigator.mediaSession.setPositionState({ |
|
|
duration: duration || 0, |
|
|
position: position || 0, |
|
|
playbackRate: playbackRate |
|
|
}) |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('设置播放位置失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setupActionHandlers() { |
|
|
if (!this.isSupported) return |
|
|
|
|
|
const actions = [ |
|
|
'play', |
|
|
'pause', |
|
|
'previoustrack', |
|
|
'nexttrack', |
|
|
'seekbackward', |
|
|
'seekforward', |
|
|
'seekto' |
|
|
] |
|
|
|
|
|
actions.forEach(action => { |
|
|
try { |
|
|
navigator.mediaSession.setActionHandler(action, (details) => { |
|
|
this.handleAction(action, details) |
|
|
}) |
|
|
} catch (error) { |
|
|
console.warn(`不支持的媒体动作: ${action}`) |
|
|
} |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleAction(action, details) { |
|
|
console.log('媒体控制动作:', action, details) |
|
|
|
|
|
switch (action) { |
|
|
case 'play': |
|
|
if (this.onPlayHandler) this.onPlayHandler() |
|
|
break |
|
|
case 'pause': |
|
|
if (this.onPauseHandler) this.onPauseHandler() |
|
|
break |
|
|
case 'previoustrack': |
|
|
if (this.onPrevHandler) this.onPrevHandler() |
|
|
break |
|
|
case 'nexttrack': |
|
|
if (this.onNextHandler) this.onNextHandler() |
|
|
break |
|
|
case 'seekto': |
|
|
if (this.onSeekToHandler && details.seekTime !== undefined) { |
|
|
this.onSeekToHandler(details.seekTime) |
|
|
} |
|
|
break |
|
|
case 'seekbackward': |
|
|
if (this.onSeekBackwardHandler) { |
|
|
this.onSeekBackwardHandler(details.seekOffset || 10) |
|
|
} |
|
|
break |
|
|
case 'seekforward': |
|
|
if (this.onSeekForwardHandler) { |
|
|
this.onSeekForwardHandler(details.seekOffset || 10) |
|
|
} |
|
|
break |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
formatArtist(artist) { |
|
|
if (!artist) return '未知艺术家' |
|
|
if (Array.isArray(artist)) { |
|
|
return artist.join(', ') |
|
|
} |
|
|
if (typeof artist === 'string') { |
|
|
return artist.replace(/;|;/g, ', ') |
|
|
} |
|
|
return String(artist) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cleanup() { |
|
|
|
|
|
this.artworkCache.forEach(artwork => { |
|
|
artwork.forEach(art => { |
|
|
if (art.src && art.src.startsWith('blob:')) { |
|
|
URL.revokeObjectURL(art.src) |
|
|
} |
|
|
}) |
|
|
}) |
|
|
this.artworkCache.clear() |
|
|
|
|
|
if (this.currentBlobUrl) { |
|
|
URL.revokeObjectURL(this.currentBlobUrl) |
|
|
this.currentBlobUrl = null |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reset() { |
|
|
if (!this.isSupported) return |
|
|
|
|
|
try { |
|
|
navigator.mediaSession.metadata = null |
|
|
navigator.mediaSession.playbackState = 'none' |
|
|
} catch (error) { |
|
|
console.error('重置媒体会话失败:', error) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const mediaSessionManager = new MediaSessionManager() |
|
|
|
|
|
export default mediaSessionManager |
|
|
export { MediaSessionManager } |