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