Skip to content

Commit 92e3514

Browse files
committed
fix: stabilize seek controls across track changes
Refs #4
1 parent 0d103ad commit 92e3514

3 files changed

Lines changed: 298 additions & 66 deletions

File tree

src/core/player/mpv.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,12 @@ export class MpvPlayerBackend implements PlayerBackend {
158158
throw error;
159159
}
160160

161+
this.idleActive = false;
161162
this.snapshot = {
162163
...this.snapshot,
163164
currentPath: filePath,
164165
errorMessage: null,
165-
status: this.idleActive ? "stopped" : "playing",
166+
status: "playing",
166167
timePositionSeconds: this.snapshot.timePositionSeconds ?? 0,
167168
};
168169
this.emit({
@@ -406,17 +407,21 @@ export class MpvPlayerBackend implements PlayerBackend {
406407
}
407408

408409
if (response.event === "end-file") {
409-
this.snapshot = {
410-
...this.snapshot,
411-
status: "stopped",
412-
timePositionSeconds: this.snapshot.durationSeconds,
413-
};
414-
this.emit({
415-
snapshot: this.snapshot,
416-
type: "state",
417-
});
410+
const reason = response.reason ?? "unknown";
411+
if (reason !== "stop" && reason !== "redirect") {
412+
this.snapshot = {
413+
...this.snapshot,
414+
status: "stopped",
415+
timePositionSeconds:
416+
reason === "eof" ? this.snapshot.durationSeconds : this.snapshot.timePositionSeconds,
417+
};
418+
this.emit({
419+
snapshot: this.snapshot,
420+
type: "state",
421+
});
422+
}
418423
this.emit({
419-
reason: response.reason ?? "unknown",
424+
reason,
420425
type: "ended",
421426
});
422427
}

src/core/player/session.ts

Lines changed: 79 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export class PlaylistPlayerSession {
5959

6060
private selectedIndex = 0;
6161

62+
private operationQueue: Promise<void> = Promise.resolve();
63+
6264
private readonly unsubscribeBackend: () => void;
6365

6466
constructor(
@@ -188,72 +190,86 @@ export class PlaylistPlayerSession {
188190
}
189191

190192
async playSelected() {
191-
await this.playIndex(this.selectedIndex);
193+
const selectedIndex = this.selectedIndex;
194+
await this.enqueueOperation(async () => {
195+
await this.playIndex(selectedIndex);
196+
});
192197
}
193198

194199
async playNext() {
195-
const nextIndex = this.getAdjacentIndex(1);
196-
if (nextIndex === null) {
197-
return;
198-
}
200+
await this.enqueueOperation(async () => {
201+
const nextIndex = this.getAdjacentIndex(1);
202+
if (nextIndex === null) {
203+
return;
204+
}
199205

200-
await this.playIndex(nextIndex);
206+
await this.playIndex(nextIndex);
207+
});
201208
}
202209

203210
async playPrevious() {
204-
const previousIndex = this.getAdjacentIndex(-1);
205-
if (previousIndex === null) {
206-
return;
207-
}
211+
await this.enqueueOperation(async () => {
212+
const previousIndex = this.getAdjacentIndex(-1);
213+
if (previousIndex === null) {
214+
return;
215+
}
208216

209-
await this.playIndex(previousIndex);
217+
await this.playIndex(previousIndex);
218+
});
210219
}
211220

212221
async togglePause() {
213-
if (this.playlist.length === 0) {
214-
return;
215-
}
216-
217-
const { status } = this.backend.getSnapshot();
218-
if (status === "stopped") {
219-
const restartIndex = this.currentIndex ?? this.selectedIndex;
220-
await this.playIndex(restartIndex);
221-
return;
222-
}
222+
const selectedIndex = this.selectedIndex;
223+
await this.enqueueOperation(async () => {
224+
if (this.playlist.length === 0) {
225+
return;
226+
}
223227

224-
try {
225-
this.clearError();
226-
await this.backend.togglePause();
227-
} catch (error) {
228-
this.reportError(error);
229-
}
228+
const { status } = this.backend.getSnapshot();
229+
if (status === "stopped") {
230+
const restartIndex = this.currentIndex ?? selectedIndex;
231+
await this.playIndex(restartIndex);
232+
return;
233+
}
234+
235+
try {
236+
this.clearError();
237+
await this.backend.togglePause();
238+
} catch (error) {
239+
this.reportError(error);
240+
}
241+
});
230242
}
231243

232244
async seekBy(seconds: number) {
233-
if (seconds === 0) {
234-
return;
235-
}
236-
237-
const { status } = this.backend.getSnapshot();
238-
if (status === "stopped") {
239-
return;
240-
}
245+
await this.enqueueOperation(async () => {
246+
if (seconds === 0) {
247+
return;
248+
}
241249

242-
try {
243-
this.clearError();
244-
await this.backend.seekBy(seconds);
245-
} catch (error) {
246-
this.reportError(error);
247-
}
250+
const { status } = this.backend.getSnapshot();
251+
if (status === "stopped") {
252+
return;
253+
}
254+
255+
try {
256+
this.clearError();
257+
await this.backend.seekBy(seconds);
258+
} catch (error) {
259+
this.reportError(error);
260+
}
261+
});
248262
}
249263

250264
async stop() {
251-
try {
252-
this.clearError();
253-
await this.backend.stop();
254-
} catch (error) {
255-
this.reportError(error);
256-
}
265+
await this.enqueueOperation(async () => {
266+
try {
267+
this.clearError();
268+
await this.backend.stop();
269+
} catch (error) {
270+
this.reportError(error);
271+
}
272+
});
257273
}
258274

259275
reportError(error: unknown) {
@@ -317,7 +333,7 @@ export class PlaylistPlayerSession {
317333
return nextIndex;
318334
}
319335

320-
private async handleBackendEvent(event: PlayerBackendEvent) {
336+
private handleBackendEvent(event: PlayerBackendEvent) {
321337
switch (event.type) {
322338
case "state":
323339
this.emit();
@@ -326,18 +342,26 @@ export class PlaylistPlayerSession {
326342
this.reportError(event.error);
327343
return;
328344
case "ended":
329-
if (event.reason === "eof") {
330-
const nextIndex = this.getAdjacentIndex(1);
331-
if (nextIndex !== null) {
332-
await this.playIndex(nextIndex);
333-
return;
345+
void this.enqueueOperation(async () => {
346+
if (event.reason === "eof") {
347+
const nextIndex = this.getAdjacentIndex(1);
348+
if (nextIndex !== null) {
349+
await this.playIndex(nextIndex);
350+
return;
351+
}
334352
}
335-
}
336353

337-
this.emit();
354+
this.emit();
355+
});
338356
}
339357
}
340358

359+
private enqueueOperation(operation: () => Promise<void>) {
360+
const queuedOperation = this.operationQueue.then(operation, operation);
361+
this.operationQueue = queuedOperation.catch(() => {});
362+
return queuedOperation;
363+
}
364+
341365
private syncSelectionToSearch() {
342366
const matchedIndex = findTrackIndexByQuery(this.playlist, this.searchQuery);
343367
if (matchedIndex !== -1) {

0 commit comments

Comments
 (0)