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