@scarlett-player/embed 0.5.3 → 1.0.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.
@@ -43,6 +43,18 @@ function mapLevels(levels, _currentLevel) {
43
43
  codec: level.codecSet
44
44
  }));
45
45
  }
46
+ function getInitialBandwidthEstimate(overrideBps) {
47
+ const HLS_DEFAULT_ESTIMATE = 5e5;
48
+ if (overrideBps !== void 0 && overrideBps > 0) {
49
+ return overrideBps;
50
+ }
51
+ const connection = navigator.connection;
52
+ if (connection?.downlink && connection.downlink > 0) {
53
+ const bps = connection.downlink * 1e6;
54
+ return Math.round(bps * 0.85);
55
+ }
56
+ return HLS_DEFAULT_ESTIMATE;
57
+ }
46
58
  var HLS_ERROR_TYPES = {
47
59
  NETWORK_ERROR: "networkError",
48
60
  MEDIA_ERROR: "mediaError",
@@ -113,6 +125,14 @@ function setupHlsEventHandlers(hls, api, callbacks) {
113
125
  });
114
126
  callbacks.onLevelSwitched?.(data.level);
115
127
  });
128
+ let lastBandwidthUpdate = 0;
129
+ addHandler("hlsFragLoaded", () => {
130
+ const now = Date.now();
131
+ if (now - lastBandwidthUpdate >= 2e3 && hls.bandwidthEstimate) {
132
+ lastBandwidthUpdate = now;
133
+ api.setState("bandwidth", Math.round(hls.bandwidthEstimate));
134
+ }
135
+ });
116
136
  addHandler("hlsFragBuffered", () => {
117
137
  api.setState("buffering", false);
118
138
  callbacks.onBufferUpdate?.();
@@ -355,6 +375,7 @@ var DEFAULT_CONFIG = {
355
375
  maxMaxBufferLength: 600,
356
376
  backBufferLength: 30,
357
377
  enableWorker: true,
378
+ capLevelToPlayerSize: true,
358
379
  // Error recovery settings
359
380
  maxNetworkRetries: 3,
360
381
  maxMediaRetries: 2,
@@ -424,11 +445,13 @@ function createHLSPlugin(config) {
424
445
  startPosition: mergedConfig.startPosition,
425
446
  startLevel: -1,
426
447
  // Auto quality selection (ABR)
448
+ abrEwmaDefaultEstimate: getInitialBandwidthEstimate(mergedConfig.initialBandwidthEstimate),
427
449
  lowLatencyMode: mergedConfig.lowLatencyMode,
428
450
  maxBufferLength: mergedConfig.maxBufferLength,
429
451
  maxMaxBufferLength: mergedConfig.maxMaxBufferLength,
430
452
  backBufferLength: mergedConfig.backBufferLength,
431
453
  enableWorker: mergedConfig.enableWorker,
454
+ capLevelToPlayerSize: mergedConfig.capLevelToPlayerSize,
432
455
  // Minimize hls.js internal retries - we handle retries ourselves
433
456
  fragLoadingMaxRetry: 1,
434
457
  manifestLoadingMaxRetry: 1,
@@ -699,7 +722,10 @@ function createHLSPlugin(config) {
699
722
  currentSrc = src;
700
723
  api.setState("playbackState", "loading");
701
724
  api.setState("buffering", true);
702
- if (isHlsJsSupported()) {
725
+ if (api.getState("airplayActive") && supportsNativeHLS()) {
726
+ api.logger.info("Using native HLS (AirPlay active)");
727
+ await loadNative(src);
728
+ } else if (isHlsJsSupported()) {
703
729
  api.logger.info("Using hls.js for HLS playback");
704
730
  await loadWithHlsJs(src);
705
731
  } else if (supportsNativeHLS()) {
@@ -3241,6 +3267,37 @@ var CaptionsButton = class {
3241
3267
  this.el.remove();
3242
3268
  }
3243
3269
  };
3270
+ var ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/><line x1="2" y1="2" x2="22" y2="22" stroke="currentColor" stroke-width="2"/></svg>`;
3271
+ var BandwidthIndicator = class {
3272
+ constructor(api) {
3273
+ this.api = api;
3274
+ this.el = createElement("div", { className: "sp-bandwidth-indicator" });
3275
+ this.el.innerHTML = ICON_SVG;
3276
+ this.el.setAttribute("aria-label", "Bandwidth is limiting video quality");
3277
+ this.el.setAttribute("title", "Bandwidth is limiting video quality");
3278
+ this.el.style.display = "none";
3279
+ }
3280
+ render() {
3281
+ return this.el;
3282
+ }
3283
+ update() {
3284
+ const bandwidth = this.api.getState("bandwidth");
3285
+ const qualities = this.api.getState("qualities");
3286
+ if (!bandwidth || !qualities || qualities.length === 0) {
3287
+ this.el.style.display = "none";
3288
+ return;
3289
+ }
3290
+ const highestBitrate = Math.max(...qualities.map((q) => q.bitrate));
3291
+ if (highestBitrate > 0 && bandwidth < highestBitrate) {
3292
+ this.el.style.display = "";
3293
+ } else {
3294
+ this.el.style.display = "none";
3295
+ }
3296
+ }
3297
+ destroy() {
3298
+ this.el.remove();
3299
+ }
3300
+ };
3244
3301
  var DEFAULT_LAYOUT = [
3245
3302
  "play",
3246
3303
  "skip-backward",
@@ -3248,6 +3305,7 @@ var DEFAULT_LAYOUT = [
3248
3305
  "volume",
3249
3306
  "time",
3250
3307
  "live-indicator",
3308
+ "bandwidth-indicator",
3251
3309
  "spacer",
3252
3310
  "settings",
3253
3311
  "captions",
@@ -3288,6 +3346,8 @@ function uiPlugin(config = {}) {
3288
3346
  return new TimeDisplay(api);
3289
3347
  case "live-indicator":
3290
3348
  return new LiveIndicator(api);
3349
+ case "bandwidth-indicator":
3350
+ return new BandwidthIndicator(api);
3291
3351
  case "quality":
3292
3352
  return new QualityMenu(api);
3293
3353
  case "settings":
@@ -3556,6 +3616,336 @@ function uiPlugin(config = {}) {
3556
3616
  }
3557
3617
  };
3558
3618
  }
3619
+ var POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
3620
+ var POSITION_STYLES = {
3621
+ "top-left": "top:10px;left:10px;",
3622
+ "top-right": "top:10px;right:10px;",
3623
+ "bottom-left": "bottom:40px;left:10px;",
3624
+ "bottom-right": "bottom:40px;right:10px;",
3625
+ "center": "top:50%;left:50%;transform:translate(-50%,-50%);"
3626
+ };
3627
+ function createWatermarkPlugin(config = {}) {
3628
+ let api = null;
3629
+ let element = null;
3630
+ let dynamicTimer = null;
3631
+ let showDelayTimer = null;
3632
+ let currentPosition = config.position || "bottom-right";
3633
+ const opacity = config.opacity ?? 0.5;
3634
+ const fontSize = config.fontSize ?? 14;
3635
+ const dynamic = config.dynamic ?? false;
3636
+ const dynamicInterval = config.dynamicInterval ?? 1e4;
3637
+ const showDelay = config.showDelay ?? 0;
3638
+ const createElement2 = () => {
3639
+ const el = document.createElement("div");
3640
+ el.className = "sp-watermark sp-watermark--hidden";
3641
+ el.style.cssText = `position:absolute;z-index:10;pointer-events:none;opacity:${opacity};font-size:${fontSize}px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.6);font-family:sans-serif;transition:all 0.5s ease;${POSITION_STYLES[currentPosition]}`;
3642
+ el.setAttribute("data-position", currentPosition);
3643
+ updateContent(el);
3644
+ return el;
3645
+ };
3646
+ const updateContent = (el, imageUrl, text) => {
3647
+ const img = imageUrl || config.imageUrl;
3648
+ const txt = text || config.text;
3649
+ el.innerHTML = "";
3650
+ if (img) {
3651
+ const imgEl = document.createElement("img");
3652
+ imgEl.src = img;
3653
+ imgEl.style.cssText = `max-height:${fontSize * 2}px;opacity:inherit;display:block;`;
3654
+ imgEl.alt = "";
3655
+ el.appendChild(imgEl);
3656
+ } else if (txt) {
3657
+ el.textContent = txt;
3658
+ }
3659
+ };
3660
+ const setPosition = (position) => {
3661
+ if (!element) return;
3662
+ currentPosition = position;
3663
+ element.style.top = "";
3664
+ element.style.right = "";
3665
+ element.style.bottom = "";
3666
+ element.style.left = "";
3667
+ element.style.transform = "";
3668
+ const styles2 = POSITION_STYLES[position];
3669
+ styles2.split(";").filter(Boolean).forEach((rule) => {
3670
+ const colonIdx = rule.indexOf(":");
3671
+ if (colonIdx === -1) return;
3672
+ const prop = rule.slice(0, colonIdx).trim();
3673
+ const val = rule.slice(colonIdx + 1).trim();
3674
+ if (prop && val) {
3675
+ element.style.setProperty(prop, val);
3676
+ }
3677
+ });
3678
+ element.setAttribute("data-position", position);
3679
+ const isVisible = element.classList.contains("sp-watermark--visible");
3680
+ const visClass = isVisible ? " sp-watermark--visible" : " sp-watermark--hidden";
3681
+ element.className = `sp-watermark sp-watermark--${position}${visClass}${dynamic ? " sp-watermark--dynamic" : ""}`;
3682
+ };
3683
+ const randomizePosition = () => {
3684
+ const available = POSITIONS.filter((p) => p !== currentPosition);
3685
+ const next = available[Math.floor(Math.random() * available.length)];
3686
+ setPosition(next);
3687
+ };
3688
+ const show = () => {
3689
+ if (!element) return;
3690
+ element.classList.remove("sp-watermark--hidden");
3691
+ element.classList.add("sp-watermark--visible");
3692
+ };
3693
+ const hide = () => {
3694
+ if (!element) return;
3695
+ element.classList.remove("sp-watermark--visible");
3696
+ element.classList.add("sp-watermark--hidden");
3697
+ };
3698
+ const startDynamic = () => {
3699
+ if (!dynamic || dynamicTimer) return;
3700
+ dynamicTimer = setInterval(randomizePosition, dynamicInterval);
3701
+ };
3702
+ const stopDynamic = () => {
3703
+ if (dynamicTimer) {
3704
+ clearInterval(dynamicTimer);
3705
+ dynamicTimer = null;
3706
+ }
3707
+ };
3708
+ const cleanup = () => {
3709
+ stopDynamic();
3710
+ if (showDelayTimer) {
3711
+ clearTimeout(showDelayTimer);
3712
+ showDelayTimer = null;
3713
+ }
3714
+ if (element?.parentNode) {
3715
+ element.parentNode.removeChild(element);
3716
+ }
3717
+ element = null;
3718
+ };
3719
+ return {
3720
+ id: "watermark",
3721
+ name: "Watermark",
3722
+ version: "1.0.0",
3723
+ type: "feature",
3724
+ description: "Anti-piracy watermark overlay with text/image support and dynamic repositioning",
3725
+ init(pluginApi) {
3726
+ api = pluginApi;
3727
+ api.logger.debug("Watermark plugin initialized");
3728
+ element = createElement2();
3729
+ api.container.appendChild(element);
3730
+ const unsubPlay = api.on("playback:play", () => {
3731
+ if (showDelay > 0) {
3732
+ showDelayTimer = setTimeout(() => {
3733
+ show();
3734
+ startDynamic();
3735
+ }, showDelay);
3736
+ } else {
3737
+ show();
3738
+ startDynamic();
3739
+ }
3740
+ });
3741
+ const unsubPause = api.on("playback:pause", () => {
3742
+ hide();
3743
+ stopDynamic();
3744
+ if (showDelayTimer) {
3745
+ clearTimeout(showDelayTimer);
3746
+ showDelayTimer = null;
3747
+ }
3748
+ });
3749
+ const unsubEnded = api.on("playback:ended", () => {
3750
+ hide();
3751
+ stopDynamic();
3752
+ });
3753
+ const unsubChange = api.on("playlist:change", ({ track }) => {
3754
+ if (!element || !track) return;
3755
+ const metadata = track.metadata;
3756
+ if (metadata) {
3757
+ const watermarkUrl = metadata.watermarkUrl;
3758
+ const watermarkText = metadata.watermarkText;
3759
+ if (watermarkUrl || watermarkText) {
3760
+ updateContent(element, watermarkUrl, watermarkText);
3761
+ }
3762
+ }
3763
+ });
3764
+ api.onDestroy(() => {
3765
+ unsubPlay();
3766
+ unsubPause();
3767
+ unsubEnded();
3768
+ unsubChange();
3769
+ cleanup();
3770
+ });
3771
+ },
3772
+ destroy() {
3773
+ api?.logger.debug("Watermark plugin destroyed");
3774
+ cleanup();
3775
+ api = null;
3776
+ },
3777
+ setText(text) {
3778
+ if (element) updateContent(element, void 0, text);
3779
+ },
3780
+ setImage(imageUrl) {
3781
+ if (element) updateContent(element, imageUrl);
3782
+ },
3783
+ setPosition,
3784
+ setOpacity(value) {
3785
+ if (element) element.style.opacity = String(Math.max(0, Math.min(1, value)));
3786
+ },
3787
+ show,
3788
+ hide,
3789
+ getConfig() {
3790
+ return { ...config, position: currentPosition, opacity: element ? parseFloat(element.style.opacity) || opacity : opacity };
3791
+ }
3792
+ };
3793
+ }
3794
+ function createCaptionsPlugin(config = {}) {
3795
+ let api = null;
3796
+ let video = null;
3797
+ let addedTrackElements = [];
3798
+ const extractFromHLS = config.extractFromHLS !== false;
3799
+ const autoSelect = config.autoSelect ?? false;
3800
+ const defaultLanguage = config.defaultLanguage ?? "en";
3801
+ const getVideo2 = () => {
3802
+ if (video) return video;
3803
+ video = api?.container.querySelector("video") ?? null;
3804
+ return video;
3805
+ };
3806
+ const cleanupTracks = () => {
3807
+ for (const trackEl of addedTrackElements) {
3808
+ trackEl.parentNode?.removeChild(trackEl);
3809
+ }
3810
+ addedTrackElements = [];
3811
+ api?.setState("textTracks", []);
3812
+ api?.setState("currentTextTrack", null);
3813
+ };
3814
+ const addTrackElement = (source) => {
3815
+ const videoEl = getVideo2();
3816
+ if (!videoEl) throw new Error("No video element");
3817
+ const trackEl = document.createElement("track");
3818
+ trackEl.kind = source.kind || "subtitles";
3819
+ trackEl.label = source.label;
3820
+ trackEl.srclang = source.language;
3821
+ trackEl.src = source.src;
3822
+ trackEl.default = false;
3823
+ videoEl.appendChild(trackEl);
3824
+ addedTrackElements.push(trackEl);
3825
+ if (trackEl.track) {
3826
+ trackEl.track.mode = "disabled";
3827
+ }
3828
+ return trackEl;
3829
+ };
3830
+ const syncTracksToState = () => {
3831
+ const videoEl = getVideo2();
3832
+ if (!videoEl) return;
3833
+ const tracks = [];
3834
+ let currentTrack = null;
3835
+ for (let i = 0; i < videoEl.textTracks.length; i++) {
3836
+ const track = videoEl.textTracks[i];
3837
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
3838
+ const scarlettTrack = {
3839
+ id: `track-${i}`,
3840
+ label: track.label || `Track ${i + 1}`,
3841
+ language: track.language || "",
3842
+ kind: track.kind,
3843
+ active: track.mode === "showing"
3844
+ };
3845
+ tracks.push(scarlettTrack);
3846
+ if (track.mode === "showing") {
3847
+ currentTrack = scarlettTrack;
3848
+ }
3849
+ }
3850
+ api?.setState("textTracks", tracks);
3851
+ api?.setState("currentTextTrack", currentTrack);
3852
+ };
3853
+ const selectTrack = (trackId) => {
3854
+ const videoEl = getVideo2();
3855
+ if (!videoEl) return;
3856
+ for (let i = 0; i < videoEl.textTracks.length; i++) {
3857
+ const track = videoEl.textTracks[i];
3858
+ if (track.kind !== "subtitles" && track.kind !== "captions") continue;
3859
+ const id = `track-${i}`;
3860
+ if (trackId && id === trackId) {
3861
+ track.mode = "showing";
3862
+ } else {
3863
+ track.mode = "disabled";
3864
+ }
3865
+ }
3866
+ syncTracksToState();
3867
+ };
3868
+ const extractHlsSubtitles = () => {
3869
+ if (!extractFromHLS || !api) return;
3870
+ const hlsPlugin = api.getPlugin("hls-provider");
3871
+ if (!hlsPlugin || hlsPlugin.isNativeHLS()) return;
3872
+ const hlsInstance = hlsPlugin.getHlsInstance();
3873
+ if (!hlsInstance?.subtitleTracks?.length) return;
3874
+ api.logger.debug("Extracting HLS subtitle tracks", {
3875
+ count: hlsInstance.subtitleTracks.length
3876
+ });
3877
+ for (const hlsTrack of hlsInstance.subtitleTracks) {
3878
+ addTrackElement({
3879
+ language: hlsTrack.lang || "unknown",
3880
+ label: hlsTrack.name || `Subtitle ${hlsTrack.id}`,
3881
+ src: hlsTrack.url,
3882
+ kind: "subtitles"
3883
+ });
3884
+ }
3885
+ syncTracksToState();
3886
+ if (autoSelect) {
3887
+ autoSelectTrack();
3888
+ }
3889
+ };
3890
+ const autoSelectTrack = () => {
3891
+ const tracks = api?.getState("textTracks") || [];
3892
+ const match = tracks.find((t) => t.language === defaultLanguage);
3893
+ if (match) {
3894
+ selectTrack(match.id);
3895
+ api?.logger.debug("Auto-selected caption track", { language: defaultLanguage, id: match.id });
3896
+ }
3897
+ };
3898
+ const initSources = () => {
3899
+ if (!config.sources?.length) return;
3900
+ for (const source of config.sources) {
3901
+ addTrackElement(source);
3902
+ }
3903
+ syncTracksToState();
3904
+ if (autoSelect) {
3905
+ autoSelectTrack();
3906
+ }
3907
+ };
3908
+ return {
3909
+ id: "captions",
3910
+ name: "Captions",
3911
+ version: "1.0.0",
3912
+ type: "feature",
3913
+ description: "WebVTT subtitles and closed captions with HLS extraction",
3914
+ init(pluginApi) {
3915
+ api = pluginApi;
3916
+ api.logger.debug("Captions plugin initialized");
3917
+ api.setState("textTracks", []);
3918
+ api.setState("currentTextTrack", null);
3919
+ const unsubTrackText = api.on("track:text", ({ trackId }) => {
3920
+ selectTrack(trackId);
3921
+ });
3922
+ const unsubLoaded = api.on("media:loaded", () => {
3923
+ video = null;
3924
+ cleanupTracks();
3925
+ initSources();
3926
+ if (extractFromHLS) {
3927
+ setTimeout(extractHlsSubtitles, 500);
3928
+ }
3929
+ });
3930
+ const unsubLoadRequest = api.on("media:load-request", () => {
3931
+ video = null;
3932
+ cleanupTracks();
3933
+ });
3934
+ api.onDestroy(() => {
3935
+ unsubTrackText();
3936
+ unsubLoaded();
3937
+ unsubLoadRequest();
3938
+ cleanupTracks();
3939
+ });
3940
+ },
3941
+ destroy() {
3942
+ api?.logger.debug("Captions plugin destroyed");
3943
+ cleanupTracks();
3944
+ video = null;
3945
+ api = null;
3946
+ }
3947
+ };
3948
+ }
3559
3949
  class Signal {
3560
3950
  constructor(initialValue) {
3561
3951
  this.subscribers = /* @__PURE__ */ new Set();
@@ -5095,6 +5485,13 @@ class ScarlettPlayer {
5095
5485
  await this.pluginManager.initPlugin(id);
5096
5486
  }
5097
5487
  }
5488
+ this.eventBus.on("media:load-request", async ({ src, autoplay }) => {
5489
+ if (this.stateManager.getValue("chromecastActive")) return;
5490
+ await this.load(src);
5491
+ if (autoplay !== false) {
5492
+ await this.play();
5493
+ }
5494
+ });
5098
5495
  if (this.initialSrc) {
5099
5496
  await this.load(this.initialSrc);
5100
5497
  }
@@ -5922,6 +6319,12 @@ async function createEmbedPlayer(container, config, pluginCreators2, availableTy
5922
6319
  artwork: config.artwork || config.poster || config.playlist?.[0]?.artwork
5923
6320
  }));
5924
6321
  }
6322
+ if (pluginCreators2.watermark && config.watermark) {
6323
+ plugins.push(pluginCreators2.watermark(config.watermark));
6324
+ }
6325
+ if (pluginCreators2.captions) {
6326
+ plugins.push(pluginCreators2.captions(config.captions || {}));
6327
+ }
5925
6328
  if (pluginCreators2.analytics && config.analytics?.beaconUrl) {
5926
6329
  plugins.push(pluginCreators2.analytics({
5927
6330
  beaconUrl: config.analytics.beaconUrl,
@@ -6039,11 +6442,13 @@ function setupAutoInit(pluginCreators2, availableTypes) {
6039
6442
  }
6040
6443
  }
6041
6444
  }
6042
- const VERSION = "0.3.0-video";
6445
+ const VERSION = "0.5.3-video";
6043
6446
  const AVAILABLE_TYPES = ["video"];
6044
6447
  const pluginCreators = {
6045
6448
  hls: createHLSPlugin,
6046
- videoUI: uiPlugin
6449
+ videoUI: uiPlugin,
6450
+ watermark: createWatermarkPlugin,
6451
+ captions: createCaptionsPlugin
6047
6452
  // Audio UI not available in this build
6048
6453
  // Analytics not available in this build
6049
6454
  // Playlist not available in this build