@geekapps/silo-elements-nextjs 0.3.16 → 0.3.18

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.js CHANGED
@@ -1536,8 +1536,47 @@ function MediaUploader({
1536
1536
  var AUTO_QUALITY = {
1537
1537
  id: "auto",
1538
1538
  label: "Auto",
1539
- type: "auto"
1539
+ type: "auto",
1540
+ supported: true
1540
1541
  };
1542
+ function deviceSupportsHdr() {
1543
+ if (typeof window === "undefined") return false;
1544
+ try {
1545
+ return window.matchMedia("(dynamic-range: high)").matches;
1546
+ } catch {
1547
+ return false;
1548
+ }
1549
+ }
1550
+ function deviceSupportsOpus() {
1551
+ if (typeof window === "undefined") return false;
1552
+ try {
1553
+ if (typeof MediaSource !== "undefined" && MediaSource.isTypeSupported) {
1554
+ if (MediaSource.isTypeSupported('audio/mp4; codecs="opus"')) return true;
1555
+ }
1556
+ const a = document.createElement("audio");
1557
+ return a.canPlayType('audio/ogg; codecs="opus"') !== "" || a.canPlayType('video/mp4; codecs="opus"') !== "";
1558
+ } catch {
1559
+ return false;
1560
+ }
1561
+ }
1562
+ function deviceSupportsCodec(codecStr) {
1563
+ if (typeof window === "undefined") return true;
1564
+ try {
1565
+ if (typeof MediaSource !== "undefined" && MediaSource.isTypeSupported) {
1566
+ return MediaSource.isTypeSupported(`video/mp4; codecs="${codecStr}"`);
1567
+ }
1568
+ return true;
1569
+ } catch {
1570
+ return true;
1571
+ }
1572
+ }
1573
+ function isHdrLevel(level) {
1574
+ const range = level?.videoRange ?? level?.video_range ?? "";
1575
+ return range === "PQ" || range === "HLG" || range === "HDR10" || typeof level?.name === "string" && /hdr/i.test(level.name);
1576
+ }
1577
+ function isHdrAudioCodec(codecStr) {
1578
+ return /opus/i.test(codecStr);
1579
+ }
1541
1580
  var PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
1542
1581
  function Source(_props) {
1543
1582
  return null;
@@ -2292,12 +2331,14 @@ function Video({
2292
2331
  if (Hls.isSupported()) {
2293
2332
  const hls = new Hls({
2294
2333
  enableWorker: true,
2295
- maxBufferLength: 30,
2296
- maxMaxBufferLength: 120,
2297
- maxBufferSize: 120 * 1e3 * 1e3,
2298
- backBufferLength: 30,
2334
+ // Buffer ~20s ahead (≈1/6 of a 2min video). maxMaxBufferLength caps the
2335
+ // absolute ceiling so HLS.js doesn't buffer the entire file on fast connections.
2336
+ maxBufferLength: 20,
2337
+ maxMaxBufferLength: 30,
2338
+ maxBufferSize: 40 * 1e3 * 1e3,
2339
+ // Keep only 10s of back-buffer for seeking — avoids holding GB of 4K data
2340
+ backBufferLength: 10,
2299
2341
  maxBufferHole: 0.5,
2300
- // Be patient with large 4K/HDR fragments before declaring a stall
2301
2342
  highBufferWatchdogPeriod: 5,
2302
2343
  nudgeOffset: 0.2,
2303
2344
  nudgeMaxRetry: 3,
@@ -2315,23 +2356,28 @@ function Video({
2315
2356
  hls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
2316
2357
  const levels = data.levels ?? hls.levels ?? [];
2317
2358
  console.debug("[Silo/hls] MANIFEST_PARSED levels=", levels.length, "audioTracks=", hls.audioTracks?.length ?? 0);
2359
+ const hdrSupported = deviceSupportsHdr();
2360
+ const opusSupported = deviceSupportsOpus();
2318
2361
  setQualities([
2319
2362
  AUTO_QUALITY,
2320
- ...levels.map((level, index) => ({
2321
- id: `hls-${index}`,
2322
- label: level.name ?? (level.height ? `${level.height}p` : `${index + 1}`),
2323
- type: "hls",
2324
- index
2325
- }))
2363
+ ...levels.map((level, index) => {
2364
+ const hdr = isHdrLevel(level);
2365
+ const codecStr = level.videoCodec ?? level.attrs?.CODECS ?? "";
2366
+ const supported = hdr ? hdrSupported : deviceSupportsCodec(codecStr);
2367
+ const baseName = level.name ?? (level.height ? `${level.height}p` : `${index + 1}`);
2368
+ const label = hdr ? `${baseName} HDR` : baseName;
2369
+ return { id: `hls-${index}`, label, type: "hls", index, supported, hdr };
2370
+ })
2326
2371
  ]);
2327
2372
  const tracks = hls.audioTracks ?? [];
2328
2373
  console.debug("[Silo/hls] audio tracks:", tracks.map((t) => ({ name: t.name, lang: t.lang, url: t.url })));
2329
2374
  if (tracks.length > 1) {
2330
2375
  setAudioTracks(
2331
- tracks.map((t, i) => ({
2332
- id: i,
2333
- label: t.name ?? t.lang ?? `Track ${i + 1}`
2334
- }))
2376
+ tracks.map((t, i) => {
2377
+ const codec = t.attrs?.CODECS ?? t.codecSet ?? "";
2378
+ const supported = isHdrAudioCodec(codec) ? opusSupported : true;
2379
+ return { id: i, label: t.name ?? t.lang ?? `Track ${i + 1}`, supported };
2380
+ })
2335
2381
  );
2336
2382
  if (pinnedAudio === -1) setSelectedAudio(hls.audioTrack ?? 0);
2337
2383
  }
@@ -2342,12 +2388,14 @@ function Video({
2342
2388
  hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, (_, data) => {
2343
2389
  const tracks = data.audioTracks ?? [];
2344
2390
  console.debug("[Silo/hls] AUDIO_TRACKS_UPDATED", tracks.length);
2391
+ const opusSupported = deviceSupportsOpus();
2345
2392
  if (tracks.length > 1) {
2346
2393
  setAudioTracks(
2347
- tracks.map((t, i) => ({
2348
- id: i,
2349
- label: t.name ?? t.lang ?? `Track ${i + 1}`
2350
- }))
2394
+ tracks.map((t, i) => {
2395
+ const codec = t.attrs?.CODECS ?? t.codecSet ?? "";
2396
+ const supported = isHdrAudioCodec(codec) ? opusSupported : true;
2397
+ return { id: i, label: t.name ?? t.lang ?? `Track ${i + 1}`, supported };
2398
+ })
2351
2399
  );
2352
2400
  }
2353
2401
  if (pinnedAudio >= 0 && hls.audioTrack !== pinnedAudio) {
@@ -2356,6 +2404,9 @@ function Video({
2356
2404
  });
2357
2405
  hls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
2358
2406
  console.debug("[Silo/hls] LEVEL_SWITCHED", data.level);
2407
+ if (pinnedLevel >= 0 && data.level === pinnedLevel) {
2408
+ hls.currentLevel = pinnedLevel;
2409
+ }
2359
2410
  if (pinnedAudio >= 0 && hls.audioTrack !== pinnedAudio) {
2360
2411
  hls.audioTrack = pinnedAudio;
2361
2412
  }
@@ -2586,7 +2637,7 @@ function Video({
2586
2637
  }
2587
2638
  if (option.type === "hls" && hlsRef.current && option.index != null) {
2588
2639
  hlsRef.current.__pinLevel?.(option.index);
2589
- hlsRef.current.currentLevel = option.index;
2640
+ hlsRef.current.nextLevel = option.index;
2590
2641
  return;
2591
2642
  }
2592
2643
  if (option.type === "dash" && dashRef.current && option.index != null) {
@@ -3038,48 +3089,56 @@ function Video({
3038
3089
  ]
3039
3090
  }
3040
3091
  ),
3041
- /* @__PURE__ */ jsx("div", { className: "py-1.5", children: [...qualities].reverse().map((quality) => /* @__PURE__ */ jsxs(
3042
- "button",
3043
- {
3044
- type: "button",
3045
- onClick: () => {
3046
- changeQuality(quality.id);
3047
- setSettingsTab("root");
3048
- },
3049
- className: "flex w-full items-center gap-3 px-4 py-2.5 text-sm transition hover:bg-white/8",
3050
- children: [
3051
- /* @__PURE__ */ jsx(
3052
- "svg",
3053
- {
3054
- className: `size-4 shrink-0 ${selectedQuality === quality.id ? "text-white" : "text-transparent"}`,
3055
- viewBox: "0 0 16 16",
3056
- fill: "none",
3057
- children: /* @__PURE__ */ jsx(
3058
- "path",
3092
+ /* @__PURE__ */ jsx("div", { className: "py-1.5", children: [...qualities].reverse().map((quality) => {
3093
+ const unavailable = quality.supported === false;
3094
+ return /* @__PURE__ */ jsxs(
3095
+ "button",
3096
+ {
3097
+ type: "button",
3098
+ onClick: () => {
3099
+ if (unavailable) return;
3100
+ changeQuality(quality.id);
3101
+ setSettingsTab("root");
3102
+ },
3103
+ disabled: unavailable,
3104
+ className: `flex w-full items-center gap-3 px-4 py-2.5 text-sm transition ${unavailable ? "cursor-not-allowed opacity-40" : "hover:bg-white/8"}`,
3105
+ children: [
3106
+ /* @__PURE__ */ jsx(
3107
+ "svg",
3108
+ {
3109
+ className: `size-4 shrink-0 ${selectedQuality === quality.id ? "text-white" : "text-transparent"}`,
3110
+ viewBox: "0 0 16 16",
3111
+ fill: "none",
3112
+ children: /* @__PURE__ */ jsx(
3113
+ "path",
3114
+ {
3115
+ d: "M3 8l3.5 3.5L13 4.5",
3116
+ stroke: "currentColor",
3117
+ strokeWidth: "1.8",
3118
+ strokeLinecap: "round",
3119
+ strokeLinejoin: "round"
3120
+ }
3121
+ )
3122
+ }
3123
+ ),
3124
+ /* @__PURE__ */ jsxs("span", { className: "flex-1 text-left", children: [
3125
+ /* @__PURE__ */ jsxs(
3126
+ "span",
3059
3127
  {
3060
- d: "M3 8l3.5 3.5L13 4.5",
3061
- stroke: "currentColor",
3062
- strokeWidth: "1.8",
3063
- strokeLinecap: "round",
3064
- strokeLinejoin: "round"
3128
+ className: selectedQuality === quality.id ? "font-semibold text-white" : "text-white/55",
3129
+ children: [
3130
+ quality.label,
3131
+ quality.id === "auto" ? " (ABR)" : ""
3132
+ ]
3065
3133
  }
3066
- )
3067
- }
3068
- ),
3069
- /* @__PURE__ */ jsxs(
3070
- "span",
3071
- {
3072
- className: selectedQuality === quality.id ? "font-semibold text-white" : "text-white/55",
3073
- children: [
3074
- quality.label,
3075
- quality.id === "auto" ? " (ABR)" : ""
3076
- ]
3077
- }
3078
- )
3079
- ]
3080
- },
3081
- quality.id
3082
- )) })
3134
+ ),
3135
+ unavailable && /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-[10px] text-white/30", children: "N\xE3o dispon\xEDvel" })
3136
+ ] })
3137
+ ]
3138
+ },
3139
+ quality.id
3140
+ );
3141
+ }) })
3083
3142
  ] }),
3084
3143
  settingsTab === "subtitles" && /* @__PURE__ */ jsxs("div", { children: [
3085
3144
  /* @__PURE__ */ jsxs(
@@ -3301,45 +3360,53 @@ function Video({
3301
3360
  ]
3302
3361
  }
3303
3362
  ),
3304
- /* @__PURE__ */ jsx("div", { className: "py-1.5", children: audioTracks.map((track) => /* @__PURE__ */ jsxs(
3305
- "button",
3306
- {
3307
- type: "button",
3308
- onClick: () => {
3309
- changeAudio(track.id);
3310
- setSettingsTab("root");
3311
- },
3312
- className: "flex w-full items-center gap-3 px-4 py-2.5 text-sm transition hover:bg-white/8",
3313
- children: [
3314
- /* @__PURE__ */ jsx(
3315
- "svg",
3316
- {
3317
- className: `size-4 shrink-0 ${selectedAudio === track.id ? "text-white" : "text-transparent"}`,
3318
- viewBox: "0 0 16 16",
3319
- fill: "none",
3320
- children: /* @__PURE__ */ jsx(
3321
- "path",
3363
+ /* @__PURE__ */ jsx("div", { className: "py-1.5", children: audioTracks.map((track) => {
3364
+ const unavailable = track.supported === false;
3365
+ return /* @__PURE__ */ jsxs(
3366
+ "button",
3367
+ {
3368
+ type: "button",
3369
+ onClick: () => {
3370
+ if (unavailable) return;
3371
+ changeAudio(track.id);
3372
+ setSettingsTab("root");
3373
+ },
3374
+ disabled: unavailable,
3375
+ className: `flex w-full items-center gap-3 px-4 py-2.5 text-sm transition ${unavailable ? "cursor-not-allowed opacity-40" : "hover:bg-white/8"}`,
3376
+ children: [
3377
+ /* @__PURE__ */ jsx(
3378
+ "svg",
3379
+ {
3380
+ className: `size-4 shrink-0 ${selectedAudio === track.id ? "text-white" : "text-transparent"}`,
3381
+ viewBox: "0 0 16 16",
3382
+ fill: "none",
3383
+ children: /* @__PURE__ */ jsx(
3384
+ "path",
3385
+ {
3386
+ d: "M3 8l3.5 3.5L13 4.5",
3387
+ stroke: "currentColor",
3388
+ strokeWidth: "1.8",
3389
+ strokeLinecap: "round",
3390
+ strokeLinejoin: "round"
3391
+ }
3392
+ )
3393
+ }
3394
+ ),
3395
+ /* @__PURE__ */ jsxs("span", { className: "flex-1 text-left", children: [
3396
+ /* @__PURE__ */ jsx(
3397
+ "span",
3322
3398
  {
3323
- d: "M3 8l3.5 3.5L13 4.5",
3324
- stroke: "currentColor",
3325
- strokeWidth: "1.8",
3326
- strokeLinecap: "round",
3327
- strokeLinejoin: "round"
3399
+ className: selectedAudio === track.id ? "font-semibold text-white" : "text-white/55",
3400
+ children: track.label
3328
3401
  }
3329
- )
3330
- }
3331
- ),
3332
- /* @__PURE__ */ jsx(
3333
- "span",
3334
- {
3335
- className: selectedAudio === track.id ? "font-semibold text-white" : "text-white/55",
3336
- children: track.label
3337
- }
3338
- )
3339
- ]
3340
- },
3341
- track.id
3342
- )) })
3402
+ ),
3403
+ unavailable && /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-[10px] text-white/30", children: "N\xE3o dispon\xEDvel" })
3404
+ ] })
3405
+ ]
3406
+ },
3407
+ track.id
3408
+ );
3409
+ }) })
3343
3410
  ] }),
3344
3411
  settingsTab === "playback" && /* @__PURE__ */ jsxs("div", { children: [
3345
3412
  /* @__PURE__ */ jsxs(