mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
fix(playback): address inline collection review feedback
This commit is contained in:
1 parent
b12197c520
commit
18ee78c433
7 files changed
+276
-203
No files matched your search
@@ -2,6 +2,8 @@ import { PlaybackPositionData, XtreamSerieEpisode } from 'shared-interfaces';
|
||||
import {
|
||||
SERIES_QUICK_START_ACTION_KIND,
|
||||
getSeriesQuickStartAction,
|
||||
getPositiveInteger,
|
||||
padEpisodePart,
|
||||
} from './series-quick-start';
|
||||
|
||||
function episode(
|
||||
@@ -243,3 +245,17 @@ describe('getSeriesQuickStartAction', () => {
|
||||
expect(action).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('episode label helpers', () => {
|
||||
it('normalizes only positive integers', () => {
|
||||
expect(getPositiveInteger(1)).toBe(1);
|
||||
expect(getPositiveInteger(0)).toBeNull();
|
||||
expect(getPositiveInteger(1.5)).toBeNull();
|
||||
expect(getPositiveInteger(Number.NaN)).toBeNull();
|
||||
});
|
||||
|
||||
it('pads single digit episode label parts', () => {
|
||||
expect(padEpisodePart(1)).toBe('01');
|
||||
expect(padEpisodePart(10)).toBe('10');
|
||||
});
|
||||
});
|
||||
@@ -116,7 +116,9 @@ function createQuickStartAction(
|
||||
};
|
||||
}
|
||||
|
||||
function getOrderedEpisodes(request: SeriesQuickStartRequest): OrderedEpisode[] {
|
||||
function getOrderedEpisodes(
|
||||
request: SeriesQuickStartRequest
|
||||
): OrderedEpisode[] {
|
||||
const orderedEpisodes: OrderedEpisode[] = [];
|
||||
|
||||
Object.entries(request.seasons)
|
||||
@@ -186,10 +188,10 @@ function getEpisodeLabel(episode: XtreamSerieEpisode): string {
|
||||
return title ? `${episodeCode} · ${title}` : episodeCode;
|
||||
}
|
||||
|
||||
function getPositiveInteger(value: number): number | null {
|
||||
export function getPositiveInteger(value: number): number | null {
|
||||
return Number.isInteger(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
function padEpisodePart(value: number): string {
|
||||
export function padEpisodePart(value: number): string {
|
||||
return value < 10 ? `0${value}` : String(value);
|
||||
}
|
||||
@@ -191,7 +191,10 @@ describe('StalkerCollectionDetailComponent', () => {
|
||||
})
|
||||
.overrideComponent(StalkerCollectionDetailComponent, {
|
||||
remove: {
|
||||
imports: [ContentHeroComponent, StalkerInlineDetailComponent],
|
||||
imports: [
|
||||
ContentHeroComponent,
|
||||
StalkerInlineDetailComponent,
|
||||
],
|
||||
},
|
||||
add: {
|
||||
imports: [
|
||||
@@ -230,7 +233,9 @@ describe('StalkerCollectionDetailComponent', () => {
|
||||
expect(stalkerStore.setSelectedContentType).toHaveBeenLastCalledWith(
|
||||
'vod'
|
||||
);
|
||||
expect(stalkerStore.setSelectedCategory).toHaveBeenLastCalledWith('vod');
|
||||
expect(stalkerStore.setSelectedCategory).toHaveBeenLastCalledWith(
|
||||
'vod'
|
||||
);
|
||||
expect(stalkerStore.setSelectedItem).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
id: '1507',
|
||||
@@ -351,6 +356,39 @@ describe('StalkerCollectionDetailComponent', () => {
|
||||
expect(portalPlayer.openResolvedPlayback).not.toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.inlinePlayback()).toEqual(playback);
|
||||
});
|
||||
|
||||
it('does not load VOD playback position when the playlist id is missing', async () => {
|
||||
const playlistsService = TestBed.inject(PlaylistsService) as {
|
||||
getPlaylistById: jest.Mock;
|
||||
};
|
||||
playlistsService.getPlaylistById.mockReturnValue(
|
||||
of({
|
||||
...playlist,
|
||||
_id: '',
|
||||
})
|
||||
);
|
||||
|
||||
fixture.componentRef.setInput(
|
||||
'item',
|
||||
buildCollectionItem({
|
||||
contentType: 'movie',
|
||||
categoryId: 'vod',
|
||||
stalkerItem: {
|
||||
id: '1702',
|
||||
title: 'Collection Movie Without Playlist',
|
||||
category_id: 'vod',
|
||||
cmd: '/media/file_1702.mpg',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await settleDetail(fixture);
|
||||
|
||||
expect(playbackPositions.getPlaybackPosition).not.toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.selectedVodPlaybackPosition()).toBe(
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function buildCollectionItem(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { ContentHeroComponent } from 'components';
|
||||
import {
|
||||
buildStalkerStateItem,
|
||||
createLogger,
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
PORTAL_PLAYBACK_POSITIONS,
|
||||
PORTAL_PLAYER,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
} from 'shared-interfaces';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { StalkerInlineDetailComponent } from './stalker-inline-detail/stalker-inline-detail.component';
|
||||
import { StalkerVodPlaybackController } from './stalker-vod-playback-controller';
|
||||
|
||||
interface StalkerCollectionStateSnapshot {
|
||||
currentPlaylist: Playlist | undefined;
|
||||
@@ -114,6 +116,7 @@ export class StalkerCollectionDetailComponent {
|
||||
private readonly portalPlayer = inject(PORTAL_PLAYER);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly logger = createLogger('StalkerCollectionDetail');
|
||||
private readonly originalState = this.captureStoreState();
|
||||
private readonly favoritesRefresh = createRefreshTrigger();
|
||||
|
||||
@@ -145,7 +148,16 @@ export class StalkerCollectionDetailComponent {
|
||||
);
|
||||
|
||||
private initRequestId = 0;
|
||||
private lastInlineSaveTime = 0;
|
||||
private readonly vodPlayback = new StalkerVodPlaybackController({
|
||||
inlinePlayback: this.inlinePlayback,
|
||||
selectedVodPosition: this.selectedVodPosition,
|
||||
playbackPositions: this.playbackPositions,
|
||||
portalPlayer: this.portalPlayer,
|
||||
snackBar: this.snackBar,
|
||||
translateService: this.translateService,
|
||||
logger: this.logger,
|
||||
playbackErrorLogMessage: 'Failed to start collection VOD playback',
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -224,50 +236,19 @@ export class StalkerCollectionDetailComponent {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}): void {
|
||||
const playback = this.inlinePlayback();
|
||||
if (!playback?.contentInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastInlineSaveTime <= 15000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastInlineSaveTime = now;
|
||||
const position: PlaybackPositionData = {
|
||||
...playback.contentInfo,
|
||||
positionSeconds: Math.floor(event.currentTime),
|
||||
durationSeconds: Math.floor(event.duration),
|
||||
};
|
||||
|
||||
void this.playbackPositions.savePlaybackPosition(
|
||||
playback.contentInfo.playlistId,
|
||||
position
|
||||
);
|
||||
this.selectedVodPosition.set(position);
|
||||
this.vodPlayback.handleInlineTimeUpdate(event);
|
||||
}
|
||||
|
||||
closeInlinePlayer(): void {
|
||||
this.inlinePlayback.set(null);
|
||||
this.lastInlineSaveTime = 0;
|
||||
this.vodPlayback.closeInlinePlayer();
|
||||
}
|
||||
|
||||
showCopyNotification(): void {
|
||||
this.snackBar.open(
|
||||
this.translateService.instant('PORTALS.STREAM_URL_COPIED'),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
this.vodPlayback.showCopyNotification();
|
||||
}
|
||||
|
||||
handleExternalFallbackRequest(request: PlaybackFallbackRequest): void {
|
||||
void this.portalPlayer.openExternalPlayback(
|
||||
request.playback,
|
||||
request.player
|
||||
);
|
||||
this.vodPlayback.handleExternalFallbackRequest(request);
|
||||
}
|
||||
|
||||
private async prepareDetail(
|
||||
@@ -304,7 +285,9 @@ export class StalkerCollectionDetailComponent {
|
||||
);
|
||||
|
||||
this.detailCategoryOverride.set(detailMode.category);
|
||||
this.stalkerStore.setSelectedContentType(detailMode.selectedContentType);
|
||||
this.stalkerStore.setSelectedContentType(
|
||||
detailMode.selectedContentType
|
||||
);
|
||||
this.stalkerStore.setSelectedCategory(
|
||||
this.resolveSelectedCategory(item, stalkerItem, detailMode)
|
||||
);
|
||||
@@ -379,11 +362,8 @@ export class StalkerCollectionDetailComponent {
|
||||
item: UnifiedCollectionItem,
|
||||
stalkerItem: StalkerPortalItem
|
||||
): StalkerCollectionDetailMode {
|
||||
const hasEmbeddedSeries = Array.isArray(
|
||||
(stalkerItem as { series?: unknown[] }).series
|
||||
)
|
||||
? (stalkerItem as { series?: unknown[] }).series!.length > 0
|
||||
: false;
|
||||
const series = (stalkerItem as { series?: unknown[] }).series;
|
||||
const hasEmbeddedSeries = Array.isArray(series) && series.length > 0;
|
||||
const isVodSeries = isStalkerSeriesFlag(
|
||||
(stalkerItem as { is_series?: unknown }).is_series
|
||||
);
|
||||
@@ -420,7 +400,9 @@ export class StalkerCollectionDetailComponent {
|
||||
return 'vod';
|
||||
}
|
||||
|
||||
return categoryId ?? toStalkerCategoryId(detailMode.selectedContentType);
|
||||
return (
|
||||
categoryId ?? toStalkerCategoryId(detailMode.selectedContentType)
|
||||
);
|
||||
}
|
||||
|
||||
private syncSelectedVodFavorite(): void {
|
||||
@@ -447,58 +429,24 @@ export class StalkerCollectionDetailComponent {
|
||||
thumbnail?: string,
|
||||
startTime?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
const playback =
|
||||
startTime === undefined
|
||||
? await this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail
|
||||
)
|
||||
: await this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
);
|
||||
|
||||
this.lastInlineSaveTime = 0;
|
||||
if (this.portalPlayer.isEmbeddedPlayer()) {
|
||||
this.inlinePlayback.set(playback);
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeInlinePlayer();
|
||||
void this.portalPlayer.openResolvedPlayback(playback, true);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message === 'nothing_to_play'
|
||||
? this.translateService.instant(
|
||||
'PORTALS.CONTENT_NOT_AVAILABLE'
|
||||
)
|
||||
: this.translateService.instant('PORTALS.PLAYBACK_ERROR');
|
||||
this.snackBar.open(errorMessage, null, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
await this.vodPlayback.startVodPlayback(() =>
|
||||
startTime === undefined
|
||||
? this.stalkerStore.resolveVodPlayback(cmd, title, thumbnail)
|
||||
: this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async loadSelectedVodPosition(
|
||||
playlistId: string,
|
||||
vodId: number
|
||||
): Promise<void> {
|
||||
if (!Number.isFinite(vodId)) {
|
||||
this.selectedVodPosition.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = await this.playbackPositions.getPlaybackPosition(
|
||||
playlistId,
|
||||
vodId,
|
||||
'vod'
|
||||
);
|
||||
this.selectedVodPosition.set(position ?? null);
|
||||
await this.vodPlayback.loadSelectedVodPosition(playlistId, vodId);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
isStalkerSeriesFlag,
|
||||
toggleStalkerVodFavorite,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import { StalkerVodPlaybackController } from '../stalker-vod-playback-controller';
|
||||
|
||||
interface StalkerFilter {
|
||||
key: string;
|
||||
@@ -136,7 +137,16 @@ export class StalkerSearchComponent {
|
||||
readonly selectedVodPlaybackPosition = computed<number | null>(
|
||||
() => this.selectedVodPosition()?.positionSeconds ?? null
|
||||
);
|
||||
private lastInlineSaveTime = 0;
|
||||
private readonly vodPlayback = new StalkerVodPlaybackController({
|
||||
inlinePlayback: this.inlinePlayback,
|
||||
selectedVodPosition: this.selectedVodPosition,
|
||||
playbackPositions: this.playbackPositions,
|
||||
portalPlayer: this.portalPlayer,
|
||||
snackBar: this.snackBar,
|
||||
translateService: this.translateService,
|
||||
logger: this.logger,
|
||||
playbackErrorLogMessage: 'Failed to start search VOD playback',
|
||||
});
|
||||
|
||||
readonly portalFavorites = createPortalFavoritesResource(
|
||||
this.playlistService,
|
||||
@@ -341,50 +351,19 @@ export class StalkerSearchComponent {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}): void {
|
||||
const playback = this.inlinePlayback();
|
||||
if (!playback?.contentInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastInlineSaveTime <= 15000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastInlineSaveTime = now;
|
||||
const position: PlaybackPositionData = {
|
||||
...playback.contentInfo,
|
||||
positionSeconds: Math.floor(event.currentTime),
|
||||
durationSeconds: Math.floor(event.duration),
|
||||
};
|
||||
|
||||
void this.playbackPositions.savePlaybackPosition(
|
||||
playback.contentInfo.playlistId,
|
||||
position
|
||||
);
|
||||
this.selectedVodPosition.set(position);
|
||||
this.vodPlayback.handleInlineTimeUpdate(event);
|
||||
}
|
||||
|
||||
closeInlinePlayer(): void {
|
||||
this.inlinePlayback.set(null);
|
||||
this.lastInlineSaveTime = 0;
|
||||
this.vodPlayback.closeInlinePlayer();
|
||||
}
|
||||
|
||||
showCopyNotification(): void {
|
||||
this.snackBar.open(
|
||||
this.translateService.instant('PORTALS.STREAM_URL_COPIED'),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
this.vodPlayback.showCopyNotification();
|
||||
}
|
||||
|
||||
handleExternalFallbackRequest(request: PlaybackFallbackRequest): void {
|
||||
void this.portalPlayer.openExternalPlayback(
|
||||
request.playback,
|
||||
request.player
|
||||
);
|
||||
this.vodPlayback.handleExternalFallbackRequest(request);
|
||||
}
|
||||
|
||||
removeFromFavorites(favoriteId: string, onDone?: () => void) {
|
||||
@@ -454,59 +433,24 @@ export class StalkerSearchComponent {
|
||||
thumbnail?: string,
|
||||
startTime?: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
const playback =
|
||||
startTime === undefined
|
||||
? await this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail
|
||||
)
|
||||
: await this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
);
|
||||
|
||||
this.lastInlineSaveTime = 0;
|
||||
if (this.portalPlayer.isEmbeddedPlayer()) {
|
||||
this.inlinePlayback.set(playback);
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeInlinePlayer();
|
||||
void this.portalPlayer.openResolvedPlayback(playback, true);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to start search VOD playback', error);
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message === 'nothing_to_play'
|
||||
? this.translateService.instant(
|
||||
'PORTALS.CONTENT_NOT_AVAILABLE'
|
||||
)
|
||||
: this.translateService.instant('PORTALS.PLAYBACK_ERROR');
|
||||
this.snackBar.open(errorMessage, null, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
await this.vodPlayback.startVodPlayback(() =>
|
||||
startTime === undefined
|
||||
? this.stalkerStore.resolveVodPlayback(cmd, title, thumbnail)
|
||||
: this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async loadSelectedVodPosition(
|
||||
playlistId: string,
|
||||
vodId: number
|
||||
): Promise<void> {
|
||||
if (!playlistId || !Number.isFinite(vodId)) {
|
||||
this.selectedVodPosition.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const position = await this.playbackPositions.getPlaybackPosition(
|
||||
playlistId,
|
||||
vodId,
|
||||
'vod'
|
||||
);
|
||||
this.selectedVodPosition.set(position ?? null);
|
||||
await this.vodPlayback.loadSelectedVodPosition(playlistId, vodId);
|
||||
}
|
||||
}
|
||||
+18
-24
@@ -30,8 +30,10 @@ import {
|
||||
SERIES_QUICK_START_ACTION_KIND,
|
||||
SeriesQuickStartAction,
|
||||
createLogger,
|
||||
getPositiveInteger,
|
||||
getSeriesQuickStartAction,
|
||||
getStalkerReturnToState,
|
||||
padEpisodePart,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
getVodSeriesSeasonKey,
|
||||
@@ -156,14 +158,11 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
const playlist = this.stalkerStore.currentPlaylist();
|
||||
if (item && playlist?._id) {
|
||||
const normalizedSeriesId = this.toSeriesId(item.id);
|
||||
this.logger.debug(
|
||||
'Loading positions for series',
|
||||
{
|
||||
id: item.id,
|
||||
seriesId: normalizedSeriesId,
|
||||
isSeries: item.is_series,
|
||||
}
|
||||
);
|
||||
this.logger.debug('Loading positions for series', {
|
||||
id: item.id,
|
||||
seriesId: normalizedSeriesId,
|
||||
isSeries: item.is_series,
|
||||
});
|
||||
if (!isNaN(normalizedSeriesId)) {
|
||||
void this.loadSeriesPositions(
|
||||
playlist._id,
|
||||
@@ -578,8 +577,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to start inline series playback', error);
|
||||
const errorMessage =
|
||||
error instanceof Error &&
|
||||
error.message === 'nothing_to_play'
|
||||
error instanceof Error && error.message === 'nothing_to_play'
|
||||
? this.translateService.instant(
|
||||
'PORTALS.CONTENT_NOT_AVAILABLE'
|
||||
)
|
||||
@@ -654,7 +652,8 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
const posterUrl = episodeInfo?.movie_image;
|
||||
const seasonNum = Number(episode.season || 1);
|
||||
const episodeNum = episode.episode_num || 1;
|
||||
const seriesTitle = item.info?.name || this.displayItem()?.info?.name || 'Series';
|
||||
const seriesTitle =
|
||||
item.info?.name || this.displayItem()?.info?.name || 'Series';
|
||||
const episodeTitle = `${seriesTitle} - S${String(seasonNum).padStart(
|
||||
2,
|
||||
'0'
|
||||
@@ -686,10 +685,11 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
playlistId: string,
|
||||
seriesXtreamId: number
|
||||
): Promise<void> {
|
||||
const positions = await this.playbackPositions.getSeriesPlaybackPositions(
|
||||
playlistId,
|
||||
seriesXtreamId
|
||||
);
|
||||
const positions =
|
||||
await this.playbackPositions.getSeriesPlaybackPositions(
|
||||
playlistId,
|
||||
seriesXtreamId
|
||||
);
|
||||
const positionsMap = new Map<number, PlaybackPositionData>();
|
||||
positions.forEach((position) => {
|
||||
positionsMap.set(position.contentXtreamId, position);
|
||||
@@ -774,7 +774,9 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
return `S${padEpisodePart(seasonNumber)}E01`;
|
||||
}
|
||||
|
||||
private updateEpisodePlaybackPosition(position: PlaybackPositionData): void {
|
||||
private updateEpisodePlaybackPosition(
|
||||
position: PlaybackPositionData
|
||||
): void {
|
||||
const updated = new Map(this.episodePlaybackPositions());
|
||||
updated.set(position.contentXtreamId, position);
|
||||
this.episodePlaybackPositions.set(updated);
|
||||
@@ -835,11 +837,3 @@ export class StalkerSeriesViewComponent implements OnDestroy {
|
||||
return Math.abs(hash);
|
||||
}
|
||||
}
|
||||
|
||||
function getPositiveInteger(value: number): number | null {
|
||||
return Number.isInteger(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
function padEpisodePart(value: number): string {
|
||||
return value < 10 ? `0${value}` : String(value);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { WritableSignal } from '@angular/core';
|
||||
import type { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import type { TranslateService } from '@ngx-translate/core';
|
||||
import type {
|
||||
Logger,
|
||||
PortalPlaybackPositions,
|
||||
PortalPlayer,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import type { PlaybackFallbackRequest } from '@iptvnator/ui/playback';
|
||||
import {
|
||||
PlaybackPositionData,
|
||||
ResolvedPortalPlayback,
|
||||
} from 'shared-interfaces';
|
||||
|
||||
interface StalkerVodPlaybackControllerConfig {
|
||||
inlinePlayback: WritableSignal<ResolvedPortalPlayback | null>;
|
||||
selectedVodPosition: WritableSignal<PlaybackPositionData | null>;
|
||||
playbackPositions: PortalPlaybackPositions;
|
||||
portalPlayer: PortalPlayer;
|
||||
snackBar: MatSnackBar;
|
||||
translateService: TranslateService;
|
||||
logger: Logger;
|
||||
playbackErrorLogMessage: string;
|
||||
}
|
||||
|
||||
export class StalkerVodPlaybackController {
|
||||
private lastInlineSaveTime = 0;
|
||||
|
||||
constructor(private readonly config: StalkerVodPlaybackControllerConfig) {}
|
||||
|
||||
async startVodPlayback(
|
||||
resolvePlayback: () => Promise<ResolvedPortalPlayback>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const playback = await resolvePlayback();
|
||||
|
||||
this.lastInlineSaveTime = 0;
|
||||
if (this.config.portalPlayer.isEmbeddedPlayer()) {
|
||||
this.config.inlinePlayback.set(playback);
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeInlinePlayer();
|
||||
void this.config.portalPlayer.openResolvedPlayback(playback, true);
|
||||
} catch (error) {
|
||||
this.config.logger.error(
|
||||
this.config.playbackErrorLogMessage,
|
||||
error
|
||||
);
|
||||
const errorMessage =
|
||||
error instanceof Error && error.message === 'nothing_to_play'
|
||||
? this.config.translateService.instant(
|
||||
'PORTALS.CONTENT_NOT_AVAILABLE'
|
||||
)
|
||||
: this.config.translateService.instant(
|
||||
'PORTALS.PLAYBACK_ERROR'
|
||||
);
|
||||
this.config.snackBar.open(errorMessage, null, {
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadSelectedVodPosition(
|
||||
playlistId: string,
|
||||
vodId: number
|
||||
): Promise<void> {
|
||||
if (!playlistId || !Number.isFinite(vodId)) {
|
||||
this.config.selectedVodPosition.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const position =
|
||||
await this.config.playbackPositions.getPlaybackPosition(
|
||||
playlistId,
|
||||
vodId,
|
||||
'vod'
|
||||
);
|
||||
this.config.selectedVodPosition.set(position ?? null);
|
||||
}
|
||||
|
||||
handleInlineTimeUpdate(event: {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}): void {
|
||||
const playback = this.config.inlinePlayback();
|
||||
if (!playback?.contentInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - this.lastInlineSaveTime <= 15000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastInlineSaveTime = now;
|
||||
const position: PlaybackPositionData = {
|
||||
...playback.contentInfo,
|
||||
positionSeconds: Math.floor(event.currentTime),
|
||||
durationSeconds: Math.floor(event.duration),
|
||||
};
|
||||
|
||||
void this.config.playbackPositions.savePlaybackPosition(
|
||||
playback.contentInfo.playlistId,
|
||||
position
|
||||
);
|
||||
this.config.selectedVodPosition.set(position);
|
||||
}
|
||||
|
||||
closeInlinePlayer(): void {
|
||||
this.config.inlinePlayback.set(null);
|
||||
this.lastInlineSaveTime = 0;
|
||||
}
|
||||
|
||||
showCopyNotification(): void {
|
||||
this.config.snackBar.open(
|
||||
this.config.translateService.instant('PORTALS.STREAM_URL_COPIED'),
|
||||
null,
|
||||
{
|
||||
duration: 2000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleExternalFallbackRequest(request: PlaybackFallbackRequest): void {
|
||||
void this.config.portalPlayer.openExternalPlayback(
|
||||
request.playback,
|
||||
request.player
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user