@lightbird/core 0.1.6 → 0.3.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 +140 -0
- package/dist/index.d.cts +72 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.js +129 -1
- 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,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var assCompiler = require('ass-compiler');
|
|
4
|
+
var webSdk = require('@openfeature/web-sdk');
|
|
5
|
+
var unleashWebProvider = require('@openfeature/unleash-web-provider');
|
|
4
6
|
var ffmpeg = require('@ffmpeg/ffmpeg');
|
|
5
7
|
var util = require('@ffmpeg/util');
|
|
6
8
|
|
|
@@ -1104,6 +1106,132 @@ function parseM3U8(text) {
|
|
|
1104
1106
|
return items;
|
|
1105
1107
|
}
|
|
1106
1108
|
|
|
1109
|
+
// src/magnet-player.ts
|
|
1110
|
+
var VIDEO_EXTENSIONS = [
|
|
1111
|
+
"mp4",
|
|
1112
|
+
"mkv",
|
|
1113
|
+
"avi",
|
|
1114
|
+
"mov",
|
|
1115
|
+
"wmv",
|
|
1116
|
+
"flv",
|
|
1117
|
+
"webm",
|
|
1118
|
+
"m4v",
|
|
1119
|
+
"ts",
|
|
1120
|
+
"m2ts",
|
|
1121
|
+
"ogv",
|
|
1122
|
+
"ogg",
|
|
1123
|
+
"divx",
|
|
1124
|
+
"xvid",
|
|
1125
|
+
"rmvb",
|
|
1126
|
+
"rm"
|
|
1127
|
+
];
|
|
1128
|
+
var DEFAULT_TRACKERS = [
|
|
1129
|
+
"wss://tracker.openwebtorrent.com",
|
|
1130
|
+
"wss://tracker.btorrent.xyz",
|
|
1131
|
+
"wss://tracker.fastcast.nz"
|
|
1132
|
+
];
|
|
1133
|
+
var DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
1134
|
+
function isMagnetUri(str) {
|
|
1135
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
1136
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
1137
|
+
}
|
|
1138
|
+
function isVideoFile(name) {
|
|
1139
|
+
if (typeof name !== "string" || !name) return false;
|
|
1140
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
1141
|
+
return VIDEO_EXTENSIONS.includes(ext);
|
|
1142
|
+
}
|
|
1143
|
+
function getVideoFiles(torrent) {
|
|
1144
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
1145
|
+
}
|
|
1146
|
+
function hasAcceptedDisclaimer() {
|
|
1147
|
+
try {
|
|
1148
|
+
return localStorage.getItem(DISCLAIMER_KEY) === "true";
|
|
1149
|
+
} catch {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function acceptDisclaimer() {
|
|
1154
|
+
try {
|
|
1155
|
+
localStorage.setItem(DISCLAIMER_KEY, "true");
|
|
1156
|
+
} catch {
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
var wtClient = null;
|
|
1160
|
+
var wtClientPromise = null;
|
|
1161
|
+
var swPromise = null;
|
|
1162
|
+
async function ensureServiceWorker() {
|
|
1163
|
+
if (swPromise) return swPromise;
|
|
1164
|
+
swPromise = (async () => {
|
|
1165
|
+
if (!("serviceWorker" in navigator)) {
|
|
1166
|
+
throw new Error("Service workers are not supported in this browser");
|
|
1167
|
+
}
|
|
1168
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
1169
|
+
if (existing?.active) return existing;
|
|
1170
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
1171
|
+
scope: "/"
|
|
1172
|
+
});
|
|
1173
|
+
if (reg.active) return reg;
|
|
1174
|
+
return new Promise((resolve, reject) => {
|
|
1175
|
+
const sw = reg.installing ?? reg.waiting;
|
|
1176
|
+
if (!sw) {
|
|
1177
|
+
if (reg.active) return resolve(reg);
|
|
1178
|
+
return reject(new Error("No service worker found after registration"));
|
|
1179
|
+
}
|
|
1180
|
+
const onStateChange = () => {
|
|
1181
|
+
if (sw.state === "activated") {
|
|
1182
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1183
|
+
resolve(reg);
|
|
1184
|
+
} else if (sw.state === "redundant") {
|
|
1185
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1186
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
sw.addEventListener("statechange", onStateChange);
|
|
1190
|
+
});
|
|
1191
|
+
})();
|
|
1192
|
+
return swPromise;
|
|
1193
|
+
}
|
|
1194
|
+
async function getWebTorrentClient() {
|
|
1195
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
1196
|
+
if (wtClientPromise) return wtClientPromise;
|
|
1197
|
+
wtClientPromise = (async () => {
|
|
1198
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
1199
|
+
const client = new WebTorrentClass();
|
|
1200
|
+
wtClient = client;
|
|
1201
|
+
try {
|
|
1202
|
+
const registration = await ensureServiceWorker();
|
|
1203
|
+
if (!client._server) {
|
|
1204
|
+
client.createServer({ controller: registration });
|
|
1205
|
+
}
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
1208
|
+
}
|
|
1209
|
+
return client;
|
|
1210
|
+
})();
|
|
1211
|
+
return wtClientPromise;
|
|
1212
|
+
}
|
|
1213
|
+
function destroyWebTorrentClient() {
|
|
1214
|
+
if (wtClient && !wtClient.destroyed) {
|
|
1215
|
+
wtClient.destroy();
|
|
1216
|
+
}
|
|
1217
|
+
wtClient = null;
|
|
1218
|
+
wtClientPromise = null;
|
|
1219
|
+
}
|
|
1220
|
+
var FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
1221
|
+
function initFeatureFlags() {
|
|
1222
|
+
const url = process.env.NEXT_PUBLIC_UNLEASH_URL ?? "";
|
|
1223
|
+
const clientKey = process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY ?? "";
|
|
1224
|
+
if (!url || !clientKey) {
|
|
1225
|
+
console.warn(
|
|
1226
|
+
"[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)."
|
|
1227
|
+
);
|
|
1228
|
+
return Promise.resolve();
|
|
1229
|
+
}
|
|
1230
|
+
return webSdk.OpenFeature.setProviderAndWait(
|
|
1231
|
+
new unleashWebProvider.UnleashWebProvider({ url, clientKey, appName: "lightbird" })
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1107
1235
|
// src/utils/media-error.ts
|
|
1108
1236
|
var MEDIA_ERR_ABORTED = 1;
|
|
1109
1237
|
var MEDIA_ERR_NETWORK = 2;
|
|
@@ -1331,21 +1459,33 @@ function resetFFmpeg() {
|
|
|
1331
1459
|
exports.ASSRenderer = ASSRenderer;
|
|
1332
1460
|
exports.CancellationError = CancellationError;
|
|
1333
1461
|
exports.DEFAULT_SHORTCUTS = DEFAULT_SHORTCUTS;
|
|
1462
|
+
exports.DEFAULT_TRACKERS = DEFAULT_TRACKERS;
|
|
1463
|
+
exports.DISCLAIMER_KEY = DISCLAIMER_KEY;
|
|
1464
|
+
exports.FLAG_MAGNET_LINK = FLAG_MAGNET_LINK;
|
|
1334
1465
|
exports.MKVPlayer = MKVPlayer;
|
|
1335
1466
|
exports.ProgressEstimator = ProgressEstimator;
|
|
1336
1467
|
exports.SimplePlayer = SimplePlayer;
|
|
1337
1468
|
exports.SubtitleConverter = SubtitleConverter;
|
|
1338
1469
|
exports.UniversalSubtitleManager = UniversalSubtitleManager;
|
|
1470
|
+
exports.VIDEO_EXTENSIONS = VIDEO_EXTENSIONS;
|
|
1471
|
+
exports.acceptDisclaimer = acceptDisclaimer;
|
|
1339
1472
|
exports.applyOffsetToVtt = applyOffsetToVtt;
|
|
1340
1473
|
exports.captureVideoThumbnail = captureVideoThumbnail;
|
|
1341
1474
|
exports.configureLightBird = configureLightBird;
|
|
1342
1475
|
exports.createOffsetVttUrl = createOffsetVttUrl;
|
|
1343
1476
|
exports.createVideoPlayer = createVideoPlayer;
|
|
1477
|
+
exports.destroyWebTorrentClient = destroyWebTorrentClient;
|
|
1344
1478
|
exports.exportPlaylist = exportPlaylist;
|
|
1345
1479
|
exports.extractNativeMetadata = extractNativeMetadata;
|
|
1346
1480
|
exports.formatShortcutKey = formatShortcutKey;
|
|
1347
1481
|
exports.getFFmpeg = getFFmpeg;
|
|
1482
|
+
exports.getVideoFiles = getVideoFiles;
|
|
1483
|
+
exports.getWebTorrentClient = getWebTorrentClient;
|
|
1484
|
+
exports.hasAcceptedDisclaimer = hasAcceptedDisclaimer;
|
|
1485
|
+
exports.initFeatureFlags = initFeatureFlags;
|
|
1348
1486
|
exports.isInteractiveElement = isInteractiveElement;
|
|
1487
|
+
exports.isMagnetUri = isMagnetUri;
|
|
1488
|
+
exports.isVideoFile = isVideoFile;
|
|
1349
1489
|
exports.loadShortcuts = loadShortcuts;
|
|
1350
1490
|
exports.matchesShortcut = matchesShortcut;
|
|
1351
1491
|
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;
|
|
@@ -314,4 +385,4 @@ declare class ProgressEstimator {
|
|
|
314
385
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
315
386
|
declare function resetFFmpeg(): void;
|
|
316
387
|
|
|
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 };
|
|
388
|
+
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;
|
|
@@ -314,4 +385,4 @@ declare class ProgressEstimator {
|
|
|
314
385
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
315
386
|
declare function resetFFmpeg(): void;
|
|
316
387
|
|
|
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 };
|
|
388
|
+
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,4 +1,6 @@
|
|
|
1
1
|
import { compile } from 'ass-compiler';
|
|
2
|
+
import { OpenFeature } from '@openfeature/web-sdk';
|
|
3
|
+
import { UnleashWebProvider } from '@openfeature/unleash-web-provider';
|
|
2
4
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
|
3
5
|
import { toBlobURL } from '@ffmpeg/util';
|
|
4
6
|
|
|
@@ -1101,6 +1103,132 @@ function parseM3U8(text) {
|
|
|
1101
1103
|
return items;
|
|
1102
1104
|
}
|
|
1103
1105
|
|
|
1106
|
+
// src/magnet-player.ts
|
|
1107
|
+
var VIDEO_EXTENSIONS = [
|
|
1108
|
+
"mp4",
|
|
1109
|
+
"mkv",
|
|
1110
|
+
"avi",
|
|
1111
|
+
"mov",
|
|
1112
|
+
"wmv",
|
|
1113
|
+
"flv",
|
|
1114
|
+
"webm",
|
|
1115
|
+
"m4v",
|
|
1116
|
+
"ts",
|
|
1117
|
+
"m2ts",
|
|
1118
|
+
"ogv",
|
|
1119
|
+
"ogg",
|
|
1120
|
+
"divx",
|
|
1121
|
+
"xvid",
|
|
1122
|
+
"rmvb",
|
|
1123
|
+
"rm"
|
|
1124
|
+
];
|
|
1125
|
+
var DEFAULT_TRACKERS = [
|
|
1126
|
+
"wss://tracker.openwebtorrent.com",
|
|
1127
|
+
"wss://tracker.btorrent.xyz",
|
|
1128
|
+
"wss://tracker.fastcast.nz"
|
|
1129
|
+
];
|
|
1130
|
+
var DISCLAIMER_KEY = "lightbird_magnet_disclaimer_accepted";
|
|
1131
|
+
function isMagnetUri(str) {
|
|
1132
|
+
if (typeof str !== "string" || !str.trim()) return false;
|
|
1133
|
+
return /^magnet:\?xt=urn:(btih:([a-f0-9]{40}|[a-z2-7]{32})|btmh:[a-z0-9]{20,})/i.test(str.trim());
|
|
1134
|
+
}
|
|
1135
|
+
function isVideoFile(name) {
|
|
1136
|
+
if (typeof name !== "string" || !name) return false;
|
|
1137
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
1138
|
+
return VIDEO_EXTENSIONS.includes(ext);
|
|
1139
|
+
}
|
|
1140
|
+
function getVideoFiles(torrent) {
|
|
1141
|
+
return [...torrent.files].filter((f) => isVideoFile(f.name)).sort((a, b) => a.path.localeCompare(b.path));
|
|
1142
|
+
}
|
|
1143
|
+
function hasAcceptedDisclaimer() {
|
|
1144
|
+
try {
|
|
1145
|
+
return localStorage.getItem(DISCLAIMER_KEY) === "true";
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function acceptDisclaimer() {
|
|
1151
|
+
try {
|
|
1152
|
+
localStorage.setItem(DISCLAIMER_KEY, "true");
|
|
1153
|
+
} catch {
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
var wtClient = null;
|
|
1157
|
+
var wtClientPromise = null;
|
|
1158
|
+
var swPromise = null;
|
|
1159
|
+
async function ensureServiceWorker() {
|
|
1160
|
+
if (swPromise) return swPromise;
|
|
1161
|
+
swPromise = (async () => {
|
|
1162
|
+
if (!("serviceWorker" in navigator)) {
|
|
1163
|
+
throw new Error("Service workers are not supported in this browser");
|
|
1164
|
+
}
|
|
1165
|
+
const existing = await navigator.serviceWorker.getRegistration("/webtorrent-sw.js");
|
|
1166
|
+
if (existing?.active) return existing;
|
|
1167
|
+
const reg = await navigator.serviceWorker.register("/webtorrent-sw.js", {
|
|
1168
|
+
scope: "/"
|
|
1169
|
+
});
|
|
1170
|
+
if (reg.active) return reg;
|
|
1171
|
+
return new Promise((resolve, reject) => {
|
|
1172
|
+
const sw = reg.installing ?? reg.waiting;
|
|
1173
|
+
if (!sw) {
|
|
1174
|
+
if (reg.active) return resolve(reg);
|
|
1175
|
+
return reject(new Error("No service worker found after registration"));
|
|
1176
|
+
}
|
|
1177
|
+
const onStateChange = () => {
|
|
1178
|
+
if (sw.state === "activated") {
|
|
1179
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1180
|
+
resolve(reg);
|
|
1181
|
+
} else if (sw.state === "redundant") {
|
|
1182
|
+
sw.removeEventListener("statechange", onStateChange);
|
|
1183
|
+
reject(new Error("Service worker became redundant during activation"));
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
sw.addEventListener("statechange", onStateChange);
|
|
1187
|
+
});
|
|
1188
|
+
})();
|
|
1189
|
+
return swPromise;
|
|
1190
|
+
}
|
|
1191
|
+
async function getWebTorrentClient() {
|
|
1192
|
+
if (wtClient && !wtClient.destroyed) return wtClient;
|
|
1193
|
+
if (wtClientPromise) return wtClientPromise;
|
|
1194
|
+
wtClientPromise = (async () => {
|
|
1195
|
+
const { default: WebTorrentClass } = await import('webtorrent');
|
|
1196
|
+
const client = new WebTorrentClass();
|
|
1197
|
+
wtClient = client;
|
|
1198
|
+
try {
|
|
1199
|
+
const registration = await ensureServiceWorker();
|
|
1200
|
+
if (!client._server) {
|
|
1201
|
+
client.createServer({ controller: registration });
|
|
1202
|
+
}
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
console.warn("[magnet-player] Service worker setup failed:", err);
|
|
1205
|
+
}
|
|
1206
|
+
return client;
|
|
1207
|
+
})();
|
|
1208
|
+
return wtClientPromise;
|
|
1209
|
+
}
|
|
1210
|
+
function destroyWebTorrentClient() {
|
|
1211
|
+
if (wtClient && !wtClient.destroyed) {
|
|
1212
|
+
wtClient.destroy();
|
|
1213
|
+
}
|
|
1214
|
+
wtClient = null;
|
|
1215
|
+
wtClientPromise = null;
|
|
1216
|
+
}
|
|
1217
|
+
var FLAG_MAGNET_LINK = "magnet-link-enabled";
|
|
1218
|
+
function initFeatureFlags() {
|
|
1219
|
+
const url = process.env.NEXT_PUBLIC_UNLEASH_URL ?? "";
|
|
1220
|
+
const clientKey = process.env.NEXT_PUBLIC_UNLEASH_CLIENT_KEY ?? "";
|
|
1221
|
+
if (!url || !clientKey) {
|
|
1222
|
+
console.warn(
|
|
1223
|
+
"[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)."
|
|
1224
|
+
);
|
|
1225
|
+
return Promise.resolve();
|
|
1226
|
+
}
|
|
1227
|
+
return OpenFeature.setProviderAndWait(
|
|
1228
|
+
new UnleashWebProvider({ url, clientKey, appName: "lightbird" })
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1104
1232
|
// src/utils/media-error.ts
|
|
1105
1233
|
var MEDIA_ERR_ABORTED = 1;
|
|
1106
1234
|
var MEDIA_ERR_NETWORK = 2;
|
|
@@ -1325,4 +1453,4 @@ function resetFFmpeg() {
|
|
|
1325
1453
|
loading = null;
|
|
1326
1454
|
}
|
|
1327
1455
|
|
|
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 };
|
|
1456
|
+
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.0",
|
|
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",
|