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
package/dist/index.js CHANGED
@@ -36,7 +36,45 @@ const ToolIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
36
36
  xmlns: "http://www.w3.org/2000/svg",
37
37
  children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z" })
38
38
  }
39
- ), SANITY_API_VERSION = "2024-03-05";
39
+ ), LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY = "mux-plugin-has-shown-drm-playback-warning", DrmPlaybackWarningContext = React.createContext({
40
+ hasShownWarning: !1,
41
+ setHasWarnedAboutDrmPlayback: () => null
42
+ }), DrmPlaybackWarningContextProvider = ({
43
+ config,
44
+ children
45
+ }) => {
46
+ const hasWarned = (config?.disableDrmPlaybackWarning ?? !1) || window.localStorage.getItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY) === "true", [hasWarnedAboutDrmPlayback, setHasWarnedAboutDrmPlayback] = React.useState(hasWarned), setHasShownWarning = (b) => {
47
+ window.localStorage.setItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY, b.toString()), setHasWarnedAboutDrmPlayback(b);
48
+ };
49
+ return /* @__PURE__ */ jsxRuntime.jsx(
50
+ DrmPlaybackWarningContext.Provider,
51
+ {
52
+ value: {
53
+ hasShownWarning: hasWarnedAboutDrmPlayback,
54
+ setHasWarnedAboutDrmPlayback: setHasShownWarning
55
+ },
56
+ children
57
+ }
58
+ );
59
+ }, useDrmPlaybackWarningContext = () => React.useContext(DrmPlaybackWarningContext), DRMWarningDialog = ({ onClose }) => {
60
+ const { setHasWarnedAboutDrmPlayback } = useDrmPlaybackWarningContext(), _onClose = () => {
61
+ setHasWarnedAboutDrmPlayback(!0), onClose();
62
+ };
63
+ return /* @__PURE__ */ jsxRuntime.jsx(
64
+ ui.Dialog,
65
+ {
66
+ open: !0,
67
+ id: "drm-playback-warn",
68
+ onClose: _onClose,
69
+ header: "DRM Playback Warning",
70
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "ghost", tone: "primary", onClick: _onClose, text: "Ok" }) }),
71
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "DRM-protected playback will generate a license with a small associated cost. The plugin will attempt to play signed or public playback IDs instead whenever possible." }) }) }),
73
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, tone: "suggest", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "This is a one time warning. If it persists, you can disable it from your plugin configuration." }) }) })
74
+ ] })
75
+ }
76
+ );
77
+ }, SANITY_API_VERSION = "2024-03-05";
40
78
  function useClient() {
41
79
  return sanity.useClient({ apiVersion: SANITY_API_VERSION });
42
80
  }
@@ -109,7 +147,7 @@ function useAssets() {
109
147
  function useDialogState() {
110
148
  return React.useState(!1);
111
149
  }
112
- function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate) {
150
+ function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate, drmConfigId) {
113
151
  const doc = {
114
152
  _id: "secrets.mux",
115
153
  _type: "mux.apiKey",
@@ -117,9 +155,10 @@ function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, s
117
155
  secretKey,
118
156
  enableSignedUrls,
119
157
  signingKeyId,
120
- signingKeyPrivate
158
+ signingKeyPrivate,
159
+ drmConfigId
121
160
  };
122
- return client.createOrReplace(doc);
161
+ return doc.signingKeyId = enableSignedUrls ? signingKeyId : "", doc.signingKeyPrivate = enableSignedUrls ? signingKeyPrivate : "", client.createOrReplace(doc);
123
162
  }
124
163
  async function createSigningKeys(client) {
125
164
  try {
@@ -172,7 +211,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
172
211
  async ({
173
212
  token,
174
213
  secretKey,
175
- enableSignedUrls
214
+ enableSignedUrls,
215
+ drmConfigId
176
216
  }) => {
177
217
  let { signingKeyId, signingKeyPrivate } = secrets;
178
218
  try {
@@ -182,7 +222,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
182
222
  secretKey,
183
223
  enableSignedUrls,
184
224
  signingKeyId,
185
- signingKeyPrivate
225
+ signingKeyPrivate,
226
+ drmConfigId
186
227
  ), !(await testSecrets(client))?.status && token && secretKey)
187
228
  throw new Error("Invalid secrets");
188
229
  } catch (err) {
@@ -201,7 +242,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
201
242
  secretKey,
202
243
  enableSignedUrls,
203
244
  signingKeyId,
204
- signingKeyPrivate
245
+ signingKeyPrivate,
246
+ drmConfigId ?? ""
205
247
  );
206
248
  } catch (err) {
207
249
  throw console.log("Error while creating and saving signing key:", err?.message), err;
@@ -211,11 +253,19 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
211
253
  secretKey,
212
254
  enableSignedUrls,
213
255
  signingKeyId,
214
- signingKeyPrivate
256
+ signingKeyPrivate,
257
+ drmConfigId
215
258
  };
216
259
  },
217
260
  [client, secrets]
218
- ), name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = ["token", "secretKey", "enableSignedUrls", "signingKeyId", "signingKeyPrivate"], useSecretsDocumentValues = () => {
261
+ ), name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = [
262
+ "token",
263
+ "secretKey",
264
+ "enableSignedUrls",
265
+ "signingKeyId",
266
+ "signingKeyPrivate",
267
+ "drmConfigId"
268
+ ], useSecretsDocumentValues = () => {
219
269
  const { error, isLoading, value } = sanity.useDocumentValues(
220
270
  muxSecretsDocumentId,
221
271
  path$1
@@ -225,7 +275,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
225
275
  secretKey: value?.secretKey || null,
226
276
  enableSignedUrls: value?.enableSignedUrls || !1,
227
277
  signingKeyId: value?.signingKeyId || null,
228
- signingKeyPrivate: value?.signingKeyPrivate || null
278
+ signingKeyPrivate: value?.signingKeyPrivate || null,
279
+ drmConfigId: value?.drmConfigId || null
229
280
  };
230
281
  return {
231
282
  isInitialSetup: !exists,
@@ -235,7 +286,7 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
235
286
  }, [value]);
236
287
  return { error, isLoading, value: cache };
237
288
  };
238
- function init({ token, secretKey, enableSignedUrls }) {
289
+ function init({ token, secretKey, enableSignedUrls, drmConfigId }) {
239
290
  return {
240
291
  submitting: !1,
241
292
  error: null,
@@ -243,7 +294,8 @@ function init({ token, secretKey, enableSignedUrls }) {
243
294
  // This ensures the `dirty` check works correctly
244
295
  token: token ?? "",
245
296
  secretKey: secretKey ?? "",
246
- enableSignedUrls: enableSignedUrls ?? !1
297
+ enableSignedUrls: enableSignedUrls ?? !1,
298
+ drmConfigId: drmConfigId ?? ""
247
299
  };
248
300
  }
249
301
  function reducer(state, action) {
@@ -271,7 +323,8 @@ function readSecrets(client) {
271
323
  secretKey,
272
324
  enableSignedUrls,
273
325
  signingKeyId,
274
- signingKeyPrivate
326
+ signingKeyPrivate,
327
+ drmConfigId
275
328
  }`,
276
329
  { _id }
277
330
  );
@@ -280,7 +333,8 @@ function readSecrets(client) {
280
333
  secretKey: data?.secretKey || null,
281
334
  enableSignedUrls: !!data?.enableSignedUrls || !1,
282
335
  signingKeyId: data?.signingKeyId || null,
283
- signingKeyPrivate: data?.signingKeyPrivate || null
336
+ signingKeyPrivate: data?.signingKeyPrivate || null,
337
+ drmConfigId: data?.drmConfigId || null
284
338
  };
285
339
  }, [cacheNs, _id, projectId, dataset]);
286
340
  }
@@ -343,20 +397,20 @@ function FormField(props) {
343
397
  ] });
344
398
  }
345
399
  var FormField$1 = React.memo(FormField);
346
- const fieldNames = ["token", "secretKey", "enableSignedUrls"];
400
+ const fieldNames = ["token", "secretKey", "enableSignedUrls", "drmConfigId"];
347
401
  function ConfigureApiDialog({ secrets, setDialogState }) {
348
402
  const client = useClient(), [state, dispatch] = useSecretsFormState(secrets), hasSecretsInitially = React.useMemo(() => secrets.token && secrets.secretKey, [secrets]), handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]), dirty = React.useMemo(
349
- () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls,
403
+ () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls || secrets.drmConfigId !== state.drmConfigId,
350
404
  [secrets, state]
351
- ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId] = React.useMemo(
405
+ ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId, drmConfigIdId] = React.useMemo(
352
406
  () => fieldNames.map((field) => `${id}-${field}`),
353
407
  [id]
354
408
  ), firstField = React.useRef(null), handleSaveSecrets = useSaveSecrets(client, secrets), saving = React.useRef(!1), handleSubmit = React.useCallback(
355
409
  (event) => {
356
410
  if (event.preventDefault(), !saving.current && event.currentTarget.reportValidity()) {
357
411
  saving.current = !0, dispatch({ type: "submit" });
358
- const { token, secretKey, enableSignedUrls } = state;
359
- handleSaveSecrets({ token, secretKey, enableSignedUrls }).then((savedSecrets) => {
412
+ const { token, secretKey, enableSignedUrls, drmConfigId } = state;
413
+ handleSaveSecrets({ token, secretKey, enableSignedUrls, drmConfigId }).then((savedSecrets) => {
360
414
  const { projectId, dataset } = client.config();
361
415
  suspendReact.clear([cacheNs, _id, projectId, dataset]), suspendReact.preload(() => Promise.resolve(savedSecrets), [cacheNs, _id, projectId, dataset]), setDialogState(!1);
362
416
  }).catch((err) => dispatch({ type: "error", payload: err.message })).finally(() => {
@@ -389,6 +443,14 @@ function ConfigureApiDialog({ secrets, setDialogState }) {
389
443
  });
390
444
  },
391
445
  [dispatch]
446
+ ), handleChangeDrmConfigId = React.useCallback(
447
+ (event) => {
448
+ dispatch({
449
+ type: "change",
450
+ payload: { name: "drmConfigId", value: event.currentTarget.value }
451
+ });
452
+ },
453
+ [dispatch]
392
454
  );
393
455
  return React.useEffect(() => {
394
456
  firstField.current && firstField.current.focus();
@@ -475,6 +537,45 @@ function ConfigureApiDialog({ secrets, setDialogState }) {
475
537
  ] })
476
538
  ] }) }) : null
477
539
  ] }),
540
+ /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "DRM Configuration ID", inputId: drmConfigIdId, children: /* @__PURE__ */ jsxRuntime.jsx(
541
+ ui.TextInput,
542
+ {
543
+ id: drmConfigIdId,
544
+ onChange: handleChangeDrmConfigId,
545
+ type: "text",
546
+ value: state.drmConfigId ?? "",
547
+ required: !1
548
+ }
549
+ ) }),
550
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "neutral", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
551
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
552
+ "DRM (Digital Rights Management) provides an extra layer of content security for video content streamed from Mux. For additional information check out our",
553
+ " ",
554
+ /* @__PURE__ */ jsxRuntime.jsx(
555
+ "a",
556
+ {
557
+ href: "https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos",
558
+ target: "_blank",
559
+ rel: "noopener noreferrer",
560
+ children: "DRM Guide"
561
+ }
562
+ ),
563
+ "."
564
+ ] }),
565
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
566
+ /* @__PURE__ */ jsxRuntime.jsx(
567
+ "a",
568
+ {
569
+ href: "https://www.mux.com/support/human",
570
+ target: "_blank",
571
+ rel: "noopener noreferrer",
572
+ children: "Contact us"
573
+ }
574
+ ),
575
+ " ",
576
+ "to get started using DRM."
577
+ ] })
578
+ ] }) }),
478
579
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
479
580
  /* @__PURE__ */ jsxRuntime.jsx(
480
581
  ui.Button,
@@ -778,6 +879,35 @@ function useInView(ref, options = {}) {
778
879
  };
779
880
  }, [options, ref]), inView;
780
881
  }
882
+ function getPlaybackId(asset, priority = ["drm", "signed", "public"]) {
883
+ try {
884
+ if (!asset)
885
+ throw new TypeError("Tried to get playback Id with no asset");
886
+ const playbackIds = asset.data?.playback_ids;
887
+ if (playbackIds && playbackIds.length > 0) {
888
+ for (const policy of priority) {
889
+ const match = playbackIds.find((entry) => entry.policy === policy);
890
+ if (match)
891
+ return match.id;
892
+ }
893
+ return playbackIds[0].id;
894
+ }
895
+ throw new TypeError("Missing playbackId");
896
+ } catch (e) {
897
+ throw console.error("Asset is missing a playbackId", { asset }, e), e;
898
+ }
899
+ }
900
+ function getPlaybackPolicy(asset) {
901
+ return asset.data?.playback_ids?.find(
902
+ (playbackId) => getPlaybackId(asset, ["drm", "signed", "public"]) === playbackId.id
903
+ ) ?? { id: "", policy: "public" };
904
+ }
905
+ function getPlaybackPolicyById(asset, playbackId) {
906
+ return asset.data?.playback_ids?.find((entry) => playbackId === entry.id);
907
+ }
908
+ function hasPlaybackPolicy(data, policy) {
909
+ return data.advanced_playback_policies && data.advanced_playback_policies.find((p) => p.policy === policy) || data.playback_policy?.find((p) => p === policy);
910
+ }
781
911
  function generateJwt(client, playbackId, aud, payload) {
782
912
  const { signingKeyId, signingKeyPrivate } = readSecrets(client);
783
913
  if (!signingKeyId)
@@ -798,20 +928,13 @@ function generateJwt(client, playbackId, aud, payload) {
798
928
  }
799
929
  );
800
930
  }
801
- function getPlaybackId(asset) {
802
- if (!asset?.playbackId)
803
- throw console.error("Asset is missing a playbackId", { asset }), new TypeError("Missing playbackId");
804
- return asset.playbackId;
805
- }
806
- function getPlaybackPolicy(asset) {
807
- return asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ?? "public";
808
- }
809
931
  function createUrlParamsObject(client, asset, params, audience) {
810
932
  const playbackId = getPlaybackId(asset);
811
933
  let searchParams = new URLSearchParams(
812
934
  JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
813
935
  );
814
- if (getPlaybackPolicy(asset) === "signed") {
936
+ const playbackPolicy = getPlaybackPolicyById(asset, playbackId)?.policy;
937
+ if (playbackPolicy === "signed" || playbackPolicy === "drm") {
815
938
  const token = generateJwt(client, playbackId, audience, params);
816
939
  searchParams = new URLSearchParams({ token });
817
940
  }
@@ -842,6 +965,15 @@ function getPosterSrc({
842
965
  const { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "t");
843
966
  return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`;
844
967
  }
968
+ function tryWithSuspend(block, onError) {
969
+ try {
970
+ return block();
971
+ } catch (errorOrPromise) {
972
+ if (errorOrPromise instanceof Promise)
973
+ throw errorOrPromise;
974
+ return onError ? onError(errorOrPromise) : void 0;
975
+ }
976
+ }
845
977
  const Image = styledComponents.styled.img`
846
978
  transition: opacity 0.175s ease-out 0s;
847
979
  display: block;
@@ -859,22 +991,22 @@ function VideoThumbnail({
859
991
  width,
860
992
  staticImage = !1
861
993
  }) {
862
- const ref = React.useRef(null), inView = useInView(ref), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), src = React.useMemo(() => {
863
- try {
994
+ const posterWidth = width || 250, client = useClient(), ref = React.useRef(null), inView = useInView(ref), [status, setStatus] = React.useState("loading"), [error, setError] = React.useState(null), thumbnailSrc = React.useMemo(() => tryWithSuspend(
995
+ () => {
864
996
  let thumbnail;
865
997
  return staticImage ? thumbnail = getPosterSrc({ asset, client, width: posterWidth }) : thumbnail = getAnimatedPosterSrc({ asset, client, width: posterWidth }), thumbnail;
866
- } catch {
867
- status !== "error" && setStatus("error");
868
- return;
998
+ },
999
+ (err) => {
1000
+ handleError(err.message);
869
1001
  }
870
- }, [asset, client, posterWidth, status, staticImage]);
1002
+ ), [asset, client, posterWidth, staticImage]);
871
1003
  function handleLoad() {
872
1004
  setStatus("loaded");
873
1005
  }
874
- function handleError() {
875
- setStatus("error");
1006
+ function handleError(err) {
1007
+ setStatus("error"), setError(err || "Failed loading thumbnail");
876
1008
  }
877
- return /* @__PURE__ */ jsxRuntime.jsx(
1009
+ return /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Preparing thumbnail" }), children: /* @__PURE__ */ jsxRuntime.jsx(
878
1010
  ui.Card,
879
1011
  {
880
1012
  style: {
@@ -915,23 +1047,23 @@ function VideoThumbnail({
915
1047
  },
916
1048
  children: [
917
1049
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { fontSize: "1.75em" } }) }),
918
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: "Failed loading thumbnail" })
1050
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: error })
919
1051
  ]
920
1052
  }
921
1053
  ),
922
1054
  /* @__PURE__ */ jsxRuntime.jsx(
923
1055
  Image,
924
1056
  {
925
- src,
1057
+ src: thumbnailSrc ?? void 0,
926
1058
  alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
927
1059
  onLoad: handleLoad,
928
- onError: handleError,
1060
+ onError: () => handleError(),
929
1061
  style: { opacity: status === "loaded" ? 1 : 0 }
930
1062
  }
931
1063
  )
932
1064
  ] }) : null
933
1065
  }
934
- );
1066
+ ) });
935
1067
  }
936
1068
  const MissingAssetCheckbox = styledComponents.styled(ui.Checkbox)`
937
1069
  position: static !important;
@@ -1165,7 +1297,7 @@ const PageSelector = (props) => {
1165
1297
  style: { cursor: "pointer" },
1166
1298
  disabled: page <= 0,
1167
1299
  onClick: () => {
1168
- setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 - 1)));
1300
+ setPage((p) => Math.min(props.total - 1, Math.max(0, p - 1)));
1169
1301
  }
1170
1302
  }
1171
1303
  ),
@@ -1184,12 +1316,32 @@ const PageSelector = (props) => {
1184
1316
  style: { cursor: "pointer" },
1185
1317
  disabled: page >= props.total - 1,
1186
1318
  onClick: () => {
1187
- setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 + 1)));
1319
+ setPage((p) => Math.min(props.total - 1, Math.max(0, p + 1)));
1188
1320
  }
1189
1321
  }
1190
1322
  )
1191
1323
  ] });
1192
1324
  };
1325
+ function addKeysToMuxData(data) {
1326
+ return {
1327
+ ...data,
1328
+ tracks: data.tracks?.map((track) => ({
1329
+ ...track,
1330
+ _key: uuid.uuid()
1331
+ })),
1332
+ playback_ids: data.playback_ids?.map((playbackId) => ({
1333
+ ...playbackId,
1334
+ _key: uuid.uuid()
1335
+ })),
1336
+ static_renditions: data.static_renditions ? {
1337
+ ...data.static_renditions,
1338
+ files: data.static_renditions.files?.map((file) => ({
1339
+ ...file,
1340
+ _key: uuid.uuid()
1341
+ }))
1342
+ } : void 0
1343
+ };
1344
+ }
1193
1345
  function useResyncMuxMetadata() {
1194
1346
  const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
1195
1347
  apiVersion: SANITY_API_VERSION
@@ -1238,6 +1390,27 @@ function useResyncMuxMetadata() {
1238
1390
  }
1239
1391
  }
1240
1392
  }
1393
+ async function syncFullData() {
1394
+ if (matchedAssets) {
1395
+ setResyncState("syncing");
1396
+ try {
1397
+ const tx = client.transaction();
1398
+ matchedAssets.forEach((matched) => {
1399
+ if (!matched.muxAsset) return;
1400
+ const dataWithKeys = addKeysToMuxData(matched.muxAsset);
1401
+ tx.patch(matched.sanityDoc._id, {
1402
+ set: {
1403
+ filename: matched.muxTitle || matched.currentTitle || "",
1404
+ status: matched.muxAsset.status,
1405
+ data: dataWithKeys
1406
+ }
1407
+ });
1408
+ }), await tx.commit({ returnDocuments: !1 }), setResyncState("done");
1409
+ } catch (error) {
1410
+ setResyncState("error"), setResyncError(error);
1411
+ }
1412
+ }
1413
+ }
1241
1414
  return {
1242
1415
  sanityAssetsLoading,
1243
1416
  closeDialog,
@@ -1247,6 +1420,7 @@ function useResyncMuxMetadata() {
1247
1420
  hasSecrets,
1248
1421
  syncAllVideos,
1249
1422
  syncOnlyEmpty,
1423
+ syncFullData,
1250
1424
  matchedAssets,
1251
1425
  muxAssets,
1252
1426
  openDialog
@@ -1262,22 +1436,78 @@ const useSanityAssets = sanity.createHookFromObservableFactory(
1262
1436
  }
1263
1437
  )
1264
1438
  );
1439
+ function OptionCard({
1440
+ id,
1441
+ selected,
1442
+ onSelect,
1443
+ title,
1444
+ count,
1445
+ description,
1446
+ disabled
1447
+ }) {
1448
+ return /* @__PURE__ */ jsxRuntime.jsx(
1449
+ ui.Card,
1450
+ {
1451
+ as: "label",
1452
+ padding: 3,
1453
+ radius: 2,
1454
+ border: !0,
1455
+ tone: selected ? "primary" : "default",
1456
+ style: {
1457
+ cursor: disabled ? "not-allowed" : "pointer",
1458
+ opacity: disabled ? 0.5 : 1
1459
+ },
1460
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, align: "flex-start", children: [
1461
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(
1462
+ ui.Radio,
1463
+ {
1464
+ checked: selected,
1465
+ onChange: () => onSelect(id),
1466
+ disabled,
1467
+ name: "sync-option"
1468
+ }
1469
+ ) }),
1470
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, flex: 1, children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", gap: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, weight: "semibold", children: [
1472
+ title,
1473
+ " (",
1474
+ count,
1475
+ ")"
1476
+ ] }) }),
1477
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: description })
1478
+ ] })
1479
+ ] })
1480
+ }
1481
+ );
1482
+ }
1265
1483
  function ResyncMetadataDialog(props) {
1266
- const { resyncState } = props, canTriggerResync = resyncState === "idle" || resyncState === "error", isResyncing = resyncState === "syncing", isDone = resyncState === "done", videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0, videosWithEmptyOrPlaceholder = props.matchedAssets?.filter(
1484
+ const { resyncState } = props, videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0, videosWithEmptyOrPlaceholder = props.matchedAssets?.filter(
1267
1485
  (m) => m.muxAsset && m.muxTitle && isEmptyOrPlaceholderTitle(m.currentTitle, m.muxAsset.id)
1268
- ).length || 0;
1486
+ ).length || 0, hasEmptyTitles = videosWithEmptyOrPlaceholder > 0, defaultOption = hasEmptyTitles ? "fillEmpty" : "syncTitles", [selectedOption, setSelectedOption] = React.useState(defaultOption), canTriggerResync = resyncState === "idle" || resyncState === "error", isResyncing = resyncState === "syncing", isDone = resyncState === "done", isLoading = props.muxAssets.loading || props.sanityAssetsLoading, handleSync = () => {
1487
+ switch (selectedOption) {
1488
+ case "fillEmpty":
1489
+ props.syncOnlyEmpty();
1490
+ break;
1491
+ case "syncTitles":
1492
+ props.syncAllVideos();
1493
+ break;
1494
+ case "fullResync":
1495
+ props.syncFullData();
1496
+ break;
1497
+ }
1498
+ };
1269
1499
  return /* @__PURE__ */ jsxRuntime.jsx(
1270
1500
  ui.Dialog,
1271
1501
  {
1272
1502
  animate: !0,
1273
- header: "Resync Metadata from Mux",
1503
+ header: "Sync with Mux",
1274
1504
  zOffset: DIALOGS_Z_INDEX,
1275
1505
  id: "resync-metadata-dialog",
1276
1506
  onClose: props.closeDialog,
1277
1507
  onClickOutside: props.closeDialog,
1278
1508
  width: 1,
1279
1509
  position: "fixed",
1280
- footer: !isDone && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
1510
+ footer: !isDone && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "flex-end", gap: 2, children: [
1281
1511
  /* @__PURE__ */ jsxRuntime.jsx(
1282
1512
  ui.Button,
1283
1513
  {
@@ -1285,97 +1515,104 @@ function ResyncMetadataDialog(props) {
1285
1515
  padding: 3,
1286
1516
  mode: "ghost",
1287
1517
  text: "Cancel",
1288
- tone: "critical",
1289
1518
  onClick: props.closeDialog,
1290
1519
  disabled: isResyncing
1291
1520
  }
1292
1521
  ),
1293
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
1294
- videosWithEmptyOrPlaceholder > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1295
- ui.Button,
1296
- {
1297
- fontSize: 2,
1298
- padding: 3,
1299
- mode: "ghost",
1300
- text: `Update empty (${videosWithEmptyOrPlaceholder})`,
1301
- tone: "caution",
1302
- onClick: props.syncOnlyEmpty,
1303
- disabled: isResyncing || !canTriggerResync
1304
- }
1305
- ),
1306
- /* @__PURE__ */ jsxRuntime.jsx(
1307
- ui.Button,
1308
- {
1309
- icon: icons.SyncIcon,
1310
- fontSize: 2,
1311
- padding: 3,
1312
- mode: "ghost",
1313
- text: `Update all (${videosToUpdate})`,
1314
- tone: "positive",
1315
- onClick: props.syncAllVideos,
1316
- iconRight: isResyncing && ui.Spinner,
1317
- disabled: !canTriggerResync
1318
- }
1319
- )
1320
- ] })
1522
+ /* @__PURE__ */ jsxRuntime.jsx(
1523
+ ui.Button,
1524
+ {
1525
+ icon: icons.SyncIcon,
1526
+ fontSize: 2,
1527
+ padding: 3,
1528
+ text: "Run sync",
1529
+ tone: "primary",
1530
+ onClick: handleSync,
1531
+ iconRight: isResyncing && ui.Spinner,
1532
+ disabled: !canTriggerResync || isLoading
1533
+ }
1534
+ )
1321
1535
  ] }) }),
1322
1536
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { padding: 4, children: [
1323
- (props.muxAssets.loading || props.sanityAssetsLoading) && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1537
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 4, padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1324
1538
  /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1325
1539
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1326
1540
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Loading assets from Mux" }),
1327
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "This may take a while." })
1541
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "This may take a while." })
1328
1542
  ] })
1329
1543
  ] }) }),
1330
- props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1544
+ props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 4, padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1331
1545
  /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1332
1546
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1333
1547
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error getting data from Mux" }),
1334
1548
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Please try again or contact a developer for help." })
1335
1549
  ] })
1336
1550
  ] }) }),
1337
- resyncState === "syncing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1551
+ resyncState === "syncing" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "primary", marginBottom: 4, padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 4, children: [
1338
1552
  /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0, size: 4 }),
1339
1553
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1340
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Updating video metadata" }),
1341
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Syncing titles from Mux..." })
1554
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Syncing metadata" }),
1555
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Updating videos from Mux..." })
1342
1556
  ] })
1343
1557
  ] }) }),
1344
- resyncState === "error" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 5, padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1558
+ resyncState === "error" && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { tone: "critical", marginBottom: 4, padding: 3, border: !0, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1345
1559
  /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { fontSize: 36 }),
1346
1560
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1347
1561
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "There was an error syncing metadata" }),
1348
1562
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: props.resyncError ? `Error: ${props.resyncError}` : "Please try again or contact a developer for help." })
1349
1563
  ] })
1350
1564
  ] }) }),
1351
- resyncState === "done" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { paddingY: 5, marginBottom: 4, space: 3, style: { textAlign: "center" }, children: [
1565
+ resyncState === "done" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { paddingY: 5, space: 3, style: { textAlign: "center" }, children: [
1352
1566
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckmarkCircleIcon, { fontSize: 48 }) }),
1353
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Metadata synced successfully" }),
1354
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "All video titles have been updated from Mux." })
1567
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 2, children: "Sync completed" }),
1568
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Videos have been updated from Mux." })
1355
1569
  ] }),
1356
- resyncState === "idle" && !props.muxAssets.loading && !props.sanityAssetsLoading && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1357
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 1, children: [
1358
- "There ",
1359
- videosToUpdate === 1 ? "is" : "are",
1360
- " ",
1570
+ !isDone && !isLoading && !props.muxAssets.error && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1571
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1572
+ "Found ",
1361
1573
  videosToUpdate,
1362
1574
  " video",
1363
1575
  videosToUpdate === 1 ? "" : "s",
1364
- " with Mux metadata"
1576
+ " linked to Mux."
1365
1577
  ] }),
1366
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This will update video titles in Sanity to match those in Mux. No new videos will be created." }),
1367
- videosWithEmptyOrPlaceholder > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, tone: "caution", border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "flex-start", gap: 2, children: [
1368
- /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, {}) }),
1369
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1370
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, weight: "semibold", children: "Videos with empty or placeholder titles" }),
1371
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
1372
- videosWithEmptyOrPlaceholder,
1373
- " video",
1374
- videosWithEmptyOrPlaceholder === 1 ? "" : "s",
1375
- ' without titles or with placeholder titles (e.g., "Asset #123") can be updated selectively.'
1376
- ] })
1377
- ] })
1378
- ] }) })
1578
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1579
+ hasEmptyTitles && /* @__PURE__ */ jsxRuntime.jsx(
1580
+ OptionCard,
1581
+ {
1582
+ id: "fillEmpty",
1583
+ selected: selectedOption === "fillEmpty",
1584
+ onSelect: setSelectedOption,
1585
+ title: "Fill missing titles only",
1586
+ count: videosWithEmptyOrPlaceholder,
1587
+ description: "Updates only videos without a title or with placeholder titles (e.g., 'Asset #123') using the title from Mux.",
1588
+ disabled: isResyncing
1589
+ }
1590
+ ),
1591
+ /* @__PURE__ */ jsxRuntime.jsx(
1592
+ OptionCard,
1593
+ {
1594
+ id: "syncTitles",
1595
+ selected: selectedOption === "syncTitles",
1596
+ onSelect: setSelectedOption,
1597
+ title: "Sync all titles",
1598
+ count: videosToUpdate,
1599
+ description: "Replaces the title in Sanity with the title from Mux for all videos.",
1600
+ disabled: isResyncing
1601
+ }
1602
+ ),
1603
+ /* @__PURE__ */ jsxRuntime.jsx(
1604
+ OptionCard,
1605
+ {
1606
+ id: "fullResync",
1607
+ selected: selectedOption === "fullResync",
1608
+ onSelect: setSelectedOption,
1609
+ title: "Full resync",
1610
+ count: videosToUpdate,
1611
+ description: "Updates all fields from Mux including status, duration, tracks, captions, and renditions.",
1612
+ disabled: isResyncing
1613
+ }
1614
+ )
1615
+ ] })
1379
1616
  ] })
1380
1617
  ] })
1381
1618
  }
@@ -1384,7 +1621,7 @@ function ResyncMetadataDialog(props) {
1384
1621
  function ResyncMetadata() {
1385
1622
  const resyncMetadata = useResyncMuxMetadata();
1386
1623
  if (resyncMetadata.hasSecrets)
1387
- return resyncMetadata.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ResyncMetadataDialog, { ...resyncMetadata }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Resync Metadata", onClick: resyncMetadata.openDialog });
1624
+ return resyncMetadata.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ResyncMetadataDialog, { ...resyncMetadata }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Sync with Mux", onClick: resyncMetadata.openDialog });
1388
1625
  }
1389
1626
  const CONTEXT_MENU_POPOVER_PROPS = {
1390
1627
  constrainSize: !0,
@@ -1465,6 +1702,55 @@ function StopWatchIcon(props) {
1465
1702
  }
1466
1703
  );
1467
1704
  }
1705
+ function useResyncAsset(options) {
1706
+ const client = useClient(), toast = ui.useToast(), [resyncState, setResyncState] = React.useState("idle"), [resyncError, setResyncError] = React.useState(null), showToast = options?.showToast ?? !1, resyncAsset = React.useCallback(
1707
+ async (asset) => {
1708
+ if (!asset.assetId) {
1709
+ showToast && toast.push({
1710
+ title: "Cannot resync",
1711
+ description: "Asset has no Mux ID",
1712
+ status: "error"
1713
+ }), options?.onError?.(new Error("Asset has no Mux ID"));
1714
+ return;
1715
+ }
1716
+ if (!asset._id) {
1717
+ showToast && toast.push({
1718
+ title: "Cannot resync",
1719
+ description: "Asset has no document ID",
1720
+ status: "error"
1721
+ }), options?.onError?.(new Error("Asset has no document ID"));
1722
+ return;
1723
+ }
1724
+ setResyncState("syncing"), setResyncError(null);
1725
+ try {
1726
+ const muxData = (await getAsset(client, asset.assetId)).data, dataWithKeys = addKeysToMuxData(muxData);
1727
+ return await client.patch(asset._id).set({
1728
+ status: muxData.status,
1729
+ data: dataWithKeys,
1730
+ ...muxData.meta?.title && { filename: muxData.meta.title }
1731
+ }).commit({ returnDocuments: !1 }), setResyncState("success"), showToast && toast.push({
1732
+ title: "Asset synced",
1733
+ description: "Data has been updated from Mux",
1734
+ status: "success"
1735
+ }), options?.onSuccess?.(muxData), muxData;
1736
+ } catch (error) {
1737
+ setResyncState("error"), setResyncError(error), console.error("Failed to refresh asset data:", error), showToast && toast.push({
1738
+ title: "Sync failed",
1739
+ description: "Could not sync asset from Mux",
1740
+ status: "error"
1741
+ }), options?.onError?.(error);
1742
+ return;
1743
+ }
1744
+ },
1745
+ [client, toast, options, showToast]
1746
+ );
1747
+ return {
1748
+ resyncState,
1749
+ resyncError,
1750
+ resyncAsset,
1751
+ isResyncing: resyncState === "syncing"
1752
+ };
1753
+ }
1468
1754
  function extractErrorMessage(error, defaultMessage = "Failed to process request") {
1469
1755
  let message = "";
1470
1756
  if (error && typeof error == "object") {
@@ -1548,9 +1834,9 @@ async function downloadVttFile(client, asset, track) {
1548
1834
  const playbackId = getPlaybackId(asset);
1549
1835
  if (!playbackId)
1550
1836
  throw new Error("Playback ID is required");
1551
- const playbackPolicy = getPlaybackPolicy(asset);
1837
+ const playbackPolicy = getPlaybackPolicy(asset)?.policy;
1552
1838
  let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
1553
- if (playbackPolicy === "signed") {
1839
+ if (playbackPolicy === "signed" || playbackPolicy === "drm") {
1554
1840
  const token = generateJwt(client, playbackId, "v");
1555
1841
  downloadUrl += `?token=${token}`;
1556
1842
  }
@@ -2028,7 +2314,7 @@ function EditCaptionDialog({ asset, track, onUpdate, onClose }) {
2028
2314
  const playbackId = getPlaybackId(asset);
2029
2315
  if (!playbackId) return "";
2030
2316
  let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
2031
- if (getPlaybackPolicy(asset) === "signed") {
2317
+ if (getPlaybackPolicy(asset)?.policy === "signed") {
2032
2318
  const token = generateJwt(client, playbackId, "v");
2033
2319
  url += `?token=${token}`;
2034
2320
  }
@@ -2367,20 +2653,7 @@ function TextTracksManager({
2367
2653
  tracks: propTracks,
2368
2654
  collapseTracks = !1
2369
2655
  }) {
2370
- const client = useClient(), toast = ui.useToast(), dialogId = `DeleteCaptionDialog${React.useId()}`, [downloadingTrackId, setDownloadingTrackId] = React.useState(null), [deletingTrackId, setDeletingTrackId] = React.useState(null), [addedTracks, setAddedTracks] = React.useState([]), [updatedTracks, setUpdatedTracks] = React.useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = React.useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = React.useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = React.useState(null), [trackToEdit, setTrackToEdit] = React.useState(null), [showAddDialog, setShowAddDialog] = React.useState(!1), [isExpanded, setIsExpanded] = React.useState(!1), MAX_VISIBLE_TRACKS = 4;
2371
- React.useEffect(() => {
2372
- if (!asset.assetId || !asset._id) return;
2373
- const assetId = asset.assetId, documentId = asset._id;
2374
- (async () => {
2375
- try {
2376
- const response = await getAsset(client, assetId);
2377
- await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2378
- } catch (error) {
2379
- console.error("Failed to refresh asset data:", error);
2380
- }
2381
- })();
2382
- }, [asset.assetId, asset._id, client]);
2383
- const activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2656
+ const client = useClient(), toast = ui.useToast(), dialogId = `DeleteCaptionDialog${React.useId()}`, { resyncAsset } = useResyncAsset(), [downloadingTrackId, setDownloadingTrackId] = React.useState(null), [deletingTrackId, setDeletingTrackId] = React.useState(null), [addedTracks, setAddedTracks] = React.useState([]), [updatedTracks, setUpdatedTracks] = React.useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = React.useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = React.useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = React.useState(null), [trackToEdit, setTrackToEdit] = React.useState(null), [showAddDialog, setShowAddDialog] = React.useState(!1), [isExpanded, setIsExpanded] = React.useState(!1), MAX_VISIBLE_TRACKS = 4, activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2384
2657
  (track) => track.id && (track.status === "ready" || track.status === "preparing" || track.status === "errored")
2385
2658
  ), allTracks = React.useMemo(() => {
2386
2659
  const tracksWithUpdates = activeTracks.map((track) => updatedTracks.get(track.id) || track), isMockTrackReplaced = (mockTrack, realTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : realTracksList.some((realTrack) => {
@@ -2411,11 +2684,11 @@ function TextTracksManager({
2411
2684
  }, [activeTracks, addedTracks]), React.useEffect(() => {
2412
2685
  if (allTracks.filter((track) => track.status === "preparing").length === 0 || !asset.assetId || !asset._id)
2413
2686
  return;
2414
- const assetId = asset.assetId, documentId = asset._id, interval = setInterval(async () => {
2687
+ const interval = setInterval(async () => {
2415
2688
  try {
2416
- const response = await getAsset(client, assetId);
2417
- await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2418
- const fetchedTracks = response.data.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2689
+ const muxData = await resyncAsset(asset);
2690
+ if (!muxData) return;
2691
+ const fetchedTracks = muxData.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2419
2692
  const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2420
2693
  return !nameMatches || !languageMatches ? !1 : realTrack.status === "ready" ? realTrack.text_source === "generated_live" || realTrack.text_source === "generated_live_final" || realTrack.text_source === "generated_vod" : realTrack.status === "preparing";
2421
2694
  }), newAutogeneratedIds = /* @__PURE__ */ new Set();
@@ -2452,7 +2725,7 @@ function TextTracksManager({
2452
2725
  }
2453
2726
  }, 3e3);
2454
2727
  return () => clearInterval(interval);
2455
- }, [allTracks, asset.assetId, asset._id, client]);
2728
+ }, [allTracks, asset, resyncAsset]);
2456
2729
  const visibleTracks = allTracks.filter(
2457
2730
  (track) => track.status === "ready" || track.status === "preparing" || track.status === "errored"
2458
2731
  ).sort((a2, b) => {
@@ -2488,14 +2761,7 @@ function TextTracksManager({
2488
2761
  try {
2489
2762
  if (!asset.assetId)
2490
2763
  throw new Error("Asset ID is required");
2491
- if (await deleteTextTrack(client, asset.assetId, track.id), asset._id)
2492
- try {
2493
- const response = await getAsset(client, asset.assetId);
2494
- await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2495
- } catch (refreshError) {
2496
- console.error("Failed to refresh asset data:", refreshError);
2497
- }
2498
- toast.push({
2764
+ await deleteTextTrack(client, asset.assetId, track.id), await resyncAsset(asset), toast.push({
2499
2765
  title: "Successfully deleted caption track",
2500
2766
  status: "success"
2501
2767
  }), setAddedTracks((prev) => prev.filter((t) => t.id !== track.id)), setUpdatedTracks((prev) => {
@@ -2523,7 +2789,7 @@ function TextTracksManager({
2523
2789
  return newMap.set(track.id, prev.size + 1), newMap;
2524
2790
  }), setShowAddDialog(!1);
2525
2791
  }, handleUpdateTrack = async (updatedTrack, oldTrackId) => {
2526
- if (oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2792
+ oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2527
2793
  const newMap = new Map(prev);
2528
2794
  return newMap.delete(oldTrackId), newMap;
2529
2795
  }), setTrackActivityOrder((prev) => {
@@ -2538,13 +2804,7 @@ function TextTracksManager({
2538
2804
  }), setTrackActivityOrder((prev) => {
2539
2805
  const newMap = new Map(prev);
2540
2806
  return newMap.set(updatedTrack.id, prev.size + 1), newMap;
2541
- }), setTrackToEdit(null), asset._id && asset.assetId)
2542
- try {
2543
- const response = await getAsset(client, asset.assetId);
2544
- await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2545
- } catch (refreshError) {
2546
- console.error("Failed to refresh asset data:", refreshError);
2547
- }
2807
+ }), setTrackToEdit(null), await resyncAsset(asset);
2548
2808
  }, getTrackSourceLabel = (track) => track.id && track.id.startsWith("generating-") || track.id && autogeneratedTrackIds.has(track.id) || track.text_source === "generated_live_final" || track.text_source === "generated_live" || track.text_source === "generated_vod" ? "Auto-generated" : track.text_source === "uploaded" ? "Uploaded" : "Custom";
2549
2809
  if (visibleTracks.length === 0 && !showAddDialog)
2550
2810
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
@@ -2684,13 +2944,13 @@ const DialogStateContext = React.createContext({
2684
2944
  setDialogState,
2685
2945
  children
2686
2946
  }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
2687
- function getVideoSrc({ asset, client }) {
2688
- const playbackId = getPlaybackId(asset), searchParams = new URLSearchParams();
2689
- if (getPlaybackPolicy(asset) === "signed") {
2690
- const token = generateJwt(client, playbackId, "v");
2947
+ function getVideoSrc({ client, muxPlaybackId: muxPlaybackId2 }) {
2948
+ const searchParams = new URLSearchParams();
2949
+ if (muxPlaybackId2.policy === "signed" || muxPlaybackId2.policy === "drm") {
2950
+ const token = generateJwt(client, muxPlaybackId2.id, "v");
2691
2951
  searchParams.set("token", token);
2692
2952
  }
2693
- return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
2953
+ return `https://stream.mux.com/${muxPlaybackId2.id}.m3u8?${searchParams}`;
2694
2954
  }
2695
2955
  function CaptionsDialog({ asset }) {
2696
2956
  const { setDialogState } = useDialogStateContext(), dialogId = `CaptionsDialog${React.useId()}`;
@@ -2800,24 +3060,56 @@ function VideoPlayer({
2800
3060
  hlsConfig,
2801
3061
  ...props
2802
3062
  }) {
2803
- const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), {
2804
- src: videoSrc,
2805
- thumbnail: thumbnailSrc,
2806
- error
2807
- } = React.useMemo(() => {
3063
+ const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), [error, setError] = React.useState(), playbackId = React.useMemo(() => {
3064
+ try {
3065
+ return getPlaybackId(asset, ["public", "signed", "drm"]);
3066
+ } catch {
3067
+ setError(new TypeError("Asset has no playback ID"));
3068
+ return;
3069
+ }
3070
+ }, [asset]), muxPlaybackId2 = React.useMemo(() => {
3071
+ if (playbackId)
3072
+ return getPlaybackPolicyById(asset, playbackId);
3073
+ }, [asset, playbackId]), src = React.useMemo(() => {
3074
+ if (playbackId && muxPlaybackId2)
3075
+ return tryWithSuspend(
3076
+ () => getVideoSrc({ muxPlaybackId: muxPlaybackId2, client }),
3077
+ (e) => {
3078
+ setError(e);
3079
+ }
3080
+ );
3081
+ }, [muxPlaybackId2, playbackId, client]), poster = React.useMemo(() => tryWithSuspend(
3082
+ () => getPosterSrc({ asset, client, width: thumbnailWidth }),
3083
+ (e) => {
3084
+ setError(e);
3085
+ }
3086
+ ), [asset, client, thumbnailWidth]), signedToken = React.useMemo(() => {
2808
3087
  try {
2809
- const thumbnail = getPosterSrc({ asset, client, width: thumbnailWidth }), src = asset?.playbackId && getVideoSrc({ client, asset });
2810
- return src ? { src, thumbnail } : { error: new TypeError("Asset has no playback ID") };
2811
- } catch (error2) {
2812
- return { error: error2 };
3088
+ return new URL(src).searchParams.get("token");
3089
+ } catch {
3090
+ return;
2813
3091
  }
2814
- }, [asset, client, thumbnailWidth]), signedToken = React.useMemo(() => {
3092
+ }, [src]), drmToken = React.useMemo(() => {
3093
+ if (playbackId && muxPlaybackId2?.policy === "drm")
3094
+ return tryWithSuspend(
3095
+ () => generateJwt(client, playbackId, "d"),
3096
+ (e) => {
3097
+ setError(e);
3098
+ }
3099
+ );
3100
+ }, [client, muxPlaybackId2?.policy, playbackId]), tokens = React.useMemo(() => {
2815
3101
  try {
2816
- return new URL(videoSrc).searchParams.get("token");
3102
+ const partialTokens = {
3103
+ playback: void 0,
3104
+ thumbnail: void 0,
3105
+ storyboard: void 0,
3106
+ drm: void 0
3107
+ };
3108
+ return signedToken && (partialTokens.playback = signedToken, partialTokens.thumbnail = signedToken, partialTokens.storyboard = signedToken), drmToken && (partialTokens.drm = drmToken), { ...partialTokens };
2817
3109
  } catch {
2818
- return !1;
3110
+ return;
2819
3111
  }
2820
- }, [videoSrc]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
3112
+ }, [signedToken, drmToken]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
2821
3113
  let aspectRatio = Math.max(MIN_ASPECT_RATIO, targetAspectRatio);
2822
3114
  return isAudio && (aspectRatio = props.forceAspectRatio ? (
2823
3115
  // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
@@ -2833,7 +3125,7 @@ function VideoPlayer({
2833
3125
  ...isAudio && { display: "flex", alignItems: "flex-end" }
2834
3126
  },
2835
3127
  children: [
2836
- videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3128
+ src && poster && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2837
3129
  isAudio && /* @__PURE__ */ jsxRuntime.jsx(
2838
3130
  AudioIcon,
2839
3131
  {
@@ -2848,34 +3140,36 @@ function VideoPlayer({
2848
3140
  }
2849
3141
  }
2850
3142
  ),
2851
- /* @__PURE__ */ jsxRuntime.jsx(
2852
- MuxPlayer__default.default,
2853
- {
2854
- poster: isAudio ? void 0 : thumbnailSrc,
2855
- ref: muxPlayer,
2856
- ...props,
2857
- playsInline: !0,
2858
- playbackId: asset.playbackId,
2859
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
2860
- preload: "metadata",
2861
- crossOrigin: "anonymous",
2862
- metadata: {
2863
- player_name: "Sanity Admin Dashboard",
2864
- player_version: "2.14.0",
2865
- page_type: "Preview Player"
2866
- },
2867
- audio: isAudio,
2868
- _hlsConfig: hlsConfig,
2869
- style: {
2870
- ...!isAudio && { height: "100%" },
2871
- width: "100%",
2872
- display: "block",
2873
- objectFit: "contain",
2874
- ...isAudio && { alignSelf: "end" }
3143
+ /* @__PURE__ */ jsxRuntime.jsxs(React.Suspense, { fallback: null, children: [
3144
+ /* @__PURE__ */ jsxRuntime.jsx(
3145
+ MuxPlayer__default.default,
3146
+ {
3147
+ poster: isAudio ? void 0 : poster,
3148
+ ref: muxPlayer,
3149
+ ...props,
3150
+ playsInline: !0,
3151
+ playbackId,
3152
+ tokens,
3153
+ preload: "metadata",
3154
+ crossOrigin: "anonymous",
3155
+ metadata: {
3156
+ player_name: "Sanity Admin Dashboard",
3157
+ player_version: "2.16.0",
3158
+ page_type: "Preview Player"
3159
+ },
3160
+ audio: isAudio,
3161
+ _hlsConfig: hlsConfig,
3162
+ style: {
3163
+ ...!isAudio && { height: "100%" },
3164
+ width: "100%",
3165
+ display: "block",
3166
+ objectFit: "contain",
3167
+ ...isAudio && { alignSelf: "end" }
3168
+ }
2875
3169
  }
2876
- }
2877
- ),
2878
- children
3170
+ ),
3171
+ children
3172
+ ] })
2879
3173
  ] }),
2880
3174
  error ? /* @__PURE__ */ jsxRuntime.jsx(
2881
3175
  "div",
@@ -3167,6 +3461,7 @@ function getVideoMetadata(doc) {
3167
3461
  playbackId: doc.playbackId,
3168
3462
  createdAt: date,
3169
3463
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
3464
+ playback_ids: doc.data?.playback_ids,
3170
3465
  aspect_ratio: doc.data?.aspect_ratio,
3171
3466
  max_stored_resolution: doc.data?.max_stored_resolution,
3172
3467
  max_stored_frame_rate: doc.data?.max_stored_frame_rate,
@@ -3176,7 +3471,10 @@ function getVideoMetadata(doc) {
3176
3471
  function useVideoDetails(props) {
3177
3472
  const documentStore = sanity.useDocumentStore(), toast = ui.useToast(), client = useClient(), [references, referencesLoading] = useDocReferences(
3178
3473
  React.useMemo(() => ({ documentStore, id: props.asset._id }), [documentStore, props.asset._id])
3179
- ), [originalAsset, setOriginalAsset] = React.useState(() => props.asset), [filename, setFilename] = React.useState(props.asset.filename), modified = filename !== originalAsset.filename, displayInfo = getVideoMetadata({ ...props.asset, filename }), [state, setState] = React.useState("idle");
3474
+ ), [originalAsset, setOriginalAsset] = React.useState(() => props.asset), [filename, setFilename] = React.useState(props.asset.filename), modified = filename !== originalAsset.filename, displayInfo = getVideoMetadata({ ...props.asset, filename }), [state, setState] = React.useState("idle"), { resyncAsset, isResyncing } = useResyncAsset({ showToast: !0 });
3475
+ async function handleResync() {
3476
+ state === "idle" && (setState("resyncing"), await resyncAsset(props.asset), setState("idle"));
3477
+ }
3180
3478
  function handleClose() {
3181
3479
  if (state === "idle") {
3182
3480
  if (modified) {
@@ -3219,7 +3517,9 @@ function useVideoDetails(props) {
3219
3517
  setState,
3220
3518
  handleClose,
3221
3519
  confirmClose,
3222
- saveChanges
3520
+ saveChanges,
3521
+ handleResync,
3522
+ isResyncing
3223
3523
  };
3224
3524
  }
3225
3525
  const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: props.label, description: props.description, inputId: props.label, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -3243,7 +3543,9 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
3243
3543
  setState,
3244
3544
  handleClose,
3245
3545
  confirmClose,
3246
- saveChanges
3546
+ saveChanges,
3547
+ handleResync,
3548
+ isResyncing
3247
3549
  } = useVideoDetails(props), isSaving = state === "saving", [containerHeight, setContainerHeight] = React.useState(null), contentsRef = React__default.default.useRef(null);
3248
3550
  return React.useEffect(() => {
3249
3551
  !contentsRef.current || !("getBoundingClientRect" in contentsRef.current) || setContainerHeight(contentsRef.current.getBoundingClientRect().height);
@@ -3259,19 +3561,35 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
3259
3561
  width: 2,
3260
3562
  position: "fixed",
3261
3563
  footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
3262
- /* @__PURE__ */ jsxRuntime.jsx(
3263
- ui.Button,
3264
- {
3265
- icon: icons.TrashIcon,
3266
- fontSize: 2,
3267
- padding: 3,
3268
- mode: "bleed",
3269
- text: "Delete",
3270
- tone: "critical",
3271
- onClick: () => setState("deleting"),
3272
- disabled: isSaving
3273
- }
3274
- ),
3564
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
3565
+ /* @__PURE__ */ jsxRuntime.jsx(
3566
+ ui.Button,
3567
+ {
3568
+ icon: icons.TrashIcon,
3569
+ fontSize: 2,
3570
+ padding: 3,
3571
+ mode: "bleed",
3572
+ text: "Delete",
3573
+ tone: "critical",
3574
+ onClick: () => setState("deleting"),
3575
+ disabled: isSaving || isResyncing
3576
+ }
3577
+ ),
3578
+ /* @__PURE__ */ jsxRuntime.jsx(
3579
+ ui.Button,
3580
+ {
3581
+ icon: icons.SyncIcon,
3582
+ fontSize: 2,
3583
+ padding: 3,
3584
+ mode: "bleed",
3585
+ text: "Resync",
3586
+ tone: "primary",
3587
+ onClick: handleResync,
3588
+ disabled: isSaving || isResyncing,
3589
+ iconRight: isResyncing && ui.Spinner
3590
+ }
3591
+ )
3592
+ ] }),
3275
3593
  modified && /* @__PURE__ */ jsxRuntime.jsx(
3276
3594
  ui.Button,
3277
3595
  {
@@ -3283,7 +3601,7 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
3283
3601
  tone: "positive",
3284
3602
  onClick: saveChanges,
3285
3603
  iconRight: isSaving && ui.Spinner,
3286
- disabled: isSaving
3604
+ disabled: isSaving || isResyncing
3287
3605
  }
3288
3606
  )
3289
3607
  ] }) }),
@@ -3469,14 +3787,7 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
3469
3787
  ),
3470
3788
  /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: `Mux ID:
3471
3789
  ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
3472
- displayInfo?.playbackId && /* @__PURE__ */ jsxRuntime.jsx(
3473
- IconInfo,
3474
- {
3475
- text: `Playback ID: ${displayInfo.playbackId}`,
3476
- icon: icons.TagIcon,
3477
- size: 2
3478
- }
3479
- )
3790
+ /* @__PURE__ */ jsxRuntime.jsx(PlaybackIds, { playback_ids: displayInfo.playback_ids })
3480
3791
  ] })
3481
3792
  ] })
3482
3793
  }
@@ -3499,9 +3810,28 @@ ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
3499
3810
  ]
3500
3811
  }
3501
3812
  );
3502
- }, VideoMetadata = (props) => {
3503
- if (!props.asset)
3504
- return null;
3813
+ }, PlaybackIds = ({ playback_ids }) => playback_ids ? playback_ids.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
3814
+ IconInfo,
3815
+ {
3816
+ text: `Playback ID [${policyToText(entry.policy)}]: ${entry.id}`,
3817
+ icon: icons.TagIcon,
3818
+ size: 2
3819
+ },
3820
+ entry.id
3821
+ )) : /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: "No Playback ID", icon: icons.TagIcon, size: 2 }), policyToText = (policy) => {
3822
+ switch (policy) {
3823
+ case "drm":
3824
+ return "DRM";
3825
+ case "signed":
3826
+ return "Signed";
3827
+ case "public":
3828
+ return "Public";
3829
+ default:
3830
+ return policy;
3831
+ }
3832
+ }, VideoMetadata = (props) => {
3833
+ if (!props.asset)
3834
+ return null;
3505
3835
  const displayInfo = getVideoMetadata(props.asset);
3506
3836
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3507
3837
  displayInfo.title && /* @__PURE__ */ jsxRuntime.jsx(
@@ -3592,10 +3922,12 @@ function VideoInBrowser({
3592
3922
  onEdit,
3593
3923
  asset
3594
3924
  }) {
3595
- const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]);
3925
+ const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]), { hasShownWarning } = useDrmPlaybackWarningContext();
3596
3926
  if (!asset)
3597
3927
  return null;
3598
- const playbackPolicy = getPlaybackPolicy(asset);
3928
+ const playbackPolicy = getPlaybackPolicy(asset), onClickPlay = () => {
3929
+ playbackPolicy?.policy === "drm" && !hasShownWarning ? setRenderVideo("pre-render-warn") : setRenderVideo("render-video");
3930
+ };
3599
3931
  return /* @__PURE__ */ jsxRuntime.jsxs(
3600
3932
  ui.Card,
3601
3933
  {
@@ -3607,7 +3939,7 @@ function VideoInBrowser({
3607
3939
  position: "relative"
3608
3940
  },
3609
3941
  children: [
3610
- playbackPolicy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
3942
+ playbackPolicy?.policy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
3611
3943
  ui.Tooltip,
3612
3944
  {
3613
3945
  animate: !0,
@@ -3624,7 +3956,7 @@ function VideoInBrowser({
3624
3956
  position: "absolute",
3625
3957
  left: "1em",
3626
3958
  top: "1em",
3627
- zIndex: 10
3959
+ zIndex: 11
3628
3960
  },
3629
3961
  padding: 2,
3630
3962
  border: !0,
@@ -3633,6 +3965,32 @@ function VideoInBrowser({
3633
3965
  )
3634
3966
  }
3635
3967
  ),
3968
+ playbackPolicy?.policy === "drm" && /* @__PURE__ */ jsxRuntime.jsx(
3969
+ ui.Tooltip,
3970
+ {
3971
+ animate: !0,
3972
+ content: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { icon: icons.LockIcon, text: "DRM playback policy", size: 2 }) }),
3973
+ placement: "right",
3974
+ fallbackPlacements: ["top", "bottom"],
3975
+ portal: !0,
3976
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3977
+ ui.Card,
3978
+ {
3979
+ tone: "caution",
3980
+ style: {
3981
+ borderRadius: "0.25rem",
3982
+ position: "absolute",
3983
+ left: "1em",
3984
+ top: "1em",
3985
+ zIndex: 11
3986
+ },
3987
+ padding: 2,
3988
+ border: !0,
3989
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, weight: "semibold", style: { color: "var(--card-icon-color)" }, children: "DRM" })
3990
+ }
3991
+ )
3992
+ }
3993
+ ),
3636
3994
  /* @__PURE__ */ jsxRuntime.jsxs(
3637
3995
  ui.Stack,
3638
3996
  {
@@ -3642,7 +4000,15 @@ function VideoInBrowser({
3642
4000
  gridTemplateRows: "min-content min-content 1fr"
3643
4001
  },
3644
4002
  children: [
3645
- renderVideo ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: () => setRenderVideo(!0), children: [
4003
+ renderVideo === "pre-render-warn" && /* @__PURE__ */ jsxRuntime.jsx(
4004
+ DRMWarningDialog,
4005
+ {
4006
+ onClose: () => {
4007
+ setRenderVideo("render-video");
4008
+ }
4009
+ }
4010
+ ),
4011
+ renderVideo === "render-video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: onClickPlay, children: [
3646
4012
  /* @__PURE__ */ jsxRuntime.jsx("div", { "data-play": !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PlayIcon, {}) }),
3647
4013
  assetIsAudio(asset) ? /* @__PURE__ */ jsxRuntime.jsx(
3648
4014
  "div",
@@ -3704,12 +4070,12 @@ function VideoInBrowser({
3704
4070
  }
3705
4071
  );
3706
4072
  }
3707
- function VideosBrowser({ onSelect }) {
4073
+ function VideosBrowser({ onSelect, config }) {
3708
4074
  const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [page, setPage] = React.useState(0), pageLimit = 20, pageTotal = Math.floor(assets.length / pageLimit) + 1, [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
3709
4075
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
3710
4076
  [editedAsset, assets]
3711
4077
  ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
3712
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4078
+ return /* @__PURE__ */ jsxRuntime.jsxs(DrmPlaybackWarningContextProvider, { config, children: [
3713
4079
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
3714
4080
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
3715
4081
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
@@ -3723,7 +4089,7 @@ function VideosBrowser({ onSelect }) {
3723
4089
  }
3724
4090
  ),
3725
4091
  /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort }),
3726
- /* @__PURE__ */ jsxRuntime.jsx(PageSelector, { page, setPage, total: pageTotal, limit: pageLimit })
4092
+ /* @__PURE__ */ jsxRuntime.jsx(PageSelector, { page, setPage, total: pageTotal })
3727
4093
  ] }),
3728
4094
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
3729
4095
  /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {}),
@@ -3764,7 +4130,7 @@ function VideosBrowser({ onSelect }) {
3764
4130
  freshEditedAsset && /* @__PURE__ */ jsxRuntime.jsx(VideoDetails, { closeDialog: () => setEditedAsset(null), asset: freshEditedAsset })
3765
4131
  ] });
3766
4132
  }
3767
- const StudioTool = () => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, {}), DEFAULT_TOOL_CONFIG = {
4133
+ const StudioTool = (config) => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { config }), DEFAULT_TOOL_CONFIG = {
3768
4134
  icon: ToolIcon,
3769
4135
  title: "Videos"
3770
4136
  };
@@ -4122,6 +4488,9 @@ function isValidUrl(url) {
4122
4488
  return !1;
4123
4489
  }
4124
4490
  }
4491
+ function isServerError(error) {
4492
+ return "statusCode" in error && typeof error.statusCode == "number" && 500 <= error.statusCode && error.statusCode <= 600;
4493
+ }
4125
4494
  function extractDroppedFiles(dataTransfer) {
4126
4495
  const files = Array.from(dataTransfer.files || []), items = Array.from(dataTransfer.items || []);
4127
4496
  return files && files.length > 0 ? Promise.resolve(files) : normalizeItems(items).then((arr) => arr.flat());
@@ -4163,7 +4532,12 @@ function walk(entry) {
4163
4532
  }
4164
4533
  return Promise.resolve([]);
4165
4534
  }
4166
- function SelectAssets({ asset: selectedAsset, onChange, setDialogState }) {
4535
+ function SelectAssets({
4536
+ asset: selectedAsset,
4537
+ onChange,
4538
+ setDialogState,
4539
+ config
4540
+ }) {
4167
4541
  const handleSelect = React.useCallback(
4168
4542
  (chosenAsset) => {
4169
4543
  chosenAsset?._id || onChange(sanity.PatchEvent.from([sanity.unset(["asset"])])), chosenAsset._id !== selectedAsset?._id && onChange(
@@ -4175,7 +4549,7 @@ function SelectAssets({ asset: selectedAsset, onChange, setDialogState }) {
4175
4549
  },
4176
4550
  [onChange, setDialogState, selectedAsset]
4177
4551
  );
4178
- return /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { onSelect: handleSelect });
4552
+ return /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { onSelect: handleSelect, config });
4179
4553
  }
4180
4554
  const StyledDialog = styledComponents.styled(ui.Dialog)`
4181
4555
  > div[data-ui='DialogCard'] > div[data-ui='Card'] {
@@ -4185,7 +4559,8 @@ const StyledDialog = styledComponents.styled(ui.Dialog)`
4185
4559
  function InputBrowser({
4186
4560
  setDialogState,
4187
4561
  asset,
4188
- onChange
4562
+ onChange,
4563
+ config
4189
4564
  }) {
4190
4565
  const id = `InputBrowser${React.useId()}`, handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]);
4191
4566
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4196,7 +4571,15 @@ function InputBrowser({
4196
4571
  id,
4197
4572
  onClose: handleClose,
4198
4573
  width: 2,
4199
- children: /* @__PURE__ */ jsxRuntime.jsx(SelectAssets, { asset, onChange, setDialogState })
4574
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4575
+ SelectAssets,
4576
+ {
4577
+ config,
4578
+ asset,
4579
+ onChange,
4580
+ setDialogState
4581
+ }
4582
+ )
4200
4583
  }
4201
4584
  );
4202
4585
  }
@@ -4412,7 +4795,9 @@ const FileButton = styledComponents.styled(ui.MenuItem)(({ theme }) => {
4412
4795
  color: white;
4413
4796
  `, isVideoAsset = (asset) => asset._type === "mux.videoAsset";
4414
4797
  function PlayerActionsMenu(props) {
4415
- const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
4798
+ const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset)?.policy === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), { resyncAsset, isResyncing } = useResyncAsset({ showToast: !0 }), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]), handleResync = React.useCallback(async () => {
4799
+ setOpen(!1), await resyncAsset(asset);
4800
+ }, [resyncAsset, asset]);
4416
4801
  return React.useEffect(() => {
4417
4802
  open && dialogState && setOpen(!1);
4418
4803
  }, [dialogState, open]), ui.useClickOutsideEvent(
@@ -4470,6 +4855,15 @@ function PlayerActionsMenu(props) {
4470
4855
  text: "Captions",
4471
4856
  onClick: () => setDialogState("edit-captions")
4472
4857
  }
4858
+ ),
4859
+ /* @__PURE__ */ jsxRuntime.jsx(
4860
+ ui.MenuItem,
4861
+ {
4862
+ icon: icons.SyncIcon,
4863
+ text: "Resync from Mux",
4864
+ onClick: handleResync,
4865
+ disabled: readOnly || isResyncing
4866
+ }
4473
4867
  )
4474
4868
  ] }),
4475
4869
  /* @__PURE__ */ jsxRuntime.jsx(ui.MenuDivider, {}),
@@ -4513,6 +4907,79 @@ function PlayerActionsMenu(props) {
4513
4907
  ] });
4514
4908
  }
4515
4909
  var PlayerActionsMenu$1 = React.memo(PlayerActionsMenu);
4910
+ function useFetchFileSize(stagedUpload, maxFileSize) {
4911
+ const [fileSize, setFileSize] = React.useState(null), [isLoadingFileSize, setIsLoadingFileSize] = React.useState(!1), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = React.useState(!1);
4912
+ return React.useEffect(() => {
4913
+ if (stagedUpload.type === "url") {
4914
+ setIsLoadingFileSize(!1), setCanSkipFileSizeValidation(!1), setFileSize(null);
4915
+ const url = stagedUpload.url;
4916
+ (async () => {
4917
+ setIsLoadingFileSize(!0);
4918
+ try {
4919
+ const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), newFileSize = contentLength ? parseInt(contentLength, 10) : null;
4920
+ setIsLoadingFileSize(!1), newFileSize && setFileSize(newFileSize), newFileSize === null && maxFileSize !== void 0 && setCanSkipFileSizeValidation(!0);
4921
+ } catch {
4922
+ console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), setIsLoadingFileSize(!1);
4923
+ }
4924
+ })();
4925
+ }
4926
+ stagedUpload.type === "file" && setFileSize(stagedUpload.files[0].size);
4927
+ }, [maxFileSize, stagedUpload, stagedUpload.type]), {
4928
+ fileSize,
4929
+ isLoadingFileSize,
4930
+ canSkipFileSizeValidation
4931
+ };
4932
+ }
4933
+ function useMediaMetadata(stagedUpload) {
4934
+ const [videoAssetMetadata, setVideoAssetMetadata] = React.useState(null), [isLoadingMetadata, setIsLoadingMetadata] = React.useState(!1);
4935
+ return React.useEffect(() => {
4936
+ let videoSrc = null;
4937
+ if (stagedUpload.type === "file") {
4938
+ const file = stagedUpload.files[0];
4939
+ videoSrc = URL.createObjectURL(file);
4940
+ }
4941
+ if (stagedUpload.type === "url" && (videoSrc = stagedUpload.url), setVideoAssetMetadata((old) => ({
4942
+ ...old,
4943
+ duration: void 0,
4944
+ width: void 0,
4945
+ height: void 0
4946
+ })), !videoSrc) return () => null;
4947
+ setIsLoadingMetadata(!0);
4948
+ const videoElement = document.createElement("video");
4949
+ videoElement.preload = "metadata";
4950
+ const metadataListeners = [
4951
+ () => {
4952
+ setIsLoadingMetadata(!1);
4953
+ },
4954
+ () => {
4955
+ const duration = videoElement.duration, width = videoElement.videoWidth, height = videoElement.videoHeight, isAudioOnly = width <= 0 && height <= 0;
4956
+ setVideoAssetMetadata((old) => ({
4957
+ ...old,
4958
+ duration,
4959
+ width,
4960
+ height,
4961
+ isAudioOnly
4962
+ }));
4963
+ }
4964
+ ], cleanupVideo = (videoEl) => {
4965
+ const currentVideoSrc = videoEl?.src;
4966
+ videoEl && (metadataListeners.forEach(
4967
+ (listener) => videoEl.removeEventListener("loadedmetadata", listener)
4968
+ ), videoEl.onerror = null, videoEl.src = "", videoEl.load()), currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc);
4969
+ };
4970
+ return metadataListeners.push(() => setTimeout(() => cleanupVideo(videoElement), 0)), videoElement.onerror = () => {
4971
+ setIsLoadingMetadata(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(videoElement);
4972
+ }, metadataListeners.forEach(
4973
+ (listener) => videoElement.addEventListener("loadedmetadata", listener)
4974
+ ), videoElement.src = videoSrc, () => {
4975
+ cleanupVideo(videoElement);
4976
+ };
4977
+ }, [stagedUpload.type, stagedUpload]), {
4978
+ videoAssetMetadata,
4979
+ setVideoAssetMetadata,
4980
+ isLoadingMetadata
4981
+ };
4982
+ }
4516
4983
  function formatBytes(bytes, si = !1, dp = 1) {
4517
4984
  const thresh = si ? 1e3 : 1024;
4518
4985
  if (Math.abs(bytes) < thresh)
@@ -4602,13 +5069,14 @@ function PlaybackPolicyOption({
4602
5069
  optionName,
4603
5070
  description,
4604
5071
  dispatch,
4605
- action
5072
+ action,
5073
+ disabled
4606
5074
  }) {
4607
5075
  const [scale, setScale] = React.useState(1), boxStyle = {
4608
5076
  outline: "0.01rem solid grey",
4609
5077
  transform: `scale(${scale})`,
4610
5078
  transition: "transform 0.1s ease-in-out",
4611
- cursor: "pointer",
5079
+ cursor: disabled ? "not-allowed" : "pointer",
4612
5080
  borderRadius: "0.25rem"
4613
5081
  }, triggerAnimation = () => {
4614
5082
  setScale(0.98), setTimeout(() => {
@@ -4616,15 +5084,24 @@ function PlaybackPolicyOption({
4616
5084
  }, 100);
4617
5085
  };
4618
5086
  return /* @__PURE__ */ jsxRuntime.jsx("label", { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, padding: 3, style: boxStyle, children: [
4619
- /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { id, required: !0, checked, onChange: () => {
4620
- triggerAnimation(), dispatch({
4621
- action,
4622
- value: !checked
4623
- });
4624
- } }),
5087
+ /* @__PURE__ */ jsxRuntime.jsx(
5088
+ ui.Checkbox,
5089
+ {
5090
+ id,
5091
+ required: !0,
5092
+ checked,
5093
+ onChange: () => {
5094
+ action && (triggerAnimation(), dispatch({
5095
+ action,
5096
+ value: !checked
5097
+ }));
5098
+ },
5099
+ disabled
5100
+ }
5101
+ ),
4625
5102
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { gap: 3, children: [
4626
5103
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 3, weight: "bold", children: optionName }),
4627
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: description })
5104
+ typeof description == "string" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: description }) : description
4628
5105
  ] })
4629
5106
  ] }) });
4630
5107
  }
@@ -4649,7 +5126,7 @@ function PlaybackPolicy({
4649
5126
  secrets,
4650
5127
  dispatch
4651
5128
  }) {
4652
- const noPolicySelected = !(config.public_policy || config.signed_policy);
5129
+ const noPolicySelected = !(config.public_policy || config.signed_policy || config.drm_policy), drmPolicyDisabled = !secrets.drmConfigId;
4653
5130
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { gap: 3, children: [
4654
5131
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", children: "Advanced Playback Policies" }),
4655
5132
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4658,7 +5135,10 @@ function PlaybackPolicy({
4658
5135
  id: `${id}--public`,
4659
5136
  checked: config.public_policy,
4660
5137
  optionName: "Public",
4661
- description: "Playback IDs are accessible by constructing an HLS URL like https://stream.mux.com/{PLAYBACK_ID}",
5138
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5139
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs are accessible by constructing an HLS URL like" }),
5140
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}" })
5141
+ ] }),
4662
5142
  dispatch,
4663
5143
  action: "public_policy"
4664
5144
  }
@@ -4669,24 +5149,146 @@ function PlaybackPolicy({
4669
5149
  id: `${id}--signed`,
4670
5150
  checked: config.signed_policy,
4671
5151
  optionName: "Signed",
4672
- description: `Playback IDs should be used with tokens https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}.
4673
- // See Secure video playback for details about creating tokens.`,
5152
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5153
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs should be used with tokens" }),
5154
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}" }),
5155
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
5156
+ "See",
5157
+ " ",
5158
+ /* @__PURE__ */ jsxRuntime.jsx(
5159
+ "a",
5160
+ {
5161
+ href: "https://www.mux.com/docs/guides/secure-video-playback",
5162
+ target: "_blank",
5163
+ rel: "noopener noreferrer",
5164
+ children: "Secure video playback"
5165
+ }
5166
+ ),
5167
+ " ",
5168
+ "for details about creating tokens."
5169
+ ] })
5170
+ ] }),
4674
5171
  dispatch,
4675
5172
  action: "signed_policy"
4676
5173
  }
4677
5174
  ),
5175
+ drmPolicyDisabled ? /* @__PURE__ */ jsxRuntime.jsx(
5176
+ PlaybackPolicyOption,
5177
+ {
5178
+ id: `${id}--drm`,
5179
+ checked: !1,
5180
+ optionName: "DRM - Disabled",
5181
+ description: /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
5182
+ "To enable DRM add your DRM Configuration Id to your plugin configuration in the API Credentials view.",
5183
+ " ",
5184
+ /* @__PURE__ */ jsxRuntime.jsx(
5185
+ "a",
5186
+ {
5187
+ href: "https://www.mux.com/support/human",
5188
+ target: "_blank",
5189
+ rel: "noopener noreferrer",
5190
+ children: "Contact us"
5191
+ }
5192
+ ),
5193
+ " ",
5194
+ "to get started using DRM."
5195
+ ] }) }),
5196
+ dispatch,
5197
+ disabled: !0
5198
+ }
5199
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
5200
+ PlaybackPolicyOption,
5201
+ {
5202
+ id: `${id}--drm`,
5203
+ checked: config.drm_policy,
5204
+ optionName: "DRM",
5205
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5206
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs should be used with tokens as with Signed playback, but require extra configuration." }),
5207
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}" }),
5208
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
5209
+ "See",
5210
+ " ",
5211
+ /* @__PURE__ */ jsxRuntime.jsx(
5212
+ "a",
5213
+ {
5214
+ href: "https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos",
5215
+ target: "_blank",
5216
+ rel: "noopener noreferrer",
5217
+ children: "Protect videos with DRM"
5218
+ }
5219
+ ),
5220
+ " ",
5221
+ "for details about configuring your player for DRM playback and",
5222
+ " ",
5223
+ /* @__PURE__ */ jsxRuntime.jsx(
5224
+ "a",
5225
+ {
5226
+ href: "https://www.mux.com/docs/guides/secure-video-playback",
5227
+ target: "_blank",
5228
+ rel: "noopener noreferrer",
5229
+ children: "Secure video playback"
5230
+ }
5231
+ ),
5232
+ " ",
5233
+ "for details about creating tokens."
5234
+ ] })
5235
+ ] }),
5236
+ dispatch,
5237
+ action: "drm_policy"
5238
+ }
5239
+ ),
4678
5240
  noPolicySelected && /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicyWarning, {})
4679
5241
  ] });
4680
5242
  }
4681
- const VIDEO_QUALITY_LEVELS = [
4682
- { value: "basic", label: "Basic" },
4683
- { value: "plus", label: "Plus" },
4684
- { value: "premium", label: "Premium" }
4685
- ], RESOLUTION_TIERS = [
5243
+ const RESOLUTION_TIERS = [
4686
5244
  { value: "1080p", label: "1080p" },
4687
5245
  { value: "1440p", label: "1440p (2k)" },
4688
5246
  { value: "2160p", label: "2160p (4k)" }
4689
- ], ADVANCED_RESOLUTIONS = [
5247
+ ], ResolutionTierSelector = ({
5248
+ id,
5249
+ config,
5250
+ dispatch,
5251
+ maxSupportedResolution
5252
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
5253
+ sanity.FormField,
5254
+ {
5255
+ title: "Resolution Tier",
5256
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5257
+ "The maximum",
5258
+ " ",
5259
+ /* @__PURE__ */ jsxRuntime.jsx(
5260
+ "a",
5261
+ {
5262
+ href: "https://docs.mux.com/api-reference#video/operation/create-direct-upload",
5263
+ target: "_blank",
5264
+ rel: "noopener noreferrer",
5265
+ children: "resolution_tier"
5266
+ }
5267
+ ),
5268
+ " ",
5269
+ "your asset is encoded, stored, and streamed at."
5270
+ ] }),
5271
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, wrap: "wrap", children: RESOLUTION_TIERS.map(({ value, label }, index) => {
5272
+ const inputId = `${id}--type-${value}`;
5273
+ return index > maxSupportedResolution ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5274
+ /* @__PURE__ */ jsxRuntime.jsx(
5275
+ ui.Radio,
5276
+ {
5277
+ checked: config.max_resolution_tier === value,
5278
+ name: "asset-resolutiontier",
5279
+ onChange: (e) => dispatch({
5280
+ action: "max_resolution_tier",
5281
+ value: e.currentTarget.value
5282
+ }),
5283
+ value,
5284
+ id: inputId
5285
+ }
5286
+ ),
5287
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, children: label })
5288
+ ] }, value);
5289
+ }) })
5290
+ }
5291
+ ), ADVANCED_RESOLUTIONS = [
4690
5292
  { value: "270p", label: "270p" },
4691
5293
  { value: "360p", label: "360p" },
4692
5294
  { value: "480p", label: "480p" },
@@ -4695,6 +5297,130 @@ const VIDEO_QUALITY_LEVELS = [
4695
5297
  { value: "1080p", label: "1080p" },
4696
5298
  { value: "1440p", label: "1440p" },
4697
5299
  { value: "2160p", label: "2160p" }
5300
+ ], StaticRenditionSelector = ({
5301
+ id,
5302
+ config,
5303
+ dispatch
5304
+ }) => {
5305
+ const isAdvancedMode = React.useMemo(() => config.static_renditions.filter(
5306
+ (r) => r !== "highest" && r !== "audio-only"
5307
+ ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = React.useState(
5308
+ isAdvancedMode ? "advanced" : "standard"
5309
+ ), toggleRendition = (rendition) => {
5310
+ const current = config.static_renditions, hasRendition = current.includes(rendition);
5311
+ dispatch(hasRendition ? {
5312
+ action: "static_renditions",
5313
+ value: current.filter((r) => r !== rendition)
5314
+ } : {
5315
+ action: "static_renditions",
5316
+ value: [...current, rendition]
5317
+ });
5318
+ }, handleModeChange = (mode) => {
5319
+ setRenditionMode(mode), dispatch(mode === "standard" ? {
5320
+ action: "static_renditions",
5321
+ value: config.static_renditions.filter((r) => r === "highest" || r === "audio-only")
5322
+ } : {
5323
+ action: "static_renditions",
5324
+ value: config.static_renditions.filter((r) => r !== "highest")
5325
+ });
5326
+ };
5327
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
5328
+ sanity.FormField,
5329
+ {
5330
+ title: "Static Renditions",
5331
+ description: "Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped.",
5332
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
5333
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, children: [
5334
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5335
+ /* @__PURE__ */ jsxRuntime.jsx(
5336
+ ui.Radio,
5337
+ {
5338
+ checked: renditionMode === "standard",
5339
+ name: "rendition-mode",
5340
+ onChange: () => handleModeChange("standard"),
5341
+ value: "standard",
5342
+ id: `${id}--mode-standard`
5343
+ }
5344
+ ),
5345
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-standard`, children: "Standard" })
5346
+ ] }),
5347
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5348
+ /* @__PURE__ */ jsxRuntime.jsx(
5349
+ ui.Radio,
5350
+ {
5351
+ checked: renditionMode === "advanced",
5352
+ name: "rendition-mode",
5353
+ onChange: () => handleModeChange("advanced"),
5354
+ value: "advanced",
5355
+ id: `${id}--mode-advanced`
5356
+ }
5357
+ ),
5358
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-advanced`, children: "Advanced" })
5359
+ ] })
5360
+ ] }),
5361
+ renditionMode === "standard" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5362
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5363
+ /* @__PURE__ */ jsxRuntime.jsx(
5364
+ ui.Checkbox,
5365
+ {
5366
+ id: `${id}--highest`,
5367
+ style: { display: "block" },
5368
+ checked: config.static_renditions.includes("highest"),
5369
+ onChange: () => toggleRendition("highest")
5370
+ }
5371
+ ),
5372
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--highest`, children: "Highest Resolution (up to 4K)" })
5373
+ ] }),
5374
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5375
+ /* @__PURE__ */ jsxRuntime.jsx(
5376
+ ui.Checkbox,
5377
+ {
5378
+ id: `${id}--audio-only-standard`,
5379
+ style: { display: "block" },
5380
+ checked: config.static_renditions.includes("audio-only"),
5381
+ onChange: () => toggleRendition("audio-only")
5382
+ }
5383
+ ),
5384
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-standard`, children: "Audio Only (M4A)" })
5385
+ ] })
5386
+ ] }),
5387
+ renditionMode === "advanced" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5388
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, muted: !0, children: "Select specific resolutions:" }),
5389
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: ADVANCED_RESOLUTIONS.map(({ value, label }) => {
5390
+ const inputId = `${id}--resolution-${value}`;
5391
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5392
+ /* @__PURE__ */ jsxRuntime.jsx(
5393
+ ui.Checkbox,
5394
+ {
5395
+ id: inputId,
5396
+ style: { display: "block" },
5397
+ checked: config.static_renditions.includes(value),
5398
+ onChange: () => toggleRendition(value)
5399
+ }
5400
+ ),
5401
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, size: 1, children: label })
5402
+ ] }, value);
5403
+ }) }),
5404
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [2, 2, 0, 2], children: [
5405
+ /* @__PURE__ */ jsxRuntime.jsx(
5406
+ ui.Checkbox,
5407
+ {
5408
+ id: `${id}--audio-only-advanced`,
5409
+ style: { display: "block" },
5410
+ checked: config.static_renditions.includes("audio-only"),
5411
+ onChange: () => toggleRendition("audio-only")
5412
+ }
5413
+ ),
5414
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-advanced`, children: "Audio Only (M4A)" })
5415
+ ] })
5416
+ ] })
5417
+ ] })
5418
+ }
5419
+ ) });
5420
+ }, VIDEO_QUALITY_LEVELS = [
5421
+ { value: "basic", label: "Basic" },
5422
+ { value: "plus", label: "Plus" },
5423
+ { value: "premium", label: "Premium" }
4698
5424
  ];
4699
5425
  function sanitizeStaticRenditions(renditions) {
4700
5426
  const hasHighest = renditions.includes("highest"), hasSpecificResolutions = renditions.some((r) => r !== "highest" && r !== "audio-only");
@@ -4726,7 +5452,8 @@ function UploadConfiguration({
4726
5452
  max_resolution_tier: "1080p",
4727
5453
  text_tracks: prev.text_tracks?.filter(({ type }) => type !== "autogenerated"),
4728
5454
  public_policy: !0,
4729
- signed_policy: !1
5455
+ signed_policy: !1,
5456
+ drm_policy: !1
4730
5457
  }) : Object.assign({}, prev, {
4731
5458
  video_quality: action.value,
4732
5459
  static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
@@ -4740,6 +5467,8 @@ function UploadConfiguration({
4740
5467
  return Object.assign({}, prev, { [action.action]: action.value });
4741
5468
  case "public_policy":
4742
5469
  return Object.assign({}, prev, { [action.action]: action.value });
5470
+ case "drm_policy":
5471
+ return Object.assign({}, prev, { [action.action]: action.value });
4743
5472
  // Updating individual tracks
4744
5473
  case "track": {
4745
5474
  const text_tracks = [...prev.text_tracks], target_track_i = text_tracks.findIndex(({ _id: _id2 }) => _id2 === action.id);
@@ -4775,75 +5504,41 @@ function UploadConfiguration({
4775
5504
  static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
4776
5505
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
4777
5506
  public_policy: pluginConfig.defaultPublic,
5507
+ drm_policy: pluginConfig.defaultDrm && !!secrets.drmConfigId,
4778
5508
  normalize_audio: pluginConfig.normalize_audio,
4779
5509
  text_tracks: autoTextTracks
4780
5510
  }
4781
- ), isAdvancedMode = React.useMemo(() => config.static_renditions.filter(
4782
- (r) => r !== "highest" && r !== "audio-only"
4783
- ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = React.useState(
4784
- isAdvancedMode ? "advanced" : "standard"
4785
- ), [videoDuration, setVideoDuration] = React.useState(null), [urlFileSize, setUrlFileSize] = React.useState(null), [isLoadingDuration, setIsLoadingDuration] = React.useState(!1), [isLoadingFileSize, setIsLoadingFileSize] = React.useState(!1), [validationError, setValidationError] = React.useState(null), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = React.useState(!1), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration;
5511
+ ), [validationError, setValidationError] = React.useState(null), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration, { fileSize, isLoadingFileSize, canSkipFileSizeValidation } = useFetchFileSize(
5512
+ stagedUpload,
5513
+ MAX_FILE_SIZE
5514
+ ), { videoAssetMetadata, setVideoAssetMetadata, isLoadingMetadata } = useMediaMetadata(stagedUpload);
4786
5515
  React.useEffect(() => {
4787
- setVideoDuration(null), setUrlFileSize(null), setIsLoadingDuration(!1), setIsLoadingFileSize(!1), setValidationError(null), setCanSkipFileSizeValidation(!1);
4788
- let videoElement = null, currentVideoSrc = null;
4789
- const cleanupVideo = (shouldRevokeUrl) => {
4790
- videoElement && (videoElement.onloadedmetadata = null, videoElement.onerror = null, videoElement.src = "", videoElement.load(), videoElement = null), shouldRevokeUrl && currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc), currentVideoSrc = null;
4791
- }, validateDuration = (videoSrc, shouldRevokeUrl = !1) => {
4792
- !MAX_DURATION_SECONDS || MAX_DURATION_SECONDS <= 0 || (setIsLoadingDuration(!0), videoElement = document.createElement("video"), videoElement.preload = "metadata", currentVideoSrc = videoSrc, videoElement.onloadedmetadata = () => {
4793
- const duration = videoElement.duration;
4794
- setVideoDuration(duration), setIsLoadingDuration(!1), duration > MAX_DURATION_SECONDS && setValidationError(
4795
- `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
4796
- ), cleanupVideo(shouldRevokeUrl);
4797
- }, videoElement.onerror = () => {
4798
- setIsLoadingDuration(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(shouldRevokeUrl);
4799
- }, videoElement.src = videoSrc);
4800
- }, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
5516
+ fileSize && setVideoAssetMetadata((old) => ({ ...old, size: fileSize }));
5517
+ }, [fileSize, setVideoAssetMetadata]), React.useEffect(() => {
5518
+ const validateDuration = (duration) => MAX_DURATION_SECONDS && duration > MAX_DURATION_SECONDS ? (setValidationError(
5519
+ `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
5520
+ ), !1) : !0, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
4801
5521
  `File size (${formatBytes(size)}) exceeds maximum allowed size of ${formatBytes(MAX_FILE_SIZE)}`
4802
- ), !1);
4803
- if (stagedUpload.type === "file") {
4804
- const file = stagedUpload.files[0];
4805
- validateFileSize(file.size) && validateDuration(URL.createObjectURL(file), !0);
4806
- }
4807
- if (stagedUpload.type === "url") {
4808
- const url = stagedUpload.url;
4809
- (async () => {
4810
- setIsLoadingFileSize(!0);
4811
- try {
4812
- const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), fileSize = contentLength ? parseInt(contentLength, 10) : null;
4813
- setIsLoadingFileSize(!1), fileSize && setUrlFileSize(fileSize);
4814
- const shouldValidateDuration = MAX_FILE_SIZE === void 0 || fileSize === null || validateFileSize(fileSize);
4815
- fileSize === null && MAX_FILE_SIZE !== void 0 && setCanSkipFileSizeValidation(!0), shouldValidateDuration && validateDuration(url);
4816
- } catch {
4817
- setIsLoadingFileSize(!1), console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), validateDuration(url);
4818
- }
4819
- })();
4820
- }
4821
- return () => {
4822
- cleanupVideo(!0);
4823
- };
4824
- }, [stagedUpload, MAX_FILE_SIZE, MAX_DURATION_SECONDS]);
4825
- const toggleRendition = (rendition) => {
4826
- const current = config.static_renditions, hasRendition = current.includes(rendition);
4827
- dispatch(hasRendition ? {
4828
- action: "static_renditions",
4829
- value: current.filter((r) => r !== rendition)
4830
- } : {
4831
- action: "static_renditions",
4832
- value: [...current, rendition]
4833
- });
4834
- }, handleModeChange = (mode) => {
4835
- setRenditionMode(mode), dispatch(mode === "standard" ? {
4836
- action: "static_renditions",
4837
- value: config.static_renditions.filter((r) => r === "highest" || r === "audio-only")
4838
- } : {
4839
- action: "static_renditions",
4840
- value: config.static_renditions.filter((r) => r !== "highest")
4841
- });
4842
- }, { disableTextTrackConfig, disableUploadConfig } = pluginConfig, skipConfig = disableTextTrackConfig && disableUploadConfig;
5522
+ ), !1), validateDrmAvailability = (isAudioOnly) => config.drm_policy && isAudioOnly ? (setValidationError("Audio-only asset cannot be DRM protected"), !1) : !0;
5523
+ let valid = !0;
5524
+ videoAssetMetadata?.size && (valid = valid && (canSkipFileSizeValidation || validateFileSize(videoAssetMetadata.size))), videoAssetMetadata?.duration && (valid = valid && validateDuration(videoAssetMetadata.duration)), videoAssetMetadata?.isAudioOnly != null && (valid = valid && validateDrmAvailability(videoAssetMetadata.isAudioOnly)), valid && setValidationError(null);
5525
+ }, [
5526
+ MAX_FILE_SIZE,
5527
+ MAX_DURATION_SECONDS,
5528
+ canSkipFileSizeValidation,
5529
+ videoAssetMetadata?.duration,
5530
+ videoAssetMetadata?.size,
5531
+ videoAssetMetadata?.height,
5532
+ videoAssetMetadata?.width,
5533
+ videoAssetMetadata,
5534
+ config.drm_policy,
5535
+ validationError
5536
+ ]);
5537
+ const { disableTextTrackConfig, disableUploadConfig } = pluginConfig, skipConfig = disableTextTrackConfig && disableUploadConfig;
4843
5538
  if (React.useEffect(() => {
4844
- skipConfig && startUpload(formatUploadConfig(config));
5539
+ skipConfig && startUpload(formatUploadConfig(config, secrets));
4845
5540
  }, []), skipConfig) return null;
4846
- const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", maxSupportedResolution = RESOLUTION_TIERS.findIndex(
5541
+ const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", playbackPolicySelected = config.public_policy || config.signed_policy || config.drm_policy, maxSupportedResolution = RESOLUTION_TIERS.findIndex(
4847
5542
  (rt) => rt.value === pluginConfig.max_resolution_tier
4848
5543
  );
4849
5544
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -4877,12 +5572,12 @@ function UploadConfiguration({
4877
5572
  /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentVideoIcon, { fontSize: "2em" }),
4878
5573
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
4879
5574
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { textOverflow: "ellipsis", as: "h2", size: 3, children: stagedUpload.type === "file" ? stagedUpload.files[0].name : stagedUpload.url }),
4880
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : urlFileSize ? `File From URL (${formatBytes(urlFileSize)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
5575
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : videoAssetMetadata?.size ? `File From URL (${formatBytes(videoAssetMetadata.size)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
4881
5576
  stagedUpload.type === "file" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 1, children: [
4882
- isLoadingDuration && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
4883
- videoDuration !== null && !validationError && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", size: 1, muted: !0, children: [
5577
+ isLoadingMetadata && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
5578
+ videoAssetMetadata?.duration && !validationError && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", size: 1, muted: !0, children: [
4884
5579
  "Duration: ",
4885
- formatSeconds(videoDuration)
5580
+ formatSeconds(videoAssetMetadata.duration)
4886
5581
  ] })
4887
5582
  ] })
4888
5583
  ] })
@@ -4928,160 +5623,37 @@ function UploadConfiguration({
4928
5623
  }) })
4929
5624
  }
4930
5625
  ),
4931
- !basicConfig && maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
4932
- sanity.FormField,
4933
- {
4934
- title: "Resolution Tier",
4935
- description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4936
- "The maximum",
4937
- " ",
4938
- /* @__PURE__ */ jsxRuntime.jsx(
4939
- "a",
4940
- {
4941
- href: "https://docs.mux.com/api-reference#video/operation/create-direct-upload",
4942
- target: "_blank",
4943
- rel: "noopener noreferrer",
4944
- children: "resolution_tier"
4945
- }
4946
- ),
4947
- " ",
4948
- "your asset is encoded, stored, and streamed at."
4949
- ] }),
4950
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, wrap: "wrap", children: RESOLUTION_TIERS.map(({ value, label }, index) => {
4951
- const inputId = `${id}--type-${value}`;
4952
- return index > maxSupportedResolution ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
4953
- /* @__PURE__ */ jsxRuntime.jsx(
4954
- ui.Radio,
4955
- {
4956
- checked: config.max_resolution_tier === value,
4957
- name: "asset-resolutiontier",
4958
- onChange: (e) => dispatch({
4959
- action: "max_resolution_tier",
4960
- value: e.currentTarget.value
4961
- }),
4962
- value,
4963
- id: inputId
4964
- }
4965
- ),
4966
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, children: label })
4967
- ] }, value);
4968
- }) })
4969
- }
4970
- ),
4971
5626
  !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
4972
5627
  /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicy, { id, config, secrets, dispatch }),
4973
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
4974
- sanity.FormField,
5628
+ maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5629
+ ResolutionTierSelector,
4975
5630
  {
4976
- title: "Static Renditions",
4977
- description: "Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped.",
4978
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
4979
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, children: [
4980
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
4981
- /* @__PURE__ */ jsxRuntime.jsx(
4982
- ui.Radio,
4983
- {
4984
- checked: renditionMode === "standard",
4985
- name: "rendition-mode",
4986
- onChange: () => handleModeChange("standard"),
4987
- value: "standard",
4988
- id: `${id}--mode-standard`
4989
- }
4990
- ),
4991
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-standard`, children: "Standard" })
4992
- ] }),
4993
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
4994
- /* @__PURE__ */ jsxRuntime.jsx(
4995
- ui.Radio,
4996
- {
4997
- checked: renditionMode === "advanced",
4998
- name: "rendition-mode",
4999
- onChange: () => handleModeChange("advanced"),
5000
- value: "advanced",
5001
- id: `${id}--mode-advanced`
5002
- }
5003
- ),
5004
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-advanced`, children: "Advanced" })
5005
- ] })
5006
- ] }),
5007
- renditionMode === "standard" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5008
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5009
- /* @__PURE__ */ jsxRuntime.jsx(
5010
- ui.Checkbox,
5011
- {
5012
- id: `${id}--highest`,
5013
- style: { display: "block" },
5014
- checked: config.static_renditions.includes("highest"),
5015
- onChange: () => toggleRendition("highest")
5016
- }
5017
- ),
5018
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--highest`, children: "Highest Resolution (up to 4K)" })
5019
- ] }),
5020
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5021
- /* @__PURE__ */ jsxRuntime.jsx(
5022
- ui.Checkbox,
5023
- {
5024
- id: `${id}--audio-only-standard`,
5025
- style: { display: "block" },
5026
- checked: config.static_renditions.includes("audio-only"),
5027
- onChange: () => toggleRendition("audio-only")
5028
- }
5029
- ),
5030
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-standard`, children: "Audio Only (M4A)" })
5031
- ] })
5032
- ] }),
5033
- renditionMode === "advanced" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5034
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, muted: !0, children: "Select specific resolutions:" }),
5035
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: ADVANCED_RESOLUTIONS.map(({ value, label }) => {
5036
- const inputId = `${id}--resolution-${value}`;
5037
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5038
- /* @__PURE__ */ jsxRuntime.jsx(
5039
- ui.Checkbox,
5040
- {
5041
- id: inputId,
5042
- style: { display: "block" },
5043
- checked: config.static_renditions.includes(value),
5044
- onChange: () => toggleRendition(value)
5045
- }
5046
- ),
5047
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, size: 1, children: label })
5048
- ] }, value);
5049
- }) }),
5050
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [2, 2, 0, 2], children: [
5051
- /* @__PURE__ */ jsxRuntime.jsx(
5052
- ui.Checkbox,
5053
- {
5054
- id: `${id}--audio-only-advanced`,
5055
- style: { display: "block" },
5056
- checked: config.static_renditions.includes("audio-only"),
5057
- onChange: () => toggleRendition("audio-only")
5058
- }
5059
- ),
5060
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-advanced`, children: "Audio Only (M4A)" })
5061
- ] })
5062
- ] })
5063
- ] })
5631
+ id,
5632
+ config,
5633
+ dispatch,
5634
+ maxSupportedResolution
5064
5635
  }
5065
- ) })
5636
+ ),
5637
+ /* @__PURE__ */ jsxRuntime.jsx(StaticRenditionSelector, { id, config, dispatch }),
5638
+ !disableTextTrackConfig && /* @__PURE__ */ jsxRuntime.jsx(
5639
+ TextTracksEditor,
5640
+ {
5641
+ tracks: config.text_tracks,
5642
+ dispatch,
5643
+ defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
5644
+ }
5645
+ )
5066
5646
  ] }) })
5067
5647
  ] }),
5068
- !disableTextTrackConfig && !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(
5069
- TextTracksEditor,
5070
- {
5071
- tracks: config.text_tracks,
5072
- dispatch,
5073
- defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
5074
- }
5075
- ),
5076
5648
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
5077
5649
  ui.Button,
5078
5650
  {
5079
- disabled: !basicConfig && !config.public_policy && !config.signed_policy || validationError !== null || isLoadingDuration || isLoadingFileSize && !canSkipFileSizeValidation,
5651
+ disabled: !basicConfig && !playbackPolicySelected || validationError !== null || isLoadingMetadata || isLoadingFileSize && !canSkipFileSizeValidation,
5080
5652
  icon: icons.UploadIcon,
5081
5653
  text: "Upload",
5082
5654
  tone: "positive",
5083
5655
  onClick: () => {
5084
- validationError || startUpload(formatUploadConfig(config));
5656
+ validationError || startUpload(formatUploadConfig(config, secrets));
5085
5657
  }
5086
5658
  }
5087
5659
  ) })
@@ -5089,11 +5661,14 @@ function UploadConfiguration({
5089
5661
  }
5090
5662
  );
5091
5663
  }
5092
- function setPlaybackPolicy(config) {
5093
- const playback_policy = [];
5094
- return config.public_policy && playback_policy.push("public"), config.signed_policy && playback_policy.push("signed"), playback_policy;
5664
+ function setAdvancedPlaybackPolicy(config, secrets) {
5665
+ const advanced_playback_policies = [];
5666
+ return config.public_policy && advanced_playback_policies.push({ policy: "public" }), config.signed_policy && advanced_playback_policies.push({ policy: "signed" }), config.drm_policy && (secrets.drmConfigId ? advanced_playback_policies.push({
5667
+ policy: "drm",
5668
+ drm_configuration_id: secrets.drmConfigId ?? void 0
5669
+ }) : console.error("Selected DRM Policy but missing DRM Configuration Id")), advanced_playback_policies;
5095
5670
  }
5096
- function formatUploadConfig(config) {
5671
+ function formatUploadConfig(config, secrets) {
5097
5672
  const generated_subtitles = config.text_tracks.filter(isAutogeneratedTrack).map((track) => ({
5098
5673
  name: track.name,
5099
5674
  language_code: track.language_code
@@ -5117,7 +5692,7 @@ function formatUploadConfig(config) {
5117
5692
  )
5118
5693
  ],
5119
5694
  static_renditions: config.static_renditions.length > 0 ? config.static_renditions.map((resolution) => ({ resolution })) : void 0,
5120
- playback_policy: setPlaybackPolicy(config),
5695
+ advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
5121
5696
  max_resolution_tier: config.max_resolution_tier,
5122
5697
  video_quality: config.video_quality,
5123
5698
  normalize_audio: config.normalize_audio
@@ -5329,8 +5904,13 @@ function Uploader(props) {
5329
5904
  case "reset":
5330
5905
  case "complete":
5331
5906
  return uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null, INITIAL_STATE;
5332
- case "error":
5333
- return uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null, Object.assign({}, INITIAL_STATE, { error: action.error });
5907
+ case "error": {
5908
+ uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null;
5909
+ let error = action.error;
5910
+ return isServerError(action.error) && hasPlaybackPolicy(action.settings, "drm") && (error = new Error(
5911
+ "Unknown Error while uploading DRM protected content. Make sure your DRM configuration ID is valid and set correctly"
5912
+ )), Object.assign({}, INITIAL_STATE, { error });
5913
+ }
5334
5914
  default:
5335
5915
  return prev;
5336
5916
  }
@@ -5409,7 +5989,7 @@ function Uploader(props) {
5409
5989
  }
5410
5990
  },
5411
5991
  complete: () => dispatch({ action: "complete" }),
5412
- error: (error) => dispatch({ action: "error", error })
5992
+ error: (error) => dispatch({ action: "error", error, settings })
5413
5993
  });
5414
5994
  }, invalidFileToast = React.useCallback(() => {
5415
5995
  toast.push({
@@ -5460,11 +6040,11 @@ function Uploader(props) {
5460
6040
  idx > -1 && dragEnteredEls.current.splice(idx, 1), dragEnteredEls.current.length === 0 && setDragState(null);
5461
6041
  };
5462
6042
  if (state.error !== null) {
5463
- const error = {};
6043
+ const error = state.error;
5464
6044
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, direction: "column", justify: "center", align: "center", children: [
5465
6045
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 5, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, {}) }),
5466
6046
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Something went wrong" }),
5467
- error instanceof Error && error.message && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: error.message }),
6047
+ error instanceof Error && error.message && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, weight: "semibold", style: { textAlign: "center" }, children: error.message }),
5468
6048
  /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Upload another file", onClick: () => dispatch({ action: "reset" }) })
5469
6049
  ] });
5470
6050
  }
@@ -5549,6 +6129,7 @@ function Uploader(props) {
5549
6129
  props.dialogState === "select-video" && /* @__PURE__ */ jsxRuntime.jsx(
5550
6130
  InputBrowser,
5551
6131
  {
6132
+ config: props.config,
5552
6133
  asset: props.asset,
5553
6134
  onChange: props.onChange,
5554
6135
  setDialogState: props.setDialogState
@@ -5633,7 +6214,12 @@ const muxVideoSchema = {
5633
6214
  { type: "number", name: "max_width" },
5634
6215
  { type: "number", name: "max_frame_rate" },
5635
6216
  { type: "number", name: "duration" },
5636
- { type: "number", name: "max_height" }
6217
+ { type: "number", name: "max_height" },
6218
+ { type: "string", name: "language_code" },
6219
+ { type: "string", name: "name" },
6220
+ { type: "string", name: "status" },
6221
+ { type: "string", name: "text_source" },
6222
+ { type: "string", name: "text_type" }
5637
6223
  ]
5638
6224
  }, muxPlaybackId = {
5639
6225
  name: "mux.playbackId",
@@ -5795,6 +6381,7 @@ const muxVideoSchema = {
5795
6381
  normalize_audio: !1,
5796
6382
  defaultPublic: !0,
5797
6383
  defaultSigned: !1,
6384
+ defaultDrm: !1,
5798
6385
  tool: DEFAULT_TOOL_CONFIG,
5799
6386
  allowedRolesForConfiguration: [],
5800
6387
  acceptedMimeTypes: ["video/*", "audio/*"]