mirror of
https://github.com/imed-ghomari/cross-player-obsidian.git
synced 2026-07-22 06:55:49 +00:00
Release 1.2.12
This commit is contained in:
parent
402e6f62bd
commit
80e4fb6868
6 changed files with 214 additions and 18 deletions
8
RELEASE_NOTES_1.2.12.md
Normal file
8
RELEASE_NOTES_1.2.12.md
Normal 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
103
main.js
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
113
src/main.ts
113
src/main.ts
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue