UNPKG

@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
/* * 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}`); } } }