mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
fix(portals): release the resume latch when the target cannot be reached
Carrying a position into a shorter cut of the same film — two hours into a 90-minute source — leaves the engine unable to ever report that time, so the one-shot latch never released: every position save was suppressed for the rest of the session, and the impossible start time kept being reported to multi-source for the next switch. The latch now also opens when a known duration puts the requested point out of reach, while a reachable one still waits as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7459f993e2
commit
0c891f13d8
3 files changed
+51
-11
No files matched your search
@@ -304,7 +304,10 @@ the save throttle and applying it would visibly rewind.
|
||||
|
||||
A resuming engine can emit a `timeupdate` at ~0 before it finishes seeking.
|
||||
`VodDetailsPlaybackService` guards this with a one-shot `resumeSettled` latch —
|
||||
a filter would have broken deliberate seek-backwards. `handleInlineTimeUpdate`
|
||||
a filter would have broken deliberate seek-backwards. The latch also releases
|
||||
when the target is out of reach: switching a two-hour position into a
|
||||
90-minute cut means the engine can never report it, and waiting would suppress
|
||||
every save for the rest of the session. `handleInlineTimeUpdate`
|
||||
returns that verdict and the route feeds multi-source the `startTime` it asked
|
||||
for until the engine gets there, so a switch or a failure during the initial
|
||||
seek does not resolve the next source at zero and restart the film. One latch
|
||||
|
||||
@@ -158,6 +158,43 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
expect(addRecentItem).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('gives up on a resume point the source cannot reach', () => {
|
||||
// Carrying a two-hour position into a 90-minute cut: the engine can
|
||||
// never report that time, so latching on it would suppress every save
|
||||
// for the whole session and keep re-reporting the impossible start.
|
||||
service.inlinePlayback.set({
|
||||
streamUrl: 'https://example.com/short-cut.mkv',
|
||||
title: 'Example Movie',
|
||||
startTime: 7200,
|
||||
contentInfo: {
|
||||
playlistId: ROUTE_PLAYLIST,
|
||||
contentXtreamId: ROUTE_VOD_ID,
|
||||
contentType: 'vod',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
service.handleInlineTimeUpdate({ currentTime: 12, duration: 5400 })
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('still waits for a resume point the source can reach', () => {
|
||||
service.inlinePlayback.set({
|
||||
streamUrl: 'https://example.com/full.mkv',
|
||||
title: 'Example Movie',
|
||||
startTime: 2538,
|
||||
contentInfo: {
|
||||
playlistId: ROUTE_PLAYLIST,
|
||||
contentXtreamId: ROUTE_VOD_ID,
|
||||
contentType: 'vod',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
service.handleInlineTimeUpdate({ currentTime: 12, duration: 7744 })
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
describe('position updates from the bridge', () => {
|
||||
function emit(playlistId: string, contentXtreamId: number, at: number) {
|
||||
positionListener?.({
|
||||
|
||||
@@ -31,10 +31,9 @@ export interface VodDetailsPlaybackBindings {
|
||||
/** Usable metadata of the selected VOD, if any */
|
||||
vodInfo: Signal<XtreamVodInfo | null>;
|
||||
/**
|
||||
* The source actually playing when it is NOT the route's own — supplied by
|
||||
* multi-source. An external player launched for an alternative carries
|
||||
* that playlist's ids, so without this the session belongs to no page and
|
||||
* its Stop button never appears.
|
||||
* The source playing when it is NOT the route's own. An external player
|
||||
* launched for an alternative carries that playlist's ids, so without
|
||||
* this the session belongs to no page and never offers Stop.
|
||||
*/
|
||||
activeSource?: Signal<PlayerContentInfo | null>;
|
||||
}
|
||||
@@ -56,9 +55,7 @@ export class VodDetailsPlaybackService {
|
||||
private readonly logger = createLogger('VodDetailsPlayback');
|
||||
|
||||
/** Signals bound from the host component via `bind()` */
|
||||
private readonly bindings = signal<VodDetailsPlaybackBindings | null>(
|
||||
null
|
||||
);
|
||||
private readonly bindings = signal<VodDetailsPlaybackBindings | null>(null);
|
||||
private lastSaveTime = 0;
|
||||
/** Cleared on every start; latches once playback reaches startTime. */
|
||||
private resumeSettled = false;
|
||||
@@ -323,7 +320,11 @@ export class VodDetailsPlaybackService {
|
||||
// is still saved normally.
|
||||
if (!this.resumeSettled) {
|
||||
const startTime = playback.startTime ?? 0;
|
||||
if (startTime > 0 && event.currentTime < startTime - 5)
|
||||
// A shorter cut can never reach a position carried into it, and
|
||||
// latching on that would suppress every save for the session.
|
||||
const reachable =
|
||||
!(event.duration > 0) || startTime < event.duration;
|
||||
if (reachable && startTime > 0 && event.currentTime < startTime - 5)
|
||||
return false;
|
||||
this.resumeSettled = true;
|
||||
}
|
||||
@@ -380,8 +381,7 @@ export class VodDetailsPlaybackService {
|
||||
* engine survive and simply re-seek to `playback.startTime`.
|
||||
*/
|
||||
startResolvedPlayback(playback: ResolvedPortalPlayback): void {
|
||||
// Same movie, different source: still a view, and the picker and
|
||||
// pin paths would otherwise leave it out of Recently Viewed.
|
||||
// Same movie, different source: still a view.
|
||||
this.addToRecentlyViewed();
|
||||
this.startPlayback(playback);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user