Skip to content

Commit d073a8b

Browse files
Merge pull request #54 from newrelic/develop
Release: Sync master with develop
2 parents 3f71a07 + 153d37e commit d073a8b

File tree

7 files changed

+640
-3
lines changed

7 files changed

+640
-3
lines changed

NRExoPlayerTracker/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
implementation project(path: ':NewRelicVideoCore')
3939
implementation 'androidx.media3:media3-exoplayer:1.1.0'
4040
testImplementation 'junit:junit:4.13.2'
41+
testImplementation 'org.robolectric:robolectric:4.10.3'
4142
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
4243
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
4344
androidTestImplementation 'androidx.test:runner:1.5.2'

NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.newrelic.videoagent.exoplayer.tracker;
22

33
import android.net.Uri;
4+
import android.os.Handler;
5+
import android.os.Looper;
46

57
import androidx.annotation.NonNull;
68
import androidx.media3.common.C;
@@ -20,9 +22,14 @@
2022
import com.newrelic.videoagent.exoplayer.BuildConfig;
2123

2224
import java.io.IOException;
25+
import java.util.Collections;
2326
import java.util.HashMap;
2427
import java.util.List;
2528
import java.util.Map;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
import java.util.concurrent.atomic.AtomicLong;
32+
import java.util.concurrent.atomic.AtomicBoolean;
2633
import java.util.regex.Matcher;
2734
import java.util.regex.Pattern;
2835

@@ -42,6 +49,19 @@ public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listene
4249
protected int lastWindow;
4350
protected String renditionChangeShift;
4451
protected long actualBitrate;
52+
private static final long DEFAULT_AGGREGATION_WINDOW_MS = 5000; // 5 seconds
53+
private static final int MAX_EVENTS_PER_AGGREGATE = 50;
54+
private volatile boolean droppedFrameAggregationEnabled = true;
55+
56+
private final ConcurrentHashMap<String, Object> lastTrackData = new ConcurrentHashMap<>();
57+
private final AtomicInteger totalLostFrames = new AtomicInteger(0);
58+
private final AtomicInteger totalLostFramesDuration = new AtomicInteger(0);
59+
private final AtomicInteger eventCount = new AtomicInteger(0);
60+
private final AtomicLong firstDropTimestamp = new AtomicLong(0);
61+
private final AtomicLong lastDropTimestamp = new AtomicLong(0);
62+
private final AtomicBoolean hasActiveAggregation = new AtomicBoolean(false);
63+
private volatile Handler aggregationHandler;
64+
private volatile Runnable flushPendingEvent;
4565

4666
/**
4767
* Init a new ExoPlayer tracker.
@@ -375,6 +395,8 @@ public void setPlaylist(List<Uri> playlist) {
375395

376396
@Override
377397
public void sendEnd() {
398+
// Flush any pending dropped frame events before ending
399+
flushPendingDroppedFrameEvent();
378400
super.sendEnd();
379401
resetState();
380402
}
@@ -386,17 +408,152 @@ public void sendEnd() {
386408
* @param elapsed Time elapsed.
387409
*/
388410
public void sendDroppedFrame(int count, int elapsed) {
411+
if (!droppedFrameAggregationEnabled) {
412+
// Fallback to original behavior
413+
sendDroppedFrameImmediate(count, elapsed);
414+
return;
415+
}
416+
long currentTime = System.currentTimeMillis();
417+
boolean shouldFlush = hasActiveAggregation.get() &&
418+
(isAggregationExpired(currentTime) || isMaxEventsReached());
419+
420+
if (shouldFlush) {
421+
flushCurrentAggregation();
422+
startNewAggregation(count, elapsed, currentTime);
423+
} else if (hasActiveAggregation.get()) {
424+
addToCurrentAggregation(count, elapsed, currentTime);
425+
} else {
426+
startNewAggregation(count, elapsed, currentTime);
427+
}
428+
updateLastFrameDropSnapshot(count, elapsed, currentTime);
429+
scheduleDelayedFlush();
430+
}
431+
private void startNewAggregation(int count, int elapsed, long timestamp) {
432+
totalLostFrames.set(count);
433+
totalLostFramesDuration.set(elapsed);
434+
eventCount.set(1);
435+
firstDropTimestamp.set(timestamp);
436+
lastDropTimestamp.set(timestamp);
437+
hasActiveAggregation.set(true);
438+
}
439+
private void addToCurrentAggregation(int count, int elapsed, long timestamp) {
440+
totalLostFrames.addAndGet(count);
441+
totalLostFramesDuration.addAndGet(elapsed);
442+
eventCount.incrementAndGet();
443+
lastDropTimestamp.set(timestamp);
444+
}
445+
private void updateLastFrameDropSnapshot(int count, int elapsed, long timestamp) {
446+
lastTrackData.put("lastFrameDropCount", count);
447+
lastTrackData.put("lastFrameDropDuration", elapsed);
448+
lastTrackData.put("lastFrameDropTime", timestamp);
449+
lastTrackData.put("lastUpdateTime", System.currentTimeMillis());
450+
451+
lastTrackData.put("currentTotalFrames", totalLostFrames.get());
452+
lastTrackData.put("currentTotalDuration", totalLostFramesDuration.get());
453+
lastTrackData.put("currentEventCount", eventCount.get());
454+
}
455+
private boolean isAggregationExpired(long currentTime) {
456+
long firstTime = firstDropTimestamp.get();
457+
return firstTime > 0 && (currentTime - firstTime) > DEFAULT_AGGREGATION_WINDOW_MS;
458+
}
459+
private boolean isMaxEventsReached() {
460+
return eventCount.get() >= MAX_EVENTS_PER_AGGREGATE;
461+
}
462+
463+
protected void scheduleDelayedFlush() {
464+
if (aggregationHandler == null) {
465+
aggregationHandler = new Handler(Looper.getMainLooper());
466+
}
467+
468+
if (flushPendingEvent != null) {
469+
aggregationHandler.removeCallbacks(flushPendingEvent);
470+
}
471+
472+
flushPendingEvent = this::flushPendingDroppedFrameEvent;
473+
aggregationHandler.postDelayed(flushPendingEvent, DEFAULT_AGGREGATION_WINDOW_MS);
474+
}
475+
protected void flushCurrentAggregation() {
476+
if (!hasActiveAggregation.get()) {
477+
return;
478+
}
479+
480+
int lostFrames = totalLostFrames.get();
481+
int lostDuration = totalLostFramesDuration.get();
482+
int count = eventCount.get();
483+
long firstTime = firstDropTimestamp.get();
484+
long lastTime = lastDropTimestamp.get();
485+
486+
Map<String, Object> eventAttributes = new HashMap<>();
487+
eventAttributes.put("lostFrames", lostFrames);
488+
eventAttributes.put("lostFramesDuration", lostDuration);
489+
eventAttributes.put("eventCount", count);
490+
eventAttributes.put("aggregationWindowMs", DEFAULT_AGGREGATION_WINDOW_MS);
491+
eventAttributes.put("firstDropTimestamp", firstTime);
492+
eventAttributes.put("lastDropTimestamp", lastTime);
493+
eventAttributes.put("actualAggregationDurationMs", lastTime - firstTime);
494+
495+
if (getState().isAd) {
496+
sendVideoAdEvent("AD_DROPPED_FRAMES", eventAttributes);
497+
} else {
498+
sendVideoEvent("CONTENT_DROPPED_FRAMES", eventAttributes);
499+
}
500+
resetAggregationState();
501+
}
502+
private void flushPendingDroppedFrameEvent() {
503+
flushCurrentAggregation();
504+
}
505+
protected void resetAggregationState() {
506+
hasActiveAggregation.set(false);
507+
totalLostFrames.set(0);
508+
totalLostFramesDuration.set(0);
509+
eventCount.set(0);
510+
firstDropTimestamp.set(0);
511+
lastDropTimestamp.set(0);
512+
}
513+
514+
private void sendDroppedFrameImmediate(int count, int elapsed) {
515+
// Original implementation for backward compatibility
389516
Map<String, Object> attr = new HashMap<>();
390517
attr.put("lostFrames", count);
391518
attr.put("lostFramesDuration", elapsed);
392-
// generatePlayElapsedTime();
519+
393520
if (getState().isAd) {
394521
sendVideoAdEvent("AD_DROPPED_FRAMES", attr);
395-
}
396-
else {
522+
} else {
397523
sendVideoEvent("CONTENT_DROPPED_FRAMES", attr);
398524
}
399525
}
526+
public void setDroppedFrameAggregationEnabled(boolean enabled) {
527+
if (!enabled && droppedFrameAggregationEnabled) {
528+
// Flush any pending events before disabling
529+
flushCurrentAggregation();
530+
}
531+
this.droppedFrameAggregationEnabled = enabled;
532+
}
533+
public ConcurrentHashMap<String, Object> getLastTrackData() {
534+
return lastTrackData;
535+
}
536+
public Map<String, Object> getCurrentAggregationStatus() {
537+
boolean hasAggregation = hasActiveAggregation.get();
538+
int lostFrames = totalLostFrames.get();
539+
int lostDuration = totalLostFramesDuration.get();
540+
int count = eventCount.get();
541+
long firstTime = firstDropTimestamp.get();
542+
long lastTime = lastDropTimestamp.get();
543+
544+
Map<String, Object> status = new HashMap<>();
545+
status.put("hasActiveAggregation", hasAggregation);
546+
status.put("totalLostFrames", lostFrames);
547+
status.put("totalLostFramesDuration", lostDuration);
548+
status.put("eventCount", count);
549+
status.put("firstDropTimestamp", firstTime);
550+
status.put("lastDropTimestamp", lastTime);
551+
552+
return Collections.unmodifiableMap(status);
553+
}
554+
public boolean isDroppedFrameAggregationEnabled() {
555+
return droppedFrameAggregationEnabled;
556+
}
400557

401558
// ExoPlayer Player.EventListener
402559

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Robolectric configuration for NRExoPlayerTracker tests
2+
sdk=28

0 commit comments

Comments
 (0)