Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import android.os.Looper;

import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.C;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MediaMetadata;
Expand All @@ -17,6 +19,7 @@
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;

import com.newrelic.videoagent.core.NRVideoConfiguration;
import com.newrelic.videoagent.core.tracker.NRVideoTracker;
import com.newrelic.videoagent.core.utils.NRLog;
import com.newrelic.videoagent.exoplayer.BuildConfig;
Expand All @@ -37,7 +40,12 @@

/**
* New Relic Video tracker for ExoPlayer.
* <p>
* @OptIn is required for Media3 APIs which are marked as @UnstableApi.
* This is by design as per Google's Media3 documentation.
* @see <a href="https://developer.android.com/media/media3/exoplayer/customization#unstable-api">Media3 Unstable API Documentation</a>
*/
@OptIn(markerClass = UnstableApi.class)
public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listener, AnalyticsListener {

protected ExoPlayer player;
Expand Down Expand Up @@ -66,15 +74,39 @@ public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listene
/**
* Init a new ExoPlayer tracker.
*/
public NRTrackerExoPlayer(NRVideoConfiguration configuration) {
super(configuration);
}

/**
* Create a new NRTrackerExoPlayer (deprecated - use constructor with configuration).
* @deprecated Use NRTrackerExoPlayer(NRVideoConfiguration) constructor instead
*/
@Deprecated
public NRTrackerExoPlayer() {
super();
}

/**
* Init a new ExoPlayer tracker.
*
* @param configuration Video configuration
* @param player ExoPlayer instance.
*/
public NRTrackerExoPlayer(NRVideoConfiguration configuration, ExoPlayer player) {
super(configuration);
setPlayer(player);
}

/**
* Init a new ExoPlayer tracker (deprecated).
*
* @param player ExoPlayer instance.
* @deprecated Use NRTrackerExoPlayer(NRVideoConfiguration, ExoPlayer) constructor instead
*/
@Deprecated
public NRTrackerExoPlayer(ExoPlayer player) {
super();
setPlayer(player);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.newrelic.videoagent.core.NRVideoConfiguration;
import com.newrelic.videoagent.core.tracker.NRVideoTracker;
import com.newrelic.videoagent.core.utils.NRLog;
import com.newrelic.videoagent.ima.BuildConfig;
Expand All @@ -13,6 +14,22 @@

public class NRTrackerIMA extends NRVideoTracker implements AdErrorEvent.AdErrorListener, AdEvent.AdEventListener {

/**
* Create a new IMA tracker with configuration.
*/
public NRTrackerIMA(NRVideoConfiguration configuration) {
super(configuration);
}

/**
* Create a new IMA tracker (deprecated - use constructor with configuration).
* @deprecated Use NRTrackerIMA(NRVideoConfiguration) constructor instead
*/
@Deprecated
public NRTrackerIMA() {
super();
}

private String adPosition = null;
private String creativeId = null;
private Long quartile = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public final class NRVideo {
private static final Object lock = new Object();

private volatile HarvestManager harvestManager;
private volatile NRVideoConfiguration configuration;
private final Map<String, Integer> trackerIds = new HashMap<>();

// Private constructor for singleton
Expand Down Expand Up @@ -60,10 +61,10 @@ public static Integer addPlayer(NRVideoPlayerConfiguration config) {
}

// Create content tracker with ExoPlayer instance
NRTracker contentTracker = createContentTracker();
NRTracker contentTracker = createContentTracker(instance.configuration);
NRTracker adsTracker = null;
if (config.isAdEnabled()) {
adsTracker = createAdTracker();
adsTracker = createAdTracker(instance.configuration);
NRLog.d("add tracker is added");
}

Expand Down Expand Up @@ -228,6 +229,9 @@ private NRVideo initialize(Context context, NRVideoConfiguration config) {
try {
Context applicationContext = context.getApplicationContext();

// Store configuration for tracker creation
this.configuration = config;

// Always use crash-safe storage - it's now the default behavior
harvestManager = new HarvestManager(config, applicationContext);

Expand Down Expand Up @@ -256,25 +260,35 @@ private NRVideo initialize(Context context, NRVideoConfiguration config) {
}
}

private static NRTracker createContentTracker() {
private static NRTracker createContentTracker(NRVideoConfiguration config) {
try {
// Create ExoPlayer tracker with player instance
// Create ExoPlayer tracker with configuration
Class<?> exoTrackerClass = Class.forName("com.newrelic.videoagent.exoplayer.tracker.NRTrackerExoPlayer");
return (NRTracker) exoTrackerClass.newInstance();
return (NRTracker) exoTrackerClass.getConstructor(NRVideoConfiguration.class).newInstance(config);
} catch (Exception e) {
// Fallback to basic video tracker
throw new RuntimeException("Failed to create NRTrackerExoPlayer", e);
// Fallback to deprecated constructor for backward compatibility
try {
Class<?> exoTrackerClass = Class.forName("com.newrelic.videoagent.exoplayer.tracker.NRTrackerExoPlayer");
return (NRTracker) exoTrackerClass.newInstance();
} catch (Exception fallbackException) {
throw new RuntimeException("Failed to create NRTrackerExoPlayer", fallbackException);
}
}
}

private static NRTracker createAdTracker() {

private static NRTracker createAdTracker(NRVideoConfiguration config) {
try {
// Always use IMA tracker for ads
// Always use IMA tracker for ads with configuration
Class<?> imaTrackerClass = Class.forName("com.newrelic.videoagent.ima.tracker.NRTrackerIMA");
return (NRTracker) imaTrackerClass.newInstance();
return (NRTracker) imaTrackerClass.getConstructor(NRVideoConfiguration.class).newInstance(config);
} catch (Exception e) {
return null;
// Fallback to deprecated constructor for backward compatibility
try {
Class<?> imaTrackerClass = Class.forName("com.newrelic.videoagent.ima.tracker.NRTrackerIMA");
return (NRTracker) imaTrackerClass.newInstance();
} catch (Exception fallbackException) {
return null;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import com.newrelic.videoagent.core.utils.NRLog;
import java.util.concurrent.atomic.AtomicBoolean;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -42,6 +43,12 @@ public final class NRVideoConfiguration {
private final boolean memoryOptimized;
private final boolean debugLoggingEnabled;
private final boolean isTV;
private final String collectorAddress;

// Runtime configuration fields (mutable, thread-safe) - Using AtomicBoolean for better performance
private final AtomicBoolean qoeAggregateEnabled = new AtomicBoolean(true);
private final AtomicBoolean runtimeConfigInitialized = new AtomicBoolean(false);


// Performance optimization constants
private static final int DEFAULT_HARVEST_CYCLE_SECONDS = 5 * 60; // 5 minutes
Expand Down Expand Up @@ -78,6 +85,11 @@ private NRVideoConfiguration(Builder builder) {
this.memoryOptimized = builder.memoryOptimized;
this.debugLoggingEnabled = builder.debugLoggingEnabled;
this.isTV = builder.isTV;
this.collectorAddress = builder.collectorAddress;

// Initialize runtime configuration
this.qoeAggregateEnabled.set(builder.qoeAggregateEnabled);
this.runtimeConfigInitialized.set(true);
}

// Immutable getters
Expand All @@ -91,6 +103,39 @@ private NRVideoConfiguration(Builder builder) {
public boolean isMemoryOptimized() { return memoryOptimized; }
public boolean isDebugLoggingEnabled() { return debugLoggingEnabled; }
public boolean isTV() { return isTV; }
public String getCollectorAddress() { return collectorAddress; }

// Runtime configuration getters and setters
/**
* Check if QOE_AGGREGATE events should be sent during harvest cycles
* @return true if QOE_AGGREGATE should be sent, false otherwise
*/
public boolean isQoeAggregateEnabled() {
if (!runtimeConfigInitialized.get()) {
throw new IllegalStateException("NRVideoConfiguration not initialized! Call build() first.");
}
return qoeAggregateEnabled.get();
}

/**
* Set whether QOE_AGGREGATE events should be sent during harvest cycles
* Lock-free, thread-safe runtime configuration using AtomicBoolean
* @param enabled true to enable QOE_AGGREGATE, false to disable
*/
public void setQoeAggregateEnabled(boolean enabled) {
this.qoeAggregateEnabled.set(enabled);
}

/**
* Initialize configuration with client settings
* @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided)
*/
public void initializeFromClient(Boolean clientQoeAggregateEnabled) {
// If client provides a value, use it; otherwise keep current default
if (clientQoeAggregateEnabled != null) {
this.qoeAggregateEnabled.set(clientQoeAggregateEnabled);
}
}

/**
* Get dead letter retry interval in milliseconds
Expand Down Expand Up @@ -158,6 +203,8 @@ public static final class Builder {
private boolean memoryOptimized = true;
private boolean debugLoggingEnabled = false;
private boolean isTV = false;
private String collectorAddress = null;
private boolean qoeAggregateEnabled = true; // Default enabled

public Builder(String applicationToken) {
this.applicationToken = applicationToken;
Expand Down Expand Up @@ -235,6 +282,35 @@ public Builder enableLogging() {
return this;
}

/**
* Set custom collector domain address for /connect and /data endpoints (optional)
* Example: "staging-mobile-collector.newrelic.com" or "mobile-collector.newrelic.com"
* If not set, will be auto-detected from application token region
*/
public Builder withCollectorAddress(String collectorAddress) {
this.collectorAddress = collectorAddress;
return this;
}

/**
* Enable QOE aggregate events (default: enabled)
* @return Builder instance for method chaining
*/
public Builder enableQoeAggregate() {
this.qoeAggregateEnabled = true;
return this;
}

/**
* Configure QOE aggregate events
* @param enabled true to enable QOE_AGGREGATE events, false to disable
* @return Builder instance for method chaining
*/
public Builder enableQoeAggregate(boolean enabled) {
this.qoeAggregateEnabled = enabled;
return this;
}

private void applyTVOptimizations() {
this.harvestCycleSeconds = TV_HARVEST_CYCLE_SECONDS;
this.liveHarvestCycleSeconds = TV_LIVE_HARVEST_CYCLE_SECONDS;
Expand All @@ -251,7 +327,10 @@ private void applyMemoryOptimizations() {
}

public NRVideoConfiguration build() {
return new NRVideoConfiguration(this);
NRVideoConfiguration config = new NRVideoConfiguration(this);
// Mark runtime configuration as initialized
config.runtimeConfigInitialized.set(true);
return config;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,25 @@ private boolean isTokenValid() {

/**
* Build token endpoint URL based on region
* If collectorAddress is explicitly set, use it for /connect endpoint
* Otherwise, auto-detect from region
*/
private String buildTokenEndpoint() {
String region = configuration.getRegion().toUpperCase();
// If collectorAddress is explicitly set, use it for /connect endpoint
if (configuration.getCollectorAddress() != null && !configuration.getCollectorAddress().isEmpty()) {
return "https://" + configuration.getCollectorAddress() + "/mobile/v5/connect";
}

// Otherwise, auto-detect from region
String region = configuration.getRegion();
region = (region != null) ? region.toUpperCase() : "US";
switch (region) {
case "EU":
return "https://mobile-collector.eu.newrelic.com/mobile/v5/connect";
case "AP":
return "https://mobile-collector.ap.newrelic.com/mobile/v5/connect";
case "GOV":
return "https://mobile-collector.gov.newrelic.com/mobile/v5/connect";
case "STAGING":
return "https://mobile-collector.staging.newrelic.com/mobile/v5/connect";
default:
return "https://mobile-collector.newrelic.com/mobile/v5/connect";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public class OptimizedHttpClient implements HttpClientInterface {
REGIONAL_ENDPOINTS.put("EU", "https://mobile-collector.eu.newrelic.com/mobile/v3/data");
REGIONAL_ENDPOINTS.put("AP", "https://mobile-collector.ap.newrelic.com/mobile/v3/data");
REGIONAL_ENDPOINTS.put("GOV", "https://mobile-collector.gov.newrelic.com/mobile/v3/data");
REGIONAL_ENDPOINTS.put("STAGING", "https://mobile-collector.staging.newrelic.com/mobile/v3/data");
REGIONAL_ENDPOINTS.put("DEFAULT", REGIONAL_ENDPOINTS.get("US"));
}

Expand All @@ -59,10 +58,16 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C
this.tokenManager = new TokenManager(context, configuration);
this.deviceInfo = DeviceInformation.getInstance(context);

// Set endpoint URL based on region, defaulting to US if not found
String region = configuration.getRegion().toUpperCase();
String regionEndpoint = REGIONAL_ENDPOINTS.get(region);
this.endpointUrl = regionEndpoint != null ? regionEndpoint : REGIONAL_ENDPOINTS.get("DEFAULT");
// If collectorAddress is explicitly set, use it
if (configuration.getCollectorAddress() != null && !configuration.getCollectorAddress().isEmpty()) {
this.endpointUrl = "https://" + configuration.getCollectorAddress() + "/mobile/v3/data";
} else {
// Otherwise, auto-detect from region
String region = configuration.getRegion();
region = (region != null) ? region.toUpperCase() : "US";
String endpoint = REGIONAL_ENDPOINTS.get(region);
this.endpointUrl = (endpoint != null) ? endpoint : REGIONAL_ENDPOINTS.get("DEFAULT");
}

if (configuration.isMemoryOptimized()) {
connectionTimeoutMs = 6000;
Expand All @@ -74,7 +79,7 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C
System.setProperty("http.keepAliveDuration", "300000"); // 5 minutes
System.setProperty("http.maxConnections", "5");

NRLog.d("Initialized with region: " + region +
NRLog.d("Initialized with region: " + configuration.getRegion() +
", endpoint URL: " + endpointUrl);
}

Expand Down
Loading