@lightbird/ui 0.7.0 → 0.9.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 CHANGED
@@ -1436,6 +1436,7 @@ function VideoInfoPanel({ metadata, onClose }) {
1436
1436
  ["Frame Rate", metadata.frameRate ? `${metadata.frameRate} fps` : "\u2014"],
1437
1437
  ["Video Codec", metadata.videoCodec ?? "\u2014"],
1438
1438
  ["Video Bitrate", formatBitrate(metadata.videoBitrate)],
1439
+ ...metadata.streamRenditions ? [["Stream Renditions", `${metadata.streamRenditions} levels`]] : [],
1439
1440
  ...metadata.audioTracks.map(
1440
1441
  (t, i) => [
1441
1442
  `Audio ${i + 1}`,
@@ -1933,7 +1934,6 @@ var MAX_RETRIES = 3;
1933
1934
  var LightBirdPlayer = () => {
1934
1935
  const videoRef = React11.useRef(null);
1935
1936
  const containerRef = React11.useRef(null);
1936
- const canvasRef = React11.useRef(null);
1937
1937
  const subtitleInputRef = React11.useRef(null);
1938
1938
  const playerRef = React11.useRef(null);
1939
1939
  const subtitleFilesMapRef = React11.useRef(/* @__PURE__ */ new Map());
@@ -1963,7 +1963,7 @@ var LightBirdPlayer = () => {
1963
1963
  getBrightness: () => filters.filters.brightness / 200,
1964
1964
  setBrightness: (v) => filters.setFilters({ ...filters.filters, brightness: Math.round(v * 200) })
1965
1965
  });
1966
- const { metadata: videoMetadata } = react.useVideoInfo(videoRef, playlist.currentItem?.file ?? null);
1966
+ const { metadata: videoMetadata, enrichMetadata } = react.useVideoInfo(videoRef, playlist.currentItem?.file ?? null);
1967
1967
  react.useProgressPersistence(videoRef, playlist.currentItem?.name ?? null);
1968
1968
  const { chapters, currentChapter, goToChapter } = react.useChapters(videoRef, playerRef);
1969
1969
  const magnetLinkEnabled = reactSdk.useBooleanFlagValue(core.FLAG_MAGNET_LINK, true);
@@ -2177,19 +2177,37 @@ var LightBirdPlayer = () => {
2177
2177
  if (item.type === "stream") {
2178
2178
  playerRef.current?.destroy();
2179
2179
  playerRef.current = null;
2180
- if (videoRef.current) videoRef.current.src = item.url;
2181
2180
  subtitles.reset();
2182
2181
  setAudioTracks([]);
2183
2182
  setActiveAudioTrack("0");
2184
2183
  isStreamRef.current = true;
2185
2184
  startStallDetection();
2185
+ if (core.isHlsUrl(item.url) && videoRef.current) {
2186
+ const player = core.createVideoPlayer(item.url);
2187
+ playerRef.current = player;
2188
+ player.initialize(videoRef.current).then(() => {
2189
+ const refresh = () => {
2190
+ if (playerRef.current !== player) return;
2191
+ enrichMetadata(player.getMetadata?.() ?? {});
2192
+ const tracks = player.getAudioTracks();
2193
+ setAudioTracks(tracks);
2194
+ setActiveAudioTrack((prev) => prev || tracks[0]?.id || "0");
2195
+ };
2196
+ player.onMetadataChange?.(refresh);
2197
+ refresh();
2198
+ }).catch((error) => {
2199
+ console.error(error);
2200
+ });
2201
+ } else if (videoRef.current) {
2202
+ videoRef.current.src = item.url;
2203
+ }
2186
2204
  } else if (item.file) {
2187
2205
  isStreamRef.current = false;
2188
2206
  stopStallDetection();
2189
2207
  const subs = subtitleFilesMapRef.current.get(item.name) ?? [];
2190
2208
  processFile(item.file, subs);
2191
2209
  }
2192
- }, [playlist.playlist, playlist.selectItem, subtitles, processFile]);
2210
+ }, [playlist.playlist, playlist.selectItem, subtitles, processFile, enrichMetadata]);
2193
2211
  const handleSkipToNext = React11.useCallback(() => {
2194
2212
  setPlayerError(null);
2195
2213
  clearRetryTimer();
@@ -2425,19 +2443,17 @@ var LightBirdPlayer = () => {
2425
2443
  }, [toast2]);
2426
2444
  const captureScreenshot = React11.useCallback(() => {
2427
2445
  const video = videoRef.current;
2428
- const canvas = canvasRef.current;
2429
- if (!video || !canvas) return;
2430
- canvas.width = video.videoWidth;
2431
- canvas.height = video.videoHeight;
2432
- const ctx = canvas.getContext("2d");
2433
- if (!ctx) return;
2434
- ctx.filter = video.style.filter;
2435
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2436
- const dataUrl = canvas.toDataURL("image/png");
2437
- const a = document.createElement("a");
2438
- a.href = dataUrl;
2439
- a.download = `lightbird-screenshot-${(/* @__PURE__ */ new Date()).toISOString()}.png`;
2440
- a.click();
2446
+ if (!video) return;
2447
+ const dataUrl = core.exportVideoFrame(video, { filter: video.style.filter });
2448
+ if (!dataUrl) {
2449
+ toast2({
2450
+ title: "Screenshot failed",
2451
+ description: "The frame could not be captured (the video may be cross-origin protected).",
2452
+ variant: "destructive"
2453
+ });
2454
+ return;
2455
+ }
2456
+ core.downloadDataUrl(dataUrl, core.frameExportFilename("png"));
2441
2457
  toast2({ title: "Screenshot Saved" });
2442
2458
  }, [toast2]);
2443
2459
  const handleABLoopCycle = React11.useCallback(() => {
@@ -2516,7 +2532,6 @@ var LightBirdPlayer = () => {
2516
2532
  crossOrigin: "anonymous"
2517
2533
  }
2518
2534
  ),
2519
- /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "hidden" }),
2520
2535
  /* @__PURE__ */ jsxRuntime.jsx(SubtitleOverlay, { videoRef, activeSubtitle: subtitles.activeSubtitle }),
2521
2536
  /* @__PURE__ */ jsxRuntime.jsx(
2522
2537
  VideoOverlay,
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable }
18
18
  import { CSS } from '@dnd-kit/utilities';
19
19
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
20
20
  import * as SelectPrimitive from '@radix-ui/react-select';
21
- import { exportPlaylist, formatShortcutKey, FLAG_MAGNET_LINK, loadShortcuts, ProgressEstimator, createVideoPlayer, CancellationError, hasAcceptedDisclaimer, acceptDisclaimer, initFeatureFlags, parseMediaError, captureVideoThumbnail, validateFile, parseM3U8, matchesShortcut, saveShortcuts, DEFAULT_SHORTCUTS } from '@lightbird/core';
21
+ import { exportPlaylist, formatShortcutKey, FLAG_MAGNET_LINK, loadShortcuts, ProgressEstimator, createVideoPlayer, CancellationError, isHlsUrl, hasAcceptedDisclaimer, acceptDisclaimer, exportVideoFrame, downloadDataUrl, frameExportFilename, initFeatureFlags, parseMediaError, captureVideoThumbnail, validateFile, parseM3U8, matchesShortcut, saveShortcuts, DEFAULT_SHORTCUTS } from '@lightbird/core';
22
22
  import * as DialogPrimitive from '@radix-ui/react-dialog';
23
23
  import { useBooleanFlagValue, OpenFeatureProvider } from '@openfeature/react-sdk';
24
24
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
@@ -1405,6 +1405,7 @@ function VideoInfoPanel({ metadata, onClose }) {
1405
1405
  ["Frame Rate", metadata.frameRate ? `${metadata.frameRate} fps` : "\u2014"],
1406
1406
  ["Video Codec", metadata.videoCodec ?? "\u2014"],
1407
1407
  ["Video Bitrate", formatBitrate(metadata.videoBitrate)],
1408
+ ...metadata.streamRenditions ? [["Stream Renditions", `${metadata.streamRenditions} levels`]] : [],
1408
1409
  ...metadata.audioTracks.map(
1409
1410
  (t, i) => [
1410
1411
  `Audio ${i + 1}`,
@@ -1902,7 +1903,6 @@ var MAX_RETRIES = 3;
1902
1903
  var LightBirdPlayer = () => {
1903
1904
  const videoRef = useRef(null);
1904
1905
  const containerRef = useRef(null);
1905
- const canvasRef = useRef(null);
1906
1906
  const subtitleInputRef = useRef(null);
1907
1907
  const playerRef = useRef(null);
1908
1908
  const subtitleFilesMapRef = useRef(/* @__PURE__ */ new Map());
@@ -1932,7 +1932,7 @@ var LightBirdPlayer = () => {
1932
1932
  getBrightness: () => filters.filters.brightness / 200,
1933
1933
  setBrightness: (v) => filters.setFilters({ ...filters.filters, brightness: Math.round(v * 200) })
1934
1934
  });
1935
- const { metadata: videoMetadata } = useVideoInfo(videoRef, playlist.currentItem?.file ?? null);
1935
+ const { metadata: videoMetadata, enrichMetadata } = useVideoInfo(videoRef, playlist.currentItem?.file ?? null);
1936
1936
  useProgressPersistence(videoRef, playlist.currentItem?.name ?? null);
1937
1937
  const { chapters, currentChapter, goToChapter } = useChapters(videoRef, playerRef);
1938
1938
  const magnetLinkEnabled = useBooleanFlagValue(FLAG_MAGNET_LINK, true);
@@ -2146,19 +2146,37 @@ var LightBirdPlayer = () => {
2146
2146
  if (item.type === "stream") {
2147
2147
  playerRef.current?.destroy();
2148
2148
  playerRef.current = null;
2149
- if (videoRef.current) videoRef.current.src = item.url;
2150
2149
  subtitles.reset();
2151
2150
  setAudioTracks([]);
2152
2151
  setActiveAudioTrack("0");
2153
2152
  isStreamRef.current = true;
2154
2153
  startStallDetection();
2154
+ if (isHlsUrl(item.url) && videoRef.current) {
2155
+ const player = createVideoPlayer(item.url);
2156
+ playerRef.current = player;
2157
+ player.initialize(videoRef.current).then(() => {
2158
+ const refresh = () => {
2159
+ if (playerRef.current !== player) return;
2160
+ enrichMetadata(player.getMetadata?.() ?? {});
2161
+ const tracks = player.getAudioTracks();
2162
+ setAudioTracks(tracks);
2163
+ setActiveAudioTrack((prev) => prev || tracks[0]?.id || "0");
2164
+ };
2165
+ player.onMetadataChange?.(refresh);
2166
+ refresh();
2167
+ }).catch((error) => {
2168
+ console.error(error);
2169
+ });
2170
+ } else if (videoRef.current) {
2171
+ videoRef.current.src = item.url;
2172
+ }
2155
2173
  } else if (item.file) {
2156
2174
  isStreamRef.current = false;
2157
2175
  stopStallDetection();
2158
2176
  const subs = subtitleFilesMapRef.current.get(item.name) ?? [];
2159
2177
  processFile(item.file, subs);
2160
2178
  }
2161
- }, [playlist.playlist, playlist.selectItem, subtitles, processFile]);
2179
+ }, [playlist.playlist, playlist.selectItem, subtitles, processFile, enrichMetadata]);
2162
2180
  const handleSkipToNext = useCallback(() => {
2163
2181
  setPlayerError(null);
2164
2182
  clearRetryTimer();
@@ -2394,19 +2412,17 @@ var LightBirdPlayer = () => {
2394
2412
  }, [toast2]);
2395
2413
  const captureScreenshot = useCallback(() => {
2396
2414
  const video = videoRef.current;
2397
- const canvas = canvasRef.current;
2398
- if (!video || !canvas) return;
2399
- canvas.width = video.videoWidth;
2400
- canvas.height = video.videoHeight;
2401
- const ctx = canvas.getContext("2d");
2402
- if (!ctx) return;
2403
- ctx.filter = video.style.filter;
2404
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
2405
- const dataUrl = canvas.toDataURL("image/png");
2406
- const a = document.createElement("a");
2407
- a.href = dataUrl;
2408
- a.download = `lightbird-screenshot-${(/* @__PURE__ */ new Date()).toISOString()}.png`;
2409
- a.click();
2415
+ if (!video) return;
2416
+ const dataUrl = exportVideoFrame(video, { filter: video.style.filter });
2417
+ if (!dataUrl) {
2418
+ toast2({
2419
+ title: "Screenshot failed",
2420
+ description: "The frame could not be captured (the video may be cross-origin protected).",
2421
+ variant: "destructive"
2422
+ });
2423
+ return;
2424
+ }
2425
+ downloadDataUrl(dataUrl, frameExportFilename("png"));
2410
2426
  toast2({ title: "Screenshot Saved" });
2411
2427
  }, [toast2]);
2412
2428
  const handleABLoopCycle = useCallback(() => {
@@ -2485,7 +2501,6 @@ var LightBirdPlayer = () => {
2485
2501
  crossOrigin: "anonymous"
2486
2502
  }
2487
2503
  ),
2488
- /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "hidden" }),
2489
2504
  /* @__PURE__ */ jsx(SubtitleOverlay, { videoRef, activeSubtitle: subtitles.activeSubtitle }),
2490
2505
  /* @__PURE__ */ jsx(
2491
2506
  VideoOverlay,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightbird/ui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Drop-in React video player component powered by LightBird. Full controls, playlist, subtitles, chapters — one import.",
5
5
  "license": "MIT",
6
6
  "author": "Punyam Singh",
@@ -65,7 +65,7 @@
65
65
  "clsx": "^2.1.1",
66
66
  "lucide-react": "^0.475.0",
67
67
  "tailwind-merge": "^3.0.1",
68
- "@lightbird/core": "0.7.0"
68
+ "@lightbird/core": "0.9.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "^18.0.0 || ^19.0.0",