stormcloud-video-player 0.7.41 → 0.7.43

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/lib/index.cjs CHANGED
@@ -470,9 +470,6 @@ __export(index_exports, {
470
470
  createAdStormPlayer: function createAdStormPlayer1() {
471
471
  return createAdStormPlayer;
472
472
  },
473
- createDemoStockTickerOverlay: function createDemoStockTickerOverlay1() {
474
- return createDemoStockTickerOverlay;
475
- },
476
473
  createStormcloudPlayer: function createStormcloudPlayer1() {
477
474
  return createStormcloudPlayer;
478
475
  },
@@ -536,9 +533,6 @@ __export(index_exports, {
536
533
  resolveImageUrl: function resolveImageUrl1() {
537
534
  return resolveImageUrl;
538
535
  },
539
- scrollerLooksLikeMarketsStock: function scrollerLooksLikeMarketsStock1() {
540
- return scrollerLooksLikeMarketsStock;
541
- },
542
536
  sendHeartbeat: function sendHeartbeat1() {
543
537
  return sendHeartbeat;
544
538
  },
@@ -5898,28 +5892,16 @@ var SWIRL_HD_AUTHORING_WIDTH = 1920;
5898
5892
  var SWIRL_HD_AUTHORING_HEIGHT = 1080;
5899
5893
  var NAB_DEMO_NAME_PREFIX = "NAB Demo \u2014 ";
5900
5894
  function overlayAuthoringDimensions(overlay, decodeWidth, decodeHeight) {
5901
- if (!decodeWidth || !decodeHeight) {
5895
+ if (overlay.name.startsWith(NAB_DEMO_NAME_PREFIX)) {
5902
5896
  return {
5903
- width: decodeWidth || SWIRL_HD_AUTHORING_WIDTH,
5904
- height: decodeHeight || SWIRL_HD_AUTHORING_HEIGHT
5905
- };
5906
- }
5907
- if (!overlay.visible) {
5908
- return {
5909
- width: decodeWidth,
5910
- height: decodeHeight
5897
+ width: SWIRL_HD_AUTHORING_WIDTH,
5898
+ height: SWIRL_HD_AUTHORING_HEIGHT
5911
5899
  };
5912
5900
  }
5913
- var extR = overlay.x + overlay.width;
5914
- var extB = overlay.y + overlay.height;
5915
- var EPS = 2;
5916
- var exceedsDecode = extR > decodeWidth + EPS || extB > decodeHeight + EPS;
5917
- var isNabDemo = overlay.name.startsWith(NAB_DEMO_NAME_PREFIX);
5918
- var isSyntheticMarketsTicker = overlay.id === -9001 || overlay.name === "Demo stock ticker";
5919
- if (exceedsDecode && (isNabDemo || isSyntheticMarketsTicker)) {
5901
+ if (!decodeWidth || !decodeHeight) {
5920
5902
  return {
5921
- width: SWIRL_HD_AUTHORING_WIDTH,
5922
- height: SWIRL_HD_AUTHORING_HEIGHT
5903
+ width: decodeWidth || SWIRL_HD_AUTHORING_WIDTH,
5904
+ height: decodeHeight || SWIRL_HD_AUTHORING_HEIGHT
5923
5905
  };
5924
5906
  }
5925
5907
  return {
@@ -6014,63 +5996,6 @@ function inferSwirlOverlayCoordinateSpace(overlays, videoWidth, videoHeight) {
6014
5996
  height: videoHeight
6015
5997
  };
6016
5998
  }
6017
- function scrollerLooksLikeMarketsStock(o) {
6018
- var _ref, _ref1, _ref2, _ref3;
6019
- if (o.type !== "scroller") return false;
6020
- var cfg = o.scroller_config;
6021
- var blob = "".concat(o.name, " ").concat((_ref = cfg === null || cfg === void 0 ? void 0 : cfg.label) !== null && _ref !== void 0 ? _ref : "", " ").concat((_ref1 = cfg === null || cfg === void 0 ? void 0 : cfg.label_line2) !== null && _ref1 !== void 0 ? _ref1 : "", " ").concat((_ref2 = cfg === null || cfg === void 0 ? void 0 : cfg.custom_text) !== null && _ref2 !== void 0 ? _ref2 : "", " ").concat((_ref3 = cfg === null || cfg === void 0 ? void 0 : cfg.preset) !== null && _ref3 !== void 0 ? _ref3 : "");
6022
- return /\b(MARKETS?|NYSE|NASDAQ|DJIA|\bS&P\b|STOCK|AAPL|TSLA|NVDA|EQUITIES)\b/i.test(blob);
6023
- }
6024
- function createDemoStockTickerOverlay(projectId, opts) {
6025
- var cw = (opts === null || opts === void 0 ? void 0 : opts.coordinateWidth) && opts.coordinateWidth > 0 ? opts.coordinateWidth : SWIRL_HD_AUTHORING_WIDTH;
6026
- var ch = (opts === null || opts === void 0 ? void 0 : opts.coordinateHeight) && opts.coordinateHeight > 0 ? opts.coordinateHeight : SWIRL_HD_AUTHORING_HEIGHT;
6027
- var sx = cw / SWIRL_HD_AUTHORING_WIDTH;
6028
- var sy = ch / SWIRL_HD_AUTHORING_HEIGHT;
6029
- var x = 36 * sx;
6030
- var y = 1002 * sy;
6031
- var width = 1848 * sx;
6032
- var height = 66 * sy;
6033
- var fontSize = Math.max(8, Math.round(13 * sy));
6034
- var scrollSpeed = Math.max(8, Math.round(36 * sx));
6035
- var borderRadius = Math.max(1, Math.round(4 * sy));
6036
- return {
6037
- id: -9001,
6038
- project_id: projectId,
6039
- name: "Demo stock ticker",
6040
- type: "scroller",
6041
- visible: true,
6042
- x: x,
6043
- y: y,
6044
- width: width,
6045
- height: height,
6046
- opacity: 100,
6047
- start_time: "00:00:00.000",
6048
- duration: "24:00:00.000",
6049
- z_index: 120,
6050
- scroller_config: {
6051
- preset: "equities_strip",
6052
- use_custom_text: true,
6053
- custom_text: "AAPL +0.84% \u2022 MSFT +0.31% \u2022 GOOGL \u22120.22% \u2022 AMZN +0.47% \u2022 NVDA +1.12% \u2022 META +0.19% \u2022 BRK.B +0.11% \u2022 JPM +0.55% \u2022 V +0.28% \u2022 UNH \u22120.17% \u2022 DJIA +0.41% \u2022 S&P 500 +0.29% \u2022 Nasdaq Composite +0.36% \u2022 Russell 2000 +0.52% \u2022 WTI crude $78.40 +0.6% \u2022 Gold $2,348/oz \u22120.2% \u2022 10Y Treasury 4.28%",
6054
- direction: "left",
6055
- scroll_speed: scrollSpeed,
6056
- font_size: fontSize,
6057
- font_weight: "600",
6058
- text_color: "#e2e8f0",
6059
- background_color: "#0a0f18",
6060
- background_opacity: 92,
6061
- border_radius: borderRadius,
6062
- padding: Math.max(3, Math.round(6 * sy)),
6063
- label: "U.S. equities",
6064
- label_line2: "",
6065
- label_color: "#1e3a5f",
6066
- label_text_color: "#f8fafc",
6067
- accent_color: "#38bdf8",
6068
- show_accent_line: true,
6069
- separator_char: "\u2022",
6070
- item_spacing: Math.max(28, Math.round(48 * sx))
6071
- }
6072
- };
6073
- }
6074
5999
  function normalizeScrollerConfig(raw) {
6075
6000
  if (!raw || (typeof raw === "undefined" ? "undefined" : _type_of(raw)) !== "object") return void 0;
6076
6001
  var r = raw;
@@ -7570,11 +7495,19 @@ function hexToRgb(hex) {
7570
7495
  return "".concat(num >> 16 & 255, ",").concat(num >> 8 & 255, ",").concat(num & 255);
7571
7496
  }
7572
7497
  var FADE_DURATION_MS = 1e3;
7573
- var SHOWCASE_CYCLE_MS_DEFAULT = 6e4;
7574
- var SHOWCASE_POP_IN_MS = 420;
7575
- var SHOWCASE_STEADY_END_MS = 46e3;
7576
- var SHOWCASE_POP_OUT_MS = 450;
7577
- var SHOWCASE_POP_OUT_END_MS = SHOWCASE_STEADY_END_MS + SHOWCASE_POP_OUT_MS;
7498
+ var SHOWCASE_CYCLE_MS_DEFAULT = 36e3;
7499
+ var SHOWCASE_MIN_BEAT_MS = 6e3;
7500
+ var BEAT_POP_IN_MS = 520;
7501
+ var BEAT_POP_OUT_MS = 520;
7502
+ var SHOWCASE_PERSISTENT_TYPES = /* @__PURE__ */ new Set([
7503
+ "scroller",
7504
+ "breaking_news",
7505
+ "image",
7506
+ "text",
7507
+ "shape",
7508
+ "countdown",
7509
+ "qr_code"
7510
+ ]);
7578
7511
  function easeOutCubic(t) {
7579
7512
  var u = 1 - t;
7580
7513
  return 1 - u * u * u;
@@ -7587,38 +7520,108 @@ function easeOutBack(t) {
7587
7520
  var c3 = c1 + 1;
7588
7521
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
7589
7522
  }
7590
- function showcaseOpacity(phase) {
7591
- if (phase < SHOWCASE_POP_IN_MS) return easeOutCubic(phase / SHOWCASE_POP_IN_MS);
7592
- if (phase >= SHOWCASE_STEADY_END_MS && phase < SHOWCASE_POP_OUT_END_MS) {
7593
- return 1 - easeInCubic((phase - SHOWCASE_STEADY_END_MS) / SHOWCASE_POP_OUT_MS);
7523
+ function beatOpacity(phase, beatMs) {
7524
+ if (phase < BEAT_POP_IN_MS) return easeOutCubic(phase / BEAT_POP_IN_MS);
7525
+ var holdEnd = beatMs - BEAT_POP_OUT_MS;
7526
+ if (phase >= holdEnd) {
7527
+ return Math.max(0, 1 - easeInCubic((phase - holdEnd) / BEAT_POP_OUT_MS));
7594
7528
  }
7595
7529
  return 1;
7596
7530
  }
7597
- function showcaseScale(phase) {
7598
- if (phase < SHOWCASE_POP_IN_MS) {
7599
- return Math.min(1, 0.78 + 0.22 * easeOutBack(phase / SHOWCASE_POP_IN_MS));
7531
+ function beatScale(phase, beatMs) {
7532
+ if (phase < BEAT_POP_IN_MS) {
7533
+ return Math.min(1, 0.92 + 0.08 * easeOutBack(phase / BEAT_POP_IN_MS));
7600
7534
  }
7601
- if (phase >= SHOWCASE_STEADY_END_MS && phase < SHOWCASE_POP_OUT_END_MS) {
7602
- return 1 - 0.14 * easeInCubic((phase - SHOWCASE_STEADY_END_MS) / SHOWCASE_POP_OUT_MS);
7535
+ var holdEnd = beatMs - BEAT_POP_OUT_MS;
7536
+ if (phase >= holdEnd) {
7537
+ return 1 - 0.06 * easeInCubic((phase - holdEnd) / BEAT_POP_OUT_MS);
7603
7538
  }
7604
7539
  return 1;
7605
7540
  }
7606
- function useShowcasePhase(enabled, cycleMs) {
7607
- var _ref = _sliced_to_array((0, import_react.useState)(0), 2), phase = _ref[0], setPhase = _ref[1];
7541
+ function partitionShowcase(overlays) {
7542
+ var visible = overlays.filter(function(o) {
7543
+ return o.visible;
7544
+ });
7545
+ var persistent = [];
7546
+ var spotlight = [];
7547
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
7548
+ try {
7549
+ for(var _iterator = visible[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
7550
+ var o = _step.value;
7551
+ if (SHOWCASE_PERSISTENT_TYPES.has(o.type)) persistent.push(o);
7552
+ else spotlight.push(o);
7553
+ }
7554
+ } catch (err) {
7555
+ _didIteratorError = true;
7556
+ _iteratorError = err;
7557
+ } finally{
7558
+ try {
7559
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
7560
+ _iterator.return();
7561
+ }
7562
+ } finally{
7563
+ if (_didIteratorError) {
7564
+ throw _iteratorError;
7565
+ }
7566
+ }
7567
+ }
7568
+ spotlight.sort(function(a, b) {
7569
+ return a.z_index - b.z_index || a.id - b.id;
7570
+ });
7571
+ var beats = [];
7572
+ var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
7573
+ try {
7574
+ for(var _iterator1 = spotlight[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
7575
+ var o1 = _step1.value;
7576
+ beats.push([
7577
+ o1
7578
+ ]);
7579
+ }
7580
+ } catch (err) {
7581
+ _didIteratorError1 = true;
7582
+ _iteratorError1 = err;
7583
+ } finally{
7584
+ try {
7585
+ if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
7586
+ _iterator1.return();
7587
+ }
7588
+ } finally{
7589
+ if (_didIteratorError1) {
7590
+ throw _iteratorError1;
7591
+ }
7592
+ }
7593
+ }
7594
+ return {
7595
+ persistent: persistent,
7596
+ beats: beats
7597
+ };
7598
+ }
7599
+ function useShowcaseBeat(enabled, cycleMs, beats) {
7600
+ var safeBeats = Math.max(1, beats);
7601
+ var beatMs = Math.max(SHOWCASE_MIN_BEAT_MS, Math.floor(cycleMs / safeBeats));
7602
+ var totalMs = beatMs * safeBeats;
7603
+ var _ref = _sliced_to_array((0, import_react.useState)(function() {
7604
+ return Date.now();
7605
+ }), 2), now = _ref[0], setNow = _ref[1];
7608
7606
  (0, import_react.useEffect)(function() {
7609
7607
  if (!enabled) return;
7610
- setPhase(Date.now() % cycleMs);
7611
7608
  var id = window.setInterval(function() {
7612
- setPhase(Date.now() % cycleMs);
7613
- }, 48);
7609
+ return setNow(Date.now());
7610
+ }, 64);
7614
7611
  return function() {
7615
7612
  return clearInterval(id);
7616
7613
  };
7617
7614
  }, [
7618
- enabled,
7619
- cycleMs
7615
+ enabled
7620
7616
  ]);
7621
- return phase;
7617
+ var phase = now % totalMs;
7618
+ var beatIndex = Math.min(safeBeats - 1, Math.floor(phase / beatMs));
7619
+ var beatPhase = phase - beatIndex * beatMs;
7620
+ return {
7621
+ beatIndex: beatIndex,
7622
+ beatPhase: beatPhase,
7623
+ beatMs: beatMs
7624
+ };
7622
7625
  }
7623
7626
  var OverlayRenderer = function OverlayRenderer(param) {
7624
7627
  var overlays = param.overlays, currentTime = param.currentTime, videoRef = param.videoRef, coordinateSpace = param.coordinateSpace, _param_showcaseMode = param.showcaseMode, showcaseMode = _param_showcaseMode === void 0 ? false : _param_showcaseMode, _param_showcaseCycleMs = param.showcaseCycleMs, showcaseCycleMs = _param_showcaseCycleMs === void 0 ? SHOWCASE_CYCLE_MS_DEFAULT : _param_showcaseCycleMs;
@@ -7626,7 +7629,28 @@ var OverlayRenderer = function OverlayRenderer(param) {
7626
7629
  var rafRef = (0, import_react.useRef)(null);
7627
7630
  var _ref1 = _sliced_to_array((0, import_react.useState)(/* @__PURE__ */ new Map()), 2), fadeMap = _ref1[0], setFadeMap = _ref1[1];
7628
7631
  var removeTimers = (0, import_react.useRef)(/* @__PURE__ */ new Map());
7629
- var showcasePhase = useShowcasePhase(showcaseMode, showcaseCycleMs);
7632
+ var _ref2 = (0, import_react.useMemo)(function() {
7633
+ return partitionShowcase(overlays);
7634
+ }, [
7635
+ overlays
7636
+ ]), showcasePersistent = _ref2.persistent, showcaseBeats = _ref2.beats;
7637
+ var _useShowcaseBeat = useShowcaseBeat(showcaseMode, showcaseCycleMs, showcaseBeats.length), showcaseBeatIndex = _useShowcaseBeat.beatIndex, showcaseBeatPhase = _useShowcaseBeat.beatPhase, showcaseBeatMs = _useShowcaseBeat.beatMs;
7638
+ var showcasePersistentIds = (0, import_react.useMemo)(function() {
7639
+ return new Set(showcasePersistent.map(function(o) {
7640
+ return o.id;
7641
+ }));
7642
+ }, [
7643
+ showcasePersistent
7644
+ ]);
7645
+ var showcaseSpotlightIds = (0, import_react.useMemo)(function() {
7646
+ var beat = showcaseBeats[showcaseBeatIndex];
7647
+ return new Set((beat !== null && beat !== void 0 ? beat : []).map(function(o) {
7648
+ return o.id;
7649
+ }));
7650
+ }, [
7651
+ showcaseBeats,
7652
+ showcaseBeatIndex
7653
+ ]);
7630
7654
  var updateDims = (0, import_react.useCallback)(function() {
7631
7655
  var video = videoRef.current;
7632
7656
  if (video) {
@@ -7659,10 +7683,9 @@ var OverlayRenderer = function OverlayRenderer(param) {
7659
7683
  ]);
7660
7684
  var activeOverlays = (0, import_react.useMemo)(function() {
7661
7685
  if (showcaseMode) {
7662
- return overlays.filter(function(o) {
7663
- if (!o.visible) return false;
7664
- return showcasePhase < SHOWCASE_POP_OUT_END_MS;
7665
- });
7686
+ var _showcaseBeats_showcaseBeatIndex;
7687
+ var beat = (_showcaseBeats_showcaseBeatIndex = showcaseBeats[showcaseBeatIndex]) !== null && _showcaseBeats_showcaseBeatIndex !== void 0 ? _showcaseBeats_showcaseBeatIndex : [];
7688
+ return _to_consumable_array(showcasePersistent).concat(_to_consumable_array(beat));
7666
7689
  }
7667
7690
  return overlays.filter(function(o) {
7668
7691
  return isOverlayActive(o, currentTime);
@@ -7671,14 +7694,9 @@ var OverlayRenderer = function OverlayRenderer(param) {
7671
7694
  overlays,
7672
7695
  currentTime,
7673
7696
  showcaseMode,
7674
- showcasePhase
7675
- ]);
7676
- var activeBeatIds = (0, import_react.useMemo)(function() {
7677
- return new Set(activeOverlays.map(function(o) {
7678
- return o.id;
7679
- }));
7680
- }, [
7681
- activeOverlays
7697
+ showcasePersistent,
7698
+ showcaseBeats,
7699
+ showcaseBeatIndex
7682
7700
  ]);
7683
7701
  (0, import_react.useLayoutEffect)(function() {
7684
7702
  var activeIds = new Set(activeOverlays.map(function(o) {
@@ -7873,10 +7891,12 @@ var OverlayRenderer = function OverlayRenderer(param) {
7873
7891
  var width = overlay.width * scaleX;
7874
7892
  var height = overlay.height * scaleY;
7875
7893
  var baseOpacity = Math.max(0, Math.min(100, overlay.opacity)) / 100;
7876
- var inShowcaseBeat = showcaseMode && activeBeatIds.has(overlay.id);
7877
- var useShowcasePop = inShowcaseBeat;
7878
- var opacity = useShowcasePop ? baseOpacity * showcaseOpacity(showcasePhase) : visible ? baseOpacity : 0;
7879
- var popScale = useShowcasePop ? showcaseScale(showcasePhase) : 1;
7894
+ var isShowcasePersistent = showcaseMode && showcasePersistentIds.has(overlay.id);
7895
+ var isShowcaseSpotlight = showcaseMode && showcaseSpotlightIds.has(overlay.id);
7896
+ var isShowcaseDormant = showcaseMode && !isShowcasePersistent && !isShowcaseSpotlight;
7897
+ var useShowcasePop = isShowcaseSpotlight;
7898
+ var opacity = useShowcasePop ? baseOpacity * beatOpacity(showcaseBeatPhase, showcaseBeatMs) : isShowcaseDormant ? 0 : visible ? baseOpacity : 0;
7899
+ var popScale = useShowcasePop ? beatScale(showcaseBeatPhase, showcaseBeatMs) : 1;
7880
7900
  var sz = {
7881
7901
  w: width,
7882
7902
  h: height
@@ -8091,27 +8111,7 @@ var StormcloudVideoPlayerComponent = import_react2.default.memo(function(props)
8091
8111
  swirlOverlayApiBaseUrl
8092
8112
  ]);
8093
8113
  var _import_react2_default_useState17 = _sliced_to_array(import_react2.default.useState(null), 2), overlayCoordSpace = _import_react2_default_useState17[0], setOverlayCoordSpace = _import_react2_default_useState17[1];
8094
- var displayOverlays = import_react2.default.useMemo(function() {
8095
- if (!swirlProjectId || !swirlShowcaseDemo) return overlays;
8096
- if (overlays.some(scrollerLooksLikeMarketsStock)) return overlays;
8097
- if (overlays.some(function(o) {
8098
- return o.visible && o.type === "scroller";
8099
- })) return overlays;
8100
- var cw = overlayCoordSpace === null || overlayCoordSpace === void 0 ? void 0 : overlayCoordSpace.width;
8101
- var ch = overlayCoordSpace === null || overlayCoordSpace === void 0 ? void 0 : overlayCoordSpace.height;
8102
- var tickerOpts = cw && cw > 0 && ch && ch > 0 ? {
8103
- coordinateWidth: cw,
8104
- coordinateHeight: ch
8105
- } : {};
8106
- return _to_consumable_array(overlays).concat([
8107
- createDemoStockTickerOverlay(swirlProjectId, tickerOpts)
8108
- ]);
8109
- }, [
8110
- overlays,
8111
- swirlProjectId,
8112
- swirlShowcaseDemo,
8113
- overlayCoordSpace
8114
- ]);
8114
+ var displayOverlays = overlays;
8115
8115
  var _import_react2_default_useState18 = _sliced_to_array(import_react2.default.useState(typeof window !== "undefined" ? window.innerWidth : 1920), 2), viewportWidth = _import_react2_default_useState18[0], setViewportWidth = _import_react2_default_useState18[1];
8116
8116
  var _import_react2_default_useState19 = _sliced_to_array(import_react2.default.useState(typeof window !== "undefined" ? window.innerHeight > window.innerWidth : false), 2), isPortrait = _import_react2_default_useState19[0], setIsPortrait = _import_react2_default_useState19[1];
8117
8117
  var _import_react2_default_useState20 = _sliced_to_array(import_react2.default.useState(DEFAULT_PLAYER_ASPECT_RATIO), 2), playerAspectRatio = _import_react2_default_useState20[0], setPlayerAspectRatio = _import_react2_default_useState20[1];
@@ -11506,7 +11506,6 @@ var StormcloudPlayer_default = StormcloudPlayer;
11506
11506
  StormcloudVideoPlayerComponent: StormcloudVideoPlayerComponent,
11507
11507
  canPlay: canPlay,
11508
11508
  createAdStormPlayer: createAdStormPlayer,
11509
- createDemoStockTickerOverlay: createDemoStockTickerOverlay,
11510
11509
  createStormcloudPlayer: createStormcloudPlayer,
11511
11510
  detectBrowser: detectBrowser,
11512
11511
  fetchProjectOverlays: fetchProjectOverlays,
@@ -11527,7 +11526,6 @@ var StormcloudPlayer_default = StormcloudPlayer;
11527
11526
  players: players,
11528
11527
  randomString: randomString,
11529
11528
  resolveImageUrl: resolveImageUrl,
11530
- scrollerLooksLikeMarketsStock: scrollerLooksLikeMarketsStock,
11531
11529
  sendHeartbeat: sendHeartbeat,
11532
11530
  sendInitialTracking: sendInitialTracking,
11533
11531
  supportsFeature: supportsFeature,