/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with License.
* You may obtain a copy of License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { avSession } from '@kit.AVSessionKit';
import { common, wantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import Logger from '../Logger';
import { AudioControlCenter } from './AudioControlCenter';
import { MediaUtils } from '../MediaUtils';
import { AVCastManager } from './AVCastManager';
const TAG = 'AVSessionController';
export class AVSessionController {
private static instance: AVSessionController;
private context: common.UIAbilityContext | undefined = undefined;
private avSession: avSession.AVSession | undefined = undefined;
private audioControlCenter: AudioControlCenter | undefined = undefined;
private avCastManager: AVCastManager | undefined = undefined;
private constructor() {
}
public static getInstance(): AVSessionController {
if (!AVSessionController.instance) {
AVSessionController.instance = new AVSessionController();
}
return AVSessionController.instance;
}
// [Start initAVSession]
public async initAVSession() {
this.context = AppStorage.get('context') as common.UIAbilityContext;
if (!this.context) {
Logger.error(TAG, 'Session create failed: context is undefined');
return;
}
this.audioControlCenter = AudioControlCenter.getInstance();
if (!this.audioControlCenter) {
Logger.error(TAG, 'Session create failed: audioControlCenter is undefined');
return;
}
try {
this.avSession = await avSession.createAVSession(this.context, 'PLAY_AUDIO', 'audio');
await this.avSession.activate().catch(() => {
Logger.info(TAG, `Session activate error`);
});
// [Start setExtras]
this.avSession.setExtras({
requireAbilityList: ['url-cast'],
}).catch(() => {
Logger.info(TAG, `Session setExtras error`);
});
await this.setAVMetadata();
// [End setExtras]
this.setLaunchAbility();
this.setListenerForMesFromController();
this.avCastManager = AVCastManager.getInstance();
Logger.info(TAG, 'AVCastManager initialized');
} catch (error) {
Logger.error(TAG, 'initAVSession error');
}
}
// [End initAVSession]
// [Start setLaunchAbility]
private setLaunchAbility() {
if (!this.context) {
return;
}
let wantAgentInfo: wantAgent.WantAgentInfo = {
wants: [
{
bundleName: this.context.abilityInfo.bundleName,
abilityName: this.context.abilityInfo.name
}
],
actionType: wantAgent.OperationType.START_ABILITIES,
requestCode: 0,
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
if (this.avSession) {
this.avSession.setLaunchAbility(agent).catch(() => {
Logger.error(TAG, `setLaunchAbility failed.`);
});
}
}).catch((err: BusinessError) => {
Logger.error(TAG, `getWantAgent failed: code: ${err.code}, message: ${err.message}`);
});
}
// [End setLaunchAbility]
// [Start setListenerForMesFromController]
private setListenerForMesFromController() {
if (!this.avSession) {
return;
}
try {
this.avSession.on('play', this.onPlay);
this.avSession.on('pause', this.onPause);
this.avSession.on('playNext', this.onPlayNext);
this.avSession.on('playPrevious', this.onPlayPrevious);
this.avSession.on('seek', this.onSeek);
} catch (error) {
Logger.error(TAG, 'audioControlCenter is undefined in onPlay');
}
}
// [End setListenerForMesFromController]
private onPlay: () => void = () => {
Logger.info(TAG, 'onPlay received');
if (!this.audioControlCenter) {
Logger.error(TAG, 'audioControlCenter is undefined in onPlay');
return;
}
this.audioControlCenter.play();
}
private onPause: () => void = () => {
Logger.info(TAG, 'onPause received');
if (!this.audioControlCenter) {
Logger.error(TAG, 'audioControlCenter is undefined in onPause');
return;
}
this.audioControlCenter.pause();
}
private onPlayNext: () => void = async () => {
Logger.info(TAG, 'onPlayNext received');
if (!this.audioControlCenter) {
Logger.error(TAG, 'audioControlCenter is undefined in onPlayNext');
return;
}
await this.audioControlCenter.nextChapter();
}
private onPlayPrevious: () => void = async () => {
Logger.info(TAG, 'onPlayPrevious received');
if (!this.audioControlCenter) {
Logger.error(TAG, 'audioControlCenter is undefined in onPlayPrevious');
return;
}
await this.audioControlCenter.previousChapter();
}
private onSeek: (curMs: number) => void = (curMs: number) => {
Logger.info(TAG, `onSeek received: ${curMs}ms`);
if (!this.audioControlCenter) {
Logger.error(TAG, 'audioControlCenter is undefined in onSeek');
return;
}
this.audioControlCenter.seekTo(curMs);
}
// [Start setAVMetadata]
public async setAVMetadata() {
try {
if (!this.context || !this.avSession || !this.audioControlCenter) {
return;
}
const currentChapterIndex = this.audioControlCenter.getCurrentChapterIndex();
const chapter = this.audioControlCenter.getCurrentChapter();
if (!chapter) {
return;
}
const duration = this.audioControlCenter.getDuration();
let metadata: avSession.AVMetadata = {
assetId: `${currentChapterIndex}`,
title: chapter.title,
artist: chapter.author,
duration: duration,
avQueueName: 'My Audio Book',
avQueueId: 'AudioBookQueueId1',
filter: avSession.ProtocolType.TYPE_CAST_PLUS_STREAM | avSession.ProtocolType.TYPE_DLNA |
avSession.ProtocolType.TYPE_CAST_PLUS_AUDIO
};
try {
const queuePixelMap = await MediaUtils.getPixelMapFromResource(this.context, $r('app.media.image_cover'));
metadata.avQueueImage = queuePixelMap;
} catch (error) {
Logger.error(TAG, `Failed to load queue image: ${JSON.stringify(error)}`);
}
// get lyric
if (chapter.lyric) {
const lrc = await MediaUtils.getLrcFromRawFile(this.context, chapter.lyric);
if (lrc) {
metadata.lyric = lrc;
}
}
// get PixelMap
if (chapter.avatar) {
try {
const pixelMap = await MediaUtils.getPixelMapFromResource(this.context, chapter.avatar);
metadata.mediaImage = pixelMap;
} catch (error) {
Logger.error(TAG, `Failed to load avatar: ${JSON.stringify(error)}`);
}
}
// set AVMetadata
this.avSession.setAVMetadata(metadata).then(() => {
Logger.info(TAG, 'setAVMetadata succeeded');
}).catch((err: BusinessError) => {
Logger.error(TAG, `setAVMetadata failed: code: ${err.code}, message: ${err.message}`);
});
} catch (error) {
Logger.error(TAG, `setAVMetadata failed: ${JSON.stringify(error)}`);
}
}
// [End setAVMetadata]
// [Start setPlayState]
public setPlayState(isPlay: boolean) {
if (!this.avSession) {
Logger.error(TAG, 'AVSession is undefined');
return;
}
this.avSession.setAVPlaybackState({
state: isPlay ? avSession.PlaybackState.PLAYBACK_STATE_PLAY : avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
}, (err: BusinessError) => {
if (err) {
Logger.error(TAG, `setPlayState failed: code: ${err.code}, message: ${err.message}`);
} else {
Logger.info(TAG, 'setPlayState succeeded');
}
});
}
// [End setPlayState]
// [Start setProgressState]
public setProgressState(ms: number) {
if (!this.avSession) {
return;
}
this.avSession.setAVPlaybackState({
position: {
elapsedTime: ms,
updateTime: new Date().getTime()
}
}, (err: BusinessError) => {
if (err) {
Logger.error(TAG, `setProgressState failed: code: ${err.code}, message: ${err.message}`);
} else {
Logger.info(TAG, 'setProgressState succeeded');
}
});
}
// [End setProgressState]
// [Start unregisterSessionListener]
public async unregisterSessionListener() {
if (!this.avSession) {
return;
}
try {
this.avSession.off('play');
this.avSession.off('pause');
this.avSession.off('playNext');
this.avSession.off('playPrevious');
this.avSession.off('seek');
this.avSession.destroy().catch(() => {
Logger.error(TAG, `avSession destroy failed`);
});
} catch (error) {
let err: BusinessError = error;
Logger.error(TAG, `setProgressState failed: code: ${err.code}, message: ${err.message}`);
}
}
// [End unregisterSessionListener]
public getAVSession(): avSession.AVSession | undefined {
return this.avSession;
}
public getAVCastManager(): AVCastManager | undefined {
return this.avCastManager;
}
}