@lightbird/core 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +144 -6
- package/dist/index.d.cts +51 -3
- package/dist/index.d.ts +51 -3
- package/dist/index.js +142 -7
- package/dist/react/index.cjs +313 -0
- package/dist/react/index.d.cts +114 -2
- package/dist/react/index.d.ts +114 -2
- package/dist/react/index.js +311 -1
- package/package.json +2 -1
package/dist/react/index.cjs
CHANGED
|
@@ -1143,6 +1143,317 @@ function useMagnet() {
|
|
|
1143
1143
|
return { torrentStatus, addMagnet, destroyMagnet };
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
|
+
// src/utils/video-thumbnail.ts
|
|
1147
|
+
async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
1148
|
+
return new Promise((resolve) => {
|
|
1149
|
+
const canvas = document.createElement("canvas");
|
|
1150
|
+
canvas.width = width;
|
|
1151
|
+
canvas.height = height;
|
|
1152
|
+
const ctx = canvas.getContext("2d");
|
|
1153
|
+
if (!ctx) {
|
|
1154
|
+
resolve(null);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
let settled = false;
|
|
1158
|
+
const cleanup = () => {
|
|
1159
|
+
videoEl.removeEventListener("seeked", onSeeked);
|
|
1160
|
+
videoEl.removeEventListener("error", onError);
|
|
1161
|
+
videoEl.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
1162
|
+
};
|
|
1163
|
+
const finish = (value) => {
|
|
1164
|
+
if (settled) return;
|
|
1165
|
+
settled = true;
|
|
1166
|
+
cleanup();
|
|
1167
|
+
resolve(value);
|
|
1168
|
+
};
|
|
1169
|
+
const draw = () => {
|
|
1170
|
+
try {
|
|
1171
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1172
|
+
finish(canvas.toDataURL("image/jpeg", 0.6));
|
|
1173
|
+
} catch {
|
|
1174
|
+
finish(null);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
const seekTo = () => {
|
|
1178
|
+
const duration = videoEl.duration || 0;
|
|
1179
|
+
const target = duration > 0 ? Math.max(0, Math.min(timeSeconds, duration)) : Math.max(0, timeSeconds);
|
|
1180
|
+
if (Math.abs(videoEl.currentTime - target) < 0.05) {
|
|
1181
|
+
draw();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
videoEl.currentTime = target;
|
|
1186
|
+
} catch {
|
|
1187
|
+
finish(null);
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
const onSeeked = () => draw();
|
|
1191
|
+
const onError = () => finish(null);
|
|
1192
|
+
const onLoadedMetadata = () => seekTo();
|
|
1193
|
+
videoEl.addEventListener("seeked", onSeeked);
|
|
1194
|
+
videoEl.addEventListener("error", onError);
|
|
1195
|
+
if (videoEl.readyState >= 1) {
|
|
1196
|
+
seekTo();
|
|
1197
|
+
} else {
|
|
1198
|
+
videoEl.addEventListener("loadedmetadata", onLoadedMetadata, { once: true });
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/react/use-seek-preview.ts
|
|
1204
|
+
function useSeekPreview(videoRef, options = {}) {
|
|
1205
|
+
const { debounceMs = 120, width = 160, height = 90 } = options;
|
|
1206
|
+
const [thumbnail, setThumbnail] = react.useState(null);
|
|
1207
|
+
const [time, setTime] = react.useState(null);
|
|
1208
|
+
const offscreenRef = react.useRef(null);
|
|
1209
|
+
const loadedSrcRef = react.useRef("");
|
|
1210
|
+
const cacheRef = react.useRef(/* @__PURE__ */ new Map());
|
|
1211
|
+
const timerRef = react.useRef(null);
|
|
1212
|
+
const requestSeqRef = react.useRef(0);
|
|
1213
|
+
const syncOffscreenSource = react.useCallback(() => {
|
|
1214
|
+
const main = videoRef.current;
|
|
1215
|
+
const src = main?.currentSrc || main?.src || "";
|
|
1216
|
+
if (!src) return null;
|
|
1217
|
+
if (!offscreenRef.current) {
|
|
1218
|
+
const v = document.createElement("video");
|
|
1219
|
+
v.muted = true;
|
|
1220
|
+
v.preload = "metadata";
|
|
1221
|
+
v.crossOrigin = "anonymous";
|
|
1222
|
+
v.setAttribute("playsinline", "");
|
|
1223
|
+
offscreenRef.current = v;
|
|
1224
|
+
}
|
|
1225
|
+
const offscreen = offscreenRef.current;
|
|
1226
|
+
if (loadedSrcRef.current !== src) {
|
|
1227
|
+
loadedSrcRef.current = src;
|
|
1228
|
+
cacheRef.current.clear();
|
|
1229
|
+
offscreen.src = src;
|
|
1230
|
+
}
|
|
1231
|
+
return offscreen;
|
|
1232
|
+
}, [videoRef]);
|
|
1233
|
+
const clearPreview = react.useCallback(() => {
|
|
1234
|
+
if (timerRef.current) {
|
|
1235
|
+
clearTimeout(timerRef.current);
|
|
1236
|
+
timerRef.current = null;
|
|
1237
|
+
}
|
|
1238
|
+
requestSeqRef.current += 1;
|
|
1239
|
+
setThumbnail(null);
|
|
1240
|
+
setTime(null);
|
|
1241
|
+
}, []);
|
|
1242
|
+
const requestPreview = react.useCallback(
|
|
1243
|
+
(timeSeconds) => {
|
|
1244
|
+
const main = videoRef.current;
|
|
1245
|
+
if (!main) return;
|
|
1246
|
+
const duration = main.duration;
|
|
1247
|
+
const hasDuration = !!duration && !Number.isNaN(duration);
|
|
1248
|
+
const clamped = hasDuration ? Math.max(0, Math.min(timeSeconds, duration)) : Math.max(0, timeSeconds);
|
|
1249
|
+
setTime(clamped);
|
|
1250
|
+
if (!hasDuration) return;
|
|
1251
|
+
const seq = ++requestSeqRef.current;
|
|
1252
|
+
const bucket = Math.round(clamped);
|
|
1253
|
+
const cached = cacheRef.current.get(bucket);
|
|
1254
|
+
if (cached) {
|
|
1255
|
+
setThumbnail(cached);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1259
|
+
timerRef.current = setTimeout(() => {
|
|
1260
|
+
timerRef.current = null;
|
|
1261
|
+
const offscreen = syncOffscreenSource();
|
|
1262
|
+
if (!offscreen) return;
|
|
1263
|
+
captureFrameAt(offscreen, bucket, width, height).then((dataUrl) => {
|
|
1264
|
+
if (seq !== requestSeqRef.current) return;
|
|
1265
|
+
if (dataUrl) {
|
|
1266
|
+
cacheRef.current.set(bucket, dataUrl);
|
|
1267
|
+
setThumbnail(dataUrl);
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}, debounceMs);
|
|
1271
|
+
},
|
|
1272
|
+
[videoRef, syncOffscreenSource, debounceMs, width, height]
|
|
1273
|
+
);
|
|
1274
|
+
react.useEffect(() => {
|
|
1275
|
+
return () => {
|
|
1276
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1277
|
+
const offscreen = offscreenRef.current;
|
|
1278
|
+
if (offscreen) offscreen.removeAttribute("src");
|
|
1279
|
+
};
|
|
1280
|
+
}, []);
|
|
1281
|
+
return { thumbnail, time, requestPreview, clearPreview };
|
|
1282
|
+
}
|
|
1283
|
+
function useABLoop(videoRef) {
|
|
1284
|
+
const [pointA, setA] = react.useState(null);
|
|
1285
|
+
const [pointB, setB] = react.useState(null);
|
|
1286
|
+
const setPointA = react.useCallback(() => {
|
|
1287
|
+
const el = videoRef.current;
|
|
1288
|
+
if (!el) return;
|
|
1289
|
+
const t = el.currentTime;
|
|
1290
|
+
setA(t);
|
|
1291
|
+
setB((b) => b !== null && b <= t ? null : b);
|
|
1292
|
+
}, [videoRef]);
|
|
1293
|
+
const setPointB = react.useCallback(() => {
|
|
1294
|
+
const el = videoRef.current;
|
|
1295
|
+
if (!el || pointA === null) return;
|
|
1296
|
+
const t = el.currentTime;
|
|
1297
|
+
if (t <= pointA) return;
|
|
1298
|
+
setB(t);
|
|
1299
|
+
}, [videoRef, pointA]);
|
|
1300
|
+
const clear = react.useCallback(() => {
|
|
1301
|
+
setA(null);
|
|
1302
|
+
setB(null);
|
|
1303
|
+
}, []);
|
|
1304
|
+
react.useEffect(() => {
|
|
1305
|
+
const el = videoRef.current;
|
|
1306
|
+
if (!el || pointA === null || pointB === null) return;
|
|
1307
|
+
const onTimeUpdate = () => {
|
|
1308
|
+
if (el.currentTime >= pointB || el.currentTime < pointA) {
|
|
1309
|
+
el.currentTime = pointA;
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
el.addEventListener("timeupdate", onTimeUpdate);
|
|
1313
|
+
return () => el.removeEventListener("timeupdate", onTimeUpdate);
|
|
1314
|
+
}, [videoRef, pointA, pointB]);
|
|
1315
|
+
react.useEffect(() => {
|
|
1316
|
+
const el = videoRef.current;
|
|
1317
|
+
if (!el) return;
|
|
1318
|
+
const onReset = () => clear();
|
|
1319
|
+
el.addEventListener("loadstart", onReset);
|
|
1320
|
+
el.addEventListener("emptied", onReset);
|
|
1321
|
+
return () => {
|
|
1322
|
+
el.removeEventListener("loadstart", onReset);
|
|
1323
|
+
el.removeEventListener("emptied", onReset);
|
|
1324
|
+
};
|
|
1325
|
+
}, [videoRef, clear]);
|
|
1326
|
+
return {
|
|
1327
|
+
pointA,
|
|
1328
|
+
pointB,
|
|
1329
|
+
isLooping: pointA !== null && pointB !== null,
|
|
1330
|
+
setPointA,
|
|
1331
|
+
setPointB,
|
|
1332
|
+
clear
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
var SWIPE_THRESHOLD = 10;
|
|
1336
|
+
var DOUBLE_TAP_DISTANCE = 40;
|
|
1337
|
+
function useTouchGestures(targetRef, handlers, options = {}) {
|
|
1338
|
+
const { enabled = true, seekSeconds = 10, doubleTapMs = 300, feedbackMs = 700 } = options;
|
|
1339
|
+
const [feedback, setFeedback] = react.useState(null);
|
|
1340
|
+
const handlersRef = react.useRef(handlers);
|
|
1341
|
+
react.useEffect(() => {
|
|
1342
|
+
handlersRef.current = handlers;
|
|
1343
|
+
});
|
|
1344
|
+
const startRef = react.useRef(null);
|
|
1345
|
+
const phaseRef = react.useRef("idle");
|
|
1346
|
+
const swipeBaseRef = react.useRef(0);
|
|
1347
|
+
const lastTapRef = react.useRef(null);
|
|
1348
|
+
const feedbackTimerRef = react.useRef(null);
|
|
1349
|
+
const showFeedback = react.useCallback(
|
|
1350
|
+
(fb) => {
|
|
1351
|
+
setFeedback(fb);
|
|
1352
|
+
if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current);
|
|
1353
|
+
feedbackTimerRef.current = setTimeout(() => setFeedback(null), feedbackMs);
|
|
1354
|
+
},
|
|
1355
|
+
[feedbackMs]
|
|
1356
|
+
);
|
|
1357
|
+
react.useEffect(() => {
|
|
1358
|
+
const el = targetRef.current;
|
|
1359
|
+
if (!el || !enabled) return;
|
|
1360
|
+
const onTouchStart = (e) => {
|
|
1361
|
+
if (e.touches.length !== 1) {
|
|
1362
|
+
startRef.current = null;
|
|
1363
|
+
phaseRef.current = "ignore";
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const t = e.touches[0];
|
|
1367
|
+
const rect = el.getBoundingClientRect();
|
|
1368
|
+
const relX = t.clientX - rect.left;
|
|
1369
|
+
startRef.current = {
|
|
1370
|
+
x: t.clientX,
|
|
1371
|
+
y: t.clientY,
|
|
1372
|
+
zone: relX < rect.width / 2 ? "left" : "right",
|
|
1373
|
+
height: rect.height || 1
|
|
1374
|
+
};
|
|
1375
|
+
phaseRef.current = "pending";
|
|
1376
|
+
};
|
|
1377
|
+
const onTouchMove = (e) => {
|
|
1378
|
+
const start = startRef.current;
|
|
1379
|
+
if (!start || phaseRef.current === "ignore" || phaseRef.current === "idle") return;
|
|
1380
|
+
const t = e.touches[0];
|
|
1381
|
+
if (!t) return;
|
|
1382
|
+
const dx = t.clientX - start.x;
|
|
1383
|
+
const dy = t.clientY - start.y;
|
|
1384
|
+
if (phaseRef.current === "pending") {
|
|
1385
|
+
if (Math.hypot(dx, dy) < SWIPE_THRESHOLD) return;
|
|
1386
|
+
if (Math.abs(dy) > Math.abs(dx)) {
|
|
1387
|
+
phaseRef.current = "swipe";
|
|
1388
|
+
const h = handlersRef.current;
|
|
1389
|
+
swipeBaseRef.current = start.zone === "left" ? h.getBrightness?.() ?? 1 : h.getVolume?.() ?? 1;
|
|
1390
|
+
} else {
|
|
1391
|
+
phaseRef.current = "ignore";
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (phaseRef.current === "swipe") {
|
|
1396
|
+
if (e.cancelable) e.preventDefault();
|
|
1397
|
+
const fraction = -dy / start.height;
|
|
1398
|
+
const next = Math.max(0, Math.min(1, swipeBaseRef.current + fraction));
|
|
1399
|
+
const h = handlersRef.current;
|
|
1400
|
+
if (start.zone === "left") {
|
|
1401
|
+
h.setBrightness?.(next);
|
|
1402
|
+
showFeedback({ type: "brightness", value: next });
|
|
1403
|
+
} else {
|
|
1404
|
+
h.setVolume?.(next);
|
|
1405
|
+
showFeedback({ type: "volume", value: next });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
const onTouchEnd = (e) => {
|
|
1410
|
+
const start = startRef.current;
|
|
1411
|
+
const phase = phaseRef.current;
|
|
1412
|
+
startRef.current = null;
|
|
1413
|
+
phaseRef.current = "idle";
|
|
1414
|
+
if (!start || phase === "ignore") return;
|
|
1415
|
+
if (phase === "swipe") {
|
|
1416
|
+
lastTapRef.current = null;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const point = e.changedTouches[0];
|
|
1420
|
+
const px = point ? point.clientX : start.x;
|
|
1421
|
+
const py = point ? point.clientY : start.y;
|
|
1422
|
+
const now = Date.now();
|
|
1423
|
+
const last = lastTapRef.current;
|
|
1424
|
+
if (last && last.zone === start.zone && now - last.time < doubleTapMs && Math.hypot(px - last.x, py - last.y) < DOUBLE_TAP_DISTANCE) {
|
|
1425
|
+
lastTapRef.current = null;
|
|
1426
|
+
if (e.cancelable) e.preventDefault();
|
|
1427
|
+
const h = handlersRef.current;
|
|
1428
|
+
if (start.zone === "left") {
|
|
1429
|
+
h.seekBy?.(-seekSeconds);
|
|
1430
|
+
showFeedback({ type: "seek", direction: "backward", seconds: seekSeconds });
|
|
1431
|
+
} else {
|
|
1432
|
+
h.seekBy?.(seekSeconds);
|
|
1433
|
+
showFeedback({ type: "seek", direction: "forward", seconds: seekSeconds });
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
lastTapRef.current = { x: px, y: py, time: now, zone: start.zone };
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
el.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
1440
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
1441
|
+
el.addEventListener("touchend", onTouchEnd, { passive: false });
|
|
1442
|
+
return () => {
|
|
1443
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
1444
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
1445
|
+
el.removeEventListener("touchend", onTouchEnd);
|
|
1446
|
+
};
|
|
1447
|
+
}, [targetRef, enabled, seekSeconds, doubleTapMs, showFeedback]);
|
|
1448
|
+
react.useEffect(() => {
|
|
1449
|
+
return () => {
|
|
1450
|
+
if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current);
|
|
1451
|
+
};
|
|
1452
|
+
}, []);
|
|
1453
|
+
return { feedback };
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
exports.useABLoop = useABLoop;
|
|
1146
1457
|
exports.useChapters = useChapters;
|
|
1147
1458
|
exports.useFullscreen = useFullscreen;
|
|
1148
1459
|
exports.useKeyboardShortcuts = useKeyboardShortcuts;
|
|
@@ -1151,7 +1462,9 @@ exports.useMediaSession = useMediaSession;
|
|
|
1151
1462
|
exports.usePictureInPicture = usePictureInPicture;
|
|
1152
1463
|
exports.usePlaylist = usePlaylist;
|
|
1153
1464
|
exports.useProgressPersistence = useProgressPersistence;
|
|
1465
|
+
exports.useSeekPreview = useSeekPreview;
|
|
1154
1466
|
exports.useSubtitles = useSubtitles;
|
|
1467
|
+
exports.useTouchGestures = useTouchGestures;
|
|
1155
1468
|
exports.useVideoFilters = useVideoFilters;
|
|
1156
1469
|
exports.useVideoInfo = useVideoInfo;
|
|
1157
1470
|
exports.useVideoPlayback = useVideoPlayback;
|
package/dist/react/index.d.cts
CHANGED
|
@@ -95,6 +95,18 @@ interface SubtitleTrackMeta {
|
|
|
95
95
|
format: string | null;
|
|
96
96
|
language: string | null;
|
|
97
97
|
}
|
|
98
|
+
/** A single HLS quality rendition (e.g. 1080p, 720p). */
|
|
99
|
+
interface QualityLevel {
|
|
100
|
+
index: number;
|
|
101
|
+
height: number;
|
|
102
|
+
bitrate: number;
|
|
103
|
+
name: string;
|
|
104
|
+
}
|
|
105
|
+
/** Result of initialising an HLS stream via `HLSPlayer`. */
|
|
106
|
+
interface HLSPlayerFile {
|
|
107
|
+
url: string;
|
|
108
|
+
qualityLevels: QualityLevel[];
|
|
109
|
+
}
|
|
98
110
|
|
|
99
111
|
declare function useVideoFilters(videoRef: RefObject<HTMLVideoElement | null>): {
|
|
100
112
|
filters: VideoFilters;
|
|
@@ -236,7 +248,7 @@ interface MKVPlayerFile {
|
|
|
236
248
|
activeSubtitleTrack: string;
|
|
237
249
|
}
|
|
238
250
|
|
|
239
|
-
type ProcessedFile = SimplePlayerFile | MKVPlayerFile;
|
|
251
|
+
type ProcessedFile = SimplePlayerFile | MKVPlayerFile | HLSPlayerFile;
|
|
240
252
|
interface VideoPlayer {
|
|
241
253
|
initialize(videoElement: HTMLVideoElement): Promise<ProcessedFile>;
|
|
242
254
|
getAudioTracks(): AudioTrack[];
|
|
@@ -272,4 +284,104 @@ interface UseMagnetReturn {
|
|
|
272
284
|
}
|
|
273
285
|
declare function useMagnet(): UseMagnetReturn;
|
|
274
286
|
|
|
275
|
-
|
|
287
|
+
interface SeekPreviewState {
|
|
288
|
+
/** Data URL of the captured frame for the hovered position, or `null`. */
|
|
289
|
+
thumbnail: string | null;
|
|
290
|
+
/** Hovered timestamp (seconds) the preview corresponds to, or `null`. */
|
|
291
|
+
time: number | null;
|
|
292
|
+
/** Request a preview at a hovered timestamp. Debounced and cached. */
|
|
293
|
+
requestPreview: (timeSeconds: number) => void;
|
|
294
|
+
/** Dismiss the preview, e.g. when the pointer leaves the seek bar. */
|
|
295
|
+
clearPreview: () => void;
|
|
296
|
+
}
|
|
297
|
+
interface UseSeekPreviewOptions {
|
|
298
|
+
/** Debounce window in ms before a frame is captured. Default `120`. */
|
|
299
|
+
debounceMs?: number;
|
|
300
|
+
/** Captured thumbnail width in pixels. Default `160`. */
|
|
301
|
+
width?: number;
|
|
302
|
+
/** Captured thumbnail height in pixels. Default `90`. */
|
|
303
|
+
height?: number;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Generates thumbnail previews for seek-bar hover.
|
|
307
|
+
*
|
|
308
|
+
* Captures frames from a dedicated offscreen video element kept in sync with
|
|
309
|
+
* the main player's source, so scrubbing the preview never disturbs playback.
|
|
310
|
+
* Captures are debounced and cached per whole second.
|
|
311
|
+
*/
|
|
312
|
+
declare function useSeekPreview(videoRef: RefObject<HTMLVideoElement | null>, options?: UseSeekPreviewOptions): SeekPreviewState;
|
|
313
|
+
|
|
314
|
+
interface ABLoopState {
|
|
315
|
+
/** Loop start point in seconds, or `null` when unset. */
|
|
316
|
+
pointA: number | null;
|
|
317
|
+
/** Loop end point in seconds, or `null` when unset. */
|
|
318
|
+
pointB: number | null;
|
|
319
|
+
/** True when both points are set and playback loops between them. */
|
|
320
|
+
isLooping: boolean;
|
|
321
|
+
/** Capture the current playback time as point A. */
|
|
322
|
+
setPointA: () => void;
|
|
323
|
+
/** Capture the current playback time as point B (requires A, must be after A). */
|
|
324
|
+
setPointB: () => void;
|
|
325
|
+
/** Clear both points and stop looping. */
|
|
326
|
+
clear: () => void;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A-B loop: repeat playback between two user-set points.
|
|
330
|
+
*
|
|
331
|
+
* `setPointA` captures the current time as the loop start; `setPointB`
|
|
332
|
+
* captures the loop end (accepted only once A is set and the playhead is past
|
|
333
|
+
* it). While both points are set, the playhead is kept within `[A, B]` and
|
|
334
|
+
* jumps back to A whenever it reaches B. Points reset when a new source loads.
|
|
335
|
+
*/
|
|
336
|
+
declare function useABLoop(videoRef: RefObject<HTMLVideoElement | null>): ABLoopState;
|
|
337
|
+
|
|
338
|
+
interface TouchGestureHandlers {
|
|
339
|
+
/** Seek relative to the current time by a signed number of seconds. */
|
|
340
|
+
seekBy?: (seconds: number) => void;
|
|
341
|
+
/** Read the current volume as a 0..1 value. */
|
|
342
|
+
getVolume?: () => number;
|
|
343
|
+
/** Set the volume to a 0..1 value. */
|
|
344
|
+
setVolume?: (value: number) => void;
|
|
345
|
+
/** Read the current brightness as a 0..1 fraction. */
|
|
346
|
+
getBrightness?: () => number;
|
|
347
|
+
/** Set the brightness to a 0..1 fraction. */
|
|
348
|
+
setBrightness?: (value: number) => void;
|
|
349
|
+
}
|
|
350
|
+
type TouchGestureFeedback = {
|
|
351
|
+
type: "seek";
|
|
352
|
+
direction: "forward" | "backward";
|
|
353
|
+
seconds: number;
|
|
354
|
+
} | {
|
|
355
|
+
type: "volume";
|
|
356
|
+
value: number;
|
|
357
|
+
} | {
|
|
358
|
+
type: "brightness";
|
|
359
|
+
value: number;
|
|
360
|
+
};
|
|
361
|
+
interface UseTouchGesturesOptions {
|
|
362
|
+
/** Disable the gesture listeners entirely. Default `true` (enabled). */
|
|
363
|
+
enabled?: boolean;
|
|
364
|
+
/** Seconds to seek on a double-tap. Default `10`. */
|
|
365
|
+
seekSeconds?: number;
|
|
366
|
+
/** Time window in ms for a second tap to count as a double-tap. Default `300`. */
|
|
367
|
+
doubleTapMs?: number;
|
|
368
|
+
/** How long the feedback indicator stays visible, in ms. Default `700`. */
|
|
369
|
+
feedbackMs?: number;
|
|
370
|
+
}
|
|
371
|
+
interface TouchGesturesState {
|
|
372
|
+
/** The most recent gesture for a transient on-screen indicator, or `null`. */
|
|
373
|
+
feedback: TouchGestureFeedback | null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Touch gesture controls for the player surface.
|
|
377
|
+
*
|
|
378
|
+
* - Double-tap the left/right half to seek backward/forward.
|
|
379
|
+
* - Vertical swipe on the right half to adjust volume.
|
|
380
|
+
* - Vertical swipe on the left half to adjust brightness.
|
|
381
|
+
*
|
|
382
|
+
* Returns transient `feedback` describing the latest gesture so a UI overlay
|
|
383
|
+
* can show what changed.
|
|
384
|
+
*/
|
|
385
|
+
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
386
|
+
|
|
387
|
+
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -95,6 +95,18 @@ interface SubtitleTrackMeta {
|
|
|
95
95
|
format: string | null;
|
|
96
96
|
language: string | null;
|
|
97
97
|
}
|
|
98
|
+
/** A single HLS quality rendition (e.g. 1080p, 720p). */
|
|
99
|
+
interface QualityLevel {
|
|
100
|
+
index: number;
|
|
101
|
+
height: number;
|
|
102
|
+
bitrate: number;
|
|
103
|
+
name: string;
|
|
104
|
+
}
|
|
105
|
+
/** Result of initialising an HLS stream via `HLSPlayer`. */
|
|
106
|
+
interface HLSPlayerFile {
|
|
107
|
+
url: string;
|
|
108
|
+
qualityLevels: QualityLevel[];
|
|
109
|
+
}
|
|
98
110
|
|
|
99
111
|
declare function useVideoFilters(videoRef: RefObject<HTMLVideoElement | null>): {
|
|
100
112
|
filters: VideoFilters;
|
|
@@ -236,7 +248,7 @@ interface MKVPlayerFile {
|
|
|
236
248
|
activeSubtitleTrack: string;
|
|
237
249
|
}
|
|
238
250
|
|
|
239
|
-
type ProcessedFile = SimplePlayerFile | MKVPlayerFile;
|
|
251
|
+
type ProcessedFile = SimplePlayerFile | MKVPlayerFile | HLSPlayerFile;
|
|
240
252
|
interface VideoPlayer {
|
|
241
253
|
initialize(videoElement: HTMLVideoElement): Promise<ProcessedFile>;
|
|
242
254
|
getAudioTracks(): AudioTrack[];
|
|
@@ -272,4 +284,104 @@ interface UseMagnetReturn {
|
|
|
272
284
|
}
|
|
273
285
|
declare function useMagnet(): UseMagnetReturn;
|
|
274
286
|
|
|
275
|
-
|
|
287
|
+
interface SeekPreviewState {
|
|
288
|
+
/** Data URL of the captured frame for the hovered position, or `null`. */
|
|
289
|
+
thumbnail: string | null;
|
|
290
|
+
/** Hovered timestamp (seconds) the preview corresponds to, or `null`. */
|
|
291
|
+
time: number | null;
|
|
292
|
+
/** Request a preview at a hovered timestamp. Debounced and cached. */
|
|
293
|
+
requestPreview: (timeSeconds: number) => void;
|
|
294
|
+
/** Dismiss the preview, e.g. when the pointer leaves the seek bar. */
|
|
295
|
+
clearPreview: () => void;
|
|
296
|
+
}
|
|
297
|
+
interface UseSeekPreviewOptions {
|
|
298
|
+
/** Debounce window in ms before a frame is captured. Default `120`. */
|
|
299
|
+
debounceMs?: number;
|
|
300
|
+
/** Captured thumbnail width in pixels. Default `160`. */
|
|
301
|
+
width?: number;
|
|
302
|
+
/** Captured thumbnail height in pixels. Default `90`. */
|
|
303
|
+
height?: number;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Generates thumbnail previews for seek-bar hover.
|
|
307
|
+
*
|
|
308
|
+
* Captures frames from a dedicated offscreen video element kept in sync with
|
|
309
|
+
* the main player's source, so scrubbing the preview never disturbs playback.
|
|
310
|
+
* Captures are debounced and cached per whole second.
|
|
311
|
+
*/
|
|
312
|
+
declare function useSeekPreview(videoRef: RefObject<HTMLVideoElement | null>, options?: UseSeekPreviewOptions): SeekPreviewState;
|
|
313
|
+
|
|
314
|
+
interface ABLoopState {
|
|
315
|
+
/** Loop start point in seconds, or `null` when unset. */
|
|
316
|
+
pointA: number | null;
|
|
317
|
+
/** Loop end point in seconds, or `null` when unset. */
|
|
318
|
+
pointB: number | null;
|
|
319
|
+
/** True when both points are set and playback loops between them. */
|
|
320
|
+
isLooping: boolean;
|
|
321
|
+
/** Capture the current playback time as point A. */
|
|
322
|
+
setPointA: () => void;
|
|
323
|
+
/** Capture the current playback time as point B (requires A, must be after A). */
|
|
324
|
+
setPointB: () => void;
|
|
325
|
+
/** Clear both points and stop looping. */
|
|
326
|
+
clear: () => void;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A-B loop: repeat playback between two user-set points.
|
|
330
|
+
*
|
|
331
|
+
* `setPointA` captures the current time as the loop start; `setPointB`
|
|
332
|
+
* captures the loop end (accepted only once A is set and the playhead is past
|
|
333
|
+
* it). While both points are set, the playhead is kept within `[A, B]` and
|
|
334
|
+
* jumps back to A whenever it reaches B. Points reset when a new source loads.
|
|
335
|
+
*/
|
|
336
|
+
declare function useABLoop(videoRef: RefObject<HTMLVideoElement | null>): ABLoopState;
|
|
337
|
+
|
|
338
|
+
interface TouchGestureHandlers {
|
|
339
|
+
/** Seek relative to the current time by a signed number of seconds. */
|
|
340
|
+
seekBy?: (seconds: number) => void;
|
|
341
|
+
/** Read the current volume as a 0..1 value. */
|
|
342
|
+
getVolume?: () => number;
|
|
343
|
+
/** Set the volume to a 0..1 value. */
|
|
344
|
+
setVolume?: (value: number) => void;
|
|
345
|
+
/** Read the current brightness as a 0..1 fraction. */
|
|
346
|
+
getBrightness?: () => number;
|
|
347
|
+
/** Set the brightness to a 0..1 fraction. */
|
|
348
|
+
setBrightness?: (value: number) => void;
|
|
349
|
+
}
|
|
350
|
+
type TouchGestureFeedback = {
|
|
351
|
+
type: "seek";
|
|
352
|
+
direction: "forward" | "backward";
|
|
353
|
+
seconds: number;
|
|
354
|
+
} | {
|
|
355
|
+
type: "volume";
|
|
356
|
+
value: number;
|
|
357
|
+
} | {
|
|
358
|
+
type: "brightness";
|
|
359
|
+
value: number;
|
|
360
|
+
};
|
|
361
|
+
interface UseTouchGesturesOptions {
|
|
362
|
+
/** Disable the gesture listeners entirely. Default `true` (enabled). */
|
|
363
|
+
enabled?: boolean;
|
|
364
|
+
/** Seconds to seek on a double-tap. Default `10`. */
|
|
365
|
+
seekSeconds?: number;
|
|
366
|
+
/** Time window in ms for a second tap to count as a double-tap. Default `300`. */
|
|
367
|
+
doubleTapMs?: number;
|
|
368
|
+
/** How long the feedback indicator stays visible, in ms. Default `700`. */
|
|
369
|
+
feedbackMs?: number;
|
|
370
|
+
}
|
|
371
|
+
interface TouchGesturesState {
|
|
372
|
+
/** The most recent gesture for a transient on-screen indicator, or `null`. */
|
|
373
|
+
feedback: TouchGestureFeedback | null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Touch gesture controls for the player surface.
|
|
377
|
+
*
|
|
378
|
+
* - Double-tap the left/right half to seek backward/forward.
|
|
379
|
+
* - Vertical swipe on the right half to adjust volume.
|
|
380
|
+
* - Vertical swipe on the left half to adjust brightness.
|
|
381
|
+
*
|
|
382
|
+
* Returns transient `feedback` describing the latest gesture so a UI overlay
|
|
383
|
+
* can show what changed.
|
|
384
|
+
*/
|
|
385
|
+
declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
|
|
386
|
+
|
|
387
|
+
export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
|