-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #70 from jwplayer/feat/url-signing-and-drm
Feat / add playlist and media entitlement
- Loading branch information
Showing
27 changed files
with
293 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ module.exports = { | |
'menu', | ||
'payment', | ||
'e2e', | ||
'signing', | ||
'entitlement', | ||
], | ||
], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { useQuery } from 'react-query'; | ||
|
||
import { getPublicToken } from '#src/services/entitlement.service'; | ||
import { useConfigStore } from '#src/stores/ConfigStore'; | ||
import type { GetPlaylistParams } from '#types/playlist'; | ||
import type { GetMediaParams } from '#types/media'; | ||
|
||
const useContentProtection = <T>( | ||
type: EntitlementType, | ||
id: string | undefined, | ||
callback: (token?: string, drmPolicyId?: string) => Promise<T | undefined>, | ||
params: GetPlaylistParams | GetMediaParams = {}, | ||
enabled: boolean = true, | ||
placeholderData?: T, | ||
) => { | ||
const signingConfig = useConfigStore((store) => store.config.contentSigningService); | ||
const host = signingConfig?.host; | ||
const drmPolicyId = signingConfig?.drmPolicyId; | ||
const drmEnabled = !!drmPolicyId; | ||
const signingEnabled = !!host && drmEnabled; | ||
|
||
const { data: token, isLoading } = useQuery( | ||
['token', type, id, params], | ||
() => { | ||
// we only want to sign public media/playlist URLs when DRM is enabled | ||
if (!!id && !!host && drmEnabled) { | ||
const { host, drmPolicyId } = signingConfig; | ||
|
||
return getPublicToken(host, type, id, undefined, params, drmPolicyId); | ||
} | ||
}, | ||
{ enabled: signingEnabled && enabled && !!id, keepPreviousData: false, staleTime: 15 * 60 * 1000 }, | ||
); | ||
|
||
const queryResult = useQuery<T | undefined>([type, id, params, token], async () => callback(token, drmPolicyId), { | ||
enabled: !!id && enabled && (!signingEnabled || !!token), | ||
placeholderData: placeholderData, | ||
retry: type === 'media', | ||
}); | ||
|
||
return { | ||
...queryResult, | ||
isLoading: isLoading || queryResult.isLoading, | ||
}; | ||
}; | ||
|
||
export default useContentProtection; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,8 @@ | ||
import { UseBaseQueryResult, useQuery } from 'react-query'; | ||
import useContentProtection from '#src/hooks/useContentProtection'; | ||
import { getMediaById } from '#src/services/api.service'; | ||
|
||
import { getMediaById } from '../services/api.service'; | ||
export default function useMedia(mediaId: string, enabled: boolean = true) { | ||
const callback = (token?: string, drmPolicyId?: string) => getMediaById(mediaId, token, drmPolicyId); | ||
|
||
import type { PlaylistItem } from '#types/playlist'; | ||
|
||
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, | ||
}); | ||
return useContentProtection('media', mediaId, callback, {}, enabled); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,23 @@ | ||
import { UseBaseQueryResult, useQuery } from 'react-query'; | ||
import useContentProtection from '#src/hooks/useContentProtection'; | ||
import { generatePlaylistPlaceholder } from '#src/utils/collection'; | ||
import type { GetPlaylistParams } from '#types/playlist'; | ||
import { getPlaylistById } from '#src/services/api.service'; | ||
import { queryClient } from '#src/providers/QueryProvider'; | ||
|
||
import { generatePlaylistPlaceholder } from '../utils/collection'; | ||
import { getPlaylistById } from '../services/api.service'; | ||
const placeholderData = generatePlaylistPlaceholder(30); | ||
|
||
import type { Playlist } from '#types/playlist'; | ||
export default function usePlaylist(playlistId: string, params: GetPlaylistParams = {}, enabled: boolean = true, usePlaceholderData: boolean = true) { | ||
const callback = async (token?: string, drmPolicyId?: string) => { | ||
const playlist = await getPlaylistById(playlistId, { token, ...params }, drmPolicyId); | ||
|
||
const placeholderData = generatePlaylistPlaceholder(30); | ||
// This pre-caches all playlist items and makes navigating a lot faster. This doesn't work when DRM is enabled | ||
// because of the token mechanism. | ||
playlist?.playlist?.forEach((playlistItem) => { | ||
queryClient.setQueryData(['media', playlistItem.mediaid, {}, undefined], playlistItem); | ||
}); | ||
|
||
export type UsePlaylistResult<TData = Playlist, TError = unknown> = UseBaseQueryResult<TData, TError>; | ||
return playlist; | ||
}; | ||
|
||
export default function usePlaylist( | ||
playlistId: string, | ||
relatedMediaId?: string, | ||
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, | ||
}); | ||
return useContentProtection('playlist', playlistId, callback, params, enabled, usePlaceholderData ? placeholderData : undefined); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { getMediaToken } from '../services/entitlement.service'; | ||
import { useAccountStore } from '../stores/AccountStore'; | ||
import { useConfigStore } from '../stores/ConfigStore'; | ||
|
||
import { getMediaById } from '#src/services/api.service'; | ||
import useEventCallback from '#src/hooks/useEventCallback'; | ||
import type { PlaylistItem } from '#types/playlist'; | ||
|
||
export const usePlaylistItemCallback = () => { | ||
const auth = useAccountStore(({ auth }) => auth); | ||
const signingConfig = useConfigStore((state) => state.config?.contentSigningService); | ||
|
||
return useEventCallback(async (item: PlaylistItem) => { | ||
const jwt = auth?.jwt; | ||
const host = signingConfig?.host; | ||
const drmPolicyId = signingConfig?.drmPolicyId; | ||
const signingEnabled = !!host; | ||
|
||
if (!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. | ||
const token = await getMediaToken(host, item.mediaid, jwt, {}, drmPolicyId); | ||
|
||
return await getMediaById(item.mediaid, token, drmPolicyId); | ||
}); | ||
}; |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.