sanity-plugin-mux-input 2.13.0 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +25 -24
  2. package/dist/index.d.mts +35 -2
  3. package/dist/index.d.ts +35 -2
  4. package/dist/index.js +2176 -461
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +2178 -463
  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/assets.ts +75 -0
  11. package/src/actions/secrets.ts +6 -1
  12. package/src/actions/upload.ts +1 -1
  13. package/src/components/AddCaptionDialog.tsx +421 -0
  14. package/src/components/CaptionsDialog.tsx +23 -0
  15. package/src/components/ConfigureApi.tsx +51 -5
  16. package/src/components/EditCaptionDialog.tsx +508 -0
  17. package/src/components/InputBrowser.tsx +8 -2
  18. package/src/components/Onboard.tsx +2 -2
  19. package/src/components/PageSelector.tsx +54 -0
  20. package/src/components/Player.styled.tsx +7 -2
  21. package/src/components/PlayerActionsMenu.tsx +14 -6
  22. package/src/components/SelectAsset.tsx +9 -3
  23. package/src/components/StudioTool.tsx +2 -2
  24. package/src/components/TextTracksManager.tsx +781 -0
  25. package/src/components/UploadConfiguration.tsx +104 -343
  26. package/src/components/Uploader.styled.tsx +8 -15
  27. package/src/components/Uploader.tsx +25 -7
  28. package/src/components/VideoDetails/VideoDetails.tsx +43 -7
  29. package/src/components/VideoInBrowser.tsx +53 -6
  30. package/src/components/VideoPlayer.tsx +122 -47
  31. package/src/components/VideoThumbnail.tsx +84 -72
  32. package/src/components/VideosBrowser.tsx +15 -5
  33. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
  34. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
  35. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
  36. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
  37. package/src/context/DrmPlaybackWarningContext.tsx +93 -0
  38. package/src/hooks/useAccessControl.ts +1 -0
  39. package/src/hooks/useDialogState.ts +1 -1
  40. package/src/hooks/useFetchFileSize.ts +54 -0
  41. package/src/hooks/useMediaMetadata.ts +100 -0
  42. package/src/hooks/useSaveSecrets.ts +10 -3
  43. package/src/hooks/useSecretsDocumentValues.ts +9 -1
  44. package/src/hooks/useSecretsFormState.ts +6 -3
  45. package/src/util/asserters.ts +14 -0
  46. package/src/util/createUrlParamsObject.ts +7 -3
  47. package/src/util/generateJwt.ts +11 -2
  48. package/src/util/getPlaybackPolicy.ts +63 -4
  49. package/src/util/getStoryboardSrc.ts +7 -3
  50. package/src/util/getVideoMetadata.ts +4 -1
  51. package/src/util/getVideoSrc.ts +9 -9
  52. package/src/util/readSecrets.ts +3 -1
  53. package/src/util/textTracks.ts +222 -0
  54. package/src/util/tryWithSuspend.ts +22 -0
  55. package/src/util/types.ts +39 -6
  56. package/src/util/getPlaybackId.ts +0 -9
package/dist/index.js CHANGED
@@ -19,11 +19,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
19
  mod
20
20
  ));
21
21
  Object.defineProperty(exports, "__esModule", { value: !0 });
22
- var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), suspendReact = require("suspend-react"), rxjs = require("rxjs"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), operators = require("rxjs/operators"), MuxPlayer = require("@mux/mux-player-react/lazy"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), reactRx = require("react-rx"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is"), LanguagesList = require("iso-639-1");
22
+ var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), icons = require("@sanity/icons"), ui = require("@sanity/ui"), React = require("react"), compact = require("lodash/compact.js"), toLower = require("lodash/toLower.js"), trim = require("lodash/trim.js"), uniq = require("lodash/uniq.js"), words = require("lodash/words.js"), suspendReact = require("suspend-react"), rxjs = require("rxjs"), styledComponents = require("styled-components"), uuid = require("@sanity/uuid"), operators = require("rxjs/operators"), LanguagesList = require("iso-639-1"), MuxPlayer = require("@mux/mux-player-react/lazy"), router = require("sanity/router"), isNumber = require("lodash/isNumber.js"), isString = require("lodash/isString.js"), reactRx = require("react-rx"), useSWR = require("swr"), scrollIntoView = require("scroll-into-view-if-needed"), upchunk = require("@mux/upchunk"), reactIs = require("react-is");
23
23
  function _interopDefaultCompat(e) {
24
24
  return e && typeof e == "object" && "default" in e ? e : { default: e };
25
25
  }
26
- var React__default = /* @__PURE__ */ _interopDefaultCompat(React), compact__default = /* @__PURE__ */ _interopDefaultCompat(compact), toLower__default = /* @__PURE__ */ _interopDefaultCompat(toLower), trim__default = /* @__PURE__ */ _interopDefaultCompat(trim), uniq__default = /* @__PURE__ */ _interopDefaultCompat(uniq), words__default = /* @__PURE__ */ _interopDefaultCompat(words), MuxPlayer__default = /* @__PURE__ */ _interopDefaultCompat(MuxPlayer), isNumber__default = /* @__PURE__ */ _interopDefaultCompat(isNumber), isString__default = /* @__PURE__ */ _interopDefaultCompat(isString), useSWR__default = /* @__PURE__ */ _interopDefaultCompat(useSWR), scrollIntoView__default = /* @__PURE__ */ _interopDefaultCompat(scrollIntoView), LanguagesList__default = /* @__PURE__ */ _interopDefaultCompat(LanguagesList);
26
+ var React__default = /* @__PURE__ */ _interopDefaultCompat(React), compact__default = /* @__PURE__ */ _interopDefaultCompat(compact), toLower__default = /* @__PURE__ */ _interopDefaultCompat(toLower), trim__default = /* @__PURE__ */ _interopDefaultCompat(trim), uniq__default = /* @__PURE__ */ _interopDefaultCompat(uniq), words__default = /* @__PURE__ */ _interopDefaultCompat(words), LanguagesList__default = /* @__PURE__ */ _interopDefaultCompat(LanguagesList), MuxPlayer__default = /* @__PURE__ */ _interopDefaultCompat(MuxPlayer), isNumber__default = /* @__PURE__ */ _interopDefaultCompat(isNumber), isString__default = /* @__PURE__ */ _interopDefaultCompat(isString), useSWR__default = /* @__PURE__ */ _interopDefaultCompat(useSWR), scrollIntoView__default = /* @__PURE__ */ _interopDefaultCompat(scrollIntoView);
27
27
  const ToolIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
28
28
  "svg",
29
29
  {
@@ -36,7 +36,45 @@ const ToolIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
36
36
  xmlns: "http://www.w3.org/2000/svg",
37
37
  children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z" })
38
38
  }
39
- ), SANITY_API_VERSION = "2024-03-05";
39
+ ), LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY = "mux-plugin-has-shown-drm-playback-warning", DrmPlaybackWarningContext = React.createContext({
40
+ hasShownWarning: !1,
41
+ setHasWarnedAboutDrmPlayback: () => null
42
+ }), DrmPlaybackWarningContextProvider = ({
43
+ config,
44
+ children
45
+ }) => {
46
+ const hasWarned = (config?.disableDrmPlaybackWarning ?? !1) || window.localStorage.getItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY) === "true", [hasWarnedAboutDrmPlayback, setHasWarnedAboutDrmPlayback] = React.useState(hasWarned), setHasShownWarning = (b) => {
47
+ window.localStorage.setItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY, b.toString()), setHasWarnedAboutDrmPlayback(b);
48
+ };
49
+ return /* @__PURE__ */ jsxRuntime.jsx(
50
+ DrmPlaybackWarningContext.Provider,
51
+ {
52
+ value: {
53
+ hasShownWarning: hasWarnedAboutDrmPlayback,
54
+ setHasWarnedAboutDrmPlayback: setHasShownWarning
55
+ },
56
+ children
57
+ }
58
+ );
59
+ }, useDrmPlaybackWarningContext = () => React.useContext(DrmPlaybackWarningContext), DRMWarningDialog = ({ onClose }) => {
60
+ const { setHasWarnedAboutDrmPlayback } = useDrmPlaybackWarningContext(), _onClose = () => {
61
+ setHasWarnedAboutDrmPlayback(!0), onClose();
62
+ };
63
+ return /* @__PURE__ */ jsxRuntime.jsx(
64
+ ui.Dialog,
65
+ {
66
+ open: !0,
67
+ id: "drm-playback-warn",
68
+ onClose: _onClose,
69
+ header: "DRM Playback Warning",
70
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "ghost", tone: "primary", onClick: _onClose, text: "Ok" }) }),
71
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "DRM-protected playback will generate a license with a small associated cost. The plugin will attempt to play signed or public playback IDs instead whenever possible." }) }) }),
73
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, tone: "suggest", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "This is a one time warning. If it persists, you can disable it from your plugin configuration." }) }) })
74
+ ] })
75
+ }
76
+ );
77
+ }, SANITY_API_VERSION = "2024-03-05";
40
78
  function useClient() {
41
79
  return sanity.useClient({ apiVersion: SANITY_API_VERSION });
42
80
  }
@@ -109,7 +147,7 @@ function useAssets() {
109
147
  function useDialogState() {
110
148
  return React.useState(!1);
111
149
  }
112
- function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate) {
150
+ function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, signingKeyPrivate, drmConfigId) {
113
151
  const doc = {
114
152
  _id: "secrets.mux",
115
153
  _type: "mux.apiKey",
@@ -117,9 +155,10 @@ function saveSecrets(client, token, secretKey, enableSignedUrls, signingKeyId, s
117
155
  secretKey,
118
156
  enableSignedUrls,
119
157
  signingKeyId,
120
- signingKeyPrivate
158
+ signingKeyPrivate,
159
+ drmConfigId
121
160
  };
122
- return client.createOrReplace(doc);
161
+ return doc.signingKeyId = enableSignedUrls ? signingKeyId : "", doc.signingKeyPrivate = enableSignedUrls ? signingKeyPrivate : "", client.createOrReplace(doc);
123
162
  }
124
163
  async function createSigningKeys(client) {
125
164
  try {
@@ -172,7 +211,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
172
211
  async ({
173
212
  token,
174
213
  secretKey,
175
- enableSignedUrls
214
+ enableSignedUrls,
215
+ drmConfigId
176
216
  }) => {
177
217
  let { signingKeyId, signingKeyPrivate } = secrets;
178
218
  try {
@@ -182,7 +222,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
182
222
  secretKey,
183
223
  enableSignedUrls,
184
224
  signingKeyId,
185
- signingKeyPrivate
225
+ signingKeyPrivate,
226
+ drmConfigId
186
227
  ), !(await testSecrets(client))?.status && token && secretKey)
187
228
  throw new Error("Invalid secrets");
188
229
  } catch (err) {
@@ -201,7 +242,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
201
242
  secretKey,
202
243
  enableSignedUrls,
203
244
  signingKeyId,
204
- signingKeyPrivate
245
+ signingKeyPrivate,
246
+ drmConfigId ?? ""
205
247
  );
206
248
  } catch (err) {
207
249
  throw console.log("Error while creating and saving signing key:", err?.message), err;
@@ -211,11 +253,19 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
211
253
  secretKey,
212
254
  enableSignedUrls,
213
255
  signingKeyId,
214
- signingKeyPrivate
256
+ signingKeyPrivate,
257
+ drmConfigId
215
258
  };
216
259
  },
217
260
  [client, secrets]
218
- ), name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = ["token", "secretKey", "enableSignedUrls", "signingKeyId", "signingKeyPrivate"], useSecretsDocumentValues = () => {
261
+ ), name = "mux-input", cacheNs = "sanity-plugin-mux-input", muxSecretsDocumentId = "secrets.mux", DIALOGS_Z_INDEX = 6e4, THUMBNAIL_ASPECT_RATIO = 1.7777777777777777, MIN_ASPECT_RATIO = 5 / 4, AUDIO_ASPECT_RATIO = 5 / 1, path$1 = [
262
+ "token",
263
+ "secretKey",
264
+ "enableSignedUrls",
265
+ "signingKeyId",
266
+ "signingKeyPrivate",
267
+ "drmConfigId"
268
+ ], useSecretsDocumentValues = () => {
219
269
  const { error, isLoading, value } = sanity.useDocumentValues(
220
270
  muxSecretsDocumentId,
221
271
  path$1
@@ -225,7 +275,8 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
225
275
  secretKey: value?.secretKey || null,
226
276
  enableSignedUrls: value?.enableSignedUrls || !1,
227
277
  signingKeyId: value?.signingKeyId || null,
228
- signingKeyPrivate: value?.signingKeyPrivate || null
278
+ signingKeyPrivate: value?.signingKeyPrivate || null,
279
+ drmConfigId: value?.drmConfigId || null
229
280
  };
230
281
  return {
231
282
  isInitialSetup: !exists,
@@ -235,7 +286,7 @@ const useSaveSecrets = (client, secrets) => React.useCallback(
235
286
  }, [value]);
236
287
  return { error, isLoading, value: cache };
237
288
  };
238
- function init({ token, secretKey, enableSignedUrls }) {
289
+ function init({ token, secretKey, enableSignedUrls, drmConfigId }) {
239
290
  return {
240
291
  submitting: !1,
241
292
  error: null,
@@ -243,7 +294,8 @@ function init({ token, secretKey, enableSignedUrls }) {
243
294
  // This ensures the `dirty` check works correctly
244
295
  token: token ?? "",
245
296
  secretKey: secretKey ?? "",
246
- enableSignedUrls: enableSignedUrls ?? !1
297
+ enableSignedUrls: enableSignedUrls ?? !1,
298
+ drmConfigId: drmConfigId ?? ""
247
299
  };
248
300
  }
249
301
  function reducer(state, action) {
@@ -271,7 +323,8 @@ function readSecrets(client) {
271
323
  secretKey,
272
324
  enableSignedUrls,
273
325
  signingKeyId,
274
- signingKeyPrivate
326
+ signingKeyPrivate,
327
+ drmConfigId
275
328
  }`,
276
329
  { _id }
277
330
  );
@@ -280,7 +333,8 @@ function readSecrets(client) {
280
333
  secretKey: data?.secretKey || null,
281
334
  enableSignedUrls: !!data?.enableSignedUrls || !1,
282
335
  signingKeyId: data?.signingKeyId || null,
283
- signingKeyPrivate: data?.signingKeyPrivate || null
336
+ signingKeyPrivate: data?.signingKeyPrivate || null,
337
+ drmConfigId: data?.drmConfigId || null
284
338
  };
285
339
  }, [cacheNs, _id, projectId, dataset]);
286
340
  }
@@ -343,20 +397,20 @@ function FormField(props) {
343
397
  ] });
344
398
  }
345
399
  var FormField$1 = React.memo(FormField);
346
- const fieldNames = ["token", "secretKey", "enableSignedUrls"];
400
+ const fieldNames = ["token", "secretKey", "enableSignedUrls", "drmConfigId"];
347
401
  function ConfigureApiDialog({ secrets, setDialogState }) {
348
402
  const client = useClient(), [state, dispatch] = useSecretsFormState(secrets), hasSecretsInitially = React.useMemo(() => secrets.token && secrets.secretKey, [secrets]), handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]), dirty = React.useMemo(
349
- () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls,
403
+ () => secrets.token !== state.token || secrets.secretKey !== state.secretKey || secrets.enableSignedUrls !== state.enableSignedUrls || secrets.drmConfigId !== state.drmConfigId,
350
404
  [secrets, state]
351
- ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId] = React.useMemo(
405
+ ), id = `ConfigureApi${React.useId()}`, [tokenId, secretKeyId, enableSignedUrlsId, drmConfigIdId] = React.useMemo(
352
406
  () => fieldNames.map((field) => `${id}-${field}`),
353
407
  [id]
354
408
  ), firstField = React.useRef(null), handleSaveSecrets = useSaveSecrets(client, secrets), saving = React.useRef(!1), handleSubmit = React.useCallback(
355
409
  (event) => {
356
410
  if (event.preventDefault(), !saving.current && event.currentTarget.reportValidity()) {
357
411
  saving.current = !0, dispatch({ type: "submit" });
358
- const { token, secretKey, enableSignedUrls } = state;
359
- handleSaveSecrets({ token, secretKey, enableSignedUrls }).then((savedSecrets) => {
412
+ const { token, secretKey, enableSignedUrls, drmConfigId } = state;
413
+ handleSaveSecrets({ token, secretKey, enableSignedUrls, drmConfigId }).then((savedSecrets) => {
360
414
  const { projectId, dataset } = client.config();
361
415
  suspendReact.clear([cacheNs, _id, projectId, dataset]), suspendReact.preload(() => Promise.resolve(savedSecrets), [cacheNs, _id, projectId, dataset]), setDialogState(!1);
362
416
  }).catch((err) => dispatch({ type: "error", payload: err.message })).finally(() => {
@@ -389,6 +443,14 @@ function ConfigureApiDialog({ secrets, setDialogState }) {
389
443
  });
390
444
  },
391
445
  [dispatch]
446
+ ), handleChangeDrmConfigId = React.useCallback(
447
+ (event) => {
448
+ dispatch({
449
+ type: "change",
450
+ payload: { name: "drmConfigId", value: event.currentTarget.value }
451
+ });
452
+ },
453
+ [dispatch]
392
454
  );
393
455
  return React.useEffect(() => {
394
456
  firstField.current && firstField.current.focus();
@@ -475,6 +537,45 @@ function ConfigureApiDialog({ secrets, setDialogState }) {
475
537
  ] })
476
538
  ] }) }) : null
477
539
  ] }),
540
+ /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { title: "DRM Configuration ID", inputId: drmConfigIdId, children: /* @__PURE__ */ jsxRuntime.jsx(
541
+ ui.TextInput,
542
+ {
543
+ id: drmConfigIdId,
544
+ onChange: handleChangeDrmConfigId,
545
+ type: "text",
546
+ value: state.drmConfigId ?? "",
547
+ required: !1
548
+ }
549
+ ) }),
550
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: [3, 3, 3], radius: 2, shadow: 1, tone: "neutral", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
551
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
552
+ "DRM (Digital Rights Management) provides an extra layer of content security for video content streamed from Mux. For additional information check out our",
553
+ " ",
554
+ /* @__PURE__ */ jsxRuntime.jsx(
555
+ "a",
556
+ {
557
+ href: "https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos",
558
+ target: "_blank",
559
+ rel: "noopener noreferrer",
560
+ children: "DRM Guide"
561
+ }
562
+ ),
563
+ "."
564
+ ] }),
565
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, children: [
566
+ /* @__PURE__ */ jsxRuntime.jsx(
567
+ "a",
568
+ {
569
+ href: "https://www.mux.com/support/human",
570
+ target: "_blank",
571
+ rel: "noopener noreferrer",
572
+ children: "Contact us"
573
+ }
574
+ ),
575
+ " ",
576
+ "to get started using DRM."
577
+ ] })
578
+ ] }) }),
478
579
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
479
580
  /* @__PURE__ */ jsxRuntime.jsx(
480
581
  ui.Button,
@@ -568,6 +669,51 @@ function listAssets(client, options) {
568
669
  query
569
670
  });
570
671
  }
672
+ function addTextTrackFromUrl(client, assetId, vttUrl, options) {
673
+ const { dataset } = client.config();
674
+ return client.request({
675
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks`,
676
+ withCredentials: !0,
677
+ method: "POST",
678
+ body: {
679
+ url: vttUrl,
680
+ type: "text",
681
+ language_code: options.language_code,
682
+ name: options.name,
683
+ text_type: options.text_type || "subtitles"
684
+ },
685
+ headers: {
686
+ "Content-Type": "application/json"
687
+ }
688
+ });
689
+ }
690
+ function generateSubtitles(client, assetId, audioTrackId, options) {
691
+ const { dataset } = client.config();
692
+ return client.request({
693
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${audioTrackId}/generate-subtitles`,
694
+ withCredentials: !0,
695
+ method: "POST",
696
+ body: {
697
+ generated_subtitles: [
698
+ {
699
+ language_code: options.language_code,
700
+ name: options.name
701
+ }
702
+ ]
703
+ },
704
+ headers: {
705
+ "Content-Type": "application/json"
706
+ }
707
+ });
708
+ }
709
+ function deleteTextTrack(client, assetId, trackId) {
710
+ const { dataset } = client.config();
711
+ return client.request({
712
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
713
+ withCredentials: !0,
714
+ method: "DELETE"
715
+ });
716
+ }
571
717
  const ASSETS_PER_PAGE = 100;
572
718
  async function fetchMuxAssetsPage(client, cursor) {
573
719
  try {
@@ -733,6 +879,35 @@ function useInView(ref, options = {}) {
733
879
  };
734
880
  }, [options, ref]), inView;
735
881
  }
882
+ function getPlaybackId(asset, priority = ["drm", "signed", "public"]) {
883
+ try {
884
+ if (!asset)
885
+ throw new TypeError("Tried to get playback Id with no asset");
886
+ const playbackIds = asset.data?.playback_ids;
887
+ if (playbackIds && playbackIds.length > 0) {
888
+ for (const policy of priority) {
889
+ const match = playbackIds.find((entry) => entry.policy === policy);
890
+ if (match)
891
+ return match.id;
892
+ }
893
+ return playbackIds[0].id;
894
+ }
895
+ throw new TypeError("Missing playbackId");
896
+ } catch (e) {
897
+ throw console.error("Asset is missing a playbackId", { asset }, e), e;
898
+ }
899
+ }
900
+ function getPlaybackPolicy(asset) {
901
+ return asset.data?.playback_ids?.find(
902
+ (playbackId) => getPlaybackId(asset, ["drm", "signed", "public"]) === playbackId.id
903
+ ) ?? { id: "", policy: "public" };
904
+ }
905
+ function getPlaybackPolicyById(asset, playbackId) {
906
+ return asset.data?.playback_ids?.find((entry) => playbackId === entry.id);
907
+ }
908
+ function hasPlaybackPolicy(data, policy) {
909
+ return data.advanced_playback_policies && data.advanced_playback_policies.find((p) => p.policy === policy) || data.playback_policy?.find((p) => p === policy);
910
+ }
736
911
  function generateJwt(client, playbackId, aud, payload) {
737
912
  const { signingKeyId, signingKeyPrivate } = readSecrets(client);
738
913
  if (!signingKeyId)
@@ -753,20 +928,13 @@ function generateJwt(client, playbackId, aud, payload) {
753
928
  }
754
929
  );
755
930
  }
756
- function getPlaybackId(asset) {
757
- if (!asset?.playbackId)
758
- throw console.error("Asset is missing a playbackId", { asset }), new TypeError("Missing playbackId");
759
- return asset.playbackId;
760
- }
761
- function getPlaybackPolicy(asset) {
762
- return asset.data?.playback_ids?.find((playbackId) => asset.playbackId === playbackId.id)?.policy ?? "public";
763
- }
764
931
  function createUrlParamsObject(client, asset, params, audience) {
765
932
  const playbackId = getPlaybackId(asset);
766
933
  let searchParams = new URLSearchParams(
767
934
  JSON.parse(JSON.stringify(params, (_, v) => v ?? void 0))
768
935
  );
769
- if (getPlaybackPolicy(asset) === "signed") {
936
+ const playbackPolicy = getPlaybackPolicyById(asset, playbackId)?.policy;
937
+ if (playbackPolicy === "signed" || playbackPolicy === "drm") {
770
938
  const token = generateJwt(client, playbackId, audience, params);
771
939
  searchParams = new URLSearchParams({ token });
772
940
  }
@@ -797,6 +965,15 @@ function getPosterSrc({
797
965
  const { playbackId, searchParams } = createUrlParamsObject(client, asset, params, "t");
798
966
  return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`;
799
967
  }
968
+ function tryWithSuspend(block, onError) {
969
+ try {
970
+ return block();
971
+ } catch (errorOrPromise) {
972
+ if (errorOrPromise instanceof Promise)
973
+ throw errorOrPromise;
974
+ return onError ? onError(errorOrPromise) : void 0;
975
+ }
976
+ }
800
977
  const Image = styledComponents.styled.img`
801
978
  transition: opacity 0.175s ease-out 0s;
802
979
  display: block;
@@ -814,22 +991,22 @@ function VideoThumbnail({
814
991
  width,
815
992
  staticImage = !1
816
993
  }) {
817
- const ref = React.useRef(null), inView = useInView(ref), posterWidth = width || 250, [status, setStatus] = React.useState("loading"), client = useClient(), src = React.useMemo(() => {
818
- try {
994
+ const posterWidth = width || 250, client = useClient(), ref = React.useRef(null), inView = useInView(ref), [status, setStatus] = React.useState("loading"), [error, setError] = React.useState(null), thumbnailSrc = React.useMemo(() => tryWithSuspend(
995
+ () => {
819
996
  let thumbnail;
820
997
  return staticImage ? thumbnail = getPosterSrc({ asset, client, width: posterWidth }) : thumbnail = getAnimatedPosterSrc({ asset, client, width: posterWidth }), thumbnail;
821
- } catch {
822
- status !== "error" && setStatus("error");
823
- return;
998
+ },
999
+ (err) => {
1000
+ handleError(err.message);
824
1001
  }
825
- }, [asset, client, posterWidth, status, staticImage]);
1002
+ ), [asset, client, posterWidth, staticImage]);
826
1003
  function handleLoad() {
827
1004
  setStatus("loaded");
828
1005
  }
829
- function handleError() {
830
- setStatus("error");
1006
+ function handleError(err) {
1007
+ setStatus("error"), setError(err || "Failed loading thumbnail");
831
1008
  }
832
- return /* @__PURE__ */ jsxRuntime.jsx(
1009
+ return /* @__PURE__ */ jsxRuntime.jsx(React.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Preparing thumbnail" }), children: /* @__PURE__ */ jsxRuntime.jsx(
833
1010
  ui.Card,
834
1011
  {
835
1012
  style: {
@@ -870,23 +1047,23 @@ function VideoThumbnail({
870
1047
  },
871
1048
  children: [
872
1049
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 4, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, { style: { fontSize: "1.75em" } }) }),
873
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: "Failed loading thumbnail" })
1050
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", children: error })
874
1051
  ]
875
1052
  }
876
1053
  ),
877
1054
  /* @__PURE__ */ jsxRuntime.jsx(
878
1055
  Image,
879
1056
  {
880
- src,
1057
+ src: thumbnailSrc ?? void 0,
881
1058
  alt: `Preview for ${staticImage ? "image" : "video"} ${asset.filename || asset.assetId}`,
882
1059
  onLoad: handleLoad,
883
- onError: handleError,
1060
+ onError: () => handleError(),
884
1061
  style: { opacity: status === "loaded" ? 1 : 0 }
885
1062
  }
886
1063
  )
887
1064
  ] }) : null
888
1065
  }
889
- );
1066
+ ) });
890
1067
  }
891
1068
  const MissingAssetCheckbox = styledComponents.styled(ui.Checkbox)`
892
1069
  position: static !important;
@@ -1105,6 +1282,46 @@ function ImportVideosFromMux() {
1105
1282
  if (importAssets.hasSecrets)
1106
1283
  return importAssets.dialogOpen ? /* @__PURE__ */ jsxRuntime.jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
1107
1284
  }
1285
+ const PageSelector = (props) => {
1286
+ const page = props.page, setPage = props.setPage;
1287
+ return React.useEffect(() => {
1288
+ const clamped = Math.min(props.total - 1, Math.max(0, page));
1289
+ page !== clamped && setPage(clamped);
1290
+ }, [page, props.total, setPage]), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1291
+ /* @__PURE__ */ jsxRuntime.jsx(
1292
+ ui.Button,
1293
+ {
1294
+ icon: icons.ChevronLeftIcon,
1295
+ mode: "bleed",
1296
+ padding: 3,
1297
+ style: { cursor: "pointer" },
1298
+ disabled: page <= 0,
1299
+ onClick: () => {
1300
+ setPage((p) => Math.min(props.total - 1, Math.max(0, p - 1)));
1301
+ }
1302
+ }
1303
+ ),
1304
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Label, { muted: !0, children: [
1305
+ "Page ",
1306
+ page + 1,
1307
+ "/",
1308
+ props.total
1309
+ ] }),
1310
+ /* @__PURE__ */ jsxRuntime.jsx(
1311
+ ui.Button,
1312
+ {
1313
+ icon: icons.ChevronRightIcon,
1314
+ mode: "bleed",
1315
+ padding: 3,
1316
+ style: { cursor: "pointer" },
1317
+ disabled: page >= props.total - 1,
1318
+ onClick: () => {
1319
+ setPage((p) => Math.min(props.total - 1, Math.max(0, p + 1)));
1320
+ }
1321
+ }
1322
+ )
1323
+ ] });
1324
+ };
1108
1325
  function useResyncMuxMetadata() {
1109
1326
  const documentStore = sanity.useDocumentStore(), client = sanity.useClient({
1110
1327
  apiVersion: SANITY_API_VERSION
@@ -1380,75 +1597,1290 @@ function StopWatchIcon(props) {
1380
1597
  }
1381
1598
  );
1382
1599
  }
1383
- const DialogStateContext = React.createContext({
1384
- dialogState: !1,
1385
- setDialogState: () => null
1386
- }), DialogStateProvider = ({
1387
- dialogState,
1388
- setDialogState,
1389
- children
1390
- }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
1391
- function getVideoSrc({ asset, client }) {
1392
- const playbackId = getPlaybackId(asset), searchParams = new URLSearchParams();
1393
- if (getPlaybackPolicy(asset) === "signed") {
1394
- const token = generateJwt(client, playbackId, "v");
1395
- searchParams.set("token", token);
1600
+ function extractErrorMessage(error, defaultMessage = "Failed to process request") {
1601
+ let message = "";
1602
+ if (error && typeof error == "object") {
1603
+ const err = error;
1604
+ message = err.response?.body?.message || err.message || "";
1605
+ } else typeof error == "string" && (message = error);
1606
+ if (!message)
1607
+ return defaultMessage;
1608
+ const match = message.match(/\(([^)]+)\)/);
1609
+ if (match && match[1])
1610
+ return match[1];
1611
+ if (message.includes("responded with")) {
1612
+ const parts = message.split("(");
1613
+ if (parts.length > 1)
1614
+ return parts[parts.length - 1].replace(")", "").trim();
1396
1615
  }
1397
- return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
1616
+ return message;
1398
1617
  }
1399
- function getDevicePixelRatio(options) {
1618
+ async function pollTrackStatus(options) {
1400
1619
  const {
1401
- defaultDpr = 1,
1402
- maxDpr = 3,
1403
- round = !0
1404
- } = options || {}, dpr = typeof window < "u" && typeof window.devicePixelRatio == "number" ? window.devicePixelRatio : defaultDpr;
1405
- return Math.min(Math.max(1, round ? Math.floor(dpr) : dpr), maxDpr);
1406
- }
1407
- function formatSeconds(seconds) {
1408
- if (typeof seconds != "number" || Number.isNaN(seconds))
1409
- return "";
1410
- const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
1411
- let ret = "";
1412
- return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
1620
+ client,
1621
+ assetId,
1622
+ trackName,
1623
+ trackLanguageCode,
1624
+ maxAttempts = 10,
1625
+ onTrackFound,
1626
+ onTrackErrored,
1627
+ onTrackReady
1628
+ } = options, trimmedName = trackName.trim(), trimmedLanguageCode = trackLanguageCode.trim();
1629
+ let newTrack, attempts = 0, trackFound = !1;
1630
+ const findTrack = (textTracks) => {
1631
+ let foundTrack = textTracks.find(
1632
+ (track) => track.name === trimmedName && track.language_code === trimmedLanguageCode
1633
+ );
1634
+ return foundTrack || (foundTrack = textTracks.find((track) => track.language_code === trimmedLanguageCode)), !foundTrack && textTracks.length > 0 && (foundTrack = textTracks[textTracks.length - 1]), foundTrack;
1635
+ };
1636
+ for (; attempts < maxAttempts; ) {
1637
+ try {
1638
+ attempts > 0 && await new Promise((resolve) => setTimeout(resolve, 1e3));
1639
+ const textTracks = (await getAsset(client, assetId)).data.tracks?.filter((track) => track.type === "text") || [], foundTrack = findTrack(textTracks);
1640
+ if (!foundTrack) {
1641
+ attempts++;
1642
+ continue;
1643
+ }
1644
+ if (trackFound = !0, newTrack = foundTrack, onTrackFound && onTrackFound(foundTrack), foundTrack.status === "ready") {
1645
+ onTrackReady && onTrackReady(foundTrack);
1646
+ break;
1647
+ }
1648
+ if (foundTrack.status === "errored")
1649
+ return onTrackErrored && onTrackErrored(foundTrack), {
1650
+ track: foundTrack,
1651
+ found: !0,
1652
+ status: "errored"
1653
+ };
1654
+ } catch (error) {
1655
+ console.error("Failed to fetch updated asset:", error);
1656
+ }
1657
+ attempts++;
1658
+ }
1659
+ return !newTrack || !trackFound ? {
1660
+ track: void 0,
1661
+ found: !1,
1662
+ status: "not-found"
1663
+ } : newTrack.status === "preparing" ? {
1664
+ track: newTrack,
1665
+ found: !0,
1666
+ status: "preparing"
1667
+ } : {
1668
+ track: newTrack,
1669
+ found: !0,
1670
+ status: "ready"
1671
+ };
1413
1672
  }
1414
- function formatSecondsToHHMMSS(seconds) {
1415
- const hrs = Math.floor(seconds / 3600).toString().padStart(2, "0"), mins = Math.floor(seconds % 3600 / 60).toString().padStart(2, "0"), secs = Math.floor(seconds % 60).toString().padStart(2, "0");
1416
- return `${hrs}:${mins}:${secs}`;
1673
+ async function downloadVttFile(client, asset, track) {
1674
+ if (!track.id)
1675
+ throw new Error("Track ID is missing");
1676
+ if (track.status !== "ready")
1677
+ throw new Error(`Track is not ready yet. Status: ${track.status}`);
1678
+ if (!asset.assetId)
1679
+ throw new Error("Asset ID is required");
1680
+ const playbackId = getPlaybackId(asset);
1681
+ if (!playbackId)
1682
+ throw new Error("Playback ID is required");
1683
+ const playbackPolicy = getPlaybackPolicy(asset)?.policy;
1684
+ let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
1685
+ if (playbackPolicy === "signed" || playbackPolicy === "drm") {
1686
+ const token = generateJwt(client, playbackId, "v");
1687
+ downloadUrl += `?token=${token}`;
1688
+ }
1689
+ const response = await fetch(downloadUrl);
1690
+ if (!response.ok)
1691
+ throw new Error(`Failed to download file: ${response.statusText}`);
1692
+ const blob = await response.blob(), blobUrl = URL.createObjectURL(blob), link = document.createElement("a");
1693
+ link.href = blobUrl, link.download = `${asset.filename || "captions"}-${track.language_code || "en"}.vtt`, document.body.appendChild(link), link.click(), document.body.removeChild(link), URL.revokeObjectURL(blobUrl);
1417
1694
  }
1418
- function isValidTimeFormat(time) {
1419
- return /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/.test(time) || time === "";
1695
+ const SUPPORTED_MUX_LANGUAGES = [
1696
+ { label: "English", code: "en", state: "Stable" },
1697
+ { label: "Spanish", code: "es", state: "Stable" },
1698
+ { label: "Italian", code: "it", state: "Stable" },
1699
+ { label: "Portuguese", code: "pt", state: "Stable" },
1700
+ { label: "German", code: "de", state: "Stable" },
1701
+ { label: "French", code: "fr", state: "Stable" },
1702
+ { label: "Polish", code: "pl", state: "Beta" },
1703
+ { label: "Russian", code: "ru", state: "Beta" },
1704
+ { label: "Dutch", code: "nl", state: "Beta" },
1705
+ { label: "Catalan", code: "ca", state: "Beta" },
1706
+ { label: "Turkish", code: "tr", state: "Beta" },
1707
+ { label: "Swedish", code: "sv", state: "Beta" },
1708
+ { label: "Ukrainian", code: "uk", state: "Beta" },
1709
+ { label: "Norwegian", code: "no", state: "Beta" },
1710
+ { label: "Finnish", code: "fi", state: "Beta" },
1711
+ { label: "Slovak", code: "sk", state: "Beta" },
1712
+ { label: "Greek", code: "el", state: "Beta" },
1713
+ { label: "Czech", code: "cs", state: "Beta" },
1714
+ { label: "Croatian", code: "hr", state: "Beta" },
1715
+ { label: "Danish", code: "da", state: "Beta" },
1716
+ { label: "Romanian", code: "ro", state: "Beta" },
1717
+ { label: "Bulgarian", code: "bg", state: "Beta" }
1718
+ ];
1719
+ function isCustomTextTrack(track) {
1720
+ return track.type !== "autogenerated";
1420
1721
  }
1421
- function getSecondsFromTimeFormat(time) {
1422
- const [hh = 0, mm = 0, ss = 0] = time.split(":").map(Number);
1423
- return hh * 3600 + mm * 60 + ss;
1722
+ function isAutogeneratedTrack(track) {
1723
+ return track.type === "autogenerated";
1424
1724
  }
1425
- function EditThumbnailDialog({ asset, currentTime = 0 }) {
1426
- const client = useClient(), { setDialogState } = useDialogStateContext(), dialogId = `EditThumbnailDialog${React.useId()}`, [timeFormatted, setTimeFormatted] = React.useState(
1427
- () => formatSecondsToHHMMSS(currentTime)
1428
- ), [nextTime, setNextTime] = React.useState(currentTime), [inputError, setInputError] = React.useState(""), assetWithNewThumbnail = React.useMemo(() => ({ ...asset, thumbTime: nextTime }), [asset, nextTime]), [saving, setSaving] = React.useState(!1), [saveThumbnailError, setSaveThumbnailError] = React.useState(null), handleSave = () => {
1429
- setSaving(!0), client.patch(asset._id).set({ thumbTime: nextTime }).commit({ returnDocuments: !1 }).then(() => void setDialogState(!1)).catch(setSaveThumbnailError).finally(() => void setSaving(!1));
1430
- }, width = 300 * getDevicePixelRatio({ maxDpr: 2 });
1431
- if (saveThumbnailError)
1432
- throw saveThumbnailError;
1725
+ const LANGUAGE_OPTIONS$1 = LanguagesList__default.default.getAllCodes().map((code) => ({
1726
+ value: code,
1727
+ label: LanguagesList__default.default.getNativeName(code)
1728
+ })), MUX_LANGUAGE_OPTIONS = SUPPORTED_MUX_LANGUAGES.map((lang) => ({
1729
+ value: lang.code,
1730
+ label: lang.label
1731
+ }));
1732
+ function AddCaptionDialog({ asset, onAdd, onClose }) {
1733
+ const client = useClient(), toast = ui.useToast(), dialogId = `AddCaptionDialog${React.useId()}`, [isAutogenerated, setIsAutogenerated] = React.useState(!1), [vttUrl, setVttUrl] = React.useState(""), [languageCode, setLanguageCode] = React.useState(""), [selectedLanguage, setSelectedLanguage] = React.useState(
1734
+ null
1735
+ ), [name2, setName] = React.useState(""), [isSubmitting, setIsSubmitting] = React.useState(!1), [selectedFile, setSelectedFile] = React.useState(null), fileInputRef = React.useRef(null), uploadVttFile = async (file) => (await client.assets.upload("file", file, {
1736
+ filename: file.name
1737
+ })).url, handleAddTrackFromUrl = async () => {
1738
+ if (!asset.assetId)
1739
+ throw new Error("Asset ID is required");
1740
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim();
1741
+ let vttUrlToUse = vttUrl.trim();
1742
+ if (selectedFile)
1743
+ try {
1744
+ vttUrlToUse = await uploadVttFile(selectedFile);
1745
+ } catch (uploadError) {
1746
+ throw toast.push({
1747
+ title: "Failed to upload VTT file",
1748
+ status: "error",
1749
+ description: "Could not upload the VTT file to Sanity. Please try again."
1750
+ }), setIsSubmitting(!1), uploadError;
1751
+ }
1752
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1753
+ language_code: trimmedLanguageCode,
1754
+ name: trimmedName,
1755
+ text_type: "subtitles"
1756
+ });
1757
+ const result = await pollTrackStatus({
1758
+ client,
1759
+ assetId: asset.assetId,
1760
+ trackName: trimmedName,
1761
+ trackLanguageCode: trimmedLanguageCode,
1762
+ onTrackErrored: (track) => {
1763
+ const errorMessage = track.error?.messages?.[0] || track.error?.type || "The track failed to download from the provided URL";
1764
+ toast.push({
1765
+ title: "Caption track failed",
1766
+ status: "error",
1767
+ description: errorMessage
1768
+ }), onAdd(track), onClose();
1769
+ }
1770
+ });
1771
+ if (!result.found || !result.track) {
1772
+ toast.push({
1773
+ title: "Caption track may have been added",
1774
+ status: "warning",
1775
+ description: "The track was created but its status could not be determined. It may still be processing. Please refresh the page to see if it appears."
1776
+ }), onClose();
1777
+ return;
1778
+ }
1779
+ if (result.status !== "errored") {
1780
+ if (result.status === "preparing") {
1781
+ toast.push({
1782
+ title: "Caption track is processing",
1783
+ status: "info",
1784
+ description: "The track was created and is being processed. It will appear in the list shortly."
1785
+ }), onAdd(result.track), onClose();
1786
+ return;
1787
+ }
1788
+ toast.push({
1789
+ title: "Caption track added",
1790
+ status: "success",
1791
+ description: "Caption track added successfully"
1792
+ }), onAdd(result.track), onClose();
1793
+ }
1794
+ }, handleGenerateSubtitles = async () => {
1795
+ if (!asset.assetId)
1796
+ throw new Error("Asset ID is required");
1797
+ const audioTrack = (await getAsset(client, asset.assetId)).data.tracks?.find((track) => track.type === "audio");
1798
+ if (!audioTrack || !audioTrack.id)
1799
+ throw toast.push({
1800
+ title: "No audio track found",
1801
+ status: "error",
1802
+ description: "The asset does not have an audio track. Auto-generated subtitles require an audio track."
1803
+ }), new Error("No audio track found");
1804
+ await generateSubtitles(client, asset.assetId, audioTrack.id, {
1805
+ language_code: languageCode.trim(),
1806
+ name: name2.trim()
1807
+ });
1808
+ const mockTrack = {
1809
+ type: "text",
1810
+ id: `generating-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1811
+ text_type: "subtitles",
1812
+ text_source: "generated_live",
1813
+ language_code: languageCode.trim(),
1814
+ name: name2.trim(),
1815
+ status: "preparing"
1816
+ };
1817
+ toast.push({
1818
+ title: "Generating subtitles",
1819
+ status: "success",
1820
+ description: "This may take a few minutes"
1821
+ }), onAdd(mockTrack), onClose();
1822
+ }, handleSubmit = async () => {
1823
+ if (!isAutogenerated) {
1824
+ if (!selectedFile && !vttUrl.trim()) {
1825
+ toast.push({
1826
+ title: "VTT file or URL required",
1827
+ status: "error",
1828
+ description: "Please select a VTT file or enter a VTT file URL"
1829
+ });
1830
+ return;
1831
+ }
1832
+ if (vttUrl.trim() && !selectedFile)
1833
+ try {
1834
+ new URL(vttUrl.trim());
1835
+ } catch {
1836
+ toast.push({
1837
+ title: "Invalid URL",
1838
+ status: "error",
1839
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
1840
+ });
1841
+ return;
1842
+ }
1843
+ }
1844
+ if (!name2.trim()) {
1845
+ toast.push({
1846
+ title: "Audio name required",
1847
+ status: "error",
1848
+ description: "Please enter an audio name for this caption track"
1849
+ });
1850
+ return;
1851
+ }
1852
+ if (!languageCode.trim()) {
1853
+ toast.push({
1854
+ title: "Language code required",
1855
+ status: "error",
1856
+ description: "Please enter a language code (e.g., en, es, fr)"
1857
+ });
1858
+ return;
1859
+ }
1860
+ setIsSubmitting(!0);
1861
+ try {
1862
+ isAutogenerated ? await handleGenerateSubtitles() : await handleAddTrackFromUrl();
1863
+ } catch (error) {
1864
+ toast.push({
1865
+ title: "Failed to add caption track",
1866
+ status: "error",
1867
+ description: extractErrorMessage(error, "Failed to add caption track")
1868
+ });
1869
+ } finally {
1870
+ setIsSubmitting(!1);
1871
+ }
1872
+ };
1433
1873
  return /* @__PURE__ */ jsxRuntime.jsx(
1434
1874
  ui.Dialog,
1435
1875
  {
1436
1876
  id: dialogId,
1437
- header: "Edit thumbnail",
1438
- onClose: () => setDialogState(!1),
1439
- footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
1440
- ui.Button,
1441
- {
1442
- disabled: inputError !== "",
1443
- mode: "ghost",
1444
- tone: "primary",
1445
- loading: saving,
1446
- onClick: handleSave,
1447
- text: "Set new thumbnail"
1448
- },
1449
- "thumbnail"
1450
- ) }),
1451
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
1877
+ header: "Add Caption Track",
1878
+ onClose,
1879
+ width: 1,
1880
+ onClickOutside: onClose,
1881
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, children: [
1882
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1883
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", marginBottom: 3, children: [
1884
+ /* @__PURE__ */ jsxRuntime.jsx(
1885
+ ui.Checkbox,
1886
+ {
1887
+ id: "autogenerated-checkbox",
1888
+ style: { display: "block" },
1889
+ checked: isAutogenerated,
1890
+ onChange: (e) => {
1891
+ setIsAutogenerated(e.currentTarget.checked), e.currentTarget.checked && setVttUrl("");
1892
+ },
1893
+ disabled: isSubmitting
1894
+ }
1895
+ ),
1896
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { flex: 1, paddingLeft: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: "autogenerated-checkbox", children: "Generate captions" }) }) })
1897
+ ] }),
1898
+ !isAutogenerated && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1899
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
1900
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", children: [
1901
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: selectedFile ? `Selected: ${selectedFile.name}` : "No file selected" }),
1902
+ /* @__PURE__ */ jsxRuntime.jsx(
1903
+ ui.Button,
1904
+ {
1905
+ icon: icons.UploadIcon,
1906
+ text: "Select File",
1907
+ mode: "ghost",
1908
+ tone: "primary",
1909
+ fontSize: 1,
1910
+ padding: 2,
1911
+ onClick: () => fileInputRef.current?.click(),
1912
+ disabled: isSubmitting
1913
+ }
1914
+ )
1915
+ ] }),
1916
+ /* @__PURE__ */ jsxRuntime.jsx(
1917
+ "input",
1918
+ {
1919
+ ref: fileInputRef,
1920
+ type: "file",
1921
+ accept: ".vtt,text/vtt",
1922
+ style: { display: "none" },
1923
+ onChange: (e) => {
1924
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
1925
+ }
1926
+ }
1927
+ )
1928
+ ] }),
1929
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, style: { textAlign: "center" }, children: "Or enter the VTT file URL" }),
1930
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1931
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vtt-url", children: "VTT File URL" }),
1932
+ /* @__PURE__ */ jsxRuntime.jsx(
1933
+ ui.TextInput,
1934
+ {
1935
+ id: "vtt-url",
1936
+ placeholder: "https://example.com/subtitles.vtt",
1937
+ value: vttUrl,
1938
+ onChange: (e) => {
1939
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
1940
+ },
1941
+ disabled: isSubmitting
1942
+ }
1943
+ )
1944
+ ] })
1945
+ ] })
1946
+ ] }),
1947
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1948
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-name", children: "Audio name" }),
1949
+ /* @__PURE__ */ jsxRuntime.jsx(
1950
+ ui.Autocomplete,
1951
+ {
1952
+ id: "caption-name",
1953
+ value: selectedLanguage?.value || "",
1954
+ onChange: (newValue) => {
1955
+ const selected = (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find((opt) => opt.value === newValue);
1956
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
1957
+ },
1958
+ options: isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1,
1959
+ icon: icons.TranslateIcon,
1960
+ placeholder: "Select language",
1961
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
1962
+ openButton: !0,
1963
+ renderValue: (value) => (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find(
1964
+ (l) => l.value === value
1965
+ )?.label || value,
1966
+ renderOption: (option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, textOverflow: "ellipsis", children: [
1967
+ option.label,
1968
+ " (",
1969
+ option.value,
1970
+ ")"
1971
+ ] }) }),
1972
+ disabled: isSubmitting
1973
+ }
1974
+ )
1975
+ ] }),
1976
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1977
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-language", children: "Language Code" }),
1978
+ /* @__PURE__ */ jsxRuntime.jsx(
1979
+ ui.TextInput,
1980
+ {
1981
+ id: "caption-language",
1982
+ placeholder: "en-US",
1983
+ value: languageCode,
1984
+ onChange: (e) => {
1985
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
1986
+ },
1987
+ disabled: isSubmitting
1988
+ }
1989
+ )
1990
+ ] }),
1991
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
1992
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
1993
+ /* @__PURE__ */ jsxRuntime.jsx(
1994
+ ui.Button,
1995
+ {
1996
+ text: "Add Caption Track",
1997
+ tone: "primary",
1998
+ icon: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(
1999
+ ui.Spinner,
2000
+ {
2001
+ style: {
2002
+ verticalAlign: "middle",
2003
+ display: "inline-block",
2004
+ marginBottom: "-3px",
2005
+ width: "1em",
2006
+ height: "1em",
2007
+ marginRight: "-6px"
2008
+ }
2009
+ }
2010
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.UploadIcon, {}),
2011
+ onClick: handleSubmit,
2012
+ disabled: isSubmitting
2013
+ }
2014
+ )
2015
+ ] })
2016
+ ] })
2017
+ }
2018
+ );
2019
+ }
2020
+ const LANGUAGE_OPTIONS = LanguagesList__default.default.getAllCodes().map((code) => ({
2021
+ value: code,
2022
+ label: LanguagesList__default.default.getNativeName(code)
2023
+ }));
2024
+ function EditCaptionDialog({ asset, track, onUpdate, onClose }) {
2025
+ const client = useClient(), toast = ui.useToast(), dialogId = `EditCaptionDialog${React.useId()}`, isAutogenerated = track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod", [vttUrl, setVttUrl] = React.useState(""), [languageCode, setLanguageCode] = React.useState(track.language_code || ""), [selectedLanguage, setSelectedLanguage] = React.useState(
2026
+ () => {
2027
+ const baseCode = track.language_code?.split("-")[0], found = LANGUAGE_OPTIONS.find(
2028
+ (opt) => opt.value === track.language_code || opt.value === baseCode
2029
+ );
2030
+ if (found) return found;
2031
+ if (track.name) {
2032
+ const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name);
2033
+ if (foundByName) return foundByName;
2034
+ }
2035
+ return null;
2036
+ }
2037
+ ), [name2, setName] = React.useState(track.name || ""), [isSubmitting, setIsSubmitting] = React.useState(!1), [downloading, setDownloading] = React.useState(!1), [selectedFile, setSelectedFile] = React.useState(null), fileInputRef = React.useRef(null);
2038
+ React.useEffect(() => {
2039
+ setLanguageCode(track.language_code || ""), setName(track.name || ""), setVttUrl("");
2040
+ const baseCode = track.language_code?.split("-")[0], foundByCode = LANGUAGE_OPTIONS.find(
2041
+ (opt) => opt.value === track.language_code || opt.value === baseCode
2042
+ ), foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null;
2043
+ setSelectedLanguage(foundByCode || foundByName || null);
2044
+ }, [track, asset, client]);
2045
+ const handleDownloadCurrentFile = async () => {
2046
+ setDownloading(!0);
2047
+ try {
2048
+ await downloadVttFile(client, asset, track);
2049
+ } catch (error) {
2050
+ let errorMessage = "Please try again", title = "Failed to download VTT file";
2051
+ error instanceof Error ? (errorMessage = error.message, error.message.includes("Track") && (title = "Cannot download")) : (error === "Track ID is missing" || error === "Track is not ready yet") && (errorMessage = String(error), title = "Cannot download"), toast.push({
2052
+ title,
2053
+ status: "error",
2054
+ description: errorMessage
2055
+ });
2056
+ } finally {
2057
+ setDownloading(!1);
2058
+ }
2059
+ }, getCurrentFileName = () => track.id && asset.filename ? `${asset.filename}-${track.language_code || "en"}.vtt` : `captions-${track.language_code || "en"}.vtt`, uploadVttFile = async (file) => (await client.assets.upload("file", file, {
2060
+ filename: file.name
2061
+ })).url, refreshAssetData = async () => {
2062
+ if (!(!asset._id || !asset.assetId))
2063
+ try {
2064
+ const latestAssetData = await getAsset(client, asset.assetId);
2065
+ await client.patch(asset._id).set({ data: latestAssetData.data, status: latestAssetData.data.status }).commit();
2066
+ } catch (refreshError) {
2067
+ console.error("Failed to refresh asset data:", refreshError);
2068
+ }
2069
+ }, handleUpdateTrackWithNewUrl = async () => {
2070
+ if (!asset.assetId)
2071
+ throw new Error("Asset ID is required");
2072
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim(), oldTrackId = track.id;
2073
+ try {
2074
+ await deleteTextTrack(client, asset.assetId, oldTrackId);
2075
+ } catch (deleteError) {
2076
+ throw toast.push({
2077
+ title: "Failed to delete old track",
2078
+ status: "error",
2079
+ description: "Could not delete the old track. Please try again or delete it manually."
2080
+ }), setIsSubmitting(!1), deleteError;
2081
+ }
2082
+ let vttUrlToUse = vttUrl.trim();
2083
+ if (selectedFile)
2084
+ try {
2085
+ vttUrlToUse = await uploadVttFile(selectedFile);
2086
+ } catch (uploadError) {
2087
+ throw toast.push({
2088
+ title: "Failed to upload VTT file",
2089
+ status: "error",
2090
+ description: "Could not upload the VTT file to Sanity. Please try again."
2091
+ }), setIsSubmitting(!1), uploadError;
2092
+ }
2093
+ try {
2094
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
2095
+ language_code: trimmedLanguageCode,
2096
+ name: trimmedName,
2097
+ text_type: "subtitles"
2098
+ });
2099
+ } catch (error) {
2100
+ throw toast.push({
2101
+ title: "Failed to update caption track",
2102
+ status: "error",
2103
+ description: extractErrorMessage(error, "Failed to update caption track")
2104
+ }), setIsSubmitting(!1), error;
2105
+ }
2106
+ const result = await pollTrackStatus({
2107
+ client,
2108
+ assetId: asset.assetId,
2109
+ trackName: trimmedName,
2110
+ trackLanguageCode: trimmedLanguageCode,
2111
+ onTrackErrored: async (erroredTrack) => {
2112
+ const errorMessage = erroredTrack.error?.messages?.[0] || erroredTrack.error?.type || "The track failed to download from the provided URL";
2113
+ toast.push({
2114
+ title: "Caption track failed",
2115
+ status: "error",
2116
+ description: errorMessage
2117
+ }), await refreshAssetData(), onUpdate(erroredTrack, oldTrackId), setIsSubmitting(!1);
2118
+ }
2119
+ });
2120
+ if (!result.found || !result.track) {
2121
+ toast.push({
2122
+ title: "Caption track may have been updated",
2123
+ status: "warning",
2124
+ description: "The track was updated but its status could not be determined. It may still be processing. Please refresh the page to see if it appears."
2125
+ }), setIsSubmitting(!1);
2126
+ return;
2127
+ }
2128
+ result.status !== "errored" && (await refreshAssetData(), result.status === "preparing" ? toast.push({
2129
+ title: "Caption track is processing",
2130
+ status: "info",
2131
+ description: "The track was updated and is being processed. It will appear in the list shortly."
2132
+ }) : toast.push({
2133
+ title: "Caption track updated",
2134
+ status: "success",
2135
+ description: "Caption track updated successfully"
2136
+ }), onUpdate(result.track, oldTrackId), setIsSubmitting(!1));
2137
+ }, handleSubmit = async () => {
2138
+ if (!name2.trim()) {
2139
+ toast.push({
2140
+ title: "Audio name required",
2141
+ status: "error",
2142
+ description: "Please enter an audio name for this caption track"
2143
+ });
2144
+ return;
2145
+ }
2146
+ if (!languageCode.trim()) {
2147
+ toast.push({
2148
+ title: "Language code required",
2149
+ status: "error",
2150
+ description: "Please enter a language code (e.g., en, es, fr)"
2151
+ });
2152
+ return;
2153
+ }
2154
+ setIsSubmitting(!0);
2155
+ try {
2156
+ if (!asset.assetId)
2157
+ throw new Error("Asset ID is required");
2158
+ const originalVttUrl = (() => {
2159
+ if (isAutogenerated || !track.id) return "";
2160
+ const playbackId = getPlaybackId(asset);
2161
+ if (!playbackId) return "";
2162
+ let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
2163
+ if (getPlaybackPolicy(asset)?.policy === "signed") {
2164
+ const token = generateJwt(client, playbackId, "v");
2165
+ url += `?token=${token}`;
2166
+ }
2167
+ return url;
2168
+ })(), urlChanged = selectedFile !== null || vttUrl.trim() && vttUrl.trim() !== originalVttUrl;
2169
+ if (!urlChanged) {
2170
+ toast.push({
2171
+ title: "No changes",
2172
+ status: "info",
2173
+ description: 'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.'
2174
+ }), setIsSubmitting(!1);
2175
+ return;
2176
+ }
2177
+ if (urlChanged) {
2178
+ if (!selectedFile && vttUrl.trim())
2179
+ try {
2180
+ new URL(vttUrl.trim());
2181
+ } catch {
2182
+ toast.push({
2183
+ title: "Invalid URL",
2184
+ status: "error",
2185
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
2186
+ }), setIsSubmitting(!1);
2187
+ return;
2188
+ }
2189
+ if (!selectedFile && !vttUrl.trim()) {
2190
+ toast.push({
2191
+ title: "VTT file or URL required",
2192
+ status: "error",
2193
+ description: "Please select a VTT file or enter a VTT file URL"
2194
+ }), setIsSubmitting(!1);
2195
+ return;
2196
+ }
2197
+ await handleUpdateTrackWithNewUrl();
2198
+ }
2199
+ onClose();
2200
+ } catch (error) {
2201
+ toast.push({
2202
+ title: "Failed to update caption track",
2203
+ status: "error",
2204
+ description: error instanceof Error ? error.message : "Please try again"
2205
+ });
2206
+ } finally {
2207
+ setIsSubmitting(!1);
2208
+ }
2209
+ };
2210
+ return /* @__PURE__ */ jsxRuntime.jsx(
2211
+ ui.Dialog,
2212
+ {
2213
+ id: dialogId,
2214
+ header: "Edit Caption Track",
2215
+ onClose,
2216
+ width: 1,
2217
+ onClickOutside: onClose,
2218
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, children: [
2219
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2220
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
2221
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", children: [
2222
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: getCurrentFileName() }),
2223
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
2224
+ track.status !== "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2225
+ ui.Button,
2226
+ {
2227
+ icon: downloading ? /* @__PURE__ */ jsxRuntime.jsx(
2228
+ ui.Spinner,
2229
+ {
2230
+ style: {
2231
+ verticalAlign: "middle",
2232
+ display: "inline-block",
2233
+ marginTop: "-2px",
2234
+ width: "0.5em",
2235
+ height: "0.5em"
2236
+ }
2237
+ }
2238
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.DownloadIcon, {}),
2239
+ text: "Download",
2240
+ mode: "ghost",
2241
+ tone: "primary",
2242
+ fontSize: 1,
2243
+ padding: 2,
2244
+ onClick: handleDownloadCurrentFile,
2245
+ disabled: downloading || isSubmitting
2246
+ }
2247
+ ),
2248
+ /* @__PURE__ */ jsxRuntime.jsx(
2249
+ ui.Button,
2250
+ {
2251
+ icon: icons.UploadIcon,
2252
+ text: "Replace",
2253
+ mode: "ghost",
2254
+ tone: "primary",
2255
+ fontSize: 1,
2256
+ padding: 2,
2257
+ onClick: () => fileInputRef.current?.click(),
2258
+ disabled: isSubmitting
2259
+ }
2260
+ )
2261
+ ] })
2262
+ ] }),
2263
+ /* @__PURE__ */ jsxRuntime.jsx(
2264
+ "input",
2265
+ {
2266
+ ref: fileInputRef,
2267
+ type: "file",
2268
+ accept: ".vtt,text/vtt",
2269
+ style: { display: "none" },
2270
+ onChange: (e) => {
2271
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
2272
+ }
2273
+ }
2274
+ ),
2275
+ selectedFile && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, style: { marginTop: 8 }, children: [
2276
+ "Selected: ",
2277
+ selectedFile.name
2278
+ ] })
2279
+ ] }),
2280
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2281
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "vtt-url", children: "VTT File URL" }),
2282
+ /* @__PURE__ */ jsxRuntime.jsx(
2283
+ ui.TextInput,
2284
+ {
2285
+ id: "vtt-url",
2286
+ placeholder: "https://example.com/subtitles.vtt",
2287
+ value: vttUrl,
2288
+ onChange: (e) => {
2289
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
2290
+ },
2291
+ disabled: isSubmitting
2292
+ }
2293
+ ),
2294
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Add a URL to replace the existing VTT file with a new one" })
2295
+ ] })
2296
+ ] }),
2297
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2298
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-name", children: "Audio name" }),
2299
+ /* @__PURE__ */ jsxRuntime.jsx(
2300
+ ui.Autocomplete,
2301
+ {
2302
+ id: "caption-name",
2303
+ value: selectedLanguage?.value || "",
2304
+ onChange: (newValue) => {
2305
+ const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue);
2306
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
2307
+ },
2308
+ options: LANGUAGE_OPTIONS,
2309
+ icon: icons.TranslateIcon,
2310
+ placeholder: "Select language",
2311
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
2312
+ openButton: !0,
2313
+ renderValue: (value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value,
2314
+ renderOption: (option) => /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, textOverflow: "ellipsis", children: [
2315
+ option.label,
2316
+ " (",
2317
+ option.value,
2318
+ ")"
2319
+ ] }) }),
2320
+ disabled: isSubmitting
2321
+ }
2322
+ )
2323
+ ] }),
2324
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
2325
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "caption-language", children: "Language Code" }),
2326
+ /* @__PURE__ */ jsxRuntime.jsx(
2327
+ ui.TextInput,
2328
+ {
2329
+ id: "caption-language",
2330
+ placeholder: "en-US",
2331
+ value: languageCode,
2332
+ onChange: (e) => {
2333
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
2334
+ },
2335
+ disabled: isSubmitting
2336
+ }
2337
+ )
2338
+ ] }),
2339
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
2340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
2341
+ /* @__PURE__ */ jsxRuntime.jsx(
2342
+ ui.Button,
2343
+ {
2344
+ text: "Update Caption Track",
2345
+ tone: "primary",
2346
+ icon: isSubmitting ? /* @__PURE__ */ jsxRuntime.jsx(
2347
+ ui.Spinner,
2348
+ {
2349
+ style: {
2350
+ verticalAlign: "middle",
2351
+ display: "inline-block",
2352
+ marginBottom: "-3px",
2353
+ width: "1em",
2354
+ height: "1em",
2355
+ marginRight: "-6px"
2356
+ }
2357
+ }
2358
+ ) : icons.UploadIcon,
2359
+ onClick: handleSubmit,
2360
+ disabled: isSubmitting
2361
+ }
2362
+ )
2363
+ ] })
2364
+ ] })
2365
+ }
2366
+ );
2367
+ }
2368
+ function TrackCard({
2369
+ track,
2370
+ iconOnly,
2371
+ downloadingTrackId,
2372
+ deletingTrackId,
2373
+ trackToEdit,
2374
+ getTrackSourceLabel,
2375
+ handleDownload,
2376
+ setTrackToEdit,
2377
+ setTrackToDelete
2378
+ }) {
2379
+ const isDisabled = (action) => action === "download" ? downloadingTrackId !== null || deletingTrackId === track.id || trackToEdit?.id === track.id : action === "edit" ? downloadingTrackId === track.id || deletingTrackId === track.id || trackToEdit?.id === track.id : downloadingTrackId === track.id || deletingTrackId !== null || trackToEdit?.id === track.id, renderActionButtons = () => track.status === "preparing" ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
2380
+ /* @__PURE__ */ jsxRuntime.jsx(
2381
+ ui.Spinner,
2382
+ {
2383
+ muted: !0,
2384
+ style: {
2385
+ width: "0.75em",
2386
+ height: "0.75em",
2387
+ verticalAlign: "middle",
2388
+ display: "inline-block",
2389
+ marginBottom: "-2px"
2390
+ }
2391
+ }
2392
+ ),
2393
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Processing..." })
2394
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, children: [
2395
+ track.status !== "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2396
+ ui.Button,
2397
+ {
2398
+ icon: downloadingTrackId === track.id ? /* @__PURE__ */ jsxRuntime.jsx(
2399
+ ui.Spinner,
2400
+ {
2401
+ style: {
2402
+ verticalAlign: "middle",
2403
+ display: "inline-block",
2404
+ marginTop: "-2px",
2405
+ width: "0.5em",
2406
+ height: "0.5em"
2407
+ }
2408
+ }
2409
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.DownloadIcon, {}),
2410
+ text: iconOnly ? void 0 : "Download",
2411
+ mode: "ghost",
2412
+ tone: "primary",
2413
+ fontSize: 1,
2414
+ padding: 2,
2415
+ onClick: () => handleDownload(track),
2416
+ disabled: isDisabled("download"),
2417
+ title: "Download"
2418
+ }
2419
+ ),
2420
+ /* @__PURE__ */ jsxRuntime.jsx(
2421
+ ui.Button,
2422
+ {
2423
+ icon: /* @__PURE__ */ jsxRuntime.jsx(icons.EditIcon, {}),
2424
+ text: iconOnly ? void 0 : "Edit",
2425
+ mode: "ghost",
2426
+ tone: "primary",
2427
+ fontSize: 1,
2428
+ padding: 2,
2429
+ disabled: isDisabled("edit"),
2430
+ onClick: () => setTrackToEdit(track),
2431
+ title: "Edit"
2432
+ }
2433
+ ),
2434
+ /* @__PURE__ */ jsxRuntime.jsx(
2435
+ ui.Button,
2436
+ {
2437
+ icon: deletingTrackId === track.id ? /* @__PURE__ */ jsxRuntime.jsx(
2438
+ ui.Spinner,
2439
+ {
2440
+ style: {
2441
+ verticalAlign: "middle",
2442
+ display: "inline-block",
2443
+ marginTop: "-2px",
2444
+ width: "0.5em",
2445
+ height: "0.5em"
2446
+ }
2447
+ }
2448
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.TrashIcon, {}),
2449
+ text: iconOnly ? void 0 : "Delete",
2450
+ mode: "ghost",
2451
+ tone: "critical",
2452
+ fontSize: 1,
2453
+ padding: 2,
2454
+ disabled: isDisabled("delete"),
2455
+ onClick: () => setTrackToDelete(track),
2456
+ title: "Delete"
2457
+ }
2458
+ )
2459
+ ] });
2460
+ return /* @__PURE__ */ jsxRuntime.jsx(
2461
+ ui.Card,
2462
+ {
2463
+ padding: 3,
2464
+ radius: 2,
2465
+ tone: track.status === "errored" ? "caution" : "transparent",
2466
+ border: !0,
2467
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", gap: 3, children: [
2468
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, flex: 1, children: [
2469
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
2470
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "semibold", children: track.name || "Untitled" }),
2471
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
2472
+ "(",
2473
+ getTrackSourceLabel(track),
2474
+ ")"
2475
+ ] }),
2476
+ track.status === "errored" && /* @__PURE__ */ jsxRuntime.jsx(
2477
+ icons.ErrorOutlineIcon,
2478
+ {
2479
+ style: { color: "var(--card-critical-color)" },
2480
+ "aria-label": "Error",
2481
+ fontSize: 20
2482
+ }
2483
+ )
2484
+ ] }),
2485
+ track.language_code && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 1, muted: !0, children: [
2486
+ "Language: ",
2487
+ track.language_code
2488
+ ] }),
2489
+ track.status === "errored" && track.error && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, style: { color: "var(--card-critical-color)" }, children: track.error.messages?.[0] || track.error.type || "Failed to process track" })
2490
+ ] }),
2491
+ renderActionButtons()
2492
+ ] })
2493
+ }
2494
+ );
2495
+ }
2496
+ function TextTracksManager({
2497
+ asset,
2498
+ iconOnly = !1,
2499
+ tracks: propTracks,
2500
+ collapseTracks = !1
2501
+ }) {
2502
+ const client = useClient(), toast = ui.useToast(), dialogId = `DeleteCaptionDialog${React.useId()}`, [downloadingTrackId, setDownloadingTrackId] = React.useState(null), [deletingTrackId, setDeletingTrackId] = React.useState(null), [addedTracks, setAddedTracks] = React.useState([]), [updatedTracks, setUpdatedTracks] = React.useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = React.useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = React.useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = React.useState(null), [trackToEdit, setTrackToEdit] = React.useState(null), [showAddDialog, setShowAddDialog] = React.useState(!1), [isExpanded, setIsExpanded] = React.useState(!1), MAX_VISIBLE_TRACKS = 4;
2503
+ React.useEffect(() => {
2504
+ if (!asset.assetId || !asset._id) return;
2505
+ const assetId = asset.assetId, documentId = asset._id;
2506
+ (async () => {
2507
+ try {
2508
+ const response = await getAsset(client, assetId);
2509
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2510
+ } catch (error) {
2511
+ console.error("Failed to refresh asset data:", error);
2512
+ }
2513
+ })();
2514
+ }, [asset.assetId, asset._id, client]);
2515
+ const activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2516
+ (track) => track.id && (track.status === "ready" || track.status === "preparing" || track.status === "errored")
2517
+ ), allTracks = React.useMemo(() => {
2518
+ const tracksWithUpdates = activeTracks.map((track) => updatedTracks.get(track.id) || track), isMockTrackReplaced = (mockTrack, realTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : realTracksList.some((realTrack) => {
2519
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2520
+ 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";
2521
+ }), isTrackAlreadyInRealTracks = (addedTrack, realTracksList) => addedTrack.id ? addedTrack.id.startsWith("generating-") ? isMockTrackReplaced(addedTrack, realTracksList) : realTracksList.some((realTrack) => realTrack.id === addedTrack.id) : !1, tracksToKeep = addedTracks.filter((addedTrack) => addedTrack.id && addedTrack.id.startsWith("generating-") ? !isMockTrackReplaced(addedTrack, tracksWithUpdates) : !isTrackAlreadyInRealTracks(addedTrack, tracksWithUpdates));
2522
+ return [...tracksWithUpdates, ...tracksToKeep];
2523
+ }, [activeTracks, addedTracks, updatedTracks]);
2524
+ React.useEffect(() => {
2525
+ const newAutogeneratedIds = /* @__PURE__ */ new Set();
2526
+ activeTracks.forEach((track) => {
2527
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2528
+ }), addedTracks.forEach((mockTrack) => {
2529
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2530
+ const realTrack = activeTracks.find((rt) => {
2531
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2532
+ return nameMatches && languageMatches;
2533
+ });
2534
+ realTrack?.id && newAutogeneratedIds.add(realTrack.id);
2535
+ }
2536
+ }), setAutogeneratedTrackIds((prev) => {
2537
+ let hasNew = !1;
2538
+ const updated = new Set(prev);
2539
+ return newAutogeneratedIds.forEach((id) => {
2540
+ prev.has(id) || (updated.add(id), hasNew = !0);
2541
+ }), hasNew ? updated : prev;
2542
+ });
2543
+ }, [activeTracks, addedTracks]), React.useEffect(() => {
2544
+ if (allTracks.filter((track) => track.status === "preparing").length === 0 || !asset.assetId || !asset._id)
2545
+ return;
2546
+ const assetId = asset.assetId, documentId = asset._id, interval = setInterval(async () => {
2547
+ try {
2548
+ const response = await getAsset(client, assetId);
2549
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2550
+ const fetchedTracks = response.data.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2551
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2552
+ 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";
2553
+ }), newAutogeneratedIds = /* @__PURE__ */ new Set();
2554
+ fetchedTracks.forEach((track) => {
2555
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2556
+ });
2557
+ const findMatchingRealTrack = (mockTrack, tracksList) => tracksList.find((rt) => {
2558
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2559
+ return nameMatches && languageMatches;
2560
+ });
2561
+ setAddedTracks((prev) => prev.filter((mockTrack) => {
2562
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2563
+ const replaced = isMockTrackReplaced(mockTrack, fetchedTracks);
2564
+ if (replaced) {
2565
+ const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks);
2566
+ realTrack?.id && (newAutogeneratedIds.add(realTrack.id), setTrackActivityOrder((prevOrder) => {
2567
+ const mockOrder = prevOrder.get(mockTrack.id);
2568
+ if (mockOrder) {
2569
+ const newMap = new Map(prevOrder);
2570
+ return newMap.set(realTrack.id, mockOrder), newMap;
2571
+ }
2572
+ return prevOrder;
2573
+ }));
2574
+ }
2575
+ return !replaced;
2576
+ }
2577
+ return !0;
2578
+ })), newAutogeneratedIds.size > 0 && setAutogeneratedTrackIds((prevIds) => {
2579
+ const updated = new Set(prevIds);
2580
+ return newAutogeneratedIds.forEach((id) => updated.add(id)), updated;
2581
+ });
2582
+ } catch (error) {
2583
+ console.error("Failed to refresh asset data:", error);
2584
+ }
2585
+ }, 3e3);
2586
+ return () => clearInterval(interval);
2587
+ }, [allTracks, asset.assetId, asset._id, client]);
2588
+ const visibleTracks = allTracks.filter(
2589
+ (track) => track.status === "ready" || track.status === "preparing" || track.status === "errored"
2590
+ ).sort((a2, b) => {
2591
+ const orderA = trackActivityOrder.get(a2.id) || 0, orderB = trackActivityOrder.get(b.id) || 0;
2592
+ if (orderA > 0 && orderB > 0)
2593
+ return orderB - orderA;
2594
+ if (orderA > 0) return -1;
2595
+ if (orderB > 0) return 1;
2596
+ const aIsPreparing = a2.status === "preparing", bIsPreparing = b.status === "preparing";
2597
+ if (aIsPreparing && !bIsPreparing) return -1;
2598
+ if (!aIsPreparing && bIsPreparing) return 1;
2599
+ const aIsAutogenerated = a2.id && a2.id.startsWith("generating-") || a2.id && autogeneratedTrackIds.has(a2.id), bIsAutogenerated = b.id && b.id.startsWith("generating-") || b.id && autogeneratedTrackIds.has(b.id);
2600
+ return aIsAutogenerated && !bIsAutogenerated ? -1 : !aIsAutogenerated && bIsAutogenerated ? 1 : 0;
2601
+ }), handleDownload = async (track) => {
2602
+ if (track.id) {
2603
+ setDownloadingTrackId(track.id);
2604
+ try {
2605
+ await downloadVttFile(client, asset, track);
2606
+ } catch (error) {
2607
+ toast.push({
2608
+ title: "Failed to download VTT file",
2609
+ status: "error",
2610
+ description: error instanceof Error ? error.message : "Please try again"
2611
+ });
2612
+ } finally {
2613
+ setDownloadingTrackId(null);
2614
+ }
2615
+ }
2616
+ }, confirmDelete = async () => {
2617
+ if (!trackToDelete || !trackToDelete.id) return;
2618
+ const track = trackToDelete;
2619
+ setTrackToDelete(null), setDeletingTrackId(track.id);
2620
+ try {
2621
+ if (!asset.assetId)
2622
+ throw new Error("Asset ID is required");
2623
+ if (await deleteTextTrack(client, asset.assetId, track.id), asset._id)
2624
+ try {
2625
+ const response = await getAsset(client, asset.assetId);
2626
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2627
+ } catch (refreshError) {
2628
+ console.error("Failed to refresh asset data:", refreshError);
2629
+ }
2630
+ toast.push({
2631
+ title: "Successfully deleted caption track",
2632
+ status: "success"
2633
+ }), setAddedTracks((prev) => prev.filter((t) => t.id !== track.id)), setUpdatedTracks((prev) => {
2634
+ const newMap = new Map(prev);
2635
+ return newMap.delete(track.id), newMap;
2636
+ }), setTrackActivityOrder((prev) => {
2637
+ const newMap = new Map(prev);
2638
+ return newMap.delete(track.id), newMap;
2639
+ }), setAutogeneratedTrackIds((prev) => {
2640
+ const updated = new Set(prev);
2641
+ return updated.delete(track.id), updated;
2642
+ });
2643
+ } catch (error) {
2644
+ toast.push({
2645
+ title: "Failed to delete caption track",
2646
+ status: "error",
2647
+ description: error instanceof Error ? error.message : "Please try again"
2648
+ });
2649
+ } finally {
2650
+ setDeletingTrackId(null);
2651
+ }
2652
+ }, handleAddTrack = (track) => {
2653
+ setAddedTracks((prev) => [...prev, track]), setTrackActivityOrder((prev) => {
2654
+ const newMap = new Map(prev);
2655
+ return newMap.set(track.id, prev.size + 1), newMap;
2656
+ }), setShowAddDialog(!1);
2657
+ }, handleUpdateTrack = async (updatedTrack, oldTrackId) => {
2658
+ if (oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2659
+ const newMap = new Map(prev);
2660
+ return newMap.delete(oldTrackId), newMap;
2661
+ }), setTrackActivityOrder((prev) => {
2662
+ const newMap = new Map(prev);
2663
+ return newMap.delete(oldTrackId), newMap;
2664
+ }), setAutogeneratedTrackIds((prev) => {
2665
+ const updated = new Set(prev);
2666
+ return updated.delete(oldTrackId), updated;
2667
+ })), addedTracks.some((t) => t.id === updatedTrack.id) ? setAddedTracks((prev) => prev.map((t) => t.id === updatedTrack.id ? updatedTrack : t)) : setUpdatedTracks((prev) => {
2668
+ const newMap = new Map(prev);
2669
+ return newMap.set(updatedTrack.id, updatedTrack), newMap;
2670
+ }), setTrackActivityOrder((prev) => {
2671
+ const newMap = new Map(prev);
2672
+ return newMap.set(updatedTrack.id, prev.size + 1), newMap;
2673
+ }), setTrackToEdit(null), asset._id && asset.assetId)
2674
+ try {
2675
+ const response = await getAsset(client, asset.assetId);
2676
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2677
+ } catch (refreshError) {
2678
+ console.error("Failed to refresh asset data:", refreshError);
2679
+ }
2680
+ }, 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";
2681
+ if (visibleTracks.length === 0 && !showAddDialog)
2682
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2683
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
2684
+ ui.Button,
2685
+ {
2686
+ icon: icons.AddIcon,
2687
+ text: "Add Caption",
2688
+ tone: "primary",
2689
+ onClick: () => setShowAddDialog(!0)
2690
+ }
2691
+ ) }),
2692
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "No captions available. Add captions when uploading a video or add them manually." }) }),
2693
+ showAddDialog && /* @__PURE__ */ jsxRuntime.jsx(
2694
+ AddCaptionDialog,
2695
+ {
2696
+ asset,
2697
+ onAdd: handleAddTrack,
2698
+ onClose: () => setShowAddDialog(!1)
2699
+ }
2700
+ )
2701
+ ] });
2702
+ const displayedTracks = collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks, hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS;
2703
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2704
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "flex-end", children: /* @__PURE__ */ jsxRuntime.jsx(
2705
+ ui.Button,
2706
+ {
2707
+ icon: icons.AddIcon,
2708
+ text: "Add Caption",
2709
+ tone: "primary",
2710
+ onClick: () => setShowAddDialog(!0)
2711
+ }
2712
+ ) }),
2713
+ displayedTracks.map((track) => /* @__PURE__ */ jsxRuntime.jsx(
2714
+ TrackCard,
2715
+ {
2716
+ track,
2717
+ iconOnly,
2718
+ downloadingTrackId,
2719
+ deletingTrackId,
2720
+ trackToEdit,
2721
+ getTrackSourceLabel,
2722
+ handleDownload,
2723
+ setTrackToEdit,
2724
+ setTrackToDelete
2725
+ },
2726
+ track.id
2727
+ )),
2728
+ hasMoreTracks && /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "center", children: /* @__PURE__ */ jsxRuntime.jsx(
2729
+ ui.Button,
2730
+ {
2731
+ icon: isExpanded ? icons.ChevronUpIcon : icons.ChevronDownIcon,
2732
+ text: isExpanded ? "Show less" : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`,
2733
+ mode: "ghost",
2734
+ tone: "primary",
2735
+ onClick: () => setIsExpanded(!isExpanded)
2736
+ }
2737
+ ) }),
2738
+ trackToDelete && /* @__PURE__ */ jsxRuntime.jsx(
2739
+ ui.Dialog,
2740
+ {
2741
+ animate: !0,
2742
+ id: dialogId,
2743
+ header: "Delete track",
2744
+ onClose: () => setTrackToDelete(null),
2745
+ onClickOutside: () => setTrackToDelete(null),
2746
+ width: 1,
2747
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2748
+ ui.Card,
2749
+ {
2750
+ padding: 3,
2751
+ style: {
2752
+ minHeight: "150px",
2753
+ display: "flex",
2754
+ alignItems: "center",
2755
+ justifyContent: "center"
2756
+ },
2757
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
2758
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { size: 2, children: [
2759
+ 'Are you sure you want to delete "',
2760
+ trackToDelete.name || trackToDelete.language_code || "Untitled",
2761
+ '"?'
2762
+ ] }),
2763
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: "This action is irreversible" }),
2764
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, marginY: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
2765
+ ui.Button,
2766
+ {
2767
+ icon: deletingTrackId === trackToDelete.id ? /* @__PURE__ */ jsxRuntime.jsx(
2768
+ ui.Spinner,
2769
+ {
2770
+ style: {
2771
+ verticalAlign: "middle",
2772
+ display: "inline-block",
2773
+ marginTop: "-2px",
2774
+ width: "0.5em",
2775
+ height: "0.5em"
2776
+ }
2777
+ }
2778
+ ) : /* @__PURE__ */ jsxRuntime.jsx(icons.TrashIcon, {}),
2779
+ fontSize: 2,
2780
+ padding: 3,
2781
+ text: "Delete track",
2782
+ tone: "critical",
2783
+ onClick: confirmDelete,
2784
+ disabled: deletingTrackId !== null
2785
+ }
2786
+ ) }) })
2787
+ ] })
2788
+ }
2789
+ )
2790
+ }
2791
+ ),
2792
+ showAddDialog && /* @__PURE__ */ jsxRuntime.jsx(
2793
+ AddCaptionDialog,
2794
+ {
2795
+ asset,
2796
+ onAdd: handleAddTrack,
2797
+ onClose: () => setShowAddDialog(!1)
2798
+ }
2799
+ ),
2800
+ trackToEdit && /* @__PURE__ */ jsxRuntime.jsx(
2801
+ EditCaptionDialog,
2802
+ {
2803
+ asset,
2804
+ track: trackToEdit,
2805
+ onUpdate: handleUpdateTrack,
2806
+ onClose: () => setTrackToEdit(null)
2807
+ }
2808
+ )
2809
+ ] });
2810
+ }
2811
+ const DialogStateContext = React.createContext({
2812
+ dialogState: !1,
2813
+ setDialogState: () => null
2814
+ }), DialogStateProvider = ({
2815
+ dialogState,
2816
+ setDialogState,
2817
+ children
2818
+ }) => /* @__PURE__ */ jsxRuntime.jsx(DialogStateContext.Provider, { value: { dialogState, setDialogState }, children }), useDialogStateContext = () => React.useContext(DialogStateContext);
2819
+ function getVideoSrc({ client, muxPlaybackId: muxPlaybackId2 }) {
2820
+ const searchParams = new URLSearchParams();
2821
+ if (muxPlaybackId2.policy === "signed" || muxPlaybackId2.policy === "drm") {
2822
+ const token = generateJwt(client, muxPlaybackId2.id, "v");
2823
+ searchParams.set("token", token);
2824
+ }
2825
+ return `https://stream.mux.com/${muxPlaybackId2.id}.m3u8?${searchParams}`;
2826
+ }
2827
+ function CaptionsDialog({ asset }) {
2828
+ const { setDialogState } = useDialogStateContext(), dialogId = `CaptionsDialog${React.useId()}`;
2829
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Dialog, { id: dialogId, header: "Edit Captions", onClose: () => setDialogState(!1), width: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(TextTracksManager, { asset }) }) });
2830
+ }
2831
+ function getDevicePixelRatio(options) {
2832
+ const {
2833
+ defaultDpr = 1,
2834
+ maxDpr = 3,
2835
+ round = !0
2836
+ } = options || {}, dpr = typeof window < "u" && typeof window.devicePixelRatio == "number" ? window.devicePixelRatio : defaultDpr;
2837
+ return Math.min(Math.max(1, round ? Math.floor(dpr) : dpr), maxDpr);
2838
+ }
2839
+ function formatSeconds(seconds) {
2840
+ if (typeof seconds != "number" || Number.isNaN(seconds))
2841
+ return "";
2842
+ const hrs = ~~(seconds / 3600), mins = ~~(seconds % 3600 / 60), secs = ~~seconds % 60;
2843
+ let ret = "";
2844
+ return hrs > 0 && (ret += "" + hrs + ":" + (mins < 10 ? "0" : "")), ret += "" + mins + ":" + (secs < 10 ? "0" : ""), ret += "" + secs, ret;
2845
+ }
2846
+ function formatSecondsToHHMMSS(seconds) {
2847
+ const hrs = Math.floor(seconds / 3600).toString().padStart(2, "0"), mins = Math.floor(seconds % 3600 / 60).toString().padStart(2, "0"), secs = Math.floor(seconds % 60).toString().padStart(2, "0");
2848
+ return `${hrs}:${mins}:${secs}`;
2849
+ }
2850
+ function isValidTimeFormat(time) {
2851
+ return /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/.test(time) || time === "";
2852
+ }
2853
+ function getSecondsFromTimeFormat(time) {
2854
+ const [hh = 0, mm = 0, ss = 0] = time.split(":").map(Number);
2855
+ return hh * 3600 + mm * 60 + ss;
2856
+ }
2857
+ function EditThumbnailDialog({ asset, currentTime = 0 }) {
2858
+ const client = useClient(), { setDialogState } = useDialogStateContext(), dialogId = `EditThumbnailDialog${React.useId()}`, [timeFormatted, setTimeFormatted] = React.useState(
2859
+ () => formatSecondsToHHMMSS(currentTime)
2860
+ ), [nextTime, setNextTime] = React.useState(currentTime), [inputError, setInputError] = React.useState(""), assetWithNewThumbnail = React.useMemo(() => ({ ...asset, thumbTime: nextTime }), [asset, nextTime]), [saving, setSaving] = React.useState(!1), [saveThumbnailError, setSaveThumbnailError] = React.useState(null), handleSave = () => {
2861
+ setSaving(!0), client.patch(asset._id).set({ thumbTime: nextTime }).commit({ returnDocuments: !1 }).then(() => void setDialogState(!1)).catch(setSaveThumbnailError).finally(() => void setSaving(!1));
2862
+ }, width = 300 * getDevicePixelRatio({ maxDpr: 2 });
2863
+ if (saveThumbnailError)
2864
+ throw saveThumbnailError;
2865
+ return /* @__PURE__ */ jsxRuntime.jsx(
2866
+ ui.Dialog,
2867
+ {
2868
+ id: dialogId,
2869
+ header: "Edit thumbnail",
2870
+ onClose: () => setDialogState(!1),
2871
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
2872
+ ui.Button,
2873
+ {
2874
+ disabled: inputError !== "",
2875
+ mode: "ghost",
2876
+ tone: "primary",
2877
+ loading: saving,
2878
+ onClick: handleSave,
2879
+ text: "Set new thumbnail"
2880
+ },
2881
+ "thumbnail"
2882
+ ) }),
2883
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, padding: 3, children: [
1452
2884
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1453
2885
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, weight: "semibold", children: "Current:" }),
1454
2886
  /* @__PURE__ */ jsxRuntime.jsx(VideoThumbnail, { asset, width, staticImage: !0 })
@@ -1500,24 +2932,56 @@ function VideoPlayer({
1500
2932
  hlsConfig,
1501
2933
  ...props
1502
2934
  }) {
1503
- const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), {
1504
- src: videoSrc,
1505
- thumbnail: thumbnailSrc,
1506
- error
1507
- } = React.useMemo(() => {
2935
+ const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = React.useRef(null), [error, setError] = React.useState(), playbackId = React.useMemo(() => {
1508
2936
  try {
1509
- const thumbnail = getPosterSrc({ asset, client, width: thumbnailWidth }), src = asset?.playbackId && getVideoSrc({ client, asset });
1510
- return src ? { src, thumbnail } : { error: new TypeError("Asset has no playback ID") };
1511
- } catch (error2) {
1512
- return { error: error2 };
2937
+ return getPlaybackId(asset, ["public", "signed", "drm"]);
2938
+ } catch {
2939
+ setError(new TypeError("Asset has no playback ID"));
2940
+ return;
2941
+ }
2942
+ }, [asset]), muxPlaybackId2 = React.useMemo(() => {
2943
+ if (playbackId)
2944
+ return getPlaybackPolicyById(asset, playbackId);
2945
+ }, [asset, playbackId]), src = React.useMemo(() => {
2946
+ if (playbackId && muxPlaybackId2)
2947
+ return tryWithSuspend(
2948
+ () => getVideoSrc({ muxPlaybackId: muxPlaybackId2, client }),
2949
+ (e) => {
2950
+ setError(e);
2951
+ }
2952
+ );
2953
+ }, [muxPlaybackId2, playbackId, client]), poster = React.useMemo(() => tryWithSuspend(
2954
+ () => getPosterSrc({ asset, client, width: thumbnailWidth }),
2955
+ (e) => {
2956
+ setError(e);
1513
2957
  }
1514
- }, [asset, client, thumbnailWidth]), signedToken = React.useMemo(() => {
2958
+ ), [asset, client, thumbnailWidth]), signedToken = React.useMemo(() => {
1515
2959
  try {
1516
- return new URL(videoSrc).searchParams.get("token");
2960
+ return new URL(src).searchParams.get("token");
1517
2961
  } catch {
1518
- return !1;
2962
+ return;
1519
2963
  }
1520
- }, [videoSrc]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
2964
+ }, [src]), drmToken = React.useMemo(() => {
2965
+ if (playbackId && muxPlaybackId2?.policy === "drm")
2966
+ return tryWithSuspend(
2967
+ () => generateJwt(client, playbackId, "d"),
2968
+ (e) => {
2969
+ setError(e);
2970
+ }
2971
+ );
2972
+ }, [client, muxPlaybackId2?.policy, playbackId]), tokens = React.useMemo(() => {
2973
+ try {
2974
+ const partialTokens = {
2975
+ playback: void 0,
2976
+ thumbnail: void 0,
2977
+ storyboard: void 0,
2978
+ drm: void 0
2979
+ };
2980
+ return signedToken && (partialTokens.playback = signedToken, partialTokens.thumbnail = signedToken, partialTokens.storyboard = signedToken), drmToken && (partialTokens.drm = drmToken), { ...partialTokens };
2981
+ } catch {
2982
+ return;
2983
+ }
2984
+ }, [signedToken, drmToken]), [width, height] = (asset?.data?.aspect_ratio ?? "16:9").split(":").map(Number), targetAspectRatio = props.forceAspectRatio || (Number.isNaN(width) ? 16 / 9 : width / height);
1521
2985
  let aspectRatio = Math.max(MIN_ASPECT_RATIO, targetAspectRatio);
1522
2986
  return isAudio && (aspectRatio = props.forceAspectRatio ? (
1523
2987
  // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
@@ -1533,7 +2997,7 @@ function VideoPlayer({
1533
2997
  ...isAudio && { display: "flex", alignItems: "flex-end" }
1534
2998
  },
1535
2999
  children: [
1536
- videoSrc && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3000
+ src && poster && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1537
3001
  isAudio && /* @__PURE__ */ jsxRuntime.jsx(
1538
3002
  AudioIcon,
1539
3003
  {
@@ -1548,34 +3012,36 @@ function VideoPlayer({
1548
3012
  }
1549
3013
  }
1550
3014
  ),
1551
- /* @__PURE__ */ jsxRuntime.jsx(
1552
- MuxPlayer__default.default,
1553
- {
1554
- poster: isAudio ? void 0 : thumbnailSrc,
1555
- ref: muxPlayer,
1556
- ...props,
1557
- playsInline: !0,
1558
- playbackId: asset.playbackId,
1559
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
1560
- preload: "metadata",
1561
- crossOrigin: "anonymous",
1562
- metadata: {
1563
- player_name: "Sanity Admin Dashboard",
1564
- player_version: "2.13.0",
1565
- page_type: "Preview Player"
1566
- },
1567
- audio: isAudio,
1568
- _hlsConfig: hlsConfig,
1569
- style: {
1570
- ...!isAudio && { height: "100%" },
1571
- width: "100%",
1572
- display: "block",
1573
- objectFit: "contain",
1574
- ...isAudio && { alignSelf: "end" }
3015
+ /* @__PURE__ */ jsxRuntime.jsxs(React.Suspense, { fallback: null, children: [
3016
+ /* @__PURE__ */ jsxRuntime.jsx(
3017
+ MuxPlayer__default.default,
3018
+ {
3019
+ poster: isAudio ? void 0 : poster,
3020
+ ref: muxPlayer,
3021
+ ...props,
3022
+ playsInline: !0,
3023
+ playbackId,
3024
+ tokens,
3025
+ preload: "metadata",
3026
+ crossOrigin: "anonymous",
3027
+ metadata: {
3028
+ player_name: "Sanity Admin Dashboard",
3029
+ player_version: "2.15.0",
3030
+ page_type: "Preview Player"
3031
+ },
3032
+ audio: isAudio,
3033
+ _hlsConfig: hlsConfig,
3034
+ style: {
3035
+ ...!isAudio && { height: "100%" },
3036
+ width: "100%",
3037
+ display: "block",
3038
+ objectFit: "contain",
3039
+ ...isAudio && { alignSelf: "end" }
3040
+ }
1575
3041
  }
1576
- }
1577
- ),
1578
- children
3042
+ ),
3043
+ children
3044
+ ] })
1579
3045
  ] }),
1580
3046
  error ? /* @__PURE__ */ jsxRuntime.jsx(
1581
3047
  "div",
@@ -1596,7 +3062,8 @@ function VideoPlayer({
1596
3062
  ]
1597
3063
  }
1598
3064
  ),
1599
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
3065
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsxRuntime.jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime }),
3066
+ dialogState === "edit-captions" && /* @__PURE__ */ jsxRuntime.jsx(CaptionsDialog, { asset })
1600
3067
  ] });
1601
3068
  }
1602
3069
  function assetIsAudio(asset) {
@@ -1866,9 +3333,11 @@ function getVideoMetadata(doc) {
1866
3333
  playbackId: doc.playbackId,
1867
3334
  createdAt: date,
1868
3335
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
3336
+ playback_ids: doc.data?.playback_ids,
1869
3337
  aspect_ratio: doc.data?.aspect_ratio,
1870
3338
  max_stored_resolution: doc.data?.max_stored_resolution,
1871
- max_stored_frame_rate: doc.data?.max_stored_frame_rate
3339
+ max_stored_frame_rate: doc.data?.max_stored_frame_rate,
3340
+ text_tracks: doc.data?.tracks?.filter((track) => track.type === "text") || []
1872
3341
  };
1873
3342
  }
1874
3343
  function useVideoDetails(props) {
@@ -2060,7 +3529,20 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
2060
3529
  minHeight: containerHeight
2061
3530
  } : void 0,
2062
3531
  children: [
2063
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
3532
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
3533
+ /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }),
3534
+ tab === "details" && /* @__PURE__ */ jsxRuntime.jsx(
3535
+ TextTracksManager,
3536
+ {
3537
+ asset: props.asset,
3538
+ iconOnly: !0,
3539
+ collapseTracks: !0,
3540
+ tracks: displayInfo?.text_tracks || props.asset.data?.tracks?.filter(
3541
+ (track) => track.type === "text"
3542
+ ) || []
3543
+ }
3544
+ )
3545
+ ] }),
2064
3546
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, flex: 1, sizing: "border", children: [
2065
3547
  /* @__PURE__ */ jsxRuntime.jsxs(ui.TabList, { space: 2, children: [
2066
3548
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2154,14 +3636,7 @@ const AssetInput = (props) => /* @__PURE__ */ jsxRuntime.jsx(FormField$1, { titl
2154
3636
  ),
2155
3637
  /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: `Mux ID:
2156
3638
  ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
2157
- displayInfo?.playbackId && /* @__PURE__ */ jsxRuntime.jsx(
2158
- IconInfo,
2159
- {
2160
- text: `Playback ID: ${displayInfo.playbackId}`,
2161
- icon: icons.TagIcon,
2162
- size: 2
2163
- }
2164
- )
3639
+ /* @__PURE__ */ jsxRuntime.jsx(PlaybackIds, { playback_ids: displayInfo.playback_ids })
2165
3640
  ] })
2166
3641
  ] })
2167
3642
  }
@@ -2184,6 +3659,25 @@ ${displayInfo.id}`, icon: icons.TagIcon, size: 2 }),
2184
3659
  ]
2185
3660
  }
2186
3661
  );
3662
+ }, PlaybackIds = ({ playback_ids }) => playback_ids ? playback_ids.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(
3663
+ IconInfo,
3664
+ {
3665
+ text: `Playback ID [${policyToText(entry.policy)}]: ${entry.id}`,
3666
+ icon: icons.TagIcon,
3667
+ size: 2
3668
+ },
3669
+ entry.id
3670
+ )) : /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { text: "No Playback ID", icon: icons.TagIcon, size: 2 }), policyToText = (policy) => {
3671
+ switch (policy) {
3672
+ case "drm":
3673
+ return "DRM";
3674
+ case "signed":
3675
+ return "Signed";
3676
+ case "public":
3677
+ return "Public";
3678
+ default:
3679
+ return policy;
3680
+ }
2187
3681
  }, VideoMetadata = (props) => {
2188
3682
  if (!props.asset)
2189
3683
  return null;
@@ -2277,10 +3771,12 @@ function VideoInBrowser({
2277
3771
  onEdit,
2278
3772
  asset
2279
3773
  }) {
2280
- const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]);
3774
+ const [renderVideo, setRenderVideo] = React.useState(!1), select = React__default.default.useCallback(() => onSelect?.(asset), [onSelect, asset]), edit = React__default.default.useCallback(() => onEdit?.(asset), [onEdit, asset]), { hasShownWarning } = useDrmPlaybackWarningContext();
2281
3775
  if (!asset)
2282
3776
  return null;
2283
- const playbackPolicy = getPlaybackPolicy(asset);
3777
+ const playbackPolicy = getPlaybackPolicy(asset), onClickPlay = () => {
3778
+ playbackPolicy?.policy === "drm" && !hasShownWarning ? setRenderVideo("pre-render-warn") : setRenderVideo("render-video");
3779
+ };
2284
3780
  return /* @__PURE__ */ jsxRuntime.jsxs(
2285
3781
  ui.Card,
2286
3782
  {
@@ -2292,7 +3788,7 @@ function VideoInBrowser({
2292
3788
  position: "relative"
2293
3789
  },
2294
3790
  children: [
2295
- playbackPolicy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
3791
+ playbackPolicy?.policy === "signed" && /* @__PURE__ */ jsxRuntime.jsx(
2296
3792
  ui.Tooltip,
2297
3793
  {
2298
3794
  animate: !0,
@@ -2309,7 +3805,7 @@ function VideoInBrowser({
2309
3805
  position: "absolute",
2310
3806
  left: "1em",
2311
3807
  top: "1em",
2312
- zIndex: 10
3808
+ zIndex: 11
2313
3809
  },
2314
3810
  padding: 2,
2315
3811
  border: !0,
@@ -2318,6 +3814,32 @@ function VideoInBrowser({
2318
3814
  )
2319
3815
  }
2320
3816
  ),
3817
+ playbackPolicy?.policy === "drm" && /* @__PURE__ */ jsxRuntime.jsx(
3818
+ ui.Tooltip,
3819
+ {
3820
+ animate: !0,
3821
+ content: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, radius: 2, children: /* @__PURE__ */ jsxRuntime.jsx(IconInfo, { icon: icons.LockIcon, text: "DRM playback policy", size: 2 }) }),
3822
+ placement: "right",
3823
+ fallbackPlacements: ["top", "bottom"],
3824
+ portal: !0,
3825
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3826
+ ui.Card,
3827
+ {
3828
+ tone: "caution",
3829
+ style: {
3830
+ borderRadius: "0.25rem",
3831
+ position: "absolute",
3832
+ left: "1em",
3833
+ top: "1em",
3834
+ zIndex: 11
3835
+ },
3836
+ padding: 2,
3837
+ border: !0,
3838
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, weight: "semibold", style: { color: "var(--card-icon-color)" }, children: "DRM" })
3839
+ }
3840
+ )
3841
+ }
3842
+ ),
2321
3843
  /* @__PURE__ */ jsxRuntime.jsxs(
2322
3844
  ui.Stack,
2323
3845
  {
@@ -2327,7 +3849,15 @@ function VideoInBrowser({
2327
3849
  gridTemplateRows: "min-content min-content 1fr"
2328
3850
  },
2329
3851
  children: [
2330
- renderVideo ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: () => setRenderVideo(!0), children: [
3852
+ renderVideo === "pre-render-warn" && /* @__PURE__ */ jsxRuntime.jsx(
3853
+ DRMWarningDialog,
3854
+ {
3855
+ onClose: () => {
3856
+ setRenderVideo("render-video");
3857
+ }
3858
+ }
3859
+ ),
3860
+ renderVideo === "render-video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { asset, autoPlay: !0, forceAspectRatio: THUMBNAIL_ASPECT_RATIO }) : /* @__PURE__ */ jsxRuntime.jsxs(PlayButton, { onClick: onClickPlay, children: [
2331
3861
  /* @__PURE__ */ jsxRuntime.jsx("div", { "data-play": !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.PlayIcon, {}) }),
2332
3862
  assetIsAudio(asset) ? /* @__PURE__ */ jsxRuntime.jsx(
2333
3863
  "div",
@@ -2389,12 +3919,12 @@ function VideoInBrowser({
2389
3919
  }
2390
3920
  );
2391
3921
  }
2392
- function VideosBrowser({ onSelect }) {
2393
- const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
3922
+ function VideosBrowser({ onSelect, config }) {
3923
+ const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [page, setPage] = React.useState(0), pageLimit = 20, pageTotal = Math.floor(assets.length / pageLimit) + 1, [editedAsset, setEditedAsset] = React.useState(null), freshEditedAsset = React.useMemo(
2394
3924
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2395
3925
  [editedAsset, assets]
2396
- );
2397
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3926
+ ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
3927
+ return /* @__PURE__ */ jsxRuntime.jsxs(DrmPlaybackWarningContextProvider, { config, children: [
2398
3928
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2399
3929
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { justify: "space-between", align: "center", children: [
2400
3930
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
@@ -2407,7 +3937,8 @@ function VideosBrowser({ onSelect }) {
2407
3937
  placeholder: "Search videos"
2408
3938
  }
2409
3939
  ),
2410
- /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort })
3940
+ /* @__PURE__ */ jsxRuntime.jsx(SelectSortOptions, { setSort, sort }),
3941
+ /* @__PURE__ */ jsxRuntime.jsx(PageSelector, { page, setPage, total: pageTotal })
2411
3942
  ] }),
2412
3943
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Inline, { space: 2, children: [
2413
3944
  /* @__PURE__ */ jsxRuntime.jsx(ImportVideosFromMux, {}),
@@ -2430,7 +3961,7 @@ function VideosBrowser({ onSelect }) {
2430
3961
  style: {
2431
3962
  gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2432
3963
  },
2433
- children: assets.map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
3964
+ children: assets.slice(pageStart, pageEnd).map((asset) => /* @__PURE__ */ jsxRuntime.jsx(
2434
3965
  VideoInBrowser,
2435
3966
  {
2436
3967
  asset,
@@ -2448,7 +3979,7 @@ function VideosBrowser({ onSelect }) {
2448
3979
  freshEditedAsset && /* @__PURE__ */ jsxRuntime.jsx(VideoDetails, { closeDialog: () => setEditedAsset(null), asset: freshEditedAsset })
2449
3980
  ] });
2450
3981
  }
2451
- const StudioTool = () => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, {}), DEFAULT_TOOL_CONFIG = {
3982
+ const StudioTool = (config) => /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { config }), DEFAULT_TOOL_CONFIG = {
2452
3983
  icon: ToolIcon,
2453
3984
  title: "Videos"
2454
3985
  };
@@ -2806,6 +4337,9 @@ function isValidUrl(url) {
2806
4337
  return !1;
2807
4338
  }
2808
4339
  }
4340
+ function isServerError(error) {
4341
+ return "statusCode" in error && typeof error.statusCode == "number" && 500 <= error.statusCode && error.statusCode <= 600;
4342
+ }
2809
4343
  function extractDroppedFiles(dataTransfer) {
2810
4344
  const files = Array.from(dataTransfer.files || []), items = Array.from(dataTransfer.items || []);
2811
4345
  return files && files.length > 0 ? Promise.resolve(files) : normalizeItems(items).then((arr) => arr.flat());
@@ -2847,7 +4381,12 @@ function walk(entry) {
2847
4381
  }
2848
4382
  return Promise.resolve([]);
2849
4383
  }
2850
- function SelectAssets({ asset: selectedAsset, onChange, setDialogState }) {
4384
+ function SelectAssets({
4385
+ asset: selectedAsset,
4386
+ onChange,
4387
+ setDialogState,
4388
+ config
4389
+ }) {
2851
4390
  const handleSelect = React.useCallback(
2852
4391
  (chosenAsset) => {
2853
4392
  chosenAsset?._id || onChange(sanity.PatchEvent.from([sanity.unset(["asset"])])), chosenAsset._id !== selectedAsset?._id && onChange(
@@ -2859,7 +4398,7 @@ function SelectAssets({ asset: selectedAsset, onChange, setDialogState }) {
2859
4398
  },
2860
4399
  [onChange, setDialogState, selectedAsset]
2861
4400
  );
2862
- return /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { onSelect: handleSelect });
4401
+ return /* @__PURE__ */ jsxRuntime.jsx(VideosBrowser, { onSelect: handleSelect, config });
2863
4402
  }
2864
4403
  const StyledDialog = styledComponents.styled(ui.Dialog)`
2865
4404
  > div[data-ui='DialogCard'] > div[data-ui='Card'] {
@@ -2869,7 +4408,8 @@ const StyledDialog = styledComponents.styled(ui.Dialog)`
2869
4408
  function InputBrowser({
2870
4409
  setDialogState,
2871
4410
  asset,
2872
- onChange
4411
+ onChange,
4412
+ config
2873
4413
  }) {
2874
4414
  const id = `InputBrowser${React.useId()}`, handleClose = React.useCallback(() => setDialogState(!1), [setDialogState]);
2875
4415
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2880,7 +4420,15 @@ function InputBrowser({
2880
4420
  id,
2881
4421
  onClose: handleClose,
2882
4422
  width: 2,
2883
- children: /* @__PURE__ */ jsxRuntime.jsx(SelectAssets, { asset, onChange, setDialogState })
4423
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4424
+ SelectAssets,
4425
+ {
4426
+ config,
4427
+ asset,
4428
+ onChange,
4429
+ setDialogState
4430
+ }
4431
+ )
2884
4432
  }
2885
4433
  );
2886
4434
  }
@@ -3096,7 +4644,7 @@ const FileButton = styledComponents.styled(ui.MenuItem)(({ theme }) => {
3096
4644
  color: white;
3097
4645
  `, isVideoAsset = (asset) => asset._type === "mux.videoAsset";
3098
4646
  function PlayerActionsMenu(props) {
3099
- const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
4647
+ const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = React.useState(!1), [menuElement, setMenuRef] = React.useState(null), isSigned = React.useMemo(() => getPlaybackPolicy(asset)?.policy === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = React.useCallback(() => onChange(sanity.PatchEvent.from(sanity.unset([]))), [onChange]);
3100
4648
  return React.useEffect(() => {
3101
4649
  open && dialogState && setOpen(!1);
3102
4650
  }, [dialogState, open]), ui.useClickOutsideEvent(
@@ -3138,14 +4686,24 @@ function PlayerActionsMenu(props) {
3138
4686
  onClick: () => setDialogState("select-video")
3139
4687
  }
3140
4688
  ),
3141
- isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsx(
3142
- ui.MenuItem,
3143
- {
3144
- icon: icons.ImageIcon,
3145
- text: "Thumbnail",
3146
- onClick: () => setDialogState("edit-thumbnail")
3147
- }
3148
- ),
4689
+ isVideoAsset(asset) && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4690
+ /* @__PURE__ */ jsxRuntime.jsx(
4691
+ ui.MenuItem,
4692
+ {
4693
+ icon: icons.ImageIcon,
4694
+ text: "Thumbnail",
4695
+ onClick: () => setDialogState("edit-thumbnail")
4696
+ }
4697
+ ),
4698
+ /* @__PURE__ */ jsxRuntime.jsx(
4699
+ ui.MenuItem,
4700
+ {
4701
+ icon: icons.TranslateIcon,
4702
+ text: "Captions",
4703
+ onClick: () => setDialogState("edit-captions")
4704
+ }
4705
+ )
4706
+ ] }),
3149
4707
  /* @__PURE__ */ jsxRuntime.jsx(ui.MenuDivider, {}),
3150
4708
  hasConfigAccess && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3151
4709
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3187,47 +4745,90 @@ function PlayerActionsMenu(props) {
3187
4745
  ] });
3188
4746
  }
3189
4747
  var PlayerActionsMenu$1 = React.memo(PlayerActionsMenu);
3190
- function formatBytes(bytes, si = !1, dp = 1) {
3191
- const thresh = si ? 1e3 : 1024;
3192
- if (Math.abs(bytes) < thresh)
3193
- return bytes + " B";
3194
- const units = si ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
3195
- let u2 = -1;
3196
- const r = 10 ** dp;
3197
- do
3198
- bytes /= thresh, ++u2;
3199
- while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
3200
- return bytes.toFixed(dp) + " " + units[u2];
4748
+ function useFetchFileSize(stagedUpload, maxFileSize) {
4749
+ const [fileSize, setFileSize] = React.useState(null), [isLoadingFileSize, setIsLoadingFileSize] = React.useState(!1), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = React.useState(!1);
4750
+ return React.useEffect(() => {
4751
+ if (stagedUpload.type === "url") {
4752
+ setIsLoadingFileSize(!1), setCanSkipFileSizeValidation(!1), setFileSize(null);
4753
+ const url = stagedUpload.url;
4754
+ (async () => {
4755
+ setIsLoadingFileSize(!0);
4756
+ try {
4757
+ const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), newFileSize = contentLength ? parseInt(contentLength, 10) : null;
4758
+ setIsLoadingFileSize(!1), newFileSize && setFileSize(newFileSize), newFileSize === null && maxFileSize !== void 0 && setCanSkipFileSizeValidation(!0);
4759
+ } catch {
4760
+ console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), setIsLoadingFileSize(!1);
4761
+ }
4762
+ })();
4763
+ }
4764
+ stagedUpload.type === "file" && setFileSize(stagedUpload.files[0].size);
4765
+ }, [maxFileSize, stagedUpload, stagedUpload.type]), {
4766
+ fileSize,
4767
+ isLoadingFileSize,
4768
+ canSkipFileSizeValidation
4769
+ };
3201
4770
  }
3202
- const SUPPORTED_MUX_LANGUAGES = [
3203
- { label: "English", code: "en", state: "Stable" },
3204
- { label: "Spanish", code: "es", state: "Stable" },
3205
- { label: "Italian", code: "it", state: "Stable" },
3206
- { label: "Portuguese", code: "pt", state: "Stable" },
3207
- { label: "German", code: "de", state: "Stable" },
3208
- { label: "French", code: "fr", state: "Stable" },
3209
- { label: "Polish", code: "pl", state: "Beta" },
3210
- { label: "Russian", code: "ru", state: "Beta" },
3211
- { label: "Dutch", code: "nl", state: "Beta" },
3212
- { label: "Catalan", code: "ca", state: "Beta" },
3213
- { label: "Turkish", code: "tr", state: "Beta" },
3214
- { label: "Swedish", code: "sv", state: "Beta" },
3215
- { label: "Ukrainian", code: "uk", state: "Beta" },
3216
- { label: "Norwegian", code: "no", state: "Beta" },
3217
- { label: "Finnish", code: "fi", state: "Beta" },
3218
- { label: "Slovak", code: "sk", state: "Beta" },
3219
- { label: "Greek", code: "el", state: "Beta" },
3220
- { label: "Czech", code: "cs", state: "Beta" },
3221
- { label: "Croatian", code: "hr", state: "Beta" },
3222
- { label: "Danish", code: "da", state: "Beta" },
3223
- { label: "Romanian", code: "ro", state: "Beta" },
3224
- { label: "Bulgarian", code: "bg", state: "Beta" }
3225
- ];
3226
- function isCustomTextTrack(track) {
3227
- return track.type !== "autogenerated";
4771
+ function useMediaMetadata(stagedUpload) {
4772
+ const [videoAssetMetadata, setVideoAssetMetadata] = React.useState(null), [isLoadingMetadata, setIsLoadingMetadata] = React.useState(!1);
4773
+ return React.useEffect(() => {
4774
+ let videoSrc = null;
4775
+ if (stagedUpload.type === "file") {
4776
+ const file = stagedUpload.files[0];
4777
+ videoSrc = URL.createObjectURL(file);
4778
+ }
4779
+ if (stagedUpload.type === "url" && (videoSrc = stagedUpload.url), setVideoAssetMetadata((old) => ({
4780
+ ...old,
4781
+ duration: void 0,
4782
+ width: void 0,
4783
+ height: void 0
4784
+ })), !videoSrc) return () => null;
4785
+ setIsLoadingMetadata(!0);
4786
+ const videoElement = document.createElement("video");
4787
+ videoElement.preload = "metadata";
4788
+ const metadataListeners = [
4789
+ () => {
4790
+ setIsLoadingMetadata(!1);
4791
+ },
4792
+ () => {
4793
+ const duration = videoElement.duration, width = videoElement.videoWidth, height = videoElement.videoHeight, isAudioOnly = width <= 0 && height <= 0;
4794
+ setVideoAssetMetadata((old) => ({
4795
+ ...old,
4796
+ duration,
4797
+ width,
4798
+ height,
4799
+ isAudioOnly
4800
+ }));
4801
+ }
4802
+ ], cleanupVideo = (videoEl) => {
4803
+ const currentVideoSrc = videoEl?.src;
4804
+ videoEl && (metadataListeners.forEach(
4805
+ (listener) => videoEl.removeEventListener("loadedmetadata", listener)
4806
+ ), videoEl.onerror = null, videoEl.src = "", videoEl.load()), currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc);
4807
+ };
4808
+ return metadataListeners.push(() => setTimeout(() => cleanupVideo(videoElement), 0)), videoElement.onerror = () => {
4809
+ setIsLoadingMetadata(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(videoElement);
4810
+ }, metadataListeners.forEach(
4811
+ (listener) => videoElement.addEventListener("loadedmetadata", listener)
4812
+ ), videoElement.src = videoSrc, () => {
4813
+ cleanupVideo(videoElement);
4814
+ };
4815
+ }, [stagedUpload.type, stagedUpload]), {
4816
+ videoAssetMetadata,
4817
+ setVideoAssetMetadata,
4818
+ isLoadingMetadata
4819
+ };
3228
4820
  }
3229
- function isAutogeneratedTrack(track) {
3230
- return track.type === "autogenerated";
4821
+ function formatBytes(bytes, si = !1, dp = 1) {
4822
+ const thresh = si ? 1e3 : 1024;
4823
+ if (Math.abs(bytes) < thresh)
4824
+ return bytes + " B";
4825
+ const units = si ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
4826
+ let u2 = -1;
4827
+ const r = 10 ** dp;
4828
+ do
4829
+ bytes /= thresh, ++u2;
4830
+ while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
4831
+ return bytes.toFixed(dp) + " " + units[u2];
3231
4832
  }
3232
4833
  const ALL_LANGUAGE_CODES = LanguagesList__default.default.getAllCodes().map((code) => ({
3233
4834
  value: code,
@@ -3306,13 +4907,14 @@ function PlaybackPolicyOption({
3306
4907
  optionName,
3307
4908
  description,
3308
4909
  dispatch,
3309
- action
4910
+ action,
4911
+ disabled
3310
4912
  }) {
3311
4913
  const [scale, setScale] = React.useState(1), boxStyle = {
3312
4914
  outline: "0.01rem solid grey",
3313
4915
  transform: `scale(${scale})`,
3314
4916
  transition: "transform 0.1s ease-in-out",
3315
- cursor: "pointer",
4917
+ cursor: disabled ? "not-allowed" : "pointer",
3316
4918
  borderRadius: "0.25rem"
3317
4919
  }, triggerAnimation = () => {
3318
4920
  setScale(0.98), setTimeout(() => {
@@ -3320,15 +4922,24 @@ function PlaybackPolicyOption({
3320
4922
  }, 100);
3321
4923
  };
3322
4924
  return /* @__PURE__ */ jsxRuntime.jsx("label", { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, padding: 3, style: boxStyle, children: [
3323
- /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { id, required: !0, checked, onChange: () => {
3324
- triggerAnimation(), dispatch({
3325
- action,
3326
- value: !checked
3327
- });
3328
- } }),
4925
+ /* @__PURE__ */ jsxRuntime.jsx(
4926
+ ui.Checkbox,
4927
+ {
4928
+ id,
4929
+ required: !0,
4930
+ checked,
4931
+ onChange: () => {
4932
+ action && (triggerAnimation(), dispatch({
4933
+ action,
4934
+ value: !checked
4935
+ }));
4936
+ },
4937
+ disabled
4938
+ }
4939
+ ),
3329
4940
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { gap: 3, children: [
3330
4941
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 3, weight: "bold", children: optionName }),
3331
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: description })
4942
+ typeof description == "string" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: description }) : description
3332
4943
  ] })
3333
4944
  ] }) });
3334
4945
  }
@@ -3353,7 +4964,7 @@ function PlaybackPolicy({
3353
4964
  secrets,
3354
4965
  dispatch
3355
4966
  }) {
3356
- const noPolicySelected = !(config.public_policy || config.signed_policy);
4967
+ const noPolicySelected = !(config.public_policy || config.signed_policy || config.drm_policy), drmPolicyDisabled = !secrets.drmConfigId;
3357
4968
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { gap: 3, children: [
3358
4969
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "bold", children: "Advanced Playback Policies" }),
3359
4970
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3362,7 +4973,10 @@ function PlaybackPolicy({
3362
4973
  id: `${id}--public`,
3363
4974
  checked: config.public_policy,
3364
4975
  optionName: "Public",
3365
- description: "Playback IDs are accessible by constructing an HLS URL like https://stream.mux.com/{PLAYBACK_ID}",
4976
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4977
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs are accessible by constructing an HLS URL like" }),
4978
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}" })
4979
+ ] }),
3366
4980
  dispatch,
3367
4981
  action: "public_policy"
3368
4982
  }
@@ -3373,24 +4987,146 @@ function PlaybackPolicy({
3373
4987
  id: `${id}--signed`,
3374
4988
  checked: config.signed_policy,
3375
4989
  optionName: "Signed",
3376
- description: `Playback IDs should be used with tokens https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}.
3377
- // See Secure video playback for details about creating tokens.`,
4990
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4991
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs should be used with tokens" }),
4992
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}" }),
4993
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
4994
+ "See",
4995
+ " ",
4996
+ /* @__PURE__ */ jsxRuntime.jsx(
4997
+ "a",
4998
+ {
4999
+ href: "https://www.mux.com/docs/guides/secure-video-playback",
5000
+ target: "_blank",
5001
+ rel: "noopener noreferrer",
5002
+ children: "Secure video playback"
5003
+ }
5004
+ ),
5005
+ " ",
5006
+ "for details about creating tokens."
5007
+ ] })
5008
+ ] }),
3378
5009
  dispatch,
3379
5010
  action: "signed_policy"
3380
5011
  }
3381
5012
  ),
5013
+ drmPolicyDisabled ? /* @__PURE__ */ jsxRuntime.jsx(
5014
+ PlaybackPolicyOption,
5015
+ {
5016
+ id: `${id}--drm`,
5017
+ checked: !1,
5018
+ optionName: "DRM - Disabled",
5019
+ description: /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
5020
+ "To enable DRM add your DRM Configuration Id to your plugin configuration in the API Credentials view.",
5021
+ " ",
5022
+ /* @__PURE__ */ jsxRuntime.jsx(
5023
+ "a",
5024
+ {
5025
+ href: "https://www.mux.com/support/human",
5026
+ target: "_blank",
5027
+ rel: "noopener noreferrer",
5028
+ children: "Contact us"
5029
+ }
5030
+ ),
5031
+ " ",
5032
+ "to get started using DRM."
5033
+ ] }) }),
5034
+ dispatch,
5035
+ disabled: !0
5036
+ }
5037
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
5038
+ PlaybackPolicyOption,
5039
+ {
5040
+ id: `${id}--drm`,
5041
+ checked: config.drm_policy,
5042
+ optionName: "DRM",
5043
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5044
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, muted: !0, children: "Playback IDs should be used with tokens as with Signed playback, but require extra configuration." }),
5045
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { children: "https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}" }),
5046
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 2, muted: !0, children: [
5047
+ "See",
5048
+ " ",
5049
+ /* @__PURE__ */ jsxRuntime.jsx(
5050
+ "a",
5051
+ {
5052
+ href: "https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos",
5053
+ target: "_blank",
5054
+ rel: "noopener noreferrer",
5055
+ children: "Protect videos with DRM"
5056
+ }
5057
+ ),
5058
+ " ",
5059
+ "for details about configuring your player for DRM playback and",
5060
+ " ",
5061
+ /* @__PURE__ */ jsxRuntime.jsx(
5062
+ "a",
5063
+ {
5064
+ href: "https://www.mux.com/docs/guides/secure-video-playback",
5065
+ target: "_blank",
5066
+ rel: "noopener noreferrer",
5067
+ children: "Secure video playback"
5068
+ }
5069
+ ),
5070
+ " ",
5071
+ "for details about creating tokens."
5072
+ ] })
5073
+ ] }),
5074
+ dispatch,
5075
+ action: "drm_policy"
5076
+ }
5077
+ ),
3382
5078
  noPolicySelected && /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicyWarning, {})
3383
5079
  ] });
3384
5080
  }
3385
- const VIDEO_QUALITY_LEVELS = [
3386
- { value: "basic", label: "Basic" },
3387
- { value: "plus", label: "Plus" },
3388
- { value: "premium", label: "Premium" }
3389
- ], RESOLUTION_TIERS = [
5081
+ const RESOLUTION_TIERS = [
3390
5082
  { value: "1080p", label: "1080p" },
3391
5083
  { value: "1440p", label: "1440p (2k)" },
3392
5084
  { value: "2160p", label: "2160p (4k)" }
3393
- ], ADVANCED_RESOLUTIONS = [
5085
+ ], ResolutionTierSelector = ({
5086
+ id,
5087
+ config,
5088
+ dispatch,
5089
+ maxSupportedResolution
5090
+ }) => /* @__PURE__ */ jsxRuntime.jsx(
5091
+ sanity.FormField,
5092
+ {
5093
+ title: "Resolution Tier",
5094
+ description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5095
+ "The maximum",
5096
+ " ",
5097
+ /* @__PURE__ */ jsxRuntime.jsx(
5098
+ "a",
5099
+ {
5100
+ href: "https://docs.mux.com/api-reference#video/operation/create-direct-upload",
5101
+ target: "_blank",
5102
+ rel: "noopener noreferrer",
5103
+ children: "resolution_tier"
5104
+ }
5105
+ ),
5106
+ " ",
5107
+ "your asset is encoded, stored, and streamed at."
5108
+ ] }),
5109
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, wrap: "wrap", children: RESOLUTION_TIERS.map(({ value, label }, index) => {
5110
+ const inputId = `${id}--type-${value}`;
5111
+ return index > maxSupportedResolution ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5112
+ /* @__PURE__ */ jsxRuntime.jsx(
5113
+ ui.Radio,
5114
+ {
5115
+ checked: config.max_resolution_tier === value,
5116
+ name: "asset-resolutiontier",
5117
+ onChange: (e) => dispatch({
5118
+ action: "max_resolution_tier",
5119
+ value: e.currentTarget.value
5120
+ }),
5121
+ value,
5122
+ id: inputId
5123
+ }
5124
+ ),
5125
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, children: label })
5126
+ ] }, value);
5127
+ }) })
5128
+ }
5129
+ ), ADVANCED_RESOLUTIONS = [
3394
5130
  { value: "270p", label: "270p" },
3395
5131
  { value: "360p", label: "360p" },
3396
5132
  { value: "480p", label: "480p" },
@@ -3399,6 +5135,130 @@ const VIDEO_QUALITY_LEVELS = [
3399
5135
  { value: "1080p", label: "1080p" },
3400
5136
  { value: "1440p", label: "1440p" },
3401
5137
  { value: "2160p", label: "2160p" }
5138
+ ], StaticRenditionSelector = ({
5139
+ id,
5140
+ config,
5141
+ dispatch
5142
+ }) => {
5143
+ const isAdvancedMode = React.useMemo(() => config.static_renditions.filter(
5144
+ (r) => r !== "highest" && r !== "audio-only"
5145
+ ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = React.useState(
5146
+ isAdvancedMode ? "advanced" : "standard"
5147
+ ), toggleRendition = (rendition) => {
5148
+ const current = config.static_renditions, hasRendition = current.includes(rendition);
5149
+ dispatch(hasRendition ? {
5150
+ action: "static_renditions",
5151
+ value: current.filter((r) => r !== rendition)
5152
+ } : {
5153
+ action: "static_renditions",
5154
+ value: [...current, rendition]
5155
+ });
5156
+ }, handleModeChange = (mode) => {
5157
+ setRenditionMode(mode), dispatch(mode === "standard" ? {
5158
+ action: "static_renditions",
5159
+ value: config.static_renditions.filter((r) => r === "highest" || r === "audio-only")
5160
+ } : {
5161
+ action: "static_renditions",
5162
+ value: config.static_renditions.filter((r) => r !== "highest")
5163
+ });
5164
+ };
5165
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
5166
+ sanity.FormField,
5167
+ {
5168
+ title: "Static Renditions",
5169
+ description: "Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped.",
5170
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
5171
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, children: [
5172
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5173
+ /* @__PURE__ */ jsxRuntime.jsx(
5174
+ ui.Radio,
5175
+ {
5176
+ checked: renditionMode === "standard",
5177
+ name: "rendition-mode",
5178
+ onChange: () => handleModeChange("standard"),
5179
+ value: "standard",
5180
+ id: `${id}--mode-standard`
5181
+ }
5182
+ ),
5183
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-standard`, children: "Standard" })
5184
+ ] }),
5185
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5186
+ /* @__PURE__ */ jsxRuntime.jsx(
5187
+ ui.Radio,
5188
+ {
5189
+ checked: renditionMode === "advanced",
5190
+ name: "rendition-mode",
5191
+ onChange: () => handleModeChange("advanced"),
5192
+ value: "advanced",
5193
+ id: `${id}--mode-advanced`
5194
+ }
5195
+ ),
5196
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-advanced`, children: "Advanced" })
5197
+ ] })
5198
+ ] }),
5199
+ renditionMode === "standard" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5200
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5201
+ /* @__PURE__ */ jsxRuntime.jsx(
5202
+ ui.Checkbox,
5203
+ {
5204
+ id: `${id}--highest`,
5205
+ style: { display: "block" },
5206
+ checked: config.static_renditions.includes("highest"),
5207
+ onChange: () => toggleRendition("highest")
5208
+ }
5209
+ ),
5210
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--highest`, children: "Highest Resolution (up to 4K)" })
5211
+ ] }),
5212
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
5213
+ /* @__PURE__ */ jsxRuntime.jsx(
5214
+ ui.Checkbox,
5215
+ {
5216
+ id: `${id}--audio-only-standard`,
5217
+ style: { display: "block" },
5218
+ checked: config.static_renditions.includes("audio-only"),
5219
+ onChange: () => toggleRendition("audio-only")
5220
+ }
5221
+ ),
5222
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-standard`, children: "Audio Only (M4A)" })
5223
+ ] })
5224
+ ] }),
5225
+ renditionMode === "advanced" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
5226
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, muted: !0, children: "Select specific resolutions:" }),
5227
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: ADVANCED_RESOLUTIONS.map(({ value, label }) => {
5228
+ const inputId = `${id}--resolution-${value}`;
5229
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
5230
+ /* @__PURE__ */ jsxRuntime.jsx(
5231
+ ui.Checkbox,
5232
+ {
5233
+ id: inputId,
5234
+ style: { display: "block" },
5235
+ checked: config.static_renditions.includes(value),
5236
+ onChange: () => toggleRendition(value)
5237
+ }
5238
+ ),
5239
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, size: 1, children: label })
5240
+ ] }, value);
5241
+ }) }),
5242
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [2, 2, 0, 2], children: [
5243
+ /* @__PURE__ */ jsxRuntime.jsx(
5244
+ ui.Checkbox,
5245
+ {
5246
+ id: `${id}--audio-only-advanced`,
5247
+ style: { display: "block" },
5248
+ checked: config.static_renditions.includes("audio-only"),
5249
+ onChange: () => toggleRendition("audio-only")
5250
+ }
5251
+ ),
5252
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-advanced`, children: "Audio Only (M4A)" })
5253
+ ] })
5254
+ ] })
5255
+ ] })
5256
+ }
5257
+ ) });
5258
+ }, VIDEO_QUALITY_LEVELS = [
5259
+ { value: "basic", label: "Basic" },
5260
+ { value: "plus", label: "Plus" },
5261
+ { value: "premium", label: "Premium" }
3402
5262
  ];
3403
5263
  function sanitizeStaticRenditions(renditions) {
3404
5264
  const hasHighest = renditions.includes("highest"), hasSpecificResolutions = renditions.some((r) => r !== "highest" && r !== "audio-only");
@@ -3430,7 +5290,8 @@ function UploadConfiguration({
3430
5290
  max_resolution_tier: "1080p",
3431
5291
  text_tracks: prev.text_tracks?.filter(({ type }) => type !== "autogenerated"),
3432
5292
  public_policy: !0,
3433
- signed_policy: !1
5293
+ signed_policy: !1,
5294
+ drm_policy: !1
3434
5295
  }) : Object.assign({}, prev, {
3435
5296
  video_quality: action.value,
3436
5297
  static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
@@ -3444,6 +5305,8 @@ function UploadConfiguration({
3444
5305
  return Object.assign({}, prev, { [action.action]: action.value });
3445
5306
  case "public_policy":
3446
5307
  return Object.assign({}, prev, { [action.action]: action.value });
5308
+ case "drm_policy":
5309
+ return Object.assign({}, prev, { [action.action]: action.value });
3447
5310
  // Updating individual tracks
3448
5311
  case "track": {
3449
5312
  const text_tracks = [...prev.text_tracks], target_track_i = text_tracks.findIndex(({ _id: _id2 }) => _id2 === action.id);
@@ -3479,75 +5342,41 @@ function UploadConfiguration({
3479
5342
  static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
3480
5343
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
3481
5344
  public_policy: pluginConfig.defaultPublic,
5345
+ drm_policy: pluginConfig.defaultDrm && !!secrets.drmConfigId,
3482
5346
  normalize_audio: pluginConfig.normalize_audio,
3483
5347
  text_tracks: autoTextTracks
3484
5348
  }
3485
- ), isAdvancedMode = React.useMemo(() => config.static_renditions.filter(
3486
- (r) => r !== "highest" && r !== "audio-only"
3487
- ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = React.useState(
3488
- isAdvancedMode ? "advanced" : "standard"
3489
- ), [videoDuration, setVideoDuration] = React.useState(null), [urlFileSize, setUrlFileSize] = React.useState(null), [isLoadingDuration, setIsLoadingDuration] = React.useState(!1), [isLoadingFileSize, setIsLoadingFileSize] = React.useState(!1), [validationError, setValidationError] = React.useState(null), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = React.useState(!1), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration;
5349
+ ), [validationError, setValidationError] = React.useState(null), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration, { fileSize, isLoadingFileSize, canSkipFileSizeValidation } = useFetchFileSize(
5350
+ stagedUpload,
5351
+ MAX_FILE_SIZE
5352
+ ), { videoAssetMetadata, setVideoAssetMetadata, isLoadingMetadata } = useMediaMetadata(stagedUpload);
3490
5353
  React.useEffect(() => {
3491
- setVideoDuration(null), setUrlFileSize(null), setIsLoadingDuration(!1), setIsLoadingFileSize(!1), setValidationError(null), setCanSkipFileSizeValidation(!1);
3492
- let videoElement = null, currentVideoSrc = null;
3493
- const cleanupVideo = (shouldRevokeUrl) => {
3494
- videoElement && (videoElement.onloadedmetadata = null, videoElement.onerror = null, videoElement.src = "", videoElement.load(), videoElement = null), shouldRevokeUrl && currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc), currentVideoSrc = null;
3495
- }, validateDuration = (videoSrc, shouldRevokeUrl = !1) => {
3496
- !MAX_DURATION_SECONDS || MAX_DURATION_SECONDS <= 0 || (setIsLoadingDuration(!0), videoElement = document.createElement("video"), videoElement.preload = "metadata", currentVideoSrc = videoSrc, videoElement.onloadedmetadata = () => {
3497
- const duration = videoElement.duration;
3498
- setVideoDuration(duration), setIsLoadingDuration(!1), duration > MAX_DURATION_SECONDS && setValidationError(
3499
- `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
3500
- ), cleanupVideo(shouldRevokeUrl);
3501
- }, videoElement.onerror = () => {
3502
- setIsLoadingDuration(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(shouldRevokeUrl);
3503
- }, videoElement.src = videoSrc);
3504
- }, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
5354
+ fileSize && setVideoAssetMetadata((old) => ({ ...old, size: fileSize }));
5355
+ }, [fileSize, setVideoAssetMetadata]), React.useEffect(() => {
5356
+ const validateDuration = (duration) => MAX_DURATION_SECONDS && duration > MAX_DURATION_SECONDS ? (setValidationError(
5357
+ `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
5358
+ ), !1) : !0, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
3505
5359
  `File size (${formatBytes(size)}) exceeds maximum allowed size of ${formatBytes(MAX_FILE_SIZE)}`
3506
- ), !1);
3507
- if (stagedUpload.type === "file") {
3508
- const file = stagedUpload.files[0];
3509
- validateFileSize(file.size) && validateDuration(URL.createObjectURL(file), !0);
3510
- }
3511
- if (stagedUpload.type === "url") {
3512
- const url = stagedUpload.url;
3513
- (async () => {
3514
- setIsLoadingFileSize(!0);
3515
- try {
3516
- const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), fileSize = contentLength ? parseInt(contentLength, 10) : null;
3517
- setIsLoadingFileSize(!1), fileSize && setUrlFileSize(fileSize);
3518
- const shouldValidateDuration = MAX_FILE_SIZE === void 0 || fileSize === null || validateFileSize(fileSize);
3519
- fileSize === null && MAX_FILE_SIZE !== void 0 && setCanSkipFileSizeValidation(!0), shouldValidateDuration && validateDuration(url);
3520
- } catch {
3521
- setIsLoadingFileSize(!1), console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), validateDuration(url);
3522
- }
3523
- })();
3524
- }
3525
- return () => {
3526
- cleanupVideo(!0);
3527
- };
3528
- }, [stagedUpload, MAX_FILE_SIZE, MAX_DURATION_SECONDS]);
3529
- const toggleRendition = (rendition) => {
3530
- const current = config.static_renditions, hasRendition = current.includes(rendition);
3531
- dispatch(hasRendition ? {
3532
- action: "static_renditions",
3533
- value: current.filter((r) => r !== rendition)
3534
- } : {
3535
- action: "static_renditions",
3536
- value: [...current, rendition]
3537
- });
3538
- }, handleModeChange = (mode) => {
3539
- setRenditionMode(mode), dispatch(mode === "standard" ? {
3540
- action: "static_renditions",
3541
- value: config.static_renditions.filter((r) => r === "highest" || r === "audio-only")
3542
- } : {
3543
- action: "static_renditions",
3544
- value: config.static_renditions.filter((r) => r !== "highest")
3545
- });
3546
- }, { disableTextTrackConfig, disableUploadConfig } = pluginConfig, skipConfig = disableTextTrackConfig && disableUploadConfig;
5360
+ ), !1), validateDrmAvailability = (isAudioOnly) => config.drm_policy && isAudioOnly ? (setValidationError("Audio-only asset cannot be DRM protected"), !1) : !0;
5361
+ let valid = !0;
5362
+ 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);
5363
+ }, [
5364
+ MAX_FILE_SIZE,
5365
+ MAX_DURATION_SECONDS,
5366
+ canSkipFileSizeValidation,
5367
+ videoAssetMetadata?.duration,
5368
+ videoAssetMetadata?.size,
5369
+ videoAssetMetadata?.height,
5370
+ videoAssetMetadata?.width,
5371
+ videoAssetMetadata,
5372
+ config.drm_policy,
5373
+ validationError
5374
+ ]);
5375
+ const { disableTextTrackConfig, disableUploadConfig } = pluginConfig, skipConfig = disableTextTrackConfig && disableUploadConfig;
3547
5376
  if (React.useEffect(() => {
3548
- skipConfig && startUpload(formatUploadConfig(config));
5377
+ skipConfig && startUpload(formatUploadConfig(config, secrets));
3549
5378
  }, []), skipConfig) return null;
3550
- const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", maxSupportedResolution = RESOLUTION_TIERS.findIndex(
5379
+ const basicConfig = config.video_quality !== "plus" && config.video_quality !== "premium", playbackPolicySelected = config.public_policy || config.signed_policy || config.drm_policy, maxSupportedResolution = RESOLUTION_TIERS.findIndex(
3551
5380
  (rt) => rt.value === pluginConfig.max_resolution_tier
3552
5381
  );
3553
5382
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -3581,12 +5410,12 @@ function UploadConfiguration({
3581
5410
  /* @__PURE__ */ jsxRuntime.jsx(icons.DocumentVideoIcon, { fontSize: "2em" }),
3582
5411
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3583
5412
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { textOverflow: "ellipsis", as: "h2", size: 3, children: stagedUpload.type === "file" ? stagedUpload.files[0].name : stagedUpload.url }),
3584
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : urlFileSize ? `File From URL (${formatBytes(urlFileSize)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
5413
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : videoAssetMetadata?.size ? `File From URL (${formatBytes(videoAssetMetadata.size)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
3585
5414
  stagedUpload.type === "file" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 1, children: [
3586
- isLoadingDuration && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
3587
- videoDuration !== null && !validationError && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", size: 1, muted: !0, children: [
5415
+ isLoadingMetadata && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
5416
+ videoAssetMetadata?.duration && !validationError && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", size: 1, muted: !0, children: [
3588
5417
  "Duration: ",
3589
- formatSeconds(videoDuration)
5418
+ formatSeconds(videoAssetMetadata.duration)
3590
5419
  ] })
3591
5420
  ] })
3592
5421
  ] })
@@ -3632,160 +5461,37 @@ function UploadConfiguration({
3632
5461
  }) })
3633
5462
  }
3634
5463
  ),
3635
- !basicConfig && maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3636
- sanity.FormField,
3637
- {
3638
- title: "Resolution Tier",
3639
- description: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3640
- "The maximum",
3641
- " ",
3642
- /* @__PURE__ */ jsxRuntime.jsx(
3643
- "a",
3644
- {
3645
- href: "https://docs.mux.com/api-reference#video/operation/create-direct-upload",
3646
- target: "_blank",
3647
- rel: "noopener noreferrer",
3648
- children: "resolution_tier"
3649
- }
3650
- ),
3651
- " ",
3652
- "your asset is encoded, stored, and streamed at."
3653
- ] }),
3654
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 3, wrap: "wrap", children: RESOLUTION_TIERS.map(({ value, label }, index) => {
3655
- const inputId = `${id}--type-${value}`;
3656
- return index > maxSupportedResolution ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3657
- /* @__PURE__ */ jsxRuntime.jsx(
3658
- ui.Radio,
3659
- {
3660
- checked: config.max_resolution_tier === value,
3661
- name: "asset-resolutiontier",
3662
- onChange: (e) => dispatch({
3663
- action: "max_resolution_tier",
3664
- value: e.currentTarget.value
3665
- }),
3666
- value,
3667
- id: inputId
3668
- }
3669
- ),
3670
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, children: label })
3671
- ] }, value);
3672
- }) })
3673
- }
3674
- ),
3675
5464
  !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(sanity.FormField, { title: "Additional Configuration", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
3676
5465
  /* @__PURE__ */ jsxRuntime.jsx(PlaybackPolicy, { id, config, secrets, dispatch }),
3677
- /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsx(
3678
- sanity.FormField,
5466
+ maxSupportedResolution > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5467
+ ResolutionTierSelector,
3679
5468
  {
3680
- title: "Static Renditions",
3681
- description: "Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped.",
3682
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
3683
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, children: [
3684
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3685
- /* @__PURE__ */ jsxRuntime.jsx(
3686
- ui.Radio,
3687
- {
3688
- checked: renditionMode === "standard",
3689
- name: "rendition-mode",
3690
- onChange: () => handleModeChange("standard"),
3691
- value: "standard",
3692
- id: `${id}--mode-standard`
3693
- }
3694
- ),
3695
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-standard`, children: "Standard" })
3696
- ] }),
3697
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3698
- /* @__PURE__ */ jsxRuntime.jsx(
3699
- ui.Radio,
3700
- {
3701
- checked: renditionMode === "advanced",
3702
- name: "rendition-mode",
3703
- onChange: () => handleModeChange("advanced"),
3704
- value: "advanced",
3705
- id: `${id}--mode-advanced`
3706
- }
3707
- ),
3708
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--mode-advanced`, children: "Advanced" })
3709
- ] })
3710
- ] }),
3711
- renditionMode === "standard" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3712
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
3713
- /* @__PURE__ */ jsxRuntime.jsx(
3714
- ui.Checkbox,
3715
- {
3716
- id: `${id}--highest`,
3717
- style: { display: "block" },
3718
- checked: config.static_renditions.includes("highest"),
3719
- onChange: () => toggleRendition("highest")
3720
- }
3721
- ),
3722
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--highest`, children: "Highest Resolution (up to 4K)" })
3723
- ] }),
3724
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [0, 2], children: [
3725
- /* @__PURE__ */ jsxRuntime.jsx(
3726
- ui.Checkbox,
3727
- {
3728
- id: `${id}--audio-only-standard`,
3729
- style: { display: "block" },
3730
- checked: config.static_renditions.includes("audio-only"),
3731
- onChange: () => toggleRendition("audio-only")
3732
- }
3733
- ),
3734
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-standard`, children: "Audio Only (M4A)" })
3735
- ] })
3736
- ] }),
3737
- renditionMode === "advanced" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
3738
- /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, muted: !0, children: "Select specific resolutions:" }),
3739
- /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { gap: 2, wrap: "wrap", children: ADVANCED_RESOLUTIONS.map(({ value, label }) => {
3740
- const inputId = `${id}--resolution-${value}`;
3741
- return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
3742
- /* @__PURE__ */ jsxRuntime.jsx(
3743
- ui.Checkbox,
3744
- {
3745
- id: inputId,
3746
- style: { display: "block" },
3747
- checked: config.static_renditions.includes(value),
3748
- onChange: () => toggleRendition(value)
3749
- }
3750
- ),
3751
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: inputId, size: 1, children: label })
3752
- ] }, value);
3753
- }) }),
3754
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, padding: [2, 2, 0, 2], children: [
3755
- /* @__PURE__ */ jsxRuntime.jsx(
3756
- ui.Checkbox,
3757
- {
3758
- id: `${id}--audio-only-advanced`,
3759
- style: { display: "block" },
3760
- checked: config.static_renditions.includes("audio-only"),
3761
- onChange: () => toggleRendition("audio-only")
3762
- }
3763
- ),
3764
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "label", htmlFor: `${id}--audio-only-advanced`, children: "Audio Only (M4A)" })
3765
- ] })
3766
- ] })
3767
- ] })
5469
+ id,
5470
+ config,
5471
+ dispatch,
5472
+ maxSupportedResolution
3768
5473
  }
3769
- ) })
5474
+ ),
5475
+ /* @__PURE__ */ jsxRuntime.jsx(StaticRenditionSelector, { id, config, dispatch }),
5476
+ !disableTextTrackConfig && /* @__PURE__ */ jsxRuntime.jsx(
5477
+ TextTracksEditor,
5478
+ {
5479
+ tracks: config.text_tracks,
5480
+ dispatch,
5481
+ defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
5482
+ }
5483
+ )
3770
5484
  ] }) })
3771
5485
  ] }),
3772
- !disableTextTrackConfig && !basicConfig && /* @__PURE__ */ jsxRuntime.jsx(
3773
- TextTracksEditor,
3774
- {
3775
- tracks: config.text_tracks,
3776
- dispatch,
3777
- defaultLang: pluginConfig.defaultAutogeneratedSubtitleLang
3778
- }
3779
- ),
3780
5486
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsx(
3781
5487
  ui.Button,
3782
5488
  {
3783
- disabled: !basicConfig && !config.public_policy && !config.signed_policy || validationError !== null || isLoadingDuration || isLoadingFileSize && !canSkipFileSizeValidation,
5489
+ disabled: !basicConfig && !playbackPolicySelected || validationError !== null || isLoadingMetadata || isLoadingFileSize && !canSkipFileSizeValidation,
3784
5490
  icon: icons.UploadIcon,
3785
5491
  text: "Upload",
3786
5492
  tone: "positive",
3787
5493
  onClick: () => {
3788
- validationError || startUpload(formatUploadConfig(config));
5494
+ validationError || startUpload(formatUploadConfig(config, secrets));
3789
5495
  }
3790
5496
  }
3791
5497
  ) })
@@ -3793,11 +5499,14 @@ function UploadConfiguration({
3793
5499
  }
3794
5500
  );
3795
5501
  }
3796
- function setPlaybackPolicy(config) {
3797
- const playback_policy = [];
3798
- return config.public_policy && playback_policy.push("public"), config.signed_policy && playback_policy.push("signed"), playback_policy;
5502
+ function setAdvancedPlaybackPolicy(config, secrets) {
5503
+ const advanced_playback_policies = [];
5504
+ 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({
5505
+ policy: "drm",
5506
+ drm_configuration_id: secrets.drmConfigId ?? void 0
5507
+ }) : console.error("Selected DRM Policy but missing DRM Configuration Id")), advanced_playback_policies;
3799
5508
  }
3800
- function formatUploadConfig(config) {
5509
+ function formatUploadConfig(config, secrets) {
3801
5510
  const generated_subtitles = config.text_tracks.filter(isAutogeneratedTrack).map((track) => ({
3802
5511
  name: track.name,
3803
5512
  language_code: track.language_code
@@ -3821,7 +5530,7 @@ function formatUploadConfig(config) {
3821
5530
  )
3822
5531
  ],
3823
5532
  static_renditions: config.static_renditions.length > 0 ? config.static_renditions.map((resolution) => ({ resolution })) : void 0,
3824
- playback_policy: setPlaybackPolicy(config),
5533
+ advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
3825
5534
  max_resolution_tier: config.max_resolution_tier,
3826
5535
  video_quality: config.video_quality,
3827
5536
  normalize_audio: config.normalize_audio
@@ -3850,12 +5559,10 @@ function withFocusRing(component) {
3850
5559
  `;
3851
5560
  });
3852
5561
  }
3853
- const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
5562
+ const UploadCardWithFocusRing = withFocusRing(ui.Card), UploadCard = React.forwardRef(
3854
5563
  ({ children, tone, onPaste, onDrop, onDragEnter, onDragLeave, onDragOver }, forwardedRef) => {
3855
- const ctrlDown = React.useRef(!1), inputRef = React.useRef(null), handleKeyDown = React.useCallback((event) => {
3856
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !0), ctrlDown.current && event.keyCode == 86 && inputRef.current.focus();
3857
- }, []), handleKeyUp = React.useCallback((event) => {
3858
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !1);
5564
+ const inputRef = React.useRef(null), handleKeyDown = React.useCallback((event) => {
5565
+ event.target.closest("#vtt-url") || (event.ctrlKey || event.metaKey) && event.key === "v" && inputRef.current.focus();
3859
5566
  }, []);
3860
5567
  return /* @__PURE__ */ jsxRuntime.jsxs(
3861
5568
  UploadCardWithFocusRing,
@@ -3867,14 +5574,13 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(ui.Card
3867
5574
  shadow: 0,
3868
5575
  tabIndex: 0,
3869
5576
  onKeyDown: handleKeyDown,
3870
- onKeyUp: handleKeyUp,
3871
5577
  onPaste,
3872
5578
  onDrop,
3873
5579
  onDragEnter,
3874
5580
  onDragLeave,
3875
5581
  onDragOver,
3876
5582
  children: [
3877
- /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef, onPaste }),
5583
+ /* @__PURE__ */ jsxRuntime.jsx(HiddenInput$1, { ref: inputRef }),
3878
5584
  children
3879
5585
  ]
3880
5586
  }
@@ -4036,8 +5742,13 @@ function Uploader(props) {
4036
5742
  case "reset":
4037
5743
  case "complete":
4038
5744
  return uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null, INITIAL_STATE;
4039
- case "error":
4040
- return uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null, Object.assign({}, INITIAL_STATE, { error: action.error });
5745
+ case "error": {
5746
+ uploadRef.current?.unsubscribe(), uploadRef.current = null, uploadingDocumentId.current = null;
5747
+ let error = action.error;
5748
+ return isServerError(action.error) && hasPlaybackPolicy(action.settings, "drm") && (error = new Error(
5749
+ "Unknown Error while uploading DRM protected content. Make sure your DRM configuration ID is valid and set correctly"
5750
+ )), Object.assign({}, INITIAL_STATE, { error });
5751
+ }
4041
5752
  default:
4042
5753
  return prev;
4043
5754
  }
@@ -4116,7 +5827,7 @@ function Uploader(props) {
4116
5827
  }
4117
5828
  },
4118
5829
  complete: () => dispatch({ action: "complete" }),
4119
- error: (error) => dispatch({ action: "error", error })
5830
+ error: (error) => dispatch({ action: "error", error, settings })
4120
5831
  });
4121
5832
  }, invalidFileToast = React.useCallback(() => {
4122
5833
  toast.push({
@@ -4132,6 +5843,8 @@ function Uploader(props) {
4132
5843
  input: { type: "file", files }
4133
5844
  });
4134
5845
  }, handlePaste = (event) => {
5846
+ if (event.target.closest("#vtt-url"))
5847
+ return;
4135
5848
  event.preventDefault(), event.stopPropagation();
4136
5849
  const url = (event.clipboardData || window.clipboardData)?.getData("text")?.trim();
4137
5850
  if (!isValidUrl(url)) {
@@ -4165,11 +5878,11 @@ function Uploader(props) {
4165
5878
  idx > -1 && dragEnteredEls.current.splice(idx, 1), dragEnteredEls.current.length === 0 && setDragState(null);
4166
5879
  };
4167
5880
  if (state.error !== null) {
4168
- const error = {};
5881
+ const error = state.error;
4169
5882
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 3, direction: "column", justify: "center", align: "center", children: [
4170
5883
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 5, muted: !0, children: /* @__PURE__ */ jsxRuntime.jsx(icons.ErrorOutlineIcon, {}) }),
4171
5884
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Something went wrong" }),
4172
- error instanceof Error && error.message && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: error.message }),
5885
+ error instanceof Error && error.message && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, weight: "semibold", style: { textAlign: "center" }, children: error.message }),
4173
5886
  /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { text: "Upload another file", onClick: () => dispatch({ action: "reset" }) })
4174
5887
  ] });
4175
5888
  }
@@ -4254,6 +5967,7 @@ function Uploader(props) {
4254
5967
  props.dialogState === "select-video" && /* @__PURE__ */ jsxRuntime.jsx(
4255
5968
  InputBrowser,
4256
5969
  {
5970
+ config: props.config,
4257
5971
  asset: props.asset,
4258
5972
  onChange: props.onChange,
4259
5973
  setDialogState: props.setDialogState
@@ -4500,6 +6214,7 @@ const muxVideoSchema = {
4500
6214
  normalize_audio: !1,
4501
6215
  defaultPublic: !0,
4502
6216
  defaultSigned: !1,
6217
+ defaultDrm: !1,
4503
6218
  tool: DEFAULT_TOOL_CONFIG,
4504
6219
  allowedRolesForConfiguration: [],
4505
6220
  acceptedMimeTypes: ["video/*", "audio/*"]