@lightbird/core 0.2.0 → 0.3.1
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 +152 -8
- package/dist/index.d.cts +80 -1
- package/dist/index.d.ts +80 -1
- package/dist/index.js +135 -3
- package/dist/react/index.cjs +242 -0
- package/dist/react/index.d.cts +24 -1
- package/dist/react/index.d.ts +24 -1
- package/dist/react/index.js +242 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var assCompiler = require('ass-compiler');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var webSdk = require('@openfeature/web-sdk');
|
|
5
|
+
var unleashWebProvider = require('@openfeature/unleash-web-provider');
|
|
6
6
|
|
|
7
7
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
8
8
|
// src/config.ts
|
|
@@ -1104,6 +1104,132 @@ function parseM3U8(text) {
|
|
|
1104
1104
|
return items;
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
|
+
// src/magnet-player.ts
|
|
1108
|
+
var VIDEO_EXTENSIONS = [
|
|
1109
|
+
"mp4",
|
|
1110
|
+
"mkv",
|
|
1111
|
+
"avi",
|
|
1112
|
+
"mov",
|
|
1113
|
+
"wmv",
|
|
1114
|
+
"flv",
|
|
1115
|
+
"webm",
|
|
1116
|
+
"m4v",
|
|
1117
|
+
"ts",
|
|
1118
|
+
"m2ts",
|
|
1119
|
+
"ogv",
|
|
1120
|
+
"ogg",
|
|
1121
|
+
"divx",
|
|
1122
|
+
"xvid",
|
|
1123
|
+
"rmvb",
|
|
1124
|
+
"rm"
|
|
1125
|
+
];
|
|
1126
|
+
var DEFAULT_TRACKERS = [
|
|
1127
|
+
"wss://tracker.openwebtorrent.com",
|
|
1128
|
+
"wss://tracker.btorrent.xyz",
|
|
1129
|
+
"wss://tracker.fastcast.nz"
|
|
1130
|
+
];
|
|
1131
|
+
var DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
1132
|
+
function isMagnetUri(str) {
|
|
1133
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
1134
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
1135
|
+
}
|
|
1136
|
+
function isVideoFile(name) {
|
|
1137
|
+
if (typeof name !== "string" || !name) return false;
|
|
1138
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
1139
|
+
return VIDEO_EXTENSIONS.includes(ext);
|
|
1140
|
+
}
|
|
1141
|
+
function getVideoFiles(torrent) {
|
|
1142
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
1143
|
+
}
|
|
1144
|
+
function hasAcceptedDisclaimer() {
|
|
1145
|
+
try {
|
|
1146
|
+
return localStorage.getItem(DISCLAIMER_KEY) === "true";
|
|
1147
|
+
} catch {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function acceptDisclaimer() {
|
|
1152
|
+
try {
|
|
1153
|
+
localStorage.setItem(DISCLAIMER_KEY, "true");
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
var wtClient = null;
|
|
1158
|
+
var wtClientPromise = null;
|
|
1159
|
+
var swPromise = null;
|
|
1160
|
+
async function ensureServiceWorker() {
|
|
1161
|
+
if (swPromise) return swPromise;
|
|
1162
|
+
swPromise = (async () => {
|
|
1163
|
+
if (!("serviceWorker" in navigator)) {
|
|
1164
|
+
throw new Error("Service workers are not supported in this browser");
|
|
1165
|
+
}
|
|
1166
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
1167
|
+
if (existing?.active) return existing;
|
|
1168
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
1169
|
+
scope: "/"
|
|
1170
|
+
});
|
|
1171
|
+
if (reg.active) return reg;
|
|
1172
|
+
return new Promise((resolve, reject) => {
|
|
1173
|
+
const sw = reg.installing ?? reg.waiting;
|
|
1174
|
+
if (!sw) {
|
|
1175
|
+
if (reg.active) return resolve(reg);
|
|
1176
|
+
return reject(new Error("No service worker found after registration"));
|
|
1177
|
+
}
|
|
1178
|
+
const onStateChange = () => {
|
|
1179
|
+
if (sw.state === "activated") {
|
|
1180
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1181
|
+
resolve(reg);
|
|
1182
|
+
} else if (sw.state === "redundant") {
|
|
1183
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1184
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
sw.addEventListener("statechange", onStateChange);
|
|
1188
|
+
});
|
|
1189
|
+
})();
|
|
1190
|
+
return swPromise;
|
|
1191
|
+
}
|
|
1192
|
+
async function getWebTorrentClient() {
|
|
1193
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
1194
|
+
if (wtClientPromise) return wtClientPromise;
|
|
1195
|
+
wtClientPromise = (async () => {
|
|
1196
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
1197
|
+
const client = new WebTorrentClass();
|
|
1198
|
+
wtClient = client;
|
|
1199
|
+
try {
|
|
1200
|
+
const registration = await ensureServiceWorker();
|
|
1201
|
+
if (!client._server) {
|
|
1202
|
+
client.createServer({ controller: registration });
|
|
1203
|
+
}
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
1206
|
+
}
|
|
1207
|
+
return client;
|
|
1208
|
+
})();
|
|
1209
|
+
return wtClientPromise;
|
|
1210
|
+
}
|
|
1211
|
+
function destroyWebTorrentClient() {
|
|
1212
|
+
if (wtClient && !wtClient.destroyed) {
|
|
1213
|
+
wtClient.destroy();
|
|
1214
|
+
}
|
|
1215
|
+
wtClient = null;
|
|
1216
|
+
wtClientPromise = null;
|
|
1217
|
+
}
|
|
1218
|
+
var FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
1219
|
+
function initFeatureFlags() {
|
|
1220
|
+
const url = process.env.NEXT_PUBLIC_UNLEASH_URL ?? "";
|
|
1221
|
+
const clientKey = process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY ?? "";
|
|
1222
|
+
if (!url || !clientKey) {
|
|
1223
|
+
console.warn(
|
|
1224
|
+
"[feature-flags] NEXT_PUBLIC_UNLEASH_URL and/or NEXT_PUBLIC_UNLEASH_CLIENT_KEY are not set \u2014 feature flags will use their default values (magnet link enabled by default)."
|
|
1225
|
+
);
|
|
1226
|
+
return Promise.resolve();
|
|
1227
|
+
}
|
|
1228
|
+
return webSdk.OpenFeature.setProviderAndWait(
|
|
1229
|
+
new unleashWebProvider.UnleashWebProvider({ url, clientKey, appName: "lightbird" })
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1107
1233
|
// src/utils/media-error.ts
|
|
1108
1234
|
var MEDIA_ERR_ABORTED = 1;
|
|
1109
1235
|
var MEDIA_ERR_NETWORK = 2;
|
|
@@ -1305,6 +1431,8 @@ var ProgressEstimator = class {
|
|
|
1305
1431
|
}
|
|
1306
1432
|
// No reset() method — create a new instance per file load instead.
|
|
1307
1433
|
};
|
|
1434
|
+
|
|
1435
|
+
// src/utils/ffmpeg-singleton.ts
|
|
1308
1436
|
var instance = null;
|
|
1309
1437
|
var loading = null;
|
|
1310
1438
|
var defaultCDN = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
|
|
@@ -1312,14 +1440,18 @@ async function getFFmpeg() {
|
|
|
1312
1440
|
if (instance) return instance;
|
|
1313
1441
|
if (loading) return loading;
|
|
1314
1442
|
loading = (async () => {
|
|
1315
|
-
const
|
|
1443
|
+
const [{ FFmpeg }, { toBlobURL }] = await Promise.all([
|
|
1444
|
+
import('@ffmpeg/ffmpeg'),
|
|
1445
|
+
import('@ffmpeg/util')
|
|
1446
|
+
]);
|
|
1447
|
+
const ffmpeg = new FFmpeg();
|
|
1316
1448
|
const baseURL = getConfig().ffmpegCDN || defaultCDN;
|
|
1317
|
-
await ffmpeg
|
|
1318
|
-
coreURL: await
|
|
1319
|
-
wasmURL: await
|
|
1449
|
+
await ffmpeg.load({
|
|
1450
|
+
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"),
|
|
1451
|
+
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm")
|
|
1320
1452
|
});
|
|
1321
|
-
instance = ffmpeg
|
|
1322
|
-
return ffmpeg
|
|
1453
|
+
instance = ffmpeg;
|
|
1454
|
+
return ffmpeg;
|
|
1323
1455
|
})();
|
|
1324
1456
|
return loading;
|
|
1325
1457
|
}
|
|
@@ -1331,21 +1463,33 @@ function resetFFmpeg() {
|
|
|
1331
1463
|
exports.ASSRenderer = ASSRenderer;
|
|
1332
1464
|
exports.CancellationError = CancellationError;
|
|
1333
1465
|
exports.DEFAULT_SHORTCUTS = DEFAULT_SHORTCUTS;
|
|
1466
|
+
exports.DEFAULT_TRACKERS = DEFAULT_TRACKERS;
|
|
1467
|
+
exports.DISCLAIMER_KEY = DISCLAIMER_KEY;
|
|
1468
|
+
exports.FLAG_MAGNET_LINK = FLAG_MAGNET_LINK;
|
|
1334
1469
|
exports.MKVPlayer = MKVPlayer;
|
|
1335
1470
|
exports.ProgressEstimator = ProgressEstimator;
|
|
1336
1471
|
exports.SimplePlayer = SimplePlayer;
|
|
1337
1472
|
exports.SubtitleConverter = SubtitleConverter;
|
|
1338
1473
|
exports.UniversalSubtitleManager = UniversalSubtitleManager;
|
|
1474
|
+
exports.VIDEO_EXTENSIONS = VIDEO_EXTENSIONS;
|
|
1475
|
+
exports.acceptDisclaimer = acceptDisclaimer;
|
|
1339
1476
|
exports.applyOffsetToVtt = applyOffsetToVtt;
|
|
1340
1477
|
exports.captureVideoThumbnail = captureVideoThumbnail;
|
|
1341
1478
|
exports.configureLightBird = configureLightBird;
|
|
1342
1479
|
exports.createOffsetVttUrl = createOffsetVttUrl;
|
|
1343
1480
|
exports.createVideoPlayer = createVideoPlayer;
|
|
1481
|
+
exports.destroyWebTorrentClient = destroyWebTorrentClient;
|
|
1344
1482
|
exports.exportPlaylist = exportPlaylist;
|
|
1345
1483
|
exports.extractNativeMetadata = extractNativeMetadata;
|
|
1346
1484
|
exports.formatShortcutKey = formatShortcutKey;
|
|
1347
1485
|
exports.getFFmpeg = getFFmpeg;
|
|
1486
|
+
exports.getVideoFiles = getVideoFiles;
|
|
1487
|
+
exports.getWebTorrentClient = getWebTorrentClient;
|
|
1488
|
+
exports.hasAcceptedDisclaimer = hasAcceptedDisclaimer;
|
|
1489
|
+
exports.initFeatureFlags = initFeatureFlags;
|
|
1348
1490
|
exports.isInteractiveElement = isInteractiveElement;
|
|
1491
|
+
exports.isMagnetUri = isMagnetUri;
|
|
1492
|
+
exports.isVideoFile = isVideoFile;
|
|
1349
1493
|
exports.loadShortcuts = loadShortcuts;
|
|
1350
1494
|
exports.matchesShortcut = matchesShortcut;
|
|
1351
1495
|
exports.parseChaptersFromFFmpegLog = parseChaptersFromFFmpegLog;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import WebTorrent from 'webtorrent';
|
|
1
2
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
2
3
|
|
|
3
4
|
interface LightBirdConfig {
|
|
@@ -16,9 +17,20 @@ interface PlaylistItem {
|
|
|
16
17
|
name: string;
|
|
17
18
|
url: string;
|
|
18
19
|
type: 'video' | 'stream';
|
|
20
|
+
/** Set only on items added via magnet link. */
|
|
21
|
+
source?: 'torrent';
|
|
19
22
|
file?: File;
|
|
20
23
|
duration?: number;
|
|
21
24
|
}
|
|
25
|
+
interface TorrentStatus {
|
|
26
|
+
status: 'idle' | 'loading-metadata' | 'ready' | 'error';
|
|
27
|
+
torrentName: string;
|
|
28
|
+
numPeers: number;
|
|
29
|
+
downloadSpeed: number;
|
|
30
|
+
uploadSpeed: number;
|
|
31
|
+
progress: number;
|
|
32
|
+
error: string | null;
|
|
33
|
+
}
|
|
22
34
|
interface SubtitleCue {
|
|
23
35
|
startTime: number;
|
|
24
36
|
endTime: number;
|
|
@@ -252,6 +264,65 @@ declare function exportPlaylist(items: PlaylistItem[]): void;
|
|
|
252
264
|
/** Parse an M3U / M3U8 text file into playlist item descriptors (without id). */
|
|
253
265
|
declare function parseM3U8(text: string): Omit<PlaylistItem, "id">[];
|
|
254
266
|
|
|
267
|
+
declare const VIDEO_EXTENSIONS: string[];
|
|
268
|
+
declare const DEFAULT_TRACKERS: string[];
|
|
269
|
+
declare const DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
270
|
+
/**
|
|
271
|
+
* Returns true if the given string is a valid magnet URI.
|
|
272
|
+
*/
|
|
273
|
+
declare function isMagnetUri(str: unknown): boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Returns true if the filename has a recognised video extension.
|
|
276
|
+
*/
|
|
277
|
+
declare function isVideoFile(name: unknown): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Filters and returns all video files from a torrent, sorted by path.
|
|
280
|
+
*/
|
|
281
|
+
declare function getVideoFiles(torrent: {
|
|
282
|
+
files: Array<{
|
|
283
|
+
name: string;
|
|
284
|
+
path: string;
|
|
285
|
+
length: number;
|
|
286
|
+
}>;
|
|
287
|
+
}): Array<{
|
|
288
|
+
name: string;
|
|
289
|
+
path: string;
|
|
290
|
+
length: number;
|
|
291
|
+
}>;
|
|
292
|
+
/**
|
|
293
|
+
* Returns true if the user has already accepted the magnet disclaimer.
|
|
294
|
+
*/
|
|
295
|
+
declare function hasAcceptedDisclaimer(): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Persists the user's acceptance of the magnet disclaimer.
|
|
298
|
+
*/
|
|
299
|
+
declare function acceptDisclaimer(): void;
|
|
300
|
+
/**
|
|
301
|
+
* Returns the lazy singleton WebTorrent client, creating it if necessary.
|
|
302
|
+
* Sets up the local streaming server backed by the service worker.
|
|
303
|
+
*/
|
|
304
|
+
declare function getWebTorrentClient(): Promise<InstanceType<typeof WebTorrent>>;
|
|
305
|
+
/**
|
|
306
|
+
* Destroys the WebTorrent client singleton (call on component unmount).
|
|
307
|
+
*/
|
|
308
|
+
declare function destroyWebTorrentClient(): void;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Feature flag keys — use these constants everywhere instead of raw strings.
|
|
312
|
+
*/
|
|
313
|
+
declare const FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
314
|
+
/**
|
|
315
|
+
* Initialise the OpenFeature SDK with the Unleash Frontend API provider.
|
|
316
|
+
* Returns a promise that resolves once Unleash has fetched the initial flag
|
|
317
|
+
* state. Each `useBooleanFlagValue` call supplies its own default, used while
|
|
318
|
+
* the provider is loading or when no provider is configured.
|
|
319
|
+
*
|
|
320
|
+
* Requires the following environment variables (set in .env.local or Vercel):
|
|
321
|
+
* NEXT_PUBLIC_UNLEASH_URL — Frontend API URL
|
|
322
|
+
* NEXT_PUBLIC_UNLEASH_CLIENT_KEY — Frontend API token
|
|
323
|
+
*/
|
|
324
|
+
declare function initFeatureFlags(): Promise<void>;
|
|
325
|
+
|
|
255
326
|
type MediaErrorType = 'aborted' | 'network' | 'decode' | 'unsupported' | 'unknown';
|
|
256
327
|
interface ParsedMediaError {
|
|
257
328
|
type: MediaErrorType;
|
|
@@ -311,7 +382,15 @@ declare class ProgressEstimator {
|
|
|
311
382
|
};
|
|
312
383
|
}
|
|
313
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Returns a lazily-initialised FFmpeg.wasm instance.
|
|
387
|
+
*
|
|
388
|
+
* `@ffmpeg/ffmpeg` and `@ffmpeg/util` are pulled in via dynamic `import()` so
|
|
389
|
+
* the multi-MB FFmpeg code path is never part of the base `@lightbird/core`
|
|
390
|
+
* entry chunk. Consumers that only play HTML5-native formats (MP4/WebM) and
|
|
391
|
+
* never call `getFFmpeg()` download zero FFmpeg code. See issue #54.
|
|
392
|
+
*/
|
|
314
393
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
315
394
|
declare function resetFFmpeg(): void;
|
|
316
395
|
|
|
317
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, UniversalSubtitleManager, type VideoFilters, type VideoMetadata, type VideoPlayer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, isInteractiveElement, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
396
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import WebTorrent from 'webtorrent';
|
|
1
2
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
2
3
|
|
|
3
4
|
interface LightBirdConfig {
|
|
@@ -16,9 +17,20 @@ interface PlaylistItem {
|
|
|
16
17
|
name: string;
|
|
17
18
|
url: string;
|
|
18
19
|
type: 'video' | 'stream';
|
|
20
|
+
/** Set only on items added via magnet link. */
|
|
21
|
+
source?: 'torrent';
|
|
19
22
|
file?: File;
|
|
20
23
|
duration?: number;
|
|
21
24
|
}
|
|
25
|
+
interface TorrentStatus {
|
|
26
|
+
status: 'idle' | 'loading-metadata' | 'ready' | 'error';
|
|
27
|
+
torrentName: string;
|
|
28
|
+
numPeers: number;
|
|
29
|
+
downloadSpeed: number;
|
|
30
|
+
uploadSpeed: number;
|
|
31
|
+
progress: number;
|
|
32
|
+
error: string | null;
|
|
33
|
+
}
|
|
22
34
|
interface SubtitleCue {
|
|
23
35
|
startTime: number;
|
|
24
36
|
endTime: number;
|
|
@@ -252,6 +264,65 @@ declare function exportPlaylist(items: PlaylistItem[]): void;
|
|
|
252
264
|
/** Parse an M3U / M3U8 text file into playlist item descriptors (without id). */
|
|
253
265
|
declare function parseM3U8(text: string): Omit<PlaylistItem, "id">[];
|
|
254
266
|
|
|
267
|
+
declare const VIDEO_EXTENSIONS: string[];
|
|
268
|
+
declare const DEFAULT_TRACKERS: string[];
|
|
269
|
+
declare const DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
270
|
+
/**
|
|
271
|
+
* Returns true if the given string is a valid magnet URI.
|
|
272
|
+
*/
|
|
273
|
+
declare function isMagnetUri(str: unknown): boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Returns true if the filename has a recognised video extension.
|
|
276
|
+
*/
|
|
277
|
+
declare function isVideoFile(name: unknown): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Filters and returns all video files from a torrent, sorted by path.
|
|
280
|
+
*/
|
|
281
|
+
declare function getVideoFiles(torrent: {
|
|
282
|
+
files: Array<{
|
|
283
|
+
name: string;
|
|
284
|
+
path: string;
|
|
285
|
+
length: number;
|
|
286
|
+
}>;
|
|
287
|
+
}): Array<{
|
|
288
|
+
name: string;
|
|
289
|
+
path: string;
|
|
290
|
+
length: number;
|
|
291
|
+
}>;
|
|
292
|
+
/**
|
|
293
|
+
* Returns true if the user has already accepted the magnet disclaimer.
|
|
294
|
+
*/
|
|
295
|
+
declare function hasAcceptedDisclaimer(): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Persists the user's acceptance of the magnet disclaimer.
|
|
298
|
+
*/
|
|
299
|
+
declare function acceptDisclaimer(): void;
|
|
300
|
+
/**
|
|
301
|
+
* Returns the lazy singleton WebTorrent client, creating it if necessary.
|
|
302
|
+
* Sets up the local streaming server backed by the service worker.
|
|
303
|
+
*/
|
|
304
|
+
declare function getWebTorrentClient(): Promise<InstanceType<typeof WebTorrent>>;
|
|
305
|
+
/**
|
|
306
|
+
* Destroys the WebTorrent client singleton (call on component unmount).
|
|
307
|
+
*/
|
|
308
|
+
declare function destroyWebTorrentClient(): void;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Feature flag keys — use these constants everywhere instead of raw strings.
|
|
312
|
+
*/
|
|
313
|
+
declare const FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
314
|
+
/**
|
|
315
|
+
* Initialise the OpenFeature SDK with the Unleash Frontend API provider.
|
|
316
|
+
* Returns a promise that resolves once Unleash has fetched the initial flag
|
|
317
|
+
* state. Each `useBooleanFlagValue` call supplies its own default, used while
|
|
318
|
+
* the provider is loading or when no provider is configured.
|
|
319
|
+
*
|
|
320
|
+
* Requires the following environment variables (set in .env.local or Vercel):
|
|
321
|
+
* NEXT_PUBLIC_UNLEASH_URL — Frontend API URL
|
|
322
|
+
* NEXT_PUBLIC_UNLEASH_CLIENT_KEY — Frontend API token
|
|
323
|
+
*/
|
|
324
|
+
declare function initFeatureFlags(): Promise<void>;
|
|
325
|
+
|
|
255
326
|
type MediaErrorType = 'aborted' | 'network' | 'decode' | 'unsupported' | 'unknown';
|
|
256
327
|
interface ParsedMediaError {
|
|
257
328
|
type: MediaErrorType;
|
|
@@ -311,7 +382,15 @@ declare class ProgressEstimator {
|
|
|
311
382
|
};
|
|
312
383
|
}
|
|
313
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Returns a lazily-initialised FFmpeg.wasm instance.
|
|
387
|
+
*
|
|
388
|
+
* `@ffmpeg/ffmpeg` and `@ffmpeg/util` are pulled in via dynamic `import()` so
|
|
389
|
+
* the multi-MB FFmpeg code path is never part of the base `@lightbird/core`
|
|
390
|
+
* entry chunk. Consumers that only play HTML5-native formats (MP4/WebM) and
|
|
391
|
+
* never call `getFFmpeg()` download zero FFmpeg code. See issue #54.
|
|
392
|
+
*/
|
|
314
393
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
315
394
|
declare function resetFFmpeg(): void;
|
|
316
395
|
|
|
317
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, UniversalSubtitleManager, type VideoFilters, type VideoMetadata, type VideoPlayer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, isInteractiveElement, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
396
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compile } from 'ass-compiler';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { OpenFeature } from '@openfeature/web-sdk';
|
|
3
|
+
import { UnleashWebProvider } from '@openfeature/unleash-web-provider';
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var config = {};
|
|
@@ -1101,6 +1101,132 @@ function parseM3U8(text) {
|
|
|
1101
1101
|
return items;
|
|
1102
1102
|
}
|
|
1103
1103
|
|
|
1104
|
+
// src/magnet-player.ts
|
|
1105
|
+
var VIDEO_EXTENSIONS = [
|
|
1106
|
+
"mp4",
|
|
1107
|
+
"mkv",
|
|
1108
|
+
"avi",
|
|
1109
|
+
"mov",
|
|
1110
|
+
"wmv",
|
|
1111
|
+
"flv",
|
|
1112
|
+
"webm",
|
|
1113
|
+
"m4v",
|
|
1114
|
+
"ts",
|
|
1115
|
+
"m2ts",
|
|
1116
|
+
"ogv",
|
|
1117
|
+
"ogg",
|
|
1118
|
+
"divx",
|
|
1119
|
+
"xvid",
|
|
1120
|
+
"rmvb",
|
|
1121
|
+
"rm"
|
|
1122
|
+
];
|
|
1123
|
+
var DEFAULT_TRACKERS = [
|
|
1124
|
+
"wss://tracker.openwebtorrent.com",
|
|
1125
|
+
"wss://tracker.btorrent.xyz",
|
|
1126
|
+
"wss://tracker.fastcast.nz"
|
|
1127
|
+
];
|
|
1128
|
+
var DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
1129
|
+
function isMagnetUri(str) {
|
|
1130
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
1131
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
1132
|
+
}
|
|
1133
|
+
function isVideoFile(name) {
|
|
1134
|
+
if (typeof name !== "string" || !name) return false;
|
|
1135
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
1136
|
+
return VIDEO_EXTENSIONS.includes(ext);
|
|
1137
|
+
}
|
|
1138
|
+
function getVideoFiles(torrent) {
|
|
1139
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
1140
|
+
}
|
|
1141
|
+
function hasAcceptedDisclaimer() {
|
|
1142
|
+
try {
|
|
1143
|
+
return localStorage.getItem(DISCLAIMER_KEY) === "true";
|
|
1144
|
+
} catch {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function acceptDisclaimer() {
|
|
1149
|
+
try {
|
|
1150
|
+
localStorage.setItem(DISCLAIMER_KEY, "true");
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
var wtClient = null;
|
|
1155
|
+
var wtClientPromise = null;
|
|
1156
|
+
var swPromise = null;
|
|
1157
|
+
async function ensureServiceWorker() {
|
|
1158
|
+
if (swPromise) return swPromise;
|
|
1159
|
+
swPromise = (async () => {
|
|
1160
|
+
if (!("serviceWorker" in navigator)) {
|
|
1161
|
+
throw new Error("Service workers are not supported in this browser");
|
|
1162
|
+
}
|
|
1163
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
1164
|
+
if (existing?.active) return existing;
|
|
1165
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
1166
|
+
scope: "/"
|
|
1167
|
+
});
|
|
1168
|
+
if (reg.active) return reg;
|
|
1169
|
+
return new Promise((resolve, reject) => {
|
|
1170
|
+
const sw = reg.installing ?? reg.waiting;
|
|
1171
|
+
if (!sw) {
|
|
1172
|
+
if (reg.active) return resolve(reg);
|
|
1173
|
+
return reject(new Error("No service worker found after registration"));
|
|
1174
|
+
}
|
|
1175
|
+
const onStateChange = () => {
|
|
1176
|
+
if (sw.state === "activated") {
|
|
1177
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1178
|
+
resolve(reg);
|
|
1179
|
+
} else if (sw.state === "redundant") {
|
|
1180
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1181
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
sw.addEventListener("statechange", onStateChange);
|
|
1185
|
+
});
|
|
1186
|
+
})();
|
|
1187
|
+
return swPromise;
|
|
1188
|
+
}
|
|
1189
|
+
async function getWebTorrentClient() {
|
|
1190
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
1191
|
+
if (wtClientPromise) return wtClientPromise;
|
|
1192
|
+
wtClientPromise = (async () => {
|
|
1193
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
1194
|
+
const client = new WebTorrentClass();
|
|
1195
|
+
wtClient = client;
|
|
1196
|
+
try {
|
|
1197
|
+
const registration = await ensureServiceWorker();
|
|
1198
|
+
if (!client._server) {
|
|
1199
|
+
client.createServer({ controller: registration });
|
|
1200
|
+
}
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
1203
|
+
}
|
|
1204
|
+
return client;
|
|
1205
|
+
})();
|
|
1206
|
+
return wtClientPromise;
|
|
1207
|
+
}
|
|
1208
|
+
function destroyWebTorrentClient() {
|
|
1209
|
+
if (wtClient && !wtClient.destroyed) {
|
|
1210
|
+
wtClient.destroy();
|
|
1211
|
+
}
|
|
1212
|
+
wtClient = null;
|
|
1213
|
+
wtClientPromise = null;
|
|
1214
|
+
}
|
|
1215
|
+
var FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
1216
|
+
function initFeatureFlags() {
|
|
1217
|
+
const url = process.env.NEXT_PUBLIC_UNLEASH_URL ?? "";
|
|
1218
|
+
const clientKey = process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY ?? "";
|
|
1219
|
+
if (!url || !clientKey) {
|
|
1220
|
+
console.warn(
|
|
1221
|
+
"[feature-flags] NEXT_PUBLIC_UNLEASH_URL and/or NEXT_PUBLIC_UNLEASH_CLIENT_KEY are not set \u2014 feature flags will use their default values (magnet link enabled by default)."
|
|
1222
|
+
);
|
|
1223
|
+
return Promise.resolve();
|
|
1224
|
+
}
|
|
1225
|
+
return OpenFeature.setProviderAndWait(
|
|
1226
|
+
new UnleashWebProvider({ url, clientKey, appName: "lightbird" })
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1104
1230
|
// src/utils/media-error.ts
|
|
1105
1231
|
var MEDIA_ERR_ABORTED = 1;
|
|
1106
1232
|
var MEDIA_ERR_NETWORK = 2;
|
|
@@ -1302,6 +1428,8 @@ var ProgressEstimator = class {
|
|
|
1302
1428
|
}
|
|
1303
1429
|
// No reset() method — create a new instance per file load instead.
|
|
1304
1430
|
};
|
|
1431
|
+
|
|
1432
|
+
// src/utils/ffmpeg-singleton.ts
|
|
1305
1433
|
var instance = null;
|
|
1306
1434
|
var loading = null;
|
|
1307
1435
|
var defaultCDN = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
|
|
@@ -1309,6 +1437,10 @@ async function getFFmpeg() {
|
|
|
1309
1437
|
if (instance) return instance;
|
|
1310
1438
|
if (loading) return loading;
|
|
1311
1439
|
loading = (async () => {
|
|
1440
|
+
const [{ FFmpeg }, { toBlobURL }] = await Promise.all([
|
|
1441
|
+
import('@ffmpeg/ffmpeg'),
|
|
1442
|
+
import('@ffmpeg/util')
|
|
1443
|
+
]);
|
|
1312
1444
|
const ffmpeg = new FFmpeg();
|
|
1313
1445
|
const baseURL = getConfig().ffmpegCDN || defaultCDN;
|
|
1314
1446
|
await ffmpeg.load({
|
|
@@ -1325,4 +1457,4 @@ function resetFFmpeg() {
|
|
|
1325
1457
|
loading = null;
|
|
1326
1458
|
}
|
|
1327
1459
|
|
|
1328
|
-
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, isInteractiveElement, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
1460
|
+
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/react/index.cjs
CHANGED
|
@@ -902,9 +902,251 @@ function useChapters(videoRef, playerRef) {
|
|
|
902
902
|
return { chapters, currentChapter, goToChapter };
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
+
// src/magnet-player.ts
|
|
906
|
+
var VIDEO_EXTENSIONS2 = [
|
|
907
|
+
"mp4",
|
|
908
|
+
"mkv",
|
|
909
|
+
"avi",
|
|
910
|
+
"mov",
|
|
911
|
+
"wmv",
|
|
912
|
+
"flv",
|
|
913
|
+
"webm",
|
|
914
|
+
"m4v",
|
|
915
|
+
"ts",
|
|
916
|
+
"m2ts",
|
|
917
|
+
"ogv",
|
|
918
|
+
"ogg",
|
|
919
|
+
"divx",
|
|
920
|
+
"xvid",
|
|
921
|
+
"rmvb",
|
|
922
|
+
"rm"
|
|
923
|
+
];
|
|
924
|
+
var DEFAULT_TRACKERS = [
|
|
925
|
+
"wss://tracker.openwebtorrent.com",
|
|
926
|
+
"wss://tracker.btorrent.xyz",
|
|
927
|
+
"wss://tracker.fastcast.nz"
|
|
928
|
+
];
|
|
929
|
+
function isMagnetUri(str) {
|
|
930
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
931
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
932
|
+
}
|
|
933
|
+
function isVideoFile(name) {
|
|
934
|
+
if (typeof name !== "string" || !name) return false;
|
|
935
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
936
|
+
return VIDEO_EXTENSIONS2.includes(ext);
|
|
937
|
+
}
|
|
938
|
+
function getVideoFiles(torrent) {
|
|
939
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
940
|
+
}
|
|
941
|
+
var wtClient = null;
|
|
942
|
+
var wtClientPromise = null;
|
|
943
|
+
var swPromise = null;
|
|
944
|
+
async function ensureServiceWorker() {
|
|
945
|
+
if (swPromise) return swPromise;
|
|
946
|
+
swPromise = (async () => {
|
|
947
|
+
if (!("serviceWorker" in navigator)) {
|
|
948
|
+
throw new Error("Service workers are not supported in this browser");
|
|
949
|
+
}
|
|
950
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
951
|
+
if (existing?.active) return existing;
|
|
952
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
953
|
+
scope: "/"
|
|
954
|
+
});
|
|
955
|
+
if (reg.active) return reg;
|
|
956
|
+
return new Promise((resolve, reject) => {
|
|
957
|
+
const sw = reg.installing ?? reg.waiting;
|
|
958
|
+
if (!sw) {
|
|
959
|
+
if (reg.active) return resolve(reg);
|
|
960
|
+
return reject(new Error("No service worker found after registration"));
|
|
961
|
+
}
|
|
962
|
+
const onStateChange = () => {
|
|
963
|
+
if (sw.state === "activated") {
|
|
964
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
965
|
+
resolve(reg);
|
|
966
|
+
} else if (sw.state === "redundant") {
|
|
967
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
968
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
sw.addEventListener("statechange", onStateChange);
|
|
972
|
+
});
|
|
973
|
+
})();
|
|
974
|
+
return swPromise;
|
|
975
|
+
}
|
|
976
|
+
async function getWebTorrentClient() {
|
|
977
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
978
|
+
if (wtClientPromise) return wtClientPromise;
|
|
979
|
+
wtClientPromise = (async () => {
|
|
980
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
981
|
+
const client = new WebTorrentClass();
|
|
982
|
+
wtClient = client;
|
|
983
|
+
try {
|
|
984
|
+
const registration = await ensureServiceWorker();
|
|
985
|
+
if (!client._server) {
|
|
986
|
+
client.createServer({ controller: registration });
|
|
987
|
+
}
|
|
988
|
+
} catch (err) {
|
|
989
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
990
|
+
}
|
|
991
|
+
return client;
|
|
992
|
+
})();
|
|
993
|
+
return wtClientPromise;
|
|
994
|
+
}
|
|
995
|
+
function destroyWebTorrentClient() {
|
|
996
|
+
if (wtClient && !wtClient.destroyed) {
|
|
997
|
+
wtClient.destroy();
|
|
998
|
+
}
|
|
999
|
+
wtClient = null;
|
|
1000
|
+
wtClientPromise = null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/react/use-magnet.ts
|
|
1004
|
+
var METADATA_TIMEOUT_MS = 3e4;
|
|
1005
|
+
var INITIAL_STATUS = {
|
|
1006
|
+
status: "idle",
|
|
1007
|
+
torrentName: "",
|
|
1008
|
+
numPeers: 0,
|
|
1009
|
+
downloadSpeed: 0,
|
|
1010
|
+
uploadSpeed: 0,
|
|
1011
|
+
progress: 0,
|
|
1012
|
+
error: null
|
|
1013
|
+
};
|
|
1014
|
+
function useMagnet() {
|
|
1015
|
+
const [torrentStatus, setTorrentStatus] = react.useState(INITIAL_STATUS);
|
|
1016
|
+
const activeTorrentRef = react.useRef(null);
|
|
1017
|
+
react.useEffect(() => {
|
|
1018
|
+
return () => {
|
|
1019
|
+
destroyWebTorrentClient();
|
|
1020
|
+
};
|
|
1021
|
+
}, []);
|
|
1022
|
+
const destroyMagnet = react.useCallback(() => {
|
|
1023
|
+
if (activeTorrentRef.current && !activeTorrentRef.current.destroyed) {
|
|
1024
|
+
activeTorrentRef.current.destroy();
|
|
1025
|
+
}
|
|
1026
|
+
activeTorrentRef.current = null;
|
|
1027
|
+
setTorrentStatus(INITIAL_STATUS);
|
|
1028
|
+
}, []);
|
|
1029
|
+
const addMagnet = react.useCallback(async (uri) => {
|
|
1030
|
+
if (!isMagnetUri(uri)) {
|
|
1031
|
+
throw new Error("Not a valid magnet link");
|
|
1032
|
+
}
|
|
1033
|
+
if (activeTorrentRef.current && !activeTorrentRef.current.destroyed) {
|
|
1034
|
+
activeTorrentRef.current.destroy();
|
|
1035
|
+
}
|
|
1036
|
+
activeTorrentRef.current = null;
|
|
1037
|
+
setTorrentStatus({
|
|
1038
|
+
...INITIAL_STATUS,
|
|
1039
|
+
status: "loading-metadata",
|
|
1040
|
+
error: null
|
|
1041
|
+
});
|
|
1042
|
+
let client;
|
|
1043
|
+
try {
|
|
1044
|
+
client = await getWebTorrentClient();
|
|
1045
|
+
} catch {
|
|
1046
|
+
const msg = "Could not initialise torrent client";
|
|
1047
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1048
|
+
throw new Error(msg);
|
|
1049
|
+
}
|
|
1050
|
+
return new Promise((resolve, reject) => {
|
|
1051
|
+
let settled = false;
|
|
1052
|
+
let torrent;
|
|
1053
|
+
const settle = (fn) => {
|
|
1054
|
+
if (settled) return;
|
|
1055
|
+
settled = true;
|
|
1056
|
+
fn();
|
|
1057
|
+
};
|
|
1058
|
+
const timeout = setTimeout(() => {
|
|
1059
|
+
settle(() => {
|
|
1060
|
+
torrent?.destroy?.();
|
|
1061
|
+
activeTorrentRef.current = null;
|
|
1062
|
+
const msg = "Could not connect to peers. Check the link and try again.";
|
|
1063
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1064
|
+
reject(new Error(msg));
|
|
1065
|
+
});
|
|
1066
|
+
}, METADATA_TIMEOUT_MS);
|
|
1067
|
+
try {
|
|
1068
|
+
torrent = client.add(uri.trim(), { announce: DEFAULT_TRACKERS });
|
|
1069
|
+
activeTorrentRef.current = torrent;
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
settle(() => {
|
|
1072
|
+
clearTimeout(timeout);
|
|
1073
|
+
const msg = "Failed to add torrent";
|
|
1074
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1075
|
+
reject(new Error(msg));
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
torrent.on("error", (err) => {
|
|
1080
|
+
settle(() => {
|
|
1081
|
+
clearTimeout(timeout);
|
|
1082
|
+
torrent?.destroy?.();
|
|
1083
|
+
activeTorrentRef.current = null;
|
|
1084
|
+
const msg = err?.message ?? "Torrent error";
|
|
1085
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1086
|
+
reject(new Error(msg));
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
torrent.on("ready", () => {
|
|
1090
|
+
clearTimeout(timeout);
|
|
1091
|
+
const videoFiles = getVideoFiles(torrent);
|
|
1092
|
+
const streamable = videoFiles.filter(
|
|
1093
|
+
(file) => Boolean(file.streamURL)
|
|
1094
|
+
);
|
|
1095
|
+
if (streamable.length === 0) {
|
|
1096
|
+
settle(() => {
|
|
1097
|
+
torrent.destroy();
|
|
1098
|
+
activeTorrentRef.current = null;
|
|
1099
|
+
const msg = videoFiles.length === 0 ? "No video files found in this torrent" : "Video files in this torrent could not be made streamable";
|
|
1100
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1101
|
+
reject(new Error(msg));
|
|
1102
|
+
});
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
settle(() => {
|
|
1106
|
+
setTorrentStatus((s) => ({
|
|
1107
|
+
...s,
|
|
1108
|
+
status: "ready",
|
|
1109
|
+
torrentName: torrent.name ?? "",
|
|
1110
|
+
progress: torrent.progress ?? 0
|
|
1111
|
+
}));
|
|
1112
|
+
const items = streamable.map((file) => ({
|
|
1113
|
+
id: crypto.randomUUID(),
|
|
1114
|
+
name: file.name,
|
|
1115
|
+
url: file.streamURL,
|
|
1116
|
+
type: "stream",
|
|
1117
|
+
source: "torrent",
|
|
1118
|
+
duration: void 0
|
|
1119
|
+
}));
|
|
1120
|
+
resolve(items);
|
|
1121
|
+
});
|
|
1122
|
+
torrent.on("download", () => {
|
|
1123
|
+
setTorrentStatus((s) => ({
|
|
1124
|
+
...s,
|
|
1125
|
+
numPeers: torrent.numPeers ?? 0,
|
|
1126
|
+
downloadSpeed: torrent.downloadSpeed ?? 0,
|
|
1127
|
+
uploadSpeed: torrent.uploadSpeed ?? 0,
|
|
1128
|
+
progress: torrent.progress ?? 0
|
|
1129
|
+
}));
|
|
1130
|
+
});
|
|
1131
|
+
torrent.on("done", () => {
|
|
1132
|
+
setTorrentStatus((s) => ({ ...s, progress: 1 }));
|
|
1133
|
+
});
|
|
1134
|
+
torrent.on("wire", () => {
|
|
1135
|
+
setTorrentStatus((s) => ({
|
|
1136
|
+
...s,
|
|
1137
|
+
numPeers: torrent.numPeers ?? 0
|
|
1138
|
+
}));
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
}, []);
|
|
1143
|
+
return { torrentStatus, addMagnet, destroyMagnet };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
905
1146
|
exports.useChapters = useChapters;
|
|
906
1147
|
exports.useFullscreen = useFullscreen;
|
|
907
1148
|
exports.useKeyboardShortcuts = useKeyboardShortcuts;
|
|
1149
|
+
exports.useMagnet = useMagnet;
|
|
908
1150
|
exports.useMediaSession = useMediaSession;
|
|
909
1151
|
exports.usePictureInPicture = usePictureInPicture;
|
|
910
1152
|
exports.usePlaylist = usePlaylist;
|
package/dist/react/index.d.cts
CHANGED
|
@@ -29,9 +29,20 @@ interface PlaylistItem {
|
|
|
29
29
|
name: string;
|
|
30
30
|
url: string;
|
|
31
31
|
type: 'video' | 'stream';
|
|
32
|
+
/** Set only on items added via magnet link. */
|
|
33
|
+
source?: 'torrent';
|
|
32
34
|
file?: File;
|
|
33
35
|
duration?: number;
|
|
34
36
|
}
|
|
37
|
+
interface TorrentStatus {
|
|
38
|
+
status: 'idle' | 'loading-metadata' | 'ready' | 'error';
|
|
39
|
+
torrentName: string;
|
|
40
|
+
numPeers: number;
|
|
41
|
+
downloadSpeed: number;
|
|
42
|
+
uploadSpeed: number;
|
|
43
|
+
progress: number;
|
|
44
|
+
error: string | null;
|
|
45
|
+
}
|
|
35
46
|
interface SubtitleCue {
|
|
36
47
|
startTime: number;
|
|
37
48
|
endTime: number;
|
|
@@ -249,4 +260,16 @@ declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: R
|
|
|
249
260
|
goToChapter: (index: number) => void;
|
|
250
261
|
};
|
|
251
262
|
|
|
252
|
-
|
|
263
|
+
interface UseMagnetReturn {
|
|
264
|
+
torrentStatus: TorrentStatus;
|
|
265
|
+
/**
|
|
266
|
+
* Resolves with the playlist items (one per video file in the torrent).
|
|
267
|
+
* Throws an Error with a user-facing message on failure.
|
|
268
|
+
*/
|
|
269
|
+
addMagnet: (uri: string) => Promise<PlaylistItem[]>;
|
|
270
|
+
/** Destroys the active torrent. */
|
|
271
|
+
destroyMagnet: () => void;
|
|
272
|
+
}
|
|
273
|
+
declare function useMagnet(): UseMagnetReturn;
|
|
274
|
+
|
|
275
|
+
export { type ShortcutHandlers, type UseMagnetReturn, type UseMediaSessionOptions, type UseSubtitlesOptions, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSubtitles, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -29,9 +29,20 @@ interface PlaylistItem {
|
|
|
29
29
|
name: string;
|
|
30
30
|
url: string;
|
|
31
31
|
type: 'video' | 'stream';
|
|
32
|
+
/** Set only on items added via magnet link. */
|
|
33
|
+
source?: 'torrent';
|
|
32
34
|
file?: File;
|
|
33
35
|
duration?: number;
|
|
34
36
|
}
|
|
37
|
+
interface TorrentStatus {
|
|
38
|
+
status: 'idle' | 'loading-metadata' | 'ready' | 'error';
|
|
39
|
+
torrentName: string;
|
|
40
|
+
numPeers: number;
|
|
41
|
+
downloadSpeed: number;
|
|
42
|
+
uploadSpeed: number;
|
|
43
|
+
progress: number;
|
|
44
|
+
error: string | null;
|
|
45
|
+
}
|
|
35
46
|
interface SubtitleCue {
|
|
36
47
|
startTime: number;
|
|
37
48
|
endTime: number;
|
|
@@ -249,4 +260,16 @@ declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: R
|
|
|
249
260
|
goToChapter: (index: number) => void;
|
|
250
261
|
};
|
|
251
262
|
|
|
252
|
-
|
|
263
|
+
interface UseMagnetReturn {
|
|
264
|
+
torrentStatus: TorrentStatus;
|
|
265
|
+
/**
|
|
266
|
+
* Resolves with the playlist items (one per video file in the torrent).
|
|
267
|
+
* Throws an Error with a user-facing message on failure.
|
|
268
|
+
*/
|
|
269
|
+
addMagnet: (uri: string) => Promise<PlaylistItem[]>;
|
|
270
|
+
/** Destroys the active torrent. */
|
|
271
|
+
destroyMagnet: () => void;
|
|
272
|
+
}
|
|
273
|
+
declare function useMagnet(): UseMagnetReturn;
|
|
274
|
+
|
|
275
|
+
export { type ShortcutHandlers, type UseMagnetReturn, type UseMediaSessionOptions, type UseSubtitlesOptions, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSubtitles, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/dist/react/index.js
CHANGED
|
@@ -900,4 +900,245 @@ function useChapters(videoRef, playerRef) {
|
|
|
900
900
|
return { chapters, currentChapter, goToChapter };
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
-
|
|
903
|
+
// src/magnet-player.ts
|
|
904
|
+
var VIDEO_EXTENSIONS2 = [
|
|
905
|
+
"mp4",
|
|
906
|
+
"mkv",
|
|
907
|
+
"avi",
|
|
908
|
+
"mov",
|
|
909
|
+
"wmv",
|
|
910
|
+
"flv",
|
|
911
|
+
"webm",
|
|
912
|
+
"m4v",
|
|
913
|
+
"ts",
|
|
914
|
+
"m2ts",
|
|
915
|
+
"ogv",
|
|
916
|
+
"ogg",
|
|
917
|
+
"divx",
|
|
918
|
+
"xvid",
|
|
919
|
+
"rmvb",
|
|
920
|
+
"rm"
|
|
921
|
+
];
|
|
922
|
+
var DEFAULT_TRACKERS = [
|
|
923
|
+
"wss://tracker.openwebtorrent.com",
|
|
924
|
+
"wss://tracker.btorrent.xyz",
|
|
925
|
+
"wss://tracker.fastcast.nz"
|
|
926
|
+
];
|
|
927
|
+
function isMagnetUri(str) {
|
|
928
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
929
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
930
|
+
}
|
|
931
|
+
function isVideoFile(name) {
|
|
932
|
+
if (typeof name !== "string" || !name) return false;
|
|
933
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
934
|
+
return VIDEO_EXTENSIONS2.includes(ext);
|
|
935
|
+
}
|
|
936
|
+
function getVideoFiles(torrent) {
|
|
937
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
938
|
+
}
|
|
939
|
+
var wtClient = null;
|
|
940
|
+
var wtClientPromise = null;
|
|
941
|
+
var swPromise = null;
|
|
942
|
+
async function ensureServiceWorker() {
|
|
943
|
+
if (swPromise) return swPromise;
|
|
944
|
+
swPromise = (async () => {
|
|
945
|
+
if (!("serviceWorker" in navigator)) {
|
|
946
|
+
throw new Error("Service workers are not supported in this browser");
|
|
947
|
+
}
|
|
948
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
949
|
+
if (existing?.active) return existing;
|
|
950
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
951
|
+
scope: "/"
|
|
952
|
+
});
|
|
953
|
+
if (reg.active) return reg;
|
|
954
|
+
return new Promise((resolve, reject) => {
|
|
955
|
+
const sw = reg.installing ?? reg.waiting;
|
|
956
|
+
if (!sw) {
|
|
957
|
+
if (reg.active) return resolve(reg);
|
|
958
|
+
return reject(new Error("No service worker found after registration"));
|
|
959
|
+
}
|
|
960
|
+
const onStateChange = () => {
|
|
961
|
+
if (sw.state === "activated") {
|
|
962
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
963
|
+
resolve(reg);
|
|
964
|
+
} else if (sw.state === "redundant") {
|
|
965
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
966
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
sw.addEventListener("statechange", onStateChange);
|
|
970
|
+
});
|
|
971
|
+
})();
|
|
972
|
+
return swPromise;
|
|
973
|
+
}
|
|
974
|
+
async function getWebTorrentClient() {
|
|
975
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
976
|
+
if (wtClientPromise) return wtClientPromise;
|
|
977
|
+
wtClientPromise = (async () => {
|
|
978
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
979
|
+
const client = new WebTorrentClass();
|
|
980
|
+
wtClient = client;
|
|
981
|
+
try {
|
|
982
|
+
const registration = await ensureServiceWorker();
|
|
983
|
+
if (!client._server) {
|
|
984
|
+
client.createServer({ controller: registration });
|
|
985
|
+
}
|
|
986
|
+
} catch (err) {
|
|
987
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
988
|
+
}
|
|
989
|
+
return client;
|
|
990
|
+
})();
|
|
991
|
+
return wtClientPromise;
|
|
992
|
+
}
|
|
993
|
+
function destroyWebTorrentClient() {
|
|
994
|
+
if (wtClient && !wtClient.destroyed) {
|
|
995
|
+
wtClient.destroy();
|
|
996
|
+
}
|
|
997
|
+
wtClient = null;
|
|
998
|
+
wtClientPromise = null;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/react/use-magnet.ts
|
|
1002
|
+
var METADATA_TIMEOUT_MS = 3e4;
|
|
1003
|
+
var INITIAL_STATUS = {
|
|
1004
|
+
status: "idle",
|
|
1005
|
+
torrentName: "",
|
|
1006
|
+
numPeers: 0,
|
|
1007
|
+
downloadSpeed: 0,
|
|
1008
|
+
uploadSpeed: 0,
|
|
1009
|
+
progress: 0,
|
|
1010
|
+
error: null
|
|
1011
|
+
};
|
|
1012
|
+
function useMagnet() {
|
|
1013
|
+
const [torrentStatus, setTorrentStatus] = useState(INITIAL_STATUS);
|
|
1014
|
+
const activeTorrentRef = useRef(null);
|
|
1015
|
+
useEffect(() => {
|
|
1016
|
+
return () => {
|
|
1017
|
+
destroyWebTorrentClient();
|
|
1018
|
+
};
|
|
1019
|
+
}, []);
|
|
1020
|
+
const destroyMagnet = useCallback(() => {
|
|
1021
|
+
if (activeTorrentRef.current && !activeTorrentRef.current.destroyed) {
|
|
1022
|
+
activeTorrentRef.current.destroy();
|
|
1023
|
+
}
|
|
1024
|
+
activeTorrentRef.current = null;
|
|
1025
|
+
setTorrentStatus(INITIAL_STATUS);
|
|
1026
|
+
}, []);
|
|
1027
|
+
const addMagnet = useCallback(async (uri) => {
|
|
1028
|
+
if (!isMagnetUri(uri)) {
|
|
1029
|
+
throw new Error("Not a valid magnet link");
|
|
1030
|
+
}
|
|
1031
|
+
if (activeTorrentRef.current && !activeTorrentRef.current.destroyed) {
|
|
1032
|
+
activeTorrentRef.current.destroy();
|
|
1033
|
+
}
|
|
1034
|
+
activeTorrentRef.current = null;
|
|
1035
|
+
setTorrentStatus({
|
|
1036
|
+
...INITIAL_STATUS,
|
|
1037
|
+
status: "loading-metadata",
|
|
1038
|
+
error: null
|
|
1039
|
+
});
|
|
1040
|
+
let client;
|
|
1041
|
+
try {
|
|
1042
|
+
client = await getWebTorrentClient();
|
|
1043
|
+
} catch {
|
|
1044
|
+
const msg = "Could not initialise torrent client";
|
|
1045
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1046
|
+
throw new Error(msg);
|
|
1047
|
+
}
|
|
1048
|
+
return new Promise((resolve, reject) => {
|
|
1049
|
+
let settled = false;
|
|
1050
|
+
let torrent;
|
|
1051
|
+
const settle = (fn) => {
|
|
1052
|
+
if (settled) return;
|
|
1053
|
+
settled = true;
|
|
1054
|
+
fn();
|
|
1055
|
+
};
|
|
1056
|
+
const timeout = setTimeout(() => {
|
|
1057
|
+
settle(() => {
|
|
1058
|
+
torrent?.destroy?.();
|
|
1059
|
+
activeTorrentRef.current = null;
|
|
1060
|
+
const msg = "Could not connect to peers. Check the link and try again.";
|
|
1061
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1062
|
+
reject(new Error(msg));
|
|
1063
|
+
});
|
|
1064
|
+
}, METADATA_TIMEOUT_MS);
|
|
1065
|
+
try {
|
|
1066
|
+
torrent = client.add(uri.trim(), { announce: DEFAULT_TRACKERS });
|
|
1067
|
+
activeTorrentRef.current = torrent;
|
|
1068
|
+
} catch (err) {
|
|
1069
|
+
settle(() => {
|
|
1070
|
+
clearTimeout(timeout);
|
|
1071
|
+
const msg = "Failed to add torrent";
|
|
1072
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1073
|
+
reject(new Error(msg));
|
|
1074
|
+
});
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
torrent.on("error", (err) => {
|
|
1078
|
+
settle(() => {
|
|
1079
|
+
clearTimeout(timeout);
|
|
1080
|
+
torrent?.destroy?.();
|
|
1081
|
+
activeTorrentRef.current = null;
|
|
1082
|
+
const msg = err?.message ?? "Torrent error";
|
|
1083
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1084
|
+
reject(new Error(msg));
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
1087
|
+
torrent.on("ready", () => {
|
|
1088
|
+
clearTimeout(timeout);
|
|
1089
|
+
const videoFiles = getVideoFiles(torrent);
|
|
1090
|
+
const streamable = videoFiles.filter(
|
|
1091
|
+
(file) => Boolean(file.streamURL)
|
|
1092
|
+
);
|
|
1093
|
+
if (streamable.length === 0) {
|
|
1094
|
+
settle(() => {
|
|
1095
|
+
torrent.destroy();
|
|
1096
|
+
activeTorrentRef.current = null;
|
|
1097
|
+
const msg = videoFiles.length === 0 ? "No video files found in this torrent" : "Video files in this torrent could not be made streamable";
|
|
1098
|
+
setTorrentStatus((s) => ({ ...s, status: "error", error: msg }));
|
|
1099
|
+
reject(new Error(msg));
|
|
1100
|
+
});
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
settle(() => {
|
|
1104
|
+
setTorrentStatus((s) => ({
|
|
1105
|
+
...s,
|
|
1106
|
+
status: "ready",
|
|
1107
|
+
torrentName: torrent.name ?? "",
|
|
1108
|
+
progress: torrent.progress ?? 0
|
|
1109
|
+
}));
|
|
1110
|
+
const items = streamable.map((file) => ({
|
|
1111
|
+
id: crypto.randomUUID(),
|
|
1112
|
+
name: file.name,
|
|
1113
|
+
url: file.streamURL,
|
|
1114
|
+
type: "stream",
|
|
1115
|
+
source: "torrent",
|
|
1116
|
+
duration: void 0
|
|
1117
|
+
}));
|
|
1118
|
+
resolve(items);
|
|
1119
|
+
});
|
|
1120
|
+
torrent.on("download", () => {
|
|
1121
|
+
setTorrentStatus((s) => ({
|
|
1122
|
+
...s,
|
|
1123
|
+
numPeers: torrent.numPeers ?? 0,
|
|
1124
|
+
downloadSpeed: torrent.downloadSpeed ?? 0,
|
|
1125
|
+
uploadSpeed: torrent.uploadSpeed ?? 0,
|
|
1126
|
+
progress: torrent.progress ?? 0
|
|
1127
|
+
}));
|
|
1128
|
+
});
|
|
1129
|
+
torrent.on("done", () => {
|
|
1130
|
+
setTorrentStatus((s) => ({ ...s, progress: 1 }));
|
|
1131
|
+
});
|
|
1132
|
+
torrent.on("wire", () => {
|
|
1133
|
+
setTorrentStatus((s) => ({
|
|
1134
|
+
...s,
|
|
1135
|
+
numPeers: torrent.numPeers ?? 0
|
|
1136
|
+
}));
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
}, []);
|
|
1141
|
+
return { torrentStatus, addMagnet, destroyMagnet };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
export { useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSubtitles, useVideoFilters, useVideoInfo, useVideoPlayback };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightbird/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Client-side video player engine. Plays MKV, MP4, WebM with full subtitle, audio track, and chapter support. No server required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Punyam Singh",
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
],
|
|
55
55
|
"sideEffects": false,
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"
|
|
57
|
+
"@openfeature/unleash-web-provider": "^0.1.1",
|
|
58
|
+
"@openfeature/web-sdk": "^1.6.0",
|
|
59
|
+
"ass-compiler": "^0.1.16",
|
|
60
|
+
"webtorrent": "^2.8.5"
|
|
58
61
|
},
|
|
59
62
|
"optionalDependencies": {
|
|
60
63
|
"@ffmpeg/core": "^0.12.6",
|