Skip to content

Commit afecd44

Browse files
Merge pull request #47 from jpradhan-nr/issue/standalone_agent
Android video agent with self harvesting
2 parents c018ba0 + 927a93d commit afecd44

40 files changed

+4400
-232
lines changed

.idea/gradle.xml

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NRExoPlayerTracker/build.gradle

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
minSdkVersion 16
99
targetSdkVersion 33
1010
versionCode 6
11-
versionName "3.0.3"
11+
versionName "4.0.0"
1212
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1313
consumerProguardFiles "consumer-rules.pro"
1414
}
@@ -37,9 +37,11 @@ dependencies {
3737
implementation 'com.google.android.material:material:1.2.1'
3838
implementation project(path: ':NewRelicVideoCore')
3939
implementation 'androidx.media3:media3-exoplayer:1.1.0'
40-
testImplementation 'junit:junit:4.+'
41-
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
42-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
40+
testImplementation 'junit:junit:4.13.2'
41+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
42+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
43+
androidTestImplementation 'androidx.test:runner:1.5.2'
44+
androidTestImplementation 'androidx.test:rules:1.5.0'
4345
}
4446
afterEvaluate {
4547
publishing {
@@ -53,4 +55,4 @@ afterEvaluate {
5355
}
5456
}
5557

56-
}
58+
}

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

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

33
import android.net.Uri;
44

5+
import androidx.annotation.NonNull;
6+
import androidx.media3.common.C;
57
import androidx.media3.common.MediaLibraryInfo;
68
import androidx.media3.common.MediaMetadata;
79
import androidx.media3.common.PlaybackException;
@@ -16,15 +18,15 @@
1618
import com.newrelic.videoagent.core.tracker.NRVideoTracker;
1719
import com.newrelic.videoagent.core.utils.NRLog;
1820
import com.newrelic.videoagent.exoplayer.BuildConfig;
19-
import static com.newrelic.videoagent.core.NRDef.*;
2021

2122
import java.io.IOException;
2223
import java.util.HashMap;
2324
import java.util.List;
2425
import java.util.Map;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
2528

2629
import static com.newrelic.videoagent.core.NRDef.*;
27-
import androidx.media3.common.C;
2830

2931
/**
3032
* New Relic Video tracker for ExoPlayer.
@@ -73,7 +75,7 @@ public void setPlayer(Object player) {
7375
*
7476
* @param action Action being generated.
7577
* @param attributes Specific attributes sent along the action.
76-
* @return
78+
* @return Map of attributes with action-specific data.
7779
*/
7880
@Override
7981
public Map<String, Object> getAttributes(String action, Map<String, Object> attributes) {
@@ -192,7 +194,7 @@ public Long getRenditionHeight() {
192194
public Long getDuration() {
193195
if (player == null) return null;
194196

195-
return player.getDuration();
197+
return Math.max(player.getDuration() , 0);
196198
}
197199

198200
/**
@@ -212,19 +214,21 @@ public Long getPlayhead() {
212214
* @return Attribute.
213215
*/
214216
public String getSrc() {
215-
if (player == null) return null;
216-
217+
// Prefer direct MediaItem URI if available
218+
if (player == null || player.getCurrentMediaItem() == null) return null;
219+
if (player.getCurrentMediaItem().localConfiguration != null) {
220+
return player.getCurrentMediaItem().localConfiguration.uri.toString();
221+
}
222+
// Fallback to playlist if available
217223
if (getPlaylist() != null) {
218-
NRLog.d("Current window index = " + player.getCurrentMediaItemIndex());
219224
try {
220225
Uri src = getPlaylist().get(player.getCurrentMediaItemIndex());
221226
return src.toString();
222-
}catch(Exception e) {
227+
} catch(Exception e) {
223228
return null;
224229
}
225-
} else {
226-
return null;
227230
}
231+
return null;
228232
}
229233

230234
/**
@@ -273,15 +277,31 @@ public Boolean getIsMuted() {
273277
* @return String of the current title
274278
*/
275279
public String getTitle() {
276-
String contentTitle = "Unknown";
277-
if (player != null && player.getCurrentMediaItem() != null && player.getCurrentMediaItem().mediaMetadata.title != null) {
280+
// Try to get title from MediaItem metadata
281+
if (player != null && player.getCurrentMediaItem() != null) {
278282
MediaMetadata mm = player.getCurrentMediaItem().mediaMetadata;
279-
contentTitle = mm.title.toString();
280-
if (mm.subtitle != null) {
281-
contentTitle += ": " + mm.subtitle; // Usually the episode title is available in subtitle
283+
if (mm != null && mm.title != null) {
284+
String contentTitle = mm.title.toString();
285+
if (mm.subtitle != null) {
286+
contentTitle += ": " + mm.subtitle;
287+
}
288+
return contentTitle;
289+
}
290+
// Fallback: use URI if title is not set
291+
if (player.getCurrentMediaItem().localConfiguration != null) {
292+
return player.getCurrentMediaItem().localConfiguration.uri.getLastPathSegment();
282293
}
283294
}
284-
return contentTitle;
295+
// Fallback: use playlist URI if available
296+
if (getPlaylist() != null && player != null) {
297+
try {
298+
Uri src = getPlaylist().get(player.getCurrentMediaItemIndex());
299+
return src.getLastPathSegment();
300+
} catch(Exception e) {
301+
// ignore
302+
}
303+
}
304+
return "Unknown";
285305
}
286306

287307
/**
@@ -458,15 +478,15 @@ private void logOnPlayerStateChanged(boolean playWhenReady, int playbackState) {
458478
}
459479

460480
@Override
461-
public void onPlayerError(PlaybackException error) {
481+
public void onPlayerError(@NonNull PlaybackException error) {
462482
NRLog.d("onPlayerError");
463483
sendError(error);
464484
}
465485

466486
// ExoPlayer AnalyticsListener
467487

468488
@Override
469-
public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.PositionInfo newPosition, int reason) {
489+
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, int reason) {
470490
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
471491
NRLog.d("onSeekStarted analytics");
472492

@@ -477,7 +497,7 @@ public void onPositionDiscontinuity(Player.PositionInfo oldPosition, Player.Posi
477497
}
478498

479499
@Override
480-
public void onTracksChanged(EventTime eventTime, Tracks tracksInfo) {
500+
public void onTracksChanged(@NonNull EventTime eventTime, @NonNull Tracks tracksInfo) {
481501
NRLog.d("onTracksChanged analytics");
482502

483503
// Next track in the playlist
@@ -489,13 +509,13 @@ public void onTracksChanged(EventTime eventTime, Tracks tracksInfo) {
489509
}
490510

491511
@Override
492-
public void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {
512+
public void onLoadError(@NonNull EventTime eventTime, @NonNull LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData, @NonNull IOException error, boolean wasCanceled) {
493513
NRLog.d("onLoadError analytics");
494514
sendError(error);
495515
}
496516

497517
@Override
498-
public void onLoadCompleted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
518+
public void onLoadCompleted(@NonNull EventTime eventTime, @NonNull LoadEventInfo loadEventInfo, @NonNull MediaLoadData mediaLoadData) {
499519
if (mediaLoadData.dataType == C.DATA_TYPE_MEDIA
500520
&& mediaLoadData.trackType == C.TRACK_TYPE_VIDEO
501521
&& loadEventInfo.loadDurationMs > 0) {
@@ -505,14 +525,14 @@ public void onLoadCompleted(EventTime eventTime, LoadEventInfo loadEventInfo, Me
505525
}
506526

507527
@Override
508-
public void onBandwidthEstimate(AnalyticsListener.EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
528+
public void onBandwidthEstimate(@NonNull AnalyticsListener.EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
509529
NRLog.d("onBandwidthEstimate analytics");
510530

511531
this.bitrateEstimate = bitrateEstimate;
512532
}
513533

514534
@Override
515-
public void onDroppedVideoFrames(AnalyticsListener.EventTime eventTime, int droppedFrames, long elapsedMs) {
535+
public void onDroppedVideoFrames(@NonNull AnalyticsListener.EventTime eventTime, int droppedFrames, long elapsedMs) {
516536
NRLog.d("onDroppedVideoFrames analytics");
517537
if (!player.isPlayingAd()) {
518538
sendDroppedFrame(droppedFrames, (int) elapsedMs);
@@ -527,8 +547,8 @@ public void onVideoSizeChanged(VideoSize videoSize) {
527547

528548
if (player.isPlayingAd()) return;
529549

530-
long currMul = width * height;
531-
long lastMul = lastWidth * lastHeight;
550+
long currMul = (long) width * height;
551+
long lastMul = (long) lastWidth * lastHeight;
532552

533553
if (lastMul != 0) {
534554
if (lastMul < currMul) {
@@ -543,4 +563,54 @@ public void onVideoSizeChanged(VideoSize videoSize) {
543563
lastHeight = height;
544564
lastWidth = width;
545565
}
546-
}
566+
567+
@Override
568+
public String getLanguage() {
569+
if (player != null && player.getCurrentMediaItem() != null) {
570+
// 1. Try to get language from URI query param
571+
if (player.getCurrentMediaItem().localConfiguration != null) {
572+
Uri uri = player.getCurrentMediaItem().localConfiguration.uri;
573+
String lang = uri.getQueryParameter("lang");
574+
if (lang != null) return lang;
575+
// 2. Try to get language from any path segment
576+
for (String segment : uri.getPathSegments()) {
577+
if (segment.matches("[a-zA-Z]{2,5}")) return segment;
578+
}
579+
// 3. Try to get language from last path segment
580+
String lastSegment = uri.getLastPathSegment();
581+
if (lastSegment != null && lastSegment.matches("[a-zA-Z]{2,5}")) return lastSegment;
582+
}
583+
// 4. Try to get language from MediaMetadata title or description
584+
MediaMetadata mm = player.getCurrentMediaItem().mediaMetadata;
585+
if (mm != null) {
586+
String[] fields = { mm.title != null ? mm.title.toString() : null, mm.description != null ? mm.description.toString() : null };
587+
for (String field : fields) {
588+
if (field == null) continue;
589+
// Look for patterns like (en), [en], en:
590+
Matcher m = Pattern.compile("(?:\\(|\\[)?([a-zA-Z]{2,5})(?:\\)|\\])?|([a-zA-Z]{2,5}):").matcher(field);
591+
if (m.find()) {
592+
if (m.group(1) != null) return m.group(1);
593+
if (m.group(2) != null) return m.group(2);
594+
}
595+
}
596+
}
597+
}
598+
// 5. Fallback: try playlist URI
599+
if (getPlaylist() != null && player != null) {
600+
try {
601+
Uri src = getPlaylist().get(player.getCurrentMediaItemIndex());
602+
String lang = src.getQueryParameter("lang");
603+
if (lang != null) return lang;
604+
for (String segment : src.getPathSegments()) {
605+
if (segment.matches("[a-zA-Z]{2,5}")) return segment;
606+
}
607+
String lastSegment = src.getLastPathSegment();
608+
if (lastSegment != null && lastSegment.matches("[a-zA-Z]{2,5}")) return lastSegment;
609+
} catch(Exception e) {
610+
// ignore
611+
}
612+
}
613+
return null;
614+
}
615+
616+
}

NRIMATracker/build.gradle

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ android {
88
minSdkVersion 16
99
targetSdkVersion 33
1010
versionCode 6
11-
versionName "3.0.3"
11+
versionName "4.0.0"
1212
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1313
consumerProguardFiles "consumer-rules.pro"
1414
}
@@ -37,9 +37,11 @@ dependencies {
3737
implementation 'com.google.android.material:material:1.3.0'
3838
implementation project(path: ':NewRelicVideoCore')
3939
implementation 'androidx.media3:media3-exoplayer-ima:1.1.0'
40-
testImplementation 'junit:junit:4.+'
41-
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
42-
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
40+
testImplementation 'junit:junit:4.13.2'
41+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
42+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
43+
androidTestImplementation 'androidx.test:runner:1.5.2'
44+
androidTestImplementation 'androidx.test:rules:1.5.0'
4345
}
4446
afterEvaluate {
4547
publishing {
@@ -52,4 +54,4 @@ afterEvaluate {
5254
}
5355
}
5456
}
55-
}
57+
}

NRIMATracker/src/main/java/com/newrelic/videoagent/ima/tracker/NRTrackerIMA.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import com.newrelic.videoagent.core.tracker.NRVideoTracker;
77
import com.newrelic.videoagent.core.utils.NRLog;
88
import com.newrelic.videoagent.ima.BuildConfig;
9+
10+
import java.lang.reflect.Method;
11+
912
import static com.newrelic.videoagent.core.NRDef.*;
1013

1114
public class NRTrackerIMA extends NRVideoTracker implements AdErrorEvent.AdErrorListener, AdEvent.AdEventListener {
@@ -98,7 +101,7 @@ private void fillAdAttributes(Ad ad) {
98101
bitrate = (long)ad.getVastMediaBitrate();
99102
renditionHeight = (long)ad.getVastMediaHeight();
100103
renditionWidth = (long)ad.getVastMediaWidth();
101-
duration = (long)ad.getDuration();
104+
duration = Math.max((long)ad.getDuration(), 0L);
102105
}
103106

104107
/**
@@ -216,4 +219,55 @@ public Long getRenditionWidth() {
216219
public Long getDuration() {
217220
return duration;
218221
}
222+
223+
/**
224+
* Register this tracker as an AdEventListener and AdErrorListener with any IMA SDK object that supports it.
225+
*/
226+
public void registerListeners(Object imaSdkObject) {
227+
if (imaSdkObject == null) return;
228+
// Skip ExoPlayer's ImaAdsLoader, which does not support these methods
229+
if (imaSdkObject.getClass().getName().contains("ImaAdsLoader")) return;
230+
try {
231+
Method addAdEventListener = imaSdkObject.getClass().getMethod("addAdEventListener", AdEvent.AdEventListener.class);
232+
addAdEventListener.invoke(imaSdkObject, this);
233+
Method addAdErrorListener = imaSdkObject.getClass().getMethod("addAdErrorListener", AdErrorEvent.AdErrorListener.class);
234+
addAdErrorListener.invoke(imaSdkObject, this);
235+
} catch (Exception e) {
236+
NRLog.d("NRTrackerIMA: Failed to register ad listeners: " + e);
237+
}
238+
}
239+
240+
/**
241+
* Unregister this tracker as an AdEventListener and AdErrorListener from any IMA SDK object that supports it.
242+
*/
243+
public void unregisterListeners(Object imaSdkObject) {
244+
if (imaSdkObject == null) return;
245+
if (imaSdkObject.getClass().getName().contains("ImaAdsLoader")) return;
246+
try {
247+
Method removeAdEventListener = imaSdkObject.getClass().getMethod("removeAdEventListener", AdEvent.AdEventListener.class);
248+
removeAdEventListener.invoke(imaSdkObject, this);
249+
Method removeAdErrorListener = imaSdkObject.getClass().getMethod("removeAdErrorListener", AdErrorEvent.AdErrorListener.class);
250+
removeAdErrorListener.invoke(imaSdkObject, this);
251+
} catch (Exception e) {
252+
NRLog.d("NRTrackerIMA: Failed to unregister ad listeners: " + e);
253+
}
254+
}
255+
256+
// Allow for
257+
// warding from user-defined listeners
258+
259+
public void handleAdEvent(AdEvent adEvent) {
260+
onAdEvent(adEvent);
261+
}
262+
public void handleAdError(AdErrorEvent adErrorEvent) {
263+
onAdError(adErrorEvent);
264+
}
265+
266+
// Provide accessors for listeners so user can register them easily
267+
public AdEvent.AdEventListener getAdEventListener() {
268+
return this;
269+
}
270+
public AdErrorEvent.AdErrorListener getAdErrorListener() {
271+
return this;
272+
}
219273
}

0 commit comments

Comments
 (0)