Release 1.2.4

This commit is contained in:
imeed166 2026-05-04 13:57:28 +01:00
parent 7f1089137e
commit 4e8385ad07
6 changed files with 212 additions and 47 deletions

9
RELEASE_NOTES_1.2.4.md Normal file
View file

@ -0,0 +1,9 @@
# Cross Player 1.2.4
This patch release fixes playback progress persistence and queue progress updates for audio files with unstable browser-reported durations, especially mp3.
Highlights:
- Fixed saved playback position updates so they persist during playback instead of only when switching files.
- Fixed queue progress bar updates for mp3 playback.
- Preserved the current playback position when closing the player pane.
- Hardened progress and completion tracking for other media formats that can report duration changes after loading.

108
main.js
View file

@ -4265,6 +4265,8 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
this.gainNode = null;
this.compressorNode = null;
this.mobileOverlayHideTimeout = null;
this.activeMediaSrc = null;
this.lastPositionPersist = 0;
this.plugin = plugin;
}
getOverlayProgressBottomOffset() {
@ -4288,6 +4290,42 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
getIcon() {
return "play";
}
isCurrentPlaybackSource() {
if (!this.videoEl || !this.currentItem || !this.activeMediaSrc)
return false;
const currentSrc = this.videoEl.currentSrc || this.videoEl.src;
return currentSrc === this.activeMediaSrc;
}
async syncCurrentItemDuration() {
if (!this.videoEl || !this.currentItem || !isFinite(this.videoEl.duration) || this.videoEl.duration <= 0) {
return;
}
const actualDuration = this.videoEl.duration;
if (Math.abs((this.currentItem.duration || 0) - actualDuration) <= 1) {
return;
}
this.currentItem.duration = actualDuration;
await this.plugin.saveData(false);
if (this.plugin.listView) {
this.plugin.listView.updateStats();
}
}
async persistCurrentPlaybackPosition(force = false) {
if (!this.videoEl || !this.currentItem || !this.isCurrentPlaybackSource() || !isFinite(this.videoEl.currentTime)) {
return;
}
this.currentItem.position = this.videoEl.currentTime;
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, force);
}
async syncCompletionStatusFromPlayback() {
if (!this.videoEl || !this.currentItem || !this.isCurrentPlaybackSource() || this.currentItem.status === "completed" || !isFinite(this.videoEl.duration) || this.videoEl.duration <= 0) {
return;
}
const progress = this.videoEl.currentTime / this.videoEl.duration;
if (progress > 0.95) {
await this.plugin.updateStatus(this.currentItem.id, "completed");
}
}
async onOpen() {
const container = this.contentEl;
container.empty();
@ -4369,7 +4407,9 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
this.showIdlePlaceholder();
this.refreshMobileOverlay();
this.videoEl.onended = async () => {
if (this.currentItem) {
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
if (this.currentItem.status !== "completed") {
await this.plugin.updateStatus(this.currentItem.id, "completed");
}
@ -4379,10 +4419,7 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
}
};
this.videoEl.ontimeupdate = async () => {
if (this.currentItem) {
if (this.videoEl.duration > 0 && Math.abs(this.videoEl.duration - this.currentItem.duration) > 5) {
return;
}
if (this.currentItem && this.isCurrentPlaybackSource()) {
this.currentItem.position = this.videoEl.currentTime;
const now = Date.now();
if (now - this.lastEtcUpdate > 5e3) {
@ -4391,6 +4428,10 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
this.plugin.listView.updateStats();
}
}
if (now - this.lastPositionPersist > 5e3) {
this.lastPositionPersist = now;
await this.persistCurrentPlaybackPosition();
}
if (now - this.lastProgressUpdate > 1e3) {
this.lastProgressUpdate = now;
if (this.plugin.listView && this.plugin.data.settings.showProgressColor && this.videoEl.duration > 0) {
@ -4399,24 +4440,42 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
}
}
this.updateOverlayProgress();
if (this.currentItem.status !== "completed" && this.videoEl.duration > 0) {
const progress = this.videoEl.currentTime / this.videoEl.duration;
if (progress > 0.95) {
await this.plugin.updateStatus(this.currentItem.id, "completed");
}
}
await this.syncCompletionStatusFromPlayback();
}
};
this.videoEl.onpause = async () => {
if (this.currentItem) {
if (this.videoEl.duration > 0 && Math.abs(this.videoEl.duration - this.currentItem.duration) > 5) {
return;
}
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, true);
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
}
this.updateOverlayProgress();
};
}
async onClose() {
const closingItem = this.currentItem;
const shouldResetStatus = (closingItem == null ? void 0 : closingItem.status) === "playing";
if (this.videoEl) {
this.videoEl.pause();
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
this.videoEl.removeAttribute("src");
this.videoEl.load();
}
if (closingItem && shouldResetStatus) {
if (closingItem.finished) {
await this.plugin.updateStatus(closingItem.id, "completed");
} else {
await this.plugin.updateStatus(closingItem.id, "pending");
}
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;
if (this.plugin.mainView === this) {
this.plugin.mainView = null;
}
this.contentEl.empty();
}
refreshMobileOverlay() {
const container = this.videoWrapperEl || this.contentEl;
const shouldShow = this.shouldUseTouchOverlay();
@ -5012,13 +5071,16 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
if (this.videoEl) {
this.videoEl.pause();
if (this.currentItem) {
this.currentItem.position = this.videoEl.currentTime;
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, true);
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
}
this.videoEl.removeAttribute("src");
this.videoEl.load();
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;
this.showIdlePlaceholder();
}
async play(item, autoPlay = false) {
@ -5063,6 +5125,7 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
}
}
this.videoEl.onloadedmetadata = () => {
void this.syncCurrentItemDuration();
this.updateOverlayProgress();
if (!sidecarFound && this.videoEl && this.videoEl.textTracks && this.videoEl.textTracks.length > 0) {
for (let i = 0; i < this.videoEl.textTracks.length; i++) {
@ -5074,6 +5137,10 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
}
}
};
this.videoEl.ondurationchange = () => {
void this.syncCurrentItemDuration();
this.updateOverlayProgress();
};
this.videoEl.onerror = () => {
var _a2;
console.error("Video playback error", (_a2 = this.videoEl) == null ? void 0 : _a2.error);
@ -5081,7 +5148,9 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
};
const file = this.plugin.app.vault.getAbstractFileByPath(item.path);
if (file instanceof import_obsidian.TFile) {
this.videoEl.src = this.plugin.app.vault.getResourcePath(file);
const resourcePath = this.plugin.app.vault.getResourcePath(file);
this.activeMediaSrc = resourcePath;
this.videoEl.src = resourcePath;
} else {
console.error("File not found for playback:", item.path);
this.showIdlePlaceholder();
@ -5090,6 +5159,7 @@ var CrossPlayerMainView = class extends import_obsidian.ItemView {
const resumePosition = item.position > 2 ? item.position - 2 : item.position;
this.videoEl.currentTime = resumePosition || 0;
this.videoEl.playbackRate = this.plugin.data.playbackSpeed || 1;
this.lastPositionPersist = 0;
this.applyAudioSettings();
const ext = (_a = item.path.split(".").pop()) == null ? void 0 : _a.toLowerCase();
const isAudio = AUDIO_EXTENSIONS.includes(ext || "");

View file

@ -1,7 +1,7 @@
{
"id": "cross-player",
"name": "Cross Player",
"version": "1.2.3",
"version": "1.2.4",
"minAppVersion": "0.15.0",
"description": "A media player plugin that watches a folder and plays media.",
"author": "Imed Ghomari",

4
package-lock.json generated
View file

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

View file

@ -1,6 +1,6 @@
{
"name": "cross-player",
"version": "1.2.3",
"version": "1.2.4",
"description": "Media player plugin for Obsidian",
"main": "main.js",
"scripts": {

View file

@ -2683,6 +2683,8 @@ class CrossPlayerMainView extends ItemView {
gainNode: GainNode | null = null;
compressorNode: DynamicsCompressorNode | null = null;
mobileOverlayHideTimeout: number | null = null;
activeMediaSrc: string | null = null;
lastPositionPersist: number = 0;
constructor(leaf: WorkspaceLeaf, plugin: CrossPlayerPlugin) {
super(leaf);
@ -2717,6 +2719,57 @@ class CrossPlayerMainView extends ItemView {
return "play";
}
isCurrentPlaybackSource() {
if (!this.videoEl || !this.currentItem || !this.activeMediaSrc) return false;
const currentSrc = this.videoEl.currentSrc || this.videoEl.src;
return currentSrc === this.activeMediaSrc;
}
async syncCurrentItemDuration() {
if (!this.videoEl || !this.currentItem || !isFinite(this.videoEl.duration) || this.videoEl.duration <= 0) {
return;
}
const actualDuration = this.videoEl.duration;
if (Math.abs((this.currentItem.duration || 0) - actualDuration) <= 1) {
return;
}
this.currentItem.duration = actualDuration;
await this.plugin.saveData(false);
if (this.plugin.listView) {
this.plugin.listView.updateStats();
}
}
async persistCurrentPlaybackPosition(force: boolean = false) {
if (!this.videoEl || !this.currentItem || !this.isCurrentPlaybackSource() || !isFinite(this.videoEl.currentTime)) {
return;
}
this.currentItem.position = this.videoEl.currentTime;
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, force);
}
async syncCompletionStatusFromPlayback() {
if (
!this.videoEl ||
!this.currentItem ||
!this.isCurrentPlaybackSource() ||
this.currentItem.status === 'completed' ||
!isFinite(this.videoEl.duration) ||
this.videoEl.duration <= 0
) {
return;
}
const progress = this.videoEl.currentTime / this.videoEl.duration;
if (progress > 0.95) {
await this.plugin.updateStatus(this.currentItem.id, 'completed');
}
}
async onOpen() {
const container = this.contentEl;
container.empty();
@ -2811,7 +2864,10 @@ class CrossPlayerMainView extends ItemView {
this.refreshMobileOverlay();
this.videoEl.onended = async () => {
if (this.currentItem) {
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
// If onended fires, it's definitely completed
if (this.currentItem.status !== 'completed') {
await this.plugin.updateStatus(this.currentItem.id, 'completed');
@ -2825,14 +2881,7 @@ class CrossPlayerMainView extends ItemView {
};
this.videoEl.ontimeupdate = async () => {
if (this.currentItem) {
// Check if the current video source duration matches the expected metadata
// This helps prevent "jumps" during source switches
if (this.videoEl.duration > 0 && Math.abs(this.videoEl.duration - this.currentItem.duration) > 5) {
// Metadata mismatch - probably transitioning source
return;
}
if (this.currentItem && this.isCurrentPlaybackSource()) {
this.currentItem.position = this.videoEl.currentTime;
// Throttled ETC update in List View
@ -2845,6 +2894,11 @@ class CrossPlayerMainView extends ItemView {
}
}
if (now - this.lastPositionPersist > 5000) {
this.lastPositionPersist = now;
await this.persistCurrentPlaybackPosition();
}
// Throttled Progress Bar update (every 1s)
if (now - this.lastProgressUpdate > 1000) {
this.lastProgressUpdate = now;
@ -2857,27 +2911,48 @@ class CrossPlayerMainView extends ItemView {
this.updateOverlayProgress();
// Mark as completed if > 95% watched
if (this.currentItem.status !== 'completed' && this.videoEl.duration > 0) {
const progress = this.videoEl.currentTime / this.videoEl.duration;
if (progress > 0.95) {
await this.plugin.updateStatus(this.currentItem.id, 'completed');
}
}
await this.syncCompletionStatusFromPlayback();
}
};
this.videoEl.onpause = async () => {
if (this.currentItem) {
// Check if the current video source duration matches the expected metadata
if (this.videoEl.duration > 0 && Math.abs(this.videoEl.duration - this.currentItem.duration) > 5) {
return;
}
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, true);
if (this.currentItem && this.isCurrentPlaybackSource()) {
await this.persistCurrentPlaybackPosition(true);
}
this.updateOverlayProgress();
}
}
async onClose() {
const closingItem = this.currentItem;
const shouldResetStatus = closingItem?.status === 'playing';
if (this.videoEl) {
this.videoEl.pause();
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
this.videoEl.removeAttribute('src');
this.videoEl.load();
}
if (closingItem && shouldResetStatus) {
if (closingItem.finished) {
await this.plugin.updateStatus(closingItem.id, 'completed');
} else {
await this.plugin.updateStatus(closingItem.id, 'pending');
}
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;
if (this.plugin.mainView === this) {
this.plugin.mainView = null;
}
this.contentEl.empty();
}
refreshMobileOverlay() {
const container = this.videoWrapperEl || this.contentEl;
const shouldShow = this.shouldUseTouchOverlay();
@ -3534,15 +3609,18 @@ class CrossPlayerMainView extends ItemView {
// 2. Save current position if we have an active item
if (this.currentItem) {
this.currentItem.position = this.videoEl.currentTime;
await this.plugin.updatePosition(this.currentItem.id, this.videoEl.currentTime, true);
await this.syncCurrentItemDuration();
await this.persistCurrentPlaybackPosition(true);
await this.syncCompletionStatusFromPlayback();
}
// 3. Clear sources and listeners
this.videoEl.removeAttribute('src');
this.videoEl.load();
}
this.activeMediaSrc = null;
this.currentItem = null;
this.lastPositionPersist = 0;
this.showIdlePlaceholder();
}
@ -3601,6 +3679,7 @@ class CrossPlayerMainView extends ItemView {
// Setup listeners BEFORE setting src
this.videoEl.onloadedmetadata = () => {
void this.syncCurrentItemDuration();
this.updateOverlayProgress();
// Logic for embedded subtitles:
// If no sidecar was found, we try to enable the first available embedded track.
@ -3614,6 +3693,10 @@ class CrossPlayerMainView extends ItemView {
}
}
};
this.videoEl.ondurationchange = () => {
void this.syncCurrentItemDuration();
this.updateOverlayProgress();
};
this.videoEl.onerror = () => {
console.error("Video playback error", this.videoEl?.error);
@ -3622,7 +3705,9 @@ class CrossPlayerMainView extends ItemView {
const file = this.plugin.app.vault.getAbstractFileByPath(item.path);
if (file instanceof TFile) {
this.videoEl.src = this.plugin.app.vault.getResourcePath(file);
const resourcePath = this.plugin.app.vault.getResourcePath(file);
this.activeMediaSrc = resourcePath;
this.videoEl.src = resourcePath;
} else {
console.error("File not found for playback:", item.path);
this.showIdlePlaceholder();
@ -3633,6 +3718,7 @@ class CrossPlayerMainView extends ItemView {
const resumePosition = item.position > 2 ? item.position - 2 : item.position;
this.videoEl.currentTime = resumePosition || 0;
this.videoEl.playbackRate = this.plugin.data.playbackSpeed || 1.0;
this.lastPositionPersist = 0;
this.applyAudioSettings();
// Handle Audio vs Video UI