@react-native-oh-tpl/react-native-incall-manager
Version:
Handling media-routes/sensors/events during a audio/video chat on React Native
329 lines (308 loc) • 12 kB
text/typescript
/*
* MIT License
*
* Copyright (C) 2024 Huawei Device Co., Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import media from '@ohos.multimedia.media';
import type resourceManager from '@ohos.resourceManager';
import { type BusinessError } from '@ohos.base';
import { type common } from '@kit.AbilityKit';
import { audio } from '@kit.AudioKit';
import { avSession } from '@kit.AVSessionKit';
import Logger from '../Logger';
import { AudioFocusType } from '../index';
const TAG: string = 'PlayModel';
const AVSESSIONTAG: string = 'ringtoneplayering';
const AVSESSIONTYPE: 'audio' = 'audio';
class PlayModelEvent {
static STATE_CHANGE: 'stateChange' = 'stateChange';
static AUDIO_INTERRUPT: 'audioInterrupt' = 'audioInterrupt';
}
class PlayModelState {
static INITIALIZED: string = 'initialized';
static PREPARED: string = 'prepared';
static COMPLETED: string = 'completed';
static PLAYING: string = 'playing';
static PAUSED: string = 'paused';
static STOPPED: string = 'stopped';
static ERROR: string = 'error';
}
export default class PlayModel {
private context: common.UIAbilityContext;
private audioRenderInfoObj: audio.AudioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0
};
private onPlayComplete: (isComplete: boolean, avplayer: media.AVPlayer) => void;
private interruptResultBack: (code: number, codeText: string) => void;
private audioLoop: boolean = false;
private isRequireAvSession: boolean = false;
private interruptMode: audio.InterruptMode = audio.InterruptMode.SHARE_MODE;
private avSession: avSession.AVSession;
public avPlayer: media.AVPlayer | null = null;
constructor(context: common.UIAbilityContext, isRequireAvSession: boolean,
interruptResultBack?: (code: number, codeText: string) => void) {
this.context = context;
this.isRequireAvSession = isRequireAvSession;
if (interruptResultBack) {
this.interruptResultBack = interruptResultBack;
}
}
private createAVSession(context: common.UIAbilityContext): void {
Logger.debug(TAG, 'createAVSession begin');
if (!this.avSession) {
avSession.createAVSession(context, AVSESSIONTAG, AVSESSIONTYPE).then((data) => {
Logger.debug(TAG, 'createAVSession succeed');
this.avSession = data;
});
}
};
private destroyAVSession(): void {
if (this.avSession) {
Logger.debug(TAG, 'destroyAVSession begin');
this.avSession?.destroy();
this.avSession = null;
}
};
public async prepareWithPlayFd(fileFd: resourceManager.RawFileDescriptor, renderInfo: audio.AudioRendererInfo,
audioLoop: boolean): Promise<void> {
Logger.info(TAG, `create AVPlayer playFd ${fileFd.fd}`);
try {
this.release();
this.audioRenderInfoObj = renderInfo;
this.audioLoop = audioLoop;
this.avPlayer = await media.createAVPlayer();
if (this.isRequireAvSession) {
this.createAVSession(this.context);
}
this.onAudioInterrupt();
this.avPlayer.on(PlayModelEvent.STATE_CHANGE, (state: media.AVPlayerState, reason: media.StateChangeReason) => {
this.onStateChange(state, reason);
});
this.avPlayer.fdSrc = fileFd;
Logger.info(TAG, `create AVPlayer success`);
} catch (err) {
Logger.error(TAG, `Invoke AVPlayer failed, code is ${(err as BusinessError).code}`);
}
}
public setOnPlayComplete(complete: (isComplete: boolean, avplayer: media.AVPlayer) => void): void {
this.onPlayComplete = complete;
}
public async stopAudio(): Promise<void> {
if (!this.avPlayer) {
return;
}
try {
let statePlist: string[] =
[PlayModelState.PREPARED, PlayModelState.PLAYING, PlayModelState.PAUSED, PlayModelState.COMPLETED];
if (statePlist.indexOf(this.avPlayer.state) >= 0) {
await this.avPlayer?.stop();
this.destroyAVSession();
Logger.info(TAG, 'Succeeded in stopping');
} else {
Logger.info(TAG, 'ingnore to stopping');
}
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(TAG, 'Failed to stop,error code is :' + err.code);
}
}
public async pausedAudio(): Promise<void> {
if (!this.avPlayer) {
return;
}
try {
let statePlist: string[] = [PlayModelState.PLAYING];
if (statePlist.indexOf(this.avPlayer.state) >= 0) {
await this.avPlayer?.pause();
Logger.info(TAG, 'Succeeded in paused');
} else {
Logger.info(TAG, 'ingnore to paused');
}
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.error(TAG, 'Failed to paused,error code is :' + err.code);
}
}
public async playAudio(): Promise<void> {
if (!this.avPlayer) {
Logger.error(TAG, 'first create avPlayer');
return;
}
try {
let isPlayerState: string = this.avPlayer.state as string;
let requireStateList: PlayModelState[] = [PlayModelState.STOPPED, PlayModelState.INITIALIZED];
let playStateList = [PlayModelState.PAUSED, PlayModelState.COMPLETED, PlayModelState.PREPARED];
if (requireStateList.indexOf(isPlayerState) >= 0) {
await this.avPlayer.prepare();
Logger.info(TAG, 'Succeeded in preparing');
} else if (playStateList.indexOf(isPlayerState) >= 0) {
await this.avPlayer?.play();
Logger.info(TAG, 'Succeeded in playing');
}
if (this.isRequireAvSession) {
this.createAVSession(this.context);
}
} catch (err) {
Logger.error(TAG, 'Failed to play,error is :' + (err as BusinessError).code);
}
}
public async release(): Promise<void> {
Logger.info(TAG, `avPlayer release`);
try {
this.destroyAVSession();
if (this.avPlayer) {
this.avPlayer.off(PlayModelEvent.STATE_CHANGE);
this.offAudioInterrupt();
let statePlist: string[] =
[PlayModelState.PREPARED, PlayModelState.PLAYING, PlayModelState.PAUSED, PlayModelState.COMPLETED];
if (statePlist.indexOf(this.avPlayer.state) >= 0) {
await this.avPlayer.stop();
}
await this.avPlayer.release();
this.avPlayer = null;
}
} catch (err) {
Logger.error(TAG, `release AVPlayer failed, code is ${(err as BusinessError).code}`);
}
}
public isPlaying(): boolean {
return this.avPlayer?.state === PlayModelState.PLAYING;
}
public onAudioInterrupt(): void {
if (this.avPlayer) {
this.avPlayer.on(PlayModelEvent.AUDIO_INTERRUPT, async (interruptEvent: audio.InterruptEvent) => {
let focusChangeStr: string = '';
if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_FORCE) {
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_NONE: {
focusChangeStr = AudioFocusType.HINT_NONE;
break;
}
case audio.InterruptHint.INTERRUPT_HINT_PAUSE: {
focusChangeStr = AudioFocusType.HINT_PAUSE;
await this.pausedAudio();
break;
}
case audio.InterruptHint.INTERRUPT_HINT_RESUME: {
focusChangeStr = AudioFocusType.HINT_RESUME;
await this.playAudio();
break;
}
case audio.InterruptHint.INTERRUPT_HINT_STOP: {
focusChangeStr = AudioFocusType.HINT_STOP;
await this.stopAudio();
break;
}
case audio.InterruptHint.INTERRUPT_HINT_DUCK: {
focusChangeStr = AudioFocusType.HINT_DUCK;
break;
}
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK: {
focusChangeStr = AudioFocusType.HINT_UNDUCK;
break;
}
default:
break;
}
} else if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_SHARE) {
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_NONE: {
focusChangeStr = AudioFocusType.HINT_NONE;
break;
}
case audio.InterruptHint.INTERRUPT_HINT_PAUSE: {
focusChangeStr = AudioFocusType.HINT_PAUSE;
await this.pausedAudio();
break;
}
case audio.InterruptHint.INTERRUPT_HINT_RESUME: {
focusChangeStr = AudioFocusType.HINT_RESUME;
await this.playAudio();
break;
}
default:
break;
}
}
if (this.interruptResultBack) {
this.interruptResultBack(interruptEvent.hintType, focusChangeStr);
}
});
}
}
public offAudioInterrupt(): void {
if (this.avPlayer) {
this.avPlayer.off(PlayModelEvent.AUDIO_INTERRUPT);
}
}
private async onStateChange(state: media.AVPlayerState, reason: media.StateChangeReason): Promise<void> {
Logger.info(TAG, `AVPlayer stateChange ${reason} : ${state}`);
try {
switch (state) {
case PlayModelState.INITIALIZED: {
if (this.avPlayer) {
this.avPlayer.audioRendererInfo = this.audioRenderInfoObj;
await this.avPlayer.prepare();
Logger.info(TAG, 'AVPlayer prepare succeeded.');
}
break;
}
case PlayModelState.PREPARED: {
if (this.avPlayer) {
this.avPlayer.loop = this.audioLoop;
this.avPlayer.audioInterruptMode = this.interruptMode;
await this.avPlayer.play();
Logger.info(TAG, 'AVPlayer play succeeded.');
}
break;
}
case PlayModelState.COMPLETED: {
if (this.onPlayComplete) {
this.onPlayComplete(true, this.avPlayer);
}
if (this.avPlayer) {
this.avPlayer.audioInterruptMode = this.interruptMode;
}
if (this.avPlayer && this.audioLoop) {
await this.avPlayer.play();
}
break;
}
case PlayModelState.PAUSED: {
this.avPlayer.audioInterruptMode = this.interruptMode;
break;
}
case PlayModelState.STOPPED:
case PlayModelState.ERROR: {
if (this.onPlayComplete) {
this.onPlayComplete(true, this.avPlayer);
}
this.release();
break;
}
}
} catch (err) {
Logger.error(TAG, `AVPlayer onStateChange failed ${(err as BusinessError).code}`);
}
}
}