Skip to content
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

Open
silencio opened this issue May 17, 2024 · 1 comment

Comments

@silencio
Copy link

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.

@pinguluk
Copy link

pinguluk commented Jul 13, 2024

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants