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

Fix android audio focus management #165

Merged
merged 1 commit into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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