mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
fix(xtream): ignore stale VOD positions
This commit is contained in:
1 parent
1d996b85f0
commit
5511d66651
2 files changed
+115
-4
No files matched your search
+98
-3
@@ -32,6 +32,32 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
const closeSession = jest.fn().mockResolvedValue(undefined);
|
||||
const openResolvedPlayback = jest.fn();
|
||||
const activeSource = signal<PlayerContentInfo | null>(null);
|
||||
const currentPlaylist = signal({ id: ROUTE_PLAYLIST });
|
||||
const routeVodId = signal(ROUTE_VOD_ID);
|
||||
const getPlaybackPosition = jest.fn();
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
});
|
||||
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function positionFor(
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
positionSeconds: number
|
||||
): PlaybackPositionData {
|
||||
return {
|
||||
playlistId,
|
||||
contentXtreamId,
|
||||
contentType: 'vod',
|
||||
positionSeconds,
|
||||
durationSeconds: 7744,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionFor(playlistId: string, contentXtreamId: number) {
|
||||
return {
|
||||
@@ -48,8 +74,12 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
beforeEach(() => {
|
||||
activeSession.set(null);
|
||||
activeSource.set(null);
|
||||
currentPlaylist.set({ id: ROUTE_PLAYLIST });
|
||||
routeVodId.set(ROUTE_VOD_ID);
|
||||
positionListener = undefined;
|
||||
addRecentItem.mockClear();
|
||||
getPlaybackPosition.mockReset();
|
||||
getPlaybackPosition.mockResolvedValue(null);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -57,7 +87,7 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
{
|
||||
provide: XtreamStore,
|
||||
useValue: {
|
||||
currentPlaylist: signal({ id: ROUTE_PLAYLIST }),
|
||||
currentPlaylist,
|
||||
addRecentItem,
|
||||
constructVodStreamUrl: jest
|
||||
.fn()
|
||||
@@ -71,7 +101,7 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
{
|
||||
provide: PORTAL_PLAYBACK_POSITIONS,
|
||||
useValue: {
|
||||
getPlaybackPosition: jest.fn(),
|
||||
getPlaybackPosition,
|
||||
savePlaybackPosition: jest.fn(),
|
||||
},
|
||||
},
|
||||
@@ -98,7 +128,7 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
|
||||
service = TestBed.inject(VodDetailsPlaybackService);
|
||||
service.bind({
|
||||
vodId: signal(ROUTE_VOD_ID),
|
||||
vodId: routeVodId,
|
||||
vodInfo: signal(null),
|
||||
activeSource,
|
||||
});
|
||||
@@ -261,4 +291,69 @@ describe('VodDetailsPlaybackService — external session ownership', () => {
|
||||
expect(service.vodPlaybackPosition()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('stored position loads', () => {
|
||||
it('drops a result after the route changes without starting another load', async () => {
|
||||
const oldLoad = deferred<PlaybackPositionData | null>();
|
||||
getPlaybackPosition.mockReturnValueOnce(oldLoad.promise);
|
||||
const pending = service.loadPosition(ROUTE_PLAYLIST, ROUTE_VOD_ID);
|
||||
|
||||
routeVodId.set(ROUTE_VOD_ID + 1);
|
||||
oldLoad.resolve(positionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID, 120));
|
||||
await pending;
|
||||
|
||||
expect(service.vodPlaybackPosition()).toBeNull();
|
||||
expect(service.routePlaybackPosition()).toBeNull();
|
||||
});
|
||||
|
||||
it('drops a result for the same VOD id after the playlist changes', async () => {
|
||||
const oldLoad = deferred<PlaybackPositionData | null>();
|
||||
getPlaybackPosition.mockReturnValueOnce(oldLoad.promise);
|
||||
const pending = service.loadPosition(ROUTE_PLAYLIST, ROUTE_VOD_ID);
|
||||
|
||||
currentPlaylist.set({ id: 'playlist-2' });
|
||||
oldLoad.resolve(positionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID, 120));
|
||||
await pending;
|
||||
|
||||
expect(service.vodPlaybackPosition()).toBeNull();
|
||||
expect(service.routePlaybackPosition()).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the newest valid result when an older load finishes last', async () => {
|
||||
const olderLoad = deferred<PlaybackPositionData | null>();
|
||||
const newerLoad = deferred<PlaybackPositionData | null>();
|
||||
getPlaybackPosition
|
||||
.mockReturnValueOnce(olderLoad.promise)
|
||||
.mockReturnValueOnce(newerLoad.promise);
|
||||
|
||||
const olderPending = service.loadPosition(
|
||||
ROUTE_PLAYLIST,
|
||||
ROUTE_VOD_ID
|
||||
);
|
||||
const newerPending = service.loadPosition(
|
||||
ROUTE_PLAYLIST,
|
||||
ROUTE_VOD_ID
|
||||
);
|
||||
newerLoad.resolve(positionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID, 300));
|
||||
await newerPending;
|
||||
olderLoad.resolve(positionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID, 120));
|
||||
await olderPending;
|
||||
|
||||
expect(service.vodPlaybackPosition()?.positionSeconds).toBe(300);
|
||||
expect(service.routePlaybackPosition()?.positionSeconds).toBe(300);
|
||||
});
|
||||
|
||||
it('does not publish a pending result after destruction', async () => {
|
||||
const oldLoad = deferred<PlaybackPositionData | null>();
|
||||
getPlaybackPosition.mockReturnValueOnce(oldLoad.promise);
|
||||
const pending = service.loadPosition(ROUTE_PLAYLIST, ROUTE_VOD_ID);
|
||||
|
||||
TestBed.resetTestingModule();
|
||||
oldLoad.resolve(positionFor(ROUTE_PLAYLIST, ROUTE_VOD_ID, 120));
|
||||
await pending;
|
||||
|
||||
expect(service.vodPlaybackPosition()).toBeNull();
|
||||
expect(service.routePlaybackPosition()).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,8 @@ export class VodDetailsPlaybackService {
|
||||
PlaybackPositionRuntimeBridgeService
|
||||
);
|
||||
private readonly logger = createLogger('VodDetailsPlayback');
|
||||
private positionLoadGeneration = 0;
|
||||
private destroyed = false;
|
||||
|
||||
/** Signals bound from the host component via `bind()` */
|
||||
private readonly bindings = signal<VodDetailsPlaybackBindings | null>(null);
|
||||
@@ -147,7 +149,11 @@ export class VodDetailsPlaybackService {
|
||||
}
|
||||
) ?? null;
|
||||
|
||||
inject(DestroyRef).onDestroy(() => unsubscribePositionUpdates?.());
|
||||
inject(DestroyRef).onDestroy(() => {
|
||||
this.destroyed = true;
|
||||
this.positionLoadGeneration++;
|
||||
unsubscribePositionUpdates?.();
|
||||
});
|
||||
}
|
||||
|
||||
private ownsContent(
|
||||
@@ -325,11 +331,21 @@ export class VodDetailsPlaybackService {
|
||||
}
|
||||
|
||||
async loadPosition(playlistId: string, vodId: number): Promise<void> {
|
||||
const generation = ++this.positionLoadGeneration;
|
||||
const position = await this.playbackPositions.getPlaybackPosition(
|
||||
playlistId,
|
||||
vodId,
|
||||
'vod'
|
||||
);
|
||||
if (
|
||||
this.destroyed ||
|
||||
generation !== this.positionLoadGeneration ||
|
||||
this.xtreamStore.currentPlaylist()?.id !== playlistId ||
|
||||
this.bindings()?.vodId() !== vodId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.vodPlaybackPosition.set(position);
|
||||
this.routePlaybackPosition.set(position);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user