Open silencio opened 3 months ago
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 await this.musicControls.create({...
you'll use the MediaSession API
...
// @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.