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