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.
Files changed (55) hide show
  1. package/README.md +25 -24
  2. package/dist/index.d.mts +13 -1
  3. package/dist/index.d.ts +13 -1
  4. package/dist/index.js +1057 -470
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1059 -472
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +1 -1
  9. package/src/_exports/index.ts +1 -0
  10. package/src/actions/secrets.ts +6 -1
  11. package/src/actions/upload.ts +1 -1
  12. package/src/components/ConfigureApi.tsx +51 -5
  13. package/src/components/EditCaptionDialog.tsx +2 -2
  14. package/src/components/InputBrowser.tsx +8 -2
  15. package/src/components/PageSelector.tsx +4 -7
  16. package/src/components/Player.styled.tsx +7 -2
  17. package/src/components/PlayerActionsMenu.tsx +15 -1
  18. package/src/components/ResyncMetadata.tsx +152 -73
  19. package/src/components/SelectAsset.tsx +9 -3
  20. package/src/components/StudioTool.tsx +2 -2
  21. package/src/components/TextTracksManager.tsx +11 -55
  22. package/src/components/UploadConfiguration.tsx +104 -343
  23. package/src/components/Uploader.tsx +18 -7
  24. package/src/components/VideoDetails/VideoDetails.tsx +55 -19
  25. package/src/components/VideoDetails/useVideoDetails.ts +15 -1
  26. package/src/components/VideoInBrowser.tsx +53 -6
  27. package/src/components/VideoPlayer.tsx +120 -47
  28. package/src/components/VideoThumbnail.tsx +84 -72
  29. package/src/components/VideosBrowser.tsx +7 -5
  30. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
  31. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
  32. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
  33. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
  34. package/src/context/DrmPlaybackWarningContext.tsx +93 -0
  35. package/src/hooks/useFetchFileSize.ts +54 -0
  36. package/src/hooks/useMediaMetadata.ts +100 -0
  37. package/src/hooks/useResyncAsset.ts +110 -0
  38. package/src/hooks/useResyncMuxMetadata.ts +33 -0
  39. package/src/hooks/useSaveSecrets.ts +10 -3
  40. package/src/hooks/useSecretsDocumentValues.ts +9 -1
  41. package/src/hooks/useSecretsFormState.ts +6 -3
  42. package/src/schema.ts +5 -0
  43. package/src/util/addKeysToMuxData.ts +30 -0
  44. package/src/util/asserters.ts +14 -0
  45. package/src/util/createUrlParamsObject.ts +7 -3
  46. package/src/util/generateJwt.ts +11 -2
  47. package/src/util/getPlaybackPolicy.ts +63 -4
  48. package/src/util/getStoryboardSrc.ts +7 -3
  49. package/src/util/getVideoMetadata.ts +1 -0
  50. package/src/util/getVideoSrc.ts +9 -9
  51. package/src/util/readSecrets.ts +3 -1
  52. package/src/util/textTracks.ts +6 -3
  53. package/src/util/tryWithSuspend.ts +22 -0
  54. package/src/util/types.ts +27 -2
  55. 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
- }: Pick<Secrets, 'token' | 'secretKey' | 'enableSignedUrls'>): Promise<Secrets> => {
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 = ['token', 'secretKey', 'enableSignedUrls', 'signingKeyId', 'signingKeyPrivate']
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 extends Pick<Secrets, 'token' | 'secretKey' | 'enableSignedUrls'> {
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
- function init({token, secretKey, enableSignedUrls}: Secrets): State {
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 any)?.type}`)
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
+ }
@@ -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 {getPlaybackId} from './getPlaybackId'
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
- if (getPlaybackPolicy(asset) === 'signed') {
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
  }
@@ -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
- : never
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 {PlaybackPolicy, VideoAssetDocument} from './types'
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
- ): PlaybackPolicy {
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
- asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ??
8
- 'public'
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 {getPlaybackId} from './getPlaybackId'
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
- if (getPlaybackPolicy(asset) === 'signed') {
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,
@@ -1,23 +1,23 @@
1
1
  import type {SanityClient} from 'sanity'
2
2
 
3
3
  import {generateJwt} from './generateJwt'
4
- import {getPlaybackId} from './getPlaybackId'
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
- asset: VideoAssetDocument
7
+ muxPlaybackId: MuxPlaybackId
10
8
  client: SanityClient
11
9
  }
12
10
 
13
- export function getVideoSrc({asset, client}: VideoSrcOptions): MuxVideoUrl {
14
- const playbackId = getPlaybackId(asset)
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 (getPlaybackPolicy(asset) === 'signed') {
18
- const token = generateJwt(client, playbackId, 'v')
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/${playbackId}.m3u8?${searchParams}`
22
+ return `https://stream.mux.com/${muxPlaybackId.id}.m3u8?${searchParams}`
23
23
  }
@@ -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
  }