summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java355
1 files changed, 355 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
new file mode 100644
index 0000000000..710934bd36
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManager.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.thirdparty.com.google.android.exoplayer2.analytics;
+
+import android.util.Base64;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Player.DiscontinuityReason;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Timeline;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Random;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
+ * timeline and also for each ad within the windows.
+ *
+ * <p>Sessions are identified by Base64-encoded, URL-safe, random strings.
+ */
+public final class DefaultPlaybackSessionManager implements PlaybackSessionManager {
+
+ private static final Random RANDOM = new Random();
+ private static final int SESSION_ID_LENGTH = 12;
+
+ private final Timeline.Window window;
+ private final Timeline.Period period;
+ private final HashMap<String, SessionDescriptor> sessions;
+
+ private @MonotonicNonNull Listener listener;
+ private Timeline currentTimeline;
+ @Nullable private MediaPeriodId currentMediaPeriodId;
+ @Nullable private String activeSessionId;
+
+ /** Creates session manager. */
+ public DefaultPlaybackSessionManager() {
+ window = new Timeline.Window();
+ period = new Timeline.Period();
+ sessions = new HashMap<>();
+ currentTimeline = Timeline.EMPTY;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public synchronized String getSessionForMediaPeriodId(
+ Timeline timeline, MediaPeriodId mediaPeriodId) {
+ int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
+ return getOrAddSession(windowIndex, mediaPeriodId).sessionId;
+ }
+
+ @Override
+ public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) {
+ SessionDescriptor sessionDescriptor = sessions.get(sessionId);
+ if (sessionDescriptor == null) {
+ return false;
+ }
+ sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId);
+ return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ }
+
+ @Override
+ public synchronized void updateSessions(EventTime eventTime) {
+ boolean isObviouslyFinished =
+ eventTime.mediaPeriodId != null
+ && currentMediaPeriodId != null
+ && eventTime.mediaPeriodId.windowSequenceNumber
+ < currentMediaPeriodId.windowSequenceNumber;
+ if (!isObviouslyFinished) {
+ SessionDescriptor descriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (!descriptor.isCreated) {
+ descriptor.isCreated = true;
+ Assertions.checkNotNull(listener).onSessionCreated(eventTime, descriptor.sessionId);
+ if (activeSessionId == null) {
+ updateActiveSession(eventTime, descriptor);
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void handleTimelineUpdate(EventTime eventTime) {
+ Assertions.checkNotNull(listener);
+ Timeline previousTimeline = currentTimeline;
+ currentTimeline = eventTime.timeline;
+ Iterator<SessionDescriptor> iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {
+ iterator.remove();
+ if (session.isCreated) {
+ if (session.sessionId.equals(activeSessionId)) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(
+ eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
+ }
+ }
+ }
+ handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL);
+ }
+
+ @Override
+ public synchronized void handlePositionDiscontinuity(
+ EventTime eventTime, @DiscontinuityReason int reason) {
+ Assertions.checkNotNull(listener);
+ boolean hasAutomaticTransition =
+ reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ || reason == Player.DISCONTINUITY_REASON_AD_INSERTION;
+ Iterator<SessionDescriptor> iterator = sessions.values().iterator();
+ while (iterator.hasNext()) {
+ SessionDescriptor session = iterator.next();
+ if (session.isFinishedAtEventTime(eventTime)) {
+ iterator.remove();
+ if (session.isCreated) {
+ boolean isRemovingActiveSession = session.sessionId.equals(activeSessionId);
+ boolean isAutomaticTransition = hasAutomaticTransition && isRemovingActiveSession;
+ if (isRemovingActiveSession) {
+ activeSessionId = null;
+ }
+ listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);
+ }
+ }
+ }
+ SessionDescriptor activeSessionDescriptor =
+ getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
+ if (eventTime.mediaPeriodId != null
+ && eventTime.mediaPeriodId.isAd()
+ && (currentMediaPeriodId == null
+ || currentMediaPeriodId.windowSequenceNumber
+ != eventTime.mediaPeriodId.windowSequenceNumber
+ || currentMediaPeriodId.adGroupIndex != eventTime.mediaPeriodId.adGroupIndex
+ || currentMediaPeriodId.adIndexInAdGroup != eventTime.mediaPeriodId.adIndexInAdGroup)) {
+ // New ad playback started. Find corresponding content session and notify ad playback started.
+ MediaPeriodId contentMediaPeriodId =
+ new MediaPeriodId(
+ eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber);
+ SessionDescriptor contentSession =
+ getOrAddSession(eventTime.windowIndex, contentMediaPeriodId);
+ if (contentSession.isCreated && activeSessionDescriptor.isCreated) {
+ listener.onAdPlaybackStarted(
+ eventTime, contentSession.sessionId, activeSessionDescriptor.sessionId);
+ }
+ }
+ updateActiveSession(eventTime, activeSessionDescriptor);
+ }
+
+ private SessionDescriptor getOrAddSession(
+ int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is
+ // null, there may be multiple matching sessions with different window sequence numbers or
+ // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for
+ // windows with ads, the content session is preferred over ad sessions.
+ SessionDescriptor bestMatch = null;
+ long bestMatchWindowSequenceNumber = Long.MAX_VALUE;
+ for (SessionDescriptor sessionDescriptor : sessions.values()) {
+ sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId);
+ if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) {
+ long windowSequenceNumber = sessionDescriptor.windowSequenceNumber;
+ if (windowSequenceNumber == C.INDEX_UNSET
+ || windowSequenceNumber < bestMatchWindowSequenceNumber) {
+ bestMatch = sessionDescriptor;
+ bestMatchWindowSequenceNumber = windowSequenceNumber;
+ } else if (windowSequenceNumber == bestMatchWindowSequenceNumber
+ && Util.castNonNull(bestMatch).adMediaPeriodId != null
+ && sessionDescriptor.adMediaPeriodId != null) {
+ bestMatch = sessionDescriptor;
+ }
+ }
+ }
+ if (bestMatch == null) {
+ String sessionId = generateSessionId();
+ bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);
+ sessions.put(sessionId, bestMatch);
+ }
+ return bestMatch;
+ }
+
+ @RequiresNonNull("listener")
+ private void updateActiveSession(EventTime eventTime, SessionDescriptor sessionDescriptor) {
+ currentMediaPeriodId = eventTime.mediaPeriodId;
+ if (sessionDescriptor.isCreated) {
+ activeSessionId = sessionDescriptor.sessionId;
+ if (!sessionDescriptor.isActive) {
+ sessionDescriptor.isActive = true;
+ listener.onSessionActive(eventTime, sessionDescriptor.sessionId);
+ }
+ }
+ }
+
+ private static String generateSessionId() {
+ byte[] randomBytes = new byte[SESSION_ID_LENGTH];
+ RANDOM.nextBytes(randomBytes);
+ return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);
+ }
+
+ /**
+ * Descriptor for a session.
+ *
+ * <p>The session may be described in one of three ways:
+ *
+ * <ul>
+ * <li>A window index with unset window sequence number and a null ad media period id
+ * <li>A content window with index and sequence number, but a null ad media period id.
+ * <li>An ad with all values set.
+ * </ul>
+ */
+ private final class SessionDescriptor {
+
+ private final String sessionId;
+
+ private int windowIndex;
+ private long windowSequenceNumber;
+ private @MonotonicNonNull MediaPeriodId adMediaPeriodId;
+
+ private boolean isCreated;
+ private boolean isActive;
+
+ public SessionDescriptor(
+ String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
+ this.sessionId = sessionId;
+ this.windowIndex = windowIndex;
+ this.windowSequenceNumber =
+ mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber;
+ if (mediaPeriodId != null && mediaPeriodId.isAd()) {
+ this.adMediaPeriodId = mediaPeriodId;
+ }
+ }
+
+ public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) {
+ windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex);
+ if (windowIndex == C.INDEX_UNSET) {
+ return false;
+ }
+ if (adMediaPeriodId == null) {
+ return true;
+ }
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ return newPeriodIndex != C.INDEX_UNSET;
+ }
+
+ public boolean belongsToSession(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (eventMediaPeriodId == null) {
+ // Events without concrete media period id are for all sessions of the same window.
+ return eventWindowIndex == windowIndex;
+ }
+ if (adMediaPeriodId == null) {
+ // If this is a content session, only events for content with the same window sequence
+ // number belong to this session.
+ return !eventMediaPeriodId.isAd()
+ && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber;
+ }
+ // If this is an ad session, only events for this ad belong to the session.
+ return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber
+ && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex
+ && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup;
+ }
+
+ public void maybeSetWindowSequenceNumber(
+ int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
+ if (windowSequenceNumber == C.INDEX_UNSET
+ && eventWindowIndex == windowIndex
+ && eventMediaPeriodId != null
+ && !eventMediaPeriodId.isAd()) {
+ // Set window sequence number for this session as soon as we have one.
+ windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber;
+ }
+ }
+
+ public boolean isFinishedAtEventTime(EventTime eventTime) {
+ if (windowSequenceNumber == C.INDEX_UNSET) {
+ // Sessions with unspecified window sequence number are kept until we know more.
+ return false;
+ }
+ if (eventTime.mediaPeriodId == null) {
+ // For event times without media period id (e.g. after seek to new window), we only keep
+ // sessions of this window.
+ return windowIndex != eventTime.windowIndex;
+ }
+ if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) {
+ // All past window sequence numbers are finished.
+ return true;
+ }
+ if (adMediaPeriodId == null) {
+ // Current or future content is not finished.
+ return false;
+ }
+ int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
+ int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
+ if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber
+ || eventPeriodIndex < adPeriodIndex) {
+ // Ads in future windows or periods are not finished.
+ return false;
+ }
+ if (eventPeriodIndex > adPeriodIndex) {
+ // Ads in past periods are finished.
+ return true;
+ }
+ if (eventTime.mediaPeriodId.isAd()) {
+ int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex;
+ int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup;
+ // Finished if event is for an ad after this one in the same period.
+ return eventAdGroup > adMediaPeriodId.adGroupIndex
+ || (eventAdGroup == adMediaPeriodId.adGroupIndex
+ && eventAdIndex > adMediaPeriodId.adIndexInAdGroup);
+ } else {
+ // Finished if the event is for content after this ad.
+ return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET
+ || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex;
+ }
+ }
+
+ private int resolveWindowIndexToNewTimeline(
+ Timeline oldTimeline, Timeline newTimeline, int windowIndex) {
+ if (windowIndex >= oldTimeline.getWindowCount()) {
+ return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET;
+ }
+ oldTimeline.getWindow(windowIndex, window);
+ for (int periodIndex = window.firstPeriodIndex;
+ periodIndex <= window.lastPeriodIndex;
+ periodIndex++) {
+ Object periodUid = oldTimeline.getUidOfPeriod(periodIndex);
+ int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid);
+ if (newPeriodIndex != C.INDEX_UNSET) {
+ return newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
+ }
+ }
+ return C.INDEX_UNSET;
+ }
+ }
+}