UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

371 lines (305 loc) • 10.1 kB
import { BehaviorSubject, Observable } from "rxjs"; import { distinctUntilChanged } from "rxjs/operators"; import { Player } from "../player"; import { createLogger, utilsLogger } from "../../../logger"; import { appStore } from "@applicaster/zapp-react-native-redux/AppStore"; import { findPluginByIdentifier, loadFeedAndPrefetchThumbnailImage, parseTimeToSeconds, retrieveFeedUrl, retrieveOverlayDuration, retrieveOverlayTriggerOffset, } from "./utils"; export const { log_verbose, log_debug, log_info, log_error } = createLogger({ category: "ChapterMarkers", subsystem: "General", parent: utilsLogger, }); type ActionChapter = { type: string; options?: { title: string; }; }; type ChapterMarkerOriginal = { id: string; title: string; start_time: string; end_time: string; actions: ActionChapter[]; }; export type ChapterMarkerEvent = { id: string; title: string; start_time: number; end_time: number; actions: ActionChapter[]; }; export type TitleSummaryEvent = { title: string | number; summary: string | number; }; type ChapterMarkersObserverProps = { player: Player; }; type PlayNextConfig = { entry: ZappEntry; duration: number; triggerOffset: number; // By default, it's from the end of the video offsetIsFromStart: boolean; }; export type PlayNextState = PlayNextConfig & { triggerTime: number; handleUserCancelPlayNext: () => void; }; export class OverlaysObserver { readonly chapterSubject: BehaviorSubject<ChapterMarkerEvent>; private playNextSubject: BehaviorSubject<PlayNextState>; private titleSummarySubject: BehaviorSubject<TitleSummaryEvent>; private feedUrl: string; private reloadData: () => void; private updateTitleAndDescription: (data: any) => void; private feedDataInterval: any; private releasePlayerObserver?: () => void; readonly entry: ZappEntry; private chapterMarkerEvents: ChapterMarkerEvent[]; readonly player: Player; private playNextConfig?: PlayNextConfig; private isCanceledByUser = false; constructor({ player }: ChapterMarkersObserverProps) { this.chapterSubject = new BehaviorSubject(null); this.playNextSubject = new BehaviorSubject(null); this.titleSummarySubject = new BehaviorSubject<TitleSummaryEvent>({ title: player.getEntry()?.title || "", summary: player.getEntry()?.summary || "", }); this.entry = player.getEntry(); this.player = player; this.chapterMarkerEvents = this.prepareChapterMarkers(); this.releasePlayerObserver = this.subscribeToPlayerEvents(); this.feedUrl = ""; this.reloadData = () => {}; this.updateTitleAndDescription = () => {}; this.feedDataInterval = null; void this.preparePlayNext(); } private setupFeedDataInterval(interval: number) { if (this.feedUrl && this.reloadData && this.updateTitleAndDescription) { this.feedDataInterval = setInterval(() => { this.reloadData(); }, interval * 1000); } } public clearFeedDataInterval() { if (this.feedDataInterval) { clearInterval(this.feedDataInterval); this.feedDataInterval = null; } } public setFeedDataHandlers( feedUrl: string, reloadData: () => void, interval: number, updateTitleAndDescription: (data: any) => void ) { this.feedUrl = feedUrl; this.reloadData = reloadData; this.updateTitleAndDescription = updateTitleAndDescription; this.setupFeedDataInterval(interval); } public getTitleSummaryObservable(): Observable<TitleSummaryEvent> { return this.titleSummarySubject.asObservable().pipe(distinctUntilChanged()); } handleUserCancelPlayNext = () => { this.isCanceledByUser = true; this.playNextSubject.next(null); }; preparePlayNext = async () => { try { const plugins = appStore.get("plugins"); const playNextPlugin: any = findPluginByIdentifier( "QuickBrickPlayNextOverlay", plugins ); if (!playNextPlugin) { log_debug( "preparePlayNext: Play next plugin is not available. Skipping..." ); return; } const playNextFeedUrl: string = retrieveFeedUrl(this.entry); if (!playNextFeedUrl || typeof playNextFeedUrl !== "string") { log_debug( "preparePlayNext: Play next feed URL is not available. Skipping..." ); return; } log_debug( `preparePlayNext: Loading play next observer with url: ${playNextFeedUrl}` ); const playNextEntry = await loadFeedAndPrefetchThumbnailImage( playNextFeedUrl, this.entry, playNextPlugin ); log_info( `preparePlayNext: play next url was successfully loaded title: ${playNextEntry.title} with url: ${playNextFeedUrl}` ); const playNextChapterMarker = this.chapterMarkerEvents?.find((chapter) => chapter.actions?.some((action) => action.type === "show_play_next") ); const chapterDuration = playNextChapterMarker ? playNextChapterMarker.end_time - playNextChapterMarker.start_time : null; this.playNextConfig = { entry: playNextEntry, duration: chapterDuration || retrieveOverlayDuration(playNextPlugin), triggerOffset: playNextChapterMarker ? playNextChapterMarker.start_time : retrieveOverlayTriggerOffset(this.entry), offsetIsFromStart: !!playNextChapterMarker, }; } catch (error) { // TODO: need to improve error message handling const errorMessage = error?.message ? typeof error.message === "function" ? error.message() : error.message : "no readable error message"; const code = error?.code || "no readable code"; log_error( `preparePlayNext: loading failed with error: ${errorMessage} code: ${code}. Play next observer, will not be executed`, { error, } ); this.playNextSubject.next(null); } }; // TODO: Hack for video end, will be replaced with playlist prev/next in the future getPlayNextEntry = () => !this.isCanceledByUser ? this.playNextConfig?.entry : null; prepareChapterMarkers = () => { const chapterMarkers: ChapterMarkerOriginal[] = this.entry?.extensions?.chapter_markers?.chapters; if (!chapterMarkers || !Array.isArray(chapterMarkers)) { return []; } try { const events = chapterMarkers.map((chapter: ChapterMarkerOriginal) => { const startTime = parseTimeToSeconds(chapter.start_time); const endTime = parseTimeToSeconds(chapter.end_time); return { ...chapter, start_time: startTime, end_time: endTime, }; }); return events; } catch (error) { // TODO: need to improve error message handling const errorMessage = error?.message ? typeof error.message === "function" ? error.message() : error.message : "no readable error message"; const code = error?.code || "no readable code"; log_error( `prepareChapterMarkers: preparation failed with error: ${errorMessage} code: ${code}.`, { error, chapterMarkers, } ); return []; } }; checkPlayNext = (event: QuickBrickPlayer.OnTimeUpdateEvent) => { if (!this.playNextConfig) { return; } if (this.player.isAd()) { return; } if (this.player.isLive()) { return; } const currentTime = event.currentTime; if (!currentTime) { return; } const duration = event.duration; const triggerTime = Math.min( Math.abs(duration - this.playNextConfig.duration), Math.abs( this.playNextConfig.offsetIsFromStart ? this.playNextConfig.triggerOffset : duration - this.playNextConfig.triggerOffset ) ) - 1; if (!triggerTime) { return; } const shouldPlayNextBeVisible = currentTime > triggerTime; if (shouldPlayNextBeVisible) { if (this.isCanceledByUser) { return; } this.playNextSubject.next({ ...this.playNextConfig, triggerTime, handleUserCancelPlayNext: this.handleUserCancelPlayNext, }); } else { this.playNextSubject.next(null); this.isCanceledByUser = false; } }; onVideoProgress = (event: QuickBrickPlayer.OnTimeUpdateEvent) => { const currentTime = event.currentTime; const currentChapter = this.chapterMarkerEvents.find((chapter) => { const startTime = chapter.start_time; const endTime = chapter.end_time; return currentTime >= startTime && currentTime <= endTime; }); // Play next this.checkPlayNext(event); // Chapter Markers this.onChapterReached(currentChapter || null); }; onPlayerClose = () => { this.chapterSubject.complete(); this.playNextSubject.complete(); this.titleSummarySubject.complete(); this.releasePlayerObserver?.(); this.clearFeedDataInterval(); this.releasePlayerObserver = null; }; subscribeToPlayerEvents = () => this.player.addListener({ id: "chapterMarkersObserver", listener: { onVideoProgress: this.onVideoProgress, onPlayerClose: this.onPlayerClose, }, }); public onChapterReached = (chapterEvent: ChapterMarkerEvent) => { this.chapterSubject.next(chapterEvent); }; public getChapterMarkerObservable = (): Observable<ChapterMarkerEvent> => { if (!this.chapterSubject) { return null; } // https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged return this.chapterSubject.pipe(distinctUntilChanged()); }; // https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged public getPlayNextObservable = (): Observable<PlayNextState> => this.playNextSubject.pipe( distinctUntilChanged( (prev, curr) => prev?.triggerTime === curr?.triggerTime ) ); }