|
@@ -0,0 +1,1568 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 独立播放器服务
|
|
|
|
|
+ * 在EntryAbility中直接控制音频播放,不依赖LocalMusic
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
|
|
|
+import { emitter } from '@kit.BasicServicesKit';
|
|
|
|
|
+import { common } from '@kit.AbilityKit';
|
|
|
|
|
+import { fileIo } from '@kit.CoreFileKit';
|
|
|
|
|
+import commonEventManager from '@ohos.commonEventManager';
|
|
|
|
|
+import { PLAYER_STATE_CHANGED_EVENT, PLAYER_SONG_CHANGED_EVENT } from '../widget/WidgetEventConstants';
|
|
|
|
|
+import { VideoItem } from '../../viewmodel/VideoItem';
|
|
|
|
|
+import { AppUtil, PreferencesUtil } from '@pura/harmony-utils';
|
|
|
|
|
+import {
|
|
|
|
|
+ IjkMediaPlayer,
|
|
|
|
|
+ InterruptEvent,
|
|
|
|
|
+ InterruptHintType,
|
|
|
|
|
+ OnPreparedListener,
|
|
|
|
|
+ OnCompletionListener,
|
|
|
|
|
+ OnErrorListener,
|
|
|
|
|
+ LogUtils
|
|
|
|
|
+} from '@ohos/ijkplayer';
|
|
|
|
|
+import { WidgetData, PlayState as WidgetPlayState, SongInfo, PlayProgress, PlaylistState, WidgetConfig } from '../widget/WidgetTypes';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 播放状态数据接口
|
|
|
|
|
+ */
|
|
|
|
|
+interface SavedPlaybackState {
|
|
|
|
|
+ isPlaying: boolean;
|
|
|
|
|
+ isPaused: boolean;
|
|
|
|
|
+ currentPosition: number;
|
|
|
|
|
+ duration: number;
|
|
|
|
|
+ timestamp: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const TAG = 'IndependentPlayerService';
|
|
|
|
|
+
|
|
|
|
|
+// 为了兼容性,定义一些本地使用的接口
|
|
|
|
|
+interface PlayState {
|
|
|
|
|
+ isPlaying: boolean;
|
|
|
|
|
+ isPaused: boolean;
|
|
|
|
|
+ isLoading: boolean;
|
|
|
|
|
+ currentPosition: number;
|
|
|
|
|
+ duration: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SongData {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ artist: string;
|
|
|
|
|
+ album: string;
|
|
|
|
|
+ filePath: string;
|
|
|
|
|
+ duration: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ProgressData {
|
|
|
|
|
+ currentPosition: number;
|
|
|
|
|
+ duration: number;
|
|
|
|
|
+ percentage: number;
|
|
|
|
|
+ currentTimeText: string;
|
|
|
|
|
+ totalTimeText: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PlaylistInfo {
|
|
|
|
|
+ hasNext: boolean;
|
|
|
|
|
+ hasPrevious: boolean;
|
|
|
|
|
+ currentIndex: number;
|
|
|
|
|
+ totalCount: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PlayerStateData {
|
|
|
|
|
+ playState: PlayState;
|
|
|
|
|
+ currentSong: SongData;
|
|
|
|
|
+ progress: ProgressData;
|
|
|
|
|
+ playlist: PlaylistInfo;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface SongChangeData {
|
|
|
|
|
+ currentSong: SongData;
|
|
|
|
|
+ playlist: PlaylistInfo;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PlayerCurrentState {
|
|
|
|
|
+ playState: PlayState;
|
|
|
|
|
+ currentSong: VideoItem | null;
|
|
|
|
|
+ playlist: VideoItem[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DirectFormUpdateServiceInstance {
|
|
|
|
|
+ updateAllForms(data: WidgetData): Promise<void>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DirectFormUpdateServiceClass {
|
|
|
|
|
+ getInstance(): DirectFormUpdateServiceInstance;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface DirectFormUpdateServiceModule {
|
|
|
|
|
+ DirectFormUpdateService: DirectFormUpdateServiceClass;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 独立播放器服务
|
|
|
|
|
+ * 提供基本的音频播放控制功能,独立于LocalMusic运行
|
|
|
|
|
+ * 直接从PreferencesUtil获取播放列表数据,与LocalMusic保持一致
|
|
|
|
|
+ */
|
|
|
|
|
+export class IndependentPlayerService {
|
|
|
|
|
+ private static instance: IndependentPlayerService;
|
|
|
|
|
+ private ijkPlayer: IjkMediaPlayer | null = null;
|
|
|
|
|
+ private currentSong: VideoItem | null = null;
|
|
|
|
|
+ private playState: PlayState = {
|
|
|
|
|
+ isPlaying: false,
|
|
|
|
|
+ isPaused: false, // 初始状态设为未暂停,避免误判为可恢复播放
|
|
|
|
|
+ isLoading: false,
|
|
|
|
|
+ currentPosition: 0,
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ };
|
|
|
|
|
+ private playlist: VideoItem[] = [];
|
|
|
|
|
+ private currentIndex: number = 0;
|
|
|
|
|
+ private isInitialized: boolean = false;
|
|
|
|
|
+ private context: common.UIAbilityContext | null = null;
|
|
|
|
|
+ private retryCount: number = 0;
|
|
|
|
|
+ private maxRetries: number = 3;
|
|
|
|
|
+ private updateProgressTimer: number = 0; // 进度更新定时器
|
|
|
|
|
+ private lastProgressBroadcastTime: number = 0; // 上次进度广播时间
|
|
|
|
|
+ private readonly PROGRESS_BROADCAST_INTERVAL: number = 1000; // 进度广播间隔(1秒)
|
|
|
|
|
+ private lastStateBroadcastTime: number = 0; // 上次状态广播时间
|
|
|
|
|
+ private readonly STATE_BROADCAST_INTERVAL: number = 500; // 状态广播间隔(0.5秒)
|
|
|
|
|
+ savedDuration: number = 0; // 保存的歌曲时长
|
|
|
|
|
+
|
|
|
|
|
+ private constructor() {}
|
|
|
|
|
+
|
|
|
|
|
+ static getInstance(): IndependentPlayerService {
|
|
|
|
|
+ if (!IndependentPlayerService.instance) {
|
|
|
|
|
+ IndependentPlayerService.instance = new IndependentPlayerService();
|
|
|
|
|
+ }
|
|
|
|
|
+ return IndependentPlayerService.instance;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 初始化服务
|
|
|
|
|
+ */
|
|
|
|
|
+ async initialize(context: common.UIAbilityContext): Promise<void> {
|
|
|
|
|
+ if (this.isInitialized) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Service already initialized');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.context = context;
|
|
|
|
|
+ AppUtil.init(this.context);
|
|
|
|
|
+ // 直接从PreferencesUtil获取播放列表数据,与LocalMusic保持一致
|
|
|
|
|
+ this.loadFromAppStorage();
|
|
|
|
|
+
|
|
|
|
|
+ // 创建音频播放器
|
|
|
|
|
+ await this.createAudioPlayer();
|
|
|
|
|
+
|
|
|
|
|
+ // 注册卡片控制事件监听器
|
|
|
|
|
+ this.registerWidgetControlListener();
|
|
|
|
|
+
|
|
|
|
|
+ // 注册数据变化监听器,实时同步PreferencesUtil数据
|
|
|
|
|
+ this.registerAppStorageListener();
|
|
|
|
|
+
|
|
|
|
|
+ this.isInitialized = true;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `✅ IndependentPlayerService initialized with ${this.playlist.length} songs`);
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟广播初始状态
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `❌ Failed to initialize IndependentPlayerService: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建音频播放器
|
|
|
|
|
+ */
|
|
|
|
|
+ private async createAudioPlayer(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Creating IjkMediaPlayer instance');
|
|
|
|
|
+
|
|
|
|
|
+ this.ijkPlayer = IjkMediaPlayer.getInstance();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkPlayer audioId set to: independentPlayer');
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频中断处理
|
|
|
|
|
+ this.setupAudioInterruptHandler();
|
|
|
|
|
+
|
|
|
|
|
+ // 设置播放器回调
|
|
|
|
|
+ this.setupPlayerCallbacks();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkMediaPlayer created and configured successfully');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to create IjkMediaPlayer: ${error}`);
|
|
|
|
|
+ throw new Error(`Failed to create IjkMediaPlayer: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 配置IjkPlayer选项(完全参考LocalMusic的配置)
|
|
|
|
|
+ */
|
|
|
|
|
+ private setupIjkPlayerOptions(): void {
|
|
|
|
|
+ if (!this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置调试模式
|
|
|
|
|
+ this.ijkPlayer.setDebug(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化配置
|
|
|
|
|
+ this.ijkPlayer.native_setup();
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音量(纯音频播放)
|
|
|
|
|
+ this.ijkPlayer.setVolume('1.0', '1.0');
|
|
|
|
|
+
|
|
|
|
|
+ // 使用精确寻帧
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", "1");
|
|
|
|
|
+
|
|
|
|
|
+ // 动态缓冲区大小配置(参考LocalMusic)
|
|
|
|
|
+ let bufferSize = 1 * 1024 * 1024; // 默认1MB缓冲区
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", bufferSize.toString());
|
|
|
|
|
+
|
|
|
|
|
+ // 重连模式
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", "1");
|
|
|
|
|
+
|
|
|
|
|
+ // 缓冲帧数设置
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100");
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-frames", "1000");
|
|
|
|
|
+
|
|
|
|
|
+ // 启动预加载
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1");
|
|
|
|
|
+
|
|
|
|
|
+ // 设置无缓冲
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0");
|
|
|
|
|
+
|
|
|
|
|
+ // 跳帧处理
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", "5");
|
|
|
|
|
+
|
|
|
|
|
+ // 最大缓冲cache
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "3000");
|
|
|
|
|
+
|
|
|
|
|
+ // 无限制收流
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1");
|
|
|
|
|
+ this.ijkPlayer.setOptionLong(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1");
|
|
|
|
|
+
|
|
|
|
|
+ // 屏幕常亮(音频播放也保持)
|
|
|
|
|
+ this.ijkPlayer.setScreenOnWhilePlaying(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置超时
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "10000000");
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "10000000");
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "addrinfo_timeout", "10000000");
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "10000000");
|
|
|
|
|
+
|
|
|
|
|
+ // 变速播放支持
|
|
|
|
|
+ this.ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", "1");
|
|
|
|
|
+ this.ijkPlayer.setSpeed('1.0f');
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkPlayer options configured with LocalMusic settings');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置音频中断处理
|
|
|
|
|
+ */
|
|
|
|
|
+ private setupAudioInterruptHandler(): void {
|
|
|
|
|
+ if (!this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ const audioInterruptCallback = (event: InterruptEvent) => {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Audio interrupt event: ${JSON.stringify(event)}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (event.hintType === InterruptHintType.INTERRUPT_HINT_PAUSE) {
|
|
|
|
|
+ this.pausePlayback();
|
|
|
|
|
+ } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_RESUME) {
|
|
|
|
|
+ // 可选择是否自动恢复播放
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Audio interrupt resumed, manual resume required');
|
|
|
|
|
+ } else if (event.hintType === InterruptHintType.INTERRUPT_HINT_STOP) {
|
|
|
|
|
+ this.stopPlayback();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.ijkPlayer.on('audioInterrupt', audioInterruptCallback);
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Audio interrupt handler set');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置播放器回调
|
|
|
|
|
+ */
|
|
|
|
|
+ private setupPlayerCallbacks(): void {
|
|
|
|
|
+ if (!this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 准备完成回调
|
|
|
|
|
+ const onPreparedListener: OnPreparedListener = {
|
|
|
|
|
+ onPrepared: () => {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkPlayer prepared successfully');
|
|
|
|
|
+
|
|
|
|
|
+ // 现在可以安全地开始播放
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.ijkPlayer?.start();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback started after onPrepared');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to start playback in onPrepared: ${error}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.duration = this.ijkPlayer?.getDuration() || 0;
|
|
|
|
|
+ this.playState.isPlaying = true;
|
|
|
|
|
+ this.playState.isPaused = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存播放状态
|
|
|
|
|
+ this.savePlaybackState();
|
|
|
|
|
+
|
|
|
|
|
+ // 立即广播状态更新
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+
|
|
|
|
|
+ // 启动进度更新定时器
|
|
|
|
|
+ this.startProgressTask();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Playback prepared and started for ${this.currentSong?.name}, duration: ${this.playState.duration}ms`);
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟恢复播放位置
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.restorePlaybackPosition();
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ this.ijkPlayer.setOnPreparedListener(onPreparedListener);
|
|
|
|
|
+
|
|
|
|
|
+ // 播放完成回调
|
|
|
|
|
+ const onCompletionListener: OnCompletionListener = {
|
|
|
|
|
+ onCompletion: () => {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback completed');
|
|
|
|
|
+ this.handlePlaybackCompletion();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ this.ijkPlayer.setOnCompletionListener(onCompletionListener);
|
|
|
|
|
+
|
|
|
|
|
+ // 错误回调
|
|
|
|
|
+ const onErrorListener: OnErrorListener = {
|
|
|
|
|
+ onError: (what: number, extra: number) => {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `IjkPlayer error: what=${what}, extra=${extra}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试重试播放
|
|
|
|
|
+ if (this.retryCount < this.maxRetries) {
|
|
|
|
|
+ this.retryCount++;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Retrying playback, attempt ${this.retryCount}/${this.maxRetries}`);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.play();
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Max retries exceeded, giving up`);
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true; // 表示错误已处理
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ this.ijkPlayer.setOnErrorListener(onErrorListener);
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Player callbacks configured');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从PreferencesUtil加载播放列表数据,保持与LocalMusic一致
|
|
|
|
|
+ */
|
|
|
|
|
+ private loadFromAppStorage(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 直接从PreferencesUtil获取数据,与LocalMusic保持一致
|
|
|
|
|
+ this.playlist = PreferencesUtil.getSync('LastMusicList', []) as Array<VideoItem>;
|
|
|
|
|
+ this.currentSong = PreferencesUtil.getSync('LastMusicInfo', undefined) as VideoItem;
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Loaded ${this.playlist.length} songs from PreferencesUtil`);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有播放列表但没有当前歌曲,使用第一首歌
|
|
|
|
|
+ if (this.playlist.length > 0 && !this.currentSong) {
|
|
|
|
|
+ this.currentSong = this.playlist[0];
|
|
|
|
|
+ this.currentIndex = 0;
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Using first song as current song');
|
|
|
|
|
+ } else if (this.currentSong && this.playlist.length > 0) {
|
|
|
|
|
+ // 在播放列表中查找当前歌曲的索引
|
|
|
|
|
+ const foundIndex = this.playlist.findIndex(song =>
|
|
|
|
|
+ song.id === this.currentSong!.id ||
|
|
|
|
|
+ song.filePath === this.currentSong!.filePath ||
|
|
|
|
|
+ song.name === this.currentSong!.name
|
|
|
|
|
+ );
|
|
|
|
|
+ this.currentIndex = foundIndex >= 0 ? foundIndex : 0;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Found current song index: ${this.currentIndex}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有播放列表或当前歌曲
|
|
|
|
|
+ this.currentIndex = 0;
|
|
|
|
|
+ this.currentSong = null;
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'No playlist or current song found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复播放状态(包括duration等信息)
|
|
|
|
|
+ if (this.currentSong) {
|
|
|
|
|
+ this.restorePlaybackState();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 同步到AppStorage以便其他组件访问
|
|
|
|
|
+ AppStorage.setOrCreate('songList', this.playlist);
|
|
|
|
|
+ AppStorage.setOrCreate('currIndex', this.currentIndex);
|
|
|
|
|
+ AppStorage.setOrCreate('currentSong', this.currentSong);
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Current index: ${this.currentIndex}, current song: ${this.currentSong?.name || 'none'}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to load from PreferencesUtil: ${error}`);
|
|
|
|
|
+ this.playlist = [];
|
|
|
|
|
+ this.currentIndex = 0;
|
|
|
|
|
+ this.currentSong = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置默认值到AppStorage
|
|
|
|
|
+ AppStorage.setOrCreate('songList', []);
|
|
|
|
|
+ AppStorage.setOrCreate('currIndex', 0);
|
|
|
|
|
+ AppStorage.setOrCreate('currentSong', null);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注册数据变化监听器(主要监听PreferencesUtil数据变化)
|
|
|
|
|
+ */
|
|
|
|
|
+ private registerAppStorageListener(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 设置初始值到AppStorage
|
|
|
|
|
+ AppStorage.setOrCreate('songList', this.playlist);
|
|
|
|
|
+ AppStorage.setOrCreate('currIndex', this.currentIndex);
|
|
|
|
|
+
|
|
|
|
|
+ // 定期检查PreferencesUtil中的数据变化
|
|
|
|
|
+ setInterval(() => {
|
|
|
|
|
+ this.checkPreferencesChanges();
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'PreferencesUtil data listeners registered successfully');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to register data listeners: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查PreferencesUtil中的数据变化
|
|
|
|
|
+ */
|
|
|
|
|
+ private checkPreferencesChanges(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查播放列表变化
|
|
|
|
|
+ const newPlaylist = PreferencesUtil.getSync('LastMusicList', []) as Array<VideoItem>;
|
|
|
|
|
+ const newCurrentSong = PreferencesUtil.getSync('LastMusicInfo', undefined) as VideoItem;
|
|
|
|
|
+
|
|
|
|
|
+ if (JSON.stringify(newPlaylist) !== JSON.stringify(this.playlist)) {
|
|
|
|
|
+ const oldLength = this.playlist.length;
|
|
|
|
|
+ this.playlist = newPlaylist;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Playlist updated from PreferencesUtil: ${oldLength} -> ${this.playlist.length} songs`);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前索引超出范围,重置为0
|
|
|
|
|
+ if (this.currentIndex >= this.playlist.length) {
|
|
|
|
|
+ this.currentIndex = 0;
|
|
|
|
|
+ this.currentSong = this.playlist.length > 0 ? this.playlist[0] : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 同步到AppStorage
|
|
|
|
|
+ AppStorage.setOrCreate('songList', this.playlist);
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查当前歌曲变化
|
|
|
|
|
+ if (newCurrentSong && JSON.stringify(newCurrentSong) !== JSON.stringify(this.currentSong)) {
|
|
|
|
|
+ this.currentSong = newCurrentSong;
|
|
|
|
|
+
|
|
|
|
|
+ // 在播放列表中查找新歌曲的索引
|
|
|
|
|
+ if (this.playlist.length > 0) {
|
|
|
|
|
+ const foundIndex = this.playlist.findIndex(song =>
|
|
|
|
|
+ song.id === newCurrentSong.id ||
|
|
|
|
|
+ song.filePath === newCurrentSong.filePath ||
|
|
|
|
|
+ song.name === newCurrentSong.name
|
|
|
|
|
+ );
|
|
|
|
|
+ if (foundIndex >= 0) {
|
|
|
|
|
+ this.currentIndex = foundIndex;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Current song updated from PreferencesUtil: ${this.currentIndex}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 同步到AppStorage
|
|
|
|
|
+ AppStorage.setOrCreate('currentSong', this.currentSong);
|
|
|
|
|
+ AppStorage.setOrCreate('currIndex', this.currentIndex);
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to check PreferencesUtil changes: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 刷新播放列表数据(从PreferencesUtil重新加载)
|
|
|
|
|
+ */
|
|
|
|
|
+ private refreshPlaylistData(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const oldPlaylistLength = this.playlist.length;
|
|
|
|
|
+ const oldCurrentIndex = this.currentIndex;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前播放列表为空,强制重新加载
|
|
|
|
|
+ if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playlist is empty, forcing reload from storage...');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.loadFromAppStorage();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果数据有变化,广播更新
|
|
|
|
|
+ if (this.playlist.length !== oldPlaylistLength || this.currentIndex !== oldCurrentIndex) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Playlist data refreshed: ${oldPlaylistLength} -> ${this.playlist.length} songs, index: ${oldCurrentIndex} -> ${this.currentIndex}`);
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ } else if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'Still no songs found after refresh');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to refresh playlist data: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注册卡片控制事件监听器
|
|
|
|
|
+ */
|
|
|
|
|
+ private registerWidgetControlListener(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const innerEvent: emitter.InnerEvent = { eventId: 9001 };
|
|
|
|
|
+ emitter.on(innerEvent, (eventData: emitter.EventData) => {
|
|
|
|
|
+ this.handleWidgetControlEvent(eventData);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Widget control listener registered');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to register widget control listener: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理卡片控制事件
|
|
|
|
|
+ */
|
|
|
|
|
+ private handleWidgetControlEvent(eventData: emitter.EventData): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = eventData.data as Record<string, Object>;
|
|
|
|
|
+ const command = data['command'] as string;
|
|
|
|
|
+ const source = data['source'] as string;
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `🎵 IndependentPlayer received command: ${command} from ${source}`);
|
|
|
|
|
+
|
|
|
|
|
+ switch (command) {
|
|
|
|
|
+ case 'PLAY_PAUSE':
|
|
|
|
|
+ this.togglePlayPause();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'NEXT_SONG':
|
|
|
|
|
+ this.playNext();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'PREV_SONG':
|
|
|
|
|
+ this.playPrevious();
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `Unknown command: ${command}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to handle widget control event: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 切换播放/暂停
|
|
|
|
|
+ */
|
|
|
|
|
+ async togglePlayPause(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, 'IjkPlayer not initialized');
|
|
|
|
|
+ // 尝试重新初始化
|
|
|
|
|
+ if (this.context) {
|
|
|
|
|
+ await this.initialize(this.context);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有播放列表,尝试刷新数据
|
|
|
|
|
+ if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'No playlist found, attempting to refresh data...');
|
|
|
|
|
+ this.refreshPlaylistData();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果刷新后仍然没有数据,返回
|
|
|
|
|
+ if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'No songs available after refresh');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 使用内部状态来判断,而不是完全依赖ijkPlayer.isPlaying()
|
|
|
|
|
+ const shouldPause = this.playState.isPlaying;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Toggle action: shouldPause=${shouldPause}, current state - isPlaying=${this.playState.isPlaying}, isPaused=${this.playState.isPaused}, ijkPlayer.isPlaying=${this.ijkPlayer.isPlaying()}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldPause) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Pausing playback');
|
|
|
|
|
+ await this.pause();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Starting/Resuming playback');
|
|
|
|
|
+ await this.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to toggle play/pause: ${error}`);
|
|
|
|
|
+ // 如果出错,尝试重置播放器
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.resetPlayer();
|
|
|
|
|
+ } catch (resetError) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to reset player after toggle error: ${resetError}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复播放状态(包括duration等信息)
|
|
|
|
|
+ */
|
|
|
|
|
+ private restorePlaybackState(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentSong) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复duration信息
|
|
|
|
|
+ const savedDuration = AppStorage.get<number>(`duration_${this.currentSong.filePath}`) || 0;
|
|
|
|
|
+ if (savedDuration > 0) {
|
|
|
|
|
+ this.playState.duration = savedDuration;
|
|
|
|
|
+ this.savedDuration = savedDuration; // 确保savedDuration也被设置
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Restored duration: ${savedDuration}ms for ${this.currentSong.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复播放状态
|
|
|
|
|
+ const stateStr = AppStorage.get<string>(`state_${this.currentSong.filePath}`) || '';
|
|
|
|
|
+ if (stateStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const stateData: SavedPlaybackState = JSON.parse(stateStr) as SavedPlaybackState;
|
|
|
|
|
+ // 检查状态是否太旧(超过24小时不恢复)
|
|
|
|
|
+ if (stateData && Date.now() - stateData.timestamp < 24 * 60 * 60 * 1000) {
|
|
|
|
|
+ // 恢复暂停状态,但不自动开始播放
|
|
|
|
|
+ this.playState.isPaused = stateData.isPaused;
|
|
|
|
|
+ this.playState.isPlaying = false; // 启动时总是设为未播放
|
|
|
|
|
+ this.playState.currentPosition = stateData.currentPosition || 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果从单独的duration存储中没有获取到duration,尝试从状态数据中获取
|
|
|
|
|
+ if (savedDuration === 0 && stateData.duration > 0) {
|
|
|
|
|
+ this.playState.duration = stateData.duration;
|
|
|
|
|
+ this.savedDuration = stateData.duration;
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Used duration from state data: ${stateData.duration}ms`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Restored playback state: isPaused=${this.playState.isPaused}, position=${this.playState.currentPosition}, duration=${this.savedDuration}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (parseError) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to parse saved state: ${parseError}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有保存的状态,设置默认值
|
|
|
|
|
+ if (!stateStr) {
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true; // 默认为暂停状态
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.currentPosition = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Final restored state for ${this.currentSong.name}: isPlaying=${this.playState.isPlaying}, isPaused=${this.playState.isPaused}, duration=${this.savedDuration}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to restore playback state: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存播放状态
|
|
|
|
|
+ */
|
|
|
|
|
+ private savePlaybackState(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentSong) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存duration信息
|
|
|
|
|
+ if (this.playState.duration > 0) {
|
|
|
|
|
+ AppStorage.setOrCreate(`duration_${this.currentSong.filePath}`, this.playState.duration);
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Saved duration: ${this.playState.duration}ms for ${this.currentSong.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存播放状态
|
|
|
|
|
+ const stateData: SavedPlaybackState = {
|
|
|
|
|
+ isPlaying: this.playState.isPlaying,
|
|
|
|
|
+ isPaused: this.playState.isPaused,
|
|
|
|
|
+ currentPosition: this.playState.currentPosition || 0,
|
|
|
|
|
+ duration: this.playState.duration || 0,
|
|
|
|
|
+ timestamp: Date.now()
|
|
|
|
|
+ };
|
|
|
|
|
+ AppStorage.setOrCreate(`state_${this.currentSong.filePath}`, JSON.stringify(stateData));
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Saved playback state: isPlaying=${this.playState.isPlaying}, isPaused=${this.playState.isPaused}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to save playback state: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 播放
|
|
|
|
|
+ */
|
|
|
|
|
+ async play(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, 'IjkPlayer not initialized');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 刷新播放列表数据
|
|
|
|
|
+ this.refreshPlaylistData();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有当前歌曲,尝试播放第一首
|
|
|
|
|
+ if (!this.currentSong && this.playlist.length > 0) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'No current song, playing first song');
|
|
|
|
|
+ await this.playSongAtIndex(0);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.currentSong) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'No song to play');
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果已经在播放,无需操作
|
|
|
|
|
+ if (this.ijkPlayer.isPlaying()) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Already playing');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否可以恢复播放(优先使用保存的duration)
|
|
|
|
|
+ const ijkDuration = this.ijkPlayer.getDuration();
|
|
|
|
|
+ const savedDuration = this.playState.duration;
|
|
|
|
|
+ const effectiveDuration = savedDuration > 0 ? savedDuration : ijkDuration;
|
|
|
|
|
+
|
|
|
|
|
+ const canResume = this.playState.isPaused &&
|
|
|
|
|
+ !this.playState.isLoading &&
|
|
|
|
|
+ effectiveDuration > 0 && // 使用有效的duration
|
|
|
|
|
+ !this.ijkPlayer.isPlaying(); // 确保确实没在播放
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Play state check: isPaused=${this.playState.isPaused}, isLoading=${this.playState.isLoading}, ijkDuration=${ijkDuration}, savedDuration=${savedDuration}, effectiveDuration=${effectiveDuration}, canResume=${canResume}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (canResume) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.ijkPlayer.start();
|
|
|
|
|
+ this.playState.isPlaying = true;
|
|
|
|
|
+ this.playState.isPaused = false;
|
|
|
|
|
+ // 恢复duration如果播放器中为0
|
|
|
|
|
+ if (ijkDuration === 0 && savedDuration > 0) {
|
|
|
|
|
+ this.playState.duration = savedDuration;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.startProgressTask(); // 启动进度更新
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Resumed playback from pause');
|
|
|
|
|
+ return;
|
|
|
|
|
+ } catch (resumeError) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `Failed to resume from pause: ${resumeError}, starting fresh`);
|
|
|
|
|
+ // 恢复失败,重置状态继续执行重新播放逻辑
|
|
|
|
|
+ this.playState.isPaused = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 开始播放当前歌曲(重新准备数据源)
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Attempting to play song: ${this.currentSong.filePath}`);
|
|
|
|
|
+ await this.playWithSimpleLogic();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to play: ${error}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 播放指定索引的歌曲
|
|
|
|
|
+ */
|
|
|
|
|
+ private async playSongAtIndex(index: number): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (index < 0 || index >= this.playlist.length) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Invalid song index: ${index}`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, 'IjkPlayer not initialized');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const song = this.playlist[index];
|
|
|
|
|
+ this.currentIndex = index;
|
|
|
|
|
+ this.currentSong = song;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新AppStorage中的currIndex,与LocalMusic保持同步
|
|
|
|
|
+ AppStorage.setOrCreate('currIndex', this.currentIndex);
|
|
|
|
|
+
|
|
|
|
|
+ this.playState.isLoading = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Switching to song: ${song.name}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 使用简单播放逻辑
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.playWithSimpleLogic();
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复播放进度(记忆播放功能)
|
|
|
|
|
+ this.restorePlaybackPosition();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Successfully switched to song: ${song.name} by ${song.artist || '未知艺术家'}`);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (playError) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to switch song: ${playError}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ throw new Error(`Failed to switch song: ${playError}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 广播歌曲变化
|
|
|
|
|
+ this.broadcastSongChange();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to play song at index ${index}: ${error}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复播放进度(记忆播放功能)
|
|
|
|
|
+ */
|
|
|
|
|
+ private restorePlaybackPosition(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentSong || !this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 从PreferencesUtil获取保存的播放位置
|
|
|
|
|
+ const savedPosition = AppStorage.get<number>(`playback_${this.currentSong.filePath}`) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (savedPosition > 0) {
|
|
|
|
|
+ // 延迟恢复播放位置,确保播放器已准备好
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (this.ijkPlayer && this.ijkPlayer.isPlaying()) {
|
|
|
|
|
+ this.ijkPlayer.seekTo(savedPosition.toString());
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Restored playback position: ${savedPosition}ms for ${this.currentSong?.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to restore playback position: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存播放进度
|
|
|
|
|
+ */
|
|
|
|
|
+ private savePlaybackPosition(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentSong || !this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ const position = this.playState.currentPosition;
|
|
|
|
|
+ const duration = this.playState.duration;
|
|
|
|
|
+ const threshold = 5000; // 5秒阈值
|
|
|
|
|
+
|
|
|
|
|
+ // 如果播放位置接近末尾,则保存为0
|
|
|
|
|
+ const playbackPosition = (duration - position < threshold) ? 0 : position;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到AppStorage,这样LocalMusic也能访问到
|
|
|
|
|
+ AppStorage.setOrCreate(`playback_${this.currentSong.filePath}`, playbackPosition);
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Saved playback position: ${playbackPosition}ms for ${this.currentSong.name}`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to save playback position: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 暂停
|
|
|
|
|
+ */
|
|
|
|
|
+ async pause(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, 'IjkPlayer not initialized');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存播放进度
|
|
|
|
|
+ this.savePlaybackPosition();
|
|
|
|
|
+
|
|
|
|
|
+ this.pausePlayback();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback paused');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to pause: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理播放完成
|
|
|
|
|
+ */
|
|
|
|
|
+ private handlePlaybackCompletion(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.playState.currentPosition = 0;
|
|
|
|
|
+ this.stopProgressTask(); // 停止进度更新
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+
|
|
|
|
|
+ // 自动播放下一首
|
|
|
|
|
+ this.playNext().catch(() => {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to auto play next`);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to handle playback completion: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 简单播放逻辑(修复回调顺序问题)
|
|
|
|
|
+ */
|
|
|
|
|
+ private async playWithSimpleLogic(): Promise<void> {
|
|
|
|
|
+ return new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer || !this.currentSong) {
|
|
|
|
|
+ reject(new Error('IjkPlayer or currentSong not available'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Setting data source: ${this.currentSong.filePath}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 重置播放器状态
|
|
|
|
|
+ this.ijkPlayer.reset();
|
|
|
|
|
+ this.playState.isLoading = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 正确的IjkPlayer初始化顺序
|
|
|
|
|
+ // 1. 设置调试模式
|
|
|
|
|
+ this.ijkPlayer.setDebug(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 初始化配置
|
|
|
|
|
+ this.ijkPlayer.native_setup();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 设置音量
|
|
|
|
|
+ this.ijkPlayer.setVolume('1.0', '1.0');
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 设置数据源
|
|
|
|
|
+ this.ijkPlayer.setDataSource(this.currentSong.filePath);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 设置数据源头部(可选,对于本地文件通常不需要)
|
|
|
|
|
+ let headers = new Map([
|
|
|
|
|
+ ["user_agent", "Mozilla/5.0 BiliDroid/7.30.0 (bbcallen@gmail.com)"],
|
|
|
|
|
+ ["referer", "https://www.bilibili.com"]
|
|
|
|
|
+ ]);
|
|
|
|
|
+ this.ijkPlayer.setDataSourceHeader(headers);
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 配置所有播放器选项
|
|
|
|
|
+ this.setupIjkPlayerOptions();
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 设置回调监听器(重要!必须在setMessageListener之前)
|
|
|
|
|
+ this.setupPlayerCallbacks();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Player callbacks configured');
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 激活消息监听(必须在回调设置完成后,prepareAsync之前调用)
|
|
|
|
|
+ this.ijkPlayer.setMessageListener();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Message listener activated');
|
|
|
|
|
+
|
|
|
|
|
+ // 9. 异步准备(不要立即调用start,等待onPrepared回调)
|
|
|
|
|
+ this.ijkPlayer.prepareAsync();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'PrepareAsync called, waiting for onPrepared callback');
|
|
|
|
|
+
|
|
|
|
|
+ // 设置加载状态并广播
|
|
|
|
|
+ this.playState.isLoading = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkPlayer configuration and start completed');
|
|
|
|
|
+
|
|
|
|
|
+ // 立即resolve,不等待onPrepared(onPrepared会在回调中处理播放状态)
|
|
|
|
|
+ resolve();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed in playWithSimpleLogic: ${error}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 播放下一首
|
|
|
|
|
+ */
|
|
|
|
|
+ async playNext(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 刷新播放列表数据
|
|
|
|
|
+ this.refreshPlaylistData();
|
|
|
|
|
+
|
|
|
|
|
+ if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'No songs in playlist');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextIndex = (this.currentIndex + 1) % this.playlist.length;
|
|
|
|
|
+ await this.playSongAtIndex(nextIndex);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to play next: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 播放上一首
|
|
|
|
|
+ */
|
|
|
|
|
+ async playPrevious(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 刷新播放列表数据
|
|
|
|
|
+ this.refreshPlaylistData();
|
|
|
|
|
+
|
|
|
|
|
+ if (this.playlist.length === 0) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'No songs in playlist');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const prevIndex = this.currentIndex === 0 ? this.playlist.length - 1 : this.currentIndex - 1;
|
|
|
|
|
+ await this.playSongAtIndex(prevIndex);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to play previous: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理播放器状态变化
|
|
|
|
|
+ */
|
|
|
|
|
+ private handlePlayerStateChange(state: string): void {
|
|
|
|
|
+ switch (state) {
|
|
|
|
|
+ case 'playing':
|
|
|
|
|
+ this.playState.isPlaying = true;
|
|
|
|
|
+ this.playState.isPaused = false;
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'paused':
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'stopped':
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.currentPosition = 0;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'prepared':
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'error':
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 广播播放器状态
|
|
|
|
|
+ */
|
|
|
|
|
+ private broadcastPlayerState(): void {
|
|
|
|
|
+ // 节流:避免短时间内重复广播
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ if (now - this.lastStateBroadcastTime < this.STATE_BROADCAST_INTERVAL) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.lastStateBroadcastTime = now;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const playlistInfo: PlaylistInfo = {
|
|
|
|
|
+ hasNext: this.currentIndex < this.playlist.length - 1,
|
|
|
|
|
+ hasPrevious: this.currentIndex > 0,
|
|
|
|
|
+ currentIndex: this.currentIndex,
|
|
|
|
|
+ totalCount: this.playlist.length
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const currentSongData: SongData = this.currentSong ? {
|
|
|
|
|
+ id: this.currentSong.id,
|
|
|
|
|
+ title: this.currentSong.name,
|
|
|
|
|
+ artist: this.currentSong.artist || '未知艺术家',
|
|
|
|
|
+ album: this.currentSong.album || '未知专辑',
|
|
|
|
|
+ filePath: this.currentSong.filePath,
|
|
|
|
|
+ duration: parseInt(this.currentSong.duration || '0')
|
|
|
|
|
+ } : {
|
|
|
|
|
+ id: '',
|
|
|
|
|
+ title: '暂无播放',
|
|
|
|
|
+ artist: '未知艺术家',
|
|
|
|
|
+ album: '未知专辑',
|
|
|
|
|
+ filePath: '',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 获取实时播放位置
|
|
|
|
|
+ const currentPosition = this.ijkPlayer?.getCurrentPosition() || this.playState.currentPosition;
|
|
|
|
|
+
|
|
|
|
|
+ const progressData: ProgressData = {
|
|
|
|
|
+ currentPosition: currentPosition,
|
|
|
|
|
+ duration: this.playState.duration,
|
|
|
|
|
+ percentage: this.playState.duration > 0 ? (currentPosition / this.playState.duration) * 100 : 0,
|
|
|
|
|
+ currentTimeText: this.formatTime(currentPosition / 1000),
|
|
|
|
|
+ totalTimeText: this.formatTime(this.playState.duration / 1000)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 创建完整的WidgetData对象(与LocalMusic保持一致)
|
|
|
|
|
+ const widgetPlayState: WidgetPlayState = {
|
|
|
|
|
+ isPlaying: this.playState.isPlaying,
|
|
|
|
|
+ isPaused: this.playState.isPaused,
|
|
|
|
|
+ isLoading: this.playState.isLoading
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const widgetSongInfo: SongInfo = this.currentSong ? {
|
|
|
|
|
+ id: this.currentSong.id,
|
|
|
|
|
+ title: this.currentSong.name,
|
|
|
|
|
+ artist: this.currentSong.artist || '未知艺术家',
|
|
|
|
|
+ album: this.currentSong.album || '未知专辑',
|
|
|
|
|
+ coverImagePath: this.currentSong.pixelMapPath || '',
|
|
|
|
|
+ duration: parseInt(this.currentSong.duration || '0')
|
|
|
|
|
+ } : {
|
|
|
|
|
+ id: '',
|
|
|
|
|
+ title: '暂无播放',
|
|
|
|
|
+ artist: '未知艺术家',
|
|
|
|
|
+ album: '未知专辑',
|
|
|
|
|
+ coverImagePath: '',
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Broadcasting song info: artist="${this.currentSong?.artist}", title="${this.currentSong?.name}"`);
|
|
|
|
|
+
|
|
|
|
|
+ const widgetProgress: PlayProgress = {
|
|
|
|
|
+ currentPosition: currentPosition,
|
|
|
|
|
+ duration: this.playState.duration,
|
|
|
|
|
+ percentage: this.playState.duration > 0 ? (currentPosition / this.playState.duration) * 100 : 0,
|
|
|
|
|
+ currentTimeText: this.formatTime(currentPosition / 1000),
|
|
|
|
|
+ totalTimeText: this.formatTime(this.playState.duration / 1000)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const widgetPlaylist: PlaylistState = {
|
|
|
|
|
+ hasNext: this.currentIndex < this.playlist.length - 1,
|
|
|
|
|
+ hasPrevious: this.currentIndex > 0,
|
|
|
|
|
+ currentIndex: this.currentIndex,
|
|
|
|
|
+ totalCount: this.playlist.length
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const widgetConfig: WidgetConfig = {
|
|
|
|
|
+ size: 'medium',
|
|
|
|
|
+ theme: 'auto',
|
|
|
|
|
+ showProgress: true,
|
|
|
|
|
+ showCover: true
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const widgetData: WidgetData = {
|
|
|
|
|
+ playState: widgetPlayState,
|
|
|
|
|
+ currentSong: widgetSongInfo,
|
|
|
|
|
+ progress: widgetProgress,
|
|
|
|
|
+ playlist: widgetPlaylist,
|
|
|
|
|
+ config: widgetConfig
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 直接通过 DirectFormUpdateService 更新卡片数据(与LocalMusic保持一致)
|
|
|
|
|
+ try {
|
|
|
|
|
+ import('../widget/DirectFormUpdateService').then((module: DirectFormUpdateServiceModule) => {
|
|
|
|
|
+ const directFormService = module.DirectFormUpdateService.getInstance();
|
|
|
|
|
+ directFormService.updateAllForms(widgetData).then(() => {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Direct form update completed successfully');
|
|
|
|
|
+ }).catch((error: Error) => {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Direct form update failed: ${error.message}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ }).catch((importError: Error) => {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to import DirectFormUpdateService: ${importError.message}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to update widget via DirectFormUpdateService: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const stateData: PlayerStateData = {
|
|
|
|
|
+ playState: this.playState,
|
|
|
|
|
+ currentSong: currentSongData,
|
|
|
|
|
+ progress: progressData,
|
|
|
|
|
+ playlist: playlistInfo
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const publishInfo: commonEventManager.CommonEventPublishData = {
|
|
|
|
|
+ data: JSON.stringify(stateData)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ commonEventManager.publish(PLAYER_STATE_CHANGED_EVENT, publishInfo, (err) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast player state: ${err}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Player state broadcasted: playing=${this.playState.isPlaying}, song=${this.currentSong?.name || 'none'}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast player state: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 广播歌曲变化
|
|
|
|
|
+ */
|
|
|
|
|
+ private broadcastSongChange(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentSong) return;
|
|
|
|
|
+
|
|
|
|
|
+ const playlistInfo: PlaylistInfo = {
|
|
|
|
|
+ hasNext: this.currentIndex < this.playlist.length - 1,
|
|
|
|
|
+ hasPrevious: this.currentIndex > 0,
|
|
|
|
|
+ currentIndex: this.currentIndex,
|
|
|
|
|
+ totalCount: this.playlist.length
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const currentSongData: SongData = {
|
|
|
|
|
+ id: this.currentSong.id,
|
|
|
|
|
+ title: this.currentSong.name,
|
|
|
|
|
+ artist: this.currentSong.artist || '未知艺术家',
|
|
|
|
|
+ album: this.currentSong.album || '未知专辑',
|
|
|
|
|
+ filePath: this.currentSong.filePath,
|
|
|
|
|
+ duration: parseInt(this.currentSong.duration || '0')
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const songData: SongChangeData = {
|
|
|
|
|
+ currentSong: currentSongData,
|
|
|
|
|
+ playlist: playlistInfo
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const publishInfo: commonEventManager.CommonEventPublishData = {
|
|
|
|
|
+ data: JSON.stringify(songData)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ commonEventManager.publish(PLAYER_SONG_CHANGED_EVENT, publishInfo, (err) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast song change: ${err}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Song change broadcasted: ${this.currentSong?.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast song change: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 启动进度更新定时器
|
|
|
|
|
+ */
|
|
|
|
|
+ private startProgressTask(): void {
|
|
|
|
|
+ // 避免重复启动定时器
|
|
|
|
|
+ if (this.updateProgressTimer > 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.updateProgressTimer = setInterval(() => {
|
|
|
|
|
+ if (this.ijkPlayer && this.playState.isPlaying) {
|
|
|
|
|
+ this.updateProgress();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 300); // 与LocalMusic保持一致:300ms间隔
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Progress update timer started');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 停止进度更新定时器
|
|
|
|
|
+ */
|
|
|
|
|
+ private stopProgressTask(): void {
|
|
|
|
|
+ if (this.updateProgressTimer > 0) {
|
|
|
|
|
+ clearInterval(this.updateProgressTimer);
|
|
|
|
|
+ this.updateProgressTimer = 0;
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Progress update timer stopped');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新播放进度
|
|
|
|
|
+ */
|
|
|
|
|
+ private updateProgress(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer || !this.currentSong) return;
|
|
|
|
|
+
|
|
|
|
|
+ const currentPosition = this.ijkPlayer.getCurrentPosition() || 0;
|
|
|
|
|
+ const duration = this.playState.duration;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新内部状态
|
|
|
|
|
+ this.playState.currentPosition = currentPosition;
|
|
|
|
|
+
|
|
|
|
|
+ // 节流广播进度更新到卡片
|
|
|
|
|
+ this.broadcastProgressIfNeeded();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to update progress: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 节流广播进度更新
|
|
|
|
|
+ */
|
|
|
|
|
+ private broadcastProgressIfNeeded(): void {
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ if (now - this.lastProgressBroadcastTime >= this.PROGRESS_BROADCAST_INTERVAL) {
|
|
|
|
|
+ this.lastProgressBroadcastTime = now;
|
|
|
|
|
+ this.broadcastPlayerProgress();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 广播播放进度到卡片
|
|
|
|
|
+ */
|
|
|
|
|
+ private broadcastPlayerProgress(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const currentPos = this.ijkPlayer?.getCurrentPosition() || 0;
|
|
|
|
|
+ const progressData: ProgressData = {
|
|
|
|
|
+ currentPosition: currentPos,
|
|
|
|
|
+ duration: this.playState.duration || 0,
|
|
|
|
|
+ percentage: this.playState.duration > 0 ? (currentPos / this.playState.duration) * 100 : 0,
|
|
|
|
|
+ currentTimeText: this.formatTime(currentPos / 1000),
|
|
|
|
|
+ totalTimeText: this.formatTime(this.playState.duration / 1000)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 广播播放进度变化
|
|
|
|
|
+ const publishInfo: commonEventManager.CommonEventPublishData = {
|
|
|
|
|
+ data: JSON.stringify(progressData)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ commonEventManager.publish('com.ttmusic.player.progress', publishInfo, (err) => {
|
|
|
|
|
+ if (err) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast progress: ${err}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Progress broadcasted: ${progressData.percentage.toFixed(1)}%`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to broadcast progress: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化时间显示
|
|
|
|
|
+ */
|
|
|
|
|
+ private formatTime(seconds: number): string {
|
|
|
|
|
+ const mins = Math.floor(seconds / 60);
|
|
|
|
|
+ const secs = Math.floor(seconds % 60);
|
|
|
|
|
+ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 暂停播放
|
|
|
|
|
+ */
|
|
|
|
|
+ private pausePlayback(): void {
|
|
|
|
|
+ if (!this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.ijkPlayer.isPlaying()) {
|
|
|
|
|
+ this.ijkPlayer.pause();
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.stopProgressTask(); // 停止进度更新
|
|
|
|
|
+ this.savePlaybackState(); // 保存播放状态
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback paused');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 即使没在播放,也要确保状态正确
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.stopProgressTask();
|
|
|
|
|
+ this.savePlaybackState();
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback paused (was not playing)');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to pause playback: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 开始播放
|
|
|
|
|
+ */
|
|
|
|
|
+ private startPlayback(): void {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.ijkPlayer) {
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Starting playback after prepared');
|
|
|
|
|
+ // 在LocalMusic模式下,播放器已经在prepareAsync后自动开始
|
|
|
|
|
+ // 这里只需要更新状态
|
|
|
|
|
+ this.playState.isPlaying = true;
|
|
|
|
|
+ this.playState.isPaused = false;
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Playback confirmed started for ${this.currentSong?.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to start playback: ${error}`);
|
|
|
|
|
+ this.playState.isLoading = false;
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 停止播放
|
|
|
|
|
+ */
|
|
|
|
|
+ private stopPlayback(): void {
|
|
|
|
|
+ if (!this.ijkPlayer) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.ijkPlayer.isPlaying()) {
|
|
|
|
|
+ this.ijkPlayer.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.playState.isPlaying = false;
|
|
|
|
|
+ this.playState.isPaused = true;
|
|
|
|
|
+ this.playState.currentPosition = 0;
|
|
|
|
|
+ this.stopProgressTask(); // 停止进度更新
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'Playback stopped');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to stop playback: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 等待播放器达到指定状态
|
|
|
|
|
+ */
|
|
|
|
|
+ private async waitForPlayerReady(timeout: number = 3000): Promise<void> {
|
|
|
|
|
+ return new Promise<void>((resolve, reject) => {
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ reject(new Error('IjkPlayer is null'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 对于 IjkPlayer,我们简单地检查它是否已创建并可用
|
|
|
|
|
+ if (this.ijkPlayer) {
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const timeoutId = setTimeout(() => {
|
|
|
|
|
+ reject(new Error(`Timeout waiting for player ready`));
|
|
|
|
|
+ }, timeout);
|
|
|
|
|
+
|
|
|
|
|
+ // 简单延迟后解析,因为 IjkPlayer 不需要复杂的状态等待
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ clearTimeout(timeoutId);
|
|
|
|
|
+ if (this.ijkPlayer) {
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reject(new Error('Player not ready'));
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重置播放器(用于错误恢复)
|
|
|
|
|
+ */
|
|
|
|
|
+ private async resetPlayer(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.ijkPlayer) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, 'IjkPlayer is null, cannot reset');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Resetting IjkPlayer`);
|
|
|
|
|
+
|
|
|
|
|
+ // 先尝试停止播放器
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.ijkPlayer.isPlaying()) {
|
|
|
|
|
+ this.ijkPlayer.pause();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.ijkPlayer.stop();
|
|
|
|
|
+ } catch (stopError) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `Failed to stop player during reset: ${stopError}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重新创建播放器
|
|
|
|
|
+ await this.recreatePlayer();
|
|
|
|
|
+ hilog.info(0x0000, TAG, `Player reset completed`);
|
|
|
|
|
+
|
|
|
|
|
+ // 重置播放状态
|
|
|
|
|
+ this.playState = {
|
|
|
|
|
+ isPlaying: false,
|
|
|
|
|
+ isPaused: true,
|
|
|
|
|
+ isLoading: false,
|
|
|
|
|
+ currentPosition: 0,
|
|
|
|
|
+ duration: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.broadcastPlayerState();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to reset player: ${error}`);
|
|
|
|
|
+ // 如果重置失败,尝试重新创建播放器
|
|
|
|
|
+ await this.recreatePlayer();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重新创建播放器(最后的恢复手段)
|
|
|
|
|
+ */
|
|
|
|
|
+ private async recreatePlayer(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, 'Recreating IjkMediaPlayer...');
|
|
|
|
|
+
|
|
|
|
|
+ // 释放旧播放器
|
|
|
|
|
+ if (this.ijkPlayer) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.ijkPlayer.release();
|
|
|
|
|
+ } catch (releaseError) {
|
|
|
|
|
+ hilog.warn(0x0000, TAG, `Failed to release old player: ${releaseError}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.ijkPlayer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置重试计数
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建新播放器
|
|
|
|
|
+ await this.createAudioPlayer();
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IjkMediaPlayer recreated successfully');
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to recreate IjkMediaPlayer: ${error}`);
|
|
|
|
|
+ this.isInitialized = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取当前播放状态
|
|
|
|
|
+ */
|
|
|
|
|
+ getCurrentState(): PlayerCurrentState {
|
|
|
|
|
+ const currentState: PlayerCurrentState = {
|
|
|
|
|
+ playState: {
|
|
|
|
|
+ isPlaying: this.playState.isPlaying,
|
|
|
|
|
+ isPaused: this.playState.isPaused,
|
|
|
|
|
+ isLoading: this.playState.isLoading,
|
|
|
|
|
+ currentPosition: this.playState.currentPosition,
|
|
|
|
|
+ duration: this.playState.duration
|
|
|
|
|
+ },
|
|
|
|
|
+ currentSong: this.currentSong,
|
|
|
|
|
+ playlist: this.playlist.slice()
|
|
|
|
|
+ };
|
|
|
|
|
+ return currentState;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 销毁服务
|
|
|
|
|
+ */
|
|
|
|
|
+ async destroy(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 停止进度更新定时器
|
|
|
|
|
+ this.stopProgressTask();
|
|
|
|
|
+
|
|
|
|
|
+ if (this.ijkPlayer) {
|
|
|
|
|
+ this.ijkPlayer.release();
|
|
|
|
|
+ this.ijkPlayer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ emitter.off(9001);
|
|
|
|
|
+ this.isInitialized = false;
|
|
|
|
|
+
|
|
|
|
|
+ hilog.info(0x0000, TAG, 'IndependentPlayerService destroyed');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ hilog.error(0x0000, TAG, `Failed to destroy service: ${error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|