22
33import android .os .Handler ;
44
5+ import com .newrelic .videoagent .core .NRVideo ;
56import com .newrelic .videoagent .core .model .NRTimeSince ;
67import com .newrelic .videoagent .core .model .NRTrackerState ;
78import com .newrelic .videoagent .core .utils .NRLog ;
@@ -60,6 +61,11 @@ public class NRVideoTracker extends NRTracker {
6061 private Long qoeTotalBitrateWeightedTime ;
6162 private Long qoeTotalActiveTime ;
6263
64+ // QOE_AGGREGATE harvest cycle tracking fields
65+ private boolean hasVideoActionInCurrentCycle = false ;
66+ private boolean qoeAggregateAlreadySent = false ;
67+ private Long lastHarvestCycleTimestamp = null ;
68+
6369 /**
6470 * Create a new NRVideoTracker.
6571 */
@@ -284,8 +290,8 @@ public void sendRequest() {
284290 sendVideoAdEvent (AD_REQUEST );
285291 } else {
286292 sendVideoEvent (CONTENT_REQUEST );
287- // Send single and latest QOE with this VideoAction event
288- sendQoeAggregate ();
293+ // Mark video action for QOE_AGGREGATE once per harvest cycle
294+ markVideoActionInCycle ();
289295 }
290296 }
291297 }
@@ -316,8 +322,8 @@ public void sendStart() {
316322 hasContentStarted = true ;
317323
318324 sendVideoEvent (CONTENT_START );
319- // Send single and latest QOE with this VideoAction event
320- sendQoeAggregate ();
325+ // Mark video action for QOE_AGGREGATE once per harvest cycle
326+ markVideoActionInCycle ();
321327 }
322328 playtimeSinceLastEventTimestamp = System .currentTimeMillis ();
323329 }
@@ -338,7 +344,7 @@ public void sendPause() {
338344 } else {
339345 sendVideoEvent (CONTENT_PAUSE );
340346 // Send single and latest QOE with this VideoAction event
341- sendQoeAggregate ();
347+ markVideoActionInCycle ();
342348 }
343349 playtimeSinceLastEventTimestamp = 0L ;
344350 }
@@ -374,7 +380,7 @@ public void sendResume() {
374380 } else {
375381 sendVideoEvent (CONTENT_RESUME );
376382 // Send single and latest QOE with this VideoAction event
377- sendQoeAggregate ();
383+ markVideoActionInCycle ();
378384 }
379385 if (!state .isBuffering && !state .isSeeking ) {
380386 playtimeSinceLastEventTimestamp = System .currentTimeMillis ();
@@ -397,7 +403,7 @@ public void sendEnd() {
397403 } else {
398404 sendVideoEvent (CONTENT_END );
399405 // Send single and latest QOE with this VideoAction event
400- sendQoeAggregate ();
406+ markVideoActionInCycle ();
401407 }
402408
403409 stopHeartbeat ();
@@ -422,7 +428,7 @@ public void sendSeekStart() {
422428 } else {
423429 sendVideoEvent (CONTENT_SEEK_START );
424430 // Send single and latest QOE with this VideoAction event
425- sendQoeAggregate ();
431+ markVideoActionInCycle ();
426432 }
427433 playtimeSinceLastEventTimestamp = 0L ;
428434 }
@@ -438,7 +444,7 @@ public void sendSeekEnd() {
438444 } else {
439445 sendVideoEvent (CONTENT_SEEK_END );
440446 // Send single and latest QOE with this VideoAction event
441- sendQoeAggregate ();
447+ markVideoActionInCycle ();
442448 }
443449 if (!state .isBuffering && !state .isPaused ) {
444450 playtimeSinceLastEventTimestamp = System .currentTimeMillis ();
@@ -460,7 +466,7 @@ public void sendBufferStart() {
460466 } else {
461467 sendVideoEvent (CONTENT_BUFFER_START );
462468 // Send single and latest QOE with this VideoAction event
463- sendQoeAggregate ();
469+ markVideoActionInCycle ();
464470 }
465471 playtimeSinceLastEventTimestamp = 0L ;
466472 }
@@ -482,7 +488,7 @@ public void sendBufferEnd() {
482488 } else {
483489 sendVideoEvent (CONTENT_BUFFER_END );
484490 // Send single and latest QOE with this VideoAction event
485- sendQoeAggregate ();
491+ markVideoActionInCycle ();
486492 }
487493 if (!state .isSeeking && !state .isPaused ) {
488494 playtimeSinceLastEventTimestamp = System .currentTimeMillis ();
@@ -508,7 +514,7 @@ public void sendHeartbeat() {
508514 } else {
509515 sendVideoEvent (CONTENT_HEARTBEAT , eventData );
510516 // Send single and latest QOE with this VideoAction event
511- sendQoeAggregate ();
517+ markVideoActionInCycle ();
512518 }
513519 }
514520 state .chrono .start ();
@@ -535,10 +541,72 @@ public void sendRenditionChange() {
535541 * Note: QoE metrics are currently limited to content-related events only, not ad events.
536542 * This design choice focuses QoE measurement on the primary content viewing experience.
537543 */
538- public void sendQoeAggregate () {
544+ /**
545+ * Mark that a video action occurred in the current harvest cycle.
546+ * This will trigger QOE_AGGREGATE to be sent once per cycle.
547+ */
548+ public void markVideoActionInCycle () {
549+ if (!state .isAd ) { // Only for content, not ads
550+ checkAndSendQoeAggregateIfNeeded (); // Check for new cycle first (may reset flags)
551+ hasVideoActionInCurrentCycle = true ; // Now mark action in current cycle
552+ // Check again now that we've marked the action
553+ if (hasVideoActionInCurrentCycle && !qoeAggregateAlreadySent ) {
554+ sendQoeAggregate ();
555+ }
556+ }
557+ }
558+
559+ /**
560+ * Check if we need to send QOE_AGGREGATE for the current harvest cycle.
561+ * Only sends once per harvest cycle and only if there was a video action.
562+ */
563+ private void checkAndSendQoeAggregateIfNeeded () {
564+ long currentTime = System .currentTimeMillis ();
565+ // Use user-configured harvest cycle
566+ long harvestCycleMs = NRVideo .getHarvestCycleSeconds () * 1000L ;
567+
568+ NRLog .d ("Checking QOE_AGGREGATE cycle - currentTime: " + currentTime +
569+ ", lastHarvestCycleTimestamp: " + lastHarvestCycleTimestamp +
570+ ", harvestCycleMs: " + harvestCycleMs );
571+
572+ // Check if we're in a new harvest cycle
573+ if (lastHarvestCycleTimestamp == null ||
574+ (currentTime - lastHarvestCycleTimestamp ) >= harvestCycleMs ) {
575+
576+ // New harvest cycle - reset flags
577+ if (lastHarvestCycleTimestamp != null ) {
578+ NRLog .d ("New harvest cycle started - resetting QOE_AGGREGATE flags (cycle: " +
579+ (harvestCycleMs / 1000 ) + "s)" );
580+ }
581+ resetHarvestCycleFlags ();
582+ lastHarvestCycleTimestamp = currentTime ;
583+ }
584+
585+ NRLog .d ("QOE_AGGREGATE flags - hasVideoActionInCurrentCycle: " + hasVideoActionInCurrentCycle +
586+ ", qoeAggregateAlreadySent: " + qoeAggregateAlreadySent );
587+
588+ // Send QOE_AGGREGATE if we haven't sent it yet and we have video actions
589+ if (hasVideoActionInCurrentCycle && !qoeAggregateAlreadySent ) {
590+ sendQoeAggregate ();
591+ }
592+ }
593+
594+ /**
595+ * Reset harvest cycle tracking flags for new cycle
596+ */
597+ private void resetHarvestCycleFlags () {
598+ hasVideoActionInCurrentCycle = false ;
599+ qoeAggregateAlreadySent = false ;
600+ }
601+
602+ /**
603+ * Send QOE_AGGREGATE event (internal method - called once per cycle)
604+ */
605+ private void sendQoeAggregate () {
539606 if (!state .isAd ) { // Only send for content, not ads
540607 Map <String , Object > kpiAttributes = calculateQOEKpiAttributes ();
541608 sendVideoEvent (QOE_AGGREGATE , kpiAttributes );
609+ qoeAggregateAlreadySent = true ;
542610 }
543611 }
544612
@@ -566,19 +634,22 @@ private Map<String, Object> calculateQOEKpiAttributes() {
566634 }
567635 kpiAttributes .put ("totalRebufferingTime" , qoeTotalRebufferingTime );
568636
569- // rebufferingRatio - Rebuffering time as a percentage of total playtime
570- if (totalPlaytime != null && totalPlaytime > 0 ) {
571- double rebufferingRatio = ((double ) qoeTotalRebufferingTime / totalPlaytime ) * 100 ;
637+ // Use elapsedTime (accumulatedVideoWatchTime) instead of totalPlaytime for QOE
638+ Long elapsedTime = state .accumulatedVideoWatchTime ;
639+ if (elapsedTime == null ) {
640+ elapsedTime = 0L ;
641+ }
642+
643+ // rebufferingRatio - Rebuffering time as a percentage of elapsed watch time
644+ if (elapsedTime > 0 ) {
645+ double rebufferingRatio = ((double ) qoeTotalRebufferingTime / elapsedTime ) * 100 ;
572646 kpiAttributes .put ("rebufferingRatio" , rebufferingRatio );
573647 } else {
574648 kpiAttributes .put ("rebufferingRatio" , 0.0 );
575649 }
576650
577- // totalPlaytime - Total milliseconds user spent watching content
578- if (totalPlaytime == null ) {
579- totalPlaytime = 0L ;
580- }
581- kpiAttributes .put ("totalPlaytime" , totalPlaytime );
651+ // totalPlaytime - Use elapsedTime (accumulated video watch time) instead of totalPlaytime
652+ kpiAttributes .put ("totalPlaytime" , elapsedTime );
582653
583654 // averageBitrate - Time-weighted average bitrate across all content playback
584655 Long timeWeightedAverage = calculateTimeWeightedAverageBitrate ();
@@ -800,6 +871,10 @@ private void resetQoeMetrics() {
800871 qoeLastRenditionChangeTime = null ;
801872 qoeTotalBitrateWeightedTime = 0L ;
802873 qoeTotalActiveTime = 0L ;
874+
875+ // Reset QOE_AGGREGATE harvest cycle tracking fields
876+ resetHarvestCycleFlags ();
877+ lastHarvestCycleTimestamp = null ;
803878 }
804879
805880
0 commit comments