NekoX/TMessagesProj/src/main/java/com/google/android/exoplayer2/analytics/DefaultPlaybackSessionManag...

372 lines
16 KiB
Java

/*
* 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 com.google.android.exoplayer2.analytics;
import android.util.Base64;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.util.Assertions;
import 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;
/**
* 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 String currentSessionId;
/** 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) {
Assertions.checkNotNull(listener);
@Nullable SessionDescriptor currentSession = sessions.get(currentSessionId);
if (eventTime.mediaPeriodId != null && currentSession != null) {
// If we receive an event associated with a media period, then it needs to be either part of
// the current window if it's the first created media period, or a window that will be played
// in the future. Otherwise, we know that it belongs to a session that was already finished
// and we can ignore the event.
boolean isAlreadyFinished =
currentSession.windowSequenceNumber == C.INDEX_UNSET
? currentSession.windowIndex != eventTime.windowIndex
: eventTime.mediaPeriodId.windowSequenceNumber < currentSession.windowSequenceNumber;
if (isAlreadyFinished) {
return;
}
}
SessionDescriptor eventSession =
getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
if (currentSessionId == null) {
currentSessionId = eventSession.sessionId;
}
if (!eventSession.isCreated) {
eventSession.isCreated = true;
listener.onSessionCreated(eventTime, eventSession.sessionId);
}
if (eventSession.sessionId.equals(currentSessionId) && !eventSession.isActive) {
eventSession.isActive = true;
listener.onSessionActive(eventTime, eventSession.sessionId);
}
}
@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(currentSessionId)) {
currentSessionId = 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 isRemovingCurrentSession = session.sessionId.equals(currentSessionId);
boolean isAutomaticTransition =
hasAutomaticTransition && isRemovingCurrentSession && session.isActive;
if (isRemovingCurrentSession) {
currentSessionId = null;
}
listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);
}
}
}
@Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId);
SessionDescriptor currentSessionDescriptor =
getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
currentSessionId = currentSessionDescriptor.sessionId;
if (eventTime.mediaPeriodId != null
&& eventTime.mediaPeriodId.isAd()
&& (previousSessionDescriptor == null
|| previousSessionDescriptor.windowSequenceNumber
!= eventTime.mediaPeriodId.windowSequenceNumber
|| previousSessionDescriptor.adMediaPeriodId == null
|| previousSessionDescriptor.adMediaPeriodId.adGroupIndex
!= eventTime.mediaPeriodId.adGroupIndex
|| previousSessionDescriptor.adMediaPeriodId.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 && currentSessionDescriptor.isCreated) {
listener.onAdPlaybackStarted(
eventTime, contentSession.sessionId, currentSessionDescriptor.sessionId);
}
}
}
@Override
public void finishAllSessions(EventTime eventTime) {
currentSessionId = null;
Iterator<SessionDescriptor> iterator = sessions.values().iterator();
while (iterator.hasNext()) {
SessionDescriptor session = iterator.next();
iterator.remove();
if (session.isCreated && listener != null) {
listener.onSessionFinished(
eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
}
}
}
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;
}
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) {
// 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;
}
}
}