Skip to content

Commit

Permalink
Merge pull request #165 from danieldspx/master
Browse files Browse the repository at this point in the history
Fix android audio focus management
  • Loading branch information
bradmartin authored Jun 2, 2020
2 parents 203fc64 + 254e071 commit 66ff256
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 51 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
134 changes: 83 additions & 51 deletions src/android/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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...');
Expand All @@ -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(
Expand All @@ -158,6 +121,7 @@ export class TNSPlayer implements TNSPlayerI {
})
);
} catch (ex) {
this._abandonAudioFocus();
TNS_Player_Log('playFromFile error', ex);
reject(ex);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand All @@ -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) => {
Expand Down
42 changes: 42 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;

/**
Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@
{
"name": "Richard Smith",
"url": "https://github.com/DickSmith"
},
{
"name": "Daniel Pereira",
"url": "https://github.com/danieldspx"
}
],
"bugs": {
Expand Down

0 comments on commit 66ff256

Please sign in to comment.