sanity-plugin-mux-input 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +771 -351
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +773 -353
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +2 -2
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/PageSelector.tsx +4 -7
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +1 -1
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +28 -8
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +120 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +7 -5
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
- package/src/context/DrmPlaybackWarningContext.tsx +93 -0
- package/src/hooks/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/util/asserters.ts +14 -0
- package/src/util/createUrlParamsObject.ts +7 -3
- package/src/util/generateJwt.ts +11 -2
- package/src/util/getPlaybackPolicy.ts +63 -4
- package/src/util/getStoryboardSrc.ts +7 -3
- package/src/util/getVideoMetadata.ts +1 -0
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +6 -3
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +27 -2
- package/src/util/getPlaybackId.ts +0 -9
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
import {StagedUpload} from '../components/Uploader'
|
|
4
|
+
|
|
5
|
+
export interface VideoAssetMetadata {
|
|
6
|
+
width?: number
|
|
7
|
+
height?: number
|
|
8
|
+
isAudioOnly?: boolean
|
|
9
|
+
duration?: number
|
|
10
|
+
size?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useMediaMetadata(stagedUpload: StagedUpload) {
|
|
14
|
+
const [videoAssetMetadata, setVideoAssetMetadata] = useState<VideoAssetMetadata | null>(null)
|
|
15
|
+
const [isLoadingMetadata, setIsLoadingMetadata] = useState(false)
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let videoSrc = null
|
|
18
|
+
// Validate file uploads
|
|
19
|
+
if (stagedUpload.type === 'file') {
|
|
20
|
+
const file = stagedUpload.files[0]
|
|
21
|
+
videoSrc = URL.createObjectURL(file)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate URL uploads
|
|
25
|
+
if (stagedUpload.type === 'url') {
|
|
26
|
+
videoSrc = stagedUpload.url
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setVideoAssetMetadata((old) => ({
|
|
30
|
+
...old,
|
|
31
|
+
duration: undefined,
|
|
32
|
+
width: undefined,
|
|
33
|
+
height: undefined,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
if (!videoSrc) return () => null
|
|
37
|
+
|
|
38
|
+
setIsLoadingMetadata(true)
|
|
39
|
+
const videoElement = document.createElement('video')
|
|
40
|
+
videoElement.preload = 'metadata'
|
|
41
|
+
|
|
42
|
+
const metadataListeners = [
|
|
43
|
+
() => {
|
|
44
|
+
setIsLoadingMetadata(false)
|
|
45
|
+
},
|
|
46
|
+
() => {
|
|
47
|
+
const duration = videoElement.duration
|
|
48
|
+
const width = videoElement.videoWidth
|
|
49
|
+
const height = videoElement.videoHeight
|
|
50
|
+
const isAudioOnly = width <= 0 && height <= 0
|
|
51
|
+
setVideoAssetMetadata((old) => {
|
|
52
|
+
return {
|
|
53
|
+
...old,
|
|
54
|
+
duration: duration,
|
|
55
|
+
width: width,
|
|
56
|
+
height: height,
|
|
57
|
+
isAudioOnly: isAudioOnly,
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const cleanupVideo = (videoEl: HTMLVideoElement) => {
|
|
64
|
+
const currentVideoSrc = videoEl?.src
|
|
65
|
+
if (videoEl) {
|
|
66
|
+
metadataListeners.forEach((listener) =>
|
|
67
|
+
videoEl.removeEventListener('loadedmetadata', listener)
|
|
68
|
+
)
|
|
69
|
+
videoEl.onerror = null
|
|
70
|
+
videoEl.src = ''
|
|
71
|
+
videoEl.load()
|
|
72
|
+
}
|
|
73
|
+
if (currentVideoSrc?.startsWith('blob:')) {
|
|
74
|
+
URL.revokeObjectURL(currentVideoSrc)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
metadataListeners.push(() => setTimeout(() => cleanupVideo(videoElement), 0))
|
|
78
|
+
|
|
79
|
+
videoElement.onerror = () => {
|
|
80
|
+
setIsLoadingMetadata(false)
|
|
81
|
+
console.warn('Could not read video metadata for validation')
|
|
82
|
+
cleanupVideo(videoElement)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
metadataListeners.forEach((listener) =>
|
|
86
|
+
videoElement.addEventListener('loadedmetadata', listener)
|
|
87
|
+
)
|
|
88
|
+
videoElement.src = videoSrc
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
cleanupVideo(videoElement)
|
|
92
|
+
}
|
|
93
|
+
}, [stagedUpload.type, stagedUpload])
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
videoAssetMetadata,
|
|
97
|
+
setVideoAssetMetadata,
|
|
98
|
+
isLoadingMetadata,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -10,7 +10,11 @@ export const useSaveSecrets = (client: SanityClient, secrets: Secrets) => {
|
|
|
10
10
|
token,
|
|
11
11
|
secretKey,
|
|
12
12
|
enableSignedUrls,
|
|
13
|
-
|
|
13
|
+
drmConfigId,
|
|
14
|
+
}: Pick<
|
|
15
|
+
Secrets,
|
|
16
|
+
'token' | 'secretKey' | 'enableSignedUrls' | 'drmConfigId'
|
|
17
|
+
>): Promise<Secrets> => {
|
|
14
18
|
let {signingKeyId, signingKeyPrivate} = secrets
|
|
15
19
|
|
|
16
20
|
try {
|
|
@@ -20,7 +24,8 @@ export const useSaveSecrets = (client: SanityClient, secrets: Secrets) => {
|
|
|
20
24
|
secretKey!,
|
|
21
25
|
enableSignedUrls,
|
|
22
26
|
signingKeyId!,
|
|
23
|
-
signingKeyPrivate
|
|
27
|
+
signingKeyPrivate!,
|
|
28
|
+
drmConfigId!
|
|
24
29
|
)
|
|
25
30
|
const valid = await testSecrets(client)
|
|
26
31
|
if (!valid?.status && token && secretKey) {
|
|
@@ -49,7 +54,8 @@ export const useSaveSecrets = (client: SanityClient, secrets: Secrets) => {
|
|
|
49
54
|
secretKey!,
|
|
50
55
|
enableSignedUrls,
|
|
51
56
|
signingKeyId,
|
|
52
|
-
signingKeyPrivate
|
|
57
|
+
signingKeyPrivate,
|
|
58
|
+
drmConfigId ?? ''
|
|
53
59
|
)
|
|
54
60
|
} catch (err: any) {
|
|
55
61
|
// eslint-disable-next-line no-console
|
|
@@ -64,6 +70,7 @@ export const useSaveSecrets = (client: SanityClient, secrets: Secrets) => {
|
|
|
64
70
|
enableSignedUrls,
|
|
65
71
|
signingKeyId,
|
|
66
72
|
signingKeyPrivate,
|
|
73
|
+
drmConfigId,
|
|
67
74
|
}
|
|
68
75
|
},
|
|
69
76
|
[client, secrets]
|
|
@@ -4,7 +4,14 @@ import {useDocumentValues} from 'sanity'
|
|
|
4
4
|
import {muxSecretsDocumentId} from '../util/constants'
|
|
5
5
|
import type {Secrets} from '../util/types'
|
|
6
6
|
|
|
7
|
-
const path = [
|
|
7
|
+
const path = [
|
|
8
|
+
'token',
|
|
9
|
+
'secretKey',
|
|
10
|
+
'enableSignedUrls',
|
|
11
|
+
'signingKeyId',
|
|
12
|
+
'signingKeyPrivate',
|
|
13
|
+
'drmConfigId',
|
|
14
|
+
]
|
|
8
15
|
export const useSecretsDocumentValues = () => {
|
|
9
16
|
const {error, isLoading, value} = useDocumentValues<Partial<Secrets> | null | undefined>(
|
|
10
17
|
muxSecretsDocumentId,
|
|
@@ -18,6 +25,7 @@ export const useSecretsDocumentValues = () => {
|
|
|
18
25
|
enableSignedUrls: value?.enableSignedUrls || false,
|
|
19
26
|
signingKeyId: value?.signingKeyId || null,
|
|
20
27
|
signingKeyPrivate: value?.signingKeyPrivate || null,
|
|
28
|
+
drmConfigId: value?.drmConfigId || null,
|
|
21
29
|
}
|
|
22
30
|
return {
|
|
23
31
|
isInitialSetup: !exists,
|
|
@@ -2,7 +2,8 @@ import {useReducer} from 'react'
|
|
|
2
2
|
|
|
3
3
|
import type {Secrets} from '../util/types'
|
|
4
4
|
|
|
5
|
-
export interface State
|
|
5
|
+
export interface State
|
|
6
|
+
extends Pick<Secrets, 'token' | 'secretKey' | 'enableSignedUrls' | 'drmConfigId'> {
|
|
6
7
|
submitting: boolean
|
|
7
8
|
error: string | null
|
|
8
9
|
}
|
|
@@ -13,7 +14,8 @@ export type Action =
|
|
|
13
14
|
| {type: 'change'; payload: {name: 'token'; value: string}}
|
|
14
15
|
| {type: 'change'; payload: {name: 'secretKey'; value: string}}
|
|
15
16
|
| {type: 'change'; payload: {name: 'enableSignedUrls'; value: boolean}}
|
|
16
|
-
|
|
17
|
+
| {type: 'change'; payload: {name: 'drmConfigId'; value: string}}
|
|
18
|
+
function init({token, secretKey, enableSignedUrls, drmConfigId}: Secrets): State {
|
|
17
19
|
return {
|
|
18
20
|
submitting: false,
|
|
19
21
|
error: null,
|
|
@@ -22,6 +24,7 @@ function init({token, secretKey, enableSignedUrls}: Secrets): State {
|
|
|
22
24
|
token: token ?? '',
|
|
23
25
|
secretKey: secretKey ?? '',
|
|
24
26
|
enableSignedUrls: enableSignedUrls ?? false,
|
|
27
|
+
drmConfigId: drmConfigId ?? '',
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
function reducer(state: State, action: Action) {
|
|
@@ -35,7 +38,7 @@ function reducer(state: State, action: Action) {
|
|
|
35
38
|
case 'change':
|
|
36
39
|
return {...state, [action.payload.name]: action.payload.value}
|
|
37
40
|
default:
|
|
38
|
-
throw new Error(`Unknown action type: ${(action as
|
|
41
|
+
throw new Error(`Unknown action type: ${(action as unknown as Action)?.type}`)
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
package/src/util/asserters.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {ServerError} from '@sanity/client'
|
|
1
2
|
import {type InputProps, isObjectInputProps, type PreviewLayoutKey, type PreviewProps} from 'sanity'
|
|
2
3
|
|
|
3
4
|
import type {MuxInputPreviewProps, MuxInputProps} from './types'
|
|
@@ -20,3 +21,16 @@ export function isValidUrl(url: string): boolean {
|
|
|
20
21
|
return false
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* We consider a server error one with status code 5XX.
|
|
27
|
+
* Used mainly to handle unknown Proxy issues.
|
|
28
|
+
*/
|
|
29
|
+
export function isServerError(error: Error): error is ServerError {
|
|
30
|
+
return (
|
|
31
|
+
'statusCode' in error &&
|
|
32
|
+
typeof error.statusCode === 'number' &&
|
|
33
|
+
500 <= error.statusCode &&
|
|
34
|
+
error.statusCode <= 600
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
3
4
|
import {Audience, generateJwt} from './generateJwt'
|
|
4
|
-
import {
|
|
5
|
-
import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
5
|
+
import {getPlaybackPolicyById} from './getPlaybackPolicy'
|
|
6
6
|
import type {AssetThumbnailOptions} from './types'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* May throw a Promise. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
10
|
+
*/
|
|
8
11
|
export function createUrlParamsObject(
|
|
9
12
|
client: SanityClient,
|
|
10
13
|
asset: AssetThumbnailOptions['asset'],
|
|
@@ -16,7 +19,8 @@ export function createUrlParamsObject(
|
|
|
16
19
|
let searchParams = new URLSearchParams(
|
|
17
20
|
JSON.parse(JSON.stringify(params, (_, v) => v ?? undefined))
|
|
18
21
|
)
|
|
19
|
-
|
|
22
|
+
const playbackPolicy = getPlaybackPolicyById(asset, playbackId)?.policy
|
|
23
|
+
if (playbackPolicy === 'signed' || playbackPolicy === 'drm') {
|
|
20
24
|
const token = generateJwt(client, playbackId, audience, params)
|
|
21
25
|
searchParams = new URLSearchParams({token})
|
|
22
26
|
}
|
package/src/util/generateJwt.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {suspend} from 'suspend-react'
|
|
|
4
4
|
import {readSecrets} from './readSecrets'
|
|
5
5
|
import type {AnimatedThumbnailOptions, ThumbnailOptions} from './types'
|
|
6
6
|
|
|
7
|
-
export type Audience = 'g' | 's' | 't' | 'v'
|
|
7
|
+
export type Audience = 'g' | 's' | 't' | 'v' | 'd'
|
|
8
8
|
|
|
9
9
|
export type Payload<T extends Audience> = T extends 'g'
|
|
10
10
|
? AnimatedThumbnailOptions
|
|
@@ -14,8 +14,13 @@ export type Payload<T extends Audience> = T extends 'g'
|
|
|
14
14
|
? ThumbnailOptions
|
|
15
15
|
: T extends 'v'
|
|
16
16
|
? never
|
|
17
|
-
:
|
|
17
|
+
: T extends 'd'
|
|
18
|
+
? never
|
|
19
|
+
: never
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Uses suspend. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
23
|
+
*/
|
|
19
24
|
export function generateJwt<T extends Audience>(
|
|
20
25
|
client: SanityClient,
|
|
21
26
|
playbackId: string,
|
|
@@ -30,6 +35,10 @@ export function generateJwt<T extends Audience>(
|
|
|
30
35
|
throw new TypeError("Missing `signingKeyPrivate`.\n Check your plugin's configuration")
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/* Using suspend means we need to use Suspense on parent components.
|
|
39
|
+
Also, this will throw a Promise under the hood (apparently common in React),
|
|
40
|
+
so if we want to catch errors we have to take this into account in catch blocks
|
|
41
|
+
and rethrow promises. */
|
|
33
42
|
// @ts-expect-error - handle missing typings for this package
|
|
34
43
|
const {default: sign} = suspend(() => import('jsonwebtoken-esm/sign'), ['jsonwebtoken-esm/sign'])
|
|
35
44
|
|
|
@@ -1,10 +1,69 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AdvancedPlaybackPolicy,
|
|
3
|
+
MuxPlaybackId,
|
|
4
|
+
PlaybackPolicy,
|
|
5
|
+
VideoAssetDocument,
|
|
6
|
+
} from './types'
|
|
7
|
+
|
|
8
|
+
/* - Returns the playback id of the asset based on the specified priority.
|
|
9
|
+
By default chooses the "strongest" policy
|
|
10
|
+
- Otherwise, returns the first playback id in the array.
|
|
11
|
+
*/
|
|
12
|
+
export function getPlaybackId(
|
|
13
|
+
asset: Pick<VideoAssetDocument, 'data'>,
|
|
14
|
+
priority: string[] = ['drm', 'signed', 'public']
|
|
15
|
+
): string {
|
|
16
|
+
try {
|
|
17
|
+
if (!asset) {
|
|
18
|
+
throw new TypeError('Tried to get playback Id with no asset')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const playbackIds = asset.data?.playback_ids
|
|
22
|
+
if (playbackIds && playbackIds.length > 0) {
|
|
23
|
+
for (const policy of priority) {
|
|
24
|
+
const match = playbackIds.find((entry) => entry.policy === policy)
|
|
25
|
+
if (match) {
|
|
26
|
+
return match.id
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return playbackIds[0].id
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw new TypeError('Missing playbackId')
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error('Asset is missing a playbackId', {asset}, e)
|
|
36
|
+
throw e
|
|
37
|
+
}
|
|
38
|
+
}
|
|
2
39
|
|
|
3
40
|
export function getPlaybackPolicy(
|
|
4
41
|
asset: Pick<VideoAssetDocument, 'data' | 'playbackId'>
|
|
5
|
-
):
|
|
42
|
+
): MuxPlaybackId | undefined {
|
|
43
|
+
return (
|
|
44
|
+
asset.data?.playback_ids?.find(
|
|
45
|
+
(playbackId) => getPlaybackId(asset, ['drm', 'signed', 'public']) === playbackId.id
|
|
46
|
+
) ?? {id: '', policy: 'public'}
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getPlaybackPolicyById(
|
|
51
|
+
asset: Pick<VideoAssetDocument, 'data'>,
|
|
52
|
+
playbackId: string
|
|
53
|
+
): MuxPlaybackId | undefined {
|
|
54
|
+
return asset.data?.playback_ids?.find((entry) => playbackId === entry.id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hasPlaybackPolicy(
|
|
58
|
+
data: Partial<{
|
|
59
|
+
playback_policy?: PlaybackPolicy[]
|
|
60
|
+
advanced_playback_policies: AdvancedPlaybackPolicy[]
|
|
61
|
+
}>,
|
|
62
|
+
policy: PlaybackPolicy
|
|
63
|
+
) {
|
|
6
64
|
return (
|
|
7
|
-
|
|
8
|
-
|
|
65
|
+
(data.advanced_playback_policies &&
|
|
66
|
+
data.advanced_playback_policies.find((p) => p.policy === policy)) ||
|
|
67
|
+
data.playback_policy?.find((p) => p === policy)
|
|
9
68
|
)
|
|
10
69
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
3
4
|
import {generateJwt} from './generateJwt'
|
|
4
|
-
import {
|
|
5
|
-
import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
5
|
+
import {getPlaybackPolicyById} from './getPlaybackPolicy'
|
|
6
6
|
import type {MuxStoryboardUrl, VideoAssetDocument} from './types'
|
|
7
7
|
|
|
8
8
|
interface StoryboardSrcOptions {
|
|
@@ -10,11 +10,15 @@ interface StoryboardSrcOptions {
|
|
|
10
10
|
client: SanityClient
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* May throw a Promise. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
15
|
+
*/
|
|
13
16
|
export function getStoryboardSrc({asset, client}: StoryboardSrcOptions): MuxStoryboardUrl {
|
|
14
17
|
const playbackId = getPlaybackId(asset)
|
|
15
18
|
const searchParams = new URLSearchParams()
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
const playbackPolicy = getPlaybackPolicyById(asset, playbackId)?.policy
|
|
21
|
+
if (playbackPolicy === 'signed' || playbackPolicy === 'drm') {
|
|
18
22
|
const token = generateJwt(client, playbackId, 's')
|
|
19
23
|
searchParams.set('token', token)
|
|
20
24
|
}
|
|
@@ -13,6 +13,7 @@ export default function getVideoMetadata(doc: VideoAssetDocument) {
|
|
|
13
13
|
playbackId: doc.playbackId,
|
|
14
14
|
createdAt: date,
|
|
15
15
|
duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : undefined,
|
|
16
|
+
playback_ids: doc.data?.playback_ids,
|
|
16
17
|
aspect_ratio: doc.data?.aspect_ratio,
|
|
17
18
|
max_stored_resolution: doc.data?.max_stored_resolution,
|
|
18
19
|
max_stored_frame_rate: doc.data?.max_stored_frame_rate,
|
package/src/util/getVideoSrc.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
3
|
import {generateJwt} from './generateJwt'
|
|
4
|
-
import {
|
|
5
|
-
import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
6
|
-
import type {MuxVideoUrl, VideoAssetDocument} from './types'
|
|
4
|
+
import type {MuxPlaybackId, MuxVideoUrl} from './types'
|
|
7
5
|
|
|
8
6
|
interface VideoSrcOptions {
|
|
9
|
-
|
|
7
|
+
muxPlaybackId: MuxPlaybackId
|
|
10
8
|
client: SanityClient
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* May throw a Promise. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
13
|
+
*/
|
|
14
|
+
export function getVideoSrc({client, muxPlaybackId}: VideoSrcOptions): MuxVideoUrl {
|
|
15
15
|
const searchParams = new URLSearchParams()
|
|
16
16
|
|
|
17
|
-
if (
|
|
18
|
-
const token = generateJwt(client,
|
|
17
|
+
if (muxPlaybackId.policy === 'signed' || muxPlaybackId.policy === 'drm') {
|
|
18
|
+
const token = generateJwt(client, muxPlaybackId.id, 'v')
|
|
19
19
|
searchParams.set('token', token)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
return `https://stream.mux.com/${
|
|
22
|
+
return `https://stream.mux.com/${muxPlaybackId.id}.m3u8?${searchParams}`
|
|
23
23
|
}
|
package/src/util/readSecrets.ts
CHANGED
|
@@ -21,7 +21,8 @@ export function readSecrets(client: SanityClient): Secrets {
|
|
|
21
21
|
secretKey,
|
|
22
22
|
enableSignedUrls,
|
|
23
23
|
signingKeyId,
|
|
24
|
-
signingKeyPrivate
|
|
24
|
+
signingKeyPrivate,
|
|
25
|
+
drmConfigId
|
|
25
26
|
}`,
|
|
26
27
|
{_id}
|
|
27
28
|
)
|
|
@@ -31,6 +32,7 @@ export function readSecrets(client: SanityClient): Secrets {
|
|
|
31
32
|
enableSignedUrls: Boolean(data?.enableSignedUrls) || false,
|
|
32
33
|
signingKeyId: data?.signingKeyId || null,
|
|
33
34
|
signingKeyPrivate: data?.signingKeyPrivate || null,
|
|
35
|
+
drmConfigId: data?.drmConfigId || null,
|
|
34
36
|
}
|
|
35
37
|
}, [cacheNs, _id, projectId, dataset])
|
|
36
38
|
}
|
package/src/util/textTracks.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type {SanityClient} from '@sanity/client'
|
|
|
2
2
|
|
|
3
3
|
import {getAsset} from '../actions/assets'
|
|
4
4
|
import {generateJwt} from './generateJwt'
|
|
5
|
-
import {getPlaybackId} from './
|
|
5
|
+
import {getPlaybackId} from './getPlaybackPolicy'
|
|
6
6
|
import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
7
7
|
import type {MuxTextTrack, VideoAssetDocument} from './types'
|
|
8
8
|
|
|
@@ -169,6 +169,9 @@ export async function pollTrackStatus(
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* May throw a Promise. Call this with {@link tryWithSuspend} or rethrow the Promise
|
|
174
|
+
*/
|
|
172
175
|
export async function downloadVttFile(
|
|
173
176
|
client: SanityClient,
|
|
174
177
|
asset: VideoAssetDocument,
|
|
@@ -191,11 +194,11 @@ export async function downloadVttFile(
|
|
|
191
194
|
throw new Error('Playback ID is required')
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
const playbackPolicy = getPlaybackPolicy(asset)
|
|
197
|
+
const playbackPolicy = getPlaybackPolicy(asset)?.policy
|
|
195
198
|
|
|
196
199
|
let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
|
|
197
200
|
|
|
198
|
-
if (playbackPolicy === 'signed') {
|
|
201
|
+
if (playbackPolicy === 'signed' || playbackPolicy === 'drm') {
|
|
199
202
|
const token = generateJwt(client, playbackId, 'v')
|
|
200
203
|
downloadUrl += `?token=${token}`
|
|
201
204
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When running `suspend()` from react-suspend a function may throw a Promise
|
|
3
|
+
* causing unexpected behavior when catching.
|
|
4
|
+
* @param block Your block of code that uses suspend
|
|
5
|
+
* @param onError (optional) How to handle a regular Error
|
|
6
|
+
* @returns Whatever is returned by the block if it succeeds, otherwise whatever is resolved by onError if defined
|
|
7
|
+
* @throws rethrows the caught Promise to comply with Suspense logic
|
|
8
|
+
*/
|
|
9
|
+
export function tryWithSuspend<T, E>(
|
|
10
|
+
block: () => T,
|
|
11
|
+
onError?: (error: Error) => E
|
|
12
|
+
): T | E | undefined {
|
|
13
|
+
try {
|
|
14
|
+
return block()
|
|
15
|
+
} catch (errorOrPromise) {
|
|
16
|
+
if (errorOrPromise instanceof Promise) {
|
|
17
|
+
// react-suspend will throw a Promise
|
|
18
|
+
throw errorOrPromise
|
|
19
|
+
}
|
|
20
|
+
return onError ? onError(errorOrPromise as Error) : undefined
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/util/types.ts
CHANGED
|
@@ -89,6 +89,13 @@ export interface MuxInputConfig {
|
|
|
89
89
|
*/
|
|
90
90
|
defaultPublic?: boolean
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Enables DRM Protection by default, if you configured your DRM Configuration Id.
|
|
94
|
+
* @see {@link https://www.mux.com/docs/guides/protect-videos-with-drm}
|
|
95
|
+
* @defaultValue true
|
|
96
|
+
*/
|
|
97
|
+
defaultDrm?: boolean
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
100
|
* Auto-generate captions for these languages by default.
|
|
94
101
|
* Requires `"video_quality": "plus"`
|
|
@@ -124,6 +131,13 @@ export interface MuxInputConfig {
|
|
|
124
131
|
*/
|
|
125
132
|
disableTextTrackConfig?: boolean
|
|
126
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether or not to show the playback warning when trying to watch DRM content for the first time.
|
|
136
|
+
*
|
|
137
|
+
* @defaultValue false
|
|
138
|
+
*/
|
|
139
|
+
disableDrmPlaybackWarning?: boolean
|
|
140
|
+
|
|
127
141
|
/**
|
|
128
142
|
* The mime types that are accepted by the input.
|
|
129
143
|
*
|
|
@@ -266,6 +280,7 @@ export interface UploadConfig
|
|
|
266
280
|
text_tracks: UploadTextTrack[]
|
|
267
281
|
signed_policy: boolean
|
|
268
282
|
public_policy: boolean
|
|
283
|
+
drm_policy: boolean
|
|
269
284
|
}
|
|
270
285
|
|
|
271
286
|
/**
|
|
@@ -308,18 +323,28 @@ export interface MuxNewAssetSettings
|
|
|
308
323
|
}[]
|
|
309
324
|
|
|
310
325
|
/** An array of playback policy names that you want applied to this asset and available through playback_ids. */
|
|
311
|
-
playback_policy
|
|
326
|
+
playback_policy?: PlaybackPolicy[]
|
|
327
|
+
|
|
328
|
+
/** An array of playback policy objects that you want applied to this asset and available through playback_ids. advanced_playback_policies must be used instead of playback_policies when creating a DRM playback ID. */
|
|
329
|
+
advanced_playback_policies: AdvancedPlaybackPolicy[]
|
|
312
330
|
|
|
313
331
|
/** Arbitrary user-supplied metadata that will be included in the asset details and related webhooks. */
|
|
314
332
|
passthrough?: string
|
|
315
333
|
}
|
|
316
334
|
|
|
335
|
+
/** Used by advanced_playback_policies, allows to define DRM config. */
|
|
336
|
+
export type AdvancedPlaybackPolicy = {
|
|
337
|
+
policy: PlaybackPolicy
|
|
338
|
+
drm_configuration_id?: string
|
|
339
|
+
}
|
|
340
|
+
|
|
317
341
|
export interface Secrets {
|
|
318
342
|
token: string | null
|
|
319
343
|
secretKey: string | null
|
|
320
344
|
enableSignedUrls: boolean
|
|
321
345
|
signingKeyId: string | null
|
|
322
346
|
signingKeyPrivate: string | null
|
|
347
|
+
drmConfigId: string | null
|
|
323
348
|
}
|
|
324
349
|
|
|
325
350
|
// This narrowed type indicates that there may be assets that are signed, and we have the secrets to access them
|
|
@@ -366,7 +391,7 @@ export interface AssetThumbnailOptions {
|
|
|
366
391
|
asset: Pick<VideoAssetDocument, 'playbackId' | 'data' | 'thumbTime' | 'filename' | 'assetId'>
|
|
367
392
|
}
|
|
368
393
|
|
|
369
|
-
export type PlaybackPolicy = 'signed' | 'public'
|
|
394
|
+
export type PlaybackPolicy = 'signed' | 'public' | 'drm'
|
|
370
395
|
|
|
371
396
|
export interface MuxErrors {
|
|
372
397
|
type: string
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type {VideoAssetDocument} from './types'
|
|
2
|
-
|
|
3
|
-
export function getPlaybackId(asset: Pick<VideoAssetDocument, 'playbackId'>): string {
|
|
4
|
-
if (!asset?.playbackId) {
|
|
5
|
-
console.error('Asset is missing a playbackId', {asset})
|
|
6
|
-
throw new TypeError(`Missing playbackId`)
|
|
7
|
-
}
|
|
8
|
-
return asset.playbackId
|
|
9
|
-
}
|