music / src /utils /mediaSession.js
ahutchen's picture
perf: 优化专辑封面图片尺寸
3b9298b
/**
* Media Session API 管理工具
* 专门处理iOS PWA的锁屏和灵动岛专辑图片显示
*
* 基于2024年最佳实践:
* - iOS 18修复了低分辨率图片问题,现在使用512x512而不是像素化放大
* - 提供多尺寸图片以兼容不同设备和显示模式
* - 使用Canvas优化图片以确保iOS兼容性
*/
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 不支持')
}
}
/**
* 设置媒体会话信息
* @param {Object} song - 歌曲信息
* @param {Object} options - 选项
* @param {Function} options.onPlay - 播放回调
* @param {Function} options.onPause - 暂停回调
* @param {Function} options.onNext - 下一首回调
* @param {Function} options.onPrev - 上一首回调
*/
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)
}
}
/**
* 获取优化后的专辑封面
* 使用Canvas处理以确保iOS兼容性
*/
async getOptimizedArtwork(song) {
// 尝试获取真实的专辑封面
let coverUrl = null
if (song.cover) {
coverUrl = song.cover
} else {
// 尝试从playerStore获取封面
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]
}
/**
* 创建多尺寸专辑封面
* 基于iOS 18的最佳实践
*/
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]
}
}
/**
* 使用Canvas处理图片,确保iOS兼容性
*/
async processImageForMediaSession(imageUrl) {
return new Promise((resolve, reject) => {
const image = new Image()
image.crossOrigin = 'anonymous'
image.onload = async () => {
try {
// 创建多个尺寸的Canvas版本
const sizes = [
{ width: 96, height: 96 }, // 小播放器
{ width: 128, height: 128 }, // 通知
{ width: 192, height: 192 }, // 中等尺寸
{ width: 256, height: 256 }, // 低端设备
{ width: 512, height: 512 } // 高分辨率,iOS 18推荐
]
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)
// 转换为blob URL
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 // 'playing', 'paused', 'none'
} 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() {
// 清理blob URLs
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 }