@newrelic/video-core 4.1.8-beta → 4.1.8

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.
@@ -89,29 +89,55 @@ class VideoTrackerState {
89
89
  this.peakBitrate = 0;
90
90
 
91
91
  /**
92
- * Last tracked bitrate
92
+ * Time-weighted bitrate calculation accumulators
93
93
  */
94
+ this.partialAverageBitrate = 0;
95
+ this._totalBitrateDuration = 0;
96
+ this.weightedBitrate = 0;
94
97
  this._lastBitrate = null;
98
+ this._lastBitrateChangeTimestamp = null;
95
99
 
96
100
  /**
97
- * Tracks the last updated timestamp for bitrate
98
- * */
99
- this._lastBitrateChangeTimestamp = null;
101
+ * Array of changed network download bitrate values (only values that differ from previous).
102
+ * Used for calculating simple mean, min, max (not time-weighted).
103
+ */
104
+ this._downloadBitrates = [];
100
105
 
101
106
  /**
102
- * total bitrate partial value for average weighted average bitrate
107
+ * Previous download bitrate value to detect changes.
103
108
  */
104
- this.partialAverageBitrate = 0;
109
+ this._lastDownloadBitrate = null;
105
110
 
106
111
  /**
107
- * Had Startup Failure: TRUE if CONTENT_ERROR occurs before CONTENT_START.
112
+ * QOE v1.1: Total number of rendition/quality switches where bitrate increased.
108
113
  */
109
- this.hadStartupFailure = false;
114
+ this.totalSwitchUps = 0;
110
115
 
111
116
  /**
112
- * Had Playback Failure: TRUE if CONTENT_ERROR occurs during content playback.
117
+ * QOE v1.1: Total number of rendition/quality switches where bitrate decreased.
113
118
  */
114
- this.hadPlaybackFailure = false;
119
+ this.totalSwitchDowns = 0;
120
+
121
+ /**
122
+ * QOE v1.1: Total time in milliseconds spent in paused state (intentional pauses only).
123
+ * Calculated from timeSincePaused chrono accumulation.
124
+ */
125
+ this.totalPauseTime = 0;
126
+
127
+ /**
128
+ * QOE v1.1: Set of distinct renditions played, keyed by "heightxwidth" (e.g., "1080x1920").
129
+ */
130
+ this._playedRenditions = new Set();
131
+
132
+ /**
133
+ * Had Startup Error: TRUE if CONTENT_ERROR occurs before CONTENT_START.
134
+ */
135
+ this.hadStartupError = false;
136
+
137
+ /**
138
+ * Had Playback Error: TRUE if CONTENT_ERROR occurs during content playback.
139
+ */
140
+ this.hadPlaybackError = false;
115
141
 
116
142
  /**
117
143
  * The amount of ms the user has been rebuffering during content playback.
@@ -347,23 +373,45 @@ class VideoTrackerState {
347
373
  const kpi = {};
348
374
 
349
375
  try {
350
- // QoE KPIs - Content only
376
+ // QoE v1 KPIs
351
377
  if (this.startupTime !== null) {
352
378
  kpi["startupTime"] = this.startupTime;
353
379
  }
354
380
  if (this.peakBitrate > 0) {
355
381
  kpi["peakBitrate"] = this.peakBitrate;
356
382
  }
357
- kpi["hadStartupFailure"] = this.hadStartupFailure;
358
- kpi["hadPlaybackFailure"] = this.hadPlaybackFailure;
383
+ kpi["hadStartupError"] = this.hadStartupError;
384
+ kpi["hadPlaybackError"] = this.hadPlaybackError;
359
385
  kpi["totalRebufferingTime"] = this.totalRebufferingTime;
360
- // Calculate rebuffering ratio as percentage (avoid division by zero)
361
386
  kpi["rebufferingRatio"] = this.totalPlaytime > 0
362
387
  ? (this.totalRebufferingTime / this.totalPlaytime) * 100
363
388
  : 0;
364
389
  kpi["totalPlaytime"] = this.totalPlaytime;
365
390
  kpi["averageBitrate"] = this.weightedBitrate;
366
391
  kpi["numberOfErrors"] = this.numberOfErrors;
392
+
393
+ // QoE v1.1 KPIs - Download rate (simple mean of changed values)
394
+ if (this._downloadBitrates.length > 0) {
395
+ const sum = this._downloadBitrates.reduce((a, b) => a + b, 0);
396
+ kpi["avgDownloadRate"] = Math.round(sum / this._downloadBitrates.length);
397
+ kpi["minDownloadRate"] = Math.min(...this._downloadBitrates);
398
+ kpi["maxDownloadRate"] = Math.max(...this._downloadBitrates);
399
+ }
400
+
401
+ // QoE v1.1 KPIs - Rendition switching
402
+ kpi["totalSwitchUps"] = this.totalSwitchUps;
403
+ kpi["totalSwitchDowns"] = this.totalSwitchDowns;
404
+
405
+ // QoE v1.1 KPIs - Pause and session timing
406
+ kpi["totalPauseTime"] = this.timeSincePaused.getDuration();
407
+ kpi["totalViewSessionTime"] = this.totalPlaytime;
408
+
409
+ // QoE v1.1 KPIs - Renditions
410
+ kpi["totalRenditions"] = this._playedRenditions.size;
411
+
412
+ // Version tag
413
+ kpi["qoeAggregateVersion"] = "1.1.0";
414
+
367
415
  } catch (error) {
368
416
  Log.error("Failed to add attributes for QOE KPIs", error.message);
369
417
  }
@@ -549,8 +597,10 @@ class VideoTrackerState {
549
597
  }
550
598
 
551
599
  // Accumulate total rebuffering time for content only
600
+ // Use exact stopped duration (stopTime - startTime) instead of live getDeltaTime()
552
601
  if (!this.isAd() && this.initialBufferingHappened) {
553
- this.totalRebufferingTime += this.timeSinceBufferBegin.getDeltaTime();
602
+ this.totalRebufferingTime +=
603
+ (this.timeSinceBufferBegin.stopTime - this.timeSinceBufferBegin.startTime);
554
604
  }
555
605
 
556
606
  return true;
@@ -670,13 +720,13 @@ class VideoTrackerState {
670
720
  } else {
671
721
  this.timeSinceLastError.start();
672
722
 
673
- // Track failure flags for content errors only
674
- // Had Startup Failure: error before content started
723
+ // Track error flags for content errors only
724
+ // Had Startup Error: error before content started
675
725
  if (!this.isStarted) {
676
- this.hadStartupFailure = true;
726
+ this.hadStartupError = true;
677
727
  } else {
678
- // Had Playback Failure: any content error
679
- this.hadPlaybackFailure = true;
728
+ // Had Playback Error: any content error during playback
729
+ this.hadPlaybackError = true;
680
730
  }
681
731
  }
682
732
  }
@@ -689,38 +739,113 @@ class VideoTrackerState {
689
739
  }
690
740
 
691
741
  /**
692
- * Updates peak bitrate with current bitrate value (content only).
742
+ * Updates peak bitrate and time-weighted average bitrate (content only).
693
743
  * @param {number} bitrate Current content bitrate in bps.
694
744
  */
695
745
  trackContentBitrateState(bitrate) {
696
746
  if (bitrate && typeof bitrate === "number") {
697
747
  this.peakBitrate = Math.max(this.peakBitrate, bitrate);
698
748
 
699
- if(this._lastBitrate === null || this._lastBitrate !== bitrate) {
700
- const deltaPlaytime = this._lastBitrateChangeTimestamp === null ? this.totalPlaytime : Date.now() - this._lastBitrateChangeTimestamp;
701
- const currentWeightedBitrate = (bitrate * deltaPlaytime);
702
- this.partialAverageBitrate += currentWeightedBitrate;
703
- this.weightedBitrate = currentWeightedBitrate / deltaPlaytime;
749
+ const now = Date.now();
750
+
751
+ // If not playing (buffering, paused, etc.), close the current segment
752
+ // so non-play time is excluded from the weighted average
753
+ if (!this.isPlaying) {
754
+ if (this._lastBitrate !== null && this._lastBitrateChangeTimestamp !== null) {
755
+ const segmentDuration = now - this._lastBitrateChangeTimestamp;
756
+ if (segmentDuration > 0) {
757
+ this.partialAverageBitrate += this._lastBitrate * segmentDuration;
758
+ this._totalBitrateDuration += segmentDuration;
759
+ }
760
+ this._lastBitrateChangeTimestamp = null; // Mark as paused
761
+ }
704
762
  this._lastBitrate = bitrate;
705
- this._lastBitrateChangeTimestamp = Date.now();
763
+ return;
764
+ }
765
+
766
+ // Playing: restart timestamp if returning from non-play state
767
+ if (this._lastBitrateChangeTimestamp === null && this._lastBitrate !== null) {
768
+ this._lastBitrateChangeTimestamp = now;
769
+ }
770
+
771
+ // Close the PREVIOUS segment when bitrate changes
772
+ if (this._lastBitrate !== null && this._lastBitrate !== bitrate
773
+ && this._lastBitrateChangeTimestamp !== null) {
774
+ const segmentDuration = now - this._lastBitrateChangeTimestamp;
775
+ if (segmentDuration > 0) {
776
+ this.partialAverageBitrate += this._lastBitrate * segmentDuration;
777
+ this._totalBitrateDuration += segmentDuration;
778
+ }
779
+ this._lastBitrateChangeTimestamp = now;
706
780
  }
781
+
782
+ // Initialize timestamp on first observation
783
+ if (this._lastBitrateChangeTimestamp === null) {
784
+ this._lastBitrateChangeTimestamp = now;
785
+ }
786
+
787
+ this._lastBitrate = bitrate;
788
+
789
+ // Compute weighted average including current in-progress segment
790
+ let totalWeighted = this.partialAverageBitrate;
791
+ let totalDuration = this._totalBitrateDuration;
792
+ const currentSegment = now - this._lastBitrateChangeTimestamp;
793
+ if (currentSegment > 0) {
794
+ totalWeighted += bitrate * currentSegment;
795
+ totalDuration += currentSegment;
796
+ }
797
+ this.weightedBitrate = totalDuration > 0
798
+ ? Math.round(totalWeighted / totalDuration)
799
+ : bitrate;
707
800
  }
708
801
  }
709
802
 
803
+ /**
804
+ * Records network download bitrate observation. Only tracks values that differ from previous.
805
+ * @param {number} bps Network throughput in bits per second.
806
+ */
807
+ trackDownloadRate(bps) {
808
+ if (!bps || typeof bps !== "number" || bps <= 0) return;
809
+ if (bps !== this._lastDownloadBitrate) {
810
+ this._downloadBitrates.push(bps);
811
+ this._lastDownloadBitrate = bps;
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Records a rendition play event (height x width combination).
817
+ * @param {number} height Rendition height in pixels.
818
+ * @param {number} width Rendition width in pixels.
819
+ */
820
+ addPlayedRendition(height, width) {
821
+ if (height && width) {
822
+ this._playedRenditions.add(`${height}x${width}`);
823
+ }
824
+ }
825
+
826
+
710
827
  /**
711
828
  * Resets tracked variable for view id change
712
829
  * */
713
830
  resetViewIdTrackedState() {
714
831
  this.peakBitrate = 0;
715
- this.partialAverageBitrate = 0;
716
832
  this.startupTime = null;
833
+ this.partialAverageBitrate = 0;
834
+ this._totalBitrateDuration = 0;
835
+ this.weightedBitrate = 0;
717
836
  this._lastBitrate = null;
718
837
  this._lastBitrateChangeTimestamp = null;
838
+ this._downloadBitrates = [];
839
+ this._lastDownloadBitrate = null;
840
+ this.totalSwitchUps = 0;
841
+ this.totalSwitchDowns = 0;
842
+ this.timeSincePaused.reset();
843
+ this._playedRenditions = new Set();
719
844
  }
720
845
 
721
846
  /** Methods to manage total ads time chrono */
722
847
  clearTotalAdsTime() {
723
- console.log("clear total ads time", this.totalAdTime);
848
+ Log.debug("clear total ads time", this.totalAdTime);
724
849
  this._totalAdPlaytime.reset();
725
850
  }
726
851
 
@@ -729,12 +854,12 @@ class VideoTrackerState {
729
854
  }
730
855
 
731
856
  startAdsTime() {
732
- console.log("startAdsTime");
857
+ Log.debug("startAdsTime");
733
858
  return this._totalAdPlaytime.start();
734
859
  }
735
860
 
736
861
  stopAdsTime() {
737
- console.log("stopAdsTime");
862
+ Log.debug("stopAdsTime");
738
863
  return this._totalAdPlaytime.stop();
739
864
  }
740
865