feat(player): use @capgo/capacitor-media-session for android compatibility
This commit is contained in:
parent
6b3f78e608
commit
56f8620505
6 changed files with 55 additions and 49 deletions
|
|
@ -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')
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
7
bun.lock
7
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=="],
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
|
|
|
|||
84
js/player.js
84
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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue