In our latest post, we discussed how to implement an analytics collector for vanilla media player and its adapter libraries in Android. It was trivial but lengthy in terms of loc not to break existing functionality in those tools and in our opinion, those libraries didn’t age well. Fortunately, Google published a new open source media player named ExoPlayer with magical abilities that works in all kinds of Android devices last year. Its modern API allows us to do more with less code. In this post, we will write an analytics collector for ExoPlayer using your favorite testing tool.
Android TV Blog Post Series
These posts build on top of each other. If you plan to learn other cool stuff as well, feel free to take a look.
A one liner API that looks like exoPlayer.addAnalyticsListener(new TestFairyExoPlayerAnalyticsListener(exoPlayer));
Sending analytic events for buffering and playback events
Keeping track of play/pause/error states
Not breaking existing functionality with ExoPlayer classes
Exposing existing public API from the wrapped classes to allow further extensions by users
In vanilla media player, not breaking existing functionality while listening all the desired events required us to build wrappers that listens all the necessary events before forwarding them to outside delegates. ExoPlayer provides a huge improvement to this workflow by supporting multiple listeners on the same player using its player.addAnalyticsListener() api.
The entire events list provided by the AnalyticsListener interface is huge. We will be mostly focusing on playback states, error cases and buffering/progress events.
Just a quick reminder: You can replace every line that calls TestFairy SDK methods in this tutorial with your testing tool of choice without any problem.
Implementing the Listener
Let’s define a class that implements the listener.
Don’t get scared by the import list. In most cases, your IDE will automatically handle those statements for you.
This class won’t compile without specifying overrides for each listener method. We will put the ones we are interested in together and leave the rest of the overrides as empty blocks. Here is an organized version of the event list.
public class TestFairyExoPlayerAnalyticsListener implements AnalyticsListener {
////////////////////THESE WE NEED/////////////////////////////
@Override
public voidonPlayerStateChanged(EventTime eventTime, boolean playWhenReady,
int playbackState){
// TODO
}
@Override
public voidonPositionDiscontinuity(EventTime eventTime, int reason){
// TODO
}
@Override
public voidonSeekProcessed(EventTime eventTime){
// TODO
}
@Override
public voidonRepeatModeChanged(EventTime eventTime, int repeatMode){
// TODO
}
@Override
public voidonShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled){
// TODO
}
@Override
public voidonPlayerError(EventTime eventTime, ExoPlaybackException error){
// TODO
}
@Override
public voidonTracksChanged(EventTime eventTime, TrackGroupArray trackGroups,
public voidonLoadingChanged(EventTime eventTime, boolean isLoading){
}
@Override
public voidonPlaybackParametersChanged(EventTime eventTime,
PlaybackParameters playbackParameters){
}
@Override
public voidonSeekStarted(EventTime eventTime){
}
@Override
public voidonTimelineChanged(EventTime eventTime, int reason){
}
@Override
public voidonDrmKeysRestored(EventTime eventTime){
}
@Override
public voidonDrmKeysRemoved(EventTime eventTime){
}
@Override
public voidonDrmSessionReleased(EventTime eventTime){
}
}
public class TestFairyExoPlayerAnalyticsListener implements AnalyticsListener {
////////////////////THESE WE NEED/////////////////////////////
@Override
public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady,
int playbackState) {
// TODO
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
// TODO
}
@Override
public void onSeekProcessed(EventTime eventTime) {
// TODO
}
@Override
public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {
// TODO
}
@Override
public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {
// TODO
}
@Override
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
// TODO
}
@Override
public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
// TODO
}
@Override
public void onLoadCompleted(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
// TODO
}
@Override
public void onLoadError(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData, IOException error,
boolean wasCanceled) {
// TODO
}
@Override
public void onVolumeChanged(EventTime eventTime, float volume) {
// TODO
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
// TODO
}
@Override
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
// TODO
}
////////////////////THESE WE DON'T NEED/////////////////////////////
@Override
public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
}
@Override
public void onVideoSizeChanged(EventTime eventTime, int width, int height,
int unappliedRotationDegrees, float pixelWidthHeightRatio) {
}
@Override
public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {
}
@Override
public void onDrmSessionAcquired(EventTime eventTime) {
}
@Override
public void onDrmKeysLoaded(EventTime eventTime) {
}
@Override
public void onDownstreamFormatChanged(EventTime eventTime,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onUpstreamDiscarded(EventTime eventTime,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onMediaPeriodCreated(EventTime eventTime) {
}
@Override
public void onMediaPeriodReleased(EventTime eventTime) {
}
@Override
public void onReadingStarted(EventTime eventTime) {
}
@Override
public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs,
long totalBytesLoaded, long bitrateEstimate) {
}
@Override
public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {
}
@Override
public void onMetadata(EventTime eventTime, Metadata metadata) {
}
@Override
public void onDecoderEnabled(EventTime eventTime, int trackType,
DecoderCounters decoderCounters) {
}
@Override
public void onDecoderInitialized(EventTime eventTime, int trackType, String decoderName,
long initializationDurationMs) {
}
@Override
public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {
}
@Override
public void onDecoderDisabled(EventTime eventTime, int trackType,
DecoderCounters decoderCounters) {
}
@Override
public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
}
@Override
public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {
}
@Override
public void onLoadCanceled(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onLoadStarted(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onLoadingChanged(EventTime eventTime, boolean isLoading) {
}
@Override
public void onPlaybackParametersChanged(EventTime eventTime,
PlaybackParameters playbackParameters) {
}
@Override
public void onSeekStarted(EventTime eventTime) {
}
@Override
public void onTimelineChanged(EventTime eventTime, int reason) {
}
@Override
public void onDrmKeysRestored(EventTime eventTime) {
}
@Override
public void onDrmKeysRemoved(EventTime eventTime) {
}
@Override
public void onDrmSessionReleased(EventTime eventTime) {
}
}
public class TestFairyExoPlayerAnalyticsListener implements AnalyticsListener {
////////////////////THESE WE NEED/////////////////////////////
@Override
public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady,
int playbackState) {
// TODO
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
// TODO
}
@Override
public void onSeekProcessed(EventTime eventTime) {
// TODO
}
@Override
public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {
// TODO
}
@Override
public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {
// TODO
}
@Override
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
// TODO
}
@Override
public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
// TODO
}
@Override
public void onLoadCompleted(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
// TODO
}
@Override
public void onLoadError(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData, IOException error,
boolean wasCanceled) {
// TODO
}
@Override
public void onVolumeChanged(EventTime eventTime, float volume) {
// TODO
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
// TODO
}
@Override
public void onDrmSessionManagerError(EventTime eventTime, Exception error) {
// TODO
}
////////////////////THESE WE DON'T NEED/////////////////////////////
@Override
public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
}
@Override
public void onVideoSizeChanged(EventTime eventTime, int width, int height,
int unappliedRotationDegrees, float pixelWidthHeightRatio) {
}
@Override
public void onRenderedFirstFrame(EventTime eventTime, @Nullable Surface surface) {
}
@Override
public void onDrmSessionAcquired(EventTime eventTime) {
}
@Override
public void onDrmKeysLoaded(EventTime eventTime) {
}
@Override
public void onDownstreamFormatChanged(EventTime eventTime,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onUpstreamDiscarded(EventTime eventTime,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onMediaPeriodCreated(EventTime eventTime) {
}
@Override
public void onMediaPeriodReleased(EventTime eventTime) {
}
@Override
public void onReadingStarted(EventTime eventTime) {
}
@Override
public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs,
long totalBytesLoaded, long bitrateEstimate) {
}
@Override
public void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {
}
@Override
public void onMetadata(EventTime eventTime, Metadata metadata) {
}
@Override
public void onDecoderEnabled(EventTime eventTime, int trackType,
DecoderCounters decoderCounters) {
}
@Override
public void onDecoderInitialized(EventTime eventTime, int trackType, String decoderName,
long initializationDurationMs) {
}
@Override
public void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {
}
@Override
public void onDecoderDisabled(EventTime eventTime, int trackType,
DecoderCounters decoderCounters) {
}
@Override
public void onAudioSessionId(EventTime eventTime, int audioSessionId) {
}
@Override
public void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {
}
@Override
public void onLoadCanceled(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onLoadStarted(EventTime eventTime,
MediaSourceEventListener.LoadEventInfo loadEventInfo,
MediaSourceEventListener.MediaLoadData mediaLoadData) {
}
@Override
public void onLoadingChanged(EventTime eventTime, boolean isLoading) {
}
@Override
public void onPlaybackParametersChanged(EventTime eventTime,
PlaybackParameters playbackParameters) {
}
@Override
public void onSeekStarted(EventTime eventTime) {
}
@Override
public void onTimelineChanged(EventTime eventTime, int reason) {
}
@Override
public void onDrmKeysRestored(EventTime eventTime) {
}
@Override
public void onDrmKeysRemoved(EventTime eventTime) {
}
@Override
public void onDrmSessionReleased(EventTime eventTime) {
}
}
ExoPlayer among all these elaborate events, doesn’t publish any playback progress events. Luckily, the player has necessary getter methods we can periodically query to calculate seek position in video. Just like we did in the previous tutorial, we will provide an on/off switch for our custom handler which will publish this event manually.
First, we need to ask for the player in our listener constructor and initialize a handler which will be used to run small blocks in the main thread asynchronously.
private final Handler handler = new Handler();
private final ExoPlayer player;
private Runnable currentPositionTracker;
private long lastKnownPlaybackPercent = -1;
public TestFairyExoPlayerAnalyticsListener(ExoPlayer player) {
this.player = player;
}
private final Handler handler = new Handler();
private final ExoPlayer player;
private Runnable currentPositionTracker;
private long lastKnownPlaybackPercent = -1;
public TestFairyExoPlayerAnalyticsListener(ExoPlayer player) {
this.player = player;
}
Our handler will run currentPositionTracker every 100 miliseconds and ask for playback progress. It will compare this progress with the stored value of its previous query to decide if a new event should be generated. Below is the boilerplate that enables and disables this behavior as desired.
privatevoidregisterCurrentPositionTracker(){
unRegisterCurrentPositionTracker();
currentPositionTracker = newRunnable(){
@Override
publicvoidrun(){
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
if(lastKnownPlaybackPercent != percent){
lastKnownPlaybackPercent = percent;
TestFairy.addEvent(String.format(Locale.ENGLISH, "Playback position %d%%", percent));
}
handler.postDelayed(currentPositionTracker, 100);
}
};
handler.postDelayed(currentPositionTracker, 100);
}
privatevoidunRegisterCurrentPositionTracker(){
if(currentPositionTracker != null){
handler.removeCallbacks(currentPositionTracker);
currentPositionTracker = null;
}
}
private void registerCurrentPositionTracker() {
unRegisterCurrentPositionTracker();
currentPositionTracker = new Runnable() {
@Override
public void run() {
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
if (lastKnownPlaybackPercent != percent) {
lastKnownPlaybackPercent = percent;
TestFairy.addEvent(String.format(Locale.ENGLISH, "Playback position %d%%", percent));
}
handler.postDelayed(currentPositionTracker, 100);
}
};
handler.postDelayed(currentPositionTracker, 100);
}
private void unRegisterCurrentPositionTracker() {
if (currentPositionTracker != null) {
handler.removeCallbacks(currentPositionTracker);
currentPositionTracker = null;
}
}
private void registerCurrentPositionTracker() {
unRegisterCurrentPositionTracker();
currentPositionTracker = new Runnable() {
@Override
public void run() {
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
if (lastKnownPlaybackPercent != percent) {
lastKnownPlaybackPercent = percent;
TestFairy.addEvent(String.format(Locale.ENGLISH, "Playback position %d%%", percent));
}
handler.postDelayed(currentPositionTracker, 100);
}
};
handler.postDelayed(currentPositionTracker, 100);
}
private void unRegisterCurrentPositionTracker() {
if (currentPositionTracker != null) {
handler.removeCallbacks(currentPositionTracker);
currentPositionTracker = null;
}
}
In our listener code, when we learn that a playback is resumed, we will register this tracker. Whenever the playback is paused or completed, we will unregister the tracker to leave the CPU idle.
Playback events reported by ExoPlayer are quite straightforward and also includs pause events occured due to slow buffering or activity lifecycle.
TestFairy.addEvent("Playback is buffering or paused automatically");
}
}else{
unRegisterCurrentPositionTracker();
TestFairy.addEvent("Playback is paused");
}
}
@Override
public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady,
int playbackState) {
if (playWhenReady && playbackState == Player.STATE_READY) {
if (currentPositionTracker == null) {
registerCurrentPositionTracker();
}
TestFairy.addEvent("Playback is playing");
} else if (playWhenReady) {
unRegisterCurrentPositionTracker();
if (player.getDuration() != 0 && player.getDuration() <= player.getCurrentPosition()) {
TestFairy.addEvent("Playback has completed");
} else {
TestFairy.addEvent("Playback is buffering or paused automatically");
}
} else {
unRegisterCurrentPositionTracker();
TestFairy.addEvent("Playback is paused");
}
}
@Override
public void onPlayerStateChanged(EventTime eventTime, boolean playWhenReady,
int playbackState) {
if (playWhenReady && playbackState == Player.STATE_READY) {
if (currentPositionTracker == null) {
registerCurrentPositionTracker();
}
TestFairy.addEvent("Playback is playing");
} else if (playWhenReady) {
unRegisterCurrentPositionTracker();
if (player.getDuration() != 0 && player.getDuration() <= player.getCurrentPosition()) {
TestFairy.addEvent("Playback has completed");
} else {
TestFairy.addEvent("Playback is buffering or paused automatically");
}
} else {
unRegisterCurrentPositionTracker();
TestFairy.addEvent("Playback is paused");
}
}
If video playback stutters without pausing entirely, we can deduce the reason with the following event.
@Override
publicvoidonPositionDiscontinuity(EventTime eventTime, int reason){
switch(reason){
case SimpleExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION:
TestFairy.addEvent("Video stutters due to period transition");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK:
TestFairy.addEvent("Video stutters due to a seek");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
TestFairy.addEvent("Video stutters due to seek adjustment");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_AD_INSERTION:
TestFairy.addEvent("Video stutters due to an inserted ad");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_INTERNAL:
TestFairy.addEvent("Video stutters due to an internal problem");
break;
}
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
switch (reason) {
case SimpleExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION:
TestFairy.addEvent("Video stutters due to period transition");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK:
TestFairy.addEvent("Video stutters due to a seek");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
TestFairy.addEvent("Video stutters due to seek adjustment");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_AD_INSERTION:
TestFairy.addEvent("Video stutters due to an inserted ad");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_INTERNAL:
TestFairy.addEvent("Video stutters due to an internal problem");
break;
}
}
@Override
public void onPositionDiscontinuity(EventTime eventTime, int reason) {
switch (reason) {
case SimpleExoPlayer.DISCONTINUITY_REASON_PERIOD_TRANSITION:
TestFairy.addEvent("Video stutters due to period transition");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK:
TestFairy.addEvent("Video stutters due to a seek");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
TestFairy.addEvent("Video stutters due to seek adjustment");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_AD_INSERTION:
TestFairy.addEvent("Video stutters due to an inserted ad");
break;
case SimpleExoPlayer.DISCONTINUITY_REASON_INTERNAL:
TestFairy.addEvent("Video stutters due to an internal problem");
break;
}
}
If a user skips a portion of the video, we have the following event generated to get the newly seeked playback position.
@Override
publicvoidonSeekProcessed(EventTime eventTime){
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
lastKnownPlaybackPercent = percent;
TestFairy
.addEvent(String.format(Locale.ENGLISH, "Playback seeks to position %d%%", percent));
}
@Override
public void onSeekProcessed(EventTime eventTime) {
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
lastKnownPlaybackPercent = percent;
TestFairy
.addEvent(String.format(Locale.ENGLISH, "Playback seeks to position %d%%", percent));
}
@Override
public void onSeekProcessed(EventTime eventTime) {
long currentPosition = player.getCurrentPosition();
long percent = (currentPosition * 100) / player.getDuration();
lastKnownPlaybackPercent = percent;
TestFairy
.addEvent(String.format(Locale.ENGLISH, "Playback seeks to position %d%%", percent));
}
ExoPlayer is a rich library with built-in shuffle and repeat functionality. These toggles also report relevant events if utilized properly.
@Override
publicvoidonRepeatModeChanged(EventTime eventTime, int repeatMode){
switch(repeatMode){
case ExoPlayer.REPEAT_MODE_OFF:
TestFairy.addEvent("Repeat mode has been changed to OFF");
case ExoPlayer.REPEAT_MODE_ONE:
TestFairy.addEvent("Repeat mode has been changed to ONE");
case ExoPlayer.REPEAT_MODE_ALL:
TestFairy.addEvent("Repeat mode has been changed to ALL");
@Override
public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {
switch (repeatMode) {
case ExoPlayer.REPEAT_MODE_OFF:
TestFairy.addEvent("Repeat mode has been changed to OFF");
case ExoPlayer.REPEAT_MODE_ONE:
TestFairy.addEvent("Repeat mode has been changed to ONE");
case ExoPlayer.REPEAT_MODE_ALL:
TestFairy.addEvent("Repeat mode has been changed to ALL");
break;
}
}
@Override
public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {
if (shuffleModeEnabled) {
TestFairy.addEvent("Shuffle mode is enabled");
} else {
TestFairy.addEvent("Shuffle mode is disabled");
}
}
@Override
public void onRepeatModeChanged(EventTime eventTime, int repeatMode) {
switch (repeatMode) {
case ExoPlayer.REPEAT_MODE_OFF:
TestFairy.addEvent("Repeat mode has been changed to OFF");
case ExoPlayer.REPEAT_MODE_ONE:
TestFairy.addEvent("Repeat mode has been changed to ONE");
case ExoPlayer.REPEAT_MODE_ALL:
TestFairy.addEvent("Repeat mode has been changed to ALL");
break;
}
}
@Override
public void onShuffleModeChanged(EventTime eventTime, boolean shuffleModeEnabled) {
if (shuffleModeEnabled) {
TestFairy.addEvent("Shuffle mode is enabled");
} else {
TestFairy.addEvent("Shuffle mode is disabled");
}
}
If we change tracks in the same player, the following event notifies the change for us.
TestFairy.addEvent("A new video has been loaded");
}
@Override
public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
lastKnownPlaybackPercent = -1;
TestFairy.addEvent("A new video has been loaded");
}
@Override
public void onTracksChanged(EventTime eventTime, TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
lastKnownPlaybackPercent = -1;
TestFairy.addEvent("A new video has been loaded");
}
ExoPlayer buffers the video in chunks, sized according to connection quality. If the file is read from a stream, it will benchmark the connection in real time to decide the best chunk size. If it is read from the file system, a small amount of large chunks will be utilized instead. In both cases, this architecture allows loading very long videos without any issue. The event defined below works for both scenarios.
TestFairy.addEvent(String.format(Locale.ENGLISH, "Volume level has changed to %s",
Float.valueOf(volume * 100f).intValue()));
}
@Override
public void onVolumeChanged(EventTime eventTime, float volume) {
TestFairy.addEvent(String.format(Locale.ENGLISH, "Volume level has changed to %s",
Float.valueOf(volume * 100f).intValue()));
}
@Override
public void onVolumeChanged(EventTime eventTime, float volume) {
TestFairy.addEvent(String.format(Locale.ENGLISH, "Volume level has changed to %s",
Float.valueOf(volume * 100f).intValue()));
}
Badly encoded or streamed videos are likely to drop frames to be able to stay in sync with the played audio. The event generated below will notify us if such issues occur.
@Override
publicvoidonDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs){
TestFairy.addEvent(
String.format(Locale.ENGLISH, "Video has dropped %d frames in %dms", droppedFrames,
elapsedMs));
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
TestFairy.addEvent(
String.format(Locale.ENGLISH, "Video has dropped %d frames in %dms", droppedFrames,
elapsedMs));
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
TestFairy.addEvent(
String.format(Locale.ENGLISH, "Video has dropped %d frames in %dms", droppedFrames,
elapsedMs));
}
Error Events
Errors in ExoPlayer come in three distinct categories.
Loading errors which occur if the loaded file is corrupted
Drm errors that occur due to a failed decryption of a copyright protected video
The remaining IOException errors due to http connection or storage issues
TestFairy SDK provides a convenient logThrowable() utility to report stack traces gathered from these events. It will help us recognize recurring errors in our session dashboard without needing to navigate through multiple session manually.
Finally, using the suggested deduction logic from ExoPlayer’s documentation, we can distinguish http errors from the rest using a catch-all event generated for all kinds of errors. If an http error is found, the http error code returned from the server will also reported as a seperate event for convenience.
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
if (error.type == ExoPlaybackException.TYPE_SOURCE) {
IOException cause = error.getSourceException();
if (cause instanceof HttpDataSource.HttpDataSourceException) {
// An HTTP error occurred.
HttpDataSource.HttpDataSourceException httpError = (HttpDataSource.HttpDataSourceException) cause;
// It's possible to find out more about the error both by casting and by
// querying the cause.
if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
// Cast to InvalidResponseCodeException and retrieve the response code,
// message and headers.
HttpDataSource.InvalidResponseCodeException ex = (HttpDataSource.InvalidResponseCodeException) httpError;
TestFairy.addEvent(
String.format(Locale.ENGLISH, "Http error during playback - %d", ex.responseCode));
TestFairy.logThrowable(cause);
} else {
// Try calling httpError.getCause() to retrieve the underlying cause,
// although note that it may be null.
TestFairy.addEvent("Http error during playback before response");
Throwable innerCause = httpError.getCause();
if (innerCause != null) {
TestFairy.logThrowable(innerCause);
} else {
TestFairy.logThrowable(cause);
}
}
} else {
TestFairy.addEvent(String.format(Locale.ENGLISH, "Player error - %s", error.toString()));
TestFairy.logThrowable(cause);
}
}
}
public void onPlayerError(EventTime eventTime, ExoPlaybackException error) {
if (error.type == ExoPlaybackException.TYPE_SOURCE) {
IOException cause = error.getSourceException();
if (cause instanceof HttpDataSource.HttpDataSourceException) {
// An HTTP error occurred.
HttpDataSource.HttpDataSourceException httpError = (HttpDataSource.HttpDataSourceException) cause;
// It's possible to find out more about the error both by casting and by
// querying the cause.
if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
// Cast to InvalidResponseCodeException and retrieve the response code,
// message and headers.
HttpDataSource.InvalidResponseCodeException ex = (HttpDataSource.InvalidResponseCodeException) httpError;
TestFairy.addEvent(
String.format(Locale.ENGLISH, "Http error during playback - %d", ex.responseCode));
TestFairy.logThrowable(cause);
} else {
// Try calling httpError.getCause() to retrieve the underlying cause,
// although note that it may be null.
TestFairy.addEvent("Http error during playback before response");
Throwable innerCause = httpError.getCause();
if (innerCause != null) {
TestFairy.logThrowable(innerCause);
} else {
TestFairy.logThrowable(cause);
}
}
} else {
TestFairy.addEvent(String.format(Locale.ENGLISH, "Player error - %s", error.toString()));
TestFairy.logThrowable(cause);
}
}
}
Give it a run!
If everything went smoothly, you can start listening for all the mentioned events with the following code.
If you’d like to jump right in without needing to glue all these code blocks manually, simply copy this file into your project and enjoy your new analytics in your session dashboard.
This post is brought to you by TestFairy, a mobile testing platform that helps companies streamline their mobile development process and fix bugs faster. Read...