Release 1.2.12

This commit is contained in:
imeed166 2026-05-26 19:26:04 +01:00
parent 402e6f62bd
commit 80e4fb6868
6 changed files with 214 additions and 18 deletions

8
RELEASE_NOTES_1.2.12.md Normal file
View file

@ -0,0 +1,8 @@
# Cross Player 1.2.12
This patch release smooths out queue updates during Android playback and includes the latest queue state persistence fix.
Highlights:
- Fixed an Android playback hitch that could happen when a newly created media file was added to the queue while another file was already playing.
- Deferred expensive duration probing for newly detected Android media until playback is idle, while still adding the item to the queue immediately.
- Includes the latest save-order fix so queue updates do not overwrite newer playback positions.

103
main.js
View file

@ -2319,6 +2319,10 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
this.lastKnownDataMtime = 0;
this.lastKnownDataSize = 0;
this.isReloadingSyncedData = false;
this.saveDataChain = Promise.resolve();
this.deferredMetadataPaths = /* @__PURE__ */ new Set();
this.deferredMetadataTimer = null;
this.isHydratingDeferredMetadata = false;
}
getLocalStorage() {
if (typeof window === "undefined")
@ -2922,10 +2926,15 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
}
async saveData(refresh = true) {
this.rememberQueueScrollPosition();
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView)
this.listView.refresh();
const runSave = async () => {
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView)
this.listView.refresh();
};
const queuedSave = this.saveDataChain.catch(() => void 0).then(runSave);
this.saveDataChain = queuedSave.catch(() => void 0);
await queuedSave;
}
registerWatchers() {
this.registerEvent(
@ -3003,6 +3012,73 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
video.src = this.app.vault.getResourcePath(file);
});
}
isAndroidPlaybackProbeSensitive() {
if (!import_obsidian.Platform.isMobile)
return false;
if (import_obsidian.Platform.isAndroidApp)
return true;
return typeof navigator !== "undefined" && /Android/i.test(navigator.userAgent);
}
shouldDeferMetadataProbe() {
var _a;
return this.isAndroidPlaybackProbeSensitive() && !!((_a = this.mainView) == null ? void 0 : _a.isActivelyPlayingLocally());
}
queueDeferredMetadataHydration(path) {
this.deferredMetadataPaths.add(path);
this.scheduleDeferredMetadataHydration();
}
scheduleDeferredMetadataHydration(delay = 2e3) {
if (this.deferredMetadataTimer !== null)
return;
this.deferredMetadataTimer = window.setTimeout(() => {
this.deferredMetadataTimer = null;
void this.flushDeferredMetadataHydration();
}, delay);
}
async flushDeferredMetadataHydration() {
if (this.isHydratingDeferredMetadata)
return;
if (this.shouldDeferMetadataProbe()) {
this.scheduleDeferredMetadataHydration(3e3);
return;
}
this.isHydratingDeferredMetadata = true;
try {
while (this.deferredMetadataPaths.size > 0) {
if (this.shouldDeferMetadataProbe()) {
this.scheduleDeferredMetadataHydration(3e3);
break;
}
const nextPath = this.deferredMetadataPaths.values().next().value;
if (!nextPath)
break;
this.deferredMetadataPaths.delete(nextPath);
const file = this.app.vault.getAbstractFileByPath(nextPath);
if (!(file instanceof import_obsidian.TFile))
continue;
const queueItem = this.data.queue.find((item) => item.path === nextPath);
if (!queueItem || queueItem.duration > 0)
continue;
const duration = await this.getMediaDuration(file);
if (duration <= 0)
continue;
const latestItem = this.data.queue.find((item) => item.path === nextPath);
if (!latestItem || latestItem.duration > 0)
continue;
latestItem.duration = duration;
latestItem.size = latestItem.size || file.stat.size;
await this.saveData(false);
if (this.listView) {
this.listView.updateStats();
}
}
} finally {
this.isHydratingDeferredMetadata = false;
if (this.deferredMetadataPaths.size > 0) {
this.scheduleDeferredMetadataHydration(3e3);
}
}
}
getQueueStats() {
let totalDuration = 0;
let totalSize = 0;
@ -3087,7 +3163,8 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
return;
let existing = this.data.queue.find((item) => item.path === file.path);
if (!existing) {
const duration = await this.getMediaDuration(file);
const deferDurationProbe = this.shouldDeferMetadataProbe();
const duration = deferDurationProbe ? 0 : await this.getMediaDuration(file);
existing = this.data.queue.find((item) => item.path === file.path);
if (existing) {
if (!existing.duration) {
@ -3108,6 +3185,9 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
size: file.stat.size
};
this.data.queue.push(newItem);
if (deferDurationProbe) {
this.queueDeferredMetadataHydration(file.path);
}
if (shouldSave) {
await this.saveData();
new import_obsidian.Notice(`Added ${file.name} to queue`);
@ -3115,8 +3195,12 @@ var CrossPlayerPlugin = class extends import_obsidian.Plugin {
} else {
let changed = false;
if (!existing.duration) {
existing.duration = await this.getMediaDuration(file);
changed = true;
if (this.shouldDeferMetadataProbe()) {
this.queueDeferredMetadataHydration(file.path);
} else {
existing.duration = await this.getMediaDuration(file);
changed = true;
}
}
if (!existing.size) {
existing.size = file.stat.size;
@ -4353,6 +4437,9 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
const currentSrc = this.videoEl.currentSrc || this.videoEl.src;
return currentSrc === this.activeMediaSrc;
}
isActivelyPlayingLocally() {
return !!this.videoEl && this.isCurrentPlaybackSource() && !this.videoEl.paused && !this.videoEl.ended;
}
async syncCurrentItemDuration() {
if (!this.videoEl || !this.currentItem || !isFinite(this.videoEl.duration) || this.videoEl.duration <= 0) {
return;
@ -4501,6 +4588,7 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
void this.plugin.playNextUnread();
}
}
void this.plugin.flushDeferredMetadataHydration();
};
this.videoEl.ontimeupdate = async () => {
if (this.currentItem && this.isCurrentPlaybackSource()) {
@ -4531,6 +4619,7 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
}
void this.plugin.flushDeferredMetadataHydration();
this.updateOverlayProgress();
};
}

View file

@ -1,7 +1,7 @@
{
"id": "cross-player",
"name": "Cross Player",
"version": "1.2.11",
"version": "1.2.12",
"minAppVersion": "1.7.2",
"description": "A media player plugin focused on playback across desktop and mobile devices.",
"author": "Imed Ghomari",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "cross-player",
"version": "1.2.10",
"version": "1.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cross-player",
"version": "1.2.10",
"version": "1.2.12",
"license": "MIT",
"dependencies": {
"@types/sortablejs": "^1.15.9",

View file

@ -1,6 +1,6 @@
{
"name": "cross-player",
"version": "1.2.11",
"version": "1.2.12",
"description": "Media player plugin for Obsidian focused on playback across desktop and mobile devices",
"main": "main.js",
"scripts": {

View file

@ -96,6 +96,10 @@ export default class CrossPlayerPlugin extends Plugin {
lastKnownDataMtime: number = 0;
lastKnownDataSize: number = 0;
isReloadingSyncedData: boolean = false;
private saveDataChain: Promise<void> = Promise.resolve();
private deferredMetadataPaths: Set<string> = new Set();
private deferredMetadataTimer: number | null = null;
private isHydratingDeferredMetadata: boolean = false;
private getLocalStorage(): Storage | null {
if (typeof window === 'undefined') return null;
@ -813,9 +817,16 @@ export default class CrossPlayerPlugin extends Plugin {
async saveData(refresh: boolean = true) {
this.rememberQueueScrollPosition();
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView) this.listView.refresh();
const runSave = async () => {
await super.saveData(this.data);
await this.refreshTrackedDataFileState();
if (refresh && this.listView) this.listView.refresh();
};
const queuedSave = this.saveDataChain.catch(() => undefined).then(runSave);
this.saveDataChain = queuedSave.catch(() => undefined);
await queuedSave;
}
registerWatchers() {
@ -918,6 +929,79 @@ export default class CrossPlayerPlugin extends Plugin {
});
}
isAndroidPlaybackProbeSensitive(): boolean {
if (!Platform.isMobile) return false;
if (Platform.isAndroidApp) return true;
return typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
}
shouldDeferMetadataProbe(): boolean {
return this.isAndroidPlaybackProbeSensitive() && !!this.mainView?.isActivelyPlayingLocally();
}
queueDeferredMetadataHydration(path: string) {
this.deferredMetadataPaths.add(path);
this.scheduleDeferredMetadataHydration();
}
scheduleDeferredMetadataHydration(delay: number = 2000) {
if (this.deferredMetadataTimer !== null) return;
this.deferredMetadataTimer = window.setTimeout(() => {
this.deferredMetadataTimer = null;
void this.flushDeferredMetadataHydration();
}, delay);
}
async flushDeferredMetadataHydration() {
if (this.isHydratingDeferredMetadata) return;
if (this.shouldDeferMetadataProbe()) {
this.scheduleDeferredMetadataHydration(3000);
return;
}
this.isHydratingDeferredMetadata = true;
try {
while (this.deferredMetadataPaths.size > 0) {
if (this.shouldDeferMetadataProbe()) {
this.scheduleDeferredMetadataHydration(3000);
break;
}
const nextPath = this.deferredMetadataPaths.values().next().value as string | undefined;
if (!nextPath) break;
this.deferredMetadataPaths.delete(nextPath);
const file = this.app.vault.getAbstractFileByPath(nextPath);
if (!(file instanceof TFile)) continue;
const queueItem = this.data.queue.find(item => item.path === nextPath);
if (!queueItem || queueItem.duration > 0) continue;
const duration = await this.getMediaDuration(file);
if (duration <= 0) continue;
const latestItem = this.data.queue.find(item => item.path === nextPath);
if (!latestItem || latestItem.duration > 0) continue;
latestItem.duration = duration;
latestItem.size = latestItem.size || file.stat.size;
await this.saveData(false);
if (this.listView) {
this.listView.updateStats();
}
}
} finally {
this.isHydratingDeferredMetadata = false;
if (this.deferredMetadataPaths.size > 0) {
this.scheduleDeferredMetadataHydration(3000);
}
}
}
getQueueStats() {
let totalDuration = 0;
let totalSize = 0;
@ -1064,7 +1148,8 @@ export default class CrossPlayerPlugin extends Plugin {
// Check if already in queue
let existing = this.data.queue.find(item => item.path === file.path);
if (!existing) {
const duration = await this.getMediaDuration(file);
const deferDurationProbe = this.shouldDeferMetadataProbe();
const duration = deferDurationProbe ? 0 : await this.getMediaDuration(file);
// Double check existence after async duration fetch to prevent race conditions
existing = this.data.queue.find(item => item.path === file.path);
@ -1087,6 +1172,9 @@ export default class CrossPlayerPlugin extends Plugin {
size: file.stat.size
};
this.data.queue.push(newItem);
if (deferDurationProbe) {
this.queueDeferredMetadataHydration(file.path);
}
if (shouldSave) {
await this.saveData();
new Notice(`Added ${file.name} to queue`);
@ -1095,8 +1183,12 @@ export default class CrossPlayerPlugin extends Plugin {
// Update duration/size if missing (migration)
let changed = false;
if (!existing.duration) {
existing.duration = await this.getMediaDuration(file);
changed = true;
if (this.shouldDeferMetadataProbe()) {
this.queueDeferredMetadataHydration(file.path);
} else {
existing.duration = await this.getMediaDuration(file);
changed = true;
}
}
if (!existing.size) {
existing.size = file.stat.size;
@ -2804,6 +2896,10 @@ class CrossPlayerMainView extends ItemView {
return currentSrc === this.activeMediaSrc;
}
isActivelyPlayingLocally() {
return !!this.videoEl && this.isCurrentPlaybackSource() && !this.videoEl.paused && !this.videoEl.ended;
}
async syncCurrentItemDuration() {
if (!this.videoEl || !this.currentItem || !isFinite(this.videoEl.duration) || this.videoEl.duration <= 0) {
return;
@ -2986,6 +3082,8 @@ class CrossPlayerMainView extends ItemView {
void this.plugin.playNextUnread();
}
}
void this.plugin.flushDeferredMetadataHydration();
};
this.videoEl.ontimeupdate = async () => {
@ -3027,8 +3125,9 @@ class CrossPlayerMainView extends ItemView {
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
}
void this.plugin.flushDeferredMetadataHydration();
this.updateOverlayProgress();
}
};
}
async onClose() {