mirror of
https://github.com/imed-ghomari/cross-player-obsidian.git
synced 2026-07-22 06:55:49 +00:00
Release 1.2.4
This commit is contained in:
parent
7f1089137e
commit
4e8385ad07
6 changed files with 212 additions and 47 deletions
9
RELEASE_NOTES_1.2.4.md
Normal file
9
RELEASE_NOTES_1.2.4.md
Normal 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
108
main.js
|
|
@ -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 || "");
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
134
src/main.ts
134
src/main.ts
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue