@layers-app/editor-video 0.1.6 → 0.1.7

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 (29) hide show
  1. package/dist/index.css +17 -3
  2. package/dist/index.js +800 -545
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugin/behaviour.d.ts.map +1 -1
  5. package/dist/types/index.d.ts +1 -0
  6. package/dist/types/index.d.ts.map +1 -1
  7. package/dist/ui/VideoBlock.d.ts.map +1 -1
  8. package/dist/ui/VideoUploadComponent.d.ts.map +1 -1
  9. package/dist/ui/components/VideoCustomControls.d.ts.map +1 -1
  10. package/dist/ui/components/VideoSettingsModal/ChaptersSection.d.ts +3 -1
  11. package/dist/ui/components/VideoSettingsModal/ChaptersSection.d.ts.map +1 -1
  12. package/dist/ui/components/VideoSettingsModal/CoverSection.d.ts.map +1 -1
  13. package/dist/ui/components/VideoSettingsModal/FooterActions.d.ts +2 -1
  14. package/dist/ui/components/VideoSettingsModal/FooterActions.d.ts.map +1 -1
  15. package/dist/ui/components/VideoSettingsModal/ManualSubtitlesPanel.d.ts +2 -1
  16. package/dist/ui/components/VideoSettingsModal/ManualSubtitlesPanel.d.ts.map +1 -1
  17. package/dist/ui/components/VideoSettingsModal/SubtitlesSection.d.ts.map +1 -1
  18. package/dist/ui/components/VideoSettingsModal/index.d.ts +2 -1
  19. package/dist/ui/components/VideoSettingsModal/index.d.ts.map +1 -1
  20. package/dist/ui/components/VideoSubtitlesMenu/index.d.ts.map +1 -1
  21. package/dist/ui/hooks/useVideoTranscoding.d.ts.map +1 -1
  22. package/dist/ui/hooks/useVideoUpload.d.ts.map +1 -1
  23. package/dist/ui/states/UploadState.d.ts.map +1 -1
  24. package/dist/ui/states/VideoPlayerState.d.ts +3 -1
  25. package/dist/ui/states/VideoPlayerState.d.ts.map +1 -1
  26. package/dist/ui/utils/api.d.ts.map +1 -1
  27. package/dist/ui/utils/videoSubtitlesApi.d.ts +5 -0
  28. package/dist/ui/utils/videoSubtitlesApi.d.ts.map +1 -1
  29. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
5
  import { createContext, useState, useRef, useCallback, useMemo, useContext, forwardRef, createElement, useEffect } from "react";
6
6
  import { createCommand, $getNodeByKey, $setSelection, COMMAND_PRIORITY_LOW, DecoratorNode } from "lexical";
7
7
  import { useTranslation } from "react-i18next";
8
- import { Tooltip, ActionIcon, Text, Progress, Paper, Button, TextInput, Menu, Box, Slider, Flex, Select, Stack, Divider, Group, Textarea, Loader, Modal } from "@mantine/core";
8
+ import { Tooltip, ActionIcon, Text, Progress, Paper, Button, TextInput, Menu, Box, Slider, Flex, Select, Stack, Divider, Group, Textarea, Loader, Switch, Modal } from "@mantine/core";
9
9
  import { BlockWithAlignableContents } from "@lexical/react/LexicalBlockWithAlignableContents";
10
10
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
11
11
  import { Dropzone as Dropzone$1, IMAGE_MIME_TYPE } from "@mantine/dropzone";
@@ -328,25 +328,34 @@ function setVideoSize(editor, nodeKey, width, height) {
328
328
  });
329
329
  }
330
330
  function setVideoPoster(editor, nodeKey, posterUrl) {
331
- editor.update(() => {
332
- const node = $getNodeByKey(nodeKey);
333
- if (!node || !$isVideoNode(node)) return;
334
- node.setPosterUrl(posterUrl || "");
335
- });
331
+ editor.update(
332
+ () => {
333
+ const node = $getNodeByKey(nodeKey);
334
+ if (!node || !$isVideoNode(node)) return;
335
+ node.setPosterUrl(posterUrl || "");
336
+ },
337
+ { tag: "skip-scroll-into-view" }
338
+ );
336
339
  }
337
340
  function setVideoPrimaryUrl(editor, nodeKey, primaryUrl) {
338
- editor.update(() => {
339
- const node = $getNodeByKey(nodeKey);
340
- if (!node || !$isVideoNode(node)) return;
341
- node.setUrl(primaryUrl);
342
- });
341
+ editor.update(
342
+ () => {
343
+ const node = $getNodeByKey(nodeKey);
344
+ if (!node || !$isVideoNode(node)) return;
345
+ node.setUrl(primaryUrl);
346
+ },
347
+ { tag: "skip-scroll-into-view" }
348
+ );
343
349
  }
344
350
  function setVideoChaptersDisabled(editor, nodeKey, disabled2) {
345
- editor.update(() => {
346
- const node = $getNodeByKey(nodeKey);
347
- if (!node || !$isVideoNode(node)) return;
348
- node.setChaptersDisabled(disabled2);
349
- });
351
+ editor.update(
352
+ () => {
353
+ const node = $getNodeByKey(nodeKey);
354
+ if (!node || !$isVideoNode(node)) return;
355
+ node.setChaptersDisabled(disabled2);
356
+ },
357
+ { tag: "skip-scroll-into-view" }
358
+ );
350
359
  }
351
360
  function removeVideoNode(editor, nodeKey, deleteNode) {
352
361
  deleteNode(editor, nodeKey);
@@ -854,11 +863,6 @@ function useVideoTranscoding({
854
863
  const now = Date.now();
855
864
  if (!procStallReportedRef.current && lastProcChangeAtRef.current > 0 && now - lastProcChangeAtRef.current > STALL_TIMEOUT_MS$1) {
856
865
  procStallReportedRef.current = true;
857
- console.warn("Processing stall detected", {
858
- stage: proc.stage,
859
- percent,
860
- stalledMs: now - lastProcChangeAtRef.current
861
- });
862
866
  }
863
867
  const monotonicPercent = Math.max(maxPercentRef.current, percent);
864
868
  maxPercentRef.current = monotonicPercent;
@@ -955,7 +959,26 @@ function useCancelUpload() {
955
959
  const DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
956
960
  const STALL_TIMEOUT_MS = 3e4;
957
961
  const MAX_CONSECUTIVE_CONFLICTS = 3;
962
+ const MAX_RETRIES_PER_CHUNK = 3;
963
+ const MAX_TOTAL_RETRIES = 15;
964
+ const RETRY_BASE_DELAY_MS = 3e3;
965
+ function abortableSleep(ms, signal) {
966
+ return new Promise((resolve) => {
967
+ if (signal == null ? void 0 : signal.aborted) {
968
+ resolve();
969
+ return;
970
+ }
971
+ const id = setTimeout(resolve, ms);
972
+ signal == null ? void 0 : signal.addEventListener("abort", () => {
973
+ clearTimeout(id);
974
+ resolve();
975
+ }, { once: true });
976
+ });
977
+ }
958
978
  function getBaseUrl() {
979
+ if (typeof window !== "undefined" && window.location.hostname === "dev-app.layers.md") {
980
+ return "https://api.layers.md";
981
+ }
959
982
  return "";
960
983
  }
961
984
  function getAuthHeaders$2() {
@@ -971,10 +994,13 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
971
994
  onXhrReady(xhr);
972
995
  }
973
996
  if (signal) {
997
+ if (signal.aborted) {
998
+ reject(new Error("Upload cancelled"));
999
+ return;
1000
+ }
974
1001
  signal.addEventListener("abort", () => {
975
1002
  xhr.abort();
976
- reject(new Error("Upload cancelled"));
977
- });
1003
+ }, { once: true });
978
1004
  }
979
1005
  xhr.upload.addEventListener("progress", (e) => {
980
1006
  if (e.lengthComputable && onProgress) {
@@ -987,18 +1013,30 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
987
1013
  const contentType = xhr.getResponseHeader("content-type") || "";
988
1014
  const isJson = contentType.includes("application/json");
989
1015
  const data = isJson ? JSON.parse(xhr.responseText) : xhr.responseText;
990
- if (xhr.status === 409 && data && typeof data === "object" && "expectedOffset" in data) {
991
- resolve({
992
- expectedOffset: data.expectedOffset
993
- });
994
- return;
995
- }
996
1016
  resolve(
997
1017
  typeof data === "object" && data !== null ? data : {}
998
1018
  );
999
1019
  } catch (error) {
1000
1020
  reject(error);
1001
1021
  }
1022
+ } else if (xhr.status === 409) {
1023
+ try {
1024
+ const contentType = xhr.getResponseHeader("content-type") || "";
1025
+ const isJson = contentType.includes("application/json");
1026
+ const data = isJson ? JSON.parse(xhr.responseText) : {};
1027
+ if (data && typeof data === "object" && "expectedOffset" in data) {
1028
+ resolve({
1029
+ expectedOffset: data.expectedOffset
1030
+ });
1031
+ return;
1032
+ }
1033
+ } catch {
1034
+ }
1035
+ reject({
1036
+ status: xhr.status,
1037
+ statusText: xhr.statusText,
1038
+ data: { message: "Upload conflict" }
1039
+ });
1002
1040
  } else {
1003
1041
  let errorData = {};
1004
1042
  try {
@@ -1035,9 +1073,7 @@ async function uploadChunkWithProgress(videoId, file, offset, end, total, baseUr
1035
1073
  Object.keys(headers).forEach((key) => {
1036
1074
  xhr.setRequestHeader(key, headers[key]);
1037
1075
  });
1038
- if (!baseUrl) {
1039
- xhr.withCredentials = true;
1040
- }
1076
+ xhr.withCredentials = true;
1041
1077
  xhr.send(blob);
1042
1078
  });
1043
1079
  }
@@ -1073,13 +1109,16 @@ function useVideoUpload({
1073
1109
  isPaused: false,
1074
1110
  isPausing: false,
1075
1111
  currentXhr: null,
1076
- uploadSession: 0
1112
+ chunkAbortController: null,
1113
+ uploadSession: 0,
1114
+ cancelled: false
1077
1115
  });
1078
1116
  const progressIntervalRef = useRef(
1079
1117
  null
1080
1118
  );
1081
1119
  const speedHistoryRef = useRef([]);
1082
1120
  const lastDisplayedRemainingRef = useRef(0);
1121
+ const maxDisplayedPercentRef = useRef(0);
1083
1122
  const MAX_SPEED_HISTORY = 20;
1084
1123
  const EMA_ALPHA = 0.15;
1085
1124
  const emaSpeedRef = useRef(null);
@@ -1096,6 +1135,11 @@ function useVideoUpload({
1096
1135
  }
1097
1136
  const timeDiff = (now - state.lastTime) / 1e3;
1098
1137
  const uploadedDiff = uploaded - state.lastBytes;
1138
+ if (uploadedDiff < 0) {
1139
+ state.lastBytes = uploaded;
1140
+ state.lastTime = now;
1141
+ return { speed: emaSpeedRef.current ?? 0, remaining: lastDisplayedRemainingRef.current };
1142
+ }
1099
1143
  if (timeDiff > 0 && !state.isPaused) {
1100
1144
  const instantSpeed = Math.max(0, uploadedDiff / timeDiff);
1101
1145
  speedHistoryRef.current.push(instantSpeed);
@@ -1134,10 +1178,12 @@ function useVideoUpload({
1134
1178
  (uploaded, total) => {
1135
1179
  const now = Date.now();
1136
1180
  const { speed, remaining } = calculateSpeed(uploaded, total, now);
1137
- const percent = Math.floor(uploaded * 100 / total);
1181
+ const rawPercent = Math.floor(uploaded * 100 / total);
1182
+ const percent = Math.max(rawPercent, maxDisplayedPercentRef.current);
1183
+ maxDisplayedPercentRef.current = percent;
1138
1184
  setUploadProgress({
1139
1185
  percent,
1140
- uploaded,
1186
+ uploaded: Math.max(uploaded, total * percent / 100),
1141
1187
  total,
1142
1188
  speed,
1143
1189
  remaining
@@ -1147,7 +1193,7 @@ function useVideoUpload({
1147
1193
  );
1148
1194
  const uploadFileChunks = useCallback(
1149
1195
  async (file, videoId, startOffset = 0) => {
1150
- var _a, _b, _c;
1196
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1151
1197
  const state = uploadStateRef.current;
1152
1198
  const total = file.size;
1153
1199
  let offset = Math.max(0, startOffset);
@@ -1170,6 +1216,8 @@ function useVideoUpload({
1170
1216
  }
1171
1217
  }, 1e3);
1172
1218
  let consecutiveConflicts = 0;
1219
+ let chunkRetries = 0;
1220
+ let totalRetries = 0;
1173
1221
  let hasSuccessfulChunk = false;
1174
1222
  if (offset > 0) {
1175
1223
  updateProgress(offset, total);
@@ -1177,7 +1225,7 @@ function useVideoUpload({
1177
1225
  updateProgress(0, total);
1178
1226
  }
1179
1227
  while (offset < total) {
1180
- if ((_a = state.signal) == null ? void 0 : _a.aborted) {
1228
+ if (state.cancelled || ((_a = state.signal) == null ? void 0 : _a.aborted)) {
1181
1229
  throw new Error("Upload cancelled");
1182
1230
  }
1183
1231
  if (state.isPaused) {
@@ -1192,13 +1240,17 @@ function useVideoUpload({
1192
1240
  const nowStall = Date.now();
1193
1241
  if (!state.uploadStallReported && nowStall - state.lastUploadAckAt > STALL_TIMEOUT_MS) {
1194
1242
  state.uploadStallReported = true;
1195
- console.warn("Upload stall detected", {
1196
- atOffset: offset,
1197
- stalledMs: nowStall - state.lastUploadAckAt
1198
- });
1199
1243
  }
1200
1244
  const end = Math.min(total - 1, offset + chunkSize - 1);
1201
1245
  const chunkStartOffset = offset;
1246
+ const chunkAc = new AbortController();
1247
+ state.chunkAbortController = chunkAc;
1248
+ const forwardAbort = () => chunkAc.abort();
1249
+ if (((_b = state.signal) == null ? void 0 : _b.aborted) || state.cancelled) {
1250
+ chunkAc.abort();
1251
+ } else if (state.signal) {
1252
+ state.signal.addEventListener("abort", forwardAbort, { once: true });
1253
+ }
1202
1254
  try {
1203
1255
  const response = await uploadChunkWithProgress(
1204
1256
  videoId,
@@ -1208,7 +1260,7 @@ function useVideoUpload({
1208
1260
  total,
1209
1261
  baseUrl,
1210
1262
  authHeaders,
1211
- state.signal || void 0,
1263
+ chunkAc.signal,
1212
1264
  (loaded) => {
1213
1265
  if (state.isPaused || state.isPausing) return;
1214
1266
  const estimatedOffset = chunkStartOffset + loaded;
@@ -1220,13 +1272,11 @@ function useVideoUpload({
1220
1272
  state.currentXhr = xhr;
1221
1273
  }
1222
1274
  );
1275
+ state.chunkAbortController = null;
1276
+ state.currentXhr = null;
1277
+ (_c = state.signal) == null ? void 0 : _c.removeEventListener("abort", forwardAbort);
1223
1278
  if (response.expectedOffset != null) {
1224
1279
  consecutiveConflicts++;
1225
- console.warn("Offset mismatch detected", {
1226
- expectedOffset: response.expectedOffset,
1227
- hadOffset: offset,
1228
- attempt: consecutiveConflicts
1229
- });
1230
1280
  if (consecutiveConflicts >= MAX_CONSECUTIVE_CONFLICTS) {
1231
1281
  throw {
1232
1282
  status: 409,
@@ -1238,10 +1288,14 @@ function useVideoUpload({
1238
1288
  continue;
1239
1289
  }
1240
1290
  consecutiveConflicts = 0;
1291
+ chunkRetries = 0;
1241
1292
  hasSuccessfulChunk = true;
1242
1293
  offset = response.nextOffset != null ? response.nextOffset : end + 1;
1294
+ if (offset < total) {
1295
+ await abortableSleep(500, state.signal ?? void 0);
1296
+ }
1243
1297
  state.currentOffset = offset;
1244
- state.currentXhr = null;
1298
+ state.chunkAbortController = null;
1245
1299
  state.lastUploadAckAt = Date.now();
1246
1300
  state.uploadStallReported = false;
1247
1301
  if (!state.isPaused && !state.isPausing) {
@@ -1255,26 +1309,54 @@ function useVideoUpload({
1255
1309
  }
1256
1310
  } catch (error) {
1257
1311
  state.currentXhr = null;
1312
+ state.chunkAbortController = null;
1313
+ (_d = state.signal) == null ? void 0 : _d.removeEventListener("abort", forwardAbort);
1258
1314
  if (state.isPaused || state.isPausing) {
1259
1315
  state.isPausing = false;
1316
+ state.isPaused = true;
1260
1317
  break;
1261
1318
  }
1262
- if ((error == null ? void 0 : error.name) === "AbortError" || ((_b = state.signal) == null ? void 0 : _b.aborted)) {
1319
+ if ((error == null ? void 0 : error.name) === "AbortError" || ((_e = state.signal) == null ? void 0 : _e.aborted) || state.cancelled) {
1263
1320
  throw new Error("Upload cancelled");
1264
1321
  }
1265
- if ((error == null ? void 0 : error.status) === 409 && ((_c = error == null ? void 0 : error.data) == null ? void 0 : _c.expectedOffset) != null) {
1322
+ if ((error == null ? void 0 : error.status) === 409 && ((_f = error == null ? void 0 : error.data) == null ? void 0 : _f.expectedOffset) != null) {
1266
1323
  consecutiveConflicts++;
1267
- console.warn("Offset conflict in error handler", {
1268
- expectedOffset: error.data.expectedOffset,
1269
- hadOffset: offset,
1270
- attempt: consecutiveConflicts
1271
- });
1272
1324
  if (consecutiveConflicts >= MAX_CONSECUTIVE_CONFLICTS) {
1273
1325
  throw error;
1274
1326
  }
1275
1327
  offset = error.data.expectedOffset;
1276
1328
  continue;
1277
1329
  }
1330
+ const isNetworkError = !(error == null ? void 0 : error.status) || (error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409;
1331
+ if (isNetworkError && chunkRetries < MAX_RETRIES_PER_CHUNK && totalRetries < MAX_TOTAL_RETRIES) {
1332
+ chunkRetries++;
1333
+ totalRetries++;
1334
+ const delay = RETRY_BASE_DELAY_MS * chunkRetries;
1335
+ await abortableSleep(delay, state.signal ?? void 0);
1336
+ if (state.cancelled || ((_g = state.signal) == null ? void 0 : _g.aborted) || state.isPaused || state.isPausing) {
1337
+ break;
1338
+ }
1339
+ try {
1340
+ const currentStatus = await fetchStatus(videoId, baseUrl, authHeaders);
1341
+ if (((_h = currentStatus.upload) == null ? void 0 : _h.nextOffset) != null) {
1342
+ const serverOffset = currentStatus.upload.nextOffset;
1343
+ if (serverOffset === offset && chunkRetries >= 2) {
1344
+ await abortableSleep(5e3, state.signal ?? void 0);
1345
+ const recheck = await fetchStatus(videoId, baseUrl, authHeaders);
1346
+ if (((_i = recheck.upload) == null ? void 0 : _i.nextOffset) != null && recheck.upload.nextOffset > offset) {
1347
+ offset = recheck.upload.nextOffset;
1348
+ state.currentOffset = offset;
1349
+ chunkRetries = 0;
1350
+ continue;
1351
+ }
1352
+ }
1353
+ offset = serverOffset;
1354
+ state.currentOffset = serverOffset;
1355
+ }
1356
+ } catch {
1357
+ }
1358
+ continue;
1359
+ }
1278
1360
  throw error;
1279
1361
  }
1280
1362
  }
@@ -1303,7 +1385,15 @@ function useVideoUpload({
1303
1385
  );
1304
1386
  const handleUpload = useCallback(
1305
1387
  async (file) => {
1306
- var _a, _b;
1388
+ var _a, _b, _c, _d, _e, _f;
1389
+ const state0 = uploadStateRef.current;
1390
+ if (state0.isPaused || state0.isPausing) {
1391
+ state0.isPaused = false;
1392
+ state0.isPausing = false;
1393
+ state0.cancelled = true;
1394
+ state0.videoId = null;
1395
+ state0.attachmentId = null;
1396
+ }
1307
1397
  setIsUploading(true);
1308
1398
  setIsPaused(false);
1309
1399
  const state = uploadStateRef.current;
@@ -1311,11 +1401,13 @@ function useVideoUpload({
1311
1401
  const mySession = state.uploadSession;
1312
1402
  state.isPaused = false;
1313
1403
  state.isPausing = false;
1404
+ state.cancelled = false;
1314
1405
  state.file = file;
1315
1406
  state.currentOffset = 0;
1316
1407
  state.lastTime = 0;
1317
1408
  state.lastBytes = 0;
1318
1409
  state.uploadStallReported = false;
1410
+ maxDisplayedPercentRef.current = 0;
1319
1411
  state.signal = start();
1320
1412
  try {
1321
1413
  if (!parentId) {
@@ -1338,19 +1430,19 @@ function useVideoUpload({
1338
1430
  state.videoId = videoId;
1339
1431
  state.attachmentId = attachmentId;
1340
1432
  setUploadVideoId(videoId);
1341
- if (initResponse.status) {
1342
- const status = initResponse.status;
1343
- if (((_a = status.upload) == null ? void 0 : _a.nextOffset) != null) {
1344
- startOffset = status.upload.nextOffset;
1433
+ try {
1434
+ const verifiedStatus = await fetchStatus(videoId, baseUrl, authHeaders);
1435
+ if (((_a = verifiedStatus.upload) == null ? void 0 : _a.nextOffset) != null && verifiedStatus.upload.nextOffset > 0) {
1436
+ startOffset = verifiedStatus.upload.nextOffset;
1437
+ } else if (((_c = (_b = initResponse.status) == null ? void 0 : _b.upload) == null ? void 0 : _c.nextOffset) != null) {
1438
+ startOffset = initResponse.status.upload.nextOffset;
1439
+ }
1440
+ } catch {
1441
+ if (((_e = (_d = initResponse.status) == null ? void 0 : _d.upload) == null ? void 0 : _e.nextOffset) != null) {
1442
+ startOffset = initResponse.status.upload.nextOffset;
1345
1443
  }
1346
1444
  }
1347
1445
  } catch (initError) {
1348
- console.error("Init video error:", {
1349
- status: initError == null ? void 0 : initError.status,
1350
- statusText: initError == null ? void 0 : initError.statusText,
1351
- data: initError == null ? void 0 : initError.data,
1352
- error: initError
1353
- });
1354
1446
  throw initError;
1355
1447
  }
1356
1448
  } else {
@@ -1360,7 +1452,7 @@ function useVideoUpload({
1360
1452
  baseUrl,
1361
1453
  authHeaders
1362
1454
  );
1363
- if (((_b = status.upload) == null ? void 0 : _b.nextOffset) != null) {
1455
+ if (((_f = status.upload) == null ? void 0 : _f.nextOffset) != null) {
1364
1456
  startOffset = status.upload.nextOffset;
1365
1457
  } else {
1366
1458
  videoId = null;
@@ -1415,20 +1507,14 @@ function useVideoUpload({
1415
1507
  if (state.isPaused || state.isPausing) {
1416
1508
  return;
1417
1509
  }
1418
- console.error("Upload error:", (error == null ? void 0 : error.message) || error);
1419
1510
  if ((error == null ? void 0 : error.message) === "Upload cancelled") {
1420
1511
  return;
1421
1512
  }
1422
1513
  if ((error == null ? void 0 : error.status) === 409) {
1423
1514
  onError("offset-mismatch", uploadStateRef.current.videoId, error);
1424
1515
  } else if (error == null ? void 0 : error.status) {
1425
- console.error(
1426
- `API error: ${error.status} ${error.statusText}`,
1427
- error.data
1428
- );
1429
1516
  onError("interrupted", uploadStateRef.current.videoId, error);
1430
1517
  } else {
1431
- console.error("Unknown upload error:", error);
1432
1518
  onError("interrupted", uploadStateRef.current.videoId, error);
1433
1519
  }
1434
1520
  setIsUploading(false);
@@ -1453,15 +1539,15 @@ function useVideoUpload({
1453
1539
  return;
1454
1540
  }
1455
1541
  state.isPausing = true;
1456
- state.isPaused = true;
1457
1542
  setIsPaused(true);
1458
1543
  if (progressIntervalRef.current) {
1459
1544
  clearInterval(progressIntervalRef.current);
1460
1545
  progressIntervalRef.current = null;
1461
1546
  }
1462
- if (state.currentXhr) {
1463
- state.currentXhr.abort();
1547
+ if (state.chunkAbortController) {
1548
+ state.chunkAbortController.abort();
1464
1549
  } else {
1550
+ state.isPaused = true;
1465
1551
  state.isPausing = false;
1466
1552
  }
1467
1553
  }, []);
@@ -1499,6 +1585,7 @@ function useVideoUpload({
1499
1585
  setIsPaused(false);
1500
1586
  state.isPaused = false;
1501
1587
  state.isPausing = false;
1588
+ state.cancelled = false;
1502
1589
  await uploadFileChunks(state.file, state.videoId, startOffset);
1503
1590
  if (state.uploadSession !== mySession) return;
1504
1591
  if (!state.isPaused && !state.isPausing && state.videoId) {
@@ -1513,7 +1600,7 @@ function useVideoUpload({
1513
1600
  if ((error == null ? void 0 : error.message) === "Upload cancelled") {
1514
1601
  return;
1515
1602
  }
1516
- if (((error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409) && state.file) {
1603
+ if (((error == null ? void 0 : error.status) === 0 || (error == null ? void 0 : error.status) === 409) && state.file && !state.cancelled) {
1517
1604
  state.videoId = null;
1518
1605
  state.attachmentId = null;
1519
1606
  handleUpload(state.file);
@@ -1532,10 +1619,14 @@ function useVideoUpload({
1532
1619
  }, [baseUrl, authHeaders, uploadFileChunks, onError, start, handleUpload]);
1533
1620
  const handleCancel = useCallback(() => {
1534
1621
  const state = uploadStateRef.current;
1535
- cancelUpload();
1536
- if (state.signal) {
1537
- state.signal = null;
1622
+ if (state.chunkAbortController) {
1623
+ try {
1624
+ state.chunkAbortController.abort();
1625
+ } catch {
1626
+ }
1538
1627
  }
1628
+ state.cancelled = true;
1629
+ cancelUpload();
1539
1630
  if (progressIntervalRef.current) {
1540
1631
  clearInterval(progressIntervalRef.current);
1541
1632
  progressIntervalRef.current = null;
@@ -1559,7 +1650,9 @@ function useVideoUpload({
1559
1650
  state.isPaused = false;
1560
1651
  state.isPausing = false;
1561
1652
  state.currentXhr = null;
1653
+ state.chunkAbortController = null;
1562
1654
  speedHistoryRef.current = [];
1655
+ maxDisplayedPercentRef.current = 0;
1563
1656
  setUploadVideoId(null);
1564
1657
  }, [cancelUpload]);
1565
1658
  return {
@@ -1669,7 +1762,7 @@ function CheckCircleIcon({
1669
1762
  "path",
1670
1763
  {
1671
1764
  d: "M16 1.66699C23.916 1.66699 30.333 8.08391 30.333 16C30.333 23.916 23.916 30.333 16 30.333C8.08391 30.333 1.66699 23.916 1.66699 16C1.66699 8.08392 8.08392 1.66699 16 1.66699Z",
1672
- fill: "var(--mantine-primary-color-filled, #3B5BDB)"
1765
+ fill: "var(--mantine-primary-color-filled, #4c6ef5)"
1673
1766
  }
1674
1767
  ),
1675
1768
  /* @__PURE__ */ jsx(
@@ -1816,7 +1909,7 @@ function ProgressBar({
1816
1909
  "%"
1817
1910
  ] }),
1818
1911
  isPaused ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "var(--mantine-color-text)", children: t("editor.video.actions.pause") }) : showRemaining && progress.remaining > 0 && /* @__PURE__ */ jsxs(Text, { size: "sm", c: "var(--mantine-color-dimmed)", children: [
1819
- "Remaining:",
1912
+ t("editor.video.upload.remaining"),
1820
1913
  /* @__PURE__ */ jsxs(Text, { component: "span", c: "var(--mantine-color-text)", children: [
1821
1914
  " ",
1822
1915
  formatTime2(progress.remaining)
@@ -1826,7 +1919,7 @@ function ProgressBar({
1826
1919
  /* @__PURE__ */ jsx(Progress, { value: clampedPercent, size: 6, radius: "md" }),
1827
1920
  showInfo && /* @__PURE__ */ jsxs("div", { className: "video-upload-progress-info", children: [
1828
1921
  /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
1829
- "Uploading:",
1922
+ t("editor.video.upload.progress"),
1830
1923
  /* @__PURE__ */ jsxs(Text, { component: "span", size: "xs", c: "var(--mantine-color-text)", inherit: true, children: [
1831
1924
  " ",
1832
1925
  formatBytes(progress.uploaded),
@@ -2413,6 +2506,7 @@ function UploadState({
2413
2506
  onCancel,
2414
2507
  isPaused = false
2415
2508
  }) {
2509
+ const { t } = useTranslation();
2416
2510
  return /* @__PURE__ */ jsxs(
2417
2511
  Paper,
2418
2512
  {
@@ -2481,7 +2575,7 @@ function UploadState({
2481
2575
  ]
2482
2576
  }
2483
2577
  ),
2484
- /* @__PURE__ */ jsx(Text, { size: "md", fw: 500, c: "var(--mantine-color-bright)", children: "Uploading File" })
2578
+ /* @__PURE__ */ jsx(Text, { size: "md", fw: 500, c: "var(--mantine-color-bright)", children: t("editor.video.upload.uploading") })
2485
2579
  ]
2486
2580
  }
2487
2581
  ),
@@ -2953,6 +3047,138 @@ function VideoSpeedMenu({
2953
3047
  }
2954
3048
  );
2955
3049
  }
3050
+ function buildApiUrl$1(path, baseUrl) {
3051
+ if (path.startsWith("http://") || path.startsWith("https://")) {
3052
+ return path;
3053
+ }
3054
+ const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3055
+ return `${base}${path}`;
3056
+ }
3057
+ function getAuthHeaders$1(authToken) {
3058
+ if (!authToken) return {};
3059
+ return { Authorization: `Bearer ${authToken}` };
3060
+ }
3061
+ async function parseJson$1(response) {
3062
+ const text = await response.text();
3063
+ if (!text) return null;
3064
+ try {
3065
+ return JSON.parse(text);
3066
+ } catch {
3067
+ return text;
3068
+ }
3069
+ }
3070
+ async function handleResponse$1(response) {
3071
+ if (!response.ok) {
3072
+ const data = await parseJson$1(response);
3073
+ const message = (data && typeof data === "object" && "message" in data ? data.message : null) || response.statusText || "Request failed";
3074
+ throw new Error(message);
3075
+ }
3076
+ return parseJson$1(response);
3077
+ }
3078
+ function normalizeSubtitleTrack(raw) {
3079
+ const id = (raw == null ? void 0 : raw.id) ?? (raw == null ? void 0 : raw.trackId) ?? (raw == null ? void 0 : raw.subtitleId) ?? "";
3080
+ const langRaw = (raw == null ? void 0 : raw.lang) ?? (raw == null ? void 0 : raw.language) ?? (raw == null ? void 0 : raw.srclang) ?? "";
3081
+ const lang = langRaw ? String(langRaw) : "und";
3082
+ const labelRaw = (raw == null ? void 0 : raw.label) ?? (raw == null ? void 0 : raw.name) ?? (raw == null ? void 0 : raw.title) ?? lang;
3083
+ const label = labelRaw ? String(labelRaw) : lang;
3084
+ const kind = (raw == null ? void 0 : raw.kind) ?? "subtitles";
3085
+ const isDefault = Boolean((raw == null ? void 0 : raw.isDefault) ?? (raw == null ? void 0 : raw.default) ?? false);
3086
+ const isDisabled = Boolean((raw == null ? void 0 : raw.isDisabled) ?? false);
3087
+ const url = typeof (raw == null ? void 0 : raw.url) === "string" ? raw.url : typeof (raw == null ? void 0 : raw.vttUrl) === "string" ? raw.vttUrl : typeof (raw == null ? void 0 : raw.src) === "string" ? raw.src : void 0;
3088
+ const createdAt = typeof (raw == null ? void 0 : raw.createdAt) === "string" ? raw.createdAt : void 0;
3089
+ const fileName = typeof (raw == null ? void 0 : raw.fileName) === "string" ? raw.fileName : void 0;
3090
+ const auto = Boolean((raw == null ? void 0 : raw.auto) ?? (raw == null ? void 0 : raw.isAuto) ?? (raw == null ? void 0 : raw.source) === "auto");
3091
+ return {
3092
+ id: String(id),
3093
+ lang,
3094
+ label,
3095
+ kind: String(kind),
3096
+ isDefault,
3097
+ isDisabled,
3098
+ url,
3099
+ createdAt,
3100
+ fileName,
3101
+ status: "ready",
3102
+ auto
3103
+ };
3104
+ }
3105
+ function isAutoGeneratedTrack(track) {
3106
+ return track.label.length <= 3;
3107
+ }
3108
+ async function listTracks(videoId, options = {}) {
3109
+ const url = buildApiUrl$1(`/v1/videos/${videoId}/subtitles`, options.baseUrl);
3110
+ const response = await fetch(url, {
3111
+ method: "GET",
3112
+ credentials: "include",
3113
+ headers: {
3114
+ ...getAuthHeaders$1(options.authToken)
3115
+ }
3116
+ });
3117
+ const data = await handleResponse$1(response);
3118
+ const rawTracks = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.tracks) ? data.tracks : [];
3119
+ return rawTracks.map(normalizeSubtitleTrack).filter((track) => track.id);
3120
+ }
3121
+ async function uploadTrack(videoId, file, uploadOptions, options = {}) {
3122
+ const params = new URLSearchParams();
3123
+ params.set("lang", uploadOptions.lang);
3124
+ if (uploadOptions.label) {
3125
+ params.set("label", uploadOptions.label);
3126
+ }
3127
+ if (uploadOptions.kind) {
3128
+ params.set("kind", uploadOptions.kind);
3129
+ }
3130
+ {
3131
+ params.set("default", "false");
3132
+ }
3133
+ const url = buildApiUrl$1(
3134
+ `/v1/videos/${videoId}/subtitles?${params.toString()}`,
3135
+ options.baseUrl
3136
+ );
3137
+ const formData = new FormData();
3138
+ formData.append("file", file);
3139
+ const response = await fetch(url, {
3140
+ method: "POST",
3141
+ credentials: "include",
3142
+ headers: {
3143
+ ...getAuthHeaders$1(options.authToken)
3144
+ },
3145
+ body: formData,
3146
+ signal: uploadOptions.signal
3147
+ });
3148
+ const data = await handleResponse$1(response);
3149
+ if (!data) return null;
3150
+ return normalizeSubtitleTrack(data);
3151
+ }
3152
+ async function patchTrackDisabled(videoId, trackId, isDisabled, options = {}) {
3153
+ const url = buildApiUrl$1(
3154
+ `/v1/videos/${videoId}/subtitles/${trackId}/disabled`,
3155
+ options.baseUrl
3156
+ );
3157
+ const response = await fetch(url, {
3158
+ method: "PATCH",
3159
+ credentials: "include",
3160
+ headers: {
3161
+ "Content-Type": "application/json",
3162
+ ...getAuthHeaders$1(options.authToken)
3163
+ },
3164
+ body: JSON.stringify({ isDisabled })
3165
+ });
3166
+ await handleResponse$1(response);
3167
+ }
3168
+ async function deleteTrack(videoId, trackId, options = {}) {
3169
+ const url = buildApiUrl$1(
3170
+ `/v1/videos/${videoId}/subtitles/${trackId}`,
3171
+ options.baseUrl
3172
+ );
3173
+ const response = await fetch(url, {
3174
+ method: "DELETE",
3175
+ credentials: "include",
3176
+ headers: {
3177
+ ...getAuthHeaders$1(options.authToken)
3178
+ }
3179
+ });
3180
+ await handleResponse$1(response);
3181
+ }
2956
3182
  const LANG_NAMES = {
2957
3183
  en: "English",
2958
3184
  es: "Español",
@@ -2988,7 +3214,7 @@ function VideoSubtitlesMenu({
2988
3214
  const items = settings.subtitles.map((item) => {
2989
3215
  const isActive = item.id === settings.currentSubtitleId;
2990
3216
  let label = item.label;
2991
- if (item.isDefault) {
3217
+ if (isAutoGeneratedTrack({ label })) {
2992
3218
  const fullName = item.lang ? LANG_NAMES[item.lang.toLowerCase()] : null;
2993
3219
  if (fullName) label = fullName;
2994
3220
  label = `${label} (${autoLabel})`;
@@ -3176,8 +3402,8 @@ function VideoCustomControls({
3176
3402
  const visibleBufferedRanges = isProcessing ? [] : bufferedRanges;
3177
3403
  const sortedChapters = useMemo(() => {
3178
3404
  if (!chapters || chapters.length === 0) return [];
3179
- return [...chapters].sort((a, b) => a.startSec - b.startSec);
3180
- }, [chapters]);
3405
+ return [...chapters].filter((ch) => duration > 0 && ch.startSec <= duration).sort((a, b) => a.startSec - b.startSec);
3406
+ }, [chapters, duration]);
3181
3407
  const chapterSegments = useMemo(() => {
3182
3408
  if (sortedChapters.length === 0 || duration <= 0) return [];
3183
3409
  const segments = [];
@@ -3538,10 +3764,6 @@ function VideoCustomControls({
3538
3764
  className: "video-player-button",
3539
3765
  type: "button",
3540
3766
  "aria-label": t("editor.video.player.settings"),
3541
- onClick: () => {
3542
- setIsSettingsOpen((prev) => !prev);
3543
- setActiveMenu("main");
3544
- },
3545
3767
  children: /* @__PURE__ */ jsx(SettingsPlayerIcon, { size: 20 })
3546
3768
  }
3547
3769
  ) }),
@@ -3579,6 +3801,7 @@ function VideoCustomControls({
3579
3801
  onSelect: (speed) => {
3580
3802
  setOptimisticSpeed(speed);
3581
3803
  onSpeedClick(speed);
3804
+ setIsSettingsOpen(false);
3582
3805
  }
3583
3806
  }
3584
3807
  ),
@@ -3588,7 +3811,10 @@ function VideoCustomControls({
3588
3811
  disabled: isSettingsDisabled,
3589
3812
  settings,
3590
3813
  onBack: () => setActiveMenu("main"),
3591
- onSelect: onSubtitleSelect
3814
+ onSelect: (subtitleId) => {
3815
+ onSubtitleSelect(subtitleId);
3816
+ setIsSettingsOpen(false);
3817
+ }
3592
3818
  }
3593
3819
  ),
3594
3820
  activeMenu === "quality" && /* @__PURE__ */ jsx(
@@ -3597,7 +3823,10 @@ function VideoCustomControls({
3597
3823
  disabled: isSettingsDisabled,
3598
3824
  settings,
3599
3825
  onBack: () => setActiveMenu("main"),
3600
- onSelect: onQualityClick
3826
+ onSelect: (qualityId) => {
3827
+ onQualityClick(qualityId);
3828
+ setIsSettingsOpen(false);
3829
+ }
3601
3830
  }
3602
3831
  )
3603
3832
  ]
@@ -3772,141 +4001,21 @@ const setSubtitle = (player, id, videoElement) => {
3772
4001
  track.mode = index === id ? "showing" : "disabled";
3773
4002
  });
3774
4003
  };
3775
- function buildApiUrl$1(path, baseUrl) {
3776
- if (path.startsWith("http://") || path.startsWith("https://")) {
3777
- return path;
4004
+ function normalizeUrl(url, baseUrl) {
4005
+ if (!url) return null;
4006
+ if (url.startsWith("http://") || url.startsWith("https://")) {
4007
+ return url;
3778
4008
  }
3779
- const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3780
- return `${base}${path}`;
3781
- }
3782
- function getAuthHeaders$1(authToken) {
3783
- if (!authToken) return {};
3784
- return { Authorization: `Bearer ${authToken}` };
3785
- }
3786
- async function parseJson$1(response) {
3787
- const text = await response.text();
3788
- if (!text) return null;
3789
- try {
3790
- return JSON.parse(text);
3791
- } catch {
3792
- return text;
4009
+ if (url.startsWith("/")) {
4010
+ const base = baseUrl || window.location.origin;
4011
+ return `${base}${url}`;
3793
4012
  }
4013
+ return url;
3794
4014
  }
3795
- async function handleResponse$1(response) {
3796
- if (!response.ok) {
3797
- const data = await parseJson$1(response);
3798
- const message = (data && typeof data === "object" && "message" in data ? data.message : null) || response.statusText || "Request failed";
3799
- throw new Error(message);
3800
- }
3801
- return parseJson$1(response);
3802
- }
3803
- function normalizeSubtitleTrack(raw) {
3804
- const id = (raw == null ? void 0 : raw.id) ?? (raw == null ? void 0 : raw.trackId) ?? (raw == null ? void 0 : raw.subtitleId) ?? "";
3805
- const langRaw = (raw == null ? void 0 : raw.lang) ?? (raw == null ? void 0 : raw.language) ?? (raw == null ? void 0 : raw.srclang) ?? "";
3806
- const lang = langRaw ? String(langRaw) : "und";
3807
- const labelRaw = (raw == null ? void 0 : raw.label) ?? (raw == null ? void 0 : raw.name) ?? (raw == null ? void 0 : raw.title) ?? lang;
3808
- const label = labelRaw ? String(labelRaw) : lang;
3809
- const kind = (raw == null ? void 0 : raw.kind) ?? "subtitles";
3810
- const isDefault = Boolean((raw == null ? void 0 : raw.isDefault) ?? (raw == null ? void 0 : raw.default) ?? false);
3811
- const url = typeof (raw == null ? void 0 : raw.url) === "string" ? raw.url : typeof (raw == null ? void 0 : raw.vttUrl) === "string" ? raw.vttUrl : typeof (raw == null ? void 0 : raw.src) === "string" ? raw.src : void 0;
3812
- const createdAt = typeof (raw == null ? void 0 : raw.createdAt) === "string" ? raw.createdAt : void 0;
3813
- const fileName = typeof (raw == null ? void 0 : raw.fileName) === "string" ? raw.fileName : void 0;
3814
- const auto = Boolean((raw == null ? void 0 : raw.auto) ?? (raw == null ? void 0 : raw.isAuto) ?? (raw == null ? void 0 : raw.source) === "auto");
3815
- return {
3816
- id: String(id),
3817
- lang,
3818
- label,
3819
- kind: String(kind),
3820
- isDefault,
3821
- url,
3822
- createdAt,
3823
- fileName,
3824
- status: "ready",
3825
- auto
3826
- };
3827
- }
3828
- async function uploadTrack(videoId, file, uploadOptions, options = {}) {
3829
- const params = new URLSearchParams();
3830
- params.set("lang", uploadOptions.lang);
3831
- if (uploadOptions.label) {
3832
- params.set("label", uploadOptions.label);
3833
- }
3834
- if (uploadOptions.kind) {
3835
- params.set("kind", uploadOptions.kind);
3836
- }
3837
- {
3838
- params.set("default", "false");
3839
- }
3840
- const url = buildApiUrl$1(
3841
- `/v1/videos/${videoId}/subtitles?${params.toString()}`,
3842
- options.baseUrl
3843
- );
3844
- const formData = new FormData();
3845
- formData.append("file", file);
3846
- const response = await fetch(url, {
3847
- method: "POST",
3848
- credentials: "include",
3849
- headers: {
3850
- ...getAuthHeaders$1(options.authToken)
3851
- },
3852
- body: formData,
3853
- signal: uploadOptions.signal
3854
- });
3855
- const data = await handleResponse$1(response);
3856
- if (!data) return null;
3857
- return normalizeSubtitleTrack(data);
3858
- }
3859
- function getVttUrl(videoId, trackId, options = {}) {
3860
- return buildApiUrl$1(
3861
- `/v1/videos/${videoId}/subtitles/${trackId}.vtt`,
3862
- options.baseUrl
3863
- );
3864
- }
3865
- function normalizeUrl$1(path, baseUrl) {
3866
- if (path.startsWith("http://") || path.startsWith("https://")) {
3867
- return path;
3868
- }
3869
- const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "");
3870
- if (path.startsWith("/")) {
3871
- return `${base}${path}`;
3872
- }
3873
- return `${base}/${path}`;
3874
- }
3875
- function resolveSubtitleUrl(videoId, track, options = {}) {
3876
- if (track.url) {
3877
- return normalizeUrl$1(track.url, options.baseUrl);
3878
- }
3879
- return getVttUrl(videoId, track.id, options);
3880
- }
3881
- async function deleteTrack(videoId, trackId, options = {}) {
3882
- const url = buildApiUrl$1(
3883
- `/v1/videos/${videoId}/subtitles/${trackId}`,
3884
- options.baseUrl
3885
- );
3886
- const response = await fetch(url, {
3887
- method: "DELETE",
3888
- credentials: "include",
3889
- headers: {
3890
- ...getAuthHeaders$1(options.authToken)
3891
- }
3892
- });
3893
- await handleResponse$1(response);
3894
- }
3895
- function normalizeUrl(url, baseUrl) {
3896
- if (!url) return null;
3897
- if (url.startsWith("http://") || url.startsWith("https://")) {
3898
- return url;
3899
- }
3900
- if (url.startsWith("/")) {
3901
- const base = baseUrl || window.location.origin;
3902
- return `${base}${url}`;
3903
- }
3904
- return url;
3905
- }
3906
- function getNativeSubtitleTracks(videoElement) {
3907
- return Array.from(videoElement.textTracks || []).filter(
3908
- (track) => track.kind === "subtitles" || track.kind === "captions"
3909
- );
4015
+ function getNativeSubtitleTracks(videoElement) {
4016
+ return Array.from(videoElement.textTracks || []).filter(
4017
+ (track) => track.kind === "subtitles" || track.kind === "captions"
4018
+ );
3910
4019
  }
3911
4020
  function getSubtitleUrl(track, baseUrl) {
3912
4021
  if (!track.url) return null;
@@ -3926,7 +4035,8 @@ function VideoPlayerState({
3926
4035
  subtitles,
3927
4036
  isProcessing = false,
3928
4037
  posterUrl,
3929
- onPlayerInfo
4038
+ onPlayerInfo,
4039
+ onFallback
3930
4040
  }) {
3931
4041
  const { t, i18n } = useTranslation();
3932
4042
  const [currentVideoUrl, setCurrentVideoUrl] = useState(null);
@@ -3953,6 +4063,9 @@ function VideoPlayerState({
3953
4063
  );
3954
4064
  const shakaAddedIdsRef = useRef([]);
3955
4065
  const shakaFilterRegisteredRef = useRef(false);
4066
+ const triedFallbackUrlsRef = useRef(/* @__PURE__ */ new Set());
4067
+ const hlsUrlCheckedRef = useRef(null);
4068
+ const savedStateLockRef = useRef(false);
3956
4069
  useEffect(() => {
3957
4070
  if (!language) {
3958
4071
  return;
@@ -3966,16 +4079,17 @@ function VideoPlayerState({
3966
4079
  return;
3967
4080
  }
3968
4081
  if (subtitles != null) {
3969
- const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
4082
+ const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id && !track.isDisabled);
3970
4083
  setSubtitleTracks(normalized);
3971
4084
  }
3972
4085
  }, [videoId, subtitles]);
3973
4086
  useEffect(() => {
3974
4087
  setIsAutoQualityEnabled(true);
4088
+ hlsUrlCheckedRef.current = null;
3975
4089
  }, [videoId]);
3976
4090
  const handleSubtitlesOpen = useCallback(() => {
3977
4091
  if (!videoId || subtitles == null) return;
3978
- const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
4092
+ const normalized = (subtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id && !track.isDisabled);
3979
4093
  setSubtitleTracks(normalized);
3980
4094
  }, [videoId, subtitles]);
3981
4095
  useEffect(() => {
@@ -4082,102 +4196,148 @@ function VideoPlayerState({
4082
4196
  selectedSubtitleMeta
4083
4197
  ]);
4084
4198
  useEffect(() => {
4085
- var _a;
4086
- if (!currentVideoUrl || !((_a = playerRef.current) == null ? void 0 : _a.player)) {
4087
- return;
4088
- }
4089
- const player = playerRef.current.player;
4090
- if (errorHandlerRef.current) {
4091
- try {
4092
- player.removeEventListener("error", errorHandlerRef.current);
4093
- } catch (e) {
4094
- console.warn("Error removing old event listener:", e);
4199
+ if (!currentVideoUrl) return;
4200
+ let cancelled = false;
4201
+ let player = null;
4202
+ const handleFatalError = (error) => {
4203
+ if (cancelled) return;
4204
+ const fallback = normalizeUrl(nativeVideoUrl, baseUrl) || normalizeUrl(rawUrl ?? null, baseUrl);
4205
+ if (fallback) {
4206
+ setHlsFailed(true);
4207
+ onFallback == null ? void 0 : onFallback(true);
4208
+ } else {
4209
+ setPlayerError(
4210
+ t("editor.video.player.playbackError", { code: error.code })
4211
+ );
4095
4212
  }
4096
- }
4213
+ };
4097
4214
  const errorHandler = (event) => {
4098
4215
  const error = event.detail;
4099
- console.error("Shaka Player error:", {
4100
- code: error.code,
4101
- category: error.category,
4102
- severity: error.severity,
4103
- data: error.data
4104
- });
4105
4216
  if (error.severity === 2) {
4106
- const fallback = normalizeUrl(nativeVideoUrl, baseUrl) || normalizeUrl(rawUrl ?? null, baseUrl);
4107
- if (fallback) {
4108
- console.warn(
4109
- `[VideoPlayer] HLS failed (code=${error.code}), falling back to native video:`,
4110
- fallback
4111
- );
4112
- setHlsFailed(true);
4113
- } else {
4114
- setPlayerError(
4115
- t("editor.video.player.playbackError", { code: error.code })
4116
- );
4117
- }
4217
+ handleFatalError(error);
4118
4218
  } else {
4119
4219
  try {
4120
- player.recover();
4121
- } catch (recoverError) {
4122
- console.error("Failed to recover from error:", recoverError);
4220
+ player == null ? void 0 : player.recover();
4221
+ } catch {
4123
4222
  }
4124
4223
  }
4125
4224
  };
4126
- errorHandlerRef.current = errorHandler;
4127
- player.addEventListener("error", errorHandler);
4225
+ const tryLoad = () => {
4226
+ var _a;
4227
+ player = (_a = playerRef.current) == null ? void 0 : _a.player;
4228
+ if (!player) {
4229
+ const raf2 = requestAnimationFrame(tryLoad);
4230
+ return () => cancelAnimationFrame(raf2);
4231
+ }
4232
+ if (errorHandlerRef.current) {
4233
+ try {
4234
+ player.removeEventListener("error", errorHandlerRef.current);
4235
+ } catch {
4236
+ }
4237
+ }
4238
+ errorHandlerRef.current = errorHandler;
4239
+ player.addEventListener("error", errorHandler);
4240
+ player.load(currentVideoUrl).catch((loadError) => {
4241
+ if (cancelled) return;
4242
+ if (loadError.code === 7e3) return;
4243
+ handleFatalError(loadError);
4244
+ });
4245
+ };
4246
+ const raf = requestAnimationFrame(tryLoad);
4128
4247
  return () => {
4248
+ cancelled = true;
4249
+ cancelAnimationFrame(raf);
4129
4250
  if (player && errorHandlerRef.current) {
4130
4251
  try {
4131
4252
  player.removeEventListener("error", errorHandlerRef.current);
4132
- } catch (e) {
4133
- console.warn("Error removing event listener on cleanup:", e);
4253
+ } catch {
4134
4254
  }
4135
4255
  }
4136
4256
  };
4137
- }, [currentVideoUrl, nativeVideoUrl, rawUrl, baseUrl]);
4257
+ }, [currentVideoUrl, nativeVideoUrl, rawUrl, baseUrl, onFallback]);
4138
4258
  useEffect(() => {
4139
- var _a, _b;
4259
+ let cancelled = false;
4260
+ let retryTimer = null;
4140
4261
  if (optimizedReady && primaryUrl) {
4141
4262
  const normalizedUrl = normalizeUrl(primaryUrl, baseUrl);
4142
4263
  if (normalizedUrl) {
4143
- if (previousVideoUrlRef.current && previousVideoUrlRef.current !== normalizedUrl) {
4144
- const videoElement = nativeVideoRef.current || ((_a = playerRef.current) == null ? void 0 : _a.videoElement);
4145
- if (videoElement) {
4146
- savedStateRef.current = {
4147
- time: videoElement.currentTime || 0,
4148
- paused: videoElement.paused
4149
- };
4264
+ if (!currentNativeVideoUrl && rawUrl) {
4265
+ const normalizedNative = normalizeUrl(rawUrl, baseUrl);
4266
+ if (normalizedNative) {
4267
+ setCurrentNativeVideoUrl(normalizedNative);
4150
4268
  }
4151
- } else if (nativeVideoRef.current && !savedStateRef.current) {
4152
- const nativeVideo = nativeVideoRef.current;
4269
+ }
4270
+ if (hlsUrlCheckedRef.current === normalizedUrl) {
4271
+ return;
4272
+ }
4273
+ const nv = nativeVideoRef.current;
4274
+ if (nv && nv.currentTime > 0) {
4153
4275
  savedStateRef.current = {
4154
- time: nativeVideo.currentTime || 0,
4155
- paused: nativeVideo.paused
4276
+ time: nv.currentTime,
4277
+ paused: nv.paused
4156
4278
  };
4279
+ savedStateLockRef.current = true;
4157
4280
  }
4158
- setCurrentVideoUrl(normalizedUrl);
4159
- setPlayerError(null);
4160
- setHlsFailed(false);
4161
- previousVideoUrlRef.current = normalizedUrl;
4281
+ const checkHls = (attempt) => {
4282
+ if (cancelled) return;
4283
+ fetch(normalizedUrl, { method: "HEAD", credentials: "include" }).then((res) => {
4284
+ if (cancelled) return;
4285
+ hlsUrlCheckedRef.current = normalizedUrl;
4286
+ if (res.ok) {
4287
+ setCurrentVideoUrl(normalizedUrl);
4288
+ setPlayerError(null);
4289
+ setHlsFailed(false);
4290
+ onFallback == null ? void 0 : onFallback(false);
4291
+ triedFallbackUrlsRef.current.clear();
4292
+ } else {
4293
+ triedFallbackUrlsRef.current.clear();
4294
+ setHlsFailed(true);
4295
+ onFallback == null ? void 0 : onFallback(true);
4296
+ scheduleRetry(attempt);
4297
+ }
4298
+ }).catch((err) => {
4299
+ if (cancelled) return;
4300
+ triedFallbackUrlsRef.current.clear();
4301
+ setHlsFailed(true);
4302
+ onFallback == null ? void 0 : onFallback(true);
4303
+ scheduleRetry(attempt);
4304
+ });
4305
+ };
4306
+ const MAX_RETRIES = 10;
4307
+ const RETRY_INTERVAL = 5e3;
4308
+ const scheduleRetry = (attempt) => {
4309
+ if (cancelled || attempt >= MAX_RETRIES) return;
4310
+ retryTimer = setTimeout(() => checkHls(attempt + 1), RETRY_INTERVAL);
4311
+ };
4312
+ checkHls(0);
4162
4313
  }
4163
4314
  } else if (nativeVideoUrl) {
4164
4315
  const normalizedUrl = normalizeUrl(nativeVideoUrl, baseUrl);
4165
4316
  if (normalizedUrl) {
4166
- if (previousVideoUrlRef.current && previousVideoUrlRef.current !== normalizedUrl) {
4167
- const videoElement = (_b = playerRef.current) == null ? void 0 : _b.videoElement;
4168
- if (videoElement) {
4169
- savedStateRef.current = {
4170
- time: videoElement.currentTime || 0,
4171
- paused: videoElement.paused
4172
- };
4173
- }
4317
+ if (previousVideoUrlRef.current === normalizedUrl) {
4318
+ return;
4319
+ }
4320
+ const nv = nativeVideoRef.current;
4321
+ if (nv && nv.readyState > 0 && !nv.error && nv.src) {
4322
+ previousVideoUrlRef.current = normalizedUrl;
4323
+ return;
4324
+ }
4325
+ if (previousVideoUrlRef.current && nv && nv.currentTime > 0) {
4326
+ savedStateRef.current = {
4327
+ time: nv.currentTime,
4328
+ paused: nv.paused
4329
+ };
4174
4330
  }
4175
4331
  setCurrentNativeVideoUrl(normalizedUrl);
4176
4332
  setCurrentVideoUrl(null);
4177
4333
  previousVideoUrlRef.current = normalizedUrl;
4178
4334
  }
4179
4335
  }
4180
- }, [videoUrl, primaryUrl, baseUrl, optimizedReady, nativeVideoUrl]);
4336
+ return () => {
4337
+ cancelled = true;
4338
+ if (retryTimer) clearTimeout(retryTimer);
4339
+ };
4340
+ }, [primaryUrl, baseUrl, optimizedReady, nativeVideoUrl]);
4181
4341
  useEffect(() => {
4182
4342
  if (optimizedReady && currentVideoUrl && playerRef.current) {
4183
4343
  const videoElement = playerRef.current.videoElement;
@@ -4191,6 +4351,9 @@ function VideoPlayerState({
4191
4351
  });
4192
4352
  }
4193
4353
  savedStateRef.current = null;
4354
+ savedStateLockRef.current = false;
4355
+ } else {
4356
+ savedStateLockRef.current = false;
4194
4357
  }
4195
4358
  }
4196
4359
  };
@@ -4224,10 +4387,35 @@ function VideoPlayerState({
4224
4387
  }
4225
4388
  }
4226
4389
  }, [optimizedReady, currentVideoUrl]);
4390
+ useEffect(() => {
4391
+ const nv = nativeVideoRef.current;
4392
+ if (!nv) return;
4393
+ if (currentVideoUrl && !hlsFailed) {
4394
+ if (nv.currentTime > 0) {
4395
+ savedStateRef.current = {
4396
+ time: nv.currentTime,
4397
+ paused: nv.paused
4398
+ };
4399
+ }
4400
+ if (!nv.paused) {
4401
+ nv.pause();
4402
+ }
4403
+ }
4404
+ }, [currentVideoUrl, hlsFailed]);
4227
4405
  useEffect(() => {
4228
4406
  var _a;
4229
- const nextVideoElement = optimizedReady && !hlsFailed ? ((_a = playerRef.current) == null ? void 0 : _a.videoElement) || null : nativeVideoRef.current;
4230
- setActiveVideoElement(nextVideoElement);
4407
+ if (currentVideoUrl && !hlsFailed) {
4408
+ const shakaVideo = ((_a = playerRef.current) == null ? void 0 : _a.videoElement) || null;
4409
+ if (shakaVideo && shakaVideo.readyState >= 1) {
4410
+ setActiveVideoElement(shakaVideo);
4411
+ } else if (shakaVideo) {
4412
+ const onReady = () => setActiveVideoElement(shakaVideo);
4413
+ shakaVideo.addEventListener("loadedmetadata", onReady, { once: true });
4414
+ return () => shakaVideo.removeEventListener("loadedmetadata", onReady);
4415
+ }
4416
+ } else {
4417
+ setActiveVideoElement(nativeVideoRef.current);
4418
+ }
4231
4419
  }, [optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl]);
4232
4420
  useEffect(() => {
4233
4421
  if (!activeVideoElement) return;
@@ -4257,16 +4445,29 @@ function VideoPlayerState({
4257
4445
  return () => observer.disconnect();
4258
4446
  }, []);
4259
4447
  useEffect(() => {
4260
- var _a;
4448
+ var _a, _b;
4261
4449
  if (!onPlayerInfo) return;
4262
4450
  const shakaPlayer = ((_a = playerRef.current) == null ? void 0 : _a.player) || null;
4263
4451
  const nativeVideo = nativeVideoRef.current || null;
4264
4452
  const containerEl = containerRef.current || null;
4265
- const mode = optimizedReady && !hlsFailed && shakaPlayer ? "shaka" : "native";
4266
- onPlayerInfo({ shakaPlayer, nativeVideo, mode, containerEl });
4267
- }, [onPlayerInfo, optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl]);
4453
+ const mode = currentVideoUrl && !hlsFailed && shakaPlayer ? "shaka" : "native";
4454
+ let videoDuration = null;
4455
+ try {
4456
+ const range = (_b = shakaPlayer == null ? void 0 : shakaPlayer.seekRange) == null ? void 0 : _b.call(shakaPlayer);
4457
+ if (range && Number.isFinite(range.end) && range.end > 0) {
4458
+ videoDuration = range.end;
4459
+ }
4460
+ } catch {
4461
+ }
4462
+ if (videoDuration == null && activeVideoElement) {
4463
+ const d = activeVideoElement.duration;
4464
+ if (Number.isFinite(d) && d > 0) videoDuration = d;
4465
+ }
4466
+ onPlayerInfo({ shakaPlayer, nativeVideo, mode, containerEl, videoDuration });
4467
+ }, [onPlayerInfo, optimizedReady, hlsFailed, currentVideoUrl, currentNativeVideoUrl, activeVideoElement]);
4268
4468
  useEffect(() => {
4269
4469
  var _a;
4470
+ console.log("[Cover] posterUrl effect:", posterUrl);
4270
4471
  const nativeVideo = nativeVideoRef.current;
4271
4472
  if (nativeVideo) {
4272
4473
  nativeVideo.poster = posterUrl || "";
@@ -4343,6 +4544,7 @@ function VideoPlayerState({
4343
4544
  nativeVideo.play().catch(() => {
4344
4545
  });
4345
4546
  }
4547
+ savedStateLockRef.current = false;
4346
4548
  }
4347
4549
  };
4348
4550
  if (nativeVideo.readyState >= 1) {
@@ -4353,10 +4555,12 @@ function VideoPlayerState({
4353
4555
  });
4354
4556
  }
4355
4557
  const handleTimeUpdate = () => {
4558
+ if (savedStateLockRef.current) return;
4356
4559
  if (timeUpdateThrottleRef.current) {
4357
4560
  clearTimeout(timeUpdateThrottleRef.current);
4358
4561
  }
4359
4562
  timeUpdateThrottleRef.current = setTimeout(() => {
4563
+ if (savedStateLockRef.current) return;
4360
4564
  if (!savedStateRef.current) {
4361
4565
  savedStateRef.current = { time: 0, paused: true };
4362
4566
  }
@@ -4364,6 +4568,7 @@ function VideoPlayerState({
4364
4568
  }, 100);
4365
4569
  };
4366
4570
  const handlePlay = () => {
4571
+ if (savedStateLockRef.current) return;
4367
4572
  if (!savedStateRef.current) {
4368
4573
  savedStateRef.current = { time: 0, paused: false };
4369
4574
  } else {
@@ -4371,6 +4576,7 @@ function VideoPlayerState({
4371
4576
  }
4372
4577
  };
4373
4578
  const handlePause = () => {
4579
+ if (savedStateLockRef.current) return;
4374
4580
  if (!savedStateRef.current) {
4375
4581
  savedStateRef.current = { time: nativeVideo.currentTime, paused: true };
4376
4582
  } else {
@@ -4388,6 +4594,12 @@ function VideoPlayerState({
4388
4594
  if (timeUpdateThrottleRef.current) {
4389
4595
  clearTimeout(timeUpdateThrottleRef.current);
4390
4596
  }
4597
+ if (nativeVideo.currentTime > 0) {
4598
+ savedStateRef.current = {
4599
+ time: nativeVideo.currentTime,
4600
+ paused: nativeVideo.paused
4601
+ };
4602
+ }
4391
4603
  };
4392
4604
  }, [currentNativeVideoUrl]);
4393
4605
  useEffect(() => {
@@ -4547,7 +4759,8 @@ function VideoPlayerState({
4547
4759
  }
4548
4760
  );
4549
4761
  }
4550
- const fallbackVideoUrl = currentNativeVideoUrl || normalizeUrl(rawUrl ?? null, baseUrl);
4762
+ const fallbackVideoUrl = normalizeUrl(rawUrl ?? null, baseUrl) || currentNativeVideoUrl;
4763
+ const shakaActive = !!(currentVideoUrl && !hlsFailed);
4551
4764
  const isCompact = playerWidth > 0 && playerWidth < 500;
4552
4765
  const isNarrow = playerWidth > 0 && playerWidth < 380;
4553
4766
  const hasChapters = ((chapters == null ? void 0 : chapters.length) ?? 0) > 0;
@@ -4576,25 +4789,41 @@ function VideoPlayerState({
4576
4789
  ref: containerRef,
4577
4790
  onClick: handleContainerClick,
4578
4791
  children: [
4579
- (!optimizedReady || hlsFailed) && /* @__PURE__ */ jsx(
4792
+ /* @__PURE__ */ jsx(
4580
4793
  "video",
4581
4794
  {
4582
4795
  ref: nativeVideoRef,
4583
- src: (hlsFailed ? fallbackVideoUrl : currentNativeVideoUrl) || void 0,
4796
+ src: currentNativeVideoUrl || (hlsFailed ? fallbackVideoUrl : null) || void 0,
4584
4797
  className: "video-player-media",
4585
- poster: posterUrl || void 0
4798
+ style: shakaActive ? { display: "none" } : void 0,
4799
+ poster: posterUrl || void 0,
4800
+ onError: (e) => {
4801
+ const video = e.currentTarget;
4802
+ const failedSrc = video.currentSrc || video.src;
4803
+ if (!failedSrc) return;
4804
+ const mediaError = video.error;
4805
+ if (!mediaError) return;
4806
+ triedFallbackUrlsRef.current.add(failedSrc);
4807
+ const candidates = [
4808
+ normalizeUrl(rawUrl ?? null, baseUrl),
4809
+ normalizeUrl(nativeVideoUrl, baseUrl)
4810
+ ].filter((url) => !!url && !triedFallbackUrlsRef.current.has(url));
4811
+ if (candidates.length > 0) {
4812
+ video.src = candidates[0];
4813
+ video.load();
4814
+ }
4815
+ }
4586
4816
  }
4587
4817
  ),
4588
- optimizedReady && !hlsFailed && currentVideoUrl && /* @__PURE__ */ jsx(
4818
+ shakaActive && /* @__PURE__ */ jsx(
4589
4819
  ShakaPlayer,
4590
4820
  {
4591
4821
  ref: playerRef,
4592
- src: currentVideoUrl,
4593
4822
  autoPlay: false,
4594
4823
  className: "video-player-media"
4595
4824
  }
4596
4825
  ),
4597
- (currentNativeVideoUrl || currentVideoUrl || hlsFailed && fallbackVideoUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
4826
+ (currentNativeVideoUrl || currentVideoUrl || primaryUrl || hlsFailed && fallbackVideoUrl) && /* @__PURE__ */ jsxs(Fragment, { children: [
4598
4827
  isPaused && !isCompact && /* @__PURE__ */ jsx(
4599
4828
  "button",
4600
4829
  {
@@ -4780,6 +5009,7 @@ function VideoUploadComponent({
4780
5009
  const [videoId, setVideoId] = useState(initialVideoId ?? null);
4781
5010
  const [linkUrl, setLinkUrl] = useState("");
4782
5011
  const [linkError, setLinkError] = useState(null);
5012
+ const [isHlsFallback, setIsHlsFallback] = useState(false);
4783
5013
  const lastUploadFileRef = useRef(null);
4784
5014
  const uploadCallbackRef = useRef(false);
4785
5015
  const uploadIdRef = useRef(null);
@@ -4869,7 +5099,16 @@ function VideoUploadComponent({
4869
5099
  const resolvedRawPlayable = sourceType === "upload" ? rawPlayable : false;
4870
5100
  const resolvedStatusAvailable = sourceType === "upload" ? isStatusAvailable : false;
4871
5101
  const chapters = chaptersOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.chapters) ?? void 0;
4872
- const subtitles = subtitlesOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.subtitles) ?? void 0;
5102
+ const rawSubtitles = subtitlesOverride ?? (resolvedStatus == null ? void 0 : resolvedStatus.subtitles) ?? void 0;
5103
+ const subtitlesRef = useRef(rawSubtitles);
5104
+ const subtitles = useMemo(() => {
5105
+ if (rawSubtitles === subtitlesRef.current) return subtitlesRef.current;
5106
+ if (JSON.stringify(rawSubtitles) === JSON.stringify(subtitlesRef.current)) {
5107
+ return subtitlesRef.current;
5108
+ }
5109
+ subtitlesRef.current = rawSubtitles;
5110
+ return rawSubtitles;
5111
+ }, [rawSubtitles]);
4873
5112
  const inferredNativeVideoUrl = useMemo(() => {
4874
5113
  if (fallbackNativeVideoUrl) return fallbackNativeVideoUrl;
4875
5114
  if (platform === "link") {
@@ -4960,7 +5199,7 @@ function VideoUploadComponent({
4960
5199
  const isReadyForPreview = currentState === "video-preview" && (sourceType === "link" ? true : resolvedStatusAvailable ? resolvedOptimizedReady : resolvedOptimizedReady || resolvedRawPlayable || hasPreviewSource);
4961
5200
  const isProcessingStatus = (resolvedStatus == null ? void 0 : resolvedStatus.state) === "PROCESSING" || (resolvedStatus == null ? void 0 : resolvedStatus.state) === "UPLOADING";
4962
5201
  const hasProcessingPercent = typeof ((_a = resolvedStatus == null ? void 0 : resolvedStatus.processing) == null ? void 0 : _a.percent) === "number";
4963
- const showProcessingBanner = currentState === "video-preview" && sourceType === "upload" && !!resolvedNativeVideoUrl && resolvedStatusAvailable && !resolvedOptimizedReady && (isProcessingStatus || hasProcessingPercent);
5202
+ const showProcessingBanner = currentState === "video-preview" && sourceType === "upload" && !!resolvedNativeVideoUrl && resolvedStatusAvailable && !resolvedOptimizedReady && !isHlsFallback && (isProcessingStatus || hasProcessingPercent);
4964
5203
  const maxBannerPercentRef = useRef(0);
4965
5204
  const rawPercent = Math.max(0, Math.min(100, ((_b = resolvedStatus == null ? void 0 : resolvedStatus.processing) == null ? void 0 : _b.percent) ?? 0));
4966
5205
  if (rawPercent > maxBannerPercentRef.current) {
@@ -5015,12 +5254,17 @@ function VideoUploadComponent({
5015
5254
  setLinkError(null);
5016
5255
  setCurrentState("video-preview");
5017
5256
  };
5257
+ const videoIdRef = useRef(videoId);
5258
+ videoIdRef.current = videoId;
5259
+ const uploadVideoIdRef = useRef(uploadVideoId);
5260
+ uploadVideoIdRef.current = uploadVideoId;
5018
5261
  useEffect(() => {
5019
5262
  return editor.registerCommand(
5020
5263
  UPLOAD_VIDEO_COMMAND,
5021
5264
  (payload) => {
5022
- if (sourceType === "upload" && payload.length > 0) {
5265
+ if (sourceType === "upload" && payload.length > 0 && !videoIdRef.current && !uploadVideoIdRef.current) {
5023
5266
  handleFileSelect(payload[0]);
5267
+ return true;
5024
5268
  }
5025
5269
  return false;
5026
5270
  },
@@ -5075,7 +5319,7 @@ function VideoUploadComponent({
5075
5319
  onStateChange == null ? void 0 : onStateChange(currentState);
5076
5320
  }, [currentState, onStateChange]);
5077
5321
  const renderState = () => {
5078
- var _a2;
5322
+ var _a2, _b2;
5079
5323
  if (sourceType === "link" && currentState !== "video-preview") {
5080
5324
  return /* @__PURE__ */ jsx(
5081
5325
  LinkState,
@@ -5168,6 +5412,8 @@ function VideoUploadComponent({
5168
5412
  }
5169
5413
  ) });
5170
5414
  }
5415
+ const effectivePoster = posterUrl ?? ((_a2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _a2.url) ?? null;
5416
+ console.log("[Cover] VideoUploadComponent render: posterUrl prop=", posterUrl, "cover.url=", (_b2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _b2.url, "effective=", effectivePoster);
5171
5417
  return /* @__PURE__ */ jsx(
5172
5418
  VideoPlayerState,
5173
5419
  {
@@ -5182,8 +5428,9 @@ function VideoUploadComponent({
5182
5428
  chapters: showProcessingBanner ? void 0 : chapters,
5183
5429
  isProcessing: showProcessingBanner,
5184
5430
  subtitles,
5185
- posterUrl: posterUrl ?? ((_a2 = resolvedStatus == null ? void 0 : resolvedStatus.cover) == null ? void 0 : _a2.url) ?? null,
5186
- onPlayerInfo
5431
+ posterUrl: effectivePoster,
5432
+ onPlayerInfo,
5433
+ onFallback: setIsHlsFallback
5187
5434
  }
5188
5435
  );
5189
5436
  default:
@@ -5323,21 +5570,6 @@ async function handleResponse(response) {
5323
5570
  }
5324
5571
  return parseJson(response);
5325
5572
  }
5326
- async function listChaptersEffective(videoId, options = {}) {
5327
- const url = buildApiUrl(
5328
- `/v1/videos/${videoId}/chapters/effective`,
5329
- options.baseUrl
5330
- );
5331
- const response = await fetch(url, {
5332
- method: "GET",
5333
- credentials: "include",
5334
- headers: {
5335
- ...getAuthHeaders(options.authToken)
5336
- }
5337
- });
5338
- const data = await handleResponse(response);
5339
- return Array.isArray(data) ? data : [];
5340
- }
5341
5573
  async function saveChapters(videoId, chapters, options = {}) {
5342
5574
  const url = buildApiUrl(`/v1/videos/${videoId}/chapters`, options.baseUrl);
5343
5575
  const response = await fetch(url, {
@@ -5465,9 +5697,7 @@ function ChapterRow({
5465
5697
  /* @__PURE__ */ jsxs(
5466
5698
  Stack,
5467
5699
  {
5468
- gap: 10,
5469
5700
  className: "video-settings-modal-chapter-times",
5470
- style: { width: 72, flex: "0 0 72px" },
5471
5701
  children: [
5472
5702
  /* @__PURE__ */ jsx(
5473
5703
  TextInput,
@@ -5527,9 +5757,7 @@ function ChapterRow({
5527
5757
  /* @__PURE__ */ jsx(
5528
5758
  Flex,
5529
5759
  {
5530
- align: "center",
5531
5760
  className: "video-settings-modal-chapter-title",
5532
- style: { flex: 1, minWidth: 0, height: 82 },
5533
5761
  children: /* @__PURE__ */ jsx(
5534
5762
  Textarea,
5535
5763
  {
@@ -5566,10 +5794,7 @@ function ChapterRow({
5566
5794
  /* @__PURE__ */ jsx(
5567
5795
  Flex,
5568
5796
  {
5569
- align: "center",
5570
- justify: "center",
5571
5797
  className: "video-settings-modal-chapter-actions",
5572
- style: { width: 36, flex: "0 0 36px", height: 82 },
5573
5798
  children: /* @__PURE__ */ jsxs(Menu, { position: "bottom-end", children: [
5574
5799
  /* @__PURE__ */ jsx(Menu.Target, { children: /* @__PURE__ */ jsx(
5575
5800
  ActionIcon,
@@ -5595,6 +5820,8 @@ function ChaptersSection({
5595
5820
  videoId,
5596
5821
  initialChapters,
5597
5822
  nativeVideo,
5823
+ shakaPlayer,
5824
+ videoDuration: videoDurationProp,
5598
5825
  baseUrl,
5599
5826
  authToken,
5600
5827
  title: title2,
@@ -5639,50 +5866,32 @@ function ChaptersSection({
5639
5866
  setEditableChapters(toEditable(list));
5640
5867
  }, []);
5641
5868
  useEffect(() => {
5642
- if (!nativeVideo) {
5643
- setDurationSec(null);
5869
+ var _a;
5870
+ if (videoDurationProp != null && videoDurationProp > 0) {
5871
+ setDurationSec(videoDurationProp);
5644
5872
  return;
5645
5873
  }
5646
- const updateDuration = () => {
5647
- const d = nativeVideo.duration;
5648
- setDurationSec(Number.isFinite(d) && d > 0 ? d : null);
5649
- };
5650
- updateDuration();
5651
- nativeVideo.addEventListener("loadedmetadata", updateDuration);
5652
- nativeVideo.addEventListener("durationchange", updateDuration);
5653
- return () => {
5654
- nativeVideo.removeEventListener("loadedmetadata", updateDuration);
5655
- nativeVideo.removeEventListener("durationchange", updateDuration);
5656
- };
5657
- }, [nativeVideo]);
5658
- const loadEffective = useCallback(async () => {
5659
- if (!videoId) return;
5660
- setIsLoading(true);
5661
- setError(null);
5662
- setValidationError(null);
5663
5874
  try {
5664
- const next = await listChaptersEffective(videoId, apiOptions);
5665
- applyChapters(next);
5666
- } catch (err) {
5667
- setError(err instanceof Error ? err.message : errorLabel);
5668
- } finally {
5669
- setIsLoading(false);
5875
+ const range = (_a = shakaPlayer == null ? void 0 : shakaPlayer.seekRange) == null ? void 0 : _a.call(shakaPlayer);
5876
+ if (range && Number.isFinite(range.end) && range.end > 0) {
5877
+ setDurationSec(range.end);
5878
+ return;
5879
+ }
5880
+ } catch {
5670
5881
  }
5671
- }, [videoId, apiOptions, applyChapters, errorLabel]);
5672
- const reliableDurationSec = useMemo(() => {
5673
- if (durationSec == null) return null;
5674
- const maxChapterStart = editableChapters.reduce(
5675
- (max, ch) => Math.max(max, ch.startSec),
5676
- 0
5677
- );
5678
- if (maxChapterStart > durationSec) return null;
5679
- return durationSec;
5680
- }, [durationSec, editableChapters]);
5882
+ if (nativeVideo) {
5883
+ const d = nativeVideo.duration;
5884
+ if (Number.isFinite(d) && d > 0) {
5885
+ setDurationSec(d);
5886
+ return;
5887
+ }
5888
+ }
5889
+ }, [videoDurationProp, shakaPlayer, nativeVideo]);
5681
5890
  const getMaxAllowedSec = useCallback(() => {
5682
- if (reliableDurationSec == null) return null;
5683
- return Math.floor(reliableDurationSec);
5684
- }, [reliableDurationSec]);
5685
- const validateChapters = useCallback(() => {
5891
+ if (durationSec == null) return null;
5892
+ return Math.floor(durationSec);
5893
+ }, [durationSec]);
5894
+ useCallback(() => {
5686
5895
  const list = [...editableChapters].filter((ch) => ch.title.trim()).sort((a, b) => a.startSec - b.startSec);
5687
5896
  const maxAllowed = getMaxAllowedSec();
5688
5897
  for (let i = 0; i < list.length; i++) {
@@ -5700,21 +5909,19 @@ function ChaptersSection({
5700
5909
  const handleSave = useCallback(async () => {
5701
5910
  if (!videoId) return null;
5702
5911
  setValidationError(null);
5703
- if (!validateChapters()) {
5704
- setValidationError(invalidRangeLabel);
5705
- return null;
5706
- }
5707
5912
  setIsSaving(true);
5708
5913
  isSavingRef.current = true;
5709
5914
  setError(null);
5710
5915
  try {
5711
- const payload = editableChapters.filter((ch) => ch.title.trim()).sort((a, b) => a.startSec - b.startSec).map(({ startSec, title: title22 }) => ({ startSec, title: title22 }));
5916
+ const payload = editableChapters.filter((ch) => ch.title.trim()).filter((ch) => durationSec == null || ch.startSec < durationSec).sort((a, b) => a.startSec - b.startSec).map(({ startSec, title: title22 }) => ({ startSec, title: title22 }));
5712
5917
  await saveChapters(videoId, payload, apiOptions);
5713
5918
  hasLocalEditsRef.current = false;
5714
- await loadEffective();
5715
5919
  return payload;
5716
5920
  } catch (err) {
5717
- setError(err instanceof Error ? err.message : errorLabel);
5921
+ const msg = err instanceof Error ? err.message : errorLabel;
5922
+ if (!/exceeds/i.test(msg)) {
5923
+ setError(msg);
5924
+ }
5718
5925
  return null;
5719
5926
  } finally {
5720
5927
  setIsSaving(false);
@@ -5724,10 +5931,8 @@ function ChaptersSection({
5724
5931
  videoId,
5725
5932
  editableChapters,
5726
5933
  apiOptions,
5727
- loadEffective,
5728
5934
  errorLabel,
5729
- validateChapters,
5730
- invalidRangeLabel
5935
+ getMaxAllowedSec
5731
5936
  ]);
5732
5937
  useEffect(() => {
5733
5938
  onSave == null ? void 0 : onSave(handleSave);
@@ -5736,7 +5941,7 @@ function ChaptersSection({
5736
5941
  hasLocalEditsRef.current = true;
5737
5942
  setValidationError(null);
5738
5943
  setEditableChapters((prev) => {
5739
- const maxAllowed = reliableDurationSec != null ? Math.floor(reliableDurationSec) : null;
5944
+ const maxAllowed = durationSec != null ? Math.floor(durationSec) : null;
5740
5945
  const sorted = [...prev].sort((a, b) => a.startSec - b.startSec);
5741
5946
  const lastStart = sorted.length > 0 ? Math.floor(sorted[sorted.length - 1].startSec) : -1;
5742
5947
  const nextStart = Math.max(0, lastStart + 1);
@@ -5807,11 +6012,8 @@ function ChaptersSection({
5807
6012
  }
5808
6013
  if (Array.isArray(initialChapters)) {
5809
6014
  applyChapters(initialChapters);
5810
- return;
5811
6015
  }
5812
- loadEffective().catch(() => {
5813
- });
5814
- }, [videoId, initialChapters, applyChapters, loadEffective]);
6016
+ }, [videoId, initialChapters, applyChapters]);
5815
6017
  return /* @__PURE__ */ jsxs(Stack, { gap: "xs", children: [
5816
6018
  /* @__PURE__ */ jsx(
5817
6019
  Text,
@@ -5851,7 +6053,7 @@ function ChaptersSection({
5851
6053
  /* @__PURE__ */ jsx(Stack, { gap: 12, children: [...editableChapters].sort((a, b) => a.startSec - b.startSec).map((chapter, index, list) => {
5852
6054
  const next = list[index + 1] || null;
5853
6055
  const nextNext = list[index + 2] || null;
5854
- const maxAllowed = reliableDurationSec != null ? Math.floor(reliableDurationSec) : null;
6056
+ const maxAllowed = durationSec != null ? Math.floor(durationSec) : null;
5855
6057
  const endSec = next != null ? Math.floor(next.startSec) : maxAllowed != null ? maxAllowed : null;
5856
6058
  const startSec = Math.floor(chapter.startSec);
5857
6059
  const prevStart = index > 0 ? Math.floor(list[index - 1].startSec) : null;
@@ -5879,7 +6081,7 @@ function ChaptersSection({
5879
6081
  endMax,
5880
6082
  disabled: false,
5881
6083
  startTimePlaceholder: timePlaceholder,
5882
- endTimePlaceholder: reliableDurationSec != null ? secondsToTimestamp(Math.floor(reliableDurationSec)) : endTimePlaceholder,
6084
+ endTimePlaceholder: durationSec != null ? secondsToTimestamp(Math.floor(durationSec)) : endTimePlaceholder,
5883
6085
  titlePlaceholder,
5884
6086
  deleteChapterLabel,
5885
6087
  onStartSecChange: handleStartSecChange,
@@ -5952,7 +6154,7 @@ function CoverSection({
5952
6154
  [(_a = status == null ? void 0 : status.cover) == null ? void 0 : _a.previewId, previews]
5953
6155
  );
5954
6156
  const effectiveSelected = selectedPreviewId ?? coverId;
5955
- const hasCoverCandidates = previews.length > 0;
6157
+ previews.length > 0;
5956
6158
  const slidesCount = previews.length + 1;
5957
6159
  const showControls = slidesCount > 3;
5958
6160
  const setPreviewLoaded = useCallback((id) => {
@@ -6000,7 +6202,7 @@ function CoverSection({
6000
6202
  }, [videoId]);
6001
6203
  const handleCoverDrop = useCallback(
6002
6204
  async (files) => {
6003
- var _a2, _b2, _c;
6205
+ var _a2, _b2, _c, _d;
6004
6206
  const file = files[0];
6005
6207
  if (!file || !onCoverUpload) return;
6006
6208
  const objectUrl = URL.createObjectURL(file);
@@ -6010,7 +6212,6 @@ function CoverSection({
6010
6212
  onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(objectUrl);
6011
6213
  try {
6012
6214
  const nextStatus = await Promise.resolve(onCoverUpload(file));
6013
- console.log("cover upload response", nextStatus);
6014
6215
  setOptimisticPreviewUrl(null);
6015
6216
  if (objectUrl) URL.revokeObjectURL(objectUrl);
6016
6217
  const nextPreviews = nextStatus == null ? void 0 : nextStatus.previews;
@@ -6018,10 +6219,16 @@ function CoverSection({
6018
6219
  if (nextStatus && hasPreviews && nextPreviews) {
6019
6220
  scrollToEndAfterUpdateRef.current = true;
6020
6221
  setStatus(nextStatus);
6021
- const fallbackId = ((_a2 = nextPreviews[0]) == null ? void 0 : _a2.id) ?? ((_b2 = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _b2.previewId);
6022
- setSelectedPreviewId(
6023
- ((_c = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _c.previewId) ?? fallbackId ?? "uploaded"
6024
- );
6222
+ const uploadedPreviewId = ((_a2 = nextPreviews[nextPreviews.length - 1]) == null ? void 0 : _a2.id) ?? ((_b2 = nextStatus == null ? void 0 : nextStatus.cover) == null ? void 0 : _b2.previewId) ?? ((_c = nextPreviews[0]) == null ? void 0 : _c.id);
6223
+ setSelectedPreviewId(uploadedPreviewId ?? "uploaded");
6224
+ if (uploadedPreviewId) {
6225
+ const uploadedUrl = (_d = nextPreviews.find(
6226
+ (p) => p.id === uploadedPreviewId
6227
+ )) == null ? void 0 : _d.url;
6228
+ if (uploadedUrl) {
6229
+ onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(uploadedUrl);
6230
+ }
6231
+ }
6025
6232
  setLoadingIds(/* @__PURE__ */ new Set());
6026
6233
  setErrorIds(/* @__PURE__ */ new Set());
6027
6234
  } else {
@@ -6038,7 +6245,7 @@ function CoverSection({
6038
6245
  loadStatus();
6039
6246
  }
6040
6247
  },
6041
- [onCoverUpload, loadStatus]
6248
+ [onCoverUpload, loadStatus, onSelectedPosterChange]
6042
6249
  );
6043
6250
  const selectedUrl = useMemo(() => {
6044
6251
  var _a2, _b2;
@@ -6159,6 +6366,7 @@ function CoverSection({
6159
6366
  className: `video-settings-modal-cover-cell video-settings-modal-cover-thumb ${isSelected && !isError ? "selected" : ""} ${isLoading2 ? "loading" : ""} ${isError ? "error" : ""}`,
6160
6367
  onClick: () => {
6161
6368
  if (isError) return;
6369
+ console.log("[Cover] click preview:", preview.id, preview.url);
6162
6370
  setSelectedPreviewId(preview.id);
6163
6371
  onSelectedPosterChange == null ? void 0 : onSelectedPosterChange(preview.url);
6164
6372
  },
@@ -6208,15 +6416,15 @@ function CoverSection({
6208
6416
  })
6209
6417
  ]
6210
6418
  }
6211
- ),
6212
- !hasCoverCandidates && !isLoading && !loadError && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: emptyCoverLabel })
6419
+ )
6213
6420
  ] });
6214
6421
  }
6215
6422
  function FooterActions({
6216
6423
  cancelLabel,
6217
6424
  saveLabel,
6218
6425
  onCancel,
6219
- onSave
6426
+ onSave,
6427
+ isSaving
6220
6428
  }) {
6221
6429
  return /* @__PURE__ */ jsxs(Group, { justify: "flex-end", gap: 8, children: [
6222
6430
  /* @__PURE__ */ jsx(
@@ -6227,6 +6435,7 @@ function FooterActions({
6227
6435
  color: "gray",
6228
6436
  radius: "md",
6229
6437
  onClick: onCancel,
6438
+ disabled: isSaving,
6230
6439
  children: cancelLabel
6231
6440
  }
6232
6441
  ),
@@ -6234,9 +6443,9 @@ function FooterActions({
6234
6443
  Button,
6235
6444
  {
6236
6445
  size: "sm",
6237
- color: "blue",
6238
6446
  radius: "md",
6239
6447
  onClick: onSave,
6448
+ loading: isSaving,
6240
6449
  children: saveLabel
6241
6450
  }
6242
6451
  )
@@ -6276,23 +6485,29 @@ const LANGUAGE_LABELS = LANGUAGE_OPTIONS.reduce(
6276
6485
  );
6277
6486
  function ManualSubtitlesPanel({
6278
6487
  videoId,
6279
- mode,
6280
- shakaPlayer,
6281
- nativeVideo,
6282
6488
  baseUrl,
6283
6489
  authToken,
6284
6490
  statusSubtitles,
6285
- onTracksChange
6491
+ onTracksChange,
6492
+ onSave
6286
6493
  }) {
6287
6494
  const { t } = useTranslation();
6495
+ const { reportError } = useVideoPluginContext();
6288
6496
  const dependencies = useVideoPluginDependencies();
6289
6497
  const SelectComponent = dependencies.Select || Select;
6290
6498
  const [tracks, setTracks] = useState([]);
6291
6499
  const [drafts, setDrafts] = useState([]);
6292
- const [loadError, setLoadError] = useState(null);
6293
6500
  const uploadControllersRef = useRef(/* @__PURE__ */ new Map());
6294
- const shakaAddedIdsRef = useRef([]);
6295
- const shakaFilterRegisteredRef = useRef(false);
6501
+ const [autoSubtitlesDisabled, setAutoSubtitlesDisabled] = useState(false);
6502
+ const autoTrack = useMemo(
6503
+ () => tracks.find(isAutoGeneratedTrack) ?? null,
6504
+ [tracks]
6505
+ );
6506
+ useEffect(() => {
6507
+ if (autoTrack) {
6508
+ setAutoSubtitlesDisabled(autoTrack.isDisabled);
6509
+ }
6510
+ }, [autoTrack]);
6296
6511
  const apiOptions = useMemo(
6297
6512
  () => ({
6298
6513
  baseUrl,
@@ -6300,26 +6515,54 @@ function ManualSubtitlesPanel({
6300
6515
  }),
6301
6516
  [baseUrl, authToken]
6302
6517
  );
6518
+ const handleAutoSubtitlesToggle = useCallback(
6519
+ (disabled2) => {
6520
+ if (!autoTrack) return;
6521
+ setAutoSubtitlesDisabled(disabled2);
6522
+ setTracks(
6523
+ (prev) => prev.map(
6524
+ (t2) => t2.id === autoTrack.id ? { ...t2, isDisabled: disabled2 } : t2
6525
+ )
6526
+ );
6527
+ },
6528
+ [autoTrack]
6529
+ );
6530
+ const initialAutoDisabledRef = useRef(null);
6531
+ useEffect(() => {
6532
+ if (autoTrack && initialAutoDisabledRef.current === null) {
6533
+ initialAutoDisabledRef.current = autoTrack.isDisabled;
6534
+ }
6535
+ }, [autoTrack]);
6303
6536
  const refreshTracks = useCallback(
6304
6537
  async (forceApi = false) => {
6305
6538
  if (!videoId) return;
6306
- if (!forceApi && statusSubtitles != null) {
6307
- const nextTracks = (statusSubtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6308
- setTracks(nextTracks);
6309
- setLoadError(null);
6310
- return;
6539
+ if (forceApi || statusSubtitles == null) {
6540
+ try {
6541
+ const freshTracks = await listTracks(videoId, apiOptions);
6542
+ setTracks(freshTracks);
6543
+ return;
6544
+ } catch {
6545
+ }
6311
6546
  }
6312
6547
  try {
6313
- const status = await fetchStatus(videoId);
6314
- const nextTracks = ((status == null ? void 0 : status.subtitles) || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6315
- setTracks(nextTracks);
6316
- setLoadError(null);
6317
- } catch (error) {
6318
- const message = error instanceof Error ? error.message : t("editor.video.errors.uploadFailed");
6319
- setLoadError(message);
6548
+ const freshTracks = await listTracks(videoId, apiOptions);
6549
+ setTracks(freshTracks);
6550
+ } catch {
6551
+ if (statusSubtitles != null) {
6552
+ const nextTracks = (statusSubtitles || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6553
+ setTracks(nextTracks);
6554
+ } else {
6555
+ try {
6556
+ const status = await fetchStatus(videoId);
6557
+ const nextTracks = ((status == null ? void 0 : status.subtitles) || []).map(normalizeSubtitleTrack).filter((track) => track.id);
6558
+ setTracks(nextTracks);
6559
+ } catch (error) {
6560
+ reportError(error, "subtitles", videoId);
6561
+ }
6562
+ }
6320
6563
  }
6321
6564
  },
6322
- [videoId, t, statusSubtitles]
6565
+ [videoId, apiOptions, reportError, statusSubtitles]
6323
6566
  );
6324
6567
  useEffect(() => {
6325
6568
  if (!videoId) {
@@ -6333,18 +6576,44 @@ function ManualSubtitlesPanel({
6333
6576
  useEffect(() => {
6334
6577
  onTracksChange == null ? void 0 : onTracksChange(tracks);
6335
6578
  }, [tracks, onTracksChange]);
6579
+ useEffect(() => {
6580
+ onSave == null ? void 0 : onSave(async () => {
6581
+ if (!videoId || !autoTrack) return;
6582
+ if (autoSubtitlesDisabled !== initialAutoDisabledRef.current) {
6583
+ await patchTrackDisabled(videoId, autoTrack.id, autoSubtitlesDisabled, apiOptions);
6584
+ initialAutoDisabledRef.current = autoSubtitlesDisabled;
6585
+ }
6586
+ });
6587
+ }, [onSave, videoId, autoTrack, autoSubtitlesDisabled, apiOptions]);
6588
+ const manualTracks = useMemo(() => {
6589
+ const manual = tracks.filter((track) => !isAutoGeneratedTrack(track));
6590
+ const byLang = /* @__PURE__ */ new Map();
6591
+ for (const track of manual) {
6592
+ byLang.set(track.lang, track);
6593
+ }
6594
+ return Array.from(byLang.values());
6595
+ }, [tracks]);
6596
+ const usedLanguages = useMemo(() => {
6597
+ const langs = new Set(manualTracks.map((t2) => t2.lang));
6598
+ drafts.forEach((d) => {
6599
+ if (d.lang) langs.add(d.lang);
6600
+ });
6601
+ return langs;
6602
+ }, [manualTracks, drafts]);
6336
6603
  const handleAddDraft = useCallback(() => {
6337
- const defaultLabel = LANGUAGE_LABELS[DEFAULT_LANG] || DEFAULT_LANG;
6604
+ var _a;
6605
+ const availableLang = ((_a = LANGUAGE_OPTIONS.find((opt) => !usedLanguages.has(opt.value))) == null ? void 0 : _a.value) ?? (usedLanguages.has(DEFAULT_LANG) ? null : DEFAULT_LANG);
6606
+ const label = availableLang ? LANGUAGE_LABELS[availableLang] || availableLang : null;
6338
6607
  setDrafts((prev) => [
6339
6608
  ...prev,
6340
6609
  {
6341
6610
  id: crypto.randomUUID(),
6342
- lang: DEFAULT_LANG,
6343
- label: defaultLabel,
6611
+ lang: availableLang,
6612
+ label,
6344
6613
  status: "draft"
6345
6614
  }
6346
6615
  ]);
6347
- }, []);
6616
+ }, [usedLanguages]);
6348
6617
  const updateDraft = useCallback(
6349
6618
  (id, updates) => {
6350
6619
  setDrafts(
@@ -6388,12 +6657,13 @@ function ManualSubtitlesPanel({
6388
6657
  } else {
6389
6658
  await refreshTracks(true);
6390
6659
  }
6391
- } catch {
6660
+ } catch (error) {
6392
6661
  uploadControllersRef.current.delete(draftId);
6393
6662
  updateDraft(draftId, { status: "error" });
6663
+ reportError(error, "subtitles", videoId);
6394
6664
  }
6395
6665
  },
6396
- [videoId, drafts, apiOptions, refreshTracks, updateDraft]
6666
+ [videoId, drafts, apiOptions, refreshTracks, updateDraft, reportError]
6397
6667
  );
6398
6668
  const handleDelete = useCallback(
6399
6669
  async (trackId) => {
@@ -6403,96 +6673,38 @@ function ManualSubtitlesPanel({
6403
6673
  },
6404
6674
  [videoId, apiOptions]
6405
6675
  );
6406
- useEffect(() => {
6407
- if (mode !== "native" || !nativeVideo) return;
6408
- const existingTracks = nativeVideo.querySelectorAll(
6409
- 'track[data-subtitle-track="managed"]'
6410
- );
6411
- existingTracks.forEach((node) => node.remove());
6412
- tracks.forEach((track) => {
6413
- const element = document.createElement("track");
6414
- element.setAttribute("data-subtitle-track", "managed");
6415
- element.kind = track.kind || "subtitles";
6416
- element.label = track.label;
6417
- element.srclang = track.lang;
6418
- element.src = resolveSubtitleUrl(videoId || "", track, apiOptions);
6419
- element.default = track.isDefault;
6420
- nativeVideo.appendChild(element);
6421
- });
6422
- }, [mode, nativeVideo, tracks, videoId, apiOptions]);
6423
- useEffect(() => {
6424
- var _a;
6425
- if (mode !== "shaka" || !shakaPlayer) return;
6426
- const engine = (_a = shakaPlayer.getNetworkingEngine) == null ? void 0 : _a.call(shakaPlayer);
6427
- if (engine && !shakaFilterRegisteredRef.current) {
6428
- engine.registerRequestFilter((type, request) => {
6429
- request.allowCrossSiteCredentials = true;
6430
- if (authToken) {
6431
- request.headers = request.headers || {};
6432
- request.headers.Authorization = `Bearer ${authToken}`;
6433
- }
6434
- });
6435
- shakaFilterRegisteredRef.current = true;
6436
- }
6437
- if (shakaPlayer.getTextTracks && shakaPlayer.removeTextTrack) {
6438
- const existing = shakaPlayer.getTextTracks();
6439
- existing.forEach((track) => {
6440
- if (shakaAddedIdsRef.current.includes(track.id)) {
6441
- shakaPlayer.removeTextTrack(track);
6442
- }
6443
- });
6444
- shakaAddedIdsRef.current = [];
6445
- }
6446
- const addTracks = async () => {
6447
- var _a2, _b, _c;
6448
- const existingTracks = ((_a2 = shakaPlayer.getTextTracks) == null ? void 0 : _a2.call(shakaPlayer)) || [];
6449
- for (const track of tracks) {
6450
- const vttUrl = resolveSubtitleUrl(videoId || "", track, apiOptions);
6451
- const label = track.label || track.lang || "und";
6452
- const lang = track.lang || "und";
6453
- const kind = track.kind || "subtitles";
6454
- const alreadyExists = existingTracks.some(
6455
- (item) => (item.language || "") === lang && (item.label || "") === label && (item.kind || "subtitles") === kind
6456
- );
6457
- if (alreadyExists) {
6458
- continue;
6459
- }
6460
- let added = null;
6461
- if (typeof shakaPlayer.addTextTrackAsync === "function") {
6462
- added = await shakaPlayer.addTextTrackAsync(
6463
- vttUrl,
6464
- lang,
6465
- kind,
6466
- label
6467
- );
6468
- } else if (typeof shakaPlayer.addTextTrack === "function") {
6469
- added = shakaPlayer.addTextTrack(vttUrl, lang, kind, label);
6470
- }
6471
- if (added && typeof added.id === "number") {
6472
- shakaAddedIdsRef.current.push(added.id);
6473
- existingTracks.push(added);
6474
- }
6475
- }
6476
- const defaultTrack = tracks.find((item) => item.isDefault);
6477
- if (defaultTrack && shakaPlayer.selectTextTrack) {
6478
- const list = ((_b = shakaPlayer.getTextTracks) == null ? void 0 : _b.call(shakaPlayer)) || [];
6479
- const target = list.find(
6480
- (item) => item.language === defaultTrack.lang && item.label === defaultTrack.label
6481
- );
6482
- if (target) {
6483
- (_c = shakaPlayer.setTextTrackVisibility) == null ? void 0 : _c.call(shakaPlayer, true);
6484
- shakaPlayer.selectTextTrack(target);
6485
- }
6486
- }
6487
- };
6488
- addTracks().catch(() => {
6489
- });
6490
- }, [mode, shakaPlayer, tracks, videoId, apiOptions, authToken]);
6491
6676
  const handleRemoveDraft = useCallback((draftId) => {
6492
6677
  setDrafts((prev) => prev.filter((item) => item.id !== draftId));
6493
6678
  }, []);
6494
6679
  return /* @__PURE__ */ jsxs("div", { className: "video-settings-modal-subtitles", children: [
6495
- loadError && /* @__PURE__ */ jsx(Text, { size: "sm", c: "red", mb: "xs", children: loadError }),
6680
+ autoTrack && /* @__PURE__ */ jsxs(
6681
+ Flex,
6682
+ {
6683
+ align: "center",
6684
+ gap: "md",
6685
+ className: "video-settings-modal-subtitles-auto-toggle",
6686
+ children: [
6687
+ /* @__PURE__ */ jsx(
6688
+ Text,
6689
+ {
6690
+ size: "sm",
6691
+ fw: 500,
6692
+ c: "var(--mantine-color-bright)",
6693
+ flex: 1,
6694
+ children: t("editor.video.settingsModal.hideAutoSubtitles")
6695
+ }
6696
+ ),
6697
+ /* @__PURE__ */ jsx(
6698
+ Switch,
6699
+ {
6700
+ size: "sm",
6701
+ checked: autoSubtitlesDisabled,
6702
+ onChange: (event) => handleAutoSubtitlesToggle(event.currentTarget.checked)
6703
+ }
6704
+ )
6705
+ ]
6706
+ }
6707
+ ),
6496
6708
  /* @__PURE__ */ jsx(
6497
6709
  Text,
6498
6710
  {
@@ -6502,7 +6714,7 @@ function ManualSubtitlesPanel({
6502
6714
  children: t("editor.video.settingsModal.uploadSubtitlesManually")
6503
6715
  }
6504
6716
  ),
6505
- tracks.filter((track) => !track.isDefault).map((track) => /* @__PURE__ */ jsxs(
6717
+ manualTracks.map((track) => /* @__PURE__ */ jsxs(
6506
6718
  Flex,
6507
6719
  {
6508
6720
  align: "center",
@@ -6555,6 +6767,7 @@ function ManualSubtitlesPanel({
6555
6767
  DraftRow,
6556
6768
  {
6557
6769
  draft,
6770
+ usedLanguages,
6558
6771
  onChange: (updates) => updateDraft(draft.id, updates),
6559
6772
  onUpload: (file) => handleUploadDraft(draft.id, file),
6560
6773
  onRemove: () => handleRemoveDraft(draft.id),
@@ -6574,9 +6787,15 @@ function ManualSubtitlesPanel({
6574
6787
  )
6575
6788
  ] });
6576
6789
  }
6577
- function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6790
+ function DraftRow({ draft, usedLanguages, onChange, onUpload, onRemove, SelectComponent }) {
6578
6791
  const { t } = useTranslation();
6579
6792
  const inputRef = useRef(null);
6793
+ const availableLanguages = useMemo(
6794
+ () => LANGUAGE_OPTIONS.filter(
6795
+ (opt) => opt.value === draft.lang || !usedLanguages.has(opt.value)
6796
+ ),
6797
+ [usedLanguages, draft.lang]
6798
+ );
6580
6799
  const handleFileChange = (e) => {
6581
6800
  var _a;
6582
6801
  const file = (_a = e.target.files) == null ? void 0 : _a[0];
@@ -6588,7 +6807,6 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6588
6807
  Flex,
6589
6808
  {
6590
6809
  align: "center",
6591
- gap: "md",
6592
6810
  wrap: "nowrap",
6593
6811
  className: "video-settings-modal-subtitles-row",
6594
6812
  children: [
@@ -6597,7 +6815,7 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6597
6815
  {
6598
6816
  size: "sm",
6599
6817
  placeholder: t("editor.video.settingsModal.selectLanguage"),
6600
- data: LANGUAGE_OPTIONS,
6818
+ data: availableLanguages,
6601
6819
  value: draft.lang,
6602
6820
  onChange: (value) => onChange({
6603
6821
  lang: value,
@@ -6610,7 +6828,7 @@ function DraftRow({ draft, onChange, onUpload, onRemove, SelectComponent }) {
6610
6828
  inputClassName: "video-settings-modal-language-select"
6611
6829
  }
6612
6830
  ),
6613
- /* @__PURE__ */ jsx(Flex, { align: "center", gap: 6, flex: 1, children: draft.status === "uploading" ? /* @__PURE__ */ jsx(Loader, { size: 22, color: "blue" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6831
+ /* @__PURE__ */ jsx(Flex, { align: "center", gap: 6, flex: 1, children: draft.status === "uploading" ? /* @__PURE__ */ jsx(Loader, { size: 22 }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6614
6832
  /* @__PURE__ */ jsx(
6615
6833
  "input",
6616
6834
  {
@@ -6668,6 +6886,7 @@ function VideoSettingsModal({
6668
6886
  chaptersDisabled = false,
6669
6887
  shakaPlayer,
6670
6888
  nativeVideo,
6889
+ videoDuration,
6671
6890
  playerMode = "native"
6672
6891
  }) {
6673
6892
  const { t } = useTranslation();
@@ -6682,17 +6901,24 @@ function VideoSettingsModal({
6682
6901
  const saveChaptersRef = useRef(
6683
6902
  null
6684
6903
  );
6904
+ const saveSubtitlesRef = useRef(null);
6905
+ const [isSaving, setIsSaving] = useState(false);
6685
6906
  const handlePosterSelect = useCallback((url) => {
6907
+ console.log("[Cover] handlePosterSelect:", url);
6686
6908
  setSelectedPosterUrl(url);
6687
6909
  }, []);
6688
6910
  useEffect(() => {
6689
- if (!opened || !videoId) {
6690
- setStatus(null);
6691
- return;
6911
+ if (initialStatus) {
6912
+ setStatus(initialStatus);
6692
6913
  }
6693
- fetchStatus(videoId).then((nextStatus) => {
6694
- setStatus(nextStatus);
6695
- onStatusChange == null ? void 0 : onStatusChange(nextStatus);
6914
+ }, [initialStatus]);
6915
+ useEffect(() => {
6916
+ if (!opened || !videoId) return;
6917
+ fetchStatus(videoId).then((freshStatus) => {
6918
+ if (freshStatus) {
6919
+ setStatus(freshStatus);
6920
+ onStatusChange == null ? void 0 : onStatusChange(freshStatus);
6921
+ }
6696
6922
  }).catch(() => {
6697
6923
  });
6698
6924
  }, [opened, videoId]);
@@ -6708,28 +6934,47 @@ function VideoSettingsModal({
6708
6934
  [videoId]
6709
6935
  );
6710
6936
  const handleSave = useCallback(async () => {
6711
- if (saveChaptersRef.current) {
6712
- const payload = await saveChaptersRef.current();
6713
- if (!Array.isArray(payload)) return;
6714
- onChaptersSaved == null ? void 0 : onChaptersSaved(payload);
6715
- }
6716
- onChaptersDisabledChange == null ? void 0 : onChaptersDisabledChange(chaptersDisabledValue);
6717
- onSubtitlesSaved == null ? void 0 : onSubtitlesSaved(subtitlesSnapshot);
6718
- if (selectedPosterUrl !== null) {
6719
- onPosterChange == null ? void 0 : onPosterChange(selectedPosterUrl);
6937
+ setIsSaving(true);
6938
+ try {
6939
+ if (saveChaptersRef.current) {
6940
+ const payload = await saveChaptersRef.current();
6941
+ if (Array.isArray(payload)) {
6942
+ onChaptersSaved == null ? void 0 : onChaptersSaved(payload);
6943
+ }
6944
+ }
6945
+ onChaptersDisabledChange == null ? void 0 : onChaptersDisabledChange(chaptersDisabledValue);
6946
+ if (saveSubtitlesRef.current) {
6947
+ await saveSubtitlesRef.current();
6948
+ }
6949
+ onSubtitlesSaved == null ? void 0 : onSubtitlesSaved(subtitlesSnapshot);
6950
+ console.log("[Cover] handleSave: selectedPosterUrl=", selectedPosterUrl, "onPosterChange=", !!onPosterChange);
6951
+ if (selectedPosterUrl !== null) {
6952
+ console.log("[Cover] calling onPosterChange with:", selectedPosterUrl);
6953
+ onPosterChange == null ? void 0 : onPosterChange(selectedPosterUrl);
6954
+ } else {
6955
+ console.log("[Cover] selectedPosterUrl is null, skipping onPosterChange");
6956
+ }
6957
+ if (videoId) {
6958
+ const updatedStatus = await fetchStatus(videoId);
6959
+ onStatusChange == null ? void 0 : onStatusChange(updatedStatus);
6960
+ }
6961
+ onClose();
6962
+ } finally {
6963
+ setIsSaving(false);
6720
6964
  }
6721
- onClose();
6722
6965
  }, [
6966
+ videoId,
6723
6967
  chaptersDisabledValue,
6724
6968
  onChaptersDisabledChange,
6725
6969
  onChaptersSaved,
6726
6970
  onClose,
6727
6971
  onPosterChange,
6972
+ onStatusChange,
6728
6973
  onSubtitlesSaved,
6729
6974
  selectedPosterUrl,
6730
6975
  subtitlesSnapshot
6731
6976
  ]);
6732
- return /* @__PURE__ */ jsx(
6977
+ return /* @__PURE__ */ jsxs(
6733
6978
  Modal,
6734
6979
  {
6735
6980
  opened,
@@ -6740,13 +6985,14 @@ function VideoSettingsModal({
6740
6985
  withinPortal: true,
6741
6986
  zIndex: 300,
6742
6987
  className: "video-settings-modal",
6743
- children: /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-body", children: /* @__PURE__ */ jsxs(Stack, { gap: 24, children: [
6744
- /* @__PURE__ */ jsxs(Stack, { gap: 32, children: [
6988
+ children: [
6989
+ /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-body", children: /* @__PURE__ */ jsxs(Stack, { gap: 32, children: [
6745
6990
  /* @__PURE__ */ jsx(
6746
6991
  CoverSection,
6747
6992
  {
6748
6993
  opened,
6749
6994
  videoId,
6995
+ initialStatus: status,
6750
6996
  title: t("editor.video.settingsModal.cover"),
6751
6997
  uploadLabel: t("editor.video.settingsModal.uploadFile"),
6752
6998
  cropLabel: t("editor.video.settingsModal.cropCoverImage"),
@@ -6785,7 +7031,10 @@ function VideoSettingsModal({
6785
7031
  shakaPlayer,
6786
7032
  nativeVideo,
6787
7033
  statusSubtitles: status == null ? void 0 : status.subtitles,
6788
- onTracksChange: setSubtitlesSnapshot
7034
+ onTracksChange: setSubtitlesSnapshot,
7035
+ onSave: (handler) => {
7036
+ saveSubtitlesRef.current = handler;
7037
+ }
6789
7038
  }
6790
7039
  )
6791
7040
  ] }),
@@ -6795,6 +7044,8 @@ function VideoSettingsModal({
6795
7044
  videoId,
6796
7045
  initialChapters: status == null ? void 0 : status.chapters,
6797
7046
  nativeVideo,
7047
+ shakaPlayer,
7048
+ videoDuration,
6798
7049
  title: t("editor.video.settingsModal.chapters"),
6799
7050
  customLabel: t("editor.video.settingsModal.chaptersCustom"),
6800
7051
  dontShowLabel: t("editor.video.settingsModal.chaptersDontShow"),
@@ -6824,17 +7075,18 @@ function VideoSettingsModal({
6824
7075
  )
6825
7076
  }
6826
7077
  )
6827
- ] }),
6828
- /* @__PURE__ */ jsx(
7078
+ ] }) }),
7079
+ /* @__PURE__ */ jsx(Box, { className: "video-settings-modal-footer", children: /* @__PURE__ */ jsx(
6829
7080
  FooterActions,
6830
7081
  {
6831
7082
  cancelLabel: t("editor.video.settingsModal.cancel"),
6832
7083
  saveLabel: t("editor.video.settingsModal.save"),
6833
7084
  onCancel: onClose,
6834
- onSave: handleSave
7085
+ onSave: handleSave,
7086
+ isSaving
6835
7087
  }
6836
- )
6837
- ] }) })
7088
+ ) })
7089
+ ]
6838
7090
  }
6839
7091
  );
6840
7092
  }
@@ -6875,7 +7127,8 @@ function VideoBlock({
6875
7127
  shakaPlayer: null,
6876
7128
  nativeVideo: null,
6877
7129
  mode: "native",
6878
- containerEl: null
7130
+ containerEl: null,
7131
+ videoDuration: null
6879
7132
  });
6880
7133
  const fallbackNativeVideoUrl = useMemo(() => {
6881
7134
  if (platform === "download" || platform === "link") {
@@ -6919,6 +7172,7 @@ function VideoBlock({
6919
7172
  };
6920
7173
  const handlePosterChange = useCallback(
6921
7174
  (posterUrl2) => {
7175
+ console.log("[Cover] handlePosterChange: setting posterUrl=", posterUrl2);
6922
7176
  setPosterUrl(posterUrl2);
6923
7177
  setVideoPoster(editor, nodeKey, posterUrl2);
6924
7178
  },
@@ -6947,7 +7201,7 @@ function VideoBlock({
6947
7201
  });
6948
7202
  }, [editor, nodeKey]);
6949
7203
  const resolvedChaptersOverride = chaptersDisabled ? [] : chaptersOverride;
6950
- const TrashIcon = /* @__PURE__ */ jsx(HugeiconsIcon, { icon: tI, size: 16, color: "var(--mantine-color-red-outline)" });
7204
+ const TrashIcon = /* @__PURE__ */ jsx("span", { style: { color: "var(--mantine-color-red-outline)", display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(HugeiconsIcon, { icon: tI, size: 16 }) });
6951
7205
  const uploadComponentProps = {
6952
7206
  className,
6953
7207
  format,
@@ -6986,7 +7240,7 @@ function VideoBlock({
6986
7240
  event.stopPropagation();
6987
7241
  setSettingsModalOpened(true);
6988
7242
  },
6989
- children: /* @__PURE__ */ jsx(SettingsIcon, { size: 16 })
7243
+ children: /* @__PURE__ */ jsx("span", { style: { color: "var(--mantine-color-gray-text)", display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx(SettingsIcon, { size: 16 }) })
6990
7244
  }
6991
7245
  ) }),
6992
7246
  /* @__PURE__ */ jsx(
@@ -7005,7 +7259,8 @@ function VideoBlock({
7005
7259
  onChaptersDisabledChange: handleChaptersDisabledChange,
7006
7260
  shakaPlayer: playerInfo.shakaPlayer,
7007
7261
  nativeVideo: playerInfo.nativeVideo,
7008
- playerMode: playerInfo.mode
7262
+ playerMode: playerInfo.mode,
7263
+ videoDuration: playerInfo.videoDuration
7009
7264
  }
7010
7265
  )
7011
7266
  ] }),