From 254e0719302566b718c578b6933774beac2cffed Mon Sep 17 00:00:00 2001 From: Daniel Pereira Date: Sun, 31 May 2020 11:52:11 -0300 Subject: [PATCH] Fix android audio focus management --- README.md | 6 ++ src/android/player.ts | 134 ++++++++++++++++++++++++++---------------- src/index.d.ts | 42 +++++++++++++ src/options.ts | 7 +++ src/package.json | 4 ++ 5 files changed, 142 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 037b5c9..478a0f5 100755 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ export class YourClass { constructor() { this._player = new TNSPlayer(); + // You can pass a duration hint to control the behavior of other application that may + // be holding audio focus. + // For example: new TNSPlayer(AudioFocusDurationHint.AUDIOFOCUS_GAIN_TRANSIENT); + // Then when you play a song, the previous owner of the + // audio focus will stop. When your song stops + // the previous holder will resume. this._player.debug = true; // set true to enable TNSPlayer console logs for debugging. this._player .initFromFile({ diff --git a/src/android/player.ts b/src/android/player.ts index 9160175..b30a2d6 100644 --- a/src/android/player.ts +++ b/src/android/player.ts @@ -2,18 +2,18 @@ import * as app from 'tns-core-modules/application'; import { Observable } from 'tns-core-modules/data/observable'; import { isFileOrResourcePath } from 'tns-core-modules/utils/utils'; import { resolveAudioFilePath, TNSPlayerI, TNSPlayerUtil, TNS_Player_Log } from '../common'; -import { AudioPlayerEvents, AudioPlayerOptions } from '../options'; +import { AudioPlayerEvents, AudioPlayerOptions, AudioFocusDurationHint } from '../options'; export class TNSPlayer implements TNSPlayerI { - private _player: android.media.MediaPlayer; + private _mediaPlayer: android.media.MediaPlayer; private _mAudioFocusGranted: boolean = false; private _lastPlayerVolume; // ref to the last volume setting so we can reset after ducking private _events: Observable; + private _durationHint: AudioFocusDurationHint; + private _options: AudioPlayerOptions; - constructor() { - // request audio focus, this will setup the onAudioFocusChangeListener - this._mAudioFocusGranted = this._requestAudioFocus(); - TNS_Player_Log('_mAudioFocusGranted', this._mAudioFocusGranted); + constructor(durationHint: AudioFocusDurationHint = AudioFocusDurationHint.AUDIOFOCUS_GAIN) { + this._durationHint = durationHint; } public get events() { @@ -74,18 +74,11 @@ export class TNSPlayer implements TNSPlayerI { options.autoPlay = true; } + this._options = options; + const audioPath = resolveAudioFilePath(options.audioFile); TNS_Player_Log('audioPath', audioPath); - if (!this._player) { - TNS_Player_Log('android mediaPlayer is not initialized, creating new instance'); - this._player = new android.media.MediaPlayer(); - } - - // request audio focus, this will setup the onAudioFocusChangeListener - this._mAudioFocusGranted = this._requestAudioFocus(); - TNS_Player_Log('_mAudioFocusGranted', this._mAudioFocusGranted); - this._player.setAudioStreamType(android.media.AudioManager.STREAM_MUSIC); TNS_Player_Log('resetting mediaPlayer...'); @@ -102,36 +95,6 @@ export class TNSPlayer implements TNSPlayerI { this._player.prepareAsync(); } - // On Complete - if (options.completeCallback) { - this._player.setOnCompletionListener( - new android.media.MediaPlayer.OnCompletionListener({ - onCompletion: mp => { - if (options.loop === true) { - mp.seekTo(5); - mp.start(); - } - - options.completeCallback({ player: mp }); - } - }) - ); - } - - // On Error - if (options.errorCallback) { - this._player.setOnErrorListener( - new android.media.MediaPlayer.OnErrorListener({ - onError: (player: any, error: number, extra: number) => { - this._player.reset(); - TNS_Player_Log('errorCallback', error); - options.errorCallback({ player, error, extra }); - return true; - } - }) - ); - } - // On Info if (options.infoCallback) { this._player.setOnInfoListener( @@ -158,6 +121,7 @@ export class TNSPlayer implements TNSPlayerI { }) ); } catch (ex) { + this._abandonAudioFocus(); TNS_Player_Log('playFromFile error', ex); reject(ex); } @@ -187,8 +151,12 @@ export class TNSPlayer implements TNSPlayerI { if (this._player && this._player.isPlaying()) { TNS_Player_Log('pausing player'); this._player.pause(); + // We abandon the audio focus but we still preserve + // the MediaPlayer so we can resume it in the future + this._abandonAudioFocus(true); this._sendEvent(AudioPlayerEvents.paused); } + resolve(true); } catch (ex) { TNS_Player_Log('pause error', ex); @@ -201,6 +169,14 @@ export class TNSPlayer implements TNSPlayerI { return new Promise((resolve, reject) => { try { if (this._player && !this._player.isPlaying()) { + // request audio focus, this will setup the onAudioFocusChangeListener + this._mAudioFocusGranted = this._requestAudioFocus(); + TNS_Player_Log('_mAudioFocusGranted', this._mAudioFocusGranted); + + if (!this._mAudioFocusGranted) { + throw new Error('Could not request audio focus'); + } + this._sendEvent(AudioPlayerEvents.started); // set volume controls // https://developer.android.com/reference/android/app/Activity.html#setVolumeControlStream(int) @@ -229,7 +205,8 @@ export class TNSPlayer implements TNSPlayerI { public resume(): void { if (this._player) { TNS_Player_Log('resume'); - this._player.start(); + // We call play so it can request audio focus + this.play(); this._sendEvent(AudioPlayerEvents.started); } } @@ -273,7 +250,9 @@ export class TNSPlayer implements TNSPlayerI { TNS_Player_Log('disposing of mediaPlayer instance', this._player); this._player.stop(); this._player.reset(); - // this._player.release(); + // Remove _options since we are back to the Idle state + // (Refer to: https://developer.android.com/reference/android/media/MediaPlayer#state-diagram) + this._options = undefined; TNS_Player_Log('unregisterBroadcastReceiver ACTION_AUDIO_BECOMING_NOISY...'); // unregister broadcast receiver @@ -328,15 +307,17 @@ export class TNSPlayer implements TNSPlayerI { * Helper method to ensure audio focus. */ private _requestAudioFocus(): boolean { - let result = false; + // If it does not enter the codition block, means that we already + // have focus. Therefore we have to start with `true`. + let result = true; if (!this._mAudioFocusGranted) { const ctx = this._getAndroidContext(); - const am = ctx.getSystemService(android.content.Context.AUDIO_SERVICE); + const am = ctx.getSystemService(android.content.Context.AUDIO_SERVICE) as android.media.AudioManager; // Request audio focus for play back const focusResult = am.requestAudioFocus( this._mOnAudioFocusChangeListener, android.media.AudioManager.STREAM_MUSIC, - android.media.AudioManager.AUDIOFOCUS_GAIN + this._durationHint ); if (focusResult === android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { @@ -349,10 +330,15 @@ export class TNSPlayer implements TNSPlayerI { return result; } - private _abandonAudioFocus(): void { + private _abandonAudioFocus(preserveMP: boolean = false): void { const ctx = this._getAndroidContext(); const am = ctx.getSystemService(android.content.Context.AUDIO_SERVICE); const result = am.abandonAudioFocus(this._mOnAudioFocusChangeListener); + // Normally we will preserve the MediaPlayer only when pausing + if (this._mediaPlayer && !preserveMP) { + this._mediaPlayer.release(); + this._mediaPlayer = undefined; + } if (result === android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { this._mAudioFocusGranted = false; } else { @@ -377,6 +363,52 @@ export class TNSPlayer implements TNSPlayerI { return ctx; } + /** + * This getter will instantiate the MediaPlayer if needed + * and register the listeners. This is done here to avoid + * code duplication. This is also the reason why we have + * a `_options` + */ + private get _player() { + if (!this._mediaPlayer && this._options) { + this._mediaPlayer = new android.media.MediaPlayer(); + TNS_Player_Log('android mediaPlayer is not initialized, creating new instance'); + + this._mediaPlayer.setOnCompletionListener( + new android.media.MediaPlayer.OnCompletionListener({ + onCompletion: mp => { + if (this._options && this._options.completeCallback) { + if (this._options.loop === true) { + mp.seekTo(5); + mp.start(); + } + this._options.completeCallback({ player: mp }); + } + + if (this._options && !this._options.loop) { + // Make sure that we abandon audio focus when playback stops + this._abandonAudioFocus(); + } + } + }) + ); + + this._mediaPlayer.setOnErrorListener( + new android.media.MediaPlayer.OnErrorListener({ + onError: (player: any, error: number, extra: number) => { + if (this._options && this._options.errorCallback) { + this._options.errorCallback({ player, error, extra }); + } + TNS_Player_Log('errorCallback', error); + this.dispose(); + return true; + } + }) + ); + } + + return this._mediaPlayer; + } private _mOnAudioFocusChangeListener = new android.media.AudioManager.OnAudioFocusChangeListener({ onAudioFocusChange: (focusChange: number) => { diff --git a/src/index.d.ts b/src/index.d.ts index 6e90a30..fd629ac 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -195,6 +195,13 @@ export declare class TNSPlayer { */ readonly currentTime: number; + /** + * @param {AudioFocusDurationHint} durationHint - Determines differents behaviors by + * the system and the other application that previously held audio focus. + * See the {@link https://developer.android.com/reference/android/media/AudioFocusRequest#the-different-types-of-focus-requests different types of focus requests} + */ + constructor(durationHint?: AudioFocusDurationHint); + initFromFile(options: AudioPlayerOptions): Promise; /** @@ -328,4 +335,39 @@ export interface IAudioPlayerEvents { paused: 'paused'; started: 'started'; } + export const AudioPlayerEvents: IAudioPlayerEvents; + +export enum AudioFocusDurationHint { + /** + * Expresses the fact that your application is now the sole source + * of audio that the user is listening to. The duration of the + * audio playback is unknown, and is possibly very long: after the + * user finishes interacting with your application, (s)he doesn’t + * expect another audio stream to resume. + */ + AUDIOFOCUS_GAIN = android.media.AudioManager.AUDIOFOCUS_GAIN, + /** + * For a situation when you know your application is temporarily + * grabbing focus from the current owner, but the user expects + * playback to go back to where it was once your application no + * longer requires audio focus. + */ + AUDIOFOCUS_GAIN_TRANSIENT = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, + /** + * This focus request type is similar to AUDIOFOCUS_GAIN_TRANSIENT + * for the temporary aspect of the focus request, but it also + * expresses the fact during the time you own focus, you allow + * another application to keep playing at a reduced volume, + * “ducked”. + */ + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + /** + * Also for a temporary request, but also expresses that your + * application expects the device to not play anything else. This + * is typically used if you are doing audio recording or speech + * recognition, and don’t want for examples notifications to be + * played by the system during that time. + */ + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK +} diff --git a/src/options.ts b/src/options.ts index a959349..d870f23 100644 --- a/src/options.ts +++ b/src/options.ts @@ -105,3 +105,10 @@ export const AudioPlayerEvents = { paused: 'paused', started: 'started' }; + +export enum AudioFocusDurationHint { + AUDIOFOCUS_GAIN = android.media.AudioManager.AUDIOFOCUS_GAIN, + AUDIOFOCUS_GAIN_TRANSIENT = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK +} diff --git a/src/package.json b/src/package.json index 6942c6e..333d420 100644 --- a/src/package.json +++ b/src/package.json @@ -126,6 +126,10 @@ { "name": "Richard Smith", "url": "https://github.com/DickSmith" + }, + { + "name": "Daniel Pereira", + "url": "https://github.com/danieldspx" } ], "bugs": {