Skip to content

Commit e6b4a5b

Browse files
committed
NR-480384: feat: fixed QOE_AGGREGATE event push
1 parent 1179552 commit e6b4a5b

File tree

2 files changed

+107
-21
lines changed

2 files changed

+107
-21
lines changed

NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideo.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ public static boolean isInitialized() {
4242
return instance != null;
4343
}
4444

45+
/**
46+
* Get configured harvest cycle in seconds
47+
* @return harvest cycle seconds, or default 60 if not initialized
48+
*/
49+
public static int getHarvestCycleSeconds() {
50+
if (instance != null && instance.harvestManager != null) {
51+
return instance.harvestManager.getFactory().getConfiguration().getHarvestCycleSeconds();
52+
}
53+
return 60; // Default harvest cycle
54+
}
55+
4556
public static Integer addPlayer(NRVideoPlayerConfiguration config) {
4657
if (!isInitialized()) {
4758
NRLog.w("NRVideo not initialized - cannot add player");

NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import android.os.Handler;
44

5+
import com.newrelic.videoagent.core.NRVideo;
56
import com.newrelic.videoagent.core.model.NRTimeSince;
67
import com.newrelic.videoagent.core.model.NRTrackerState;
78
import 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

Comments
 (0)