mirror of https://github.com/NekoX-Dev/NekoX.git
372 lines
16 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|