Tracking Video Events in Android TV (with MediaPlayer)
|byDiego Perini
When it comes to TV apps, one thing you can’t do enough is testing your video players. Unlike consumer phones, people’s expectations from TV apps are quite high. These apps don’t update too often and the main experience is mostly high quality video playback. As developers with high standards, we cannot tolerate any hiccups in our video players. In this post, we are going to learn how to do that by using your favorite testing platform.
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.
We are going to implement a wrapper which behind the scenes will send video playback events to an analytics server.
Don’t worry! The abstraction we are going to create will be able to work with any tracking mechanism other than TestFairy. All you will have to do is changing 5 lines if TestFairy is not suitable to your needs.
Here is a list of our goals:
Support for MediaPlayer from Android SDK
Support for MediaPlayerAdapter from leanback support libraries
A one liner API that looks like wrapper = wrap(mediaPlayer) or wrapper = wrap(mediaPlayerAdapter)
Sending analytic events for buffering and playback events
Keeping track of play/pause/error states
Not breaking existing functionality with MediaPlayer and MediaPlayerAdapter classes
Exposing existing public API from the wrapped classes to allow further extensions by users
“Hello TestFairyMediaPlayerGlue!”
Sorry for the obvious name
Let’s create some compile time unit tests for each scenario. We will compile these once we finish our implementations.
// use wrapper to configure further listeners and behavior
// Find/create a media player
android.media.MediaPlayer myMediaPlayer = ...;
// Initialize
TestFairyMediaPlayerGlue.PlayerWrapper wrapper = TestFairyMediaPlayerGlue.createByWrapping(myMediaPlayer);
// use wrapper to configure further listeners and behavior
// Find/create a media player
android.media.MediaPlayer myMediaPlayer = ...;
// Initialize
TestFairyMediaPlayerGlue.PlayerWrapper wrapper = TestFairyMediaPlayerGlue.createByWrapping(myMediaPlayer);
// use wrapper to configure further listeners and behavior
// use wrapper to configure further listeners and behavior
// Find/create a media player adapter
MediaPlayerAdapter myPlayerAdapter = ...;
// Initialize
TestFairyMediaPlayerGlue.PlayerAdapterWrapper wrapper = TestFairyMediaPlayerGlue.createByWrapping(myPlayerAdapter);
// use wrapper to configure further listeners and behavior
// Find/create a media player adapter
MediaPlayerAdapter myPlayerAdapter = ...;
// Initialize
TestFairyMediaPlayerGlue.PlayerAdapterWrapper wrapper = TestFairyMediaPlayerGlue.createByWrapping(myPlayerAdapter);
// use wrapper to configure further listeners and behavior
thrownewNullPointerException("MediaPlayerAdapter cannot be null.");
}
// TODO : Do adapter specific stuff, implement wrapper
return wrapper;
}
// TODO : implement PlayerWrapper
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
// TODO : Do player specific stuff, implement wrapper
return wrapper;
}
// TODO : implement PlayerAdapterWrapper
public static PlayerAdapterWrapper createByWrapping(@NonNull final MediaPlayerAdapter playerAdapter) {
if (playerAdapter == null) {
throw new NullPointerException("MediaPlayerAdapter cannot be null.");
}
// TODO : Do adapter specific stuff, implement wrapper
return wrapper;
}
// TODO : implement PlayerWrapper
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
// TODO : Do player specific stuff, implement wrapper
return wrapper;
}
// TODO : implement PlayerAdapterWrapper
public static PlayerAdapterWrapper createByWrapping(@NonNull final MediaPlayerAdapter playerAdapter) {
if (playerAdapter == null) {
throw new NullPointerException("MediaPlayerAdapter cannot be null.");
}
// TODO : Do adapter specific stuff, implement wrapper
return wrapper;
}
We could define a strategy pattern to decouple the wrapper internals from the factory but we find that there aren’t that many video players for Android TV out there. The complete implementation is still small enough to fit in a single java file after all. It is also safe to not expect from people to use different kind of players in different parts of their TV apps. Thus, if we wanted to implement a new factory for another video player such as Google’s famous ExoPlayer, we would probably put it in a java file of its own and remove anything related to MediaPlayer and leanback support library inside the implementation. This post will therefore leave usage of such design patterns as an excersice to the reader.
What to use from the Android SDK
MediaPlayer already supports the events below out of box. The wrapper for them will be quite straight forward.
Unfortunately, MediaPlayer doesn’t ship a playback event that is triggered whenever current play percentage changes. We need to implement our own. Making it optional (default is turned on) is a good idea in order not to spam our events server.
We can instantiate an anonymous class which will intercept all these events, send them to TestFairy, and then republish all of it to the wrapped listeners.
What to use from the leanback library
Luckily, MediaPlayerAdapter already has a callback api named setCallback(PlayerAdapter.Callback) that provides all the events we need to listen. We will wrap that and add the extra configuration we defined earlier to be consistent accors different usages.
Our glue will provide a default implementation just in case.
The Glue
The implementers of these interfaces will be created by a single factory class named TestFairyMediaPlayerGlue. Each time we request a wrapper, this factory will temporarily instantiate itself to configure the returned wrappers. Keeping track of how we created our temporary factory will help us share some of the functionality in both scenarios while isolating the non-shared state.
Remember that MediaPlayer still needs our help to support playback position tracking. This will pollute FromMediaPlayer a little but no worries, it is just a few lines.
private static abstract class CreatedFrom {
protected abstract void registerCurrentPositionTracker(final Runnable runnable);
protected abstract void unRegisterCurrentPositionTracker();
private abstract static class FromMediaPlayer extends CreatedFrom {
private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener;
private MediaPlayer.OnCompletionListener onCompletionListener;
private MediaPlayer.OnErrorListener onErrorListener;
private MediaPlayer.OnMediaTimeDiscontinuityListener onMediaTimeDiscontinuityListener;
private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener;
private static Runnable createPositionTracker(final MediaPlayer mediaPlayer, final TestFairyMediaPlayerGlue factory) {
return new Runnable() {
@Override
public void run() {
// TODO : implement a single event dispatch for playback progress
}
};
}
// TODO : wrap a player to be able to periodically query for playback progress
protected abstract MediaPlayer getMediaPlayer();
}
private abstract static class FromMediaPlayerAdapter extends CreatedFrom {
private PlayerAdapter.Callback playerAdapterListenerCallbacks;
}
}
private static abstract class CreatedFrom {
protected abstract void registerCurrentPositionTracker(final Runnable runnable);
protected abstract void unRegisterCurrentPositionTracker();
private abstract static class FromMediaPlayer extends CreatedFrom {
private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener;
private MediaPlayer.OnCompletionListener onCompletionListener;
private MediaPlayer.OnErrorListener onErrorListener;
private MediaPlayer.OnMediaTimeDiscontinuityListener onMediaTimeDiscontinuityListener;
private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener;
private static Runnable createPositionTracker(final MediaPlayer mediaPlayer, final TestFairyMediaPlayerGlue factory) {
return new Runnable() {
@Override
public void run() {
// TODO : implement a single event dispatch for playback progress
}
};
}
// TODO : wrap a player to be able to periodically query for playback progress
protected abstract MediaPlayer getMediaPlayer();
}
private abstract static class FromMediaPlayerAdapter extends CreatedFrom {
private PlayerAdapter.Callback playerAdapterListenerCallbacks;
}
}
Now that our public *Wrapper interfaces and private states named conveniently as CreatedFrom are defined, we can start filling the very first TODO notes we created earlier.
// Factory
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayer() {
private final Handler handler = new Handler();
private Runnable currentPositionTracker;
@Override
protected void registerCurrentPositionTracker(final Runnable runnable) {
// TODO : Run given runnable every few seconds to keep track of playback progress
}
@Override
protected void unRegisterCurrentPositionTracker() {
// TODO : unregisted any previously registered tracker
}
// TODO : use this to ask for the player in the tracker
@Override
protected MediaPlayer getMediaPlayer() {
return mediaPlayer;
}
});
final CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) listener.createdFrom;
castedCreationMethod.registerCurrentPositionTracker(CreatedFrom.FromMediaPlayer.createPositionTracker(mediaPlayer, listener));
final PlayerWrapperImpl playerWrapper = listener.createPlayerWrapper();
mediaPlayer.setOnBufferingUpdateListener(playerWrapper);
mediaPlayer.setOnCompletionListener(playerWrapper);
mediaPlayer.setOnErrorListener(playerWrapper);
mediaPlayer.setOnMediaTimeDiscontinuityListener(playerWrapper);
mediaPlayer.setOnSeekCompleteListener(playerWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return playerWrapper;
}
// Wrapper creation
private static abstract class PlayerWrapperImpl implements PlayerWrapper, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnMediaTimeDiscontinuityListener, MediaPlayer.OnSeekCompleteListener {
}
private PlayerWrapperImpl createPlayerWrapper() {
return new PlayerWrapperImpl() {
private int lastKnownBufferingPercent = -1;
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onBufferingUpdateListener != null) {
castedCreationMethod.onBufferingUpdateListener.onBufferingUpdate(mp, percent);
}
}
if (lastKnownBufferingPercent != percent && testFairyBridge != null) {
testFairyBridge.onBufferingUpdate(percent);
}
lastKnownBufferingPercent = percent;
}
@Override
public void onCompletion(MediaPlayer mp) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onCompletionListener != null) {
castedCreationMethod.onCompletionListener.onCompletion(mp);
}
}
if (testFairyBridge != null) {
testFairyBridge.onComplete();
}
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
boolean onErrorResult = false;
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onErrorListener != null) {
onErrorResult = castedCreationMethod.onErrorListener.onError(mp, what, extra);
}
}
if (testFairyBridge != null) {
testFairyBridge.onError(what, extra);
}
return onErrorResult;
}
private boolean lastKnownPlaybackStateIsPlaying = false;
@Override
public void onMediaTimeDiscontinuity(MediaPlayer mp, MediaTimestamp mts) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onMediaTimeDiscontinuityListener != null) {
castedCreationMethod.onMediaTimeDiscontinuityListener.onMediaTimeDiscontinuity(mp, mts);
}
}
if (mp.isPlaying() != lastKnownPlaybackStateIsPlaying && testFairyBridge != null) {
testFairyBridge.onPlaybackStateChange(mp.isPlaying());
}
lastKnownPlaybackStateIsPlaying = mp.isPlaying();
}
@Override
public void onSeekComplete(MediaPlayer mp) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onSeekCompleteListener != null) {
castedCreationMethod.onSeekCompleteListener.onSeekComplete(mp);
}
}
}
@Override
public void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onBufferingUpdateListener = onBufferingUpdateListener;
}
}
@Override
public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onCompletionListener = onCompletionListener;
}
}
@Override
public void setOnErrorListener(MediaPlayer.OnErrorListener onErrorListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onErrorListener = onErrorListener;
}
}
@Override
public void setOnMediaTimeDiscontinuityListener(MediaPlayer.OnMediaTimeDiscontinuityListener onMediaTimeDiscontinuityListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onMediaTimeDiscontinuityListener = onMediaTimeDiscontinuityListener;
}
}
@Override
public void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener onSeekCompleteListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onSeekCompleteListener = onSeekCompleteListener;
}
}
@Override
public void trackPlaybackPosition() {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
TestFairyMediaPlayerGlue.this.createdFrom.registerCurrentPositionTracker(
CreatedFrom.FromMediaPlayer.createPositionTracker(
castedCreationMethod.getMediaPlayer(),
TestFairyMediaPlayerGlue.this
)
);
}
}
@Override
public void untrackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.unRegisterCurrentPositionTracker();
}
@Override
public void setTestFairyBridge(TestFairyBridge bridge) {
testFairyBridge = bridge;
}
@Override
public TestFairyBridge getTestFairyBridge() {
return testFairyBridge;
}
};
}
// Factory
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayer() {
private final Handler handler = new Handler();
private Runnable currentPositionTracker;
@Override
protected void registerCurrentPositionTracker(final Runnable runnable) {
// TODO : Run given runnable every few seconds to keep track of playback progress
}
@Override
protected void unRegisterCurrentPositionTracker() {
// TODO : unregisted any previously registered tracker
}
// TODO : use this to ask for the player in the tracker
@Override
protected MediaPlayer getMediaPlayer() {
return mediaPlayer;
}
});
final CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) listener.createdFrom;
castedCreationMethod.registerCurrentPositionTracker(CreatedFrom.FromMediaPlayer.createPositionTracker(mediaPlayer, listener));
final PlayerWrapperImpl playerWrapper = listener.createPlayerWrapper();
mediaPlayer.setOnBufferingUpdateListener(playerWrapper);
mediaPlayer.setOnCompletionListener(playerWrapper);
mediaPlayer.setOnErrorListener(playerWrapper);
mediaPlayer.setOnMediaTimeDiscontinuityListener(playerWrapper);
mediaPlayer.setOnSeekCompleteListener(playerWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return playerWrapper;
}
// Wrapper creation
private static abstract class PlayerWrapperImpl implements PlayerWrapper, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnMediaTimeDiscontinuityListener, MediaPlayer.OnSeekCompleteListener {
}
private PlayerWrapperImpl createPlayerWrapper() {
return new PlayerWrapperImpl() {
private int lastKnownBufferingPercent = -1;
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onBufferingUpdateListener != null) {
castedCreationMethod.onBufferingUpdateListener.onBufferingUpdate(mp, percent);
}
}
if (lastKnownBufferingPercent != percent && testFairyBridge != null) {
testFairyBridge.onBufferingUpdate(percent);
}
lastKnownBufferingPercent = percent;
}
@Override
public void onCompletion(MediaPlayer mp) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onCompletionListener != null) {
castedCreationMethod.onCompletionListener.onCompletion(mp);
}
}
if (testFairyBridge != null) {
testFairyBridge.onComplete();
}
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
boolean onErrorResult = false;
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onErrorListener != null) {
onErrorResult = castedCreationMethod.onErrorListener.onError(mp, what, extra);
}
}
if (testFairyBridge != null) {
testFairyBridge.onError(what, extra);
}
return onErrorResult;
}
private boolean lastKnownPlaybackStateIsPlaying = false;
@Override
public void onMediaTimeDiscontinuity(MediaPlayer mp, MediaTimestamp mts) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onMediaTimeDiscontinuityListener != null) {
castedCreationMethod.onMediaTimeDiscontinuityListener.onMediaTimeDiscontinuity(mp, mts);
}
}
if (mp.isPlaying() != lastKnownPlaybackStateIsPlaying && testFairyBridge != null) {
testFairyBridge.onPlaybackStateChange(mp.isPlaying());
}
lastKnownPlaybackStateIsPlaying = mp.isPlaying();
}
@Override
public void onSeekComplete(MediaPlayer mp) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
if (castedCreationMethod.onSeekCompleteListener != null) {
castedCreationMethod.onSeekCompleteListener.onSeekComplete(mp);
}
}
}
@Override
public void setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onBufferingUpdateListener = onBufferingUpdateListener;
}
}
@Override
public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onCompletionListener = onCompletionListener;
}
}
@Override
public void setOnErrorListener(MediaPlayer.OnErrorListener onErrorListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onErrorListener = onErrorListener;
}
}
@Override
public void setOnMediaTimeDiscontinuityListener(MediaPlayer.OnMediaTimeDiscontinuityListener onMediaTimeDiscontinuityListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onMediaTimeDiscontinuityListener = onMediaTimeDiscontinuityListener;
}
}
@Override
public void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener onSeekCompleteListener) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
castedCreationMethod.onSeekCompleteListener = onSeekCompleteListener;
}
}
@Override
public void trackPlaybackPosition() {
if (createdFrom instanceof CreatedFrom.FromMediaPlayer) {
CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) createdFrom;
TestFairyMediaPlayerGlue.this.createdFrom.registerCurrentPositionTracker(
CreatedFrom.FromMediaPlayer.createPositionTracker(
castedCreationMethod.getMediaPlayer(),
TestFairyMediaPlayerGlue.this
)
);
}
}
@Override
public void untrackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.unRegisterCurrentPositionTracker();
}
@Override
public void setTestFairyBridge(TestFairyBridge bridge) {
testFairyBridge = bridge;
}
@Override
public TestFairyBridge getTestFairyBridge() {
return testFairyBridge;
}
};
}
Note that many of the overriden listeners delegate their events to the wrapped ones. We decorate some of these delegations to be able to dispatch our analytic events.
// Factory
public static PlayerAdapterWrapper createByWrapping(@NonNull final MediaPlayerAdapter playerAdapter) {
if (playerAdapter == null) {
throw new NullPointerException("MediaPlayerAdapter cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayerAdapter() {
@Override
protected void registerCurrentPositionTracker(Runnable _) {
unRegisterCurrentPositionTracker();
playerAdapter.setProgressUpdatingEnabled(true);
}
@Override
protected void unRegisterCurrentPositionTracker() {
playerAdapter.setProgressUpdatingEnabled(false);
}
});
final PlayerAdapterWrapperImpl callbacksWrapper = listener.createPlayerAdapterCallbacksWrapper(playerAdapter.getCallback());
playerAdapter.setCallback(callbacksWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return callbacksWrapper;
}
// Wrapper creation
private static abstract class PlayerAdapterWrapperImpl extends PlayerAdapter.Callback implements PlayerAdapterWrapper {
}
private PlayerAdapterWrapperImpl createPlayerAdapterCallbacksWrapper(final PlayerAdapter.Callback originalCallbacks) {
return new PlayerAdapterWrapperImpl() {
private boolean lastKnownPlaybackStateIsPlaying = false;
@Override
public void onPlayStateChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPlayStateChanged(adapter);
}
originalCallbacks.onPlayStateChanged(adapter);
}
if (adapter.isPlaying() != lastKnownPlaybackStateIsPlaying && testFairyBridge != null) {
testFairyBridge.onPlaybackStateChange(adapter.isPlaying());
}
lastKnownPlaybackStateIsPlaying = adapter.isPlaying();
}
@Override
public void onPreparedStateChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPreparedStateChanged(adapter);
}
originalCallbacks.onPreparedStateChanged(adapter);
}
}
@Override
public void onPlayCompleted(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPlayCompleted(adapter);
}
originalCallbacks.onPlayCompleted(adapter);
}
if (testFairyBridge != null) {
testFairyBridge.onComplete();
}
}
private int lastKnownPlaybackPercent = -1;
@Override
public void onCurrentPositionChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onCurrentPositionChanged(adapter);
}
originalCallbacks.onCurrentPositionChanged(adapter);
}
if (adapter.getDuration() != 0) {
long currentPosition = adapter.getCurrentPosition();
long percent = (currentPosition * 100) / adapter.getDuration();
if (lastKnownPlaybackPercent != percent && testFairyBridge != null) {
testFairyBridge.onPlaybackPositionUpdate((int) percent);
}
lastKnownPlaybackPercent = (int) percent;
}
}
private int lastKnownBufferingPercent = -1;
@Override
public void onBufferedPositionChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onBufferedPositionChanged(adapter);
}
originalCallbacks.onBufferedPositionChanged(adapter);
}
if (adapter.getDuration() != 0) {
long currentPosition = adapter.getBufferedPosition();
long percent = (currentPosition * 100) / adapter.getDuration();
if (lastKnownBufferingPercent != percent && testFairyBridge != null) {
testFairyBridge.onBufferingUpdate((int) percent);
}
lastKnownBufferingPercent = (int) percent;
}
}
@Override
public void onDurationChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onDurationChanged(adapter);
}
originalCallbacks.onDurationChanged(adapter);
}
}
@Override
public void onVideoSizeChanged(PlayerAdapter adapter, int width, int height) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onVideoSizeChanged(adapter, width, height);
}
originalCallbacks.onVideoSizeChanged(adapter, width, height);
}
}
@Override
public void onError(PlayerAdapter adapter, int errorCode, String errorMessage) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onError(adapter, errorCode, errorMessage);
}
originalCallbacks.onError(adapter, errorCode, errorMessage);
}
if (testFairyBridge != null) {
testFairyBridge.onError(errorCode, errorMessage);
}
}
@Override
public void onBufferingStateChanged(PlayerAdapter adapter, boolean start) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onBufferingStateChanged(adapter, start);
}
originalCallbacks.onBufferingStateChanged(adapter, start);
}
}
@Override
public void onMetadataChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onMetadataChanged(adapter);
}
originalCallbacks.onMetadataChanged(adapter);
}
}
@Override
public void setCallbacks(PlayerAdapter.Callback callbacks) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks = callbacks;
}
}
}
@Override
public void trackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.registerCurrentPositionTracker(null);
}
@Override
public void untrackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.unRegisterCurrentPositionTracker();
}
@Override
public void setTestFairyBridge(TestFairyBridge bridge) {
testFairyBridge = bridge;
}
@Override
public TestFairyBridge getTestFairyBridge() {
return testFairyBridge;
}
};
}
// Factory
public static PlayerAdapterWrapper createByWrapping(@NonNull final MediaPlayerAdapter playerAdapter) {
if (playerAdapter == null) {
throw new NullPointerException("MediaPlayerAdapter cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayerAdapter() {
@Override
protected void registerCurrentPositionTracker(Runnable _) {
unRegisterCurrentPositionTracker();
playerAdapter.setProgressUpdatingEnabled(true);
}
@Override
protected void unRegisterCurrentPositionTracker() {
playerAdapter.setProgressUpdatingEnabled(false);
}
});
final PlayerAdapterWrapperImpl callbacksWrapper = listener.createPlayerAdapterCallbacksWrapper(playerAdapter.getCallback());
playerAdapter.setCallback(callbacksWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return callbacksWrapper;
}
// Wrapper creation
private static abstract class PlayerAdapterWrapperImpl extends PlayerAdapter.Callback implements PlayerAdapterWrapper {
}
private PlayerAdapterWrapperImpl createPlayerAdapterCallbacksWrapper(final PlayerAdapter.Callback originalCallbacks) {
return new PlayerAdapterWrapperImpl() {
private boolean lastKnownPlaybackStateIsPlaying = false;
@Override
public void onPlayStateChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPlayStateChanged(adapter);
}
originalCallbacks.onPlayStateChanged(adapter);
}
if (adapter.isPlaying() != lastKnownPlaybackStateIsPlaying && testFairyBridge != null) {
testFairyBridge.onPlaybackStateChange(adapter.isPlaying());
}
lastKnownPlaybackStateIsPlaying = adapter.isPlaying();
}
@Override
public void onPreparedStateChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPreparedStateChanged(adapter);
}
originalCallbacks.onPreparedStateChanged(adapter);
}
}
@Override
public void onPlayCompleted(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onPlayCompleted(adapter);
}
originalCallbacks.onPlayCompleted(adapter);
}
if (testFairyBridge != null) {
testFairyBridge.onComplete();
}
}
private int lastKnownPlaybackPercent = -1;
@Override
public void onCurrentPositionChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onCurrentPositionChanged(adapter);
}
originalCallbacks.onCurrentPositionChanged(adapter);
}
if (adapter.getDuration() != 0) {
long currentPosition = adapter.getCurrentPosition();
long percent = (currentPosition * 100) / adapter.getDuration();
if (lastKnownPlaybackPercent != percent && testFairyBridge != null) {
testFairyBridge.onPlaybackPositionUpdate((int) percent);
}
lastKnownPlaybackPercent = (int) percent;
}
}
private int lastKnownBufferingPercent = -1;
@Override
public void onBufferedPositionChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onBufferedPositionChanged(adapter);
}
originalCallbacks.onBufferedPositionChanged(adapter);
}
if (adapter.getDuration() != 0) {
long currentPosition = adapter.getBufferedPosition();
long percent = (currentPosition * 100) / adapter.getDuration();
if (lastKnownBufferingPercent != percent && testFairyBridge != null) {
testFairyBridge.onBufferingUpdate((int) percent);
}
lastKnownBufferingPercent = (int) percent;
}
}
@Override
public void onDurationChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onDurationChanged(adapter);
}
originalCallbacks.onDurationChanged(adapter);
}
}
@Override
public void onVideoSizeChanged(PlayerAdapter adapter, int width, int height) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onVideoSizeChanged(adapter, width, height);
}
originalCallbacks.onVideoSizeChanged(adapter, width, height);
}
}
@Override
public void onError(PlayerAdapter adapter, int errorCode, String errorMessage) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onError(adapter, errorCode, errorMessage);
}
originalCallbacks.onError(adapter, errorCode, errorMessage);
}
if (testFairyBridge != null) {
testFairyBridge.onError(errorCode, errorMessage);
}
}
@Override
public void onBufferingStateChanged(PlayerAdapter adapter, boolean start) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onBufferingStateChanged(adapter, start);
}
originalCallbacks.onBufferingStateChanged(adapter, start);
}
}
@Override
public void onMetadataChanged(PlayerAdapter adapter) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks.onMetadataChanged(adapter);
}
originalCallbacks.onMetadataChanged(adapter);
}
}
@Override
public void setCallbacks(PlayerAdapter.Callback callbacks) {
if (createdFrom instanceof CreatedFrom.FromMediaPlayerAdapter) {
CreatedFrom.FromMediaPlayerAdapter castedCreationMethod = (CreatedFrom.FromMediaPlayerAdapter) createdFrom;
if (castedCreationMethod.playerAdapterListenerCallbacks != null) {
castedCreationMethod.playerAdapterListenerCallbacks = callbacks;
}
}
}
@Override
public void trackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.registerCurrentPositionTracker(null);
}
@Override
public void untrackPlaybackPosition() {
TestFairyMediaPlayerGlue.this.createdFrom.unRegisterCurrentPositionTracker();
}
@Override
public void setTestFairyBridge(TestFairyBridge bridge) {
testFairyBridge = bridge;
}
@Override
public TestFairyBridge getTestFairyBridge() {
return testFairyBridge;
}
};
}
Note that we still keep a reference to the original callbacks before overriding them to be able to keep all the functionality bundled with the adapter. Not doing so would break the existing code in our wrapped MediaPlayerAdapter users.
Missing Pieces
The adapter test is ready for compilation but we still lack some functionality in the player’s. CreatedFrom.FromMediaPlayer have to define how it queries the player for current progress.
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return playerWrapper;
}
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayer() {
private final Handler handler = new Handler();
private Runnable currentPositionTracker;
@Override
protected void registerCurrentPositionTracker(final Runnable runnable) {
unRegisterCurrentPositionTracker();
// Wrap given runnable to call it periodically
currentPositionTracker = new Runnable() {
@Override
public void run() {
runnable.run();
handler.postDelayed(currentPositionTracker, 1000);
}
};
// Ask for current position every second (1000ms)
handler.postDelayed(currentPositionTracker, 1000);
}
@Override
protected void unRegisterCurrentPositionTracker() {
if (currentPositionTracker != null) {
handler.removeCallbacks(currentPositionTracker);
currentPositionTracker = null;
}
}
@Override
protected MediaPlayer getMediaPlayer() {
return mediaPlayer;
}
});
final CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) listener.createdFrom;
castedCreationMethod.registerCurrentPositionTracker(CreatedFrom.FromMediaPlayer.createPositionTracker(mediaPlayer, listener));
final PlayerWrapperImpl playerWrapper = listener.createPlayerWrapper();
mediaPlayer.setOnBufferingUpdateListener(playerWrapper);
mediaPlayer.setOnCompletionListener(playerWrapper);
mediaPlayer.setOnErrorListener(playerWrapper);
mediaPlayer.setOnMediaTimeDiscontinuityListener(playerWrapper);
mediaPlayer.setOnSeekCompleteListener(playerWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return playerWrapper;
}
public static PlayerWrapper createByWrapping(@NonNull final MediaPlayer mediaPlayer) {
if (mediaPlayer == null) {
throw new NullPointerException("MediaPlayer cannot be null.");
}
final TestFairyMediaPlayerGlue listener = new TestFairyMediaPlayerGlue(new CreatedFrom.FromMediaPlayer() {
private final Handler handler = new Handler();
private Runnable currentPositionTracker;
@Override
protected void registerCurrentPositionTracker(final Runnable runnable) {
unRegisterCurrentPositionTracker();
// Wrap given runnable to call it periodically
currentPositionTracker = new Runnable() {
@Override
public void run() {
runnable.run();
handler.postDelayed(currentPositionTracker, 1000);
}
};
// Ask for current position every second (1000ms)
handler.postDelayed(currentPositionTracker, 1000);
}
@Override
protected void unRegisterCurrentPositionTracker() {
if (currentPositionTracker != null) {
handler.removeCallbacks(currentPositionTracker);
currentPositionTracker = null;
}
}
@Override
protected MediaPlayer getMediaPlayer() {
return mediaPlayer;
}
});
final CreatedFrom.FromMediaPlayer castedCreationMethod = (CreatedFrom.FromMediaPlayer) listener.createdFrom;
castedCreationMethod.registerCurrentPositionTracker(CreatedFrom.FromMediaPlayer.createPositionTracker(mediaPlayer, listener));
final PlayerWrapperImpl playerWrapper = listener.createPlayerWrapper();
mediaPlayer.setOnBufferingUpdateListener(playerWrapper);
mediaPlayer.setOnCompletionListener(playerWrapper);
mediaPlayer.setOnErrorListener(playerWrapper);
mediaPlayer.setOnMediaTimeDiscontinuityListener(playerWrapper);
mediaPlayer.setOnSeekCompleteListener(playerWrapper);
if (TestFairy.getSessionUrl() == null) {
Log.w("TestFairyMediaPlayerGlue", "Media player events will not be sent unless you call TestFairy.begin()");
}
return playerWrapper;
}
And the given tracker should convert timestamps to percentages for simpler events.
Unity is a great platform for developers. At TestFairy, we’re committed to helping Unity developers that want to use our SDK. Part of that commitment...
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...