|
@@ -1,5 +1,6 @@
|
|
|
import { VideoItem } from '../../viewmodel/VideoItem';
|
|
import { VideoItem } from '../../viewmodel/VideoItem';
|
|
|
import { PlayerManager, IPlayerManager, PlayerStateCallback } from './PlayerManager';
|
|
import { PlayerManager, IPlayerManager, PlayerStateCallback } from './PlayerManager';
|
|
|
|
|
+import { MD5 } from '@pura/harmony-utils';
|
|
|
import {
|
|
import {
|
|
|
PlayerStateModel,
|
|
PlayerStateModel,
|
|
|
PlayerState,
|
|
PlayerState,
|
|
@@ -168,6 +169,11 @@ interface CachedWidgetData {
|
|
|
finalImagePath: string | null;
|
|
finalImagePath: string | null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface ImageCacheInfo {
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ imgName: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 统一播放器服务接口
|
|
* 统一播放器服务接口
|
|
|
* 提供统一的播放控制接口,替代LocalMusic中的播放器逻辑
|
|
* 提供统一的播放控制接口,替代LocalMusic中的播放器逻辑
|
|
@@ -478,6 +484,7 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
// 图片缓存相关属性
|
|
// 图片缓存相关属性
|
|
|
private imageCacheDir: string = ''; // 图片缓存目录
|
|
private imageCacheDir: string = ''; // 图片缓存目录
|
|
|
|
|
+ private lastImageCache: ImageCacheInfo | null = null; // 图片缓存优化
|
|
|
private readonly CACHE_EXPIRY_DAYS: number = 7; // 缓存有效期:7天
|
|
private readonly CACHE_EXPIRY_DAYS: number = 7; // 缓存有效期:7天
|
|
|
private readonly MAX_CACHE_SIZE_MB: number = 50; // 最大缓存大小:50MB
|
|
private readonly MAX_CACHE_SIZE_MB: number = 50; // 最大缓存大小:50MB
|
|
|
private hasChecked: boolean=false;
|
|
private hasChecked: boolean=false;
|
|
@@ -1058,7 +1065,7 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
// 更新播放列表状态
|
|
// 更新播放列表状态
|
|
|
await this.updatePlaylistStateInModel();
|
|
await this.updatePlaylistStateInModel();
|
|
|
|
|
|
|
|
- // 播放新歌曲后,等待一段时间确保播放器状态稳定,然后更新卡片
|
|
|
|
|
|
|
+ // 优化:减少延迟,立即更新卡片状态,避免重复更新AVSession
|
|
|
setTimeout(async () => {
|
|
setTimeout(async () => {
|
|
|
try {
|
|
try {
|
|
|
// 确保播放列表状态是最新的
|
|
// 确保播放列表状态是最新的
|
|
@@ -1066,15 +1073,15 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
// 强制更新所有桌面卡片状态
|
|
// 强制更新所有桌面卡片状态
|
|
|
await this.updateAllForms(true);
|
|
await this.updateAllForms(true);
|
|
|
- if (newSong) {
|
|
|
|
|
- await this.updateAvSessionMetadata(newSong)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 移除此处的AVSession更新,避免与onPrepared中的更新冲突
|
|
|
|
|
+ // AVSession会在onPrepared回调中正确更新
|
|
|
|
|
|
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService: Widget state updated after track change');
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService: Widget state updated after track change');
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService: Failed to update widgets after track change: ${error}`);
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService: Failed to update widgets after track change: ${error}`);
|
|
|
}
|
|
}
|
|
|
- }, 200); // 延迟200ms确保播放器状态稳定
|
|
|
|
|
|
|
+ }, 50); // 减少延迟到50ms,提高响应速度
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
await this.handlePlaybackError(error as Error, PlayerErrorType.PLAYBACK_ERROR);
|
|
await this.handlePlaybackError(error as Error, PlayerErrorType.PLAYBACK_ERROR);
|
|
@@ -2763,10 +2770,12 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
// 更新 AVSession 状态
|
|
// 更新 AVSession 状态
|
|
|
if (isActuallyPlaying) {
|
|
if (isActuallyPlaying) {
|
|
|
- let currentSong = this.getCurrentSong();
|
|
|
|
|
|
|
+ // 优化:确保使用最新的歌曲数据
|
|
|
|
|
+ let currentSong = this.playlistModel.getCurrentSong();
|
|
|
if (currentSong) {
|
|
if (currentSong) {
|
|
|
console.log(`Heanup2 UnifiedPlayerService: onPrepared - updateAvSessionMetadata: ${json.stringify(currentSong)}`);
|
|
console.log(`Heanup2 UnifiedPlayerService: onPrepared - updateAvSessionMetadata: ${json.stringify(currentSong)}`);
|
|
|
- this.updateAvSessionMetadata(currentSong)
|
|
|
|
|
|
|
+ // 立即更新,避免延迟导致的状态不一致
|
|
|
|
|
+ this.updateAvSessionMetadata(currentSong);
|
|
|
}
|
|
}
|
|
|
this.updateSessionPlayState(true); // 强制更新,不使用防抖
|
|
this.updateSessionPlayState(true); // 强制更新,不使用防抖
|
|
|
}
|
|
}
|
|
@@ -2990,12 +2999,12 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 更新所有桌面卡片状态
|
|
|
|
|
|
|
+ * 更新所有桌面卡片状态(优化为UI优先响应)
|
|
|
* 将VideoItem和PlayerState数据传入桌面卡片,控制卡片的显示
|
|
* 将VideoItem和PlayerState数据传入桌面卡片,控制卡片的显示
|
|
|
*/
|
|
*/
|
|
|
async updateAllForms(forceUpdate: boolean = false) {
|
|
async updateAllForms(forceUpdate: boolean = false) {
|
|
|
// 防抖和重入保护,防止短时间内重复执行更新
|
|
// 防抖和重入保护,防止短时间内重复执行更新
|
|
|
- if (this.isUpdatingForms) {
|
|
|
|
|
|
|
+ if (this.isUpdatingForms && !forceUpdate) {
|
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: Update already in progress, skipping.');
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: Update already in progress, skipping.');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -3009,38 +3018,44 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 根据存储配置获取Form列表
|
|
|
|
|
- let formInfoList: FormInfo[] = [];
|
|
|
|
|
- if (WIDGET_DATA_STORAGE_TYPE === WidgetDataStorageType.DATABASE) {
|
|
|
|
|
- formInfoList = await FormRdbHelper.getInstance(context).queryAllForms();
|
|
|
|
|
- } else {
|
|
|
|
|
- // 从Preferences获取Form列表
|
|
|
|
|
- formInfoList = await this.getFormInfoListFromPreferences(context);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 优化:异步获取Form列表,不阻塞UI更新
|
|
|
|
|
+ const formInfoListPromise = this.getFormInfoListAsync(context);
|
|
|
|
|
|
|
|
- if (formInfoList.length === 0) {
|
|
|
|
|
- LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: No forms found');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // --- 单阶段更新策略 ---
|
|
|
|
|
- // 直接获取包含所有数据的卡片信息,包括图片。
|
|
|
|
|
- // 图片加载有缓存机制,对于已加载过的歌曲,此操作很快。
|
|
|
|
|
- // 这可以避免双阶段更新导致的UI闪烁问题(例如背景色先变默认再变主色调)。
|
|
|
|
|
- const widgetData = await this.getWidgetFormData(); // true: 加载图片
|
|
|
|
|
|
|
+ // 优化:立即获取卡片数据,优先响应UI
|
|
|
|
|
+ const widgetData = await this.getWidgetFormData();
|
|
|
if (!widgetData) {
|
|
if (!widgetData) {
|
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: Failed to get widget data.');
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: Failed to get widget data.');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const bindingData = formBindingData.createFormBindingData(widgetData);
|
|
const bindingData = formBindingData.createFormBindingData(widgetData);
|
|
|
- // 并行更新所有卡片,提高效率
|
|
|
|
|
- const updatePromises = formInfoList.map(formInfo =>
|
|
|
|
|
- formProvider.updateForm(formInfo.formId, bindingData)
|
|
|
|
|
- );
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 等待Form列表获取完成
|
|
|
|
|
+ const formInfoList = await formInfoListPromise;
|
|
|
|
|
+ if (formInfoList.length === 0) {
|
|
|
|
|
+ LogUtils.getInstance().LOGI('UnifiedPlayerService updateAllForms: No forms found');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 优化:并行更新所有卡片,使用批量更新提高效率
|
|
|
|
|
+ const updatePromises = formInfoList.map(formInfo => {
|
|
|
|
|
+ // 每个卡片更新都是异步的,避免阻塞
|
|
|
|
|
+ return formProvider.updateForm(formInfo.formId, bindingData).catch((error: Error) => {
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`Failed to update form ${formInfo.formId}: ${error.message}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
await Promise.all(updatePromises);
|
|
await Promise.all(updatePromises);
|
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService: All forms updated for ${formInfoList.length} forms.`);
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService: All forms updated for ${formInfoList.length} forms.`);
|
|
|
|
|
|
|
|
|
|
+ // 异步保存数据到数据库,不阻塞UI
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const currentSong = this.getCurrentSong();
|
|
|
|
|
+ if (currentSong) {
|
|
|
|
|
+ this.saveWidgetDataToDatabase(widgetData, currentSong);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService updateAllForms error: ${error}`);
|
|
LogUtils.getInstance().LOGI(`Heanup UnifiedPlayerService updateAllForms error: ${error}`);
|
|
|
} finally {
|
|
} finally {
|
|
@@ -3048,6 +3063,22 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 异步获取FormInfo列表(优化方法)
|
|
|
|
|
+ */
|
|
|
|
|
+ private async getFormInfoListAsync(context: Context): Promise<FormInfo[]> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (WIDGET_DATA_STORAGE_TYPE === WidgetDataStorageType.DATABASE) {
|
|
|
|
|
+ return await FormRdbHelper.getInstance(context).queryAllForms();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return await this.getFormInfoListFromPreferences(context);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`Failed to get form info list: ${error}`);
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public async getWidgetFormData(loadImage: boolean = true): Promise<widgeData | undefined> {
|
|
public async getWidgetFormData(loadImage: boolean = true): Promise<widgeData | undefined> {
|
|
|
this.updatePlaylistStateInModelSync();
|
|
this.updatePlaylistStateInModelSync();
|
|
|
const formData: VideoItem = this.getCurrentSong()!; // VideoItem
|
|
const formData: VideoItem = this.getCurrentSong()!; // VideoItem
|
|
@@ -3172,12 +3203,27 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
if (cachedData.finalImagePath) {
|
|
if (cachedData.finalImagePath) {
|
|
|
try {
|
|
try {
|
|
|
- // 每次都重新打开文件获取新的fd,这是最安全的方式
|
|
|
|
|
- const file = fileIo.openSync(cachedData.finalImagePath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
- // 使用时间戳和随机数生成唯一名称,强制卡片刷新图片
|
|
|
|
|
- imgName = `songCover_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
|
|
|
- imgMap[imgName] = file.fd;
|
|
|
|
|
- // 注意:我们不关闭这个fd,系统会在处理完卡片数据后关闭它
|
|
|
|
|
|
|
+ // 优化:复用已缓存的图片文件描述符,避免重复文件操作
|
|
|
|
|
+ const currentSongPath = cachedData.finalImagePath;
|
|
|
|
|
+ const cacheKey = `img_${currentSongPath.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有缓存的图片名称,避免重复生成
|
|
|
|
|
+ if (this.lastImageCache && this.lastImageCache.path === currentSongPath && this.lastImageCache.imgName) {
|
|
|
|
|
+ imgName = this.lastImageCache.imgName;
|
|
|
|
|
+ const file = fileIo.openSync(currentSongPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ imgMap[imgName] = file.fd;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 仅在图片真正改变时才重新打开文件
|
|
|
|
|
+ const file = fileIo.openSync(currentSongPath, fileIo.OpenMode.READ_ONLY);
|
|
|
|
|
+ imgName = `songCover_${cacheKey}`;
|
|
|
|
|
+ imgMap[imgName] = file.fd;
|
|
|
|
|
+
|
|
|
|
|
+ // 缓存当前图片信息
|
|
|
|
|
+ this.lastImageCache = {
|
|
|
|
|
+ path: currentSongPath,
|
|
|
|
|
+ imgName: imgName
|
|
|
|
|
+ } as ImageCacheInfo;
|
|
|
|
|
+ }
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
LogUtils.getInstance().LOGI(`UnifiedPlayerService: Failed to open image file [${cachedData.finalImagePath}]: ${JSON.stringify(error)}`);
|
|
LogUtils.getInstance().LOGI(`UnifiedPlayerService: Failed to open image file [${cachedData.finalImagePath}]: ${JSON.stringify(error)}`);
|
|
|
imgMap = {};
|
|
imgMap = {};
|
|
@@ -3527,8 +3573,15 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 优化:确保使用最新的歌曲数据,避免状态不一致
|
|
|
const currentSong = this.playlistModel.getCurrentSong();
|
|
const currentSong = this.playlistModel.getCurrentSong();
|
|
|
const currentPosition = this.getCurrentPosition();
|
|
const currentPosition = this.getCurrentPosition();
|
|
|
|
|
+
|
|
|
|
|
+ // 添加数据一致性检查
|
|
|
|
|
+ if (!currentSong) {
|
|
|
|
|
+ LogUtils.getInstance().LOGI('UnifiedPlayerService: No current song available for AVSession update');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 优先从播放器获取实际duration
|
|
// 优先从播放器获取实际duration
|
|
|
let duration = 0;
|
|
let duration = 0;
|
|
@@ -3581,35 +3634,42 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 更新AVSession元数据(带防抖和智能更新)
|
|
|
|
|
|
|
+ * 更新AVSession元数据(优化防抖逻辑,修复竞态条件)
|
|
|
*/
|
|
*/
|
|
|
private async updateAvSessionMetadata(song: VideoItem): Promise<void> {
|
|
private async updateAvSessionMetadata(song: VideoItem): Promise<void> {
|
|
|
- // 清除之前的定时器
|
|
|
|
|
|
|
+ // 清除之前的定时器,避免重复更新
|
|
|
if (this.avMetadataUpdateTimer !== -1) {
|
|
if (this.avMetadataUpdateTimer !== -1) {
|
|
|
clearTimeout(this.avMetadataUpdateTimer);
|
|
clearTimeout(this.avMetadataUpdateTimer);
|
|
|
|
|
+ this.avMetadataUpdateTimer = -1;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 优化:减少防抖时间,提高响应速度
|
|
|
const now = Date.now();
|
|
const now = Date.now();
|
|
|
- if (now - this.lastAvSessionUpdateTime < 1000) {
|
|
|
|
|
|
|
+ if (now - this.lastAvSessionUpdateTime < 500) {
|
|
|
|
|
+ // 如果更新过于频繁,延迟到下一个时间窗口
|
|
|
|
|
+ this.avMetadataUpdateTimer = setTimeout(() => {
|
|
|
|
|
+ this.doUpdateAvSessionMetadata(song);
|
|
|
|
|
+ }, 500 - (now - this.lastAvSessionUpdateTime));
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
this.lastAvSessionUpdateTime = now;
|
|
this.lastAvSessionUpdateTime = now;
|
|
|
- // 如果播放器还没准备好,延迟更新等待播放器准备完成
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 检查播放器状态,决定更新时机
|
|
|
const ijkPlayer = this.playerManager.getIjkPlayer();
|
|
const ijkPlayer = this.playerManager.getIjkPlayer();
|
|
|
const playerDuration = ijkPlayer ? ijkPlayer.getDuration() : 0;
|
|
const playerDuration = ijkPlayer ? ijkPlayer.getDuration() : 0;
|
|
|
const shouldWaitForPlayer = playerDuration <= 0 && this.isPlayerPreparedForCurrentSong === false;
|
|
const shouldWaitForPlayer = playerDuration <= 0 && this.isPlayerPreparedForCurrentSong === false;
|
|
|
- this.avMetadataUpdateTimer = setTimeout(() => {
|
|
|
|
|
- this.doUpdateAvSessionMetadata(song);
|
|
|
|
|
- }, 800);
|
|
|
|
|
|
|
+
|
|
|
if (shouldWaitForPlayer) {
|
|
if (shouldWaitForPlayer) {
|
|
|
- // 播放器还没准备好,延迟更新
|
|
|
|
|
|
|
+ // 播放器还没准备好,适当延迟
|
|
|
this.avMetadataUpdateTimer = setTimeout(() => {
|
|
this.avMetadataUpdateTimer = setTimeout(() => {
|
|
|
this.doUpdateAvSessionMetadata(song);
|
|
this.doUpdateAvSessionMetadata(song);
|
|
|
- }, 800);
|
|
|
|
|
|
|
+ }, 300);
|
|
|
} else {
|
|
} else {
|
|
|
- // 播放器已准备好或者不需要等待,稍微延迟更新
|
|
|
|
|
|
|
+ // 播放器已准备好,立即更新
|
|
|
this.avMetadataUpdateTimer = setTimeout(() => {
|
|
this.avMetadataUpdateTimer = setTimeout(() => {
|
|
|
this.doUpdateAvSessionMetadata(song);
|
|
this.doUpdateAvSessionMetadata(song);
|
|
|
- }, 800);
|
|
|
|
|
|
|
+ }, 50);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3623,9 +3683,23 @@ export class UnifiedPlayerService implements IPlayerService, PlaylistSyncListene
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 避免过于频繁的元数据更新(最小间隔600ms)
|
|
|
|
|
|
|
+ // 关键修复:验证歌曲数据的一致性,确保使用最新的歌曲信息
|
|
|
|
|
+ const currentSong = this.playlistModel.getCurrentSong();
|
|
|
|
|
+ if (!currentSong || currentSong.filePath !== song.filePath) {
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`UnifiedPlayerService: Song data inconsistency detected. Current: ${currentSong?.name}, Provided: ${song.name}`);
|
|
|
|
|
+ // 使用最新的歌曲数据
|
|
|
|
|
+ if (currentSong) {
|
|
|
|
|
+ song = currentSong;
|
|
|
|
|
+ LogUtils.getInstance().LOGI(`UnifiedPlayerService: Using latest song data: ${song.name}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ LogUtils.getInstance().LOGI('UnifiedPlayerService: No current song available, skipping metadata update');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 减少防抖时间,提高响应速度
|
|
|
const now = Date.now();
|
|
const now = Date.now();
|
|
|
- if (now - this.lastAvMetadataUpdate < 600) {
|
|
|
|
|
|
|
+ if (now - this.lastAvMetadataUpdate < 300) {
|
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService: Skipping AVSession metadata update due to rate limit');
|
|
LogUtils.getInstance().LOGI('UnifiedPlayerService: Skipping AVSession metadata update due to rate limit');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|