sanity-plugin-mux-input 2.12.1 → 2.14.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 (35) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +133 -0
  3. package/dist/index.d.mts +63 -1
  4. package/dist/index.d.ts +63 -1
  5. package/dist/index.js +1564 -153
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1565 -154
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +1 -1
  10. package/src/_exports/index.ts +1 -0
  11. package/src/actions/assets.ts +75 -0
  12. package/src/components/AddCaptionDialog.tsx +421 -0
  13. package/src/components/CaptionsDialog.tsx +23 -0
  14. package/src/components/EditCaptionDialog.tsx +508 -0
  15. package/src/components/FileInputButton.tsx +2 -2
  16. package/src/components/Onboard.tsx +2 -2
  17. package/src/components/PageSelector.tsx +57 -0
  18. package/src/components/Player.tsx +4 -3
  19. package/src/components/PlayerActionsMenu.tsx +17 -8
  20. package/src/components/TextTracksManager.tsx +781 -0
  21. package/src/components/UploadConfiguration.tsx +181 -4
  22. package/src/components/UploadPlaceholder.tsx +14 -6
  23. package/src/components/Uploader.styled.tsx +8 -15
  24. package/src/components/Uploader.tsx +61 -6
  25. package/src/components/VideoDetails/VideoDetails.tsx +16 -0
  26. package/src/components/VideoInBrowser.tsx +2 -7
  27. package/src/components/VideoPlayer.tsx +35 -6
  28. package/src/components/VideosBrowser.tsx +9 -1
  29. package/src/components/icons/Audio.tsx +13 -0
  30. package/src/hooks/useAccessControl.ts +1 -0
  31. package/src/hooks/useDialogState.ts +1 -1
  32. package/src/util/getVideoMetadata.ts +3 -1
  33. package/src/util/textTracks.ts +219 -0
  34. package/src/util/types.ts +56 -3
  35. package/src/components/FileInputArea.tsx +0 -93
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
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";
3
+ import { ErrorOutlineIcon, InfoOutlineIcon, RetryIcon, CheckmarkCircleIcon, RetrieveIcon, ChevronLeftIcon, ChevronRightIcon, SyncIcon, SortIcon, UploadIcon, TranslateIcon, DownloadIcon, AddIcon, ChevronUpIcon, ChevronDownIcon, TrashIcon, EditIcon, WarningOutlineIcon, PublishIcon, DocumentIcon, RevertIcon, SearchIcon, ClockIcon, CropIcon, CalendarIcon, TagIcon, CheckmarkIcon, LockIcon, PlayIcon, PlugIcon, EllipsisHorizontalIcon, ImageIcon, ResetIcon, WarningFilledIcon, DocumentVideoIcon } from "@sanity/icons";
4
+ import { useTheme_v2, Stack, Flex, Box, Text, Button, Dialog, Card, TextInput, Checkbox, Code, Inline, Spinner, Heading, Label as Label$1, MenuButton, Menu, MenuItem, useToast, Autocomplete, Tooltip, TabList, Tab, TabPanel, Grid, useClickOutsideEvent, Popover, MenuDivider, Radio, rem } from "@sanity/ui";
5
5
  import React, { useState, useMemo, useCallback, useReducer, useId, memo, useRef, useEffect, createContext, useContext, isValidElement, PureComponent, createElement, forwardRef, Suspense } from "react";
6
6
  import compact from "lodash/compact.js";
7
7
  import toLower from "lodash/toLower.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
  {
@@ -567,6 +567,51 @@ function listAssets(client, options) {
567
567
  query
568
568
  });
569
569
  }
570
+ function addTextTrackFromUrl(client, assetId, vttUrl, options) {
571
+ const { dataset } = client.config();
572
+ return client.request({
573
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks`,
574
+ withCredentials: !0,
575
+ method: "POST",
576
+ body: {
577
+ url: vttUrl,
578
+ type: "text",
579
+ language_code: options.language_code,
580
+ name: options.name,
581
+ text_type: options.text_type || "subtitles"
582
+ },
583
+ headers: {
584
+ "Content-Type": "application/json"
585
+ }
586
+ });
587
+ }
588
+ function generateSubtitles(client, assetId, audioTrackId, options) {
589
+ const { dataset } = client.config();
590
+ return client.request({
591
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${audioTrackId}/generate-subtitles`,
592
+ withCredentials: !0,
593
+ method: "POST",
594
+ body: {
595
+ generated_subtitles: [
596
+ {
597
+ language_code: options.language_code,
598
+ name: options.name
599
+ }
600
+ ]
601
+ },
602
+ headers: {
603
+ "Content-Type": "application/json"
604
+ }
605
+ });
606
+ }
607
+ function deleteTextTrack(client, assetId, trackId) {
608
+ const { dataset } = client.config();
609
+ return client.request({
610
+ url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
611
+ withCredentials: !0,
612
+ method: "DELETE"
613
+ });
614
+ }
570
615
  const ASSETS_PER_PAGE = 100;
571
616
  async function fetchMuxAssetsPage(client, cursor) {
572
617
  try {
@@ -1104,6 +1149,46 @@ function ImportVideosFromMux() {
1104
1149
  if (importAssets.hasSecrets)
1105
1150
  return importAssets.dialogOpen ? /* @__PURE__ */ jsx(ImportVideosDialog, { ...importAssets }) : /* @__PURE__ */ jsx(Button, { mode: "bleed", text: "Import from Mux", onClick: importAssets.openDialog });
1106
1151
  }
1152
+ const PageSelector = (props) => {
1153
+ const page = props.page, setPage = props.setPage;
1154
+ return useEffect(() => {
1155
+ const clamped = Math.min(props.total - 1, Math.max(0, page));
1156
+ page !== clamped && setPage(clamped);
1157
+ }, [page, props.total, setPage]), /* @__PURE__ */ jsxs(Fragment, { children: [
1158
+ /* @__PURE__ */ jsx(
1159
+ Button,
1160
+ {
1161
+ icon: ChevronLeftIcon,
1162
+ mode: "bleed",
1163
+ padding: 3,
1164
+ style: { cursor: "pointer" },
1165
+ disabled: page <= 0,
1166
+ onClick: () => {
1167
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 - 1)));
1168
+ }
1169
+ }
1170
+ ),
1171
+ /* @__PURE__ */ jsxs(Label$1, { muted: !0, children: [
1172
+ "Page ",
1173
+ page + 1,
1174
+ "/",
1175
+ props.total
1176
+ ] }),
1177
+ /* @__PURE__ */ jsx(
1178
+ Button,
1179
+ {
1180
+ icon: ChevronRightIcon,
1181
+ mode: "bleed",
1182
+ padding: 3,
1183
+ style: { cursor: "pointer" },
1184
+ disabled: page >= props.total - 1,
1185
+ onClick: () => {
1186
+ setPage((page2) => Math.min(props.total - 1, Math.max(0, page2 + 1)));
1187
+ }
1188
+ }
1189
+ )
1190
+ ] });
1191
+ };
1107
1192
  function useResyncMuxMetadata() {
1108
1193
  const documentStore = useDocumentStore(), client = useClient$1({
1109
1194
  apiVersion: SANITY_API_VERSION
@@ -1346,38 +1431,1249 @@ const SpinnerBox = () => /* @__PURE__ */ jsx(
1346
1431
  /* @__PURE__ */ jsx(Text, { size: (props.size || 1) + 1, muted: !0, children: /* @__PURE__ */ jsx(Icon, {}) }),
1347
1432
  /* @__PURE__ */ jsx(Text, { size: props.size || 1, muted: props.muted, children: props.text })
1348
1433
  ] });
1349
- };
1350
- function ResolutionIcon(props) {
1351
- return /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsx(
1352
- "path",
1434
+ };
1435
+ function ResolutionIcon(props) {
1436
+ return /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsx(
1437
+ "path",
1438
+ {
1439
+ fill: "currentColor",
1440
+ d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
1441
+ }
1442
+ ) });
1443
+ }
1444
+ function StopWatchIcon(props) {
1445
+ return /* @__PURE__ */ jsxs(
1446
+ "svg",
1447
+ {
1448
+ xmlns: "http://www.w3.org/2000/svg",
1449
+ width: "1em",
1450
+ height: "1em",
1451
+ viewBox: "0 0 512 512",
1452
+ ...props,
1453
+ children: [
1454
+ /* @__PURE__ */ jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1455
+ /* @__PURE__ */ jsx(
1456
+ "path",
1457
+ {
1458
+ d: "M407.67 170.271l30.786-30.786-33.942-33.941-30.785 30.786C341.217 111.057 300.369 96 256 96 149.961 96 64 181.961 64 288s85.961 192 192 192 192-85.961 192-192c0-44.369-15.057-85.217-40.33-117.729zm-45.604 223.795C333.734 422.398 296.066 438 256 438s-77.735-15.602-106.066-43.934C121.602 365.735 106 328.066 106 288s15.602-77.735 43.934-106.066C178.265 153.602 215.934 138 256 138s77.734 15.602 106.066 43.934C390.398 210.265 406 247.934 406 288s-15.602 77.735-43.934 106.066z",
1459
+ fill: "currentColor"
1460
+ }
1461
+ ),
1462
+ /* @__PURE__ */ jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1463
+ ]
1464
+ }
1465
+ );
1466
+ }
1467
+ function extractErrorMessage(error, defaultMessage = "Failed to process request") {
1468
+ let message = "";
1469
+ if (error && typeof error == "object") {
1470
+ const err = error;
1471
+ message = err.response?.body?.message || err.message || "";
1472
+ } else typeof error == "string" && (message = error);
1473
+ if (!message)
1474
+ return defaultMessage;
1475
+ const match = message.match(/\(([^)]+)\)/);
1476
+ if (match && match[1])
1477
+ return match[1];
1478
+ if (message.includes("responded with")) {
1479
+ const parts = message.split("(");
1480
+ if (parts.length > 1)
1481
+ return parts[parts.length - 1].replace(")", "").trim();
1482
+ }
1483
+ return message;
1484
+ }
1485
+ async function pollTrackStatus(options) {
1486
+ const {
1487
+ client,
1488
+ assetId,
1489
+ trackName,
1490
+ trackLanguageCode,
1491
+ maxAttempts = 10,
1492
+ onTrackFound,
1493
+ onTrackErrored,
1494
+ onTrackReady
1495
+ } = options, trimmedName = trackName.trim(), trimmedLanguageCode = trackLanguageCode.trim();
1496
+ let newTrack, attempts = 0, trackFound = !1;
1497
+ const findTrack = (textTracks) => {
1498
+ let foundTrack = textTracks.find(
1499
+ (track) => track.name === trimmedName && track.language_code === trimmedLanguageCode
1500
+ );
1501
+ return foundTrack || (foundTrack = textTracks.find((track) => track.language_code === trimmedLanguageCode)), !foundTrack && textTracks.length > 0 && (foundTrack = textTracks[textTracks.length - 1]), foundTrack;
1502
+ };
1503
+ for (; attempts < maxAttempts; ) {
1504
+ try {
1505
+ attempts > 0 && await new Promise((resolve) => setTimeout(resolve, 1e3));
1506
+ const textTracks = (await getAsset(client, assetId)).data.tracks?.filter((track) => track.type === "text") || [], foundTrack = findTrack(textTracks);
1507
+ if (!foundTrack) {
1508
+ attempts++;
1509
+ continue;
1510
+ }
1511
+ if (trackFound = !0, newTrack = foundTrack, onTrackFound && onTrackFound(foundTrack), foundTrack.status === "ready") {
1512
+ onTrackReady && onTrackReady(foundTrack);
1513
+ break;
1514
+ }
1515
+ if (foundTrack.status === "errored")
1516
+ return onTrackErrored && onTrackErrored(foundTrack), {
1517
+ track: foundTrack,
1518
+ found: !0,
1519
+ status: "errored"
1520
+ };
1521
+ } catch (error) {
1522
+ console.error("Failed to fetch updated asset:", error);
1523
+ }
1524
+ attempts++;
1525
+ }
1526
+ return !newTrack || !trackFound ? {
1527
+ track: void 0,
1528
+ found: !1,
1529
+ status: "not-found"
1530
+ } : newTrack.status === "preparing" ? {
1531
+ track: newTrack,
1532
+ found: !0,
1533
+ status: "preparing"
1534
+ } : {
1535
+ track: newTrack,
1536
+ found: !0,
1537
+ status: "ready"
1538
+ };
1539
+ }
1540
+ async function downloadVttFile(client, asset, track) {
1541
+ if (!track.id)
1542
+ throw new Error("Track ID is missing");
1543
+ if (track.status !== "ready")
1544
+ throw new Error(`Track is not ready yet. Status: ${track.status}`);
1545
+ if (!asset.assetId)
1546
+ throw new Error("Asset ID is required");
1547
+ const playbackId = getPlaybackId(asset);
1548
+ if (!playbackId)
1549
+ throw new Error("Playback ID is required");
1550
+ const playbackPolicy = getPlaybackPolicy(asset);
1551
+ let downloadUrl = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
1552
+ if (playbackPolicy === "signed") {
1553
+ const token = generateJwt(client, playbackId, "v");
1554
+ downloadUrl += `?token=${token}`;
1555
+ }
1556
+ const response = await fetch(downloadUrl);
1557
+ if (!response.ok)
1558
+ throw new Error(`Failed to download file: ${response.statusText}`);
1559
+ const blob = await response.blob(), blobUrl = URL.createObjectURL(blob), link = document.createElement("a");
1560
+ 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);
1561
+ }
1562
+ const SUPPORTED_MUX_LANGUAGES = [
1563
+ { label: "English", code: "en", state: "Stable" },
1564
+ { label: "Spanish", code: "es", state: "Stable" },
1565
+ { label: "Italian", code: "it", state: "Stable" },
1566
+ { label: "Portuguese", code: "pt", state: "Stable" },
1567
+ { label: "German", code: "de", state: "Stable" },
1568
+ { label: "French", code: "fr", state: "Stable" },
1569
+ { label: "Polish", code: "pl", state: "Beta" },
1570
+ { label: "Russian", code: "ru", state: "Beta" },
1571
+ { label: "Dutch", code: "nl", state: "Beta" },
1572
+ { label: "Catalan", code: "ca", state: "Beta" },
1573
+ { label: "Turkish", code: "tr", state: "Beta" },
1574
+ { label: "Swedish", code: "sv", state: "Beta" },
1575
+ { label: "Ukrainian", code: "uk", state: "Beta" },
1576
+ { label: "Norwegian", code: "no", state: "Beta" },
1577
+ { label: "Finnish", code: "fi", state: "Beta" },
1578
+ { label: "Slovak", code: "sk", state: "Beta" },
1579
+ { label: "Greek", code: "el", state: "Beta" },
1580
+ { label: "Czech", code: "cs", state: "Beta" },
1581
+ { label: "Croatian", code: "hr", state: "Beta" },
1582
+ { label: "Danish", code: "da", state: "Beta" },
1583
+ { label: "Romanian", code: "ro", state: "Beta" },
1584
+ { label: "Bulgarian", code: "bg", state: "Beta" }
1585
+ ];
1586
+ function isCustomTextTrack(track) {
1587
+ return track.type !== "autogenerated";
1588
+ }
1589
+ function isAutogeneratedTrack(track) {
1590
+ return track.type === "autogenerated";
1591
+ }
1592
+ const LANGUAGE_OPTIONS$1 = LanguagesList.getAllCodes().map((code) => ({
1593
+ value: code,
1594
+ label: LanguagesList.getNativeName(code)
1595
+ })), MUX_LANGUAGE_OPTIONS = SUPPORTED_MUX_LANGUAGES.map((lang) => ({
1596
+ value: lang.code,
1597
+ label: lang.label
1598
+ }));
1599
+ function AddCaptionDialog({ asset, onAdd, onClose }) {
1600
+ const client = useClient(), toast = useToast(), dialogId = `AddCaptionDialog${useId()}`, [isAutogenerated, setIsAutogenerated] = useState(!1), [vttUrl, setVttUrl] = useState(""), [languageCode, setLanguageCode] = useState(""), [selectedLanguage, setSelectedLanguage] = useState(
1601
+ null
1602
+ ), [name2, setName] = useState(""), [isSubmitting, setIsSubmitting] = useState(!1), [selectedFile, setSelectedFile] = useState(null), fileInputRef = useRef(null), uploadVttFile = async (file) => (await client.assets.upload("file", file, {
1603
+ filename: file.name
1604
+ })).url, handleAddTrackFromUrl = async () => {
1605
+ if (!asset.assetId)
1606
+ throw new Error("Asset ID is required");
1607
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim();
1608
+ let vttUrlToUse = vttUrl.trim();
1609
+ if (selectedFile)
1610
+ try {
1611
+ vttUrlToUse = await uploadVttFile(selectedFile);
1612
+ } catch (uploadError) {
1613
+ throw toast.push({
1614
+ title: "Failed to upload VTT file",
1615
+ status: "error",
1616
+ description: "Could not upload the VTT file to Sanity. Please try again."
1617
+ }), setIsSubmitting(!1), uploadError;
1618
+ }
1619
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1620
+ language_code: trimmedLanguageCode,
1621
+ name: trimmedName,
1622
+ text_type: "subtitles"
1623
+ });
1624
+ const result = await pollTrackStatus({
1625
+ client,
1626
+ assetId: asset.assetId,
1627
+ trackName: trimmedName,
1628
+ trackLanguageCode: trimmedLanguageCode,
1629
+ onTrackErrored: (track) => {
1630
+ const errorMessage = track.error?.messages?.[0] || track.error?.type || "The track failed to download from the provided URL";
1631
+ toast.push({
1632
+ title: "Caption track failed",
1633
+ status: "error",
1634
+ description: errorMessage
1635
+ }), onAdd(track), onClose();
1636
+ }
1637
+ });
1638
+ if (!result.found || !result.track) {
1639
+ toast.push({
1640
+ title: "Caption track may have been added",
1641
+ status: "warning",
1642
+ 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."
1643
+ }), onClose();
1644
+ return;
1645
+ }
1646
+ if (result.status !== "errored") {
1647
+ if (result.status === "preparing") {
1648
+ toast.push({
1649
+ title: "Caption track is processing",
1650
+ status: "info",
1651
+ description: "The track was created and is being processed. It will appear in the list shortly."
1652
+ }), onAdd(result.track), onClose();
1653
+ return;
1654
+ }
1655
+ toast.push({
1656
+ title: "Caption track added",
1657
+ status: "success",
1658
+ description: "Caption track added successfully"
1659
+ }), onAdd(result.track), onClose();
1660
+ }
1661
+ }, handleGenerateSubtitles = async () => {
1662
+ if (!asset.assetId)
1663
+ throw new Error("Asset ID is required");
1664
+ const audioTrack = (await getAsset(client, asset.assetId)).data.tracks?.find((track) => track.type === "audio");
1665
+ if (!audioTrack || !audioTrack.id)
1666
+ throw toast.push({
1667
+ title: "No audio track found",
1668
+ status: "error",
1669
+ description: "The asset does not have an audio track. Auto-generated subtitles require an audio track."
1670
+ }), new Error("No audio track found");
1671
+ await generateSubtitles(client, asset.assetId, audioTrack.id, {
1672
+ language_code: languageCode.trim(),
1673
+ name: name2.trim()
1674
+ });
1675
+ const mockTrack = {
1676
+ type: "text",
1677
+ id: `generating-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
1678
+ text_type: "subtitles",
1679
+ text_source: "generated_live",
1680
+ language_code: languageCode.trim(),
1681
+ name: name2.trim(),
1682
+ status: "preparing"
1683
+ };
1684
+ toast.push({
1685
+ title: "Generating subtitles",
1686
+ status: "success",
1687
+ description: "This may take a few minutes"
1688
+ }), onAdd(mockTrack), onClose();
1689
+ }, handleSubmit = async () => {
1690
+ if (!isAutogenerated) {
1691
+ if (!selectedFile && !vttUrl.trim()) {
1692
+ toast.push({
1693
+ title: "VTT file or URL required",
1694
+ status: "error",
1695
+ description: "Please select a VTT file or enter a VTT file URL"
1696
+ });
1697
+ return;
1698
+ }
1699
+ if (vttUrl.trim() && !selectedFile)
1700
+ try {
1701
+ new URL(vttUrl.trim());
1702
+ } catch {
1703
+ toast.push({
1704
+ title: "Invalid URL",
1705
+ status: "error",
1706
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
1707
+ });
1708
+ return;
1709
+ }
1710
+ }
1711
+ if (!name2.trim()) {
1712
+ toast.push({
1713
+ title: "Audio name required",
1714
+ status: "error",
1715
+ description: "Please enter an audio name for this caption track"
1716
+ });
1717
+ return;
1718
+ }
1719
+ if (!languageCode.trim()) {
1720
+ toast.push({
1721
+ title: "Language code required",
1722
+ status: "error",
1723
+ description: "Please enter a language code (e.g., en, es, fr)"
1724
+ });
1725
+ return;
1726
+ }
1727
+ setIsSubmitting(!0);
1728
+ try {
1729
+ isAutogenerated ? await handleGenerateSubtitles() : await handleAddTrackFromUrl();
1730
+ } catch (error) {
1731
+ toast.push({
1732
+ title: "Failed to add caption track",
1733
+ status: "error",
1734
+ description: extractErrorMessage(error, "Failed to add caption track")
1735
+ });
1736
+ } finally {
1737
+ setIsSubmitting(!1);
1738
+ }
1739
+ };
1740
+ return /* @__PURE__ */ jsx(
1741
+ Dialog,
1742
+ {
1743
+ id: dialogId,
1744
+ header: "Add Caption Track",
1745
+ onClose,
1746
+ width: 1,
1747
+ onClickOutside: onClose,
1748
+ children: /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, children: [
1749
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1750
+ /* @__PURE__ */ jsxs(Flex, { align: "center", marginBottom: 3, children: [
1751
+ /* @__PURE__ */ jsx(
1752
+ Checkbox,
1753
+ {
1754
+ id: "autogenerated-checkbox",
1755
+ style: { display: "block" },
1756
+ checked: isAutogenerated,
1757
+ onChange: (e) => {
1758
+ setIsAutogenerated(e.currentTarget.checked), e.currentTarget.checked && setVttUrl("");
1759
+ },
1760
+ disabled: isSubmitting
1761
+ }
1762
+ ),
1763
+ /* @__PURE__ */ jsx(Flex, { flex: 1, paddingLeft: 2, children: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx("label", { htmlFor: "autogenerated-checkbox", children: "Generate captions" }) }) })
1764
+ ] }),
1765
+ !isAutogenerated && /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1766
+ /* @__PURE__ */ jsxs(Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
1767
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", children: [
1768
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: selectedFile ? `Selected: ${selectedFile.name}` : "No file selected" }),
1769
+ /* @__PURE__ */ jsx(
1770
+ Button,
1771
+ {
1772
+ icon: UploadIcon,
1773
+ text: "Select File",
1774
+ mode: "ghost",
1775
+ tone: "primary",
1776
+ fontSize: 1,
1777
+ padding: 2,
1778
+ onClick: () => fileInputRef.current?.click(),
1779
+ disabled: isSubmitting
1780
+ }
1781
+ )
1782
+ ] }),
1783
+ /* @__PURE__ */ jsx(
1784
+ "input",
1785
+ {
1786
+ ref: fileInputRef,
1787
+ type: "file",
1788
+ accept: ".vtt,text/vtt",
1789
+ style: { display: "none" },
1790
+ onChange: (e) => {
1791
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
1792
+ }
1793
+ }
1794
+ )
1795
+ ] }),
1796
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, style: { textAlign: "center" }, children: "Or enter the VTT file URL" }),
1797
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1798
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "vtt-url", children: "VTT File URL" }),
1799
+ /* @__PURE__ */ jsx(
1800
+ TextInput,
1801
+ {
1802
+ id: "vtt-url",
1803
+ placeholder: "https://example.com/subtitles.vtt",
1804
+ value: vttUrl,
1805
+ onChange: (e) => {
1806
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
1807
+ },
1808
+ disabled: isSubmitting
1809
+ }
1810
+ )
1811
+ ] })
1812
+ ] })
1813
+ ] }),
1814
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1815
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-name", children: "Audio name" }),
1816
+ /* @__PURE__ */ jsx(
1817
+ Autocomplete,
1818
+ {
1819
+ id: "caption-name",
1820
+ value: selectedLanguage?.value || "",
1821
+ onChange: (newValue) => {
1822
+ const selected = (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find((opt) => opt.value === newValue);
1823
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
1824
+ },
1825
+ options: isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1,
1826
+ icon: TranslateIcon,
1827
+ placeholder: "Select language",
1828
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
1829
+ openButton: !0,
1830
+ renderValue: (value) => (isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS$1).find(
1831
+ (l) => l.value === value
1832
+ )?.label || value,
1833
+ renderOption: (option) => /* @__PURE__ */ jsx(Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxs(Text, { size: 2, textOverflow: "ellipsis", children: [
1834
+ option.label,
1835
+ " (",
1836
+ option.value,
1837
+ ")"
1838
+ ] }) }),
1839
+ disabled: isSubmitting
1840
+ }
1841
+ )
1842
+ ] }),
1843
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
1844
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-language", children: "Language Code" }),
1845
+ /* @__PURE__ */ jsx(
1846
+ TextInput,
1847
+ {
1848
+ id: "caption-language",
1849
+ placeholder: "en-US",
1850
+ value: languageCode,
1851
+ onChange: (e) => {
1852
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
1853
+ },
1854
+ disabled: isSubmitting
1855
+ }
1856
+ )
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
1859
+ /* @__PURE__ */ jsx(Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
1860
+ /* @__PURE__ */ jsx(
1861
+ Button,
1862
+ {
1863
+ text: "Add Caption Track",
1864
+ tone: "primary",
1865
+ icon: isSubmitting ? /* @__PURE__ */ jsx(
1866
+ Spinner,
1867
+ {
1868
+ style: {
1869
+ verticalAlign: "middle",
1870
+ display: "inline-block",
1871
+ marginBottom: "-3px",
1872
+ width: "1em",
1873
+ height: "1em",
1874
+ marginRight: "-6px"
1875
+ }
1876
+ }
1877
+ ) : /* @__PURE__ */ jsx(UploadIcon, {}),
1878
+ onClick: handleSubmit,
1879
+ disabled: isSubmitting
1880
+ }
1881
+ )
1882
+ ] })
1883
+ ] })
1884
+ }
1885
+ );
1886
+ }
1887
+ const LANGUAGE_OPTIONS = LanguagesList.getAllCodes().map((code) => ({
1888
+ value: code,
1889
+ label: LanguagesList.getNativeName(code)
1890
+ }));
1891
+ function EditCaptionDialog({ asset, track, onUpdate, onClose }) {
1892
+ 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(
1893
+ () => {
1894
+ const baseCode = track.language_code?.split("-")[0], found = LANGUAGE_OPTIONS.find(
1895
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1896
+ );
1897
+ if (found) return found;
1898
+ if (track.name) {
1899
+ const foundByName = LANGUAGE_OPTIONS.find((opt) => opt.label === track.name);
1900
+ if (foundByName) return foundByName;
1901
+ }
1902
+ return null;
1903
+ }
1904
+ ), [name2, setName] = useState(track.name || ""), [isSubmitting, setIsSubmitting] = useState(!1), [downloading, setDownloading] = useState(!1), [selectedFile, setSelectedFile] = useState(null), fileInputRef = useRef(null);
1905
+ useEffect(() => {
1906
+ setLanguageCode(track.language_code || ""), setName(track.name || ""), setVttUrl("");
1907
+ const baseCode = track.language_code?.split("-")[0], foundByCode = LANGUAGE_OPTIONS.find(
1908
+ (opt) => opt.value === track.language_code || opt.value === baseCode
1909
+ ), foundByName = track.name ? LANGUAGE_OPTIONS.find((opt) => opt.label === track.name) : null;
1910
+ setSelectedLanguage(foundByCode || foundByName || null);
1911
+ }, [track, asset, client]);
1912
+ const handleDownloadCurrentFile = async () => {
1913
+ setDownloading(!0);
1914
+ try {
1915
+ await downloadVttFile(client, asset, track);
1916
+ } catch (error) {
1917
+ let errorMessage = "Please try again", title = "Failed to download VTT file";
1918
+ 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({
1919
+ title,
1920
+ status: "error",
1921
+ description: errorMessage
1922
+ });
1923
+ } finally {
1924
+ setDownloading(!1);
1925
+ }
1926
+ }, 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, {
1927
+ filename: file.name
1928
+ })).url, refreshAssetData = async () => {
1929
+ if (!(!asset._id || !asset.assetId))
1930
+ try {
1931
+ const latestAssetData = await getAsset(client, asset.assetId);
1932
+ await client.patch(asset._id).set({ data: latestAssetData.data, status: latestAssetData.data.status }).commit();
1933
+ } catch (refreshError) {
1934
+ console.error("Failed to refresh asset data:", refreshError);
1935
+ }
1936
+ }, handleUpdateTrackWithNewUrl = async () => {
1937
+ if (!asset.assetId)
1938
+ throw new Error("Asset ID is required");
1939
+ const trimmedName = name2.trim(), trimmedLanguageCode = languageCode.trim(), oldTrackId = track.id;
1940
+ try {
1941
+ await deleteTextTrack(client, asset.assetId, oldTrackId);
1942
+ } catch (deleteError) {
1943
+ throw toast.push({
1944
+ title: "Failed to delete old track",
1945
+ status: "error",
1946
+ description: "Could not delete the old track. Please try again or delete it manually."
1947
+ }), setIsSubmitting(!1), deleteError;
1948
+ }
1949
+ let vttUrlToUse = vttUrl.trim();
1950
+ if (selectedFile)
1951
+ try {
1952
+ vttUrlToUse = await uploadVttFile(selectedFile);
1953
+ } catch (uploadError) {
1954
+ throw toast.push({
1955
+ title: "Failed to upload VTT file",
1956
+ status: "error",
1957
+ description: "Could not upload the VTT file to Sanity. Please try again."
1958
+ }), setIsSubmitting(!1), uploadError;
1959
+ }
1960
+ try {
1961
+ await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
1962
+ language_code: trimmedLanguageCode,
1963
+ name: trimmedName,
1964
+ text_type: "subtitles"
1965
+ });
1966
+ } catch (error) {
1967
+ throw toast.push({
1968
+ title: "Failed to update caption track",
1969
+ status: "error",
1970
+ description: extractErrorMessage(error, "Failed to update caption track")
1971
+ }), setIsSubmitting(!1), error;
1972
+ }
1973
+ const result = await pollTrackStatus({
1974
+ client,
1975
+ assetId: asset.assetId,
1976
+ trackName: trimmedName,
1977
+ trackLanguageCode: trimmedLanguageCode,
1978
+ onTrackErrored: async (erroredTrack) => {
1979
+ const errorMessage = erroredTrack.error?.messages?.[0] || erroredTrack.error?.type || "The track failed to download from the provided URL";
1980
+ toast.push({
1981
+ title: "Caption track failed",
1982
+ status: "error",
1983
+ description: errorMessage
1984
+ }), await refreshAssetData(), onUpdate(erroredTrack, oldTrackId), setIsSubmitting(!1);
1985
+ }
1986
+ });
1987
+ if (!result.found || !result.track) {
1988
+ toast.push({
1989
+ title: "Caption track may have been updated",
1990
+ status: "warning",
1991
+ 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."
1992
+ }), setIsSubmitting(!1);
1993
+ return;
1994
+ }
1995
+ result.status !== "errored" && (await refreshAssetData(), result.status === "preparing" ? toast.push({
1996
+ title: "Caption track is processing",
1997
+ status: "info",
1998
+ description: "The track was updated and is being processed. It will appear in the list shortly."
1999
+ }) : toast.push({
2000
+ title: "Caption track updated",
2001
+ status: "success",
2002
+ description: "Caption track updated successfully"
2003
+ }), onUpdate(result.track, oldTrackId), setIsSubmitting(!1));
2004
+ }, handleSubmit = async () => {
2005
+ if (!name2.trim()) {
2006
+ toast.push({
2007
+ title: "Audio name required",
2008
+ status: "error",
2009
+ description: "Please enter an audio name for this caption track"
2010
+ });
2011
+ return;
2012
+ }
2013
+ if (!languageCode.trim()) {
2014
+ toast.push({
2015
+ title: "Language code required",
2016
+ status: "error",
2017
+ description: "Please enter a language code (e.g., en, es, fr)"
2018
+ });
2019
+ return;
2020
+ }
2021
+ setIsSubmitting(!0);
2022
+ try {
2023
+ if (!asset.assetId)
2024
+ throw new Error("Asset ID is required");
2025
+ const originalVttUrl = (() => {
2026
+ if (isAutogenerated || !track.id) return "";
2027
+ const playbackId = getPlaybackId(asset);
2028
+ if (!playbackId) return "";
2029
+ let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`;
2030
+ if (getPlaybackPolicy(asset) === "signed") {
2031
+ const token = generateJwt(client, playbackId, "v");
2032
+ url += `?token=${token}`;
2033
+ }
2034
+ return url;
2035
+ })(), urlChanged = selectedFile !== null || vttUrl.trim() && vttUrl.trim() !== originalVttUrl;
2036
+ if (!urlChanged) {
2037
+ toast.push({
2038
+ title: "No changes",
2039
+ status: "info",
2040
+ description: 'Please provide a new VTT file or URL using the "Replace" button or URL field to update the track.'
2041
+ }), setIsSubmitting(!1);
2042
+ return;
2043
+ }
2044
+ if (urlChanged) {
2045
+ if (!selectedFile && vttUrl.trim())
2046
+ try {
2047
+ new URL(vttUrl.trim());
2048
+ } catch {
2049
+ toast.push({
2050
+ title: "Invalid URL",
2051
+ status: "error",
2052
+ description: "Please enter a valid URL (e.g., https://example.com/subtitles.vtt)"
2053
+ }), setIsSubmitting(!1);
2054
+ return;
2055
+ }
2056
+ if (!selectedFile && !vttUrl.trim()) {
2057
+ toast.push({
2058
+ title: "VTT file or URL required",
2059
+ status: "error",
2060
+ description: "Please select a VTT file or enter a VTT file URL"
2061
+ }), setIsSubmitting(!1);
2062
+ return;
2063
+ }
2064
+ await handleUpdateTrackWithNewUrl();
2065
+ }
2066
+ onClose();
2067
+ } catch (error) {
2068
+ toast.push({
2069
+ title: "Failed to update caption track",
2070
+ status: "error",
2071
+ description: error instanceof Error ? error.message : "Please try again"
2072
+ });
2073
+ } finally {
2074
+ setIsSubmitting(!1);
2075
+ }
2076
+ };
2077
+ return /* @__PURE__ */ jsx(
2078
+ Dialog,
2079
+ {
2080
+ id: dialogId,
2081
+ header: "Edit Caption Track",
2082
+ onClose,
2083
+ width: 1,
2084
+ onClickOutside: onClose,
2085
+ children: /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, children: [
2086
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2087
+ /* @__PURE__ */ jsxs(Card, { padding: 3, marginBottom: 2, tone: "transparent", border: !0, radius: 2, children: [
2088
+ /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", children: [
2089
+ /* @__PURE__ */ jsx(Text, { children: getCurrentFileName() }),
2090
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2091
+ track.status !== "errored" && /* @__PURE__ */ jsx(
2092
+ Button,
2093
+ {
2094
+ icon: downloading ? /* @__PURE__ */ jsx(
2095
+ Spinner,
2096
+ {
2097
+ style: {
2098
+ verticalAlign: "middle",
2099
+ display: "inline-block",
2100
+ marginTop: "-2px",
2101
+ width: "0.5em",
2102
+ height: "0.5em"
2103
+ }
2104
+ }
2105
+ ) : /* @__PURE__ */ jsx(DownloadIcon, {}),
2106
+ text: "Download",
2107
+ mode: "ghost",
2108
+ tone: "primary",
2109
+ fontSize: 1,
2110
+ padding: 2,
2111
+ onClick: handleDownloadCurrentFile,
2112
+ disabled: downloading || isSubmitting
2113
+ }
2114
+ ),
2115
+ /* @__PURE__ */ jsx(
2116
+ Button,
2117
+ {
2118
+ icon: UploadIcon,
2119
+ text: "Replace",
2120
+ mode: "ghost",
2121
+ tone: "primary",
2122
+ fontSize: 1,
2123
+ padding: 2,
2124
+ onClick: () => fileInputRef.current?.click(),
2125
+ disabled: isSubmitting
2126
+ }
2127
+ )
2128
+ ] })
2129
+ ] }),
2130
+ /* @__PURE__ */ jsx(
2131
+ "input",
2132
+ {
2133
+ ref: fileInputRef,
2134
+ type: "file",
2135
+ accept: ".vtt,text/vtt",
2136
+ style: { display: "none" },
2137
+ onChange: (e) => {
2138
+ e.target.files && e.target.files.length > 0 && !isSubmitting && (setSelectedFile(e.target.files[0]), setVttUrl(""));
2139
+ }
2140
+ }
2141
+ ),
2142
+ selectedFile && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, style: { marginTop: 8 }, children: [
2143
+ "Selected: ",
2144
+ selectedFile.name
2145
+ ] })
2146
+ ] }),
2147
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2148
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "vtt-url", children: "VTT File URL" }),
2149
+ /* @__PURE__ */ jsx(
2150
+ TextInput,
2151
+ {
2152
+ id: "vtt-url",
2153
+ placeholder: "https://example.com/subtitles.vtt",
2154
+ value: vttUrl,
2155
+ onChange: (e) => {
2156
+ setVttUrl(e.currentTarget.value), setSelectedFile(null);
2157
+ },
2158
+ disabled: isSubmitting
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Add a URL to replace the existing VTT file with a new one" })
2162
+ ] })
2163
+ ] }),
2164
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2165
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-name", children: "Audio name" }),
2166
+ /* @__PURE__ */ jsx(
2167
+ Autocomplete,
2168
+ {
2169
+ id: "caption-name",
2170
+ value: selectedLanguage?.value || "",
2171
+ onChange: (newValue) => {
2172
+ const selected = LANGUAGE_OPTIONS.find((opt) => opt.value === newValue);
2173
+ selected && (setSelectedLanguage(selected), setLanguageCode(selected.value), setName(selected.label));
2174
+ },
2175
+ options: LANGUAGE_OPTIONS,
2176
+ icon: TranslateIcon,
2177
+ placeholder: "Select language",
2178
+ filterOption: (query, option) => option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 || option.value.toLowerCase().indexOf(query.toLowerCase()) > -1,
2179
+ openButton: !0,
2180
+ renderValue: (value) => LANGUAGE_OPTIONS.find((l) => l.value === value)?.label || value,
2181
+ renderOption: (option) => /* @__PURE__ */ jsx(Card, { "data-as": "button", padding: 3, radius: 2, tone: "inherit", children: /* @__PURE__ */ jsxs(Text, { size: 2, textOverflow: "ellipsis", children: [
2182
+ option.label,
2183
+ " (",
2184
+ option.value,
2185
+ ")"
2186
+ ] }) }),
2187
+ disabled: isSubmitting
2188
+ }
2189
+ )
2190
+ ] }),
2191
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
2192
+ /* @__PURE__ */ jsx(Label$1, { htmlFor: "caption-language", children: "Language Code" }),
2193
+ /* @__PURE__ */ jsx(
2194
+ TextInput,
2195
+ {
2196
+ id: "caption-language",
2197
+ placeholder: "en-US",
2198
+ value: languageCode,
2199
+ onChange: (e) => {
2200
+ setLanguageCode(e.currentTarget.value), selectedLanguage && selectedLanguage.value !== e.currentTarget.value && (setSelectedLanguage(null), (!name2 || name2 === selectedLanguage.label) && setName(""));
2201
+ },
2202
+ disabled: isSubmitting
2203
+ }
2204
+ )
2205
+ ] }),
2206
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justify: "flex-end", marginTop: 2, children: [
2207
+ /* @__PURE__ */ jsx(Button, { text: "Cancel", mode: "ghost", onClick: onClose, disabled: isSubmitting }),
2208
+ /* @__PURE__ */ jsx(
2209
+ Button,
2210
+ {
2211
+ text: "Update Caption Track",
2212
+ tone: "primary",
2213
+ icon: isSubmitting ? /* @__PURE__ */ jsx(
2214
+ Spinner,
2215
+ {
2216
+ style: {
2217
+ verticalAlign: "middle",
2218
+ display: "inline-block",
2219
+ marginBottom: "-3px",
2220
+ width: "1em",
2221
+ height: "1em",
2222
+ marginRight: "-6px"
2223
+ }
2224
+ }
2225
+ ) : UploadIcon,
2226
+ onClick: handleSubmit,
2227
+ disabled: isSubmitting
2228
+ }
2229
+ )
2230
+ ] })
2231
+ ] })
2232
+ }
2233
+ );
2234
+ }
2235
+ function TrackCard({
2236
+ track,
2237
+ iconOnly,
2238
+ downloadingTrackId,
2239
+ deletingTrackId,
2240
+ trackToEdit,
2241
+ getTrackSourceLabel,
2242
+ handleDownload,
2243
+ setTrackToEdit,
2244
+ setTrackToDelete
2245
+ }) {
2246
+ 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: [
2247
+ /* @__PURE__ */ jsx(
2248
+ Spinner,
2249
+ {
2250
+ muted: !0,
2251
+ style: {
2252
+ width: "0.75em",
2253
+ height: "0.75em",
2254
+ verticalAlign: "middle",
2255
+ display: "inline-block",
2256
+ marginBottom: "-2px"
2257
+ }
2258
+ }
2259
+ ),
2260
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Processing..." })
2261
+ ] }) : /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
2262
+ track.status !== "errored" && /* @__PURE__ */ jsx(
2263
+ Button,
2264
+ {
2265
+ icon: downloadingTrackId === track.id ? /* @__PURE__ */ jsx(
2266
+ Spinner,
2267
+ {
2268
+ style: {
2269
+ verticalAlign: "middle",
2270
+ display: "inline-block",
2271
+ marginTop: "-2px",
2272
+ width: "0.5em",
2273
+ height: "0.5em"
2274
+ }
2275
+ }
2276
+ ) : /* @__PURE__ */ jsx(DownloadIcon, {}),
2277
+ text: iconOnly ? void 0 : "Download",
2278
+ mode: "ghost",
2279
+ tone: "primary",
2280
+ fontSize: 1,
2281
+ padding: 2,
2282
+ onClick: () => handleDownload(track),
2283
+ disabled: isDisabled("download"),
2284
+ title: "Download"
2285
+ }
2286
+ ),
2287
+ /* @__PURE__ */ jsx(
2288
+ Button,
2289
+ {
2290
+ icon: /* @__PURE__ */ jsx(EditIcon, {}),
2291
+ text: iconOnly ? void 0 : "Edit",
2292
+ mode: "ghost",
2293
+ tone: "primary",
2294
+ fontSize: 1,
2295
+ padding: 2,
2296
+ disabled: isDisabled("edit"),
2297
+ onClick: () => setTrackToEdit(track),
2298
+ title: "Edit"
2299
+ }
2300
+ ),
2301
+ /* @__PURE__ */ jsx(
2302
+ Button,
2303
+ {
2304
+ icon: deletingTrackId === track.id ? /* @__PURE__ */ jsx(
2305
+ Spinner,
2306
+ {
2307
+ style: {
2308
+ verticalAlign: "middle",
2309
+ display: "inline-block",
2310
+ marginTop: "-2px",
2311
+ width: "0.5em",
2312
+ height: "0.5em"
2313
+ }
2314
+ }
2315
+ ) : /* @__PURE__ */ jsx(TrashIcon, {}),
2316
+ text: iconOnly ? void 0 : "Delete",
2317
+ mode: "ghost",
2318
+ tone: "critical",
2319
+ fontSize: 1,
2320
+ padding: 2,
2321
+ disabled: isDisabled("delete"),
2322
+ onClick: () => setTrackToDelete(track),
2323
+ title: "Delete"
2324
+ }
2325
+ )
2326
+ ] });
2327
+ return /* @__PURE__ */ jsx(
2328
+ Card,
1353
2329
  {
1354
- fill: "currentColor",
1355
- d: "M20 9V6h-3V4h5v5h-2ZM2 9V4h5v2H4v3H2Zm15 11v-2h3v-3h2v5h-5ZM2 20v-5h2v3h3v2H2Zm4-4V8h12v8H6Zm2-2h8v-4H8v4Zm0 0v-4v4Z"
2330
+ padding: 3,
2331
+ radius: 2,
2332
+ tone: track.status === "errored" ? "caution" : "transparent",
2333
+ border: !0,
2334
+ children: /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 3, children: [
2335
+ /* @__PURE__ */ jsxs(Stack, { space: 2, flex: 1, children: [
2336
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
2337
+ /* @__PURE__ */ jsx(Text, { weight: "semibold", children: track.name || "Untitled" }),
2338
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2339
+ "(",
2340
+ getTrackSourceLabel(track),
2341
+ ")"
2342
+ ] }),
2343
+ track.status === "errored" && /* @__PURE__ */ jsx(
2344
+ ErrorOutlineIcon,
2345
+ {
2346
+ style: { color: "var(--card-critical-color)" },
2347
+ "aria-label": "Error",
2348
+ fontSize: 20
2349
+ }
2350
+ )
2351
+ ] }),
2352
+ track.language_code && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
2353
+ "Language: ",
2354
+ track.language_code
2355
+ ] }),
2356
+ 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" })
2357
+ ] }),
2358
+ renderActionButtons()
2359
+ ] })
1356
2360
  }
1357
- ) });
2361
+ );
1358
2362
  }
1359
- function StopWatchIcon(props) {
1360
- return /* @__PURE__ */ jsxs(
1361
- "svg",
1362
- {
1363
- xmlns: "http://www.w3.org/2000/svg",
1364
- width: "1em",
1365
- height: "1em",
1366
- viewBox: "0 0 512 512",
1367
- ...props,
1368
- children: [
1369
- /* @__PURE__ */ jsx("path", { d: "M232 306.667h48V176h-48v130.667z", fill: "currentColor" }),
1370
- /* @__PURE__ */ jsx(
1371
- "path",
1372
- {
1373
- d: "M407.67 170.271l30.786-30.786-33.942-33.941-30.785 30.786C341.217 111.057 300.369 96 256 96 149.961 96 64 181.961 64 288s85.961 192 192 192 192-85.961 192-192c0-44.369-15.057-85.217-40.33-117.729zm-45.604 223.795C333.734 422.398 296.066 438 256 438s-77.735-15.602-106.066-43.934C121.602 365.735 106 328.066 106 288s15.602-77.735 43.934-106.066C178.265 153.602 215.934 138 256 138s77.734 15.602 106.066 43.934C390.398 210.265 406 247.934 406 288s-15.602 77.735-43.934 106.066z",
1374
- fill: "currentColor"
2363
+ function TextTracksManager({
2364
+ asset,
2365
+ iconOnly = !1,
2366
+ tracks: propTracks,
2367
+ collapseTracks = !1
2368
+ }) {
2369
+ const client = useClient(), toast = useToast(), dialogId = `DeleteCaptionDialog${useId()}`, [downloadingTrackId, setDownloadingTrackId] = useState(null), [deletingTrackId, setDeletingTrackId] = useState(null), [addedTracks, setAddedTracks] = useState([]), [updatedTracks, setUpdatedTracks] = useState(/* @__PURE__ */ new Map()), [trackActivityOrder, setTrackActivityOrder] = useState(/* @__PURE__ */ new Map()), [autogeneratedTrackIds, setAutogeneratedTrackIds] = useState(/* @__PURE__ */ new Set()), [trackToDelete, setTrackToDelete] = useState(null), [trackToEdit, setTrackToEdit] = useState(null), [showAddDialog, setShowAddDialog] = useState(!1), [isExpanded, setIsExpanded] = useState(!1), MAX_VISIBLE_TRACKS = 4;
2370
+ useEffect(() => {
2371
+ if (!asset.assetId || !asset._id) return;
2372
+ const assetId = asset.assetId, documentId = asset._id;
2373
+ (async () => {
2374
+ try {
2375
+ const response = await getAsset(client, assetId);
2376
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2377
+ } catch (error) {
2378
+ console.error("Failed to refresh asset data:", error);
2379
+ }
2380
+ })();
2381
+ }, [asset.assetId, asset._id, client]);
2382
+ const activeTracks = (propTracks || asset.data?.tracks?.filter((track) => track.type === "text") || []).filter(
2383
+ (track) => track.id && (track.status === "ready" || track.status === "preparing" || track.status === "errored")
2384
+ ), allTracks = useMemo(() => {
2385
+ const tracksWithUpdates = activeTracks.map((track) => updatedTracks.get(track.id) || track), isMockTrackReplaced = (mockTrack, realTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : realTracksList.some((realTrack) => {
2386
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2387
+ 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";
2388
+ }), 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));
2389
+ return [...tracksWithUpdates, ...tracksToKeep];
2390
+ }, [activeTracks, addedTracks, updatedTracks]);
2391
+ useEffect(() => {
2392
+ const newAutogeneratedIds = /* @__PURE__ */ new Set();
2393
+ activeTracks.forEach((track) => {
2394
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2395
+ }), addedTracks.forEach((mockTrack) => {
2396
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2397
+ const realTrack = activeTracks.find((rt) => {
2398
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2399
+ return nameMatches && languageMatches;
2400
+ });
2401
+ realTrack?.id && newAutogeneratedIds.add(realTrack.id);
2402
+ }
2403
+ }), setAutogeneratedTrackIds((prev) => {
2404
+ let hasNew = !1;
2405
+ const updated = new Set(prev);
2406
+ return newAutogeneratedIds.forEach((id) => {
2407
+ prev.has(id) || (updated.add(id), hasNew = !0);
2408
+ }), hasNew ? updated : prev;
2409
+ });
2410
+ }, [activeTracks, addedTracks]), useEffect(() => {
2411
+ if (allTracks.filter((track) => track.status === "preparing").length === 0 || !asset.assetId || !asset._id)
2412
+ return;
2413
+ const assetId = asset.assetId, documentId = asset._id, interval = setInterval(async () => {
2414
+ try {
2415
+ const response = await getAsset(client, assetId);
2416
+ await client.patch(documentId).set({ data: response.data, status: response.data.status }).commit();
2417
+ const fetchedTracks = response.data.tracks?.filter((track) => track.type === "text") || [], isMockTrackReplaced = (mockTrack, fetchedTracksList) => !mockTrack.id || !mockTrack.id.startsWith("generating-") ? !1 : fetchedTracksList.some((realTrack) => {
2418
+ const nameMatches = realTrack.name === mockTrack.name, languageMatches = realTrack.language_code === mockTrack.language_code;
2419
+ return !nameMatches || !languageMatches ? !1 : realTrack.status === "ready" ? realTrack.text_source === "generated_live" || realTrack.text_source === "generated_live_final" || realTrack.text_source === "generated_vod" : realTrack.status === "preparing";
2420
+ }), newAutogeneratedIds = /* @__PURE__ */ new Set();
2421
+ fetchedTracks.forEach((track) => {
2422
+ track.id && (track.text_source === "generated_live" || track.text_source === "generated_live_final" || track.text_source === "generated_vod") && newAutogeneratedIds.add(track.id);
2423
+ });
2424
+ const findMatchingRealTrack = (mockTrack, tracksList) => tracksList.find((rt) => {
2425
+ const nameMatches = rt.name === mockTrack.name, languageMatches = rt.language_code === mockTrack.language_code;
2426
+ return nameMatches && languageMatches;
2427
+ });
2428
+ setAddedTracks((prev) => prev.filter((mockTrack) => {
2429
+ if (mockTrack.id && mockTrack.id.startsWith("generating-")) {
2430
+ const replaced = isMockTrackReplaced(mockTrack, fetchedTracks);
2431
+ if (replaced) {
2432
+ const realTrack = findMatchingRealTrack(mockTrack, fetchedTracks);
2433
+ realTrack?.id && (newAutogeneratedIds.add(realTrack.id), setTrackActivityOrder((prevOrder) => {
2434
+ const mockOrder = prevOrder.get(mockTrack.id);
2435
+ if (mockOrder) {
2436
+ const newMap = new Map(prevOrder);
2437
+ return newMap.set(realTrack.id, mockOrder), newMap;
2438
+ }
2439
+ return prevOrder;
2440
+ }));
2441
+ }
2442
+ return !replaced;
1375
2443
  }
1376
- ),
1377
- /* @__PURE__ */ jsx("path", { d: "M192 32h128v48H192z", fill: "currentColor" })
1378
- ]
2444
+ return !0;
2445
+ })), newAutogeneratedIds.size > 0 && setAutogeneratedTrackIds((prevIds) => {
2446
+ const updated = new Set(prevIds);
2447
+ return newAutogeneratedIds.forEach((id) => updated.add(id)), updated;
2448
+ });
2449
+ } catch (error) {
2450
+ console.error("Failed to refresh asset data:", error);
2451
+ }
2452
+ }, 3e3);
2453
+ return () => clearInterval(interval);
2454
+ }, [allTracks, asset.assetId, asset._id, client]);
2455
+ const visibleTracks = allTracks.filter(
2456
+ (track) => track.status === "ready" || track.status === "preparing" || track.status === "errored"
2457
+ ).sort((a2, b) => {
2458
+ const orderA = trackActivityOrder.get(a2.id) || 0, orderB = trackActivityOrder.get(b.id) || 0;
2459
+ if (orderA > 0 && orderB > 0)
2460
+ return orderB - orderA;
2461
+ if (orderA > 0) return -1;
2462
+ if (orderB > 0) return 1;
2463
+ const aIsPreparing = a2.status === "preparing", bIsPreparing = b.status === "preparing";
2464
+ if (aIsPreparing && !bIsPreparing) return -1;
2465
+ if (!aIsPreparing && bIsPreparing) return 1;
2466
+ 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);
2467
+ return aIsAutogenerated && !bIsAutogenerated ? -1 : !aIsAutogenerated && bIsAutogenerated ? 1 : 0;
2468
+ }), handleDownload = async (track) => {
2469
+ if (track.id) {
2470
+ setDownloadingTrackId(track.id);
2471
+ try {
2472
+ await downloadVttFile(client, asset, track);
2473
+ } catch (error) {
2474
+ toast.push({
2475
+ title: "Failed to download VTT file",
2476
+ status: "error",
2477
+ description: error instanceof Error ? error.message : "Please try again"
2478
+ });
2479
+ } finally {
2480
+ setDownloadingTrackId(null);
2481
+ }
1379
2482
  }
1380
- );
2483
+ }, confirmDelete = async () => {
2484
+ if (!trackToDelete || !trackToDelete.id) return;
2485
+ const track = trackToDelete;
2486
+ setTrackToDelete(null), setDeletingTrackId(track.id);
2487
+ try {
2488
+ if (!asset.assetId)
2489
+ throw new Error("Asset ID is required");
2490
+ if (await deleteTextTrack(client, asset.assetId, track.id), asset._id)
2491
+ try {
2492
+ const response = await getAsset(client, asset.assetId);
2493
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2494
+ } catch (refreshError) {
2495
+ console.error("Failed to refresh asset data:", refreshError);
2496
+ }
2497
+ toast.push({
2498
+ title: "Successfully deleted caption track",
2499
+ status: "success"
2500
+ }), setAddedTracks((prev) => prev.filter((t) => t.id !== track.id)), setUpdatedTracks((prev) => {
2501
+ const newMap = new Map(prev);
2502
+ return newMap.delete(track.id), newMap;
2503
+ }), setTrackActivityOrder((prev) => {
2504
+ const newMap = new Map(prev);
2505
+ return newMap.delete(track.id), newMap;
2506
+ }), setAutogeneratedTrackIds((prev) => {
2507
+ const updated = new Set(prev);
2508
+ return updated.delete(track.id), updated;
2509
+ });
2510
+ } catch (error) {
2511
+ toast.push({
2512
+ title: "Failed to delete caption track",
2513
+ status: "error",
2514
+ description: error instanceof Error ? error.message : "Please try again"
2515
+ });
2516
+ } finally {
2517
+ setDeletingTrackId(null);
2518
+ }
2519
+ }, handleAddTrack = (track) => {
2520
+ setAddedTracks((prev) => [...prev, track]), setTrackActivityOrder((prev) => {
2521
+ const newMap = new Map(prev);
2522
+ return newMap.set(track.id, prev.size + 1), newMap;
2523
+ }), setShowAddDialog(!1);
2524
+ }, handleUpdateTrack = async (updatedTrack, oldTrackId) => {
2525
+ if (oldTrackId && (setAddedTracks((prev) => prev.filter((t) => t.id !== oldTrackId)), setUpdatedTracks((prev) => {
2526
+ const newMap = new Map(prev);
2527
+ return newMap.delete(oldTrackId), newMap;
2528
+ }), setTrackActivityOrder((prev) => {
2529
+ const newMap = new Map(prev);
2530
+ return newMap.delete(oldTrackId), newMap;
2531
+ }), setAutogeneratedTrackIds((prev) => {
2532
+ const updated = new Set(prev);
2533
+ return updated.delete(oldTrackId), updated;
2534
+ })), addedTracks.some((t) => t.id === updatedTrack.id) ? setAddedTracks((prev) => prev.map((t) => t.id === updatedTrack.id ? updatedTrack : t)) : setUpdatedTracks((prev) => {
2535
+ const newMap = new Map(prev);
2536
+ return newMap.set(updatedTrack.id, updatedTrack), newMap;
2537
+ }), setTrackActivityOrder((prev) => {
2538
+ const newMap = new Map(prev);
2539
+ return newMap.set(updatedTrack.id, prev.size + 1), newMap;
2540
+ }), setTrackToEdit(null), asset._id && asset.assetId)
2541
+ try {
2542
+ const response = await getAsset(client, asset.assetId);
2543
+ await client.patch(asset._id).set({ data: response.data, status: response.data.status }).commit();
2544
+ } catch (refreshError) {
2545
+ console.error("Failed to refresh asset data:", refreshError);
2546
+ }
2547
+ }, getTrackSourceLabel = (track) => track.id && track.id.startsWith("generating-") || track.id && autogeneratedTrackIds.has(track.id) || track.text_source === "generated_live_final" || track.text_source === "generated_live" || track.text_source === "generated_vod" ? "Auto-generated" : track.text_source === "uploaded" ? "Uploaded" : "Custom";
2548
+ if (visibleTracks.length === 0 && !showAddDialog)
2549
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2550
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(
2551
+ Button,
2552
+ {
2553
+ icon: AddIcon,
2554
+ text: "Add Caption",
2555
+ tone: "primary",
2556
+ onClick: () => setShowAddDialog(!0)
2557
+ }
2558
+ ) }),
2559
+ /* @__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." }) }),
2560
+ showAddDialog && /* @__PURE__ */ jsx(
2561
+ AddCaptionDialog,
2562
+ {
2563
+ asset,
2564
+ onAdd: handleAddTrack,
2565
+ onClose: () => setShowAddDialog(!1)
2566
+ }
2567
+ )
2568
+ ] });
2569
+ const displayedTracks = collapseTracks && !isExpanded ? visibleTracks.slice(0, MAX_VISIBLE_TRACKS) : visibleTracks, hasMoreTracks = collapseTracks && visibleTracks.length > MAX_VISIBLE_TRACKS;
2570
+ return /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2571
+ /* @__PURE__ */ jsx(Flex, { justify: "flex-end", children: /* @__PURE__ */ jsx(
2572
+ Button,
2573
+ {
2574
+ icon: AddIcon,
2575
+ text: "Add Caption",
2576
+ tone: "primary",
2577
+ onClick: () => setShowAddDialog(!0)
2578
+ }
2579
+ ) }),
2580
+ displayedTracks.map((track) => /* @__PURE__ */ jsx(
2581
+ TrackCard,
2582
+ {
2583
+ track,
2584
+ iconOnly,
2585
+ downloadingTrackId,
2586
+ deletingTrackId,
2587
+ trackToEdit,
2588
+ getTrackSourceLabel,
2589
+ handleDownload,
2590
+ setTrackToEdit,
2591
+ setTrackToDelete
2592
+ },
2593
+ track.id
2594
+ )),
2595
+ hasMoreTracks && /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(
2596
+ Button,
2597
+ {
2598
+ icon: isExpanded ? ChevronUpIcon : ChevronDownIcon,
2599
+ text: isExpanded ? "Show less" : `Show ${visibleTracks.length - MAX_VISIBLE_TRACKS} more`,
2600
+ mode: "ghost",
2601
+ tone: "primary",
2602
+ onClick: () => setIsExpanded(!isExpanded)
2603
+ }
2604
+ ) }),
2605
+ trackToDelete && /* @__PURE__ */ jsx(
2606
+ Dialog,
2607
+ {
2608
+ animate: !0,
2609
+ id: dialogId,
2610
+ header: "Delete track",
2611
+ onClose: () => setTrackToDelete(null),
2612
+ onClickOutside: () => setTrackToDelete(null),
2613
+ width: 1,
2614
+ children: /* @__PURE__ */ jsx(
2615
+ Card,
2616
+ {
2617
+ padding: 3,
2618
+ style: {
2619
+ minHeight: "150px",
2620
+ display: "flex",
2621
+ alignItems: "center",
2622
+ justifyContent: "center"
2623
+ },
2624
+ children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
2625
+ /* @__PURE__ */ jsxs(Heading, { size: 2, children: [
2626
+ 'Are you sure you want to delete "',
2627
+ trackToDelete.name || trackToDelete.language_code || "Untitled",
2628
+ '"?'
2629
+ ] }),
2630
+ /* @__PURE__ */ jsx(Text, { size: 2, children: "This action is irreversible" }),
2631
+ /* @__PURE__ */ jsx(Stack, { space: 4, marginY: 4, children: /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
2632
+ Button,
2633
+ {
2634
+ icon: deletingTrackId === trackToDelete.id ? /* @__PURE__ */ jsx(
2635
+ Spinner,
2636
+ {
2637
+ style: {
2638
+ verticalAlign: "middle",
2639
+ display: "inline-block",
2640
+ marginTop: "-2px",
2641
+ width: "0.5em",
2642
+ height: "0.5em"
2643
+ }
2644
+ }
2645
+ ) : /* @__PURE__ */ jsx(TrashIcon, {}),
2646
+ fontSize: 2,
2647
+ padding: 3,
2648
+ text: "Delete track",
2649
+ tone: "critical",
2650
+ onClick: confirmDelete,
2651
+ disabled: deletingTrackId !== null
2652
+ }
2653
+ ) }) })
2654
+ ] })
2655
+ }
2656
+ )
2657
+ }
2658
+ ),
2659
+ showAddDialog && /* @__PURE__ */ jsx(
2660
+ AddCaptionDialog,
2661
+ {
2662
+ asset,
2663
+ onAdd: handleAddTrack,
2664
+ onClose: () => setShowAddDialog(!1)
2665
+ }
2666
+ ),
2667
+ trackToEdit && /* @__PURE__ */ jsx(
2668
+ EditCaptionDialog,
2669
+ {
2670
+ asset,
2671
+ track: trackToEdit,
2672
+ onUpdate: handleUpdateTrack,
2673
+ onClose: () => setTrackToEdit(null)
2674
+ }
2675
+ )
2676
+ ] });
1381
2677
  }
1382
2678
  const DialogStateContext = createContext({
1383
2679
  dialogState: !1,
@@ -1395,6 +2691,10 @@ function getVideoSrc({ asset, client }) {
1395
2691
  }
1396
2692
  return `https://stream.mux.com/${playbackId}.m3u8?${searchParams}`;
1397
2693
  }
2694
+ function CaptionsDialog({ asset }) {
2695
+ const { setDialogState } = useDialogStateContext(), dialogId = `CaptionsDialog${useId()}`;
2696
+ 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 }) }) });
2697
+ }
1398
2698
  function getDevicePixelRatio(options) {
1399
2699
  const {
1400
2700
  defaultDpr = 1,
@@ -1482,10 +2782,21 @@ function EditThumbnailDialog({ asset, currentTime = 0 }) {
1482
2782
  }
1483
2783
  );
1484
2784
  }
2785
+ function AudioIcon(props) {
2786
+ return /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 24 24", ...props, children: /* @__PURE__ */ jsx(
2787
+ "path",
2788
+ {
2789
+ fill: "currentColor",
2790
+ style: { opacity: "0.65" },
2791
+ d: "M10.75 19q.95 0 1.6-.65t.65-1.6V13h3v-2h-4v3.875q-.275-.2-.587-.288t-.663-.087q-.95 0-1.6.65t-.65 1.6t.65 1.6t1.6.65M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13V4H6v16h12V9zM6 4v5zv16z"
2792
+ }
2793
+ ) });
2794
+ }
1485
2795
  function VideoPlayer({
1486
2796
  asset,
1487
2797
  thumbnailWidth = 250,
1488
2798
  children,
2799
+ hlsConfig,
1489
2800
  ...props
1490
2801
  }) {
1491
2802
  const client = useClient(), { dialogState } = useDialogStateContext(), isAudio = assetIsAudio(asset), muxPlayer = useRef(null), {
@@ -1511,53 +2822,81 @@ function VideoPlayer({
1511
2822
  // Make it wider when forcing aspect ratio to balance with videos' rendering height (audio players overflow a bit)
1512
2823
  props.forceAspectRatio * 1.2
1513
2824
  ) : AUDIO_ASPECT_RATIO), /* @__PURE__ */ jsxs(Fragment, { children: [
1514
- /* @__PURE__ */ jsxs(Card, { tone: "transparent", style: { aspectRatio, position: "relative" }, children: [
1515
- videoSrc && /* @__PURE__ */ jsxs(Fragment, { children: [
1516
- /* @__PURE__ */ jsx(
1517
- MuxPlayer,
1518
- {
1519
- poster: thumbnailSrc,
1520
- ref: muxPlayer,
1521
- ...props,
1522
- playsInline: !0,
1523
- playbackId: asset.playbackId,
1524
- tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
1525
- preload: "metadata",
1526
- crossOrigin: "anonymous",
1527
- metadata: {
1528
- player_name: "Sanity Admin Dashboard",
1529
- player_version: "2.12.1",
1530
- page_type: "Preview Player"
1531
- },
1532
- audio: isAudio,
1533
- style: {
1534
- height: "100%",
1535
- width: "100%",
1536
- display: "block",
1537
- objectFit: "contain"
2825
+ /* @__PURE__ */ jsxs(
2826
+ Card,
2827
+ {
2828
+ tone: "transparent",
2829
+ style: {
2830
+ aspectRatio,
2831
+ position: "relative",
2832
+ ...isAudio && { display: "flex", alignItems: "flex-end" }
2833
+ },
2834
+ children: [
2835
+ videoSrc && /* @__PURE__ */ jsxs(Fragment, { children: [
2836
+ isAudio && /* @__PURE__ */ jsx(
2837
+ AudioIcon,
2838
+ {
2839
+ style: {
2840
+ padding: "0.5em",
2841
+ width: "2.2em",
2842
+ height: "2.2em",
2843
+ position: "absolute",
2844
+ top: 0,
2845
+ left: 0,
2846
+ zIndex: 1
2847
+ }
2848
+ }
2849
+ ),
2850
+ /* @__PURE__ */ jsx(
2851
+ MuxPlayer,
2852
+ {
2853
+ poster: isAudio ? void 0 : thumbnailSrc,
2854
+ ref: muxPlayer,
2855
+ ...props,
2856
+ playsInline: !0,
2857
+ playbackId: asset.playbackId,
2858
+ tokens: signedToken ? { playback: signedToken, thumbnail: signedToken, storyboard: signedToken } : void 0,
2859
+ preload: "metadata",
2860
+ crossOrigin: "anonymous",
2861
+ metadata: {
2862
+ player_name: "Sanity Admin Dashboard",
2863
+ player_version: "2.14.0",
2864
+ page_type: "Preview Player"
2865
+ },
2866
+ audio: isAudio,
2867
+ _hlsConfig: hlsConfig,
2868
+ style: {
2869
+ ...!isAudio && { height: "100%" },
2870
+ width: "100%",
2871
+ display: "block",
2872
+ objectFit: "contain",
2873
+ ...isAudio && { alignSelf: "end" }
2874
+ }
2875
+ }
2876
+ ),
2877
+ children
2878
+ ] }),
2879
+ error ? /* @__PURE__ */ jsx(
2880
+ "div",
2881
+ {
2882
+ style: {
2883
+ position: "absolute",
2884
+ top: "50%",
2885
+ left: "50%",
2886
+ transform: "translate(-50%, -50%)"
2887
+ },
2888
+ children: /* @__PURE__ */ jsxs(Text, { muted: !0, children: [
2889
+ /* @__PURE__ */ jsx(ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
2890
+ typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
2891
+ ] })
1538
2892
  }
1539
- }
1540
- ),
1541
- children
1542
- ] }),
1543
- error ? /* @__PURE__ */ jsx(
1544
- "div",
1545
- {
1546
- style: {
1547
- position: "absolute",
1548
- top: "50%",
1549
- left: "50%",
1550
- transform: "translate(-50%, -50%)"
1551
- },
1552
- children: /* @__PURE__ */ jsxs(Text, { muted: !0, children: [
1553
- /* @__PURE__ */ jsx(ErrorOutlineIcon, { style: { marginRight: "0.15em" } }),
1554
- typeof error == "object" && "message" in error && typeof error.message == "string" ? error.message : "Error loading video"
1555
- ] })
1556
- }
1557
- ) : null,
1558
- children
1559
- ] }),
1560
- dialogState === "edit-thumbnail" && /* @__PURE__ */ jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime })
2893
+ ) : null,
2894
+ children
2895
+ ]
2896
+ }
2897
+ ),
2898
+ dialogState === "edit-thumbnail" && /* @__PURE__ */ jsx(EditThumbnailDialog, { asset, currentTime: muxPlayer?.current?.currentTime }),
2899
+ dialogState === "edit-captions" && /* @__PURE__ */ jsx(CaptionsDialog, { asset })
1561
2900
  ] });
1562
2901
  }
1563
2902
  function assetIsAudio(asset) {
@@ -1829,7 +3168,8 @@ function getVideoMetadata(doc) {
1829
3168
  duration: doc.data?.duration ? formatSeconds(doc.data?.duration) : void 0,
1830
3169
  aspect_ratio: doc.data?.aspect_ratio,
1831
3170
  max_stored_resolution: doc.data?.max_stored_resolution,
1832
- max_stored_frame_rate: doc.data?.max_stored_frame_rate
3171
+ max_stored_frame_rate: doc.data?.max_stored_frame_rate,
3172
+ text_tracks: doc.data?.tracks?.filter((track) => track.type === "text") || []
1833
3173
  };
1834
3174
  }
1835
3175
  function useVideoDetails(props) {
@@ -2021,7 +3361,20 @@ const AssetInput = (props) => /* @__PURE__ */ jsx(FormField$1, { title: props.la
2021
3361
  minHeight: containerHeight
2022
3362
  } : void 0,
2023
3363
  children: [
2024
- /* @__PURE__ */ jsx(Stack, { space: 4, flex: 1, sizing: "border", children: /* @__PURE__ */ jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }) }),
3364
+ /* @__PURE__ */ jsxs(Stack, { space: 4, flex: 1, sizing: "border", children: [
3365
+ /* @__PURE__ */ jsx(VideoPlayer, { asset: props.asset, autoPlay: props.asset.autoPlay || !1 }),
3366
+ tab === "details" && /* @__PURE__ */ jsx(
3367
+ TextTracksManager,
3368
+ {
3369
+ asset: props.asset,
3370
+ iconOnly: !0,
3371
+ collapseTracks: !0,
3372
+ tracks: displayInfo?.text_tracks || props.asset.data?.tracks?.filter(
3373
+ (track) => track.type === "text"
3374
+ ) || []
3375
+ }
3376
+ )
3377
+ ] }),
2025
3378
  /* @__PURE__ */ jsxs(Stack, { space: 4, flex: 1, sizing: "border", children: [
2026
3379
  /* @__PURE__ */ jsxs(TabList, { space: 2, children: [
2027
3380
  /* @__PURE__ */ jsx(
@@ -2299,14 +3652,7 @@ function VideoInBrowser({
2299
3652
  alignItems: "center",
2300
3653
  justifyContent: "center"
2301
3654
  },
2302
- children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "3em", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
2303
- "path",
2304
- {
2305
- fill: "currentColor",
2306
- style: { opacity: "0.65" },
2307
- d: "M10.75 19q.95 0 1.6-.65t.65-1.6V13h3v-2h-4v3.875q-.275-.2-.587-.288t-.663-.087q-.95 0-1.6.65t-.65 1.6t.65 1.6t1.6.65M6 22q-.825 0-1.412-.587T4 20V4q0-.825.588-1.412T6 2h8l6 6v12q0 .825-.587 1.413T18 22zm7-13V4H6v16h12V9zM6 4v5zv16z"
2308
- }
2309
- ) })
3655
+ children: /* @__PURE__ */ jsx(AudioIcon, { width: "3em", height: "3em" })
2310
3656
  }
2311
3657
  ) : /* @__PURE__ */ jsx(VideoThumbnail, { asset })
2312
3658
  ] }),
@@ -2358,10 +3704,10 @@ function VideoInBrowser({
2358
3704
  );
2359
3705
  }
2360
3706
  function VideosBrowser({ onSelect }) {
2361
- const { assets, isLoading, searchQuery, setSearchQuery, setSort, sort } = useAssets(), [editedAsset, setEditedAsset] = useState(null), freshEditedAsset = useMemo(
3707
+ 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(
2362
3708
  () => assets.find((a2) => a2._id === editedAsset?._id) || editedAsset,
2363
3709
  [editedAsset, assets]
2364
- );
3710
+ ), pageStart = page * pageLimit, pageEnd = pageStart + pageLimit;
2365
3711
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2366
3712
  /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 4, style: { minHeight: "50vh" }, children: [
2367
3713
  /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
@@ -2375,7 +3721,8 @@ function VideosBrowser({ onSelect }) {
2375
3721
  placeholder: "Search videos"
2376
3722
  }
2377
3723
  ),
2378
- /* @__PURE__ */ jsx(SelectSortOptions, { setSort, sort })
3724
+ /* @__PURE__ */ jsx(SelectSortOptions, { setSort, sort }),
3725
+ /* @__PURE__ */ jsx(PageSelector, { page, setPage, total: pageTotal, limit: pageLimit })
2379
3726
  ] }),
2380
3727
  (onSelect ? "input" : "tool") == "tool" && /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
2381
3728
  /* @__PURE__ */ jsx(ImportVideosFromMux, {}),
@@ -2398,7 +3745,7 @@ function VideosBrowser({ onSelect }) {
2398
3745
  style: {
2399
3746
  gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))"
2400
3747
  },
2401
- children: assets.map((asset) => /* @__PURE__ */ jsx(
3748
+ children: assets.slice(pageStart, pageEnd).map((asset) => /* @__PURE__ */ jsx(
2402
3749
  VideoInBrowser,
2403
3750
  {
2404
3751
  asset,
@@ -2931,7 +4278,7 @@ const TopControls = styled.div`
2931
4278
  }
2932
4279
  ) : null
2933
4280
  ] }) });
2934
- }, Player = ({ asset, buttons, readOnly, onChange }) => {
4281
+ }, Player = ({ asset, buttons, readOnly, onChange, config }) => {
2935
4282
  const isLoading = useMemo(() => asset?.status === "preparing" ? "Preparing the video" : asset?.status === "waiting_for_upload" ? "Waiting for upload to start" : asset?.status === "waiting" ? "Processing upload" : !(asset?.status === "ready" || typeof asset?.status > "u"), [asset]), isPreparingStaticRenditions = useMemo(() => {
2936
4283
  if (asset?.data?.static_renditions?.status && asset?.data?.static_renditions?.status !== "disabled")
2937
4284
  return !1;
@@ -2952,7 +4299,7 @@ const TopControls = styled.div`
2952
4299
  text: isLoading !== !0 && isLoading || "Waiting for Mux to complete the upload",
2953
4300
  onCancel: readOnly ? void 0 : () => handleCancelUpload()
2954
4301
  }
2955
- ) : /* @__PURE__ */ jsxs(VideoPlayer, { asset, children: [
4302
+ ) : /* @__PURE__ */ jsxs(VideoPlayer, { asset, hlsConfig: config?.hlsConfig, children: [
2956
4303
  buttons && /* @__PURE__ */ jsx(TopControls, { slot: "top-chrome", children: buttons }),
2957
4304
  isPreparingStaticRenditions && /* @__PURE__ */ jsx(
2958
4305
  Card,
@@ -3064,7 +4411,7 @@ const FileButton = styled(MenuItem)(({ theme }) => {
3064
4411
  color: white;
3065
4412
  `, isVideoAsset = (asset) => asset._type === "mux.videoAsset";
3066
4413
  function PlayerActionsMenu(props) {
3067
- const { asset, readOnly, dialogState, setDialogState, onChange, onSelect } = 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]);
4414
+ const { asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept } = props, [open, setOpen] = useState(!1), [menuElement, setMenuRef] = useState(null), isSigned = useMemo(() => getPlaybackPolicy(asset) === "signed", [asset]), { hasConfigAccess } = useAccessControl(props.config), onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange]);
3068
4415
  return useEffect(() => {
3069
4416
  open && dialogState && setOpen(!1);
3070
4417
  }, [dialogState, open]), useClickOutsideEvent(
@@ -3090,7 +4437,7 @@ function PlayerActionsMenu(props) {
3090
4437
  /* @__PURE__ */ jsx(
3091
4438
  FileInputMenuItem,
3092
4439
  {
3093
- accept: "video/*",
4440
+ accept,
3094
4441
  icon: UploadIcon,
3095
4442
  onSelect,
3096
4443
  text: "Upload",
@@ -3106,14 +4453,24 @@ function PlayerActionsMenu(props) {
3106
4453
  onClick: () => setDialogState("select-video")
3107
4454
  }
3108
4455
  ),
3109
- isVideoAsset(asset) && /* @__PURE__ */ jsx(
3110
- MenuItem,
3111
- {
3112
- icon: ImageIcon,
3113
- text: "Thumbnail",
3114
- onClick: () => setDialogState("edit-thumbnail")
3115
- }
3116
- ),
4456
+ isVideoAsset(asset) && /* @__PURE__ */ jsxs(Fragment, { children: [
4457
+ /* @__PURE__ */ jsx(
4458
+ MenuItem,
4459
+ {
4460
+ icon: ImageIcon,
4461
+ text: "Thumbnail",
4462
+ onClick: () => setDialogState("edit-thumbnail")
4463
+ }
4464
+ ),
4465
+ /* @__PURE__ */ jsx(
4466
+ MenuItem,
4467
+ {
4468
+ icon: TranslateIcon,
4469
+ text: "Captions",
4470
+ onClick: () => setDialogState("edit-captions")
4471
+ }
4472
+ )
4473
+ ] }),
3117
4474
  /* @__PURE__ */ jsx(MenuDivider, {}),
3118
4475
  hasConfigAccess && /* @__PURE__ */ jsxs(Fragment, { children: [
3119
4476
  /* @__PURE__ */ jsx(
@@ -3167,36 +4524,6 @@ function formatBytes(bytes, si = !1, dp = 1) {
3167
4524
  while (Math.round(Math.abs(bytes) * r) / r >= thresh && u2 < units.length - 1);
3168
4525
  return bytes.toFixed(dp) + " " + units[u2];
3169
4526
  }
3170
- const SUPPORTED_MUX_LANGUAGES = [
3171
- { label: "English", code: "en", state: "Stable" },
3172
- { label: "Spanish", code: "es", state: "Stable" },
3173
- { label: "Italian", code: "it", state: "Stable" },
3174
- { label: "Portuguese", code: "pt", state: "Stable" },
3175
- { label: "German", code: "de", state: "Stable" },
3176
- { label: "French", code: "fr", state: "Stable" },
3177
- { label: "Polish", code: "pl", state: "Beta" },
3178
- { label: "Russian", code: "ru", state: "Beta" },
3179
- { label: "Dutch", code: "nl", state: "Beta" },
3180
- { label: "Catalan", code: "ca", state: "Beta" },
3181
- { label: "Turkish", code: "tr", state: "Beta" },
3182
- { label: "Swedish", code: "sv", state: "Beta" },
3183
- { label: "Ukrainian", code: "uk", state: "Beta" },
3184
- { label: "Norwegian", code: "no", state: "Beta" },
3185
- { label: "Finnish", code: "fi", state: "Beta" },
3186
- { label: "Slovak", code: "sk", state: "Beta" },
3187
- { label: "Greek", code: "el", state: "Beta" },
3188
- { label: "Czech", code: "cs", state: "Beta" },
3189
- { label: "Croatian", code: "hr", state: "Beta" },
3190
- { label: "Danish", code: "da", state: "Beta" },
3191
- { label: "Romanian", code: "ro", state: "Beta" },
3192
- { label: "Bulgarian", code: "bg", state: "Beta" }
3193
- ];
3194
- function isCustomTextTrack(track) {
3195
- return track.type !== "autogenerated";
3196
- }
3197
- function isAutogeneratedTrack(track) {
3198
- return track.type === "autogenerated";
3199
- }
3200
4527
  const ALL_LANGUAGE_CODES = LanguagesList.getAllCodes().map((code) => ({
3201
4528
  value: code,
3202
4529
  label: LanguagesList.getNativeName(code)
@@ -3454,7 +4781,47 @@ function UploadConfiguration({
3454
4781
  (r) => r !== "highest" && r !== "audio-only"
3455
4782
  ).length > 0, [config.static_renditions]), [renditionMode, setRenditionMode] = useState(
3456
4783
  isAdvancedMode ? "advanced" : "standard"
3457
- ), toggleRendition = (rendition) => {
4784
+ ), [videoDuration, setVideoDuration] = useState(null), [urlFileSize, setUrlFileSize] = useState(null), [isLoadingDuration, setIsLoadingDuration] = useState(!1), [isLoadingFileSize, setIsLoadingFileSize] = useState(!1), [validationError, setValidationError] = useState(null), [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = useState(!1), MAX_FILE_SIZE = pluginConfig.maxAssetFileSize, MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration;
4785
+ useEffect(() => {
4786
+ setVideoDuration(null), setUrlFileSize(null), setIsLoadingDuration(!1), setIsLoadingFileSize(!1), setValidationError(null), setCanSkipFileSizeValidation(!1);
4787
+ let videoElement = null, currentVideoSrc = null;
4788
+ const cleanupVideo = (shouldRevokeUrl) => {
4789
+ videoElement && (videoElement.onloadedmetadata = null, videoElement.onerror = null, videoElement.src = "", videoElement.load(), videoElement = null), shouldRevokeUrl && currentVideoSrc?.startsWith("blob:") && URL.revokeObjectURL(currentVideoSrc), currentVideoSrc = null;
4790
+ }, validateDuration = (videoSrc, shouldRevokeUrl = !1) => {
4791
+ !MAX_DURATION_SECONDS || MAX_DURATION_SECONDS <= 0 || (setIsLoadingDuration(!0), videoElement = document.createElement("video"), videoElement.preload = "metadata", currentVideoSrc = videoSrc, videoElement.onloadedmetadata = () => {
4792
+ const duration = videoElement.duration;
4793
+ setVideoDuration(duration), setIsLoadingDuration(!1), duration > MAX_DURATION_SECONDS && setValidationError(
4794
+ `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
4795
+ ), cleanupVideo(shouldRevokeUrl);
4796
+ }, videoElement.onerror = () => {
4797
+ setIsLoadingDuration(!1), console.warn("Could not read video metadata for validation"), cleanupVideo(shouldRevokeUrl);
4798
+ }, videoElement.src = videoSrc);
4799
+ }, validateFileSize = (size) => MAX_FILE_SIZE === void 0 || size <= MAX_FILE_SIZE ? !0 : (setValidationError(
4800
+ `File size (${formatBytes(size)}) exceeds maximum allowed size of ${formatBytes(MAX_FILE_SIZE)}`
4801
+ ), !1);
4802
+ if (stagedUpload.type === "file") {
4803
+ const file = stagedUpload.files[0];
4804
+ validateFileSize(file.size) && validateDuration(URL.createObjectURL(file), !0);
4805
+ }
4806
+ if (stagedUpload.type === "url") {
4807
+ const url = stagedUpload.url;
4808
+ (async () => {
4809
+ setIsLoadingFileSize(!0);
4810
+ try {
4811
+ const contentLength = (await fetch(url, { method: "HEAD" })).headers.get("content-length"), fileSize = contentLength ? parseInt(contentLength, 10) : null;
4812
+ setIsLoadingFileSize(!1), fileSize && setUrlFileSize(fileSize);
4813
+ const shouldValidateDuration = MAX_FILE_SIZE === void 0 || fileSize === null || validateFileSize(fileSize);
4814
+ fileSize === null && MAX_FILE_SIZE !== void 0 && setCanSkipFileSizeValidation(!0), shouldValidateDuration && validateDuration(url);
4815
+ } catch {
4816
+ setIsLoadingFileSize(!1), console.warn("Could not validate file size from URL"), setCanSkipFileSizeValidation(!0), validateDuration(url);
4817
+ }
4818
+ })();
4819
+ }
4820
+ return () => {
4821
+ cleanupVideo(!0);
4822
+ };
4823
+ }, [stagedUpload, MAX_FILE_SIZE, MAX_DURATION_SECONDS]);
4824
+ const toggleRendition = (rendition) => {
3458
4825
  const current = config.static_renditions, hasRendition = current.includes(rendition);
3459
4826
  dispatch(hasRendition ? {
3460
4827
  action: "static_renditions",
@@ -3489,6 +4856,13 @@ function UploadConfiguration({
3489
4856
  header: "Configure Mux Upload",
3490
4857
  onClose,
3491
4858
  children: /* @__PURE__ */ jsxs(Stack, { padding: 4, space: 2, children: [
4859
+ validationError && /* @__PURE__ */ jsx(Card, { padding: 3, tone: "critical", radius: 2, marginBottom: 2, children: /* @__PURE__ */ jsxs(Flex, { gap: 2, align: "flex-start", children: [
4860
+ /* @__PURE__ */ jsx(ErrorOutlineIcon, { width: 20, height: 20 }),
4861
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
4862
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: "Validation Error" }),
4863
+ /* @__PURE__ */ jsx(Text, { size: 1, children: validationError })
4864
+ ] })
4865
+ ] }) }),
3492
4866
  /* @__PURE__ */ jsx(Label$1, { size: 3, children: "FILE TO UPLOAD" }),
3493
4867
  /* @__PURE__ */ jsx(
3494
4868
  Card,
@@ -3502,7 +4876,14 @@ function UploadConfiguration({
3502
4876
  /* @__PURE__ */ jsx(DocumentVideoIcon, { fontSize: "2em" }),
3503
4877
  /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
3504
4878
  /* @__PURE__ */ jsx(Text, { textOverflow: "ellipsis", as: "h2", size: 3, children: stagedUpload.type === "file" ? stagedUpload.files[0].name : stagedUpload.url }),
3505
- /* @__PURE__ */ jsx(Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : "File From URL (Unknown size)" })
4879
+ /* @__PURE__ */ jsx(Text, { as: "p", size: 1, muted: !0, children: stagedUpload.type === "file" ? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})` : urlFileSize ? `File From URL (${formatBytes(urlFileSize)})` : isLoadingFileSize ? "File From URL (Loading size...)" : "File From URL (Unknown size)" }),
4880
+ stagedUpload.type === "file" && /* @__PURE__ */ jsxs(Stack, { space: 1, children: [
4881
+ isLoadingDuration && /* @__PURE__ */ jsx(Text, { as: "p", size: 1, muted: !0, children: "Reading video metadata..." }),
4882
+ videoDuration !== null && !validationError && /* @__PURE__ */ jsxs(Text, { as: "p", size: 1, muted: !0, children: [
4883
+ "Duration: ",
4884
+ formatSeconds(videoDuration)
4885
+ ] })
4886
+ ] })
3506
4887
  ] })
3507
4888
  ] })
3508
4889
  }
@@ -3694,11 +5075,13 @@ function UploadConfiguration({
3694
5075
  /* @__PURE__ */ jsx(Box, { marginTop: 4, children: /* @__PURE__ */ jsx(
3695
5076
  Button,
3696
5077
  {
3697
- disabled: !basicConfig && !config.public_policy && !config.signed_policy,
5078
+ disabled: !basicConfig && !config.public_policy && !config.signed_policy || validationError !== null || isLoadingDuration || isLoadingFileSize && !canSkipFileSizeValidation,
3698
5079
  icon: UploadIcon,
3699
5080
  text: "Upload",
3700
5081
  tone: "positive",
3701
- onClick: () => startUpload(formatUploadConfig(config))
5082
+ onClick: () => {
5083
+ validationError || startUpload(formatUploadConfig(config));
5084
+ }
3702
5085
  }
3703
5086
  ) })
3704
5087
  ] })
@@ -3762,12 +5145,10 @@ function withFocusRing(component) {
3762
5145
  `;
3763
5146
  });
3764
5147
  }
3765
- const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card), UploadCard = forwardRef(
5148
+ const UploadCardWithFocusRing = withFocusRing(Card), UploadCard = forwardRef(
3766
5149
  ({ children, tone, onPaste, onDrop, onDragEnter, onDragLeave, onDragOver }, forwardedRef) => {
3767
- const ctrlDown = useRef(!1), inputRef = useRef(null), handleKeyDown = useCallback((event) => {
3768
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !0), ctrlDown.current && event.keyCode == 86 && inputRef.current.focus();
3769
- }, []), handleKeyUp = useCallback((event) => {
3770
- (event.keyCode == ctrlKey || event.keyCode == cmdKey) && (ctrlDown.current = !1);
5150
+ const inputRef = useRef(null), handleKeyDown = useCallback((event) => {
5151
+ event.target.closest("#vtt-url") || (event.ctrlKey || event.metaKey) && event.key === "v" && inputRef.current.focus();
3771
5152
  }, []);
3772
5153
  return /* @__PURE__ */ jsxs(
3773
5154
  UploadCardWithFocusRing,
@@ -3779,14 +5160,13 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card),
3779
5160
  shadow: 0,
3780
5161
  tabIndex: 0,
3781
5162
  onKeyDown: handleKeyDown,
3782
- onKeyUp: handleKeyUp,
3783
5163
  onPaste,
3784
5164
  onDrop,
3785
5165
  onDragEnter,
3786
5166
  onDragLeave,
3787
5167
  onDragOver,
3788
5168
  children: [
3789
- /* @__PURE__ */ jsx(HiddenInput$1, { ref: inputRef, onPaste }),
5169
+ /* @__PURE__ */ jsx(HiddenInput$1, { ref: inputRef }),
3790
5170
  children
3791
5171
  ]
3792
5172
  }
@@ -3821,7 +5201,7 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card),
3821
5201
  /* @__PURE__ */ jsx(
3822
5202
  HiddenInput,
3823
5203
  {
3824
- accept: accept || "video/*",
5204
+ accept,
3825
5205
  ref: inputRef,
3826
5206
  tabIndex: 0,
3827
5207
  type: "file",
@@ -3842,8 +5222,11 @@ const ctrlKey = 17, cmdKey = 91, UploadCardWithFocusRing = withFocusRing(Card),
3842
5222
  )
3843
5223
  ] });
3844
5224
  };
5225
+ function formatAcceptString(accept) {
5226
+ return accept.split(",").map((type) => type.trim().replace("/*", "")).join(" or ");
5227
+ }
3845
5228
  function UploadPlaceholder(props) {
3846
- const { setDialogState, readOnly, onSelect, hovering, needsSetup } = props, handleBrowse = useCallback(() => setDialogState("select-video"), [setDialogState]), handleConfigureApi = useCallback(() => setDialogState("secrets"), [setDialogState]), { hasConfigAccess } = useAccessControl(props.config);
5229
+ const { setDialogState, readOnly, onSelect, hovering, needsSetup, accept } = props, handleBrowse = useCallback(() => setDialogState("select-video"), [setDialogState]), handleConfigureApi = useCallback(() => setDialogState("secrets"), [setDialogState]), { hasConfigAccess } = useAccessControl(props.config);
3847
5230
  return /* @__PURE__ */ jsx(
3848
5231
  Card,
3849
5232
  {
@@ -3866,12 +5249,17 @@ function UploadPlaceholder(props) {
3866
5249
  children: [
3867
5250
  /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "flex-start", gap: 2, flex: 1, children: [
3868
5251
  /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(Text, { muted: !0, children: /* @__PURE__ */ jsx(DocumentVideoIcon, {}) }) }),
3869
- /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Drag video or paste URL here" }) })
5252
+ /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
5253
+ "Drag ",
5254
+ formatAcceptString(accept),
5255
+ " file or paste URL here"
5256
+ ] }) })
3870
5257
  ] }),
3871
5258
  /* @__PURE__ */ jsxs(Inline, { space: 2, children: [
3872
5259
  /* @__PURE__ */ jsx(
3873
5260
  FileInputButton,
3874
5261
  {
5262
+ accept,
3875
5263
  mode: "bleed",
3876
5264
  tone: "default",
3877
5265
  icon: UploadIcon,
@@ -4022,21 +5410,35 @@ function Uploader(props) {
4022
5410
  complete: () => dispatch({ action: "complete" }),
4023
5411
  error: (error) => dispatch({ action: "error", error })
4024
5412
  });
4025
- }, handleUpload = (files) => {
4026
- dispatch({
5413
+ }, invalidFileToast = useCallback(() => {
5414
+ toast.push({
5415
+ status: "error",
5416
+ title: `Invalid file type. Accepted types: ${props.config.acceptedMimeTypes?.join(", ")}`
5417
+ });
5418
+ }, [props.config.acceptedMimeTypes, toast]), isInvalidFile = (files) => Array.from(files).some((file) => !props.config.acceptedMimeTypes?.some((acceptedType) => {
5419
+ const pattern = `^${acceptedType.replace("*", ".*")}$`;
5420
+ return new RegExp(pattern).test(file.type);
5421
+ })), handleUpload = (files) => {
5422
+ isInvalidFile(files) || dispatch({
4027
5423
  action: "stageUpload",
4028
5424
  input: { type: "file", files }
4029
5425
  });
4030
5426
  }, handlePaste = (event) => {
5427
+ if (event.target.closest("#vtt-url"))
5428
+ return;
4031
5429
  event.preventDefault(), event.stopPropagation();
4032
- const url = (event.clipboardData || window.clipboardData).getData("text")?.trim();
5430
+ const url = (event.clipboardData || window.clipboardData)?.getData("text")?.trim();
4033
5431
  if (!isValidUrl(url)) {
4034
5432
  toast.push({ status: "error", title: "Invalid URL for Mux video input." });
4035
5433
  return;
4036
5434
  }
4037
5435
  dispatch({ action: "stageUpload", input: { type: "url", url } });
4038
5436
  }, handleDrop = (event) => {
4039
- setDragState(null), event.preventDefault(), event.stopPropagation(), extractDroppedFiles(event.nativeEvent.dataTransfer).then((files) => {
5437
+ if (event.preventDefault(), event.stopPropagation(), dragState === "invalid") {
5438
+ invalidFileToast(), setDragState(null);
5439
+ return;
5440
+ }
5441
+ setDragState(null), extractDroppedFiles(event.nativeEvent.dataTransfer).then((files) => {
4040
5442
  dispatch({
4041
5443
  action: "stageUpload",
4042
5444
  input: { type: "file", files }
@@ -4046,8 +5448,11 @@ function Uploader(props) {
4046
5448
  event.preventDefault(), event.stopPropagation();
4047
5449
  }, handleDragEnter = (event) => {
4048
5450
  event.stopPropagation(), dragEnteredEls.current.push(event.target);
4049
- const type = event.dataTransfer.items?.[0]?.type;
4050
- setDragState(type?.startsWith("video/") ? "valid" : "invalid");
5451
+ const type = event.dataTransfer.items?.[0]?.type, isValidType = props.config.acceptedMimeTypes?.some((acceptedType) => {
5452
+ const pattern = `^${acceptedType.replace("*", ".*")}$`;
5453
+ return new RegExp(pattern).test(type);
5454
+ });
5455
+ setDragState(isValidType ? "valid" : "invalid");
4051
5456
  }, handleDragLeave = (event) => {
4052
5457
  event.stopPropagation();
4053
5458
  const idx = dragEnteredEls.current.indexOf(event.target);
@@ -4085,7 +5490,9 @@ function Uploader(props) {
4085
5490
  }
4086
5491
  );
4087
5492
  let tone;
4088
- return dragState && (tone = dragState === "valid" ? "positive" : "critical"), /* @__PURE__ */ jsxs(Fragment, { children: [
5493
+ dragState && (tone = dragState === "valid" ? "positive" : "critical");
5494
+ const acceptMimeString = props.config?.acceptedMimeTypes?.length ? props.config.acceptedMimeTypes.join(",") : "video/*, audio/*";
5495
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4089
5496
  /* @__PURE__ */ jsx(
4090
5497
  UploadCard,
4091
5498
  {
@@ -4107,9 +5514,11 @@ function Uploader(props) {
4107
5514
  readOnly: props.readOnly,
4108
5515
  asset: props.asset,
4109
5516
  onChange: props.onChange,
5517
+ config: props.config,
4110
5518
  buttons: /* @__PURE__ */ jsx(
4111
5519
  PlayerActionsMenu$1,
4112
5520
  {
5521
+ accept: acceptMimeString,
4113
5522
  asset: props.asset,
4114
5523
  dialogState: props.dialogState,
4115
5524
  setDialogState: props.setDialogState,
@@ -4125,6 +5534,7 @@ function Uploader(props) {
4125
5534
  ) : /* @__PURE__ */ jsx(
4126
5535
  UploadPlaceholder,
4127
5536
  {
5537
+ accept: acceptMimeString,
4128
5538
  hovering: dragState !== null,
4129
5539
  onSelect: handleUpload,
4130
5540
  readOnly: !!props.readOnly,
@@ -4385,7 +5795,8 @@ const muxVideoSchema = {
4385
5795
  defaultPublic: !0,
4386
5796
  defaultSigned: !1,
4387
5797
  tool: DEFAULT_TOOL_CONFIG,
4388
- allowedRolesForConfiguration: []
5798
+ allowedRolesForConfiguration: [],
5799
+ acceptedMimeTypes: ["video/*", "audio/*"]
4389
5800
  };
4390
5801
  function convertLegacyConfig(config) {
4391
5802
  return config.static_renditions && config.static_renditions.length > 0 ? { static_renditions: config.static_renditions } : config.mp4_support === "standard" ? { static_renditions: ["highest"] } : { static_renditions: [] };