• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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