diff options
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.java | 355 |
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; + } + } +} |