Skip to content

Commit

Permalink
feat(signing): add playlist and media entitlement using a service
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed May 23, 2022
1 parent 7b0b0ae commit e93a655
Show file tree
Hide file tree
Showing 19 changed files with 248 additions and 101 deletions.
1 change: 1 addition & 0 deletions .commitlintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
'menu',
'payment',
'e2e',
'signing',
],
],
},
Expand Down
8 changes: 7 additions & 1 deletion src/containers/Cinema/Cinema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { PlaylistItem } from '#types/playlist';
import type { Config } from '#types/Config';
import { saveItem } from '#src/stores/WatchHistoryController';
import type { VideoProgress } from '#types/video';
import useEventCallback from '#src/hooks/useEventCallback';
import { usePlaylistItemCallback } from '#src/hooks/usePlaylistItemCallback';

type Props = {
item: PlaylistItem;
Expand All @@ -38,6 +40,9 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActi
const scriptUrl = `https://content.jwplatform.com/libraries/${config.player}.js`;
const enableWatchHistory = config.options.enableContinueWatching && !isTrailer;
const setPlayer = useOttAnalytics(item, feedId);
const playlistItemCallback = usePlaylistItemCallback();

const handlePlaylistItemCallback = useEventCallback(playlistItemCallback);

const getProgress = useCallback((): VideoProgress | null => {
if (!playerRef.current) return null;
Expand Down Expand Up @@ -158,6 +163,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActi
};

playerRef.current.on('beforePlay', handleBeforePlay);
playerRef.current.setPlaylistItemCallback(handlePlaylistItemCallback);
};

if (playerRef.current) {
Expand All @@ -167,7 +173,7 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause, onComplete, onUserActi
if (libLoaded) {
initializePlayer();
}
}, [libLoaded, item, onPlay, onPause, onUserActive, onUserInActive, onComplete, config.player, enableWatchHistory, setPlayer]);
}, [libLoaded, item, onPlay, onPause, onUserActive, onUserInActive, onComplete, config.player, enableWatchHistory, setPlayer, handlePlaylistItemCallback]);

useEffect(() => {
return () => {
Expand Down
12 changes: 3 additions & 9 deletions src/containers/Playlist/PlaylistContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import usePlaylist, { UsePlaylistResult } from '#src/hooks/usePlaylist';
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore';
import { useFavoritesStore } from '#src/stores/FavoritesStore';
import { PLAYLIST_LIMIT } from '#src/config';
import type { Playlist, PlaylistItem } from '#types/playlist';
import type { Playlist } from '#types/playlist';

type ChildrenParams = {
playlist: Playlist;
Expand All @@ -16,21 +16,19 @@ type ChildrenParams = {

type Props = {
playlistId: string;
relatedItem?: PlaylistItem;
onPlaylistUpdate?: (playlist: Playlist) => void;
children: (childrenParams: ChildrenParams) => JSX.Element;
style?: React.CSSProperties;
showEmpty?: boolean;
};

const PlaylistContainer = ({ playlistId, relatedItem, onPlaylistUpdate, style, children, showEmpty = false }: Props): JSX.Element | null => {
const PlaylistContainer = ({ playlistId, onPlaylistUpdate, style, children, showEmpty = false }: Props): JSX.Element | null => {
const isAlternativeShelf = PersonalShelves.includes(playlistId as PersonalShelf);
const {
isLoading,
error,
data: fetchedPlaylist = { title: '', playlist: [] },
}: UsePlaylistResult = usePlaylist(playlistId, relatedItem?.mediaid, !isAlternativeShelf && !!playlistId, true, PLAYLIST_LIMIT);

}: UsePlaylistResult = usePlaylist(playlistId, { page_limit: PLAYLIST_LIMIT.toString() }, !isAlternativeShelf, true);
let playlist = fetchedPlaylist;

const favoritesPlaylist = useFavoritesStore((state) => state.getPlaylist());
Expand All @@ -48,10 +46,6 @@ const PlaylistContainer = ({ playlistId, relatedItem, onPlaylistUpdate, style, c
return null;
}

if (relatedItem && !playlist.playlist.some(({ mediaid }) => mediaid === relatedItem.mediaid)) {
playlist.playlist.unshift(relatedItem);
}

return children({ playlist, isLoading, error, style });
};

Expand Down
31 changes: 31 additions & 0 deletions src/hooks/useEventCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useCallback, useLayoutEffect, useRef } from 'react';

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* The `useEventCallback` hook can be compared to the `useCallback` hook but without dependencies. It is a "shortcut"
* to prevent re-renders based on callback changes due to dependency changes. This can be useful to improve the
* performance or to prevent adding/removing event listeners to third-party libraries such as JW Player.
*
* @see {https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down}
*
* @param {function} [callback]
*/
const useEventCallback = <T extends (...args: any[]) => unknown>(callback?: T): T => {
const fnRef = useRef(() => {
throw new Error('Callback called in render');
}) as unknown as React.MutableRefObject<T | undefined>;

useLayoutEffect(() => {
fnRef.current = callback;
}, [callback]);

// @ts-ignore
// ignore since we just want to pass all arguments to the callback function (which we don't know)
return useCallback((...args) => {
if (typeof fnRef.current === 'function') {
return fnRef.current(...args);
}
}, []);
};

export default useEventCallback;
31 changes: 26 additions & 5 deletions src/hooks/useMedia.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import { UseBaseQueryResult, useQuery } from 'react-query';

import { getMediaById } from '../services/api.service';
import { getDRMMediaById, getMediaById } from '../services/api.service';

import type { PlaylistItem } from '#types/playlist';
import { getPublicToken } from '#src/services/entitlement.service';
import { useAccountStore } from '#src/stores/AccountStore';
import { useConfigStore } from '#src/stores/ConfigStore';

export type UseMediaResult<TData = PlaylistItem, TError = unknown> = UseBaseQueryResult<TData, TError>;

export default function useMedia(mediaId: string, enabled: boolean = true): UseMediaResult {
return useQuery(['media', mediaId], () => getMediaById(mediaId), {
enabled: !!mediaId && enabled,
keepPreviousData: true,
});
const jwt = useAccountStore((store) => store.auth?.jwt);
const signingConfig = useConfigStore((store) => store.config.contentSigningService);

return useQuery(
['media', mediaId],
async () => {
const drmEnabled = !!signingConfig?.host && !!signingConfig?.drmEnabled && !!signingConfig?.drmPolicyId;

if (drmEnabled && signingConfig?.drmEnabled && signingConfig?.drmPolicyId) {
const { host, drmPolicyId } = signingConfig;
const token = await getPublicToken(host, 'media', mediaId, drmPolicyId, {}, jwt);

return getDRMMediaById(mediaId, signingConfig.drmPolicyId, token);
}

return getMediaById(mediaId);
},
{
enabled: !!mediaId && enabled,
keepPreviousData: true,
},
);
}
55 changes: 45 additions & 10 deletions src/hooks/usePlaylist.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,59 @@
import { UseBaseQueryResult, useQuery } from 'react-query';

import { generatePlaylistPlaceholder } from '../utils/collection';
import { getPlaylistById } from '../services/api.service';
import { getDRMPlaylistById, getPlaylistById } from '../services/api.service';

import type { Playlist } from '#types/playlist';
import type { Playlist, PlaylistParams } from '#types/playlist';
import { useConfigStore } from '#src/stores/ConfigStore';
import { getPublicToken } from '#src/services/entitlement.service';
import { useAccountStore } from '#src/stores/AccountStore';

const placeholderData = generatePlaylistPlaceholder(30);

export type UsePlaylistResult<TData = Playlist, TError = unknown> = UseBaseQueryResult<TData, TError>;

export default function usePlaylist(
const filterRelatedMediaItem = (playlist: Playlist | undefined, relatedMediaId?: string): Playlist | undefined => {
if (playlist?.playlist && relatedMediaId) {
playlist.playlist = playlist.playlist.filter((playlistItem) => playlistItem.mediaid !== relatedMediaId);
}

return playlist;
};

export default function usePlaylist (
playlistId: string,
relatedMediaId?: string,
params: PlaylistParams = {},
enabled: boolean = true,
usePlaceholderData: boolean = true,
limit?: number,
): UsePlaylistResult {
return useQuery(['playlist', playlistId, relatedMediaId], () => getPlaylistById(playlistId, relatedMediaId, limit), {
enabled: !!playlistId && enabled,
placeholderData: usePlaceholderData ? placeholderData : undefined,
retry: false,
});
const jwt = useAccountStore((store) => store.auth?.jwt);
const signingConfig = useConfigStore((store) => store.config.contentSigningService);

return useQuery(
['playlist', playlistId, params],
async () => {
const drmEnabled = !!signingConfig?.host && !!signingConfig?.drmEnabled && !!signingConfig?.drmPolicyId;

if (drmEnabled && signingConfig?.drmEnabled && signingConfig?.drmPolicyId) {
const { host, drmPolicyId } = signingConfig;
const token = await getPublicToken(host, 'playlist', playlistId, jwt, params, drmPolicyId);

const playlist = await getDRMPlaylistById(playlistId, signingConfig.drmPolicyId, {
...params,
token,
});

return filterRelatedMediaItem(playlist, params.related_media_id);
}

const playlist = await getPlaylistById(playlistId, {}).then(filterRelatedMediaItem);

return filterRelatedMediaItem(playlist, params.related_media_id);
},
{
enabled: !!playlistId && enabled,
placeholderData: usePlaceholderData ? placeholderData : undefined,
retry: false,
},
);
}
34 changes: 34 additions & 0 deletions src/hooks/usePlaylistItemCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback } from 'react';

import { getMediaToken } from '../services/entitlement.service';
import { useAccountStore } from '../stores/AccountStore';
import { useConfigStore } from '../stores/ConfigStore';

import { getDRMMediaById, getMediaById } from '#src/services/api.service';
import type { PlaylistItem } from '#types/playlist';

export const usePlaylistItemCallback = () => {
const { auth } = useAccountStore(({ auth, subscription }) => ({ auth, subscription }));
const signingConfig = useConfigStore((state) => state.config?.contentSigningService);

return useCallback(
async (item: PlaylistItem) => {
const jwt = auth?.jwt;
const signingEnabled = !!signingConfig?.host;
const drmEnabled = signingEnabled && signingConfig?.drmEnabled && signingConfig?.drmPolicyId;

if (!signingConfig && !signingEnabled) return item;

// if signing is enabled, we need to sign the media item first. Assuming that the media item given to the player
// isn't signed. An alternative way is to
const { host, drmPolicyId } = signingConfig;

const token = await getMediaToken({ host, drmPolicyId, id: item.mediaid, jwt, params: {} });

if (drmEnabled && drmPolicyId) return getDRMMediaById(item.mediaid, drmPolicyId, token);

return await getMediaById(item.mediaid, token);
},
[auth],
);
};
25 changes: 0 additions & 25 deletions src/hooks/useRecommendationsPlaylist.ts

This file was deleted.

24 changes: 0 additions & 24 deletions src/hooks/useSearchPlaylist.ts

This file was deleted.

7 changes: 1 addition & 6 deletions src/providers/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,7 @@ const ConfigProvider: FunctionComponent<ProviderProps> = ({ children, configLoca
return 'SVOD';
};

return (
<ConfigContext.Provider value={config}>
{loading ? <LoadingOverlay /> : null}
{children}
</ConfigContext.Provider>
);
return <ConfigContext.Provider value={config}>{loading ? <LoadingOverlay /> : children}</ConfigContext.Provider>;
};

export default ConfigProvider;
4 changes: 2 additions & 2 deletions src/screens/Movie/Movie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import useMedia from '#src/hooks/useMedia';
import { generateMovieJSONLD } from '#src/utils/structuredData';
import { copyToClipboard } from '#src/utils/dom';
import LoadingOverlay from '#src/components/LoadingOverlay/LoadingOverlay';
import useRecommendedPlaylist from '#src/hooks/useRecommendationsPlaylist';
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore';
import { useConfigStore } from '#src/stores/ConfigStore';
import { useAccountStore } from '#src/stores/AccountStore';
Expand All @@ -26,6 +25,7 @@ import { isAllowedToWatch } from '#src/utils/cleeng';
import { addConfigParamToUrl } from '#src/utils/configOverride';
import { useFavoritesStore } from '#src/stores/FavoritesStore';
import { removeItem, saveItem } from '#src/stores/FavoritesController';
import usePlaylist from '#src/hooks/usePlaylist';

type MovieRouteParams = {
id: string;
Expand All @@ -52,7 +52,7 @@ const Movie = ({ match, location }: RouteComponentProps<MovieRouteParams>): JSX.
const itemRequiresSubscription = item?.requiresSubscription !== 'false';
useBlurImageUpdater(item);
const { data: trailerItem } = useMedia(item?.trailerId || '');
const { data: playlist } = useRecommendedPlaylist(recommendationsPlaylist || '', item);
const { data: playlist } = usePlaylist(recommendationsPlaylist || '', { related_media_id: id });

const isFavorited = useFavoritesStore((state) => !!item && state.hasItem(item));

Expand Down
5 changes: 3 additions & 2 deletions src/screens/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import type { PlaylistItem } from '../../../types/playlist';
import CardGrid from '../../components/CardGrid/CardGrid';
import { cardUrl } from '../../utils/formatting';
import useFirstRender from '../../hooks/useFirstRender';
import useSearchPlaylist from '../../hooks/useSearchPlaylist';
import { useAccountStore } from '../../stores/AccountStore';
import { useConfigStore } from '../../stores/ConfigStore';

import styles from './Search.module.scss';

import usePlaylist from '#src/hooks/usePlaylist';

type SearchRouteParams = {
query: string;
};
Expand All @@ -36,7 +37,7 @@ const Search: React.FC<RouteComponentProps<SearchRouteParams>> = ({
const searchQuery = useUIStore((state) => state.searchQuery);
const { updateSearchQuery } = useSearchQueryUpdater();
const history = useHistory();
const { isFetching, error, data: { playlist } = { playlist: [] } } = useSearchPlaylist(searchPlaylist || '', query, firstRender);
const { isFetching, error, data: { playlist } = { playlist: [] } } = usePlaylist(searchPlaylist || '', { search: query }, !firstRender, !!query);

const updateBlurImage = useBlurImageUpdater(playlist);

Expand Down
2 changes: 1 addition & 1 deletion src/screens/Series/Series.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const Series = ({ match, location }: RouteComponentProps<SeriesRouteParams>): JS
const itemRequiresSubscription = item?.requiresSubscription !== 'false';
useBlurImageUpdater(item);
const { data: trailerItem } = useMedia(item?.trailerId || '');
const { isLoading: playlistIsLoading, error: playlistError, data: seriesPlaylist = { title: '', playlist: [] } } = usePlaylist(id, undefined, true, false);
const { isLoading: playlistIsLoading, error: playlistError, data: seriesPlaylist = { title: '', playlist: [] } } = usePlaylist(id, {}, true, false);
const [seasonFilter, setSeasonFilter] = useState<string>('');
const filters = getFiltersFromSeries(seriesPlaylist.playlist);
const filteredPlaylist = useMemo(() => filterSeries(seriesPlaylist.playlist, seasonFilter), [seriesPlaylist, seasonFilter]);
Expand Down
Loading

0 comments on commit e93a655

Please sign in to comment.