| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863 |
- import { common, Context, wantAgent, Want } from '@kit.AbilityKit'
- import { BusinessError } from '@kit.BasicServicesKit'
- import { avSession } from '@kit.AVSessionKit'
- import { image } from '@kit.ImageKit'
- import { FileUtil, ImageUtil, PreferencesUtil, StrUtil } from '@pura/harmony-utils'
- import {
- DeviceChangeReason,
- InterruptEvent,
- InterruptHintType,
- IjkMediaPlayer
- } from '@ohos/ijkplayer'
- import {
- ImplOnCompletionListener,
- ImplOnErrorListener,
- ImplOnPreparedListener,
- ImplOnSeekCompleteListener
- } from '../common/IjkPlayerListenerImpls'
- import { PlayStatus } from '../common/PlayStatus'
- import { MusicCardActionConstants } from '../common/player/MusicCardActionConstants'
- import { isPlaybackControlPlaying } from '../common/player/PlaybackControlStateHelper'
- import { MusicCardManager, MusicCardPlaybackStateOptions } from '../common/player/MusicCardManager'
- import { BackgroundTaskManager } from '../common/util/BackgroundTaskManager'
- import Logger from '../common/util/Logger'
- import { imagePathToPixelMap } from '../common/util/CommUtils'
- import { setVideoUrlForSong } from '../common/util/RemotePlayerUtil'
- import { MusicPlaybackController } from '../controller/MusicPlaybackController'
- import { PlaybackRuntime } from '../controller/PlaybackCoordinator'
- import { VideoItem } from '../viewmodel/VideoItem'
- import { PlaybackSnapshotStore } from './PlaybackSnapshotStore'
- import {
- BackgroundAudioControlDecision,
- BackgroundAudioControlKind,
- BackgroundAudioPersistedState,
- BackgroundAudioPlaybackHostHelper,
- BackgroundAudioRecoveredQueue
- } from './BackgroundAudioPlaybackHostHelper'
- import { PlaybackSnapshot, PlaybackSnapshotSong, resolvePlaybackSnapshotProgressValue } from './model/PlaybackSnapshot'
- const TAG = 'BackgroundAudioHost'
- const PLAYER_ID = 'audioIjkId'
- const PROGRESS_INTERVAL_MS = 1000
- const PLAYBACK_VERIFY_DELAY_MS = 200
- const PLAYBACK_VERIFY_RETRY_DELAY_MS = 350
- interface PlaybackSnapshotWriter {
- write(snapshot: PlaybackSnapshot): void
- }
- export class BackgroundAudioPlaybackHost {
- private static instance: BackgroundAudioPlaybackHost
- private context: common.Context | undefined = undefined
- private player: IjkMediaPlayer | undefined = undefined
- private queue: VideoItem[] = []
- private currentIndex: number = -1
- private currentSong: VideoItem | undefined = undefined
- private playType: number = 0
- private isPlaying: boolean = false
- private isPrepared: boolean = false
- private currentUrl: string = ''
- private progressTimerId: number = -1
- private playbackStartVerifyToken: number = 0
- private playbackSession: avSession.AVSession | undefined = undefined
- private creatingPlaybackSession: boolean = false
- private playbackSessionCallbacksRegistered: boolean = false
- private snapshotStore: PlaybackSnapshotWriter = new PlaybackSnapshotStore()
- private readonly runtime: PlaybackRuntime = {
- playQueue: async (queue: VideoItem[], startIndex: number, source: string, playType?: number): Promise<void> => {
- await this.playQueue(queue, startIndex, source, playType)
- },
- playOrPause: async (): Promise<void> => {
- await this.playOrPause()
- },
- playNext: async (): Promise<void> => {
- await this.playNext()
- },
- playPrevious: async (): Promise<void> => {
- await this.playPrevious()
- },
- setLoopMode: async (): Promise<void> => {
- await this.setLoopMode()
- },
- seekTo: async (value: string): Promise<void> => {
- await this.seekTo(value)
- }
- }
- public static getInstance(): BackgroundAudioPlaybackHost {
- if (!BackgroundAudioPlaybackHost.instance) {
- BackgroundAudioPlaybackHost.instance = new BackgroundAudioPlaybackHost()
- }
- return BackgroundAudioPlaybackHost.instance
- }
- public setContext(context: common.Context | undefined): void {
- this.context = context
- Logger.info(TAG, `[MusicCast] setContext contextReady=${context !== undefined}`)
- }
- public getRuntime(): PlaybackRuntime {
- return this.runtime
- }
- public async playQueue(queue: VideoItem[], startIndex: number, source: string, playType?: number): Promise<void> {
- if (queue.length <= 0) {
- Logger.warn(TAG, `[MusicCast] playQueue ignored empty queue source=${source}`)
- return
- }
- const safeIndex = Math.max(0, Math.min(startIndex, queue.length - 1))
- this.queue = [...queue]
- this.currentIndex = safeIndex
- this.currentSong = this.queue[safeIndex]
- if (playType !== undefined) {
- this.playType = playType
- }
- Logger.info(TAG,
- `[MusicCast] playQueue source=${source}, queueLength=${this.queue.length}, startIndex=${startIndex}, ` +
- `safeIndex=${safeIndex}, playType=${this.playType}`)
- this.persistCurrentQueue()
- await this.playIndex(safeIndex)
- }
- public async playOrPause(): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- await this.handleControlAction(MusicCardActionConstants.ACTION_PLAY_PAUSE)
- }
- public async playNext(): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- await this.handleControlAction(MusicCardActionConstants.ACTION_NEXT)
- }
- public async playPrevious(): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- await this.handleControlAction(MusicCardActionConstants.ACTION_PREVIOUS)
- }
- public async seekTo(value: string, _source?: string): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- await this.handleControlAction(MusicCardActionConstants.ACTION_SEEK_TO, value)
- }
- public async setLoopMode(): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- const nextMode = MusicPlaybackController.resolveNextLoopMode(this.playType)
- this.playType = nextMode.playType
- PreferencesUtil.putSync('musicPlayType', this.playType)
- this.persistCurrentQueue()
- this.syncHostStorage(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.syncPlaybackSessionState(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.persistPlaybackSnapshot(this.isPlaying)
- Logger.info(TAG, `[MusicCast] setLoopMode playType=${this.playType}, toast=${nextMode.toastText}`)
- }
- public syncCurrentSongFavoriteState(isFavorite: boolean): void {
- this.applyCurrentSongFavoriteState(isFavorite)
- }
- public getSpectrumData(): number[] {
- if (!this.player) {
- return []
- }
- return this.player.getSpectrumData()
- }
- public async handleControlAction(action: string, seekPositionMs: string = ''): Promise<boolean> {
- try {
- Logger.info(TAG,
- `[MusicCast] handleControlAction action=${action}, seek=${seekPositionMs}, queueLength=${this.queue.length}, ` +
- `currentIndex=${this.currentIndex}, isPlaying=${this.isPlaying}, isPrepared=${this.isPrepared}`)
- this.restorePersistedQueueIfNeeded()
- if (action === MusicCardActionConstants.ACTION_SEEK_TO) {
- const handled = this.seekToInternal(seekPositionMs)
- Logger.info(TAG, `[MusicCast] seek decision handled=${handled}, seek=${seekPositionMs}`)
- return handled
- }
- const decision = BackgroundAudioPlaybackHostHelper.resolveControlDecision(
- action,
- this.currentIndex,
- this.queue.length,
- this.isPlaying,
- this.playType
- )
- Logger.info(TAG,
- `[MusicCast] decision kind=${decision.kind}, targetIndex=${decision.targetIndex}, ` +
- `queueLength=${this.queue.length}, currentIndex=${this.currentIndex}, playType=${this.playType}`)
- return this.executeDecision(decision)
- } catch (error) {
- Logger.error(TAG, `[MusicCast] handleControlAction failed: ${(error as Error).message}`)
- return false
- }
- }
- private async executeDecision(decision: BackgroundAudioControlDecision): Promise<boolean> {
- if (decision.kind === BackgroundAudioControlKind.PAUSE) {
- this.pause()
- return true
- }
- if (decision.kind === BackgroundAudioControlKind.STOP) {
- this.stop()
- return true
- }
- if (decision.kind === BackgroundAudioControlKind.PLAY_INDEX) {
- if (!this.isPlaying && this.isPrepared && decision.targetIndex === this.currentIndex) {
- this.resume()
- return true
- }
- return this.playIndex(decision.targetIndex)
- }
- return false
- }
- private restorePersistedQueueIfNeeded(): void {
- if (this.queue.length > 0 && this.currentIndex >= 0 && this.currentIndex < this.queue.length) {
- Logger.info(TAG,
- `[MusicCast] skip restore because queue already ready queueLength=${this.queue.length}, currentIndex=${this.currentIndex}`)
- return
- }
- const persistedState: BackgroundAudioPersistedState = BackgroundAudioPlaybackHostHelper.readPersistedState(
- () => PreferencesUtil.getSync('LastMusicList', []) as VideoItem[],
- () => PreferencesUtil.getSync('LastMusicInfo', undefined) as VideoItem | undefined,
- () => PreferencesUtil.getNumberSync('LastPlayModeType', PreferencesUtil.getNumberSync('musicPlayType', 0))
- )
- if (persistedState.errorMessage) {
- Logger.error(TAG, `[MusicCast] restore persisted read failed: ${persistedState.errorMessage}`)
- }
- Logger.info(TAG,
- `[MusicCast] restore persisted queueLength=${persistedState.queue.length}, currentSong=${persistedState.currentSong?.name ?? ''}, ` +
- `path=${persistedState.currentSong?.filePath ?? ''}, playType=${persistedState.playType}`)
- const restored = BackgroundAudioPlaybackHostHelper.restorePersistedQueue(
- persistedState.queue,
- persistedState.currentSong,
- persistedState.playType
- )
- this.applyRecoveredQueue(restored)
- }
- private applyRecoveredQueue(restored: BackgroundAudioRecoveredQueue): void {
- this.queue = restored.queue
- this.currentIndex = restored.currentIndex
- this.currentSong = restored.currentSong
- this.playType = restored.playType
- Logger.info(TAG,
- `[MusicCast] applyRecoveredQueue queueLength=${this.queue.length}, currentIndex=${this.currentIndex}, ` +
- `currentSong=${this.currentSong?.name ?? ''}, path=${this.currentSong?.filePath ?? ''}, playType=${this.playType}`)
- this.syncHostStorage(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.persistPlaybackSnapshot(this.isPlaying)
- }
- private async playIndex(index: number): Promise<boolean> {
- if (index < 0 || index >= this.queue.length) {
- Logger.warn(TAG, `[MusicCast] playIndex ignored invalid index=${index}, queueLength=${this.queue.length}`)
- return false
- }
- this.currentIndex = index
- this.currentSong = this.queue[index]
- Logger.info(TAG,
- `[MusicCast] playIndex index=${index}, song=${this.currentSong?.name ?? ''}, path=${this.currentSong?.filePath ?? ''}, type=${this.currentSong?.type ?? -1}`)
- this.persistCurrentQueue()
- return this.prepareAndStartCurrentSong()
- }
- private async prepareAndStartCurrentSong(): Promise<boolean> {
- if (!this.currentSong) {
- Logger.warn(TAG, '[MusicCast] prepare skipped because currentSong missing')
- return false
- }
- if (!this.context) {
- Logger.warn(TAG, '[MusicCast] prepare skipped because context missing')
- return false
- }
- try {
- await this.ensurePlaybackSession()
- const player = this.ensurePlayer()
- this.playbackStartVerifyToken++
- this.isPrepared = false
- this.isPlaying = false
- this.currentUrl = await setVideoUrlForSong(this.currentSong, {
- context: this.context as Context,
- extractAudioInfo: false,
- extractCover: false,
- extractLyric: false
- })
- Logger.info(TAG,
- `[MusicCast] prepare resolved url=${this.currentUrl}, song=${this.currentSong.name}, path=${this.currentSong.filePath}`)
- player.reset()
- player.setAudioId(PLAYER_ID)
- player.native_setup()
- player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, 'start-on-prepared', '1')
- player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, 'packet-buffering', '0')
- player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, 'soundtouch', '1')
- player.setVolume('1', '1')
- player.setDataSource(this.currentUrl)
- const headers = new Map<string, string>()
- headers.set('User-Agent', 'TTMusic-Widget/1.0')
- headers.set('Accept', '*/*')
- player.setDataSourceHeader(headers)
- void this.syncPlaybackSessionMetadata()
- player.prepareAsync()
- player.start()
- Logger.info(TAG, `[MusicCast] prepare issued start song=${this.currentSong.name}, playerId=${PLAYER_ID}`)
- this.syncHostStorage(PlayStatus.PAUSE)
- this.publishSnapshot(false)
- return true
- } catch (error) {
- Logger.error(TAG, `[MusicCast] prepare failed: ${(error as Error).message}`)
- this.isPrepared = false
- this.isPlaying = false
- this.syncHostStorage(PlayStatus.PAUSE)
- this.publishSnapshot(false)
- return false
- }
- }
- private ensurePlayer(): IjkMediaPlayer {
- if (this.player) {
- return this.player
- }
- const player = new IjkMediaPlayer()
- player.setAudioId(PLAYER_ID)
- player.native_setup()
- player.setOnPreparedListener(new ImplOnPreparedListener(() => {
- this.isPrepared = true
- player.start()
- this.isPlaying = player.isPlaying()
- Logger.info(TAG,
- `[MusicCast] onPrepared song=${this.currentSong?.name ?? ''}, path=${this.currentSong?.filePath ?? ''}, ` +
- `duration=${player.getDuration()}, isPlaying=${player.isPlaying()}, position=${player.getCurrentPosition()}, ` +
- `audioSessionId=${player.getAudioSessionId()}`)
- this.startProgressTimer()
- this.verifyPlaybackStarted(this.playbackStartVerifyToken, 'onPrepared')
- this.syncHostStorage(PlayStatus.PLAY)
- this.syncPlaybackSessionState(PlayStatus.PLAY)
- this.publishSnapshot(true)
- }))
- player.setOnCompletionListener(new ImplOnCompletionListener(() => {
- Logger.info(TAG,
- `[MusicCast] onCompletion currentIndex=${this.currentIndex}, queueLength=${this.queue.length}, playType=${this.playType}`)
- this.handleCompletion()
- }))
- player.setOnErrorListener(new ImplOnErrorListener((what: number, extra: number) => {
- Logger.error(TAG, `[MusicCast] player error what=${what}, extra=${extra}, url=${this.currentUrl}`)
- this.isPrepared = false
- this.isPlaying = false
- this.stopProgressTimer()
- this.syncHostStorage(PlayStatus.PAUSE)
- this.syncPlaybackSessionState(PlayStatus.PAUSE)
- this.publishSnapshot(false)
- }))
- player.setOnSeekCompleteListener(new ImplOnSeekCompleteListener(() => {
- Logger.info(TAG, `[MusicCast] onSeekComplete position=${player.getCurrentPosition()}`)
- this.publishProgress()
- }))
- player.on('audioInterrupt', (event: InterruptEvent) => {
- this.handleAudioInterrupt(player, event)
- })
- player.on('deviceChange', (event: InterruptEvent) => {
- Logger.info(TAG, `[MusicCast] deviceChange reason=${event.reason ?? DeviceChangeReason.REASON_UNKNOWN}`)
- })
- player.setMessageListener()
- this.player = player
- Logger.info(TAG, `[MusicCast] ensurePlayer created playerId=${PLAYER_ID}`)
- return player
- }
- private handleCompletion(): void {
- const completion = MusicPlaybackController.resolveCompletionAction(this.playType, this.currentIndex, this.queue.length)
- if (completion.action === 'replay_current') {
- void this.playIndex(this.currentIndex)
- return
- }
- if (completion.action === 'stop_current') {
- this.stop()
- return
- }
- const nextIndex = MusicPlaybackController.resolveNextQueueIndex(this.currentIndex, this.queue.length).nextIndex
- void this.playIndex(nextIndex)
- }
- private pause(): void {
- const player = this.player
- if (!player) {
- Logger.warn(TAG, '[MusicCast] pause ignored because player missing')
- return
- }
- player.pause()
- this.isPlaying = false
- this.stopProgressTimer()
- Logger.info(TAG, `[MusicCast] pause position=${player.getCurrentPosition()}, song=${this.currentSong?.name ?? ''}`)
- this.syncHostStorage(PlayStatus.PAUSE)
- this.syncPlaybackSessionState(PlayStatus.PAUSE)
- this.publishSnapshot(false)
- }
- private resume(): void {
- const player = this.player
- if (!player) {
- Logger.warn(TAG, '[MusicCast] resume ignored because player missing')
- return
- }
- player.start()
- this.isPlaying = true
- this.startProgressTimer()
- Logger.info(TAG, `[MusicCast] resume position=${player.getCurrentPosition()}, song=${this.currentSong?.name ?? ''}`)
- this.syncHostStorage(PlayStatus.PLAY)
- this.syncPlaybackSessionState(PlayStatus.PLAY)
- this.publishSnapshot(true)
- }
- private verifyPlaybackStarted(token: number, source: string): void {
- setTimeout(() => {
- if (token !== this.playbackStartVerifyToken || !this.player) {
- return
- }
- const player = this.player
- const isPlayingNow = player.isPlaying()
- const currentPosition = player.getCurrentPosition()
- Logger.info(TAG,
- `[MusicCast] verifyPlaybackStarted source=${source}, token=${token}, isPlaying=${isPlayingNow}, position=${currentPosition}`)
- if (isPlayingNow && currentPosition > 0) {
- this.isPlaying = true
- this.syncPlaybackSessionState(PlayStatus.PLAY)
- return
- }
- Logger.warn(TAG,
- `[MusicCast] verifyPlaybackStarted retry start source=${source}, token=${token}, ` +
- `isPlaying=${isPlayingNow}, position=${currentPosition}`)
- player.start()
- setTimeout(() => {
- if (token !== this.playbackStartVerifyToken || !this.player) {
- return
- }
- const retryPlaying = this.player.isPlaying()
- const retryPosition = this.player.getCurrentPosition()
- this.isPlaying = retryPlaying
- Logger.info(TAG,
- `[MusicCast] verifyPlaybackStarted afterRetry source=${source}, token=${token}, ` +
- `isPlaying=${retryPlaying}, position=${retryPosition}`)
- this.syncPlaybackSessionState(retryPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- }, PLAYBACK_VERIFY_RETRY_DELAY_MS)
- }, PLAYBACK_VERIFY_DELAY_MS)
- }
- private stop(): void {
- const player = this.player
- const abilityContext = this.context as common.UIAbilityContext | undefined
- if (player) {
- Logger.info(TAG, `[MusicCast] stop position=${player.getCurrentPosition()}, song=${this.currentSong?.name ?? ''}`)
- player.stop()
- }
- this.isPlaying = false
- this.isPrepared = false
- this.stopProgressTimer()
- this.syncHostStorage(PlayStatus.INIT)
- this.syncPlaybackSessionState(PlayStatus.INIT)
- this.publishSnapshot(false)
- BackgroundTaskManager.stopContinuousTask(abilityContext)
- }
- private seekToInternal(value: string): boolean {
- const player = this.player
- if (!player || !this.currentSong || value === '') {
- Logger.warn(TAG,
- `[MusicCast] seek ignored playerReady=${player !== undefined}, songReady=${this.currentSong !== undefined}, value=${value}`)
- return false
- }
- Logger.info(TAG, `[MusicCast] seekTo value=${value}, song=${this.currentSong.name}`)
- player.seekTo(value)
- return true
- }
- private startProgressTimer(): void {
- this.stopProgressTimer()
- this.progressTimerId = setInterval(() => {
- this.publishProgress()
- }, PROGRESS_INTERVAL_MS)
- }
- private stopProgressTimer(): void {
- if (this.progressTimerId >= 0) {
- clearInterval(this.progressTimerId)
- this.progressTimerId = -1
- }
- }
- private publishProgress(): void {
- if (!this.player || !this.currentSong || !this.context) {
- return
- }
- const durationMs = this.resolveDurationMs()
- const positionMs = Math.max(0, this.player.getCurrentPosition())
- const progressValue = resolvePlaybackSnapshotProgressValue(positionMs, durationMs, 100)
- Logger.info(TAG,
- `[MusicCast] progress position=${positionMs}, duration=${durationMs}, isPlaying=${this.player.isPlaying()}, ` +
- `audioSessionId=${this.player.getAudioSessionId()}`)
- MusicCardManager.getInstance().notifyProgressTick(
- this.context,
- this.currentSong,
- this.isPlaying,
- positionMs,
- durationMs,
- this.currentSong.lyricContent,
- this.currentSong.pixelMapPath
- )
- this.syncPlaybackDisplayState(progressValue, positionMs, durationMs)
- this.syncPlaybackSessionState(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.persistPlaybackSnapshot(this.isPlaying)
- }
- private async ensurePlaybackSession(): Promise<void> {
- const abilityContext = this.context as common.UIAbilityContext | undefined
- if (!abilityContext) {
- Logger.warn(TAG, '[MusicCast] ensurePlaybackSession skipped because abilityContext missing')
- return
- }
- BackgroundTaskManager.startContinuousTask(abilityContext)
- if (this.playbackSession || this.creatingPlaybackSession) {
- return
- }
- this.creatingPlaybackSession = true
- try {
- const session = await avSession.createAVSession(abilityContext, 'music_card_background_audio', 'audio')
- this.playbackSession = session
- Logger.info(TAG, `[MusicCast] playbackSession created sessionId=${session.sessionId}`)
- try {
- await session.activate()
- Logger.info(TAG, '[MusicCast] playbackSession activated')
- } catch (error) {
- const err = error as BusinessError
- Logger.error(TAG, `[MusicCast] playbackSession activate failed code=${err.code}, message=${err.message}`)
- }
- await this.setPlaybackSessionLaunchAbility(abilityContext, session)
- this.registerPlaybackSessionCallbacks(session)
- await this.syncPlaybackSessionMetadata()
- this.syncPlaybackSessionState(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- } catch (error) {
- const err = error as BusinessError
- Logger.error(TAG, `[MusicCast] playbackSession create failed code=${err.code}, message=${err.message}`)
- } finally {
- this.creatingPlaybackSession = false
- }
- }
- private async setPlaybackSessionLaunchAbility(abilityContext: common.UIAbilityContext,
- session: avSession.AVSession): Promise<void> {
- try {
- const want = new Want()
- want.bundleName = abilityContext.abilityInfo.bundleName
- want.abilityName = abilityContext.abilityInfo.name
- const wantAgentInfo: wantAgent.WantAgentInfo = {
- wants: [want],
- operationType: wantAgent.OperationType.START_ABILITIES,
- requestCode: 0,
- wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
- }
- const agent = await wantAgent.getWantAgent(wantAgentInfo)
- await session.setLaunchAbility(agent)
- Logger.info(TAG, '[MusicCast] playbackSession launchAbility attached')
- } catch (error) {
- const err = error as BusinessError
- Logger.error(TAG, `[MusicCast] playbackSession launchAbility failed code=${err.code}, message=${err.message}`)
- }
- }
- private registerPlaybackSessionCallbacks(session: avSession.AVSession): void {
- if (this.playbackSessionCallbacksRegistered) {
- return
- }
- session.on('play', () => {
- void this.handlePlaybackSessionPlay()
- })
- session.on('pause', () => {
- this.handlePlaybackSessionPause()
- })
- session.on('stop', () => {
- this.handlePlaybackSessionStop()
- })
- session.on('playNext', () => {
- void this.playNext()
- })
- session.on('playPrevious', () => {
- void this.playPrevious()
- })
- session.on('seek', (time: number) => {
- void this.seekTo(`${time}`)
- })
- session.on('setLoopMode', (_mode: number) => {
- void this.setLoopMode()
- })
- session.on('toggleFavorite', (_assetId: string) => {
- this.toggleCurrentSongFavoriteState()
- })
- this.playbackSessionCallbacksRegistered = true
- }
- private async handlePlaybackSessionPlay(): Promise<void> {
- this.restorePersistedQueueIfNeeded()
- if (this.isPrepared && !this.isPlaying) {
- this.resume()
- return
- }
- if (this.currentIndex >= 0) {
- await this.playIndex(this.currentIndex)
- return
- }
- if (this.queue.length > 0) {
- await this.playIndex(0)
- }
- }
- private handlePlaybackSessionPause(): void {
- if (this.isPlaying) {
- this.pause()
- }
- }
- private handlePlaybackSessionStop(): void {
- this.stop()
- }
- private async syncPlaybackSessionMetadata(): Promise<void> {
- if (!this.playbackSession || !this.currentSong) {
- return
- }
- try {
- const mediaImage = await this.resolvePlaybackSessionMediaImage()
- let metadata: avSession.AVMetadata = {
- assetId: this.currentSong.filePath ?? this.currentSong.name ?? '',
- title: this.currentSong.name,
- artist: this.currentSong.artist,
- mediaImage: mediaImage,
- duration: this.resolveDurationMs()
- }
- await this.playbackSession.setAVMetadata(metadata)
- Logger.info(TAG,
- `[MusicCast] playbackSession metadata updated title=${this.currentSong.name}, duration=${metadata.duration ?? 0}`)
- } catch (error) {
- const err = error as BusinessError
- Logger.error(TAG, `[MusicCast] playbackSession metadata failed code=${err.code}, message=${err.message}`)
- }
- }
- private async resolvePlaybackSessionMediaImage(): Promise<image.PixelMap | string> {
- const coverPath = this.currentSong?.pixelMapPath ?? ''
- if (StrUtil.isNotEmpty(coverPath)) {
- if (coverPath.startsWith('http://') || coverPath.startsWith('https://')) {
- return coverPath
- }
- try {
- const localCoverPath = coverPath.startsWith('file://') ? FileUtil.getFilePath(coverPath) : coverPath
- return await imagePathToPixelMap(localCoverPath)
- } catch (error) {
- Logger.warn(TAG, `[MusicCast] cover pixelMap failed path=${coverPath}, message=${(error as Error).message}`)
- }
- }
- return await ImageUtil.getPixelMapFromMedia($r('app.media.alt'))
- }
- private syncPlaybackSessionState(status: number): void {
- if (!this.playbackSession || !this.player) {
- return
- }
- let state: avSession.PlaybackState = avSession.PlaybackState.PLAYBACK_STATE_INITIAL
- if (status === PlayStatus.PLAY) {
- state = avSession.PlaybackState.PLAYBACK_STATE_PLAY
- } else if (status === PlayStatus.PAUSE) {
- state = avSession.PlaybackState.PLAYBACK_STATE_PAUSE
- } else if (status === PlayStatus.INIT) {
- state = avSession.PlaybackState.PLAYBACK_STATE_STOP
- }
- let playbackState: avSession.AVPlaybackState = {
- state: state,
- loopMode: MusicPlaybackController.resolveAvSessionLoopMode(this.playType),
- isFavorite: this.currentSong?.isFav === 1,
- position: {
- elapsedTime: Math.max(0, this.player.getCurrentPosition()),
- updateTime: Date.now()
- }
- }
- this.playbackSession.setAVPlaybackState(playbackState).then(() => {
- Logger.info(TAG,
- `[MusicCast] playbackSession state=${state}, position=${playbackState.position?.elapsedTime ?? 0}`)
- }).catch((error: BusinessError) => {
- Logger.error(TAG, `[MusicCast] playbackSession state failed code=${error.code}, message=${error.message}`)
- })
- }
- private handleAudioInterrupt(player: IjkMediaPlayer, event: InterruptEvent): void {
- Logger.info(TAG,
- `[MusicCast] audioInterrupt forceType=${event.forceType ?? -1}, hintType=${event.hintType ?? -1}, ` +
- `isPlaying=${player.isPlaying()}, position=${player.getCurrentPosition()}`)
- switch (event.hintType) {
- case InterruptHintType.INTERRUPT_HINT_PAUSE:
- case InterruptHintType.INTERRUPT_HINT_STOP:
- this.isPlaying = false
- this.stopProgressTimer()
- this.syncHostStorage(PlayStatus.PAUSE)
- this.syncPlaybackSessionState(PlayStatus.PAUSE)
- this.publishSnapshot(false)
- break
- case InterruptHintType.INTERRUPT_HINT_RESUME:
- if (this.isPrepared && !player.isPlaying()) {
- player.start()
- this.isPlaying = true
- this.startProgressTimer()
- this.syncHostStorage(PlayStatus.PLAY)
- this.syncPlaybackSessionState(PlayStatus.PLAY)
- this.publishSnapshot(true)
- }
- break
- default:
- break
- }
- }
- private publishSnapshot(isPlaying: boolean): void {
- this.persistPlaybackSnapshot(isPlaying)
- if (!this.context) {
- return
- }
- const options = new MusicCardPlaybackStateOptions()
- options.currentSong = this.currentSong
- options.isPlaying = isPlaying
- options.positionMs = this.player ? Math.max(0, this.player.getCurrentPosition()) : 0
- options.durationMs = this.resolveDurationMs()
- options.lyricText = this.currentSong?.lyricContent
- options.coverPath = this.currentSong?.pixelMapPath
- MusicCardManager.getInstance().notifyPlaybackStateChanged(
- this.context,
- MusicCardManager.getInstance().buildSnapshotFromPlaybackState(options)
- )
- }
- private persistPlaybackSnapshot(isPlaying: boolean): void {
- const snapshot = this.buildPlaybackSnapshot(isPlaying)
- Logger.info(TAG,
- `[MiniState] persistPlaybackSnapshot song=${snapshot.currentSongName}, index=${snapshot.currentIndex}, ` +
- `isPlaying=${snapshot.isPlaying}, shouldResume=${snapshot.shouldResumeWhenActivated}, ` +
- `position=${snapshot.positionMs}, duration=${snapshot.durationMs}`)
- this.snapshotStore.write(snapshot)
- }
- private buildPlaybackSnapshot(isPlaying: boolean): PlaybackSnapshot {
- return {
- queue: this.queue.map((item: VideoItem): PlaybackSnapshotSong => {
- return {
- filePath: item.filePath,
- id: item.id,
- type: item.type,
- name: item.name,
- isFav: item.isFav,
- artist: item.artist,
- album: item.album,
- duration: item.duration,
- pixelMapPath: item.pixelMapPath,
- lyricContent: item.lyricContent,
- remote_rel_path: item.remote_rel_path,
- webdav_account_id: item.webdav_account_id
- }
- }),
- currentIndex: this.currentIndex,
- currentSongKey: this.currentSong?.filePath ?? '',
- currentSongName: this.currentSong?.name ?? '',
- currentArtist: this.currentSong?.artist,
- isPlaying,
- positionMs: this.player ? Math.max(0, this.player.getCurrentPosition()) : 0,
- durationMs: this.resolveDurationMs(),
- cover: this.currentSong?.pixelMapPath,
- playType: this.playType,
- playlistContext: 'host_runtime',
- updatedAt: Date.now(),
- shouldResumeWhenActivated: isPlaying || this.isPrepared
- }
- }
- private resolveDurationMs(): number {
- if (this.player) {
- const playerDuration = this.player.getDuration()
- if (playerDuration > 0) {
- return playerDuration
- }
- }
- return this.parseDurationText(this.currentSong?.duration)
- }
- private parseDurationText(durationText: string | undefined): number {
- if (!durationText || durationText === '') {
- return 0
- }
- const parts = durationText.split(':')
- if (parts.length < 2) {
- return 0
- }
- const numbers = parts.map((part: string): number => Number.parseInt(part, 10))
- if (numbers.some((value: number): boolean => Number.isNaN(value))) {
- return 0
- }
- if (numbers.length === 2) {
- return (numbers[0] * 60 + numbers[1]) * 1000
- }
- return (numbers[0] * 3600 + numbers[1] * 60 + numbers[2]) * 1000
- }
- private persistCurrentQueue(): void {
- PreferencesUtil.putSync('LastMusicInfo', this.currentSong)
- PreferencesUtil.putSync('LastMusicList', this.queue)
- PreferencesUtil.putSync('LastPlayModeType', this.playType)
- PreferencesUtil.putSync('musicPlayType', this.playType)
- Logger.info(TAG,
- `[MusicCast] persist queueLength=${this.queue.length}, currentIndex=${this.currentIndex}, ` +
- `currentSong=${this.currentSong?.name ?? ''}, path=${this.currentSong?.filePath ?? ''}, playType=${this.playType}`)
- }
- private syncHostStorage(status: PlayStatus): void {
- const isPlaying = isPlaybackControlPlaying(status)
- const positionMs = this.player ? Math.max(0, this.player.getCurrentPosition()) : 0
- const durationMs = this.resolveDurationMs()
- const progressValue = resolvePlaybackSnapshotProgressValue(positionMs, durationMs, 100)
- AppStorage.setOrCreate('CONTROL_PlayStatus', status)
- AppStorage.setOrCreate('isPlaying', isPlaying)
- AppStorage.setOrCreate('animationState', isPlaying ? AnimationStatus.Running : AnimationStatus.Paused)
- AppStorage.setOrCreate('musicPlayType', this.playType)
- AppStorage.setOrCreate('songList', this.queue)
- AppStorage.setOrCreate('currIndex', this.currentIndex)
- AppStorage.setOrCreate('currentSong', this.currentSong)
- this.syncPlaybackDisplayState(progressValue, positionMs, durationMs)
- AppStorage.setOrCreate('cover', this.currentSong?.pixelMapPath ?? '')
- AppStorage.setOrCreate('currentSongName', this.currentSong?.name ?? '')
- AppStorage.setOrCreate('currentSongArtist', this.currentSong?.artist ?? '')
- Logger.info(TAG,
- `[MusicCast] syncHostStorage status=${status}, currentSong=${this.currentSong?.name ?? ''}, ` +
- `cover=${this.currentSong?.pixelMapPath ?? ''}`)
- }
- private syncPlaybackDisplayState(progressValue: number, positionMs: number, durationMs: number): void {
- AppStorage.setOrCreate('progressValue', progressValue)
- AppStorage.setOrCreate('playbackPositionMs', positionMs)
- AppStorage.setOrCreate('playbackDurationMs', durationMs)
- AppStorage.setOrCreate('playbackIsFavorite', this.currentSong?.isFav === 1)
- }
- private toggleCurrentSongFavoriteState(): void {
- const nextFavorite = this.currentSong?.isFav !== 1
- this.applyCurrentSongFavoriteState(nextFavorite)
- }
- private applyCurrentSongFavoriteState(isFavorite: boolean): void {
- if (!this.currentSong) {
- return
- }
- const nextFavorite = isFavorite ? 1 : 0
- this.currentSong.isFav = nextFavorite
- if (this.currentIndex >= 0 && this.currentIndex < this.queue.length) {
- this.queue[this.currentIndex].isFav = nextFavorite
- }
- this.persistCurrentQueue()
- this.syncHostStorage(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.syncPlaybackSessionState(this.isPlaying ? PlayStatus.PLAY : PlayStatus.PAUSE)
- this.persistPlaybackSnapshot(this.isPlaying)
- Logger.info(TAG, `[MusicCast] favorite updated song=${this.currentSong.name}, isFavorite=${isFavorite}`)
- }
- }
|