@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
371 lines (305 loc) • 10.1 kB
text/typescript
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
)
);
}