1 /* 2 * Copyright (C) 2018 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.view.Surface; 19 import androidx.annotation.Nullable; 20 import com.google.android.exoplayer2.C; 21 import com.google.android.exoplayer2.ExoPlaybackException; 22 import com.google.android.exoplayer2.Format; 23 import com.google.android.exoplayer2.PlaybackParameters; 24 import com.google.android.exoplayer2.Player; 25 import com.google.android.exoplayer2.Player.PlaybackSuppressionReason; 26 import com.google.android.exoplayer2.Timeline; 27 import com.google.android.exoplayer2.Timeline.Period; 28 import com.google.android.exoplayer2.Timeline.Window; 29 import com.google.android.exoplayer2.analytics.AnalyticsListener.EventTime; 30 import com.google.android.exoplayer2.audio.AudioAttributes; 31 import com.google.android.exoplayer2.audio.AudioListener; 32 import com.google.android.exoplayer2.audio.AudioRendererEventListener; 33 import com.google.android.exoplayer2.decoder.DecoderCounters; 34 import com.google.android.exoplayer2.drm.DrmSessionEventListener; 35 import com.google.android.exoplayer2.metadata.Metadata; 36 import com.google.android.exoplayer2.metadata.MetadataOutput; 37 import com.google.android.exoplayer2.source.LoadEventInfo; 38 import com.google.android.exoplayer2.source.MediaLoadData; 39 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 40 import com.google.android.exoplayer2.source.MediaSourceEventListener; 41 import com.google.android.exoplayer2.source.TrackGroupArray; 42 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 43 import com.google.android.exoplayer2.upstream.BandwidthMeter; 44 import com.google.android.exoplayer2.util.Assertions; 45 import com.google.android.exoplayer2.util.Clock; 46 import com.google.android.exoplayer2.video.VideoListener; 47 import com.google.android.exoplayer2.video.VideoRendererEventListener; 48 import java.io.IOException; 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Set; 54 import java.util.concurrent.CopyOnWriteArraySet; 55 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 56 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 57 58 /** 59 * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by 60 * listening to all available ExoPlayer listeners. 61 */ 62 public class AnalyticsCollector 63 implements Player.EventListener, 64 MetadataOutput, 65 AudioRendererEventListener, 66 VideoRendererEventListener, 67 MediaSourceEventListener, 68 BandwidthMeter.EventListener, 69 DrmSessionEventListener, 70 VideoListener, 71 AudioListener { 72 73 private final CopyOnWriteArraySet<AnalyticsListener> listeners; 74 private final Clock clock; 75 private final Window window; 76 private final MediaPeriodQueueTracker mediaPeriodQueueTracker; 77 78 private @MonotonicNonNull Player player; 79 private boolean isSeeking; 80 81 /** 82 * Creates an analytics collector. 83 * 84 * @param clock A {@link Clock} used to generate timestamps. 85 */ AnalyticsCollector(Clock clock)86 public AnalyticsCollector(Clock clock) { 87 this.clock = Assertions.checkNotNull(clock); 88 listeners = new CopyOnWriteArraySet<>(); 89 mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); 90 window = new Window(); 91 } 92 93 /** 94 * Adds a listener for analytics events. 95 * 96 * @param listener The listener to add. 97 */ addListener(AnalyticsListener listener)98 public void addListener(AnalyticsListener listener) { 99 listeners.add(listener); 100 } 101 102 /** 103 * Removes a previously added analytics event listener. 104 * 105 * @param listener The listener to remove. 106 */ removeListener(AnalyticsListener listener)107 public void removeListener(AnalyticsListener listener) { 108 listeners.remove(listener); 109 } 110 111 /** 112 * Sets the player for which data will be collected. Must only be called if no player has been set 113 * yet or the current player is idle. 114 * 115 * @param player The {@link Player} for which data will be collected. 116 */ setPlayer(Player player)117 public void setPlayer(Player player) { 118 Assertions.checkState( 119 this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty()); 120 this.player = Assertions.checkNotNull(player); 121 } 122 123 // External events. 124 125 /** 126 * Notify analytics collector that a seek operation will start. Should be called before the player 127 * adjusts its state and position to the seek. 128 */ notifySeekStarted()129 public final void notifySeekStarted() { 130 if (!isSeeking) { 131 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 132 isSeeking = true; 133 for (AnalyticsListener listener : listeners) { 134 listener.onSeekStarted(eventTime); 135 } 136 } 137 } 138 139 /** Resets the analytics collector for a new playlist. */ resetForNewPlaylist()140 public final void resetForNewPlaylist() { 141 // Copying the list is needed because onMediaPeriodReleased will modify the list. 142 List<MediaPeriodInfo> mediaPeriodInfos = 143 new ArrayList<>(mediaPeriodQueueTracker.mediaPeriodInfoQueue); 144 for (MediaPeriodInfo mediaPeriodInfo : mediaPeriodInfos) { 145 onMediaPeriodReleased(mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); 146 } 147 } 148 149 // MetadataOutput implementation. 150 151 @Override onMetadata(Metadata metadata)152 public final void onMetadata(Metadata metadata) { 153 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 154 for (AnalyticsListener listener : listeners) { 155 listener.onMetadata(eventTime, metadata); 156 } 157 } 158 159 // AudioRendererEventListener implementation. 160 161 @Override onAudioEnabled(DecoderCounters counters)162 public final void onAudioEnabled(DecoderCounters counters) { 163 EventTime eventTime = generateReadingMediaPeriodEventTime(); 164 for (AnalyticsListener listener : listeners) { 165 listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_AUDIO, counters); 166 } 167 } 168 169 @Override onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs)170 public final void onAudioDecoderInitialized( 171 String decoderName, long initializedTimestampMs, long initializationDurationMs) { 172 EventTime eventTime = generateReadingMediaPeriodEventTime(); 173 for (AnalyticsListener listener : listeners) { 174 listener.onDecoderInitialized( 175 eventTime, C.TRACK_TYPE_AUDIO, decoderName, initializationDurationMs); 176 } 177 } 178 179 @Override onAudioInputFormatChanged(Format format)180 public final void onAudioInputFormatChanged(Format format) { 181 EventTime eventTime = generateReadingMediaPeriodEventTime(); 182 for (AnalyticsListener listener : listeners) { 183 listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); 184 } 185 } 186 187 @Override onAudioSinkUnderrun( int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)188 public final void onAudioSinkUnderrun( 189 int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { 190 EventTime eventTime = generateReadingMediaPeriodEventTime(); 191 for (AnalyticsListener listener : listeners) { 192 listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); 193 } 194 } 195 196 @Override onAudioDisabled(DecoderCounters counters)197 public final void onAudioDisabled(DecoderCounters counters) { 198 EventTime eventTime = generatePlayingMediaPeriodEventTime(); 199 for (AnalyticsListener listener : listeners) { 200 listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_AUDIO, counters); 201 } 202 } 203 204 // AudioListener implementation. 205 206 @Override onAudioSessionId(int audioSessionId)207 public final void onAudioSessionId(int audioSessionId) { 208 EventTime eventTime = generateReadingMediaPeriodEventTime(); 209 for (AnalyticsListener listener : listeners) { 210 listener.onAudioSessionId(eventTime, audioSessionId); 211 } 212 } 213 214 @Override onAudioAttributesChanged(AudioAttributes audioAttributes)215 public void onAudioAttributesChanged(AudioAttributes audioAttributes) { 216 EventTime eventTime = generateReadingMediaPeriodEventTime(); 217 for (AnalyticsListener listener : listeners) { 218 listener.onAudioAttributesChanged(eventTime, audioAttributes); 219 } 220 } 221 222 @Override onSkipSilenceEnabledChanged(boolean skipSilenceEnabled)223 public void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { 224 EventTime eventTime = generateReadingMediaPeriodEventTime(); 225 for (AnalyticsListener listener : listeners) { 226 listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled); 227 } 228 } 229 230 @Override onVolumeChanged(float audioVolume)231 public void onVolumeChanged(float audioVolume) { 232 EventTime eventTime = generateReadingMediaPeriodEventTime(); 233 for (AnalyticsListener listener : listeners) { 234 listener.onVolumeChanged(eventTime, audioVolume); 235 } 236 } 237 238 // VideoRendererEventListener implementation. 239 240 @Override onVideoEnabled(DecoderCounters counters)241 public final void onVideoEnabled(DecoderCounters counters) { 242 EventTime eventTime = generateReadingMediaPeriodEventTime(); 243 for (AnalyticsListener listener : listeners) { 244 listener.onDecoderEnabled(eventTime, C.TRACK_TYPE_VIDEO, counters); 245 } 246 } 247 248 @Override onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs)249 public final void onVideoDecoderInitialized( 250 String decoderName, long initializedTimestampMs, long initializationDurationMs) { 251 EventTime eventTime = generateReadingMediaPeriodEventTime(); 252 for (AnalyticsListener listener : listeners) { 253 listener.onDecoderInitialized( 254 eventTime, C.TRACK_TYPE_VIDEO, decoderName, initializationDurationMs); 255 } 256 } 257 258 @Override onVideoInputFormatChanged(Format format)259 public final void onVideoInputFormatChanged(Format format) { 260 EventTime eventTime = generateReadingMediaPeriodEventTime(); 261 for (AnalyticsListener listener : listeners) { 262 listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); 263 } 264 } 265 266 @Override onDroppedFrames(int count, long elapsedMs)267 public final void onDroppedFrames(int count, long elapsedMs) { 268 EventTime eventTime = generatePlayingMediaPeriodEventTime(); 269 for (AnalyticsListener listener : listeners) { 270 listener.onDroppedVideoFrames(eventTime, count, elapsedMs); 271 } 272 } 273 274 @Override onVideoDisabled(DecoderCounters counters)275 public final void onVideoDisabled(DecoderCounters counters) { 276 EventTime eventTime = generatePlayingMediaPeriodEventTime(); 277 for (AnalyticsListener listener : listeners) { 278 listener.onDecoderDisabled(eventTime, C.TRACK_TYPE_VIDEO, counters); 279 } 280 } 281 282 @Override onRenderedFirstFrame(@ullable Surface surface)283 public final void onRenderedFirstFrame(@Nullable Surface surface) { 284 EventTime eventTime = generateReadingMediaPeriodEventTime(); 285 for (AnalyticsListener listener : listeners) { 286 listener.onRenderedFirstFrame(eventTime, surface); 287 } 288 } 289 290 @Override onVideoFrameProcessingOffset( long totalProcessingOffsetUs, int frameCount, Format format)291 public final void onVideoFrameProcessingOffset( 292 long totalProcessingOffsetUs, int frameCount, Format format) { 293 EventTime eventTime = generatePlayingMediaPeriodEventTime(); 294 for (AnalyticsListener listener : listeners) { 295 listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount, format); 296 } 297 } 298 299 // VideoListener implementation. 300 301 @Override onRenderedFirstFrame()302 public final void onRenderedFirstFrame() { 303 // Do nothing. Already reported in VideoRendererEventListener.onRenderedFirstFrame. 304 } 305 306 @Override onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)307 public final void onVideoSizeChanged( 308 int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { 309 EventTime eventTime = generateReadingMediaPeriodEventTime(); 310 for (AnalyticsListener listener : listeners) { 311 listener.onVideoSizeChanged( 312 eventTime, width, height, unappliedRotationDegrees, pixelWidthHeightRatio); 313 } 314 } 315 316 @Override onSurfaceSizeChanged(int width, int height)317 public void onSurfaceSizeChanged(int width, int height) { 318 EventTime eventTime = generateReadingMediaPeriodEventTime(); 319 for (AnalyticsListener listener : listeners) { 320 listener.onSurfaceSizeChanged(eventTime, width, height); 321 } 322 } 323 324 // MediaSourceEventListener implementation. 325 326 @Override onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId)327 public final void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) { 328 mediaPeriodQueueTracker.onMediaPeriodCreated( 329 windowIndex, mediaPeriodId, Assertions.checkNotNull(player)); 330 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 331 for (AnalyticsListener listener : listeners) { 332 listener.onMediaPeriodCreated(eventTime); 333 } 334 } 335 336 @Override onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId)337 public final void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) { 338 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 339 if (mediaPeriodQueueTracker.onMediaPeriodReleased( 340 mediaPeriodId, Assertions.checkNotNull(player))) { 341 for (AnalyticsListener listener : listeners) { 342 listener.onMediaPeriodReleased(eventTime); 343 } 344 } 345 } 346 347 @Override onLoadStarted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)348 public final void onLoadStarted( 349 int windowIndex, 350 @Nullable MediaPeriodId mediaPeriodId, 351 LoadEventInfo loadEventInfo, 352 MediaLoadData mediaLoadData) { 353 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 354 for (AnalyticsListener listener : listeners) { 355 listener.onLoadStarted(eventTime, loadEventInfo, mediaLoadData); 356 } 357 } 358 359 @Override onLoadCompleted( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)360 public final void onLoadCompleted( 361 int windowIndex, 362 @Nullable MediaPeriodId mediaPeriodId, 363 LoadEventInfo loadEventInfo, 364 MediaLoadData mediaLoadData) { 365 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 366 for (AnalyticsListener listener : listeners) { 367 listener.onLoadCompleted(eventTime, loadEventInfo, mediaLoadData); 368 } 369 } 370 371 @Override onLoadCanceled( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)372 public final void onLoadCanceled( 373 int windowIndex, 374 @Nullable MediaPeriodId mediaPeriodId, 375 LoadEventInfo loadEventInfo, 376 MediaLoadData mediaLoadData) { 377 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 378 for (AnalyticsListener listener : listeners) { 379 listener.onLoadCanceled(eventTime, loadEventInfo, mediaLoadData); 380 } 381 } 382 383 @Override onLoadError( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled)384 public final void onLoadError( 385 int windowIndex, 386 @Nullable MediaPeriodId mediaPeriodId, 387 LoadEventInfo loadEventInfo, 388 MediaLoadData mediaLoadData, 389 IOException error, 390 boolean wasCanceled) { 391 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 392 for (AnalyticsListener listener : listeners) { 393 listener.onLoadError(eventTime, loadEventInfo, mediaLoadData, error, wasCanceled); 394 } 395 } 396 397 @Override onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId)398 public final void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) { 399 mediaPeriodQueueTracker.onReadingStarted(mediaPeriodId); 400 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 401 for (AnalyticsListener listener : listeners) { 402 listener.onReadingStarted(eventTime); 403 } 404 } 405 406 @Override onUpstreamDiscarded( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)407 public final void onUpstreamDiscarded( 408 int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { 409 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 410 for (AnalyticsListener listener : listeners) { 411 listener.onUpstreamDiscarded(eventTime, mediaLoadData); 412 } 413 } 414 415 @Override onDownstreamFormatChanged( int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData)416 public final void onDownstreamFormatChanged( 417 int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) { 418 EventTime eventTime = generateMediaPeriodEventTime(windowIndex, mediaPeriodId); 419 for (AnalyticsListener listener : listeners) { 420 listener.onDownstreamFormatChanged(eventTime, mediaLoadData); 421 } 422 } 423 424 // Player.EventListener implementation. 425 426 // TODO: Add onFinishedReportingChanges to Player.EventListener to know when a set of simultaneous 427 // callbacks finished. This helps to assign exactly the same EventTime to all of them instead of 428 // having slightly different real times. 429 430 @Override onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason)431 public final void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) { 432 mediaPeriodQueueTracker.onTimelineChanged(timeline, Assertions.checkNotNull(player)); 433 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 434 for (AnalyticsListener listener : listeners) { 435 listener.onTimelineChanged(eventTime, reason); 436 } 437 } 438 439 @Override onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections)440 public final void onTracksChanged( 441 TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { 442 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 443 for (AnalyticsListener listener : listeners) { 444 listener.onTracksChanged(eventTime, trackGroups, trackSelections); 445 } 446 } 447 448 @Override onIsLoadingChanged(boolean isLoading)449 public final void onIsLoadingChanged(boolean isLoading) { 450 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 451 for (AnalyticsListener listener : listeners) { 452 listener.onIsLoadingChanged(eventTime, isLoading); 453 } 454 } 455 456 @SuppressWarnings("deprecation") 457 @Override onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState)458 public final void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) { 459 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 460 for (AnalyticsListener listener : listeners) { 461 listener.onPlayerStateChanged(eventTime, playWhenReady, playbackState); 462 } 463 } 464 465 @Override onPlaybackStateChanged(@layer.State int state)466 public final void onPlaybackStateChanged(@Player.State int state) { 467 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 468 for (AnalyticsListener listener : listeners) { 469 listener.onPlaybackStateChanged(eventTime, state); 470 } 471 } 472 473 @Override onPlayWhenReadyChanged( boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)474 public final void onPlayWhenReadyChanged( 475 boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { 476 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 477 for (AnalyticsListener listener : listeners) { 478 listener.onPlayWhenReadyChanged(eventTime, playWhenReady, reason); 479 } 480 } 481 482 @Override onPlaybackSuppressionReasonChanged( @laybackSuppressionReason int playbackSuppressionReason)483 public void onPlaybackSuppressionReasonChanged( 484 @PlaybackSuppressionReason int playbackSuppressionReason) { 485 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 486 for (AnalyticsListener listener : listeners) { 487 listener.onPlaybackSuppressionReasonChanged(eventTime, playbackSuppressionReason); 488 } 489 } 490 491 @Override onIsPlayingChanged(boolean isPlaying)492 public void onIsPlayingChanged(boolean isPlaying) { 493 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 494 for (AnalyticsListener listener : listeners) { 495 listener.onIsPlayingChanged(eventTime, isPlaying); 496 } 497 } 498 499 @Override onRepeatModeChanged(@layer.RepeatMode int repeatMode)500 public final void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { 501 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 502 for (AnalyticsListener listener : listeners) { 503 listener.onRepeatModeChanged(eventTime, repeatMode); 504 } 505 } 506 507 @Override onShuffleModeEnabledChanged(boolean shuffleModeEnabled)508 public final void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { 509 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 510 for (AnalyticsListener listener : listeners) { 511 listener.onShuffleModeChanged(eventTime, shuffleModeEnabled); 512 } 513 } 514 515 @Override onPlayerError(ExoPlaybackException error)516 public final void onPlayerError(ExoPlaybackException error) { 517 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 518 for (AnalyticsListener listener : listeners) { 519 listener.onPlayerError(eventTime, error); 520 } 521 } 522 523 @Override onPositionDiscontinuity(@layer.DiscontinuityReason int reason)524 public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { 525 if (reason == Player.DISCONTINUITY_REASON_SEEK) { 526 isSeeking = false; 527 } 528 mediaPeriodQueueTracker.onPositionDiscontinuity(Assertions.checkNotNull(player)); 529 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 530 for (AnalyticsListener listener : listeners) { 531 listener.onPositionDiscontinuity(eventTime, reason); 532 } 533 } 534 535 /** 536 * @deprecated Use {@link #onPlaybackSpeedChanged(float)} and {@link 537 * #onSkipSilenceEnabledChanged(boolean)} instead. 538 */ 539 @SuppressWarnings("deprecation") 540 @Deprecated 541 @Override onPlaybackParametersChanged(PlaybackParameters playbackParameters)542 public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { 543 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 544 for (AnalyticsListener listener : listeners) { 545 listener.onPlaybackParametersChanged(eventTime, playbackParameters); 546 } 547 } 548 549 @Override onPlaybackSpeedChanged(float playbackSpeed)550 public void onPlaybackSpeedChanged(float playbackSpeed) { 551 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 552 for (AnalyticsListener listener : listeners) { 553 listener.onPlaybackSpeedChanged(eventTime, playbackSpeed); 554 } 555 } 556 557 @SuppressWarnings("deprecation") 558 @Override onSeekProcessed()559 public final void onSeekProcessed() { 560 EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); 561 for (AnalyticsListener listener : listeners) { 562 listener.onSeekProcessed(eventTime); 563 } 564 } 565 566 // BandwidthMeter.Listener implementation. 567 568 @Override onBandwidthSample(int elapsedMs, long bytes, long bitrate)569 public final void onBandwidthSample(int elapsedMs, long bytes, long bitrate) { 570 EventTime eventTime = generateLoadingMediaPeriodEventTime(); 571 for (AnalyticsListener listener : listeners) { 572 listener.onBandwidthEstimate(eventTime, elapsedMs, bytes, bitrate); 573 } 574 } 575 576 // DefaultDrmSessionManager.EventListener implementation. 577 578 @Override onDrmSessionAcquired()579 public final void onDrmSessionAcquired() { 580 EventTime eventTime = generateReadingMediaPeriodEventTime(); 581 for (AnalyticsListener listener : listeners) { 582 listener.onDrmSessionAcquired(eventTime); 583 } 584 } 585 586 @Override onDrmKeysLoaded()587 public final void onDrmKeysLoaded() { 588 EventTime eventTime = generateReadingMediaPeriodEventTime(); 589 for (AnalyticsListener listener : listeners) { 590 listener.onDrmKeysLoaded(eventTime); 591 } 592 } 593 594 @Override onDrmSessionManagerError(Exception error)595 public final void onDrmSessionManagerError(Exception error) { 596 EventTime eventTime = generateReadingMediaPeriodEventTime(); 597 for (AnalyticsListener listener : listeners) { 598 listener.onDrmSessionManagerError(eventTime, error); 599 } 600 } 601 602 @Override onDrmKeysRestored()603 public final void onDrmKeysRestored() { 604 EventTime eventTime = generateReadingMediaPeriodEventTime(); 605 for (AnalyticsListener listener : listeners) { 606 listener.onDrmKeysRestored(eventTime); 607 } 608 } 609 610 @Override onDrmKeysRemoved()611 public final void onDrmKeysRemoved() { 612 EventTime eventTime = generateReadingMediaPeriodEventTime(); 613 for (AnalyticsListener listener : listeners) { 614 listener.onDrmKeysRemoved(eventTime); 615 } 616 } 617 618 @Override onDrmSessionReleased()619 public final void onDrmSessionReleased() { 620 EventTime eventTime = generatePlayingMediaPeriodEventTime(); 621 for (AnalyticsListener listener : listeners) { 622 listener.onDrmSessionReleased(eventTime); 623 } 624 } 625 626 // Internal methods. 627 628 /** Returns read-only set of registered listeners. */ getListeners()629 protected Set<AnalyticsListener> getListeners() { 630 return Collections.unmodifiableSet(listeners); 631 } 632 633 /** Returns a new {@link EventTime} for the specified timeline, window and media period id. */ 634 @RequiresNonNull("player") generateEventTime( Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId)635 protected EventTime generateEventTime( 636 Timeline timeline, int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { 637 if (timeline.isEmpty()) { 638 // Ensure media period id is only reported together with a valid timeline. 639 mediaPeriodId = null; 640 } 641 long realtimeMs = clock.elapsedRealtime(); 642 long eventPositionMs; 643 boolean isInCurrentWindow = 644 timeline.equals(player.getCurrentTimeline()) 645 && windowIndex == player.getCurrentWindowIndex(); 646 if (mediaPeriodId != null && mediaPeriodId.isAd()) { 647 boolean isCurrentAd = 648 isInCurrentWindow 649 && player.getCurrentAdGroupIndex() == mediaPeriodId.adGroupIndex 650 && player.getCurrentAdIndexInAdGroup() == mediaPeriodId.adIndexInAdGroup; 651 // Assume start position of 0 for future ads. 652 eventPositionMs = isCurrentAd ? player.getCurrentPosition() : 0; 653 } else if (isInCurrentWindow) { 654 eventPositionMs = player.getContentPosition(); 655 } else { 656 // Assume default start position for future content windows. If timeline is not available yet, 657 // assume start position of 0. 658 eventPositionMs = 659 timeline.isEmpty() ? 0 : timeline.getWindow(windowIndex, window).getDefaultPositionMs(); 660 } 661 return new EventTime( 662 realtimeMs, 663 timeline, 664 windowIndex, 665 mediaPeriodId, 666 eventPositionMs, 667 player.getCurrentPosition(), 668 player.getTotalBufferedDuration()); 669 } 670 generateEventTime(@ullable MediaPeriodInfo mediaPeriodInfo)671 private EventTime generateEventTime(@Nullable MediaPeriodInfo mediaPeriodInfo) { 672 Assertions.checkNotNull(player); 673 if (mediaPeriodInfo == null) { 674 int windowIndex = player.getCurrentWindowIndex(); 675 Timeline timeline = player.getCurrentTimeline(); 676 boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); 677 return generateEventTime( 678 windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); 679 } 680 return generateEventTime( 681 mediaPeriodInfo.timeline, mediaPeriodInfo.windowIndex, mediaPeriodInfo.mediaPeriodId); 682 } 683 684 private EventTime generateCurrentPlayerMediaPeriodEventTime() { 685 return generateEventTime(mediaPeriodQueueTracker.getCurrentPlayerMediaPeriod()); 686 } 687 688 private EventTime generatePlayingMediaPeriodEventTime() { 689 return generateEventTime(mediaPeriodQueueTracker.getPlayingMediaPeriod()); 690 } 691 692 private EventTime generateReadingMediaPeriodEventTime() { 693 return generateEventTime(mediaPeriodQueueTracker.getReadingMediaPeriod()); 694 } 695 696 private EventTime generateLoadingMediaPeriodEventTime() { 697 return generateEventTime(mediaPeriodQueueTracker.getLoadingMediaPeriod()); 698 } 699 700 private EventTime generateMediaPeriodEventTime( 701 int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { 702 Assertions.checkNotNull(player); 703 if (mediaPeriodId != null) { 704 MediaPeriodInfo mediaPeriodInfo = mediaPeriodQueueTracker.getMediaPeriodInfo(mediaPeriodId); 705 return mediaPeriodInfo != null 706 ? generateEventTime(mediaPeriodInfo) 707 : generateEventTime(Timeline.EMPTY, windowIndex, mediaPeriodId); 708 } 709 Timeline timeline = player.getCurrentTimeline(); 710 boolean windowIsInTimeline = windowIndex < timeline.getWindowCount(); 711 return generateEventTime( 712 windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); 713 } 714 715 /** Keeps track of the active media periods and currently playing and reading media period. */ 716 private static final class MediaPeriodQueueTracker { 717 718 // TODO: Investigate reporting MediaPeriodId in renderer events and adding a listener of queue 719 // changes, which would hopefully remove the need to track the queue here. 720 721 private final ArrayList<MediaPeriodInfo> mediaPeriodInfoQueue; 722 private final HashMap<MediaPeriodId, MediaPeriodInfo> mediaPeriodIdToInfo; 723 private final Period period; 724 725 @Nullable private MediaPeriodInfo currentPlayerMediaPeriod; 726 private @MonotonicNonNull MediaPeriodInfo playingMediaPeriod; 727 private @MonotonicNonNull MediaPeriodInfo readingMediaPeriod; 728 private Timeline timeline; 729 730 public MediaPeriodQueueTracker() { 731 mediaPeriodInfoQueue = new ArrayList<>(); 732 mediaPeriodIdToInfo = new HashMap<>(); 733 period = new Period(); 734 timeline = Timeline.EMPTY; 735 } 736 737 /** 738 * Returns the {@link MediaPeriodInfo} of the media period corresponding the current position of 739 * the player. 740 * 741 * <p>May be null if no matching media period has been created yet. 742 */ 743 @Nullable 744 public MediaPeriodInfo getCurrentPlayerMediaPeriod() { 745 return currentPlayerMediaPeriod; 746 } 747 748 /** 749 * Returns the {@link MediaPeriodInfo} of the media period at the front of the queue. If the 750 * queue is empty, this is the last media period which was at the front of the queue. 751 * 752 * <p>May be null, if no media period has been created yet. 753 */ 754 @Nullable 755 public MediaPeriodInfo getPlayingMediaPeriod() { 756 return playingMediaPeriod; 757 } 758 759 /** 760 * Returns the {@link MediaPeriodInfo} of the media period currently being read by the player. 761 * 762 * <p>May be null, if the player has not started reading any media period. 763 */ 764 @Nullable 765 public MediaPeriodInfo getReadingMediaPeriod() { 766 return readingMediaPeriod; 767 } 768 769 /** 770 * Returns the {@link MediaPeriodInfo} of the media period at the end of the queue which is 771 * currently loading or will be the next one loading. 772 * 773 * <p>May be null, if no media period is active yet. 774 */ 775 @Nullable 776 public MediaPeriodInfo getLoadingMediaPeriod() { 777 return mediaPeriodInfoQueue.isEmpty() 778 ? null 779 : mediaPeriodInfoQueue.get(mediaPeriodInfoQueue.size() - 1); 780 } 781 782 /** Returns the {@link MediaPeriodInfo} for the given {@link MediaPeriodId}. */ 783 @Nullable 784 public MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId mediaPeriodId) { 785 return mediaPeriodIdToInfo.get(mediaPeriodId); 786 } 787 788 /** Updates the queue with a reported position discontinuity. */ 789 public void onPositionDiscontinuity(Player player) { 790 currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); 791 } 792 793 /** Updates the queue with a reported timeline change. */ 794 public void onTimelineChanged(Timeline timeline, Player player) { 795 for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { 796 MediaPeriodInfo newMediaPeriodInfo = 797 updateMediaPeriodInfoToNewTimeline(mediaPeriodInfoQueue.get(i), timeline); 798 mediaPeriodInfoQueue.set(i, newMediaPeriodInfo); 799 mediaPeriodIdToInfo.put(newMediaPeriodInfo.mediaPeriodId, newMediaPeriodInfo); 800 } 801 if (!mediaPeriodInfoQueue.isEmpty()) { 802 playingMediaPeriod = mediaPeriodInfoQueue.get(0); 803 } else if (playingMediaPeriod != null) { 804 playingMediaPeriod = updateMediaPeriodInfoToNewTimeline(playingMediaPeriod, timeline); 805 } 806 if (readingMediaPeriod != null) { 807 readingMediaPeriod = updateMediaPeriodInfoToNewTimeline(readingMediaPeriod, timeline); 808 } else if (playingMediaPeriod != null) { 809 readingMediaPeriod = playingMediaPeriod; 810 } 811 this.timeline = timeline; 812 currentPlayerMediaPeriod = findMatchingMediaPeriodInQueue(player); 813 } 814 815 /** Updates the queue with a newly created media period. */ 816 public void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId, Player player) { 817 int periodIndex = timeline.getIndexOfPeriod(mediaPeriodId.periodUid); 818 boolean isInTimeline = periodIndex != C.INDEX_UNSET; 819 MediaPeriodInfo mediaPeriodInfo = 820 new MediaPeriodInfo( 821 mediaPeriodId, 822 isInTimeline ? timeline : Timeline.EMPTY, 823 isInTimeline ? timeline.getPeriod(periodIndex, period).windowIndex : windowIndex); 824 mediaPeriodInfoQueue.add(mediaPeriodInfo); 825 mediaPeriodIdToInfo.put(mediaPeriodId, mediaPeriodInfo); 826 playingMediaPeriod = mediaPeriodInfoQueue.get(0); 827 if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { 828 currentPlayerMediaPeriod = playingMediaPeriod; 829 } 830 if (mediaPeriodInfoQueue.size() == 1) { 831 readingMediaPeriod = playingMediaPeriod; 832 } 833 } 834 835 /** 836 * Updates the queue with a released media period. Returns whether the media period was still in 837 * the queue. 838 */ 839 public boolean onMediaPeriodReleased(MediaPeriodId mediaPeriodId, Player player) { 840 @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.remove(mediaPeriodId); 841 if (mediaPeriodInfo == null) { 842 // The media period has already been removed from the queue in resetForNewPlaylist(). 843 return false; 844 } 845 mediaPeriodInfoQueue.remove(mediaPeriodInfo); 846 if (readingMediaPeriod != null && mediaPeriodId.equals(readingMediaPeriod.mediaPeriodId)) { 847 readingMediaPeriod = 848 mediaPeriodInfoQueue.isEmpty() 849 ? Assertions.checkNotNull(playingMediaPeriod) 850 : mediaPeriodInfoQueue.get(0); 851 } 852 if (!mediaPeriodInfoQueue.isEmpty()) { 853 playingMediaPeriod = mediaPeriodInfoQueue.get(0); 854 } 855 if (currentPlayerMediaPeriod == null && isMatchingPlayingMediaPeriod(player)) { 856 currentPlayerMediaPeriod = playingMediaPeriod; 857 } 858 return true; 859 } 860 861 /** Update the queue with a change in the reading media period. */ 862 public void onReadingStarted(MediaPeriodId mediaPeriodId) { 863 @Nullable MediaPeriodInfo mediaPeriodInfo = mediaPeriodIdToInfo.get(mediaPeriodId); 864 if (mediaPeriodInfo == null) { 865 // The media period has already been removed from the queue in resetForNewPlaylist(). 866 return; 867 } 868 readingMediaPeriod = mediaPeriodInfo; 869 } 870 871 @Nullable 872 private MediaPeriodInfo findMatchingMediaPeriodInQueue(Player player) { 873 Timeline playerTimeline = player.getCurrentTimeline(); 874 int playerPeriodIndex = player.getCurrentPeriodIndex(); 875 @Nullable 876 Object playerPeriodUid = 877 playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); 878 int playerNextAdGroupIndex = 879 player.isPlayingAd() || playerTimeline.isEmpty() 880 ? C.INDEX_UNSET 881 : playerTimeline 882 .getPeriod(playerPeriodIndex, period) 883 .getAdGroupIndexAfterPositionUs( 884 C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs()); 885 for (int i = 0; i < mediaPeriodInfoQueue.size(); i++) { 886 MediaPeriodInfo mediaPeriodInfo = mediaPeriodInfoQueue.get(i); 887 if (isMatchingMediaPeriod( 888 mediaPeriodInfo, 889 playerTimeline, 890 player.getCurrentWindowIndex(), 891 playerPeriodUid, 892 player.isPlayingAd(), 893 player.getCurrentAdGroupIndex(), 894 player.getCurrentAdIndexInAdGroup(), 895 playerNextAdGroupIndex)) { 896 return mediaPeriodInfo; 897 } 898 } 899 if (mediaPeriodInfoQueue.isEmpty() && playingMediaPeriod != null) { 900 if (isMatchingMediaPeriod( 901 playingMediaPeriod, 902 playerTimeline, 903 player.getCurrentWindowIndex(), 904 playerPeriodUid, 905 player.isPlayingAd(), 906 player.getCurrentAdGroupIndex(), 907 player.getCurrentAdIndexInAdGroup(), 908 playerNextAdGroupIndex)) { 909 return playingMediaPeriod; 910 } 911 } 912 return null; 913 } 914 915 private boolean isMatchingPlayingMediaPeriod(Player player) { 916 if (playingMediaPeriod == null) { 917 return false; 918 } 919 Timeline playerTimeline = player.getCurrentTimeline(); 920 int playerPeriodIndex = player.getCurrentPeriodIndex(); 921 @Nullable 922 Object playerPeriodUid = 923 playerTimeline.isEmpty() ? null : playerTimeline.getUidOfPeriod(playerPeriodIndex); 924 int playerNextAdGroupIndex = 925 player.isPlayingAd() || playerTimeline.isEmpty() 926 ? C.INDEX_UNSET 927 : playerTimeline 928 .getPeriod(playerPeriodIndex, period) 929 .getAdGroupIndexAfterPositionUs( 930 C.msToUs(player.getCurrentPosition()) - period.getPositionInWindowUs()); 931 return isMatchingMediaPeriod( 932 playingMediaPeriod, 933 playerTimeline, 934 player.getCurrentWindowIndex(), 935 playerPeriodUid, 936 player.isPlayingAd(), 937 player.getCurrentAdGroupIndex(), 938 player.getCurrentAdIndexInAdGroup(), 939 playerNextAdGroupIndex); 940 } 941 942 private static boolean isMatchingMediaPeriod( 943 MediaPeriodInfo mediaPeriodInfo, 944 Timeline playerTimeline, 945 int playerWindowIndex, 946 @Nullable Object playerPeriodUid, 947 boolean isPlayingAd, 948 int playerAdGroupIndex, 949 int playerAdIndexInAdGroup, 950 int playerNextAdGroupIndex) { 951 if (mediaPeriodInfo.timeline.isEmpty() 952 || !mediaPeriodInfo.timeline.equals(playerTimeline) 953 || mediaPeriodInfo.windowIndex != playerWindowIndex 954 || !mediaPeriodInfo.mediaPeriodId.periodUid.equals(playerPeriodUid)) { 955 return false; 956 } 957 // Timeline period matches. Still need to check ad information. 958 return (isPlayingAd 959 && mediaPeriodInfo.mediaPeriodId.adGroupIndex == playerAdGroupIndex 960 && mediaPeriodInfo.mediaPeriodId.adIndexInAdGroup == playerAdIndexInAdGroup) 961 || (!isPlayingAd 962 && mediaPeriodInfo.mediaPeriodId.adGroupIndex == C.INDEX_UNSET 963 && mediaPeriodInfo.mediaPeriodId.nextAdGroupIndex == playerNextAdGroupIndex); 964 } 965 966 private MediaPeriodInfo updateMediaPeriodInfoToNewTimeline( 967 MediaPeriodInfo info, Timeline newTimeline) { 968 int newPeriodIndex = newTimeline.getIndexOfPeriod(info.mediaPeriodId.periodUid); 969 if (newPeriodIndex == C.INDEX_UNSET) { 970 // Media period is not yet or no longer available in the new timeline. Keep it as it is. 971 return info; 972 } 973 int newWindowIndex = newTimeline.getPeriod(newPeriodIndex, period).windowIndex; 974 return new MediaPeriodInfo(info.mediaPeriodId, newTimeline, newWindowIndex); 975 } 976 } 977 978 /** Information about a media period and its associated timeline. */ 979 private static final class MediaPeriodInfo { 980 981 /** The {@link MediaPeriodId} of the media period. */ 982 public final MediaPeriodId mediaPeriodId; 983 /** 984 * The {@link Timeline} in which the media period can be found. Or {@link Timeline#EMPTY} if the 985 * media period is not part of a known timeline yet. 986 */ 987 public final Timeline timeline; 988 /** 989 * The window index of the media period in the timeline. If the timeline is empty, this is the 990 * prospective window index. 991 */ 992 public final int windowIndex; 993 994 public MediaPeriodInfo(MediaPeriodId mediaPeriodId, Timeline timeline, int windowIndex) { 995 this.mediaPeriodId = mediaPeriodId; 996 this.timeline = timeline; 997 this.windowIndex = windowIndex; 998 } 999 } 1000 } 1001