stormcloud-video-player 0.8.47 → 0.8.49

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.
@@ -0,0 +1,133 @@
1
+ interface AdBreak {
2
+ id?: string;
3
+ startTimeMs: number;
4
+ durationMs?: number;
5
+ vastTagUrl?: string;
6
+ }
7
+ interface StormcloudVideoPlayerConfig {
8
+ videoElement: HTMLVideoElement;
9
+ src: string;
10
+ autoplay?: boolean;
11
+ muted?: boolean;
12
+ allowNativeHls?: boolean;
13
+ lowLatencyMode?: boolean;
14
+ isLiveStream?: boolean;
15
+ pauseContentDuringAds?: boolean;
16
+ driftToleranceMs?: number;
17
+ immediateManifestAds?: boolean;
18
+ debugAdTiming?: boolean;
19
+ adFailsafeTimeoutMs?: number;
20
+ adBreakCheckIntervalMs?: number;
21
+ maxAdBreakExtensionMs?: number;
22
+ minRemainingMsToStartAd?: number;
23
+ showCustomControls?: boolean;
24
+ hideLoadingIndicator?: boolean;
25
+ onVolumeToggle?: () => void;
26
+ onFullscreenToggle?: () => void;
27
+ onControlClick?: () => void;
28
+ licenseKey?: string;
29
+ vastTagUrl?: string;
30
+ isVmap?: boolean;
31
+ vmapUrl?: string;
32
+ vastMode?: 'adstorm' | 'default';
33
+ minSegmentsBeforePlay?: number;
34
+ ctvAdRequest?: boolean;
35
+ adTest?: boolean;
36
+ podMaxAds?: number;
37
+ podMaxDurationMs?: number;
38
+ podExactDuration?: boolean;
39
+ }
40
+ interface Scte35Marker {
41
+ type: "start" | "end" | "progress";
42
+ ptsSeconds?: number;
43
+ durationSeconds?: number;
44
+ raw?: unknown;
45
+ }
46
+ interface Id3TagInfo {
47
+ key: string;
48
+ value: string | Uint8Array;
49
+ ptsSeconds?: number;
50
+ }
51
+ interface AdController {
52
+ initialize: () => void;
53
+ requestAds: (vastTagUrl: string) => Promise<void>;
54
+ play: () => Promise<void>;
55
+ pause: () => void;
56
+ resume: () => void;
57
+ stop: () => Promise<void>;
58
+ destroy: () => void;
59
+ isAdPlaying: () => boolean;
60
+ resize: (width: number, height: number) => void;
61
+ on: (event: string, listener: (payload?: any) => void) => void;
62
+ off: (event: string, listener: (payload?: any) => void) => void;
63
+ updateOriginalMutedState: (muted: boolean, volume?: number) => void;
64
+ getOriginalMutedState: () => boolean;
65
+ getOriginalVolume: () => number;
66
+ setAdVolume: (volume: number) => void;
67
+ getAdVolume: () => number;
68
+ showPlaceholder: () => void;
69
+ hidePlaceholder: () => void;
70
+ getLoadedAdDuration?: () => number | undefined;
71
+ getLoadedAdId?: () => string | undefined;
72
+ getPodAdCount?: () => number;
73
+ getPodTotalDurationMs?: () => number;
74
+ }
75
+ interface ClientInfo {
76
+ brand: string;
77
+ os: string;
78
+ model: string;
79
+ deviceType: "tv" | "mobile" | "tablet" | "desktop";
80
+ isSmartTV: boolean;
81
+ isAndroid: boolean;
82
+ isWebView: boolean;
83
+ isWebApp: boolean;
84
+ domain: string;
85
+ origin: string;
86
+ path: string;
87
+ userAgent: string;
88
+ vendor: string;
89
+ platform: string;
90
+ screen: {
91
+ width?: number;
92
+ height?: number;
93
+ availWidth?: number;
94
+ availHeight?: number;
95
+ orientation?: string;
96
+ pixelDepth?: number;
97
+ };
98
+ hardwareConcurrency: number;
99
+ deviceMemory: number | null;
100
+ maxTouchPoints: number;
101
+ language: string;
102
+ languages: string;
103
+ cookieEnabled: boolean;
104
+ doNotTrack: string;
105
+ referrer: string;
106
+ visibilityState: string;
107
+ }
108
+ interface PlayerAnalyticsContext {
109
+ inputStreamType?: string;
110
+ debugAdTiming?: boolean;
111
+ }
112
+ interface AdDetectInfo {
113
+ source: "scte35";
114
+ durationSeconds?: number;
115
+ ptsSeconds?: number;
116
+ detectedAtFragmentSn?: number;
117
+ timestamp: string;
118
+ }
119
+ interface AdLoadedInfo {
120
+ source: "vast" | "hls";
121
+ vastUrl?: string;
122
+ durationSeconds?: number;
123
+ timestamp: string;
124
+ }
125
+ interface AdImpressionInfo {
126
+ source: "vast" | "hls";
127
+ adIndex: number;
128
+ adUrl?: string;
129
+ durationSeconds?: number;
130
+ timestamp: string;
131
+ }
132
+
133
+ export type { AdController as A, ClientInfo as C, Id3TagInfo as I, PlayerAnalyticsContext as P, StormcloudVideoPlayerConfig as S, Scte35Marker as a, AdBreak as b, AdDetectInfo as c, AdImpressionInfo as d, AdLoadedInfo as e };
@@ -411,9 +411,11 @@ function createHlsAdPlayer(contentVideo, options) {
411
411
  var pendingTimeouts = [];
412
412
  var STALL_TIMEOUT_MS = 4e3;
413
413
  var STALL_CHECK_INTERVAL_MS = 1e3;
414
+ var BLACKFRAME_MIN_PLAYBACK_S = 1.5;
414
415
  var stallWatchdogId;
415
416
  var lastAdProgressTime = 0;
416
417
  var lastAdProgressPosition = 0;
418
+ var sawDecodedVideoFrame = false;
417
419
  var trackingFired = {
418
420
  impression: false,
419
421
  start: false,
@@ -555,6 +557,61 @@ function createHlsAdPlayer(contentVideo, options) {
555
557
  var url = mediaFile.url.toLowerCase();
556
558
  return type.startsWith("video/") || url.endsWith(".mp4") || url.includes(".mp4?");
557
559
  }
560
+ function isAudioMediaFile(mediaFile) {
561
+ var type = mediaFile.type.toLowerCase();
562
+ var url = (mediaFile.url.toLowerCase().split("?")[0] || "").trim();
563
+ return type.startsWith("audio/") || url.endsWith(".mp3") || url.endsWith(".aac") || url.endsWith(".m4a") || url.endsWith(".ogg") || url.endsWith(".oga") || url.endsWith(".opus") || url.endsWith(".wav") || url.endsWith(".weba");
564
+ }
565
+ function collectVideoMediaFiles(scope, verbose) {
566
+ var mediaFileElements = scope.querySelectorAll("MediaFile");
567
+ var mediaFiles = [];
568
+ if (verbose) {
569
+ console.log("[HlsAdPlayer] Found ".concat(mediaFileElements.length, " MediaFile element(s) in VAST XML"));
570
+ }
571
+ mediaFileElements.forEach(function(mf, index) {
572
+ var _mf_textContent;
573
+ var type = mf.getAttribute("type") || "";
574
+ var url = ((_mf_textContent = mf.textContent) === null || _mf_textContent === void 0 ? void 0 : _mf_textContent.trim()) || "";
575
+ var width = mf.getAttribute("width") || "";
576
+ var height = mf.getAttribute("height") || "";
577
+ if (verbose) {
578
+ console.log("[HlsAdPlayer] MediaFile ".concat(index, ': type="').concat(type, '", url="').concat(url, '", width="').concat(width, '", height="').concat(height, '"'));
579
+ }
580
+ if (!url) {
581
+ if (verbose) {
582
+ console.warn("[HlsAdPlayer] MediaFile ".concat(index, " has empty URL"));
583
+ }
584
+ return;
585
+ }
586
+ var bitrateAttr = mf.getAttribute("bitrate");
587
+ var bitrateValue = bitrateAttr ? parseInt(bitrateAttr, 10) : void 0;
588
+ var mediaFile = {
589
+ url: url,
590
+ type: type,
591
+ width: parseInt(width || "1920", 10),
592
+ height: parseInt(height || "1080", 10),
593
+ bitrate: bitrateValue && bitrateValue > 0 ? bitrateValue : void 0
594
+ };
595
+ if (isAudioMediaFile(mediaFile)) {
596
+ console.warn("[HlsAdPlayer] MediaFile ".concat(index, ' skipped (audio-only, type="').concat(type, '") — audio ads are not played to avoid a black screen'));
597
+ return;
598
+ }
599
+ var hasExplicitDimensions = mf.getAttribute("width") != null && mf.getAttribute("height") != null;
600
+ if (!isHlsMediaFile(mediaFile) && hasExplicitDimensions && (mediaFile.width <= 1 || mediaFile.height <= 1)) {
601
+ console.warn("[HlsAdPlayer] MediaFile ".concat(index, " skipped (degenerate dimensions ").concat(mediaFile.width, "x").concat(mediaFile.height, ', type="').concat(type, '") — likely an audio-only placeholder rendition'));
602
+ return;
603
+ }
604
+ if (isHlsMediaFile(mediaFile) || isProgressiveMediaFile(mediaFile)) {
605
+ mediaFiles.push(mediaFile);
606
+ if (verbose) {
607
+ console.log("[HlsAdPlayer] Added ".concat(isHlsMediaFile(mediaFile) ? "HLS" : "progressive", " MediaFile: ").concat(url));
608
+ }
609
+ } else if (verbose) {
610
+ console.log("[HlsAdPlayer] MediaFile ".concat(index, ' ignored (type="').concat(type, '" is not supported)'));
611
+ }
612
+ });
613
+ return mediaFiles;
614
+ }
558
615
  function createEmptyTrackingUrls() {
559
616
  return {
560
617
  impression: [],
@@ -642,37 +699,7 @@ function createHlsAdPlayer(contentVideo, options) {
642
699
  var durationText = ((_xmlDoc_querySelector3 = xmlDoc.querySelector("Duration")) === null || _xmlDoc_querySelector3 === void 0 ? void 0 : _xmlDoc_querySelector3.textContent) || "00:00:30";
643
700
  var durationParts = durationText.split(":");
644
701
  var duration = parseInt(durationParts[0] || "0", 10) * 3600 + parseInt(durationParts[1] || "0", 10) * 60 + parseInt(durationParts[2] || "0", 10);
645
- var mediaFileElements = xmlDoc.querySelectorAll("MediaFile");
646
- var mediaFiles = [];
647
- console.log("[HlsAdPlayer] Found ".concat(mediaFileElements.length, " MediaFile element(s) in VAST XML"));
648
- mediaFileElements.forEach(function(mf, index) {
649
- var _mf_textContent;
650
- var type = mf.getAttribute("type") || "";
651
- var url = ((_mf_textContent = mf.textContent) === null || _mf_textContent === void 0 ? void 0 : _mf_textContent.trim()) || "";
652
- var width = mf.getAttribute("width") || "";
653
- var height = mf.getAttribute("height") || "";
654
- console.log("[HlsAdPlayer] MediaFile ".concat(index, ': type="').concat(type, '", url="').concat(url, '", width="').concat(width, '", height="').concat(height, '"'));
655
- var mediaFile = {
656
- url: url,
657
- type: type,
658
- width: parseInt(width || "1920", 10),
659
- height: parseInt(height || "1080", 10),
660
- bitrate: void 0
661
- };
662
- if (!url) {
663
- console.warn("[HlsAdPlayer] MediaFile ".concat(index, " has empty URL"));
664
- return;
665
- }
666
- var bitrateAttr = mf.getAttribute("bitrate");
667
- var bitrateValue = bitrateAttr ? parseInt(bitrateAttr, 10) : void 0;
668
- mediaFile.bitrate = bitrateValue && bitrateValue > 0 ? bitrateValue : void 0;
669
- if (isHlsMediaFile(mediaFile) || isProgressiveMediaFile(mediaFile)) {
670
- mediaFiles.push(mediaFile);
671
- console.log("[HlsAdPlayer] Added ".concat(isHlsMediaFile(mediaFile) ? "HLS" : "progressive", " MediaFile: ").concat(url));
672
- } else {
673
- console.log("[HlsAdPlayer] MediaFile ".concat(index, ' ignored (type="').concat(type, '" is not supported)'));
674
- }
675
- });
702
+ var mediaFiles = collectVideoMediaFiles(xmlDoc, true);
676
703
  if (mediaFiles.length === 0) {
677
704
  if (isNoAdAvailable) {
678
705
  console.warn("[HlsAdPlayer] No ads available (VAST response indicates no ads)");
@@ -720,28 +747,7 @@ function createHlsAdPlayer(contentVideo, options) {
720
747
  var durationText = ((_adElement_querySelector3 = adElement.querySelector("Duration")) === null || _adElement_querySelector3 === void 0 ? void 0 : _adElement_querySelector3.textContent) || "00:00:30";
721
748
  var durationParts = durationText.split(":");
722
749
  var duration = parseInt(durationParts[0] || "0", 10) * 3600 + parseInt(durationParts[1] || "0", 10) * 60 + parseInt(durationParts[2] || "0", 10);
723
- var mediaFileElements = adElement.querySelectorAll("MediaFile");
724
- var mediaFiles = [];
725
- mediaFileElements.forEach(function(mf) {
726
- var _mf_textContent;
727
- var type = mf.getAttribute("type") || "";
728
- var url = ((_mf_textContent = mf.textContent) === null || _mf_textContent === void 0 ? void 0 : _mf_textContent.trim()) || "";
729
- var width = mf.getAttribute("width") || "";
730
- var height = mf.getAttribute("height") || "";
731
- if (!url) return;
732
- var bitrateAttr = mf.getAttribute("bitrate");
733
- var bitrateValue = bitrateAttr ? parseInt(bitrateAttr, 10) : void 0;
734
- var mediaFile = {
735
- url: url,
736
- type: type,
737
- width: parseInt(width || "1920", 10),
738
- height: parseInt(height || "1080", 10),
739
- bitrate: bitrateValue && bitrateValue > 0 ? bitrateValue : void 0
740
- };
741
- if (isHlsMediaFile(mediaFile) || isProgressiveMediaFile(mediaFile)) {
742
- mediaFiles.push(mediaFile);
743
- }
744
- });
750
+ var mediaFiles = collectVideoMediaFiles(adElement, false);
745
751
  if (mediaFiles.length === 0) {
746
752
  if (isNoAdAvailable) {
747
753
  console.warn("[HlsAdPlayer] Pod <Ad> indicates no ad available");
@@ -1062,6 +1068,14 @@ function createHlsAdPlayer(contentVideo, options) {
1062
1068
  fireTrackingPixels(currentAd.trackingUrls.thirdQuartile);
1063
1069
  }
1064
1070
  };
1071
+ var onLoadedMetadata = function onLoadedMetadata() {
1072
+ if (!adPlaying || !adVideoElement) return;
1073
+ var videoWidth = adVideoElement.videoWidth, videoHeight = adVideoElement.videoHeight;
1074
+ if (videoWidth <= 1 || videoHeight <= 1) {
1075
+ console.warn("[HlsAdPlayer] Ad creative has no usable video dimensions (".concat(videoWidth, "x").concat(videoHeight, ") - skipping this creative"));
1076
+ handleVideoDecodeFailure();
1077
+ }
1078
+ };
1065
1079
  var onPlaying = function onPlaying() {
1066
1080
  startStallWatchdog();
1067
1081
  if (!currentAd || trackingFired.start) return;
@@ -1113,6 +1127,10 @@ function createHlsAdPlayer(contentVideo, options) {
1113
1127
  type: "timeupdate",
1114
1128
  handler: onTimeUpdate
1115
1129
  },
1130
+ {
1131
+ type: "loadedmetadata",
1132
+ handler: onLoadedMetadata
1133
+ },
1116
1134
  {
1117
1135
  type: "playing",
1118
1136
  handler: onPlaying
@@ -1302,13 +1320,51 @@ function createHlsAdPlayer(contentVideo, options) {
1302
1320
  }
1303
1321
  handleAdError();
1304
1322
  }
1323
+ function getDecodedVideoFrameCount(video) {
1324
+ try {
1325
+ if (typeof video.getVideoPlaybackQuality === "function") {
1326
+ var quality = video.getVideoPlaybackQuality();
1327
+ if (quality && typeof quality.totalVideoFrames === "number") {
1328
+ return quality.totalVideoFrames;
1329
+ }
1330
+ }
1331
+ var legacyCount = video.webkitDecodedFrameCount;
1332
+ if (typeof legacyCount === "number") {
1333
+ return legacyCount;
1334
+ }
1335
+ } catch (error) {
1336
+ console.warn("[HlsAdPlayer] Unable to read decoded video frame count:", error);
1337
+ }
1338
+ return void 0;
1339
+ }
1340
+ function handleVideoDecodeFailure() {
1341
+ console.warn("[HlsAdPlayer] Ad audio is advancing but no video frames decoded (black screen) - skipping this creative");
1342
+ clearStallWatchdog();
1343
+ if (currentAd) {
1344
+ fireTrackingPixels(currentAd.trackingUrls.error);
1345
+ }
1346
+ if (podIndex < podAds.length - 1 && advanceToNextPodAd()) {
1347
+ return;
1348
+ }
1349
+ handleAdError();
1350
+ }
1305
1351
  function startStallWatchdog() {
1306
1352
  clearStallWatchdog();
1307
1353
  noteAdProgress();
1354
+ sawDecodedVideoFrame = false;
1308
1355
  stallWatchdogId = window.setInterval(function() {
1309
1356
  if (!adPlaying || !adVideoElement) {
1310
1357
  return;
1311
1358
  }
1359
+ if (!sawDecodedVideoFrame && !adVideoElement.paused) {
1360
+ var decodedFrames = getDecodedVideoFrameCount(adVideoElement);
1361
+ if (decodedFrames !== void 0 && decodedFrames > 0) {
1362
+ sawDecodedVideoFrame = true;
1363
+ } else if (decodedFrames === 0 && adVideoElement.currentTime >= BLACKFRAME_MIN_PLAYBACK_S) {
1364
+ handleVideoDecodeFailure();
1365
+ return;
1366
+ }
1367
+ }
1312
1368
  if (adVideoElement.currentTime > lastAdProgressPosition + 0.05) {
1313
1369
  noteAdProgress();
1314
1370
  return;