-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
XCode 15: plugin no longer working on iOS. Does not show info, does not register control events #103
Comments
A workaround is to check if the platform is ios and use MediaSession API instead, to create and update the media/music controller Example from my code: import { Component, OnInit, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core';
import { Platform, NavController, ModalController } from '@ionic/angular';
import { ActivatedRoute } from "@angular/router";
import { MusicControls } from '@ionic-native/music-controls/ngx';
import { FirebaseProvider } from '../../../services/firebase/firebase';
import { ApiService } from '../../../services/api/api';
import { ModalHelper } from 'src/app/services/helper/modal';
import { LoginModal } from 'src/app/components/login-modal/login-modal.modal';
import { UserProvider } from 'src/app/services/user/user';
import { BackgroundMode } from '@ionic-native/background-mode/ngx';
@Component({
selector: 'app-hei-vibes-playlist',
templateUrl: './radio.page.html',
styleUrls: ['./radio.page.scss'],
})
export class RadioPage {
@ViewChild('audioPlayer') audioPlayerRef: ElementRef;
audio: HTMLAudioElement;
tracks: any = [
...
];
played: any = [];
isPlaying: boolean = false;
buffering: boolean = false;
currentTrack: any = {};
currentTrackIndex: number | null = null;
buttonIcon: string;
currentTrackListenStartTime: number?;
logCurrentTrackListeningInterval: any;
logCurrentTrackListeningIntervalNumber: number = 5000; // 5 seconds
constructor(
public platform: Platform,
public navCtrl: NavController,
public api: ApiService,
private fb: FirebaseProvider,
public modalCtrl: ModalController,
public modalHelper: ModalHelper,
private user: UserProvider,
private musicControls: MusicControls,
private backgroundMode: BackgroundMode,
) { }
ngAfterViewInit() {
this.audio = this.audioPlayerRef.nativeElement;
this.setupAudioListeners();
}
ionViewWillEnter() {
if (this.platform.is('android')) {
if (this.backgroundMode.isEnabled() || this.backgroundMode.isActive()) {
return;
}
this.backgroundMode.enable();
this.backgroundMode.on("activate").subscribe(() => {
this.backgroundMode.disableWebViewOptimizations();
this.backgroundMode.disableBatteryOptimizations();
});
}
if (this.user.get()) {
this.play(0);
}
}
ionViewDidEnter() {
this.fb.setScreenName("...");
}
ionViewWillLeave() {
this.stop();
if (this.platform.is('android')) {
this.backgroundMode.disable();
}
}
ionViewDidLeave() {
this.destroyMusicControls();
}
setupAudioListeners() {
this.audio.onplay = () => {
console.log('Audio playback onplay');
this.isPlaying = true;
this.buffering = false;
this.startListeningTracking();
if (this.platform.is('android')) {
this.musicControls.updateIsPlaying(true);
} else {
// @ts-ignore
navigator.mediaSession.playbackState = "playing";
}
this.createMusicControls();
};
this.audio.onpause = () => {
console.log('Audio playback onpause');
this.isPlaying = false;
this.clearListeningTracking();
if (this.platform.is('android')) {
this.musicControls.updateIsPlaying(false);
}
else {
// @ts-ignore
navigator.mediaSession.playbackState = "paused";
}
};
this.audio.onended = () => {
console.log('Audio playback onended');
this.next();
};
this.audio.onerror = () => {
console.log('Audio playback onerror');
console.error('Audio playback error');
this.clearListeningTracking();
};
this.audio.onwaiting = () => {
console.log('Audio playback onwaiting');
this.buffering = true;
};
this.audio.oncanplay = () => {
console.log('Audio playback oncanplay');
this.buffering = false;
};
}
play(trackIndex: number | null = null) {
// If user is not logged in, show login modal
if (!this.user.get()) {
this.showLoginModal();
return;
}
// If there are no tracks, return
if (this.tracks.length === 0) return;
// We reset the listening tracking
this.clearListeningTracking();
// If track index is provided
if (trackIndex !== null) {
// If it's different than the current track index, play the given track
if (this.currentTrackIndex !== trackIndex) {
if (this.currentTrack.title) {
this.logCurrentTrackListeningDuration(this.currentTrack);
}
this.playStream(trackIndex);
}
}
// If track index is not provided (this.play())
else {
// If it's currently playing, pause
if (this.isPlaying) {
this.pause();
}
// Else if it's not playing, resume
else {
this.resumeAndBufferToLive();
}
}
}
resumeAndBufferToLive() {
// First, pause the audio
this.audio.pause();
// Clear the source and reload it to get the latest stream
const currentSrc = this.audio.src;
this.audio.src = '';
this.audio.load();
this.audio.src = currentSrc;
// Start buffering
this.buffering = true;
// Attempt to play
this.audio.play().then(() => {
console.log('Resumed and buffered to live successfully');
this.buffering = false;
}).catch(error => {
console.error('Error resuming and buffering to live:', error);
this.buffering = false;
});
}
playStream(trackIndex: number) {
this.played.push(trackIndex);
this.currentTrackIndex = trackIndex;
this.currentTrack = this.tracks[trackIndex];
this.fb.logEvent("...", {
title: this.currentTrack.title,
genre: this.currentTrack.genre
});
this.audio.src = this.currentTrack.stream_url;
this.audio.load();
this.audio.play();
}
pause() {
this.audio.pause();
}
stop() {
this.audio.pause();
this.audio.currentTime = 0;
this.isPlaying = false;
this.clearListeningTracking();
this.currentTrackIndex = -1;
this.currentTrack = { title: '' };
}
previous() {
let index: number;
// If current track is the first track, play the last track
if (this.currentTrackIndex === 0) {
index = this.tracks.length - 1;
}
// Else play the previous track
else {
index = this.currentTrackIndex - 1;
}
this.play(index);
}
next() {
let index: number;
// If current track is the last track, play the first track
if (this.currentTrackIndex === this.tracks.length - 1) {
index = 0;
}
// Else play the next track
else {
index = this.currentTrackIndex + 1;
}
this.play(index);
}
get playIcon() {
return `assets/icons/track_${this.isPlaying ? 'pause' : 'play'}.svg`;
}
startListeningTracking() {
this.currentTrackListenStartTime = Date.now();
this.logCurrentTrackListeningInterval = setInterval(() => {
this.logCurrentTrackListeningDuration(this.currentTrack);
}, this.logCurrentTrackListeningIntervalNumber);
}
clearListeningTracking() {
clearInterval(this.logCurrentTrackListeningInterval);
this.currentTrackListenStartTime = null;
}
logCurrentTrackListeningDuration(currentTrack) {
if (!this.currentTrackListenStartTime) return;
const currentTrackListenDuration = Date.now() - this.currentTrackListenStartTime;
const listeningDuration = {
track: currentTrack.title,
genre: currentTrack.genre,
short_listen_time: this.logCurrentTrackListeningIntervalNumber / 1000,
current_total_listen_time: Number((currentTrackListenDuration / 1000).toFixed(2))
};
console.log('Listening duration', listeningDuration);
this.fb.logEvent("...", listeningDuration);
}
showLoginModal() {
if (this.modalHelper.modalInstances.includes('login-modal')) return;
this.modalHelper.modalInstances.push('login-modal');
this.modalCtrl.create({
component: LoginModal,
id: 'login-modal',
cssClass: "login-modal",
backdropDismiss: false,
}).then(modal => {
modal.present();
modal.onDidDismiss().then(() => {
this.modalHelper.modalInstances = this.modalHelper.modalInstances.filter(e => e !== 'login-modal');
if (this.user.get()) {
this.play(0);
}
});
});
}
async createMusicControls() {
// If Android, create music controller via plugin
if (this.platform.is('android')) {
console.log("Creating music controls Android");
await this.musicControls.create({
track: this.currentTrack.title, // optional, default : ''
artist: this.currentTrack.genre, // optional, default : ''
//cover : 'assets/imgs/vibes.png', // optional, default : nothing
cover: "https://www.example.com/image.png", // optional, default : nothing
// cover can be a local path (use fullpath 'file:///storage/emulated/...', or only 'my_image.jpg' if my_image.jpg is in the www folder of your app)
// or a remote url ('http://...', 'https://...', 'ftp://...')
isPlaying: true, // optional, default : true
dismissable: true, // optional, default : false
// hide previous/next/close buttons:
hasPrev: true, // show previous button, optional, default: true
hasNext: true, // show next button, optional, default: true
hasClose: true, // show close button, optional, default: false
// iOS only, optional
//album : 'Absolution', // optional, default: ''
hasSkipForward: true, // show skip forward button, optional, default: false
hasSkipBackward: true, // show skip backward button, optional, default: false
skipForwardInterval: 15, // display number for skip forward, optional, default: 0
skipBackwardInterval: 15, // display number for skip backward, optional, default: 0
hasScrubbing: false, // enable scrubbing from control center and lockscreen progress bar, optional
// Android only, optional
// text displayed in the status bar when the notification (and the ticker) are updated, optional
ticker: this.currentTrack.title,
// All icons default to their built-in android equivalents
// The supplied drawable name, e.g. 'media_play', is the name of a drawable found under android/res/drawable* folders
playIcon: 'media_pause',
pauseIcon: 'media_play',
prevIcon: 'media_prev',
nextIcon: 'media_next',
closeIcon: 'media_close',
notificationIcon: 'notification'
}).then((s) => {
console.log("musicControls created");
//this.api.debugger(s);
}).catch((e) => {
//this.api.debugger(e);
});
this.musicControls.subscribe().subscribe((action) => {
const message = JSON.parse(action).message;
console.log('musicControls', message);
switch (message) {
case 'music-controls-next':
this.next();
break;
case 'music-controls-previous':
this.previous();
break;
case 'music-controls-pause':
this.play();
break;
case 'music-controls-play':
this.pause();
break;
case 'music-controls-destroy':
this.destroyMusicControls();
break;
// External controls (iOS only)
case 'music-controls-toggle-play-pause':
this.play();
break;
case 'music-controls-seek-to':
// Do something
break;
case 'music-controls-skip-forward':
this.next();
break;
case 'music-controls-skip-backward':
this.previous();
break;
// Headset events (Android only)
// All media button events are listed below
case 'music-controls-media-button':
// Do something
break;
case 'music-controls-headset-unplugged':
// Do something
break;
case 'music-controls-headset-plugged':
// Do something
break;
default:
break;
}
});
this.musicControls.listen(); // activates the observable above
this.musicControls.updateIsPlaying(true);
}
// Else iOS (or web) and use MediaSession API to create music controller
else {
// @ts-ignore
if ('mediaSession' in navigator) {
// @ts-ignore
navigator.mediaSession.metadata = new MediaMetadata({
title: this.currentTrack.title,
artist: this.currentTrack.genre,
album: 'Radio',
artwork: [
{ src: 'https://www.example.com/image.png', sizes: '96x96', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '128x128', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '192x192', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '256x256', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '384x384', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '512x512', type: 'image/png' },
]
});
// @ts-ignore
navigator.mediaSession.setActionHandler('play', () => {
this.play();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('pause', () => {
this.pause();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('previoustrack', () => {
this.previous();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('nexttrack', () => {
this.next();
});
// @ts-ignore
navigator.mediaSession.playbackState = this.isPlaying ? "playing" : "paused";
}
}
}
destroyMusicControls(action = false) {
if (this.platform.is('android')) {
this.musicControls.destroy();
}
else {
if ('mediaSession' in navigator) {
// @ts-ignore
navigator.mediaSession.metadata = null;
// @ts-ignore
navigator.mediaSession.setActionHandler('play', null);
// @ts-ignore
navigator.mediaSession.setActionHandler('pause', null);
// @ts-ignore
navigator.mediaSession.setActionHandler('previoustrack', null);
// @ts-ignore
navigator.mediaSession.setActionHandler('nexttrack', null);
}
}
}
} Basically, instead of ...
// @ts-ignore
if ('mediaSession' in navigator) {
// @ts-ignore
navigator.mediaSession.metadata = new MediaMetadata({
title: this.currentTrack.title,
artist: this.currentTrack.genre,
album: 'Radio',
artwork: [
{ src: 'https://www.example.com/image.png', sizes: '96x96', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '128x128', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '192x192', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '256x256', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '384x384', type: 'image/png' },
{ src: 'https://www.example.com/image.png', sizes: '512x512', type: 'image/png' },
]
});
// @ts-ignore
navigator.mediaSession.setActionHandler('play', () => {
this.play();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('pause', () => {
this.pause();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('previoustrack', () => {
this.previous();
});
// @ts-ignore
navigator.mediaSession.setActionHandler('nexttrack', () => {
this.next();
});
// @ts-ignore
navigator.mediaSession.playbackState = this.isPlaying ? "playing" : "paused";
}
... Keep in mind that Android WebView doesn't support this API unfortunately , so you'll have to stick with the plugin for Android and the MediaSession API for iOS (and web/desktop). |
This plugin completely fails in my Cordova app built with XCode 15 on iOS.
Instead of my metadata and custom events handlers, I only get a basic info (the current window title) + a play/pause button linked to the active HTML audio.
This error is showing up:
[assertion] Error acquiring assertion: <Error Domain=RBSServiceErrorDomain Code=1 "(originator doesn't have entitlement com.apple.runningboard.assertions.webkit AND originator doesn't have entitlement com.apple.multitasking.systemappassertions)" UserInfo={NSLocalizedFailureReason=(originator doesn't have entitlement com.apple.runningboard.assertions.webkit AND originator doesn't have entitlement com.apple.multitasking.systemappassertions)}>
[ProcessSuspension] 0x10d001c60 - ProcessAssertion::acquireSync Failed to acquire RBS assertion 'WebKit Media Playback' for process with PID=XXX, error: Error Domain=RBSServiceErrorDomain Code=1 "(originator doesn't have entitlement com.apple.runningboard.assertions.webkit AND originator doesn't have entitlement com.apple.multitasking.systemappassertions)" UserInfo={NSLocalizedFailureReason=(originator doesn't have entitlement com.apple.runningboard.assertions.webkit AND originator doesn't have entitlement com.apple.multitasking.systemappassertions)}
These are private entitlements not exposed to developers.
The text was updated successfully, but these errors were encountered: