sanity-plugin-mux-input 2.14.0 → 2.16.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 +1057 -470
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1059 -472
- 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 +15 -1
- package/src/components/ResyncMetadata.tsx +152 -73
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +11 -55
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +55 -19
- package/src/components/VideoDetails/useVideoDetails.ts +15 -1
- 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/useResyncAsset.ts +110 -0
- package/src/hooks/useResyncMuxMetadata.ts +33 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/schema.ts +5 -0
- package/src/util/addKeysToMuxData.ts +30 -0
- 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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {useToast} from '@sanity/ui'
|
|
2
|
+
import {useCallback, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
import {getAsset} from '../actions/assets'
|
|
5
|
+
import {addKeysToMuxData} from '../util/addKeysToMuxData'
|
|
6
|
+
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
7
|
+
import {useClient} from './useClient'
|
|
8
|
+
|
|
9
|
+
type ResyncAssetState = 'idle' | 'syncing' | 'success' | 'error'
|
|
10
|
+
|
|
11
|
+
interface UseResyncAssetOptions {
|
|
12
|
+
showToast?: boolean
|
|
13
|
+
onSuccess?: (updatedData: MuxAsset) => void
|
|
14
|
+
onError?: (error: unknown) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UseResyncAssetReturn {
|
|
18
|
+
resyncState: ResyncAssetState
|
|
19
|
+
resyncError: unknown
|
|
20
|
+
resyncAsset: (asset: VideoAssetDocument) => Promise<MuxAsset | undefined>
|
|
21
|
+
isResyncing: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useResyncAsset(options?: UseResyncAssetOptions): UseResyncAssetReturn {
|
|
25
|
+
const client = useClient()
|
|
26
|
+
const toast = useToast()
|
|
27
|
+
const [resyncState, setResyncState] = useState<ResyncAssetState>('idle')
|
|
28
|
+
const [resyncError, setResyncError] = useState<unknown>(null)
|
|
29
|
+
|
|
30
|
+
const showToast = options?.showToast ?? false
|
|
31
|
+
|
|
32
|
+
const resyncAsset = useCallback(
|
|
33
|
+
async (asset: VideoAssetDocument) => {
|
|
34
|
+
if (!asset.assetId) {
|
|
35
|
+
if (showToast) {
|
|
36
|
+
toast.push({
|
|
37
|
+
title: 'Cannot resync',
|
|
38
|
+
description: 'Asset has no Mux ID',
|
|
39
|
+
status: 'error',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
options?.onError?.(new Error('Asset has no Mux ID'))
|
|
43
|
+
return undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!asset._id) {
|
|
47
|
+
if (showToast) {
|
|
48
|
+
toast.push({
|
|
49
|
+
title: 'Cannot resync',
|
|
50
|
+
description: 'Asset has no document ID',
|
|
51
|
+
status: 'error',
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
options?.onError?.(new Error('Asset has no document ID'))
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setResyncState('syncing')
|
|
59
|
+
setResyncError(null)
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const response = await getAsset(client, asset.assetId)
|
|
63
|
+
const muxData = response.data
|
|
64
|
+
const dataWithKeys = addKeysToMuxData(muxData)
|
|
65
|
+
|
|
66
|
+
await client
|
|
67
|
+
.patch(asset._id)
|
|
68
|
+
.set({
|
|
69
|
+
status: muxData.status,
|
|
70
|
+
data: dataWithKeys,
|
|
71
|
+
...(muxData.meta?.title && {filename: muxData.meta.title}),
|
|
72
|
+
})
|
|
73
|
+
.commit({returnDocuments: false})
|
|
74
|
+
|
|
75
|
+
setResyncState('success')
|
|
76
|
+
if (showToast) {
|
|
77
|
+
toast.push({
|
|
78
|
+
title: 'Asset synced',
|
|
79
|
+
description: 'Data has been updated from Mux',
|
|
80
|
+
status: 'success',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
options?.onSuccess?.(muxData)
|
|
85
|
+
return muxData
|
|
86
|
+
} catch (error) {
|
|
87
|
+
setResyncState('error')
|
|
88
|
+
setResyncError(error)
|
|
89
|
+
console.error('Failed to refresh asset data:', error)
|
|
90
|
+
if (showToast) {
|
|
91
|
+
toast.push({
|
|
92
|
+
title: 'Sync failed',
|
|
93
|
+
description: 'Could not sync asset from Mux',
|
|
94
|
+
status: 'error',
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
options?.onError?.(error)
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[client, toast, options, showToast]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
resyncState,
|
|
106
|
+
resyncError,
|
|
107
|
+
resyncAsset,
|
|
108
|
+
isResyncing: resyncState === 'syncing',
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useDocumentStore,
|
|
7
7
|
} from 'sanity'
|
|
8
8
|
|
|
9
|
+
import {addKeysToMuxData} from '../util/addKeysToMuxData'
|
|
9
10
|
import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
|
|
10
11
|
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
11
12
|
import {SANITY_API_VERSION} from './useClient'
|
|
@@ -115,6 +116,37 @@ export default function useResyncMuxMetadata() {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
async function syncFullData() {
|
|
120
|
+
if (!matchedAssets) return
|
|
121
|
+
|
|
122
|
+
setResyncState('syncing')
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const tx = client.transaction()
|
|
126
|
+
|
|
127
|
+
matchedAssets.forEach((matched) => {
|
|
128
|
+
if (!matched.muxAsset) return
|
|
129
|
+
|
|
130
|
+
const dataWithKeys = addKeysToMuxData(matched.muxAsset)
|
|
131
|
+
|
|
132
|
+
// Update all fields: filename, status, and full data from Mux
|
|
133
|
+
tx.patch(matched.sanityDoc._id, {
|
|
134
|
+
set: {
|
|
135
|
+
filename: matched.muxTitle || matched.currentTitle || '',
|
|
136
|
+
status: matched.muxAsset.status,
|
|
137
|
+
data: dataWithKeys,
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await tx.commit({returnDocuments: false})
|
|
143
|
+
setResyncState('done')
|
|
144
|
+
} catch (error) {
|
|
145
|
+
setResyncState('error')
|
|
146
|
+
setResyncError(error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
return {
|
|
119
151
|
sanityAssetsLoading,
|
|
120
152
|
closeDialog,
|
|
@@ -124,6 +156,7 @@ export default function useResyncMuxMetadata() {
|
|
|
124
156
|
hasSecrets,
|
|
125
157
|
syncAllVideos,
|
|
126
158
|
syncOnlyEmpty,
|
|
159
|
+
syncFullData,
|
|
127
160
|
matchedAssets,
|
|
128
161
|
muxAssets,
|
|
129
162
|
openDialog,
|
|
@@ -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/schema.ts
CHANGED
|
@@ -23,6 +23,11 @@ const muxTrack = {
|
|
|
23
23
|
{type: 'number', name: 'max_frame_rate'},
|
|
24
24
|
{type: 'number', name: 'duration'},
|
|
25
25
|
{type: 'number', name: 'max_height'},
|
|
26
|
+
{type: 'string', name: 'language_code'},
|
|
27
|
+
{type: 'string', name: 'name'},
|
|
28
|
+
{type: 'string', name: 'status'},
|
|
29
|
+
{type: 'string', name: 'text_source'},
|
|
30
|
+
{type: 'string', name: 'text_type'},
|
|
26
31
|
],
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {uuid} from '@sanity/uuid'
|
|
2
|
+
|
|
3
|
+
import type {MuxAsset} from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Adds _key to array items in MuxAsset data for Sanity compatibility.
|
|
7
|
+
* Sanity requires _key on array items for proper editing support.
|
|
8
|
+
*/
|
|
9
|
+
export function addKeysToMuxData(data: MuxAsset): MuxAsset {
|
|
10
|
+
return {
|
|
11
|
+
...data,
|
|
12
|
+
tracks: data.tracks?.map((track) => ({
|
|
13
|
+
...track,
|
|
14
|
+
_key: uuid(),
|
|
15
|
+
})),
|
|
16
|
+
playback_ids: data.playback_ids?.map((playbackId) => ({
|
|
17
|
+
...playbackId,
|
|
18
|
+
_key: uuid(),
|
|
19
|
+
})),
|
|
20
|
+
static_renditions: data.static_renditions
|
|
21
|
+
? {
|
|
22
|
+
...data.static_renditions,
|
|
23
|
+
files: data.static_renditions.files?.map((file) => ({
|
|
24
|
+
...file,
|
|
25
|
+
_key: uuid(),
|
|
26
|
+
})),
|
|
27
|
+
}
|
|
28
|
+
: undefined,
|
|
29
|
+
}
|
|
30
|
+
}
|
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
|
}
|