diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 4e379a3..fe3fb30 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') implementation project(':capacitor-haptics') + implementation project(':capgo-capacitor-media-session') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index df5cd63..0603a5b 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -7,3 +7,6 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/ include ':capacitor-haptics' project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') + +include ':capgo-capacitor-media-session' +project(':capgo-capacitor-media-session').projectDir = new File('../node_modules/@capgo/capacitor-media-session/android') diff --git a/bun.lock b/bun.lock index 8494616..ea7a58f 100644 --- a/bun.lock +++ b/bun.lock @@ -10,6 +10,7 @@ "@capacitor/core": "^8.2.0", "@capacitor/haptics": "^8.0.1", "@capacitor/ios": "^8.2.0", + "@capgo/capacitor-media-session": "^8.0.19", "@dantheman827/taglib-ts": "^0.1.5", "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", @@ -18,7 +19,7 @@ "@svta/common-media-library": "^0.18.1", "@types/wicg-file-system-access": "^2023.10.7", "@typescript-eslint/eslint-plugin": "^8.57.2", - "@uimaxbai/am-lyrics": "^1.1.7", + "@uimaxbai/am-lyrics": "^1.1.8", "@vitest/web-worker": "^4.1.2", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", @@ -277,6 +278,8 @@ "@capacitor/ios": ["@capacitor/ios@8.3.0", "", { "peerDependencies": { "@capacitor/core": "^8.3.0" } }, "sha512-5Rtwv8SITKlYTt8lAZG+khnVIdzPtqbocH3eP+JkEmX1vpSMwx4TOKtT8OBz8gpQ+pUJDRp7DBYOv3U6l/obCw=="], + "@capgo/capacitor-media-session": ["@capgo/capacitor-media-session@8.0.19", "", { "peerDependencies": { "@capacitor/core": ">=8.0.0" } }, "sha512-m0eCJvnuYpxz3wj3Snc1kIHCr45XQOBgPwOhkId/xXZ01DzXsOpw/lIT8Fl/Y1AscpNxs2fF5Qoj4E8QMIyJJg=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260401.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZSmceM70jH6k+/62VkEcmMNzrpr4kSctkX5Lsgqv38KktfhPY/hsh75y1lRoPWS3H3kgMa4p2pUSlidZR1u2hw=="], "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260401.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7UKWF+IUZ3NXMVPsDg8Cjg0r58b+uYlfvs5Yt8bvtU+geCtW4P2MxRHmRSEo8SryckXOJjb/b8tcncgCykFu8g=="], @@ -675,7 +678,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="], - "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.1.7", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-hEwPl4dFmJ08sJf4VBaR7k7yxA3BNaoINS89j0KrkSFJYpCkohHDy24AIfzEMonPloJ3H6HBA55nCFMnAzm50w=="], + "@uimaxbai/am-lyrics": ["@uimaxbai/am-lyrics@1.1.8", "", { "dependencies": { "@babel/runtime": "^7.27.6", "lit": "^3.1.4" }, "peerDependencies": { "@lit/react": "^1.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@lit/react", "react"] }, "sha512-VcbrlB2cOmkOjElmivf2SZujDmj8UAUaBkXyIfJ8dYq/Iv4H3PxmQY/s9VaRfF6UTnCgfix8ZPll1T1MA8eS4A=="], "@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="], diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift index 80f1c8f..b94a368 100644 --- a/ios/App/CapApp-SPM/Package.swift +++ b/ios/App/CapApp-SPM/Package.swift @@ -11,9 +11,10 @@ let package = Package( targets: ["CapApp-SPM"]) ], dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.2.0"), + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.0"), .package(name: "CapacitorApp", path: "../../../node_modules/@capacitor/app"), - .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics") + .package(name: "CapacitorHaptics", path: "../../../node_modules/@capacitor/haptics"), + .package(name: "CapgoCapacitorMediaSession", path: "../../../node_modules/@capgo/capacitor-media-session") ], targets: [ .target( @@ -22,7 +23,8 @@ let package = Package( .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), .product(name: "CapacitorApp", package: "CapacitorApp"), - .product(name: "CapacitorHaptics", package: "CapacitorHaptics") + .product(name: "CapacitorHaptics", package: "CapacitorHaptics"), + .product(name: "CapgoCapacitorMediaSession", package: "CapgoCapacitorMediaSession") ] ) ] diff --git a/js/player.js b/js/player.js index 508e9d5..96e3a92 100644 --- a/js/player.js +++ b/js/player.js @@ -23,6 +23,7 @@ import { db } from './db.js'; import { SVG_CLOCK, SVG_ATMOS } from './icons.js'; import { UIRenderer } from './ui.js'; +import { MediaSession } from '@capgo/capacitor-media-session'; export class Player { static #instance = null; @@ -422,10 +423,8 @@ export class Player { } async setupMediaSession() { - if (!('mediaSession' in navigator)) return; - const setHandlers = async () => { - navigator.mediaSession.setActionHandler('play', async () => { + await MediaSession.setActionHandler({ action: 'play' }, async () => { const el = this.activeElement; // Initialize and resume audio context first (required for iOS lock screen) // Must happen before audio.play() or audio won't route through Web Audio @@ -444,11 +443,11 @@ export class Player { } }); - navigator.mediaSession.setActionHandler('pause', () => { + await MediaSession.setActionHandler({ action: 'pause' }, () => { this.activeElement.pause(); }); - navigator.mediaSession.setActionHandler('previoustrack', async () => { + await MediaSession.setActionHandler({ action: 'previoustrack' }, async () => { // Ensure audio context is active for iOS lock screen controls if (!audioContextManager.isReady()) { audioContextManager.init(this.activeElement); @@ -458,7 +457,7 @@ export class Player { this.playPrev(); }); - navigator.mediaSession.setActionHandler('nexttrack', async () => { + await MediaSession.setActionHandler({ action: 'nexttrack' }, async () => { // Ensure audio context is active for iOS lock screen controls if (!audioContextManager.isReady()) { audioContextManager.init(this.activeElement); @@ -469,24 +468,24 @@ export class Player { }); if (!this.isIOS) { - navigator.mediaSession.setActionHandler('seekbackward', (details) => { + await MediaSession.setActionHandler({ action: 'seekbackward' }, (details) => { const skipTime = details.seekOffset || 10; this.seekBackward(skipTime); }); - navigator.mediaSession.setActionHandler('seekforward', (details) => { + await MediaSession.setActionHandler({ action: 'seekforward' }, (details) => { const skipTime = details.seekOffset || 10; this.seekForward(skipTime); }); } - navigator.mediaSession.setActionHandler('seekto', (details) => { + await MediaSession.setActionHandler({ action: 'seekto' }, (details) => { if (details.seekTime !== undefined) { this.activeElement.currentTime = Math.max(0, details.seekTime); this.updateMediaSessionPositionState(); } }); - navigator.mediaSession.setActionHandler('stop', () => { + await MediaSession.setActionHandler({ action: 'stop' }, () => { this.activeElement.pause(); this.activeElement.currentTime = 0; this.updateMediaSessionPlaybackState(); @@ -2070,37 +2069,38 @@ export class Player { } updateMediaSession(track) { - if (!('mediaSession' in navigator)) return; - - // Force a refresh for picky Bluetooth systems by clearing metadata first - navigator.mediaSession.metadata = null; const coverId = track.album?.cover; const trackTitle = getTrackTitle(track); - navigator.mediaSession.metadata = new MediaMetadata({ - title: trackTitle || 'Unknown Title', - artist: getTrackArtists(track) || 'Unknown Artist', - album: track.album?.title || 'Unknown Album', - artwork: coverId - ? [ - { - src: this.api.getCoverUrl(coverId, '1280'), - sizes: '1280x1280', - type: 'image/jpeg', - }, - ] - : undefined, + // Force a refresh for picky Bluetooth systems by clearing metadata first + MediaSession.setMetadata({}) + .finally(() => + MediaSession.setMetadata({ + title: trackTitle || 'Unknown Title', + artist: getTrackArtists(track) || 'Unknown Artist', + album: track.album?.title || 'Unknown Album', + artwork: coverId + ? [ + { + src: this.api.getCoverUrl(coverId, '1280'), + sizes: '1280x1280', + type: 'image/jpeg', + }, + ] + : undefined, + }) + ) + .catch(() => {}) + .finally(() => { + this.updateMediaSessionPlaybackState(); + this.updateMediaSessionPositionState(); }); - - this.updateMediaSessionPlaybackState(); - this.updateMediaSessionPositionState(); } updateMediaSessionPlaybackState() { - if (!('mediaSession' in navigator)) return; const isPlaying = !this.activeElement.paused; - navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'; + void MediaSession.setPlaybackState({ playbackState: isPlaying ? 'playing' : 'paused' }); // Start/stop Android foreground service to prevent background audio throttling this._updateBackgroundAudioService(isPlaying); @@ -2137,9 +2137,6 @@ export class Player { } updateMediaSessionPositionState() { - if (!('mediaSession' in navigator)) return; - if (!('setPositionState' in navigator.mediaSession)) return; - const el = this.activeElement; const duration = el.duration; @@ -2147,15 +2144,14 @@ export class Player { return; } - try { - navigator.mediaSession.setPositionState({ - duration: duration, - playbackRate: el.playbackRate || 1, - position: Math.min(el.currentTime, duration), - }); - } catch (error) { - console.log('Failed to update Media Session position:', error); - } + MediaSession.setPositionState({ + duration: duration, + playbackRate: el.playbackRate || 1, + position: Math.min(el.currentTime, duration), + }) + .catch((error) => { + console.log('Failed to update Media Session position:', error); + }); } async safePlay(element = this.activeElement) { diff --git a/package.json b/package.json index 2e03d4e..6a22ef6 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@capacitor/core": "^8.2.0", "@capacitor/haptics": "^8.0.1", "@capacitor/ios": "^8.2.0", + "@capgo/capacitor-media-session": "^8.0.19", "@dantheman827/taglib-ts": "^0.1.5", "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15",