• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.analytics;
17 
18 import android.util.Base64;
19 import androidx.annotation.Nullable;
20 import com.google.android.exoplayer2.C;
21 import com.google.android.exoplayer2.Player;
22 import com.google.android.exoplayer2.Player.DiscontinuityReason;
23 import com.google.android.exoplayer2.Timeline;
24 import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime;
25 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
26 import com.google.android.exoplayer2.util.Assertions;
27 import com.google.android.exoplayer2.util.Supplier;
28 import com.google.android.exoplayer2.util.Util;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.Random;
32 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
33 
34 /**
35  * Default {@link PlaybackSessionManager} which instantiates a new session for each window in the
36  * timeline and also for each ad within the windows.
37  *
38  * <p>By default, sessions are identified by Base64-encoded, URL-safe, random strings.
39  */
40 public final class DefaultPlaybackSessionManager implements PlaybackSessionManager {
41 
42   /** Default generator for unique session ids that are random, Based64-encoded and URL-safe. */
43   public static final Supplier<String> DEFAULT_SESSION_ID_GENERATOR =
44       DefaultPlaybackSessionManager::generateDefaultSessionId;
45 
46   private static final Random RANDOM = new Random();
47   private static final int SESSION_ID_LENGTH = 12;
48 
49   private final Timeline.Window window;
50   private final Timeline.Period period;
51   private final HashMap<String, SessionDescriptor> sessions;
52   private final Supplier<String> sessionIdGenerator;
53 
54   private @MonotonicNonNull Listener listener;
55   private Timeline currentTimeline;
56   @Nullable private String currentSessionId;
57 
58   /**
59    * Creates session manager with a {@link #DEFAULT_SESSION_ID_GENERATOR} to generate session ids.
60    */
DefaultPlaybackSessionManager()61   public DefaultPlaybackSessionManager() {
62     this(DEFAULT_SESSION_ID_GENERATOR);
63   }
64 
65   /**
66    * Creates session manager.
67    *
68    * @param sessionIdGenerator A generator for new session ids. All generated session ids must be
69    *     unique.
70    */
DefaultPlaybackSessionManager(Supplier<String> sessionIdGenerator)71   public DefaultPlaybackSessionManager(Supplier<String> sessionIdGenerator) {
72     this.sessionIdGenerator = sessionIdGenerator;
73     window = new Timeline.Window();
74     period = new Timeline.Period();
75     sessions = new HashMap<>();
76     currentTimeline = Timeline.EMPTY;
77   }
78 
79   @Override
setListener(Listener listener)80   public void setListener(Listener listener) {
81     this.listener = listener;
82   }
83 
84   @Override
getSessionForMediaPeriodId( Timeline timeline, MediaPeriodId mediaPeriodId)85   public synchronized String getSessionForMediaPeriodId(
86       Timeline timeline, MediaPeriodId mediaPeriodId) {
87     int windowIndex = timeline.getPeriodByUid(mediaPeriodId.periodUid, period).windowIndex;
88     return getOrAddSession(windowIndex, mediaPeriodId).sessionId;
89   }
90 
91   @Override
belongsToSession(EventTime eventTime, String sessionId)92   public synchronized boolean belongsToSession(EventTime eventTime, String sessionId) {
93     SessionDescriptor sessionDescriptor = sessions.get(sessionId);
94     if (sessionDescriptor == null) {
95       return false;
96     }
97     sessionDescriptor.maybeSetWindowSequenceNumber(eventTime.windowIndex, eventTime.mediaPeriodId);
98     return sessionDescriptor.belongsToSession(eventTime.windowIndex, eventTime.mediaPeriodId);
99   }
100 
101   @Override
updateSessions(EventTime eventTime)102   public synchronized void updateSessions(EventTime eventTime) {
103     Assertions.checkNotNull(listener);
104     @Nullable SessionDescriptor currentSession = sessions.get(currentSessionId);
105     if (eventTime.mediaPeriodId != null && currentSession != null) {
106       // If we receive an event associated with a media period, then it needs to be either part of
107       // the current window if it's the first created media period, or a window that will be played
108       // in the future. Otherwise, we know that it belongs to a session that was already finished
109       // and we can ignore the event.
110       boolean isAlreadyFinished =
111           currentSession.windowSequenceNumber == C.INDEX_UNSET
112               ? currentSession.windowIndex != eventTime.windowIndex
113               : eventTime.mediaPeriodId.windowSequenceNumber < currentSession.windowSequenceNumber;
114       if (isAlreadyFinished) {
115         return;
116       }
117     }
118     SessionDescriptor eventSession =
119         getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
120     if (currentSessionId == null) {
121       currentSessionId = eventSession.sessionId;
122     }
123     if (!eventSession.isCreated) {
124       eventSession.isCreated = true;
125       listener.onSessionCreated(eventTime, eventSession.sessionId);
126     }
127     if (eventSession.sessionId.equals(currentSessionId) && !eventSession.isActive) {
128       eventSession.isActive = true;
129       listener.onSessionActive(eventTime, eventSession.sessionId);
130     }
131   }
132 
133   @Override
134   public synchronized void handleTimelineUpdate(EventTime eventTime) {
135     Assertions.checkNotNull(listener);
136     Timeline previousTimeline = currentTimeline;
137     currentTimeline = eventTime.timeline;
138     Iterator<SessionDescriptor> iterator = sessions.values().iterator();
139     while (iterator.hasNext()) {
140       SessionDescriptor session = iterator.next();
141       if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {
142         iterator.remove();
143         if (session.isCreated) {
144           if (session.sessionId.equals(currentSessionId)) {
145             currentSessionId = null;
146           }
147           listener.onSessionFinished(
148               eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
149         }
150       }
151     }
152     handlePositionDiscontinuity(eventTime, Player.DISCONTINUITY_REASON_INTERNAL);
153   }
154 
155   @Override
156   public synchronized void handlePositionDiscontinuity(
157       EventTime eventTime, @DiscontinuityReason int reason) {
158     Assertions.checkNotNull(listener);
159     boolean hasAutomaticTransition =
160         reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
161             || reason == Player.DISCONTINUITY_REASON_AD_INSERTION;
162     Iterator<SessionDescriptor> iterator = sessions.values().iterator();
163     while (iterator.hasNext()) {
164       SessionDescriptor session = iterator.next();
165       if (session.isFinishedAtEventTime(eventTime)) {
166         iterator.remove();
167         if (session.isCreated) {
168           boolean isRemovingCurrentSession = session.sessionId.equals(currentSessionId);
169           boolean isAutomaticTransition =
170               hasAutomaticTransition && isRemovingCurrentSession && session.isActive;
171           if (isRemovingCurrentSession) {
172             currentSessionId = null;
173           }
174           listener.onSessionFinished(eventTime, session.sessionId, isAutomaticTransition);
175         }
176       }
177     }
178     @Nullable SessionDescriptor previousSessionDescriptor = sessions.get(currentSessionId);
179     SessionDescriptor currentSessionDescriptor =
180         getOrAddSession(eventTime.windowIndex, eventTime.mediaPeriodId);
181     currentSessionId = currentSessionDescriptor.sessionId;
182     if (eventTime.mediaPeriodId != null
183         && eventTime.mediaPeriodId.isAd()
184         && (previousSessionDescriptor == null
185             || previousSessionDescriptor.windowSequenceNumber
186                 != eventTime.mediaPeriodId.windowSequenceNumber
187             || previousSessionDescriptor.adMediaPeriodId == null
188             || previousSessionDescriptor.adMediaPeriodId.adGroupIndex
189                 != eventTime.mediaPeriodId.adGroupIndex
190             || previousSessionDescriptor.adMediaPeriodId.adIndexInAdGroup
191                 != eventTime.mediaPeriodId.adIndexInAdGroup)) {
192       // New ad playback started. Find corresponding content session and notify ad playback started.
193       MediaPeriodId contentMediaPeriodId =
194           new MediaPeriodId(
195               eventTime.mediaPeriodId.periodUid, eventTime.mediaPeriodId.windowSequenceNumber);
196       SessionDescriptor contentSession =
197           getOrAddSession(eventTime.windowIndex, contentMediaPeriodId);
198       if (contentSession.isCreated && currentSessionDescriptor.isCreated) {
199         listener.onAdPlaybackStarted(
200             eventTime, contentSession.sessionId, currentSessionDescriptor.sessionId);
201       }
202     }
203   }
204 
205   @Override
206   public void finishAllSessions(EventTime eventTime) {
207     currentSessionId = null;
208     Iterator<SessionDescriptor> iterator = sessions.values().iterator();
209     while (iterator.hasNext()) {
210       SessionDescriptor session = iterator.next();
211       iterator.remove();
212       if (session.isCreated && listener != null) {
213         listener.onSessionFinished(
214             eventTime, session.sessionId, /* automaticTransitionToNextPlayback= */ false);
215       }
216     }
217   }
218 
219   private SessionDescriptor getOrAddSession(
220       int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
221     // There should only be one matching session if mediaPeriodId is non-null. If mediaPeriodId is
222     // null, there may be multiple matching sessions with different window sequence numbers or
223     // adMediaPeriodIds. The best match is the one with the smaller window sequence number, and for
224     // windows with ads, the content session is preferred over ad sessions.
225     SessionDescriptor bestMatch = null;
226     long bestMatchWindowSequenceNumber = Long.MAX_VALUE;
227     for (SessionDescriptor sessionDescriptor : sessions.values()) {
228       sessionDescriptor.maybeSetWindowSequenceNumber(windowIndex, mediaPeriodId);
229       if (sessionDescriptor.belongsToSession(windowIndex, mediaPeriodId)) {
230         long windowSequenceNumber = sessionDescriptor.windowSequenceNumber;
231         if (windowSequenceNumber == C.INDEX_UNSET
232             || windowSequenceNumber < bestMatchWindowSequenceNumber) {
233           bestMatch = sessionDescriptor;
234           bestMatchWindowSequenceNumber = windowSequenceNumber;
235         } else if (windowSequenceNumber == bestMatchWindowSequenceNumber
236             && Util.castNonNull(bestMatch).adMediaPeriodId != null
237             && sessionDescriptor.adMediaPeriodId != null) {
238           bestMatch = sessionDescriptor;
239         }
240       }
241     }
242     if (bestMatch == null) {
243       String sessionId = sessionIdGenerator.get();
244       bestMatch = new SessionDescriptor(sessionId, windowIndex, mediaPeriodId);
245       sessions.put(sessionId, bestMatch);
246     }
247     return bestMatch;
248   }
249 
250   private static String generateDefaultSessionId() {
251     byte[] randomBytes = new byte[SESSION_ID_LENGTH];
252     RANDOM.nextBytes(randomBytes);
253     return Base64.encodeToString(randomBytes, Base64.URL_SAFE | Base64.NO_WRAP);
254   }
255 
256   /**
257    * Descriptor for a session.
258    *
259    * <p>The session may be described in one of three ways:
260    *
261    * <ul>
262    *   <li>A window index with unset window sequence number and a null ad media period id
263    *   <li>A content window with index and sequence number, but a null ad media period id.
264    *   <li>An ad with all values set.
265    * </ul>
266    */
267   private final class SessionDescriptor {
268 
269     private final String sessionId;
270 
271     private int windowIndex;
272     private long windowSequenceNumber;
273     private @MonotonicNonNull MediaPeriodId adMediaPeriodId;
274 
275     private boolean isCreated;
276     private boolean isActive;
277 
278     public SessionDescriptor(
279         String sessionId, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) {
280       this.sessionId = sessionId;
281       this.windowIndex = windowIndex;
282       this.windowSequenceNumber =
283           mediaPeriodId == null ? C.INDEX_UNSET : mediaPeriodId.windowSequenceNumber;
284       if (mediaPeriodId != null && mediaPeriodId.isAd()) {
285         this.adMediaPeriodId = mediaPeriodId;
286       }
287     }
288 
289     public boolean tryResolvingToNewTimeline(Timeline oldTimeline, Timeline newTimeline) {
290       windowIndex = resolveWindowIndexToNewTimeline(oldTimeline, newTimeline, windowIndex);
291       if (windowIndex == C.INDEX_UNSET) {
292         return false;
293       }
294       if (adMediaPeriodId == null) {
295         return true;
296       }
297       int newPeriodIndex = newTimeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
298       return newPeriodIndex != C.INDEX_UNSET;
299     }
300 
301     public boolean belongsToSession(
302         int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
303       if (eventMediaPeriodId == null) {
304         // Events without concrete media period id are for all sessions of the same window.
305         return eventWindowIndex == windowIndex;
306       }
307       if (adMediaPeriodId == null) {
308         // If this is a content session, only events for content with the same window sequence
309         // number belong to this session.
310         return !eventMediaPeriodId.isAd()
311             && eventMediaPeriodId.windowSequenceNumber == windowSequenceNumber;
312       }
313       // If this is an ad session, only events for this ad belong to the session.
314       return eventMediaPeriodId.windowSequenceNumber == adMediaPeriodId.windowSequenceNumber
315           && eventMediaPeriodId.adGroupIndex == adMediaPeriodId.adGroupIndex
316           && eventMediaPeriodId.adIndexInAdGroup == adMediaPeriodId.adIndexInAdGroup;
317     }
318 
319     public void maybeSetWindowSequenceNumber(
320         int eventWindowIndex, @Nullable MediaPeriodId eventMediaPeriodId) {
321       if (windowSequenceNumber == C.INDEX_UNSET
322           && eventWindowIndex == windowIndex
323           && eventMediaPeriodId != null) {
324         // Set window sequence number for this session as soon as we have one.
325         windowSequenceNumber = eventMediaPeriodId.windowSequenceNumber;
326       }
327     }
328 
329     public boolean isFinishedAtEventTime(EventTime eventTime) {
330       if (windowSequenceNumber == C.INDEX_UNSET) {
331         // Sessions with unspecified window sequence number are kept until we know more.
332         return false;
333       }
334       if (eventTime.mediaPeriodId == null) {
335         // For event times without media period id (e.g. after seek to new window), we only keep
336         // sessions of this window.
337         return windowIndex != eventTime.windowIndex;
338       }
339       if (eventTime.mediaPeriodId.windowSequenceNumber > windowSequenceNumber) {
340         // All past window sequence numbers are finished.
341         return true;
342       }
343       if (adMediaPeriodId == null) {
344         // Current or future content is not finished.
345         return false;
346       }
347       int eventPeriodIndex = eventTime.timeline.getIndexOfPeriod(eventTime.mediaPeriodId.periodUid);
348       int adPeriodIndex = eventTime.timeline.getIndexOfPeriod(adMediaPeriodId.periodUid);
349       if (eventTime.mediaPeriodId.windowSequenceNumber < adMediaPeriodId.windowSequenceNumber
350           || eventPeriodIndex < adPeriodIndex) {
351         // Ads in future windows or periods are not finished.
352         return false;
353       }
354       if (eventPeriodIndex > adPeriodIndex) {
355         // Ads in past periods are finished.
356         return true;
357       }
358       if (eventTime.mediaPeriodId.isAd()) {
359         int eventAdGroup = eventTime.mediaPeriodId.adGroupIndex;
360         int eventAdIndex = eventTime.mediaPeriodId.adIndexInAdGroup;
361         // Finished if event is for an ad after this one in the same period.
362         return eventAdGroup > adMediaPeriodId.adGroupIndex
363             || (eventAdGroup == adMediaPeriodId.adGroupIndex
364                 && eventAdIndex > adMediaPeriodId.adIndexInAdGroup);
365       } else {
366         // Finished if the event is for content after this ad.
367         return eventTime.mediaPeriodId.nextAdGroupIndex == C.INDEX_UNSET
368             || eventTime.mediaPeriodId.nextAdGroupIndex > adMediaPeriodId.adGroupIndex;
369       }
370     }
371 
resolveWindowIndexToNewTimeline( Timeline oldTimeline, Timeline newTimeline, int windowIndex)372     private int resolveWindowIndexToNewTimeline(
373         Timeline oldTimeline, Timeline newTimeline, int windowIndex) {
374       if (windowIndex >= oldTimeline.getWindowCount()) {
375         return windowIndex < newTimeline.getWindowCount() ? windowIndex : C.INDEX_UNSET;
376       }
377       oldTimeline.getWindow(windowIndex, window);
378       for (int periodIndex = window.firstPeriodIndex;
379           periodIndex <= window.lastPeriodIndex;
380           periodIndex++) {
381         Object periodUid = oldTimeline.getUidOfPeriod(periodIndex);
382         int newPeriodIndex = newTimeline.getIndexOfPeriod(periodUid);
383         if (newPeriodIndex != C.INDEX_UNSET) {
384           return newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
385         }
386       }
387       return C.INDEX_UNSET;
388     }
389   }
390 }
391