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.os.SystemClock; 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.Player; 24 import com.google.android.exoplayer2.Timeline; 25 import com.google.android.exoplayer2.Timeline.Period; 26 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndException; 27 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndFormat; 28 import com.google.android.exoplayer2.analytics.PlaybackStats.EventTimeAndPlaybackState; 29 import com.google.android.exoplayer2.analytics.PlaybackStats.PlaybackState; 30 import com.google.android.exoplayer2.source.LoadEventInfo; 31 import com.google.android.exoplayer2.source.MediaLoadData; 32 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 33 import com.google.android.exoplayer2.source.TrackGroupArray; 34 import com.google.android.exoplayer2.trackselection.TrackSelection; 35 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 36 import com.google.android.exoplayer2.util.Assertions; 37 import com.google.android.exoplayer2.util.MimeTypes; 38 import com.google.android.exoplayer2.util.Util; 39 import java.io.IOException; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 47 /** 48 * {@link AnalyticsListener} to gather {@link PlaybackStats} from the player. 49 * 50 * <p>For accurate measurements, the listener should be added to the player before loading media, 51 * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. 52 * 53 * <p>Playback stats are gathered separately for each playback session, i.e. each window in the 54 * {@link Timeline} and each single ad. 55 */ 56 public final class PlaybackStatsListener 57 implements AnalyticsListener, PlaybackSessionManager.Listener { 58 59 /** A listener for {@link PlaybackStats} updates. */ 60 public interface Callback { 61 62 /** 63 * Called when a playback session ends and its {@link PlaybackStats} are ready. 64 * 65 * @param eventTime The {@link EventTime} at which the playback session started. Can be used to 66 * identify the playback session. 67 * @param playbackStats The {@link PlaybackStats} for the ended playback session. 68 */ onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats)69 void onPlaybackStatsReady(EventTime eventTime, PlaybackStats playbackStats); 70 } 71 72 private final PlaybackSessionManager sessionManager; 73 private final Map<String, PlaybackStatsTracker> playbackStatsTrackers; 74 private final Map<String, EventTime> sessionStartEventTimes; 75 @Nullable private final Callback callback; 76 private final boolean keepHistory; 77 private final Period period; 78 79 private PlaybackStats finishedPlaybackStats; 80 @Nullable private String activeContentPlayback; 81 @Nullable private String activeAdPlayback; 82 private boolean playWhenReady; 83 @Player.State private int playbackState; 84 private boolean isSuppressed; 85 private float playbackSpeed; 86 private boolean onSeekStartedCalled; 87 88 /** 89 * Creates listener for playback stats. 90 * 91 * @param keepHistory Whether the reported {@link PlaybackStats} should keep the full history of 92 * events. 93 * @param callback An optional callback for finished {@link PlaybackStats}. 94 */ PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback)95 public PlaybackStatsListener(boolean keepHistory, @Nullable Callback callback) { 96 this.callback = callback; 97 this.keepHistory = keepHistory; 98 sessionManager = new DefaultPlaybackSessionManager(); 99 playbackStatsTrackers = new HashMap<>(); 100 sessionStartEventTimes = new HashMap<>(); 101 finishedPlaybackStats = PlaybackStats.EMPTY; 102 playWhenReady = false; 103 playbackState = Player.STATE_IDLE; 104 playbackSpeed = 1f; 105 period = new Period(); 106 sessionManager.setListener(this); 107 } 108 109 /** 110 * Returns the combined {@link PlaybackStats} for all playback sessions this listener was and is 111 * listening to. 112 * 113 * <p>Note that these {@link PlaybackStats} will not contain the full history of events. 114 * 115 * @return The combined {@link PlaybackStats} for all playback sessions. 116 */ getCombinedPlaybackStats()117 public PlaybackStats getCombinedPlaybackStats() { 118 PlaybackStats[] allPendingPlaybackStats = new PlaybackStats[playbackStatsTrackers.size() + 1]; 119 allPendingPlaybackStats[0] = finishedPlaybackStats; 120 int index = 1; 121 for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { 122 allPendingPlaybackStats[index++] = tracker.build(/* isFinal= */ false); 123 } 124 return PlaybackStats.merge(allPendingPlaybackStats); 125 } 126 127 /** 128 * Returns the {@link PlaybackStats} for the currently playback session, or null if no session is 129 * active. 130 * 131 * @return {@link PlaybackStats} for the current playback session. 132 */ 133 @Nullable getPlaybackStats()134 public PlaybackStats getPlaybackStats() { 135 @Nullable 136 PlaybackStatsTracker activeStatsTracker = 137 activeAdPlayback != null 138 ? playbackStatsTrackers.get(activeAdPlayback) 139 : activeContentPlayback != null 140 ? playbackStatsTrackers.get(activeContentPlayback) 141 : null; 142 return activeStatsTracker == null ? null : activeStatsTracker.build(/* isFinal= */ false); 143 } 144 145 /** 146 * Finishes all pending playback sessions. Should be called when the listener is removed from the 147 * player or when the player is released. 148 */ finishAllSessions()149 public void finishAllSessions() { 150 // TODO: Add AnalyticsListener.onAttachedToPlayer and onDetachedFromPlayer to auto-release with 151 // an actual EventTime. Should also simplify other cases where the listener needs to be released 152 // separately from the player. 153 EventTime dummyEventTime = 154 new EventTime( 155 SystemClock.elapsedRealtime(), 156 Timeline.EMPTY, 157 /* windowIndex= */ 0, 158 /* mediaPeriodId= */ null, 159 /* eventPlaybackPositionMs= */ 0, 160 /* currentPlaybackPositionMs= */ 0, 161 /* totalBufferedDurationMs= */ 0); 162 sessionManager.finishAllSessions(dummyEventTime); 163 } 164 165 // PlaybackSessionManager.Listener implementation. 166 167 @Override onSessionCreated(EventTime eventTime, String session)168 public void onSessionCreated(EventTime eventTime, String session) { 169 PlaybackStatsTracker tracker = new PlaybackStatsTracker(keepHistory, eventTime); 170 if (onSeekStartedCalled) { 171 tracker.onSeekStarted(eventTime, /* belongsToPlayback= */ true); 172 } 173 tracker.onPlaybackStateChanged(eventTime, playbackState, /* belongsToPlayback= */ true); 174 tracker.onPlayWhenReadyChanged(eventTime, playWhenReady, /* belongsToPlayback= */ true); 175 tracker.onIsSuppressedChanged(eventTime, isSuppressed, /* belongsToPlayback= */ true); 176 tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); 177 playbackStatsTrackers.put(session, tracker); 178 sessionStartEventTimes.put(session, eventTime); 179 } 180 181 @Override onSessionActive(EventTime eventTime, String session)182 public void onSessionActive(EventTime eventTime, String session) { 183 Assertions.checkNotNull(playbackStatsTrackers.get(session)).onForeground(eventTime); 184 if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) { 185 activeAdPlayback = session; 186 } else { 187 activeContentPlayback = session; 188 } 189 } 190 191 @Override onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession)192 public void onAdPlaybackStarted(EventTime eventTime, String contentSession, String adSession) { 193 Assertions.checkState(Assertions.checkNotNull(eventTime.mediaPeriodId).isAd()); 194 long contentPeriodPositionUs = 195 eventTime 196 .timeline 197 .getPeriodByUid(eventTime.mediaPeriodId.periodUid, period) 198 .getAdGroupTimeUs(eventTime.mediaPeriodId.adGroupIndex); 199 long contentWindowPositionUs = 200 contentPeriodPositionUs == C.TIME_END_OF_SOURCE 201 ? C.TIME_END_OF_SOURCE 202 : contentPeriodPositionUs + period.getPositionInWindowUs(); 203 EventTime contentEventTime = 204 new EventTime( 205 eventTime.realtimeMs, 206 eventTime.timeline, 207 eventTime.windowIndex, 208 new MediaPeriodId( 209 eventTime.mediaPeriodId.periodUid, 210 eventTime.mediaPeriodId.windowSequenceNumber, 211 eventTime.mediaPeriodId.adGroupIndex), 212 /* eventPlaybackPositionMs= */ C.usToMs(contentWindowPositionUs), 213 eventTime.currentPlaybackPositionMs, 214 eventTime.totalBufferedDurationMs); 215 Assertions.checkNotNull(playbackStatsTrackers.get(contentSession)) 216 .onInterruptedByAd(contentEventTime); 217 } 218 219 @Override onSessionFinished(EventTime eventTime, String session, boolean automaticTransition)220 public void onSessionFinished(EventTime eventTime, String session, boolean automaticTransition) { 221 if (session.equals(activeAdPlayback)) { 222 activeAdPlayback = null; 223 } else if (session.equals(activeContentPlayback)) { 224 activeContentPlayback = null; 225 } 226 PlaybackStatsTracker tracker = Assertions.checkNotNull(playbackStatsTrackers.remove(session)); 227 EventTime startEventTime = Assertions.checkNotNull(sessionStartEventTimes.remove(session)); 228 if (automaticTransition) { 229 // Simulate ENDED state to record natural ending of playback. 230 tracker.onPlaybackStateChanged(eventTime, Player.STATE_ENDED, /* belongsToPlayback= */ false); 231 } 232 tracker.onFinished(eventTime); 233 PlaybackStats playbackStats = tracker.build(/* isFinal= */ true); 234 finishedPlaybackStats = PlaybackStats.merge(finishedPlaybackStats, playbackStats); 235 if (callback != null) { 236 callback.onPlaybackStatsReady(startEventTime, playbackStats); 237 } 238 } 239 240 // AnalyticsListener implementation. 241 242 @Override onPlaybackStateChanged(EventTime eventTime, @Player.State int state)243 public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) { 244 playbackState = state; 245 maybeAddSession(eventTime); 246 for (String session : playbackStatsTrackers.keySet()) { 247 boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); 248 playbackStatsTrackers 249 .get(session) 250 .onPlaybackStateChanged(eventTime, playbackState, belongsToPlayback); 251 } 252 } 253 254 @Override onPlayWhenReadyChanged( EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason)255 public void onPlayWhenReadyChanged( 256 EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { 257 this.playWhenReady = playWhenReady; 258 maybeAddSession(eventTime); 259 for (String session : playbackStatsTrackers.keySet()) { 260 boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); 261 playbackStatsTrackers 262 .get(session) 263 .onPlayWhenReadyChanged(eventTime, playWhenReady, belongsToPlayback); 264 } 265 } 266 267 @Override onPlaybackSuppressionReasonChanged( EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason)268 public void onPlaybackSuppressionReasonChanged( 269 EventTime eventTime, @Player.PlaybackSuppressionReason int playbackSuppressionReason) { 270 isSuppressed = playbackSuppressionReason != Player.PLAYBACK_SUPPRESSION_REASON_NONE; 271 maybeAddSession(eventTime); 272 for (String session : playbackStatsTrackers.keySet()) { 273 boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); 274 playbackStatsTrackers 275 .get(session) 276 .onIsSuppressedChanged(eventTime, isSuppressed, belongsToPlayback); 277 } 278 } 279 280 @Override onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason)281 public void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) { 282 sessionManager.handleTimelineUpdate(eventTime); 283 maybeAddSession(eventTime); 284 for (String session : playbackStatsTrackers.keySet()) { 285 if (sessionManager.belongsToSession(eventTime, session)) { 286 playbackStatsTrackers.get(session).onPositionDiscontinuity(eventTime, /* isSeek= */ false); 287 } 288 } 289 } 290 291 @Override onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason)292 public void onPositionDiscontinuity(EventTime eventTime, @Player.DiscontinuityReason int reason) { 293 sessionManager.handlePositionDiscontinuity(eventTime, reason); 294 maybeAddSession(eventTime); 295 if (reason == Player.DISCONTINUITY_REASON_SEEK) { 296 onSeekStartedCalled = false; 297 } 298 for (String session : playbackStatsTrackers.keySet()) { 299 if (sessionManager.belongsToSession(eventTime, session)) { 300 playbackStatsTrackers 301 .get(session) 302 .onPositionDiscontinuity( 303 eventTime, /* isSeek= */ reason == Player.DISCONTINUITY_REASON_SEEK); 304 } 305 } 306 } 307 308 @Override onSeekStarted(EventTime eventTime)309 public void onSeekStarted(EventTime eventTime) { 310 maybeAddSession(eventTime); 311 for (String session : playbackStatsTrackers.keySet()) { 312 boolean belongsToPlayback = sessionManager.belongsToSession(eventTime, session); 313 playbackStatsTrackers.get(session).onSeekStarted(eventTime, belongsToPlayback); 314 } 315 onSeekStartedCalled = true; 316 } 317 318 @Override onPlayerError(EventTime eventTime, ExoPlaybackException error)319 public void onPlayerError(EventTime eventTime, ExoPlaybackException error) { 320 maybeAddSession(eventTime); 321 for (String session : playbackStatsTrackers.keySet()) { 322 if (sessionManager.belongsToSession(eventTime, session)) { 323 playbackStatsTrackers.get(session).onFatalError(eventTime, error); 324 } 325 } 326 } 327 328 @Override onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed)329 public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { 330 this.playbackSpeed = playbackSpeed; 331 maybeAddSession(eventTime); 332 for (PlaybackStatsTracker tracker : playbackStatsTrackers.values()) { 333 tracker.onPlaybackSpeedChanged(eventTime, playbackSpeed); 334 } 335 } 336 337 @Override onTracksChanged( EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections)338 public void onTracksChanged( 339 EventTime eventTime, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { 340 maybeAddSession(eventTime); 341 for (String session : playbackStatsTrackers.keySet()) { 342 if (sessionManager.belongsToSession(eventTime, session)) { 343 playbackStatsTrackers.get(session).onTracksChanged(eventTime, trackSelections); 344 } 345 } 346 } 347 348 @Override onLoadStarted( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData)349 public void onLoadStarted( 350 EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) { 351 maybeAddSession(eventTime); 352 for (String session : playbackStatsTrackers.keySet()) { 353 if (sessionManager.belongsToSession(eventTime, session)) { 354 playbackStatsTrackers.get(session).onLoadStarted(eventTime); 355 } 356 } 357 } 358 359 @Override onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData)360 public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { 361 maybeAddSession(eventTime); 362 for (String session : playbackStatsTrackers.keySet()) { 363 if (sessionManager.belongsToSession(eventTime, session)) { 364 playbackStatsTrackers.get(session).onDownstreamFormatChanged(eventTime, mediaLoadData); 365 } 366 } 367 } 368 369 @Override onVideoSizeChanged( EventTime eventTime, int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio)370 public void onVideoSizeChanged( 371 EventTime eventTime, 372 int width, 373 int height, 374 int unappliedRotationDegrees, 375 float pixelWidthHeightRatio) { 376 maybeAddSession(eventTime); 377 for (String session : playbackStatsTrackers.keySet()) { 378 if (sessionManager.belongsToSession(eventTime, session)) { 379 playbackStatsTrackers.get(session).onVideoSizeChanged(eventTime, width, height); 380 } 381 } 382 } 383 384 @Override onBandwidthEstimate( EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate)385 public void onBandwidthEstimate( 386 EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) { 387 maybeAddSession(eventTime); 388 for (String session : playbackStatsTrackers.keySet()) { 389 if (sessionManager.belongsToSession(eventTime, session)) { 390 playbackStatsTrackers.get(session).onBandwidthData(totalLoadTimeMs, totalBytesLoaded); 391 } 392 } 393 } 394 395 @Override onAudioUnderrun( EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs)396 public void onAudioUnderrun( 397 EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { 398 maybeAddSession(eventTime); 399 for (String session : playbackStatsTrackers.keySet()) { 400 if (sessionManager.belongsToSession(eventTime, session)) { 401 playbackStatsTrackers.get(session).onAudioUnderrun(); 402 } 403 } 404 } 405 406 @Override onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs)407 public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) { 408 maybeAddSession(eventTime); 409 for (String session : playbackStatsTrackers.keySet()) { 410 if (sessionManager.belongsToSession(eventTime, session)) { 411 playbackStatsTrackers.get(session).onDroppedVideoFrames(droppedFrames); 412 } 413 } 414 } 415 416 @Override onLoadError( EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled)417 public void onLoadError( 418 EventTime eventTime, 419 LoadEventInfo loadEventInfo, 420 MediaLoadData mediaLoadData, 421 IOException error, 422 boolean wasCanceled) { 423 maybeAddSession(eventTime); 424 for (String session : playbackStatsTrackers.keySet()) { 425 if (sessionManager.belongsToSession(eventTime, session)) { 426 playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); 427 } 428 } 429 } 430 431 @Override onDrmSessionManagerError(EventTime eventTime, Exception error)432 public void onDrmSessionManagerError(EventTime eventTime, Exception error) { 433 maybeAddSession(eventTime); 434 for (String session : playbackStatsTrackers.keySet()) { 435 if (sessionManager.belongsToSession(eventTime, session)) { 436 playbackStatsTrackers.get(session).onNonFatalError(eventTime, error); 437 } 438 } 439 } 440 maybeAddSession(EventTime eventTime)441 private void maybeAddSession(EventTime eventTime) { 442 boolean isCompletelyIdle = eventTime.timeline.isEmpty() && playbackState == Player.STATE_IDLE; 443 if (!isCompletelyIdle) { 444 sessionManager.updateSessions(eventTime); 445 } 446 } 447 448 /** Tracker for playback stats of a single playback. */ 449 private static final class PlaybackStatsTracker { 450 451 // Final stats. 452 private final boolean keepHistory; 453 private final long[] playbackStateDurationsMs; 454 private final List<EventTimeAndPlaybackState> playbackStateHistory; 455 private final List<long[]> mediaTimeHistory; 456 private final List<EventTimeAndFormat> videoFormatHistory; 457 private final List<EventTimeAndFormat> audioFormatHistory; 458 private final List<EventTimeAndException> fatalErrorHistory; 459 private final List<EventTimeAndException> nonFatalErrorHistory; 460 private final boolean isAd; 461 462 private long firstReportedTimeMs; 463 private boolean hasBeenReady; 464 private boolean hasEnded; 465 private boolean isJoinTimeInvalid; 466 private int pauseCount; 467 private int pauseBufferCount; 468 private int seekCount; 469 private int rebufferCount; 470 private long maxRebufferTimeMs; 471 private int initialVideoFormatHeight; 472 private long initialVideoFormatBitrate; 473 private long initialAudioFormatBitrate; 474 private long videoFormatHeightTimeMs; 475 private long videoFormatHeightTimeProduct; 476 private long videoFormatBitrateTimeMs; 477 private long videoFormatBitrateTimeProduct; 478 private long audioFormatTimeMs; 479 private long audioFormatBitrateTimeProduct; 480 private long bandwidthTimeMs; 481 private long bandwidthBytes; 482 private long droppedFrames; 483 private long audioUnderruns; 484 private int fatalErrorCount; 485 private int nonFatalErrorCount; 486 487 // Current player state tracking. 488 private @PlaybackState int currentPlaybackState; 489 private long currentPlaybackStateStartTimeMs; 490 private boolean isSeeking; 491 private boolean isForeground; 492 private boolean isInterruptedByAd; 493 private boolean isFinished; 494 private boolean playWhenReady; 495 @Player.State private int playerPlaybackState; 496 private boolean isSuppressed; 497 private boolean hasFatalError; 498 private boolean startedLoading; 499 private long lastRebufferStartTimeMs; 500 @Nullable private Format currentVideoFormat; 501 @Nullable private Format currentAudioFormat; 502 private long lastVideoFormatStartTimeMs; 503 private long lastAudioFormatStartTimeMs; 504 private float currentPlaybackSpeed; 505 506 /** 507 * Creates a tracker for playback stats. 508 * 509 * @param keepHistory Whether to keep a full history of events. 510 * @param startTime The {@link EventTime} at which the playback stats start. 511 */ PlaybackStatsTracker(boolean keepHistory, EventTime startTime)512 public PlaybackStatsTracker(boolean keepHistory, EventTime startTime) { 513 this.keepHistory = keepHistory; 514 playbackStateDurationsMs = new long[PlaybackStats.PLAYBACK_STATE_COUNT]; 515 playbackStateHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 516 mediaTimeHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 517 videoFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 518 audioFormatHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 519 fatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 520 nonFatalErrorHistory = keepHistory ? new ArrayList<>() : Collections.emptyList(); 521 currentPlaybackState = PlaybackStats.PLAYBACK_STATE_NOT_STARTED; 522 currentPlaybackStateStartTimeMs = startTime.realtimeMs; 523 playerPlaybackState = Player.STATE_IDLE; 524 firstReportedTimeMs = C.TIME_UNSET; 525 maxRebufferTimeMs = C.TIME_UNSET; 526 isAd = startTime.mediaPeriodId != null && startTime.mediaPeriodId.isAd(); 527 initialAudioFormatBitrate = C.LENGTH_UNSET; 528 initialVideoFormatBitrate = C.LENGTH_UNSET; 529 initialVideoFormatHeight = C.LENGTH_UNSET; 530 currentPlaybackSpeed = 1f; 531 } 532 533 /** 534 * Notifies the tracker of a playback state change event, including all playback state changes 535 * while the playback is not in the foreground. 536 * 537 * @param eventTime The {@link EventTime}. 538 * @param state The current {@link Player.State}. 539 * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. 540 */ onPlaybackStateChanged( EventTime eventTime, @Player.State int state, boolean belongsToPlayback)541 public void onPlaybackStateChanged( 542 EventTime eventTime, @Player.State int state, boolean belongsToPlayback) { 543 playerPlaybackState = state; 544 if (state != Player.STATE_IDLE) { 545 hasFatalError = false; 546 } 547 if (state != Player.STATE_BUFFERING) { 548 isSeeking = false; 549 } 550 if (state == Player.STATE_IDLE || state == Player.STATE_ENDED) { 551 isInterruptedByAd = false; 552 } 553 maybeUpdatePlaybackState(eventTime, belongsToPlayback); 554 } 555 556 /** 557 * Notifies the tracker of a play when ready change event, including all play when ready changes 558 * while the playback is not in the foreground. 559 * 560 * @param eventTime The {@link EventTime}. 561 * @param playWhenReady Whether the playback will proceed when ready. 562 * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. 563 */ onPlayWhenReadyChanged( EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback)564 public void onPlayWhenReadyChanged( 565 EventTime eventTime, boolean playWhenReady, boolean belongsToPlayback) { 566 this.playWhenReady = playWhenReady; 567 maybeUpdatePlaybackState(eventTime, belongsToPlayback); 568 } 569 570 /** 571 * Notifies the tracker of a change to the playback suppression (e.g. due to audio focus loss), 572 * including all updates while the playback is not in the foreground. 573 * 574 * @param eventTime The {@link EventTime}. 575 * @param isSuppressed Whether playback is suppressed. 576 * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. 577 */ onIsSuppressedChanged( EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback)578 public void onIsSuppressedChanged( 579 EventTime eventTime, boolean isSuppressed, boolean belongsToPlayback) { 580 this.isSuppressed = isSuppressed; 581 maybeUpdatePlaybackState(eventTime, belongsToPlayback); 582 } 583 584 /** 585 * Notifies the tracker of a position discontinuity or timeline update for the current playback. 586 * 587 * @param eventTime The {@link EventTime}. 588 * @param isSeek Whether the position discontinuity is for a seek. 589 */ onPositionDiscontinuity(EventTime eventTime, boolean isSeek)590 public void onPositionDiscontinuity(EventTime eventTime, boolean isSeek) { 591 if (isSeek && playerPlaybackState == Player.STATE_IDLE) { 592 isSeeking = false; 593 } 594 isInterruptedByAd = false; 595 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); 596 } 597 598 /** 599 * Notifies the tracker of the start of a seek, including all seeks while the playback is not in 600 * the foreground. 601 * 602 * @param eventTime The {@link EventTime}. 603 * @param belongsToPlayback Whether the {@code eventTime} belongs to the current playback. 604 */ onSeekStarted(EventTime eventTime, boolean belongsToPlayback)605 public void onSeekStarted(EventTime eventTime, boolean belongsToPlayback) { 606 isSeeking = true; 607 maybeUpdatePlaybackState(eventTime, belongsToPlayback); 608 } 609 610 /** 611 * Notifies the tracker of fatal player error in the current playback. 612 * 613 * @param eventTime The {@link EventTime}. 614 */ onFatalError(EventTime eventTime, Exception error)615 public void onFatalError(EventTime eventTime, Exception error) { 616 fatalErrorCount++; 617 if (keepHistory) { 618 fatalErrorHistory.add(new EventTimeAndException(eventTime, error)); 619 } 620 hasFatalError = true; 621 isInterruptedByAd = false; 622 isSeeking = false; 623 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); 624 } 625 626 /** 627 * Notifies the tracker that a load for the current playback has started. 628 * 629 * @param eventTime The {@link EventTime}. 630 */ onLoadStarted(EventTime eventTime)631 public void onLoadStarted(EventTime eventTime) { 632 startedLoading = true; 633 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); 634 } 635 636 /** 637 * Notifies the tracker that the current playback became the active foreground playback. 638 * 639 * @param eventTime The {@link EventTime}. 640 */ onForeground(EventTime eventTime)641 public void onForeground(EventTime eventTime) { 642 isForeground = true; 643 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); 644 } 645 646 /** 647 * Notifies the tracker that the current playback has been interrupted for ad playback. 648 * 649 * @param eventTime The {@link EventTime}. 650 */ onInterruptedByAd(EventTime eventTime)651 public void onInterruptedByAd(EventTime eventTime) { 652 isInterruptedByAd = true; 653 isSeeking = false; 654 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ true); 655 } 656 657 /** 658 * Notifies the tracker that the current playback has finished. 659 * 660 * @param eventTime The {@link EventTime}. Not guaranteed to belong to the current playback. 661 */ onFinished(EventTime eventTime)662 public void onFinished(EventTime eventTime) { 663 isFinished = true; 664 maybeUpdatePlaybackState(eventTime, /* belongsToPlayback= */ false); 665 } 666 667 /** 668 * Notifies the tracker that the track selection for the current playback changed. 669 * 670 * @param eventTime The {@link EventTime}. 671 * @param trackSelections The new {@link TrackSelectionArray}. 672 */ onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections)673 public void onTracksChanged(EventTime eventTime, TrackSelectionArray trackSelections) { 674 boolean videoEnabled = false; 675 boolean audioEnabled = false; 676 for (TrackSelection trackSelection : trackSelections.getAll()) { 677 if (trackSelection != null && trackSelection.length() > 0) { 678 int trackType = MimeTypes.getTrackType(trackSelection.getFormat(0).sampleMimeType); 679 if (trackType == C.TRACK_TYPE_VIDEO) { 680 videoEnabled = true; 681 } else if (trackType == C.TRACK_TYPE_AUDIO) { 682 audioEnabled = true; 683 } 684 } 685 } 686 if (!videoEnabled) { 687 maybeUpdateVideoFormat(eventTime, /* newFormat= */ null); 688 } 689 if (!audioEnabled) { 690 maybeUpdateAudioFormat(eventTime, /* newFormat= */ null); 691 } 692 } 693 694 /** 695 * Notifies the tracker that a format being read by the renderers for the current playback 696 * changed. 697 * 698 * @param eventTime The {@link EventTime}. 699 * @param mediaLoadData The {@link MediaLoadData} describing the format change. 700 */ onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData)701 public void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) { 702 if (mediaLoadData.trackType == C.TRACK_TYPE_VIDEO 703 || mediaLoadData.trackType == C.TRACK_TYPE_DEFAULT) { 704 maybeUpdateVideoFormat(eventTime, mediaLoadData.trackFormat); 705 } else if (mediaLoadData.trackType == C.TRACK_TYPE_AUDIO) { 706 maybeUpdateAudioFormat(eventTime, mediaLoadData.trackFormat); 707 } 708 } 709 710 /** 711 * Notifies the tracker that the video size for the current playback changed. 712 * 713 * @param eventTime The {@link EventTime}. 714 * @param width The video width in pixels. 715 * @param height The video height in pixels. 716 */ onVideoSizeChanged(EventTime eventTime, int width, int height)717 public void onVideoSizeChanged(EventTime eventTime, int width, int height) { 718 if (currentVideoFormat != null && currentVideoFormat.height == Format.NO_VALUE) { 719 Format formatWithHeight = 720 currentVideoFormat.buildUpon().setWidth(width).setHeight(height).build(); 721 maybeUpdateVideoFormat(eventTime, formatWithHeight); 722 } 723 } 724 725 /** 726 * Notifies the tracker of a playback speed change, including all playback speed changes while 727 * the playback is not in the foreground. 728 * 729 * @param eventTime The {@link EventTime}. 730 * @param playbackSpeed The new playback speed. 731 */ onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed)732 public void onPlaybackSpeedChanged(EventTime eventTime, float playbackSpeed) { 733 maybeUpdateMediaTimeHistory(eventTime.realtimeMs, eventTime.eventPlaybackPositionMs); 734 maybeRecordVideoFormatTime(eventTime.realtimeMs); 735 maybeRecordAudioFormatTime(eventTime.realtimeMs); 736 currentPlaybackSpeed = playbackSpeed; 737 } 738 739 /** Notifies the builder of an audio underrun for the current playback. */ onAudioUnderrun()740 public void onAudioUnderrun() { 741 audioUnderruns++; 742 } 743 744 /** 745 * Notifies the tracker of dropped video frames for the current playback. 746 * 747 * @param droppedFrames The number of dropped video frames. 748 */ onDroppedVideoFrames(int droppedFrames)749 public void onDroppedVideoFrames(int droppedFrames) { 750 this.droppedFrames += droppedFrames; 751 } 752 753 /** 754 * Notifies the tracker of bandwidth measurement data for the current playback. 755 * 756 * @param timeMs The time for which bandwidth measurement data is available, in milliseconds. 757 * @param bytes The bytes transferred during {@code timeMs}. 758 */ onBandwidthData(long timeMs, long bytes)759 public void onBandwidthData(long timeMs, long bytes) { 760 bandwidthTimeMs += timeMs; 761 bandwidthBytes += bytes; 762 } 763 764 /** 765 * Notifies the tracker of a non-fatal error in the current playback. 766 * 767 * @param eventTime The {@link EventTime}. 768 * @param error The error. 769 */ onNonFatalError(EventTime eventTime, Exception error)770 public void onNonFatalError(EventTime eventTime, Exception error) { 771 nonFatalErrorCount++; 772 if (keepHistory) { 773 nonFatalErrorHistory.add(new EventTimeAndException(eventTime, error)); 774 } 775 } 776 777 /** 778 * Builds the playback stats. 779 * 780 * @param isFinal Whether this is the final build and no further events are expected. 781 */ build(boolean isFinal)782 public PlaybackStats build(boolean isFinal) { 783 long[] playbackStateDurationsMs = this.playbackStateDurationsMs; 784 List<long[]> mediaTimeHistory = this.mediaTimeHistory; 785 if (!isFinal) { 786 long buildTimeMs = SystemClock.elapsedRealtime(); 787 playbackStateDurationsMs = 788 Arrays.copyOf(this.playbackStateDurationsMs, PlaybackStats.PLAYBACK_STATE_COUNT); 789 long lastStateDurationMs = Math.max(0, buildTimeMs - currentPlaybackStateStartTimeMs); 790 playbackStateDurationsMs[currentPlaybackState] += lastStateDurationMs; 791 maybeUpdateMaxRebufferTimeMs(buildTimeMs); 792 maybeRecordVideoFormatTime(buildTimeMs); 793 maybeRecordAudioFormatTime(buildTimeMs); 794 mediaTimeHistory = new ArrayList<>(this.mediaTimeHistory); 795 if (keepHistory && currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING) { 796 mediaTimeHistory.add(guessMediaTimeBasedOnElapsedRealtime(buildTimeMs)); 797 } 798 } 799 boolean isJoinTimeInvalid = this.isJoinTimeInvalid || !hasBeenReady; 800 long validJoinTimeMs = 801 isJoinTimeInvalid 802 ? C.TIME_UNSET 803 : playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND]; 804 boolean hasBackgroundJoin = 805 playbackStateDurationsMs[PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND] > 0; 806 List<EventTimeAndFormat> videoHistory = 807 isFinal ? videoFormatHistory : new ArrayList<>(videoFormatHistory); 808 List<EventTimeAndFormat> audioHistory = 809 isFinal ? audioFormatHistory : new ArrayList<>(audioFormatHistory); 810 return new PlaybackStats( 811 /* playbackCount= */ 1, 812 playbackStateDurationsMs, 813 isFinal ? playbackStateHistory : new ArrayList<>(playbackStateHistory), 814 mediaTimeHistory, 815 firstReportedTimeMs, 816 /* foregroundPlaybackCount= */ isForeground ? 1 : 0, 817 /* abandonedBeforeReadyCount= */ hasBeenReady ? 0 : 1, 818 /* endedCount= */ hasEnded ? 1 : 0, 819 /* backgroundJoiningCount= */ hasBackgroundJoin ? 1 : 0, 820 validJoinTimeMs, 821 /* validJoinTimeCount= */ isJoinTimeInvalid ? 0 : 1, 822 pauseCount, 823 pauseBufferCount, 824 seekCount, 825 rebufferCount, 826 maxRebufferTimeMs, 827 /* adPlaybackCount= */ isAd ? 1 : 0, 828 videoHistory, 829 audioHistory, 830 videoFormatHeightTimeMs, 831 videoFormatHeightTimeProduct, 832 videoFormatBitrateTimeMs, 833 videoFormatBitrateTimeProduct, 834 audioFormatTimeMs, 835 audioFormatBitrateTimeProduct, 836 /* initialVideoFormatHeightCount= */ initialVideoFormatHeight == C.LENGTH_UNSET ? 0 : 1, 837 /* initialVideoFormatBitrateCount= */ initialVideoFormatBitrate == C.LENGTH_UNSET ? 0 : 1, 838 initialVideoFormatHeight, 839 initialVideoFormatBitrate, 840 /* initialAudioFormatBitrateCount= */ initialAudioFormatBitrate == C.LENGTH_UNSET ? 0 : 1, 841 initialAudioFormatBitrate, 842 bandwidthTimeMs, 843 bandwidthBytes, 844 droppedFrames, 845 audioUnderruns, 846 /* fatalErrorPlaybackCount= */ fatalErrorCount > 0 ? 1 : 0, 847 fatalErrorCount, 848 nonFatalErrorCount, 849 fatalErrorHistory, 850 nonFatalErrorHistory); 851 } 852 maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback)853 private void maybeUpdatePlaybackState(EventTime eventTime, boolean belongsToPlayback) { 854 @PlaybackState int newPlaybackState = resolveNewPlaybackState(); 855 if (newPlaybackState == currentPlaybackState) { 856 return; 857 } 858 Assertions.checkArgument(eventTime.realtimeMs >= currentPlaybackStateStartTimeMs); 859 860 long stateDurationMs = eventTime.realtimeMs - currentPlaybackStateStartTimeMs; 861 playbackStateDurationsMs[currentPlaybackState] += stateDurationMs; 862 if (firstReportedTimeMs == C.TIME_UNSET) { 863 firstReportedTimeMs = eventTime.realtimeMs; 864 } 865 isJoinTimeInvalid |= isInvalidJoinTransition(currentPlaybackState, newPlaybackState); 866 hasBeenReady |= isReadyState(newPlaybackState); 867 hasEnded |= newPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED; 868 if (!isPausedState(currentPlaybackState) && isPausedState(newPlaybackState)) { 869 pauseCount++; 870 } 871 if (newPlaybackState == PlaybackStats.PLAYBACK_STATE_SEEKING) { 872 seekCount++; 873 } 874 if (!isRebufferingState(currentPlaybackState) && isRebufferingState(newPlaybackState)) { 875 rebufferCount++; 876 lastRebufferStartTimeMs = eventTime.realtimeMs; 877 } 878 if (isRebufferingState(currentPlaybackState) 879 && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING 880 && newPlaybackState == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING) { 881 pauseBufferCount++; 882 } 883 884 maybeUpdateMediaTimeHistory( 885 eventTime.realtimeMs, 886 /* mediaTimeMs= */ belongsToPlayback ? eventTime.eventPlaybackPositionMs : C.TIME_UNSET); 887 maybeUpdateMaxRebufferTimeMs(eventTime.realtimeMs); 888 maybeRecordVideoFormatTime(eventTime.realtimeMs); 889 maybeRecordAudioFormatTime(eventTime.realtimeMs); 890 891 currentPlaybackState = newPlaybackState; 892 currentPlaybackStateStartTimeMs = eventTime.realtimeMs; 893 if (keepHistory) { 894 playbackStateHistory.add(new EventTimeAndPlaybackState(eventTime, currentPlaybackState)); 895 } 896 } 897 resolveNewPlaybackState()898 private @PlaybackState int resolveNewPlaybackState() { 899 if (isFinished) { 900 // Keep VIDEO_STATE_ENDED if playback naturally ended (or progressed to next item). 901 return currentPlaybackState == PlaybackStats.PLAYBACK_STATE_ENDED 902 ? PlaybackStats.PLAYBACK_STATE_ENDED 903 : PlaybackStats.PLAYBACK_STATE_ABANDONED; 904 } else if (isSeeking && isForeground) { 905 // Seeking takes precedence over errors such that we report a seek while in error state. 906 return PlaybackStats.PLAYBACK_STATE_SEEKING; 907 } else if (hasFatalError) { 908 return PlaybackStats.PLAYBACK_STATE_FAILED; 909 } else if (!isForeground) { 910 // Before the playback becomes foreground, only report background joining and not started. 911 return startedLoading 912 ? PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND 913 : PlaybackStats.PLAYBACK_STATE_NOT_STARTED; 914 } else if (isInterruptedByAd) { 915 return PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD; 916 } else if (playerPlaybackState == Player.STATE_ENDED) { 917 return PlaybackStats.PLAYBACK_STATE_ENDED; 918 } else if (playerPlaybackState == Player.STATE_BUFFERING) { 919 if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_NOT_STARTED 920 || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND 921 || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND 922 || currentPlaybackState == PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { 923 return PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND; 924 } 925 if (!playWhenReady) { 926 return PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; 927 } 928 return isSuppressed 929 ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING 930 : PlaybackStats.PLAYBACK_STATE_BUFFERING; 931 } else if (playerPlaybackState == Player.STATE_READY) { 932 if (!playWhenReady) { 933 return PlaybackStats.PLAYBACK_STATE_PAUSED; 934 } 935 return isSuppressed 936 ? PlaybackStats.PLAYBACK_STATE_SUPPRESSED 937 : PlaybackStats.PLAYBACK_STATE_PLAYING; 938 } else if (playerPlaybackState == Player.STATE_IDLE 939 && currentPlaybackState != PlaybackStats.PLAYBACK_STATE_NOT_STARTED) { 940 // This case only applies for calls to player.stop(). All other IDLE cases are handled by 941 // !isForeground, hasFatalError or isSuspended. NOT_STARTED is deliberately ignored. 942 return PlaybackStats.PLAYBACK_STATE_STOPPED; 943 } 944 return currentPlaybackState; 945 } 946 maybeUpdateMaxRebufferTimeMs(long nowMs)947 private void maybeUpdateMaxRebufferTimeMs(long nowMs) { 948 if (isRebufferingState(currentPlaybackState)) { 949 long rebufferDurationMs = nowMs - lastRebufferStartTimeMs; 950 if (maxRebufferTimeMs == C.TIME_UNSET || rebufferDurationMs > maxRebufferTimeMs) { 951 maxRebufferTimeMs = rebufferDurationMs; 952 } 953 } 954 } 955 maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs)956 private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { 957 if (!keepHistory) { 958 return; 959 } 960 if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { 961 if (mediaTimeMs == C.TIME_UNSET) { 962 return; 963 } 964 if (!mediaTimeHistory.isEmpty()) { 965 long previousMediaTimeMs = mediaTimeHistory.get(mediaTimeHistory.size() - 1)[1]; 966 if (previousMediaTimeMs != mediaTimeMs) { 967 mediaTimeHistory.add(new long[] {realtimeMs, previousMediaTimeMs}); 968 } 969 } 970 } 971 mediaTimeHistory.add( 972 mediaTimeMs == C.TIME_UNSET 973 ? guessMediaTimeBasedOnElapsedRealtime(realtimeMs) 974 : new long[] {realtimeMs, mediaTimeMs}); 975 } 976 guessMediaTimeBasedOnElapsedRealtime(long realtimeMs)977 private long[] guessMediaTimeBasedOnElapsedRealtime(long realtimeMs) { 978 long[] previousKnownMediaTimeHistory = mediaTimeHistory.get(mediaTimeHistory.size() - 1); 979 long previousRealtimeMs = previousKnownMediaTimeHistory[0]; 980 long previousMediaTimeMs = previousKnownMediaTimeHistory[1]; 981 long elapsedMediaTimeEstimateMs = 982 (long) ((realtimeMs - previousRealtimeMs) * currentPlaybackSpeed); 983 long mediaTimeEstimateMs = previousMediaTimeMs + elapsedMediaTimeEstimateMs; 984 return new long[] {realtimeMs, mediaTimeEstimateMs}; 985 } 986 maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat)987 private void maybeUpdateVideoFormat(EventTime eventTime, @Nullable Format newFormat) { 988 if (Util.areEqual(currentVideoFormat, newFormat)) { 989 return; 990 } 991 maybeRecordVideoFormatTime(eventTime.realtimeMs); 992 if (newFormat != null) { 993 if (initialVideoFormatHeight == C.LENGTH_UNSET && newFormat.height != Format.NO_VALUE) { 994 initialVideoFormatHeight = newFormat.height; 995 } 996 if (initialVideoFormatBitrate == C.LENGTH_UNSET && newFormat.bitrate != Format.NO_VALUE) { 997 initialVideoFormatBitrate = newFormat.bitrate; 998 } 999 } 1000 currentVideoFormat = newFormat; 1001 if (keepHistory) { 1002 videoFormatHistory.add(new EventTimeAndFormat(eventTime, currentVideoFormat)); 1003 } 1004 } 1005 maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat)1006 private void maybeUpdateAudioFormat(EventTime eventTime, @Nullable Format newFormat) { 1007 if (Util.areEqual(currentAudioFormat, newFormat)) { 1008 return; 1009 } 1010 maybeRecordAudioFormatTime(eventTime.realtimeMs); 1011 if (newFormat != null 1012 && initialAudioFormatBitrate == C.LENGTH_UNSET 1013 && newFormat.bitrate != Format.NO_VALUE) { 1014 initialAudioFormatBitrate = newFormat.bitrate; 1015 } 1016 currentAudioFormat = newFormat; 1017 if (keepHistory) { 1018 audioFormatHistory.add(new EventTimeAndFormat(eventTime, currentAudioFormat)); 1019 } 1020 } 1021 maybeRecordVideoFormatTime(long nowMs)1022 private void maybeRecordVideoFormatTime(long nowMs) { 1023 if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING 1024 && currentVideoFormat != null) { 1025 long mediaDurationMs = (long) ((nowMs - lastVideoFormatStartTimeMs) * currentPlaybackSpeed); 1026 if (currentVideoFormat.height != Format.NO_VALUE) { 1027 videoFormatHeightTimeMs += mediaDurationMs; 1028 videoFormatHeightTimeProduct += mediaDurationMs * currentVideoFormat.height; 1029 } 1030 if (currentVideoFormat.bitrate != Format.NO_VALUE) { 1031 videoFormatBitrateTimeMs += mediaDurationMs; 1032 videoFormatBitrateTimeProduct += mediaDurationMs * currentVideoFormat.bitrate; 1033 } 1034 } 1035 lastVideoFormatStartTimeMs = nowMs; 1036 } 1037 maybeRecordAudioFormatTime(long nowMs)1038 private void maybeRecordAudioFormatTime(long nowMs) { 1039 if (currentPlaybackState == PlaybackStats.PLAYBACK_STATE_PLAYING 1040 && currentAudioFormat != null 1041 && currentAudioFormat.bitrate != Format.NO_VALUE) { 1042 long mediaDurationMs = (long) ((nowMs - lastAudioFormatStartTimeMs) * currentPlaybackSpeed); 1043 audioFormatTimeMs += mediaDurationMs; 1044 audioFormatBitrateTimeProduct += mediaDurationMs * currentAudioFormat.bitrate; 1045 } 1046 lastAudioFormatStartTimeMs = nowMs; 1047 } 1048 isReadyState(@laybackState int state)1049 private static boolean isReadyState(@PlaybackState int state) { 1050 return state == PlaybackStats.PLAYBACK_STATE_PLAYING 1051 || state == PlaybackStats.PLAYBACK_STATE_PAUSED 1052 || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED; 1053 } 1054 isPausedState(@laybackState int state)1055 private static boolean isPausedState(@PlaybackState int state) { 1056 return state == PlaybackStats.PLAYBACK_STATE_PAUSED 1057 || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING; 1058 } 1059 isRebufferingState(@laybackState int state)1060 private static boolean isRebufferingState(@PlaybackState int state) { 1061 return state == PlaybackStats.PLAYBACK_STATE_BUFFERING 1062 || state == PlaybackStats.PLAYBACK_STATE_PAUSED_BUFFERING 1063 || state == PlaybackStats.PLAYBACK_STATE_SUPPRESSED_BUFFERING; 1064 } 1065 isInvalidJoinTransition( @laybackState int oldState, @PlaybackState int newState)1066 private static boolean isInvalidJoinTransition( 1067 @PlaybackState int oldState, @PlaybackState int newState) { 1068 if (oldState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND 1069 && oldState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND 1070 && oldState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD) { 1071 return false; 1072 } 1073 return newState != PlaybackStats.PLAYBACK_STATE_JOINING_BACKGROUND 1074 && newState != PlaybackStats.PLAYBACK_STATE_JOINING_FOREGROUND 1075 && newState != PlaybackStats.PLAYBACK_STATE_INTERRUPTED_BY_AD 1076 && newState != PlaybackStats.PLAYBACK_STATE_PLAYING 1077 && newState != PlaybackStats.PLAYBACK_STATE_PAUSED 1078 && newState != PlaybackStats.PLAYBACK_STATE_SUPPRESSED 1079 && newState != PlaybackStats.PLAYBACK_STATE_ENDED; 1080 } 1081 } 1082 } 1083