11package com .newrelic .videoagent .exoplayer .tracker ;
22
33import android .net .Uri ;
4+ import android .os .Handler ;
5+ import android .os .Looper ;
46
57import androidx .annotation .NonNull ;
68import androidx .media3 .common .C ;
2022import com .newrelic .videoagent .exoplayer .BuildConfig ;
2123
2224import java .io .IOException ;
25+ import java .util .Collections ;
2326import java .util .HashMap ;
2427import java .util .List ;
2528import 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 ;
2633import java .util .regex .Matcher ;
2734import 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
0 commit comments