1/* 2* Copyright (C) 2023 Huawei Device Co., Ltd. 3* Licensed under the Apache License, Version 2.0 (the "License"); 4* you may not use this file except in compliance with the License. 5* You may obtain a copy of the License at 6* 7* http://www.apache.org/licenses/LICENSE-2.0 8* 9* Unless required by applicable law or agreed to in writing, software 10* distributed under the License is distributed on an "AS IS" BASIS, 11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12* See the License for the specific language governing permissions and 13* limitations under the License. 14*/ 15 16import avSession from '@ohos.multimedia.avsession'; 17import media from '@ohos.multimedia.media'; 18import promptAction from '@ohos.promptAction'; 19import CommonUtils from '../common/CommonUtils'; 20import common from '@ohos.app.ability.common'; 21import AudioUtils from '../common/AudioUtils'; 22import connection from '@ohos.net.connection'; 23import wantAgent from '@ohos.app.ability.wantAgent'; 24import Constants from '../common/Constants'; 25import AVCastPicker from '@ohos.multimedia.avCastPicker'; 26import audio from '@ohos.multimedia.audio'; 27import fs from '@ohos.file.fs'; 28import util from '@ohos.util'; 29import image from '@ohos.multimedia.image'; 30import { BusinessError } from '@ohos.base'; 31import Log from '../common/Log'; 32import deviceInfo from '@ohos.deviceInfo'; 33 34@Entry 35@Component 36struct Index { 37 @State message: string = 'Hello World'; 38 @State outputDevice: avSession.OutputDeviceInfo = {devices: []}; 39 @State outputDeviceInfo: avSession.OutputDeviceInfo = {devices: []}; 40 @State castController: avSession.AVCastController | undefined = undefined; 41 @State castControllerSession: avSession.AVSessionController | undefined = undefined; 42 @State session: avSession.AVSession | undefined = undefined; 43 @State controller: avSession.AVSessionController | undefined = undefined; 44 @State albumImage: image.PixelMap | undefined = undefined; 45 @State playType: 'local' | 'cast' = 'local'; 46 @StorageLink('playState') playState: number = -1; 47 @State isFavorMap: Map<string, boolean> = new Map(); 48 @State volume: number = 0 ; 49 @State seedPosition: number = 0; 50 @State duration: number = 0; 51 @State private currentIndex: number = 0; 52 @State @Watch('playInfoUpdated') currentPlayInfo: avSession.AVMediaDescription | undefined = undefined; 53 @State currentMediaId: string = ''; 54 @State currentLoopMode: number = 2; 55 @State hasNetwork: boolean = false; 56 @State isProgressSliding: boolean = false; 57 @State audioType: 'url' | 'rawfile' | 'scan' | 'video' = 'url'; 58 @State avCastPickerColor:Color = Color.White; 59 private audioUtils: AudioUtils = new AudioUtils(); 60 private avPlayer: media.AVPlayer | undefined = undefined; 61 private audioVolumeGroupManager: audio.AudioVolumeGroupManager | undefined = undefined; 62 private localAudioRation = 1; 63 private audioManager: audio.AudioManager | undefined = undefined; 64 private netCon?: connection.NetConnection; 65 private sliderTimer?: number; 66 private mXComponentController: XComponentController = new XComponentController(); 67 @State private songList: Array<avSession.AVMediaDescription> = []; 68 private urlVideoList: Array<avSession.AVMediaDescription> = Constants.URL_VIDEO_LIST; 69 70 async aboutToAppear() { 71 Log.info('about to appear'); 72 this.songList = this.urlVideoList; 73 this.audioType = 'video'; 74 this.currentPlayInfo = this.urlVideoList[0]; 75 this.avPlayer = await this.audioUtils.init(); 76 this.avPlayer?.on('audioInterrupt', (info: audio.InterruptEvent) => { 77 Log.info('audioInterrupt success, and InterruptEvent info is: ' + info); 78 if (this.avPlayer?.state === 'playing') { 79 Log.info('audio interrupt, start pause'); 80 this.avPlayer?.pause(); 81 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PAUSE); 82 promptAction.showToast({ message: 'audio interrupt, pause done' }); 83 } 84 }) 85 this.avPlayer?.on('timeUpdate', (time: number) => { 86 Log.info('timeUpdate time: ' + time); 87 if (!this.isProgressSliding) { 88 if (this.duration == 0) { 89 this.seedPosition = 0; 90 } else { 91 this.seedPosition = time / this.duration * 100; 92 } 93 const params: avSession.AVPlaybackState = { 94 position: { 95 elapsedTime: time, 96 updateTime: new Date().getTime() 97 }, 98 }; 99 this.session?.setAVPlaybackState(params); 100 } 101 }) 102 this.avPlayer?.on('durationUpdate', (duration: number) => { 103 Log.info('durationUpdate duration: ' + duration); 104 this.duration = duration; 105 if (this.duration !== 0) { 106 const playMetaData: avSession.AVMetadata = { 107 assetId: this.currentPlayInfo?.assetId as string, // origin assetId 108 title: this.currentPlayInfo?.title as string, 109 artist: this.currentPlayInfo?.artist as string, 110 mediaImage: this.albumImage, // origin mediaImage 111 album: this.currentPlayInfo?.albumTitle as string, 112 duration: this.duration 113 } 114 this.session?.setAVMetadata(playMetaData); 115 } 116 117 }) 118 this.avPlayer?.on('videoSizeChange', (width: number, height: number) => { 119 Log.info('videoSizeChange success, and width is: ' + width + ', height is: ' + height); 120 }) 121 await this.setAudioManager(); 122 await this.autoStartAll(false); 123 this.addNetworkListener(); 124 this.readLRCFile(); 125 Log.info('about to appear done: ' + !!this.avPlayer); 126 } 127 128 readLRCFile(): void { 129 const context = getContext(this) as common.UIAbilityContext; 130 context.resourceManager.getRawFileContent('test.lrc', (error: BusinessError, value: Uint8Array) => { 131 if (error != null) { 132 Log.info('error is: ' + error); 133 } else { 134 let rawFile = value; 135 let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }); 136 let retStr = textDecoder.decodeWithStream(rawFile, { stream: false }); 137 Log.info('get lrc file: ' + retStr); 138 } 139 }); 140 } 141 142 addNetworkListener(): void { 143 Log.info('start add Network Listener'); 144 this.netCon = connection.createNetConnection(); 145 this.netCon?.register((error: BusinessError) => { 146 Log.error('network error: ' + JSON.stringify(error)); 147 }) 148 connection.getAllNets().then(data => { 149 Log.info('get all network: ' + JSON.stringify(data)); 150 this.hasNetwork = data?.length > 0; 151 }) 152 this.netCon?.on('netAvailable', data => { 153 Log.info('network Available: ' + JSON.stringify(data)); 154 this.hasNetwork = true; 155 }) 156 this.netCon?.on('netLost', data => { 157 Log.info('network Lost: ' + JSON.stringify(data)); 158 connection.getAllNets().then(data => { 159 Log.info('get all network: ' + JSON.stringify(data)); 160 this.hasNetwork = data?.length > 0; 161 }); 162 }) 163 } 164 165 onPageHide() { 166 Log.info('indexPage onPAgeHide in.'); 167 } 168 169 async playInfoUpdated() { 170 Log.info('playInfoUpdated: ' + JSON.stringify(this.currentPlayInfo)); 171 this.currentMediaId = this.currentPlayInfo?.assetId as string; 172 this.albumImage = this.currentPlayInfo?.mediaImage as image.PixelMap 173 if (this.playType === 'local') { 174 await this.setLocalMediaInfo(); 175 } else { 176 await this.setRemoteMediaInfo(); 177 } 178 Log.info('playInfoUpdate: done') 179 } 180 181 async setRemoteMediaInfo() { 182 Log.info('set remote media info: ' + JSON.stringify(this.currentPlayInfo) + ', ' + this.currentIndex); 183 let queueItem: avSession.AVQueueItem = { 184 itemId: this.currentIndex, 185 description: this.currentPlayInfo 186 }; 187 188 await this.castController?.prepare(queueItem); 189 const isPlaying = this.playState === avSession.PlaybackState.PLAYBACK_STATE_PLAY; 190 if (isPlaying) { 191 await this.castController?.start(queueItem); 192 await this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 193 } 194 if (this.audioType === 'scan') { 195 const playMetaData: avSession.AVMetadata = { 196 assetId: this.currentPlayInfo?.assetId as string, // origin assetId 197 title: this.currentPlayInfo?.title as string, 198 artist: this.currentPlayInfo?.artist as string, 199 mediaImage: this.albumImage, // origin mediaImage 200 album: this.currentPlayInfo?.albumTitle as string, 201 duration: this.duration, 202 }; 203 Log.info('try set AV Metadata while cast for scan: ' + JSON.stringify(playMetaData)); 204 this.session?.setAVMetadata(playMetaData); 205 } 206 Log.info('set remote media info done'); 207 } 208 209 async setLocalMediaInfo() { 210 Log.info('set local media info: ' + JSON.stringify(this.currentPlayInfo)); 211 if (!this.session) { 212 Log.info('set local media info: no session'); 213 return; 214 } 215 if (this.audioUtils) { 216 const isPlaying = this.playState === avSession.PlaybackState.PLAYBACK_STATE_PLAY; 217 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PAUSE, this.currentPlayInfo?.assetId, 218 !!this.isFavorMap[this.currentPlayInfo?.assetId as string]); 219 Log.info('set play state pause'); 220 if (this.audioType === 'url' || this.audioType === 'video') { 221 await this.audioUtils.loadFromNetwork(this.currentPlayInfo?.mediaUri as string); 222 } else if (this.audioType === 'rawfile') { 223 await this.audioUtils.loadFromNetwork(this.currentPlayInfo?.mediaUri as string); 224 } else if (this.audioType === 'scan') { 225 await this.audioUtils.loadFromSrcFd(this.currentPlayInfo?.fdSrc as media.AVFileDescriptor); 226 } 227 Log.info('local local audio done: ' + isPlaying + ', ' + this.playType + ', ' + this.avPlayer?.state); 228 if (isPlaying) { 229 this.audioUtils.on('prepared', () => { 230 Log.info('AVPlayer state prepare, state play'); 231 if (this.playType === 'local') { 232 this.localPlayOrPause(); 233 } else { 234 this.remotePlayOrPause(); 235 } 236 }); 237 } 238 } else { 239 Log.info('set local media fail: no audioUtils'); 240 } 241 this.albumImage = this.currentPlayInfo?.mediaImage as image.PixelMap; 242 const playMetaData: avSession.AVMetadata = { 243 assetId: this.currentPlayInfo?.assetId as string, // origin assetId 244 title: this.currentPlayInfo?.title as string, 245 artist: this.currentPlayInfo?.artist as string, 246 mediaImage: this.albumImage, // origin mediaImage 247 album: this.currentPlayInfo?.albumTitle as string, 248 duration: this.duration, 249 }; 250 Log.info('try set AV Metadata: ' + JSON.stringify(playMetaData)); 251 this.session?.setAVMetadata(playMetaData); 252 Log.info('set AV Metadata: '); 253 } 254 255 aboutToDisappear() { 256 Log.info('about to disappear'); 257 if (this.controller) { 258 this.controller.off('outputDeviceChange'); 259 this.controller.destroy(); 260 } 261 if (this.castController) { 262 this.castController?.off('playbackStateChange'); 263 this.castController?.off('error'); 264 this.castController?.off('playPrevious'); 265 this.castController?.off('playNext'); 266 } 267 try { 268 if (this.session) { 269 this.session?.stopCasting(); 270 this.session?.destroy(); 271 } 272 } catch (err) { 273 Log.info( `err is: ${JSON.stringify(err)}`); 274 } 275 if (this.avPlayer) { 276 this.avPlayer?.release(); 277 } 278 // 使用unregister接口取消订阅 279 this.netCon?.unregister((error) => { 280 Log.info('error is: ' + JSON.stringify(error)); 281 }) 282 } 283 284 async setAudioManager() { 285 Log.info('try get audio manger'); 286 const audioManager = audio.getAudioManager(); 287 if (!audioManager) { 288 Log.error('get audio manager fail: fail get audioManager'); 289 return; 290 } 291 this.audioManager = audioManager; 292 const volumeManager = audioManager.getVolumeManager(); 293 if (!volumeManager) { 294 Log.error('get audio manager fail: fail get volumeManager'); 295 return; 296 } 297 volumeManager.on('volumeChange', (volumeEvent) => { 298 Log.info(`VolumeType of stream : ${JSON.stringify(volumeEvent)}`); 299 let type: audio.AudioVolumeType = volumeEvent.volumeType; 300 let num: number = volumeEvent.volume; 301 if(type == audio.AudioVolumeType.MEDIA && this.playType === 'local') { 302 this.volume = num / this.localAudioRation; 303 } 304 }); 305 this.audioVolumeGroupManager = await volumeManager.getVolumeGroupManager(audio.DEFAULT_VOLUME_GROUP_ID); 306 if (!this.audioVolumeGroupManager) { 307 Log.error('get audio manager fail: fail get audioVolumeGroupManager'); 308 return; 309 } 310 const maxVolume = await this.audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA); 311 const minVolume = await this.audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA); 312 const volume = await this.audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA); 313 this.localAudioRation = (maxVolume - minVolume) / 100; 314 this.volume = volume / this.localAudioRation; 315 Log.info('get audio manager done, ' + maxVolume + ', ' + minVolume + ', ' + this.localAudioRation); 316 } 317 318 async setPlayState(state?: number, id?: string, favor?: boolean, elapsedTime?: number) { 319 if (!this.session) { 320 Log.info('fail set state, session undefined'); 321 promptAction.showToast({ message: 'No Session' }); 322 return null; 323 } 324 const params: avSession.AVPlaybackState = {}; 325 if (typeof state !== 'undefined') { 326 this.playState = state; 327 params.state = state; 328 } 329 if (typeof id !== 'undefined') { 330 this.isFavorMap[id] = favor; 331 params.isFavorite = favor; 332 } 333 // 更新播放进度 334 if (elapsedTime !== undefined) { 335 params.position = { 336 elapsedTime: elapsedTime, 337 updateTime: new Date().getTime(), 338 } 339 } 340 this.session?.setAVPlaybackState(params); 341 Log.info('params test ' + JSON.stringify(params)); 342 Log.info('isFavorMap test, ' + id + ', ' + JSON.stringify(this.isFavorMap)); 343 return this.session?.setAVPlaybackState(params); 344 } 345 346 async setListenerForMesFromController() { 347 Log.info('setListenerForMesFromController'); 348 this.session?.on('play', () => { 349 Log.info('on play, do play test'); 350 if (this.avPlayer) { 351 this.avPlayer?.play(); 352 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 353 } 354 }); 355 this.session?.on('pause', () => { 356 Log.info('on pause, do pause test'); 357 if (this.avPlayer) { 358 this.avPlayer?.pause(); 359 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PAUSE); 360 } 361 }); 362 this.session?.on('stop', () => { 363 Log.info('on stop, do stop test'); 364 if (this.avPlayer) { 365 this.avPlayer?.stop(); 366 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_STOP); 367 } 368 }); 369 this.session?.on('playPrevious', () => { 370 Log.info('on playPrevious, do playPrevious test'); 371 this.switchToPreviousByLoopMode(); 372 }); 373 this.session?.on('playNext', () => { 374 Log.info('on playNext, do playNext test'); 375 this.switchToNextByLoopMode(); 376 }); 377 this.session?.on('toggleFavorite', (id) => { 378 Log.info('on toggleFavorite session, do toggleFavorite test: ' + id); 379 this.setPlayState(undefined, id, !this.isFavorMap[id]); 380 }); 381 // 注册播放快退命令监听 382 this.session?.on('rewind', (time?: number) => { 383 time = time ? time : 0; 384 let currentTime = this.avPlayer ? this.avPlayer.currentTime : 0; 385 let timeMs: number = ((currentTime - time * 1000) <= 0) ? 0 : (currentTime - time *1000); 386 this.avPlayer?.seek(timeMs); 387 Log.info('rewind currentTime ' + timeMs); 388 this.avPlayer?.play(); 389 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 390 }); 391 // 注册播放快进命令监听 392 this.session?.on('fastForward', (time?: number) => { 393 time = time ? time : 0; 394 let currentTime = this.avPlayer ? this.avPlayer.currentTime : 0; 395 let timeMs: number = ((time * 1000 + currentTime) > this.duration) ? this.duration : (time *1000 + currentTime); 396 if (time * 1000 + currentTime > this.duration) { 397 this.switchToNextByLoopMode(); 398 } else { 399 this.avPlayer?.seek(timeMs); 400 this.avPlayer?.play(); 401 this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 402 } 403 }); 404 this.session?.on('seek', (position) => { 405 Log.info('on seek: seek test: ' + position); 406 // 修改播放进度 407 this.avPlayer?.seek(position); 408 // 重新设置播放进度 409 const params: avSession.AVPlaybackState = { 410 position: { 411 elapsedTime: position, 412 updateTime: new Date().getTime(), 413 }, 414 }; 415 this.session?.setAVPlaybackState(params); 416 }); 417 } 418 419 async unregisterSessionListener() { 420 if (this.session) { 421 this.session?.off('play'); 422 this.session?.off('pause'); 423 this.session?.off('stop'); 424 this.session?.off('playNext'); 425 this.session?.off('playPrevious'); 426 this.session?.off('seek'); 427 // 主动销毁已创建的session 428 this.session?.destroy((err) => { 429 if (err) { 430 Log.info(`Destroy BusinessError: code: ${err.code}, message: ${err.message}`); 431 } else { 432 Log.info('Destroy: SUCCESS '); 433 } 434 }); 435 } 436 } 437 438 updateVolume(value: number) { 439 Log.info('update volume: ' + this.playType + ', ' + value); 440 if (this.volume === value) { 441 Log.info('update volume: volume not change'); 442 return; 443 } 444 this.volume = value; 445 if (this.playType === 'cast' && this.castController) { 446 this.castController?.sendControlCommand({ 447 command: 'setVolume', 448 parameter: value, 449 }); 450 } 451 if (this.playType === 'local' && this.audioManager) { 452 Log.info('update local volume: ' + value); 453 this.audioManager.setVolume(audio.AudioVolumeType.MEDIA, value * this.localAudioRation); 454 } 455 } 456 457 async localPlayOrPause() { 458 Log.info('start local play or pause' + this.avPlayer?.state); 459 if (!this.avPlayer) { 460 Log.error('no avplayer'); 461 return; 462 } 463 if (this.avPlayer?.state === 'playing') { 464 Log.info('start pause'); 465 await this.avPlayer?.pause(); 466 await this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PAUSE); 467 promptAction.showToast({message: 'pause done'}); 468 } else if (this.avPlayer?.state === 'stopped') { 469 Log.info('start play from stopped'); 470 await this.avPlayer?.prepare(); 471 await this.avPlayer?.play(); 472 await this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 473 promptAction.showToast({message: 'play done'}); 474 } else { 475 Log.info('start play from stopped'); 476 await this.avPlayer?.play(); 477 await this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PLAY); 478 promptAction.showToast({message: 'play done'}); 479 Log.info('start play done'); 480 } 481 } 482 483 async remotePlayOrPause() { 484 Log.info('start remote play or pause' + this.playState); 485 if (!this.castController) { 486 Log.error('no castController found'); 487 return; 488 } 489 if (this.playState === avSession.PlaybackState.PLAYBACK_STATE_INITIAL 490 || this.playState === avSession.PlaybackState.PLAYBACK_STATE_PREPARE) { 491 Log.info('start'); 492 let queueItem: avSession.AVQueueItem = { 493 itemId: 0, 494 description: this.currentPlayInfo 495 }; 496 497 await this.castController?.start(queueItem); 498 this.playState = avSession.PlaybackState.PLAYBACK_STATE_PLAY; 499 } else if (this.playState === avSession.PlaybackState.PLAYBACK_STATE_PLAY) { 500 Log.info('pause'); 501 this.castController?.sendControlCommand({ 502 command: 'pause', 503 }) 504 this.playState = avSession.PlaybackState.PLAYBACK_STATE_PAUSE; 505 } else { 506 Log.info('play'); 507 this.castController?.sendControlCommand({ 508 command: 'play', 509 }) 510 this.playState = avSession.PlaybackState.PLAYBACK_STATE_PLAY; 511 } 512 } 513 514 async autoStartAll(needStart: boolean){ 515 Log.info('try auto start all'); 516 Log.info('create session'); 517 this.session = await avSession.createAVSession(getContext(), 'audiotestr', 'video'); 518 this.session?.setExtras({ 519 requireAbilityList: ['url-cast'], 520 }); 521 const params: avSession.AVPlaybackState = { 522 position: { 523 elapsedTime: 0, 524 updateTime: new Date().getTime() 525 }, 526 }; 527 Log.info('try SET SESSION PLAYSTATE'); 528 this.session?.setAVPlaybackState(params); 529 Log.info('create session res: ' + JSON.stringify(this.session)); 530 if (!this.session) { 531 Log.error('fail to create session'); 532 return; 533 } 534 Log.info('create controller: ' + this.session?.sessionId); 535 this.controller = await this.session?.getController(); 536 if (!this.controller) { 537 Log.error('fail to create controller'); 538 return; 539 } 540 Log.info('create controller done: ' + this.controller.sessionId); 541 542 Log.info('add outputDeviceChange listener'); 543 this.controller.on('outputDeviceChange', async (connectState: avSession.ConnectionState, 544 device: avSession.OutputDeviceInfo) => { 545 this.outputDeviceInfo = device; 546 promptAction.showToast({ message: 'output device changed: ' + connectState }); 547 if (connectState === avSession.ConnectionState.STATE_CONNECTING) { 548 Log.info('connecting'); 549 return; 550 } 551 const isPlaying = this.avPlayer && this.avPlayer?.state === 'playing'; 552 Log.info('outputDeviceChange res: ' + JSON.stringify(device) + '|' + connectState + ',' + isPlaying); 553 await this.processDeviceChange(connectState, device); 554 Log.info(`process Device Change done, ${this.playType}, ${!!this.castController}`); 555 if (this.playType === 'cast' && this.castController) { 556 Log.info('prepare remote audio info ' + ', ' + isPlaying); 557 const queueItem: avSession.AVQueueItem = { 558 itemId: this.currentIndex, 559 description: this.currentPlayInfo 560 }; 561 Log.info(`try prepare info, ${JSON.stringify(queueItem)}`); 562 563 await this.castController?.prepare(queueItem); 564 if (isPlaying) { 565 566 await this.castController?.start(queueItem); 567 } 568 await this.castController?.sendControlCommand({ 569 command: 'setLoopMode', 570 parameter: this.currentLoopMode, 571 }); 572 } 573 Log.info('output device change processing finished'); 574 }) 575 Log.info('add outputDeviceChange Listener done'); 576 Log.info('try prepare local audio: ' + this.session?.sessionId); 577 this.setListenerForMesFromController(); 578 await this.session?.activate(); 579 await this.setLocalMediaInfo(); 580 await this.setPlayState(avSession.PlaybackState.PLAYBACK_STATE_PAUSE); 581 if (needStart) { 582 Log.info('start play local'); 583 setTimeout(() => { 584 promptAction.showToast({ message: 'auto start done' }); 585 this.localPlayOrPause(); 586 }, 100); 587 } 588 wantAgent.getWantAgent({ 589 wants: [ 590 { 591 bundleName: 'com.samples.videoplayer', 592 abilityName: 'com.samples.videoplayer.EntryAbility' 593 } 594 ], 595 operationType: wantAgent.OperationType.START_ABILITIES, 596 requestCode: 0, 597 wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] 598 }).then((agent) => { 599 this.session?.setLaunchAbility(agent); 600 }) 601 } 602 603 async switchToPreviousByLoopMode(){ 604 Log.info('switch to previous by loop mode: ' + this.currentLoopMode); 605 if (this.currentLoopMode === avSession.LoopMode.LOOP_MODE_SINGLE) { 606 this.playInfoUpdated(); 607 return; 608 } 609 if (this.currentLoopMode === avSession.LoopMode.LOOP_MODE_SINGLE) { 610 const random = Math.floor((Math.random() * 100) + 1); 611 const target = random % this.songList.length; 612 if (target === this.currentIndex) { 613 this.currentIndex = target === 0 ? this.songList.length - 1 : target - 1; 614 } else { 615 this.currentIndex = target; 616 } 617 this.updateCurrentPlayInfo(this.songList[this.currentIndex], this.audioType); 618 return; 619 } 620 this.currentIndex = this.currentIndex === 0 ? this.songList.length - 1 : this.currentIndex - 1; 621 this.updateCurrentPlayInfo(this.songList[this.currentIndex], this.audioType); 622 } 623 624 async switchToNextByLoopMode(){ 625 Log.info('switch to next by loop mode: ' + this.currentLoopMode); 626 if (this.currentLoopMode === avSession.LoopMode.LOOP_MODE_SINGLE) { 627 this.playInfoUpdated(); 628 return; 629 } 630 if (this.currentLoopMode === avSession.LoopMode.LOOP_MODE_SHUFFLE) { 631 const random = Math.floor((Math.random() * 100) + 1); 632 const target = random % this.songList.length; 633 if (target === this.currentIndex) { 634 this.currentIndex = target === this.songList.length - 1 ? 0 : target + 1; 635 } else { 636 this.currentIndex = target; 637 } 638 this.updateCurrentPlayInfo(this.songList[this.currentIndex], this.audioType); 639 return; 640 } 641 this.currentIndex = this.currentIndex === this.songList.length - 1 ? 0 : this.currentIndex + 1; 642 this.updateCurrentPlayInfo(this.songList[this.currentIndex], this.audioType); 643 } 644 645 async updateCurrentPlayInfo(item: avSession.AVMediaDescription, audioType: string){ 646 const temp: avSession.AVMediaDescription = { 647 assetId: item.assetId, 648 title: item.title, 649 artist: item.artist, 650 mediaType: item.mediaType, 651 mediaSize: item.mediaSize, 652 startPosition: item.startPosition, 653 duration: item.duration, 654 mediaImage: item.mediaImage, 655 albumTitle: item.albumTitle, 656 appName: item.appName, 657 }; 658 if (audioType === 'scan') { 659 let fd = 0; 660 await fs.open(item.mediaUri).then(async (file) => { 661 Log.info('fs res: ' + file?.fd); 662 fd = file?.fd 663 if (fd != -1 && fd) { 664 Log.info('open fd suc: '+ fd); 665 temp.fdSrc = { 666 fd, 667 }; 668 } 669 }).catch((err: BusinessError) => { 670 Log.error('start local file cast: ' + JSON.stringify(err)); 671 }) 672 } else { 673 temp.mediaUri = item.mediaUri; 674 } 675 this.currentPlayInfo = temp; 676 } 677 678 async processDeviceChange(connectState: avSession.ConnectionState, device: avSession.OutputDeviceInfo){ 679 if (device?.devices?.[0].castCategory === 0 || connectState === avSession.ConnectionState.STATE_DISCONNECTED) { 680 this.playType = 'local'; 681 this.playState = avSession.PlaybackState.PLAYBACK_STATE_PAUSE; 682 if (this.audioVolumeGroupManager) { 683 const volume = await this.audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA); 684 this.volume = volume / this.localAudioRation; 685 } 686 await this.setLocalMediaInfo(); 687 return; 688 } 689 this.playType = 'cast'; 690 const isRefresh = !!this.castController; 691 this.castController = await this.session?.getAVCastController(); 692 if (!this.castController) { 693 Log.error('fail to get cast controller'); 694 return; 695 } 696 let avPlaybackState = await this.castController?.getAVPlaybackState(); 697 this.playState = avPlaybackState.state || 0; 698 if (typeof avPlaybackState?.volume !== 'undefined' && avPlaybackState?.volume >= 0) { 699 this.volume = avPlaybackState?.volume; 700 } 701 if (typeof avPlaybackState?.loopMode !== 'undefined') { 702 this.currentLoopMode = avPlaybackState?.loopMode; 703 } 704 Log.info('get AVPlaybackState res: ' + JSON.stringify(avPlaybackState) + ', ' + isRefresh); 705 if (this.avPlayer && this.avPlayer?.state === 'playing') { 706 Log.info('stop avplayer'); 707 this.avPlayer?.stop(); 708 } 709 Log.info('set on playbackStateChange listener: ' + connectState); 710 this.castController?.on('playbackStateChange', 'all', (state) => { 711 Log.info('play state change: ' + JSON.stringify(state)); 712 if (typeof state?.state !== 'undefined') { 713 this.playState = state?.state; 714 } 715 if (typeof state?.volume !== 'undefined') { 716 this.volume = state?.volume; 717 } 718 if (typeof state?.loopMode !== 'undefined') { 719 this.currentLoopMode = state?.loopMode; 720 } 721 if (typeof state?.extras?.duration !== 'undefined') { 722 this.duration = state?.extras?.duration as number; 723 } 724 if (typeof state?.position?.elapsedTime !== 'undefined' && !this.isProgressSliding) { 725 this.seedPosition = (state?.position?.elapsedTime / this.duration) * 100; 726 } 727 }); 728 this.castController?.on('playPrevious', async (state) => { 729 Log.info('playPrevious: ' + JSON.stringify(state)); 730 this.switchToPreviousByLoopMode() 731 }); 732 this.castController?.on('playNext', async (state) => { 733 Log.info('playNext: ' + JSON.stringify(state)); 734 this.switchToNextByLoopMode() 735 }); 736 this.castController?.on('error', (err) => { 737 Log.info('on command error: ' + JSON.stringify(err)); 738 promptAction.showToast({ message: 'error: ' + JSON.stringify(err) }); 739 }); 740 Log.info('set on playbackStateChange listener done') 741 } 742 743 build() { 744 Column() { 745 Flex({ 746 direction: FlexDirection.Column, 747 justifyContent: FlexAlign.SpaceBetween, 748 alignItems: ItemAlign.Center 749 }) { 750 // title 751 Column() { 752 Flex({ 753 direction: FlexDirection.Row, 754 justifyContent: FlexAlign.SpaceBetween, 755 alignItems: ItemAlign.Center 756 }) { 757 Column() { 758 Text($r('app.string.EntryAbility_title')) 759 .fontWeight(FontWeight.Normal) 760 .fontSize(24) 761 .textAlign(TextAlign.Start) 762 .width("100%") 763 .fontColor(Color.White) 764 } 765 .width('70%') 766 .height(24) 767 768 Button() { 769 if (deviceInfo.productModel === 'ohos') { 770 Image($r('app.media.ohos_ic_public_cast_stream')) 771 .size({ width: '100%', height: '100%' }) 772 .fillColor(Color.White) 773 .backgroundColor(Color.Black) 774 } else { 775 AVCastPicker({ normalColor: this.avCastPickerColor, activeColor: this.avCastPickerColor }) 776 .size({ height: '100%', width: '100%' }) 777 .backgroundColor(Color.Black) 778 .align(Alignment.Center); 779 } 780 } 781 .width(24) 782 .height(24) 783 .backgroundColor(Color.Black) 784 } 785 .margin({ left: 24, right: 24, top: 12 }) 786 } 787 .width('100%') 788 .backgroundColor(Color.Transparent) 789 790 // video 791 if (this.playType === 'local') { 792 Row() { 793 Stack({ alignContent: Alignment.Bottom }) { 794 XComponent({ id: '', type: 'surface', controller: this.mXComponentController }) 795 .onLoad(() => { 796 const surfaceId = this.mXComponentController.getXComponentSurfaceId(); 797 Log.info('XComponent onLoad, surfaceId = ' + surfaceId); 798 this.audioUtils.surfaceId = surfaceId; 799 }) 800 } 801 .width('100%') 802 .height(200) 803 } 804 .flexShrink(0) 805 .width('100%') 806 } else { 807 Row() { 808 Stack({ alignContent: Alignment.Center }) { 809 Text($r('app.string.EntryAbility_sink')) 810 .fontColor(Color.White) 811 .fontSize(28) 812 } 813 .width('100%') 814 .height(200) 815 .backgroundColor(Color.Grey) 816 } 817 .flexShrink(0) 818 .width('100%') 819 } 820 821 // control 822 Row() { 823 Flex({ 824 direction: FlexDirection.Column, 825 justifyContent: FlexAlign.SpaceAround, 826 alignItems: ItemAlign.Center 827 }){ 828 Flex({ 829 direction: FlexDirection.Row, 830 justifyContent: FlexAlign.SpaceEvenly, 831 alignItems: ItemAlign.Center 832 }) 833 { 834 Button() { 835 Image($r('app.media.music_last')) 836 .size({ width: '24vp', height: '24vp' }) 837 .fillColor(Color.White) 838 .backgroundColor(Color.White) 839 } 840 .size({ 841 width: '48vp', 842 height: '48vp' 843 }) 844 .backgroundColor(Color.Black) 845 .onClick(() => { 846 Log.info('click play next'); 847 this.switchToPreviousByLoopMode(); 848 }) 849 .key('music_last') 850 851 Button() { 852 Image(this.playState === 2 ? $r('app.media.music_stop') : $r('app.media.music_play')) 853 .size({ width: '24vp', height: '24vp' }) 854 .fillColor($r('sys.color.ohos_id_color_primary')) 855 .backgroundColor(Color.White) 856 } 857 .size({ 858 width: '48vp', 859 height: '48vp' 860 }) 861 .backgroundColor(Color.Transparent) 862 .onClick(() => { 863 Log.info(`click play/pause: ${this.playType} ,${this.session}, ${this.controller}`); 864 if (!this.session && !this.controller) { 865 this.autoStartAll(true); 866 } else if (this.playType === 'local') { 867 this.localPlayOrPause(); 868 } else { 869 this.remotePlayOrPause(); 870 } 871 }) 872 .key('music_play_or_pause') 873 874 Button() { 875 Image($r('app.media.music_next')) 876 .size({ width: '24vp', height: '24vp' }) 877 .fillColor($r('sys.color.ohos_id_color_primary')) 878 .backgroundColor(Color.White) 879 } 880 .size({ 881 width: '48vp', 882 height: '48vp' 883 }) 884 .backgroundColor(Color.Transparent) 885 .onClick(() => { 886 Log.info('click play next'); 887 this.switchToNextByLoopMode(); 888 }) 889 .key('music_next') 890 } 891 892 Flex({ 893 direction: FlexDirection.Row, 894 justifyContent: FlexAlign.SpaceEvenly, 895 alignItems: ItemAlign.Center 896 }) 897 { 898 Text(`${CommonUtils.millSecond2Minutes(this.seedPosition / 100 * this.duration)}`) 899 .fontWeight(FontWeight.Normal) 900 .fontSize(12) 901 .textAlign(TextAlign.Start) 902 .fontColor('rgba(255,255,255,0.9)') 903 904 Slider({ 905 value: this.seedPosition, 906 min: 0, 907 max: 100, 908 style: SliderStyle.OutSet 909 }) 910 .trackThickness(4) 911 .blockColor('rgba(255,255,255,1)') 912 .trackColor('rgba(255,255,255,0.3)') 913 .selectedColor('rgba(255,255,255,0.9)') 914 .showSteps(false) 915 .showTips(false) 916 .onChange((value: number, mode: SliderChangeMode) => { 917 Log.info('value: ' + value + 'mode: ' + mode.toString() ) 918 if (mode === SliderChangeMode.End) { 919 if (this.playType === 'local') { 920 this.avPlayer?.seek(value / 100 * this.duration); 921 const params: avSession.AVPlaybackState = { 922 position: { 923 elapsedTime: Math.floor(value / 100 * this.duration), 924 updateTime: new Date().getTime(), 925 }, 926 }; 927 this.session?.setAVPlaybackState(params); 928 Log.info('params.position + ' + JSON.stringify((params.position))) 929 } else { 930 this.castController?.sendControlCommand({ 931 command: 'seek', 932 parameter: value / 100 * this.duration, 933 }); 934 } 935 } 936 this.seedPosition = value; 937 }) 938 .width('70%') 939 .opacity(1) 940 .onTouch((event: TouchEvent) => { 941 Log.info('progress touch: ' + event.type) 942 if (event.type === TouchType.Up) { 943 this.sliderTimer = setTimeout(() => { 944 this.isProgressSliding = false; 945 }, 200); 946 } else { 947 clearTimeout(this.sliderTimer); 948 this.isProgressSliding = true; 949 } 950 }) 951 Text(`${CommonUtils.millSecond2Minutes(this.duration)}`) 952 .fontWeight(FontWeight.Normal) 953 .fontSize(12) 954 .textAlign(TextAlign.Start) 955 .fontColor('rgba(255,255,255,0.9)') 956 } 957 .width('100%') 958 .height(50) 959 .padding({ left: 10, right: 10 }) 960 } 961 .padding({ top: 8 }) 962 } 963 .width('100%') 964 .height(150) 965 .padding({ bottom: 20 }) 966 } 967 } 968 .width('100%') 969 .height('100%') 970 .backgroundColor(Color.Black) 971 } 972} 973 974 975 976