I have a media player on one activity (called player) and I want to be able to support continuous video playback from when the player is closed into a miniature view on the parent activity.
I am able to do this just fine when it is audio only, the problem lies when I attach a SurfaceView via mediaPlayer.setDisplay();
I can attach the SurfaceView just fine initially but the problems start when I close the Player activity. If I make no changes, the mediaPlayer gets thrown into an error state somehow with the usual unhelpful errors (1, -19) etc.
I have tried using setDisplay(null) when the Player SurfaceView is destroyed which appears to work. But for some reason it resets the video stream. I've tried overriding seekTo() in order to figure out what is happening but seekTo() is not being called. I've also put logging statements everywhere I can think of but nothing is being triggered.
Why would setDisplay(null) cause my video stream to restart?
Here is my current MediaPlayer code (some of the weird stuff is from me trying to solve the issue (like isReallyPlaying()):
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture beeperhandle;
private boolean isPaused = false;
private BaseMedia currentMedia;
private PlaybackService.RepeatStatus repeatStatus = PlaybackService.RepeatStatus.REPEAT_NONE;
public void startMedia(BaseMedia model, Integer position) {
Timber.d("Starting media");
startBeeper();
isPaused = false;
lastBeep = -1;
currentMedia = model;
if (position != null) {
seekTo(position);
}
super.start();
}
public BaseMedia getCurrentMedia() {
return currentMedia;
}
#Override
public void start() throws IllegalStateException {
Timber.e("Invalid start called, should request startSong or startVideo");
}
private int lastBeep = -1;
// Because isPlaying is returning false and canceling the beeper. Probably has something to do with the surfaceview being destroyed
private boolean isStillPlaying() {
if (lastBeep != getCurrentPosition()) {
lastBeep = getCurrentPosition();
return true;
}
return false;
}
private final Runnable seekBarCheck = new Runnable() {
public void run() {
if (isStillPlaying() && !beeperhandle.isCancelled()) {
EventBus.getDefault().post(new MusicStatusTimeEvent(
currentMedia, true, GevaldMediaPlayer.this));
} else {
Timber.d("Canceling Beeper, !isPlaying");
beeperhandle.cancel(true);
}
}
};
private void startBeeper() {
Timber.d("Starting Beeper");
beeperhandle = scheduler.scheduleAtFixedRate(seekBarCheck, 100, 100, TimeUnit.MILLISECONDS);
}
#Override
public void seekTo(final int msec) throws IllegalStateException {
Timber.d("Seeking to " + msec);
if (beeperhandle != null) {
Timber.d("Canceling beeper in prep for seek");
beeperhandle.cancel(true);
}
setOnSeekCompleteListener(new OnSeekCompleteListener() {
#Override
public void onSeekComplete(MediaPlayer mp) {
Timber.d("Seek complete to: " + msec);
startBeeper();
}
});
super.seekTo(msec);
}
#Override
public void stop() throws IllegalStateException {
super.stop();
Timber.d("Stopping media");
doStop();
}
private void doStop() {
if (beeperhandle != null) {
Timber.d("Canceling beeper, doStop");
beeperhandle.cancel(true);
}
isPaused = false;
}
#Override
public void pause() throws IllegalStateException {
Timber.d("Pause requested");
if (beeperhandle != null) {
Timber.d("Canceling beeper, pause");
beeperhandle.cancel(true);
}
doStop();
EventBus.getDefault().post(new MusicStatusStoppedEvent(this));
super.pause();
}
public boolean isPaused() {
return isPaused;
}
Figured it out. Apparently closing an activity causes an audio loss with a value of AudioManager.AUDIOFOCUS_LOSS.
Since I was being a good Android citizen, that was set to release the media player. But audio would then be regained which would cause my media player to get reset and hence start from the beginning.
It just happened to line up that this occurred right around the setDisplay() method.
Related
I start by loading a Media player into a composition class:
public class MediaPlayerWURI {
private final MediaPlayer mediaPlayer;
final Uri uri;
final ActivityMain activityMain;
boolean isPrepared = true;
MediaPlayerWURI(ActivityMain activityMain, MediaPlayer mediaPlayer, Uri uri){
this.activityMain = activityMain;
this.mediaPlayer = mediaPlayer;
this.uri= uri;
mediaPlayer.setOnPreparedListener(null);
mediaPlayer.setOnErrorListener(null);
mediaPlayer.setOnPreparedListener(new MOnPreparedListener(this));
mediaPlayer.setOnErrorListener(new MOnErrorListener());
}
public void prepareAsync(){
isPrepared = false;
mediaPlayer.prepareAsync();
}
public void start(){
mediaPlayer.start();
}
public void stop(){
isPrepared = false;
mediaPlayer.stop();
}
class MOnPreparedListener implements MediaPlayer.OnPreparedListener{
final MediaPlayerWURI mediaPlayerWURI;
public MOnPreparedListener(MediaPlayerWURI mediaPlayerWURI){
this.mediaPlayerWURI = mediaPlayerWURI;
}
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayerWURI.isPrepared = true;
}
}
class MOnErrorListener implements MediaPlayer.OnErrorListener {
public MOnErrorListener(){
}
#Override
public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
activityMain.releaseMediaPlayers();
return false;
}
}
}
The media player passed in is created with MediaPlayer.create(getApplicationContext()) and is started successfully.
The following code do not trigger onPrepared() and gets stuck in a loop.
mediaPlayerWURI.stop();
mediaPlayerWURI.prepareAsync();
while (!mediaPlayerWURI.isPrepared) { }
mediaPlayerWURI.start();
I have tried prepareAsync() on another thread:
executorService.submit(new Runnable() {
#Override
public void run() {
mediaPlayerWURI.prepareAsync();
}
});
My guess is it is a threading issue, but I am not sure how to handle this, or if it even is a threading issue. My understanding is that the MediaPlayer is preparing in another thread and that the loop shouldn't prevent it from calling on prepared. I am not sure what thread onPrepare() is ran on, but from the above, I think it means the main thread is supposed to run onPrepare() and is waiting for the loop to end.
Also, I am getting weird behavior where onPrepared() is being called after the construction of the MediaPlayer. Is that normal? My assumption is that onPrepared() is called when setOnPrepared() is called on a prepared MediaPlayer. This means the listener is attached.
The problem was that while waiting for the MediaPlayer to be prepared while (!mediaPlayerWURI.isPrepared) { }, I was hogging the UI thread, which is the same thread that onPrepared() uses.
To fix this, I had to stop hogging the UI thread. I added a boolean to my MediaPlayerWURI wrapper class that indicates to play the MediaPlayer on prepared.
private final MediaPlayer mediaPlayer;
volatile boolean isPrepared;
volatile boolean shouldPlay;
synchronized public void shouldStart(boolean shouldPlay){
if(shouldPlay && isPrepared){
mediaPlayer.start();
} else {
this.shouldPlay = shouldPlay;
}
}
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
synchronized (mediaPlayerWURI) {
mediaPlayerWURI.isPrepared = true;
if (shouldPlay) {
mediaPlayer.start();
shouldPlay = false;
}
}
}
I am trying to use ExoPlayer, as opposed to MediaPlayer and I can't seem to figure it out...
MediaPlayer has .start() / .pause() commands... and I can just seekTo(1287) and it automatically starts playing...
How do I do this with ExoPlayer? I have tried to do seekTo(1287) but it doesn't start playing after... I have also added .setPlayWhenReady(true) after that, and still no luck...
I am able to .stop()... but I can't get it to start playing again after that unless I .prepare() again... but I don't think I should have to do that between every pause and play.
I am using my own controls and methods opposed to MediaController like in the ExoPlayer Demo... I can't quite see how the controls are implemented...
Any suggestions anyone?
Edit:
OK, I figured out pause and start...
.setPlayWhenReady(true) // start
.setPlayWhenReady(false) // pause
But I'm still having problems with the tracking... .seekTo works intermittently... sometimes it works... but other times I get this error:
E/AudioTrack: AudioTrack::set : Exit
(and it only gets to the buffer state... doesn't quite get to "ready"...
The official example of the PlayerControl in the ExoPlayer source code do exactly what you asked:
public class PlayerControl implements MediaPlayerControl {
private final ExoPlayer exoPlayer;
public PlayerControl(ExoPlayer exoPlayer) {
this.exoPlayer = exoPlayer;
}
#Override
public boolean canPause() {
return true;
}
#Override
public boolean canSeekBackward() {
return true;
}
#Override
public boolean canSeekForward() {
return true;
}
#Override
public int getAudioSessionId() {
throw new UnsupportedOperationException();
}
#Override
public int getBufferPercentage() {
return exoPlayer.getBufferedPercentage();
}
#Override
public int getCurrentPosition() {
return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getCurrentPosition();
}
#Override
public int getDuration() {
return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: (int) exoPlayer.getDuration();
}
#Override
public boolean isPlaying() {
return exoPlayer.getPlayWhenReady();
}
#Override
public void start() {
exoPlayer.setPlayWhenReady(true);
}
#Override
public void pause() {
exoPlayer.setPlayWhenReady(false);
}
#Override
public void seekTo(int timeMillis) {
long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0
: Math.min(Math.max(0, timeMillis), getDuration());
exoPlayer.seekTo(seekPosition);
}
}
If you are experiencing strange behaviors during the seek operation, it may be due to you particular stream/file type. I can suggest you to take a look at the base implementation of the ExoPlayer and, eventually, report any issue on Github.
Here's how the example code does it for Exoplayer 2:
player.setPlayWhenReady(true);
starts playback, (false stops)
If the player is already in the ready state then this method can be used to pause and resume playback.
To seek, they use
boolean haveStartPosition = startWindow != C.INDEX_UNSET;
if (haveStartPosition) {
player.seekTo(startWindow, startPosition);
}
player.prepare(mediaSource, !haveStartPosition, false);
So it seems you need to prepare after the seekTo.
How can I monitor progress changes on ExoPlayer?
I tried to implement a hidden MediaController and overriding setOnSeekBarChangeListener methods, but for now without success. I'm wondering if there is another way to listen to the ExoPlayer progress.
I know this question is very old. But, I landed on this while implementing ExoPlayer. This is to help the others who do the same later on:)
So, I have followed the following methods to track progress of the playback. This is the way it is done in the ExoPlayer Google Docs. It works as needed.
Checkout PlayerControlView.java in Google ExoPlayer repository
updateProgressBar() is the function to update the SeekBar progress:
private void updateProgressBar() {
long duration = player == null ? 0 : player.getDuration();
long position = player == null ? 0 : player.getCurrentPosition();
if (!dragging) {
mSeekBar.setProgress(progressBarValue(position));
}
long bufferedPosition = player == null ? 0 : player.getBufferedPosition();
mSeekBar.setSecondaryProgress(progressBarValue(bufferedPosition));
// Remove scheduled updates.
handler.removeCallbacks(updateProgressAction);
// Schedule an update if necessary.
int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
long delayMs;
if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) {
delayMs = 1000 - (position % 1000);
if (delayMs < 200) {
delayMs += 1000;
}
} else {
delayMs = 1000;
}
handler.postDelayed(updateProgressAction, delayMs);
}
}
private final Runnable updateProgressAction = new Runnable() {
#Override
public void run() {
updateProgressBar();
}
};
We call updateProgressBar() within updateProgressAction repeatedly until the playback stops.
The function is called the first time whenever there is a state change. We use removeCallbacks(Runnable runnable) so that there is always one updateProgressAction to care about.
#Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateProgressBar();
}
Hope this helps!
Just try this, its working for me :
handler = new Handler();
runnable = new Runnable() {
#Override
public void run() {
progressbar.setProgress((int) ((exoPlayer.getCurrentPosition()*100)/exoPlayer.getDuration()));
handler.postDelayed(runnable, 1000);
}
};
handler.postDelayed(runnable, 0);
Here,
getCurrentPosition() : return The current playback position in milliseconds.
getDuration(): The duration of the track in millisecond.
I've found a pretty elegant solution using RxJava. This involves a polling pattern as well, but we make sure to use an interval to poll every 1 second.
public Observable<Long> playbackProgressObservable =
Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
The logic here is we create an Observable that will emit a sequential number every second. We then use the map operator to transform the number into the current playback position.
public Observable<Long> playbackProgressObservable =
Observable.interval(1, TimeUnit.SECONDS)
.map( { exoPlayer.getCurrentPosition() } );
To finally hooked this together, just call subscribe, ad the progress updates will be emitted every second:
playbackProgressObservable.subscribe( { progress -> // Update logic here } )
Note: Observable.interval runs on a default Scheduler of Schedulers.computation(). Therefore, you'll probably need to add an observeOn() operator to make sure the results are sent to the right thread.
playbackProgressObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(progress -> {}) // Update logic here
The above statement will give you a Disposable which must be disposed when you are done observing.
You can do something like this ->
private var playbackDisposable: Disposable? = null
playbackDisposable = playbackProgressObservable
.observeOn(AndroidSchedulers.mainThead())
.subscribe(progress -> {}) // Update logic here
then to dispose the resource ->
playbackDisposable?.dispose()
Well, I did this through kotlin flow..
private fun audioProgress(exoPlayer: SimpleExoPlayer?) = flow<Int> {
while (true) {
emit(((exoPlayer?.currentPosition?.toFloat()?.div(exoPlayer.duration.toFloat())?.times(100))?.toInt()!!))
delay(1000)
}
}.flowOn(Dispatchers.IO)
then collect the progress like this...
val audioProgressJob = launch {
audioProgress(exoPlayer).collect {
MP_progress_bar.progress = it
}
}
Not sure it is the best way, but I achieved this by overloading the TrackRenderer.
I'm using videoPlayer.getBufferedPercentage(), but you might be able to compute the percentage yourself as well, by just using TrackRenderer's getBufferedPositionUs() and getDurationUs()
public interface ProgressListener {
public void onProgressChange(long progress);
}
public class CustomVideoRenderer extends MediaCodecVideoTrackRenderer {
long progress = 0;
private final CopyOnWriteArraySet<ProgressListener> progressListeners = new CopyOnWriteArraySet();
// [...]
// Skipped constructors
// [...]
public void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
super.doSomeWork(positionUs, elapsedRealtimeUs);
long tmpProgress = videoPlayer.getBufferedPercentage();
if (tmpProgress != this.progress) {
this.progress = tmpProgress;
for (ProgressListener progressListener : this.progressListeners) {
progressListener.onProgressChange(progress);
}
}
}
public void addProgressListener(ProgressListener listener) {
this.progressListeners.add(listener);
}
}
To make it clear,there isn't a build in EventListener for the progress event, but you can call Handler.postDelayed inside you updateProgress() function to get the current progress
private void updateProgress(){
//get current progress
long position = player == null ? 0 : player.getCurrentPosition();
//updateProgress() will be called repeatedly, you can check
//player state to end it
handler.postDelayed(updateProgressAction,1000)
}
private final Runnable updateProgressAction = new Runnable() {
#Override
public void run() {
updateProgress();
}
};
for more details, see the source of PlaybackControlView.java inside Exoplayer
I'm not sure if it's the right approach, but I used EventBus and TimerTask to update the progress of the audio being played.
In my MusicController class I put:
private void sendElapsedDuration() {
//To send the current elapsed time
final Timer t = new Timer();
Runnable r = new Runnable() {
#Override
public void run() {
t.schedule(new TimerTask() {
#Override
public void run() {
EventBus.getDefault().post(
new ProgressNotification(
player.getCurrentPosition(), player.getDuration())
);
if (player.getCurrentPosition() >= player.getDuration() ){
// The audio is ended, we pause the playback,
// and reset the position
player.seekTo(0);
player.setPlayWhenReady(false);
this.cancel();
// stopping the Runnable to avoid memory leak
mainHandler.removeCallbacks(this);
}
}
},0,1000);
}
};
if(player != null) {
if (player.getPlaybackState() != Player.STATE_ENDED)
mainHandler.postDelayed(r, 500);
else {
//We put the TimerTask to sleep when audio is not playing
t.cancel();
}
}
}
Then I called the method inside the onPlayerStateChanged when adding the listener to my SimpleExoPlayer instance. The code above, sends the elapsed and total duration of the audio being played every 1 second (1000 ms) via the EventBus. Then inside the activity hosting the SeekBar:
#Subscribe(threadMode = ThreadMode.MAIN)
public void updateProgress(ProgressNotification pn) {
seekBar.setMax((int) pn.duration);
seekBar.setProgress((int) pn.currentPosition);
}
I had this problem too, and i found the solution on this link
But solution:
1. create a class like this:
public class ProgressTracker implements Runnable {
public interface PositionListener{
public void progress(long position);
}
private final Player player;
private final Handler handler;
private PositionListener positionListener;
public ProgressTracker(Player player, PositionListener positionListener) {
this.player = player;
this.positionListener = positionListener;
handler = new Handler();
handler.post(this);
}
public void run() {
long position = player.getCurrentPosition();
positionListener.progress(position);
handler.postDelayed(this, 1000);
}
public void purgeHandler() {
handler.removeCallbacks(this);
}
}
2. and finally use it in your code:
tracker = new ProgressTracker(player, new ProgressTracker.PositionListener() {
#Override
public void progress(long position) {
Log.i(TAG, "VideoViewActivity/progress: position=" + position);
}
});
3. in the last step dont forget call purgeHandler when you want release player (important)
tracker.purgeHandler();
player.release();
player = null;
Extend your current player class (SimpleExoPlayer for ex.) and add
public interface PlayerEventsListener {
void onSeek(int from, int to);
void onProgressUpdate(long progress);
}
private PlayerEventsListener mListener;
private Handler mHandler;
private Runnable mProgressUpdater;
private boolean isUpdatingProgress = false;
public SomePlayersConstructor(Activity activity, /*...*/) {
//...
mListener = (PlayerEventsListener) activity;
mHandler = new Handler();
mProgressUpdater = new ProgressUpdater();
}
// Here u gain access to seek events
#Override
public void seekTo(long positionMs) {
mListener.onSeek(-1, (int)positionMs/1000);
super.seekTo(positionMs);
}
#Override
public void seekTo(int windowIndex, long positionMs) {
mListener.onSeek((int)getCurrentPosition()/1000, (int)positionMs/1000);
super.seekTo(windowIndex, positionMs);
}
// Here u gain access to progress
public void startProgressUpdater() {
if (!isUpdatingProgress) {
mProgressUpdater.run();
isUpdatingProgress = true;
}
}
private class ProgressUpdater implements Runnable {
private static final int TIME_UPDATE_MS = 500;
#Override
public void run() {
mListener.onProgressUpdate(getCurrentPosition());
mHandler.postDelayed(mProgressUpdater, TIME_UPDATE_MS);
}
}
Then inside player activity just implement interface and start updates with player.startProgressUpdater();
If you want to accomplish this, just listen to onPositionDiscontinuity(). It will give you information if the seekbar is being scrub
rx java implementation :
private val disposablesVideoControlsDisposable = CompositeDisposable()
fun showVideoControlsAndSimilarTray() {
videoSeekBar?.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) seekVideoProgress(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
val disposable = Observable.interval(0, 1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
calculateVideoProgress()
}
disposablesVideoControlsDisposable.add(disposable)
}
private fun calculateVideoProgress() {
val currentMill = exoPlayer.currentPosition
val totalMillis = exoPlayer.duration
if (totalMillis > 0L) {
val remainMillis = (totalMillis - currentMill).toFloat() / 1000
val remainMins = (remainMillis / 60).toInt()
val remainSecs = (remainMillis % 60).toInt()
videoProgressText.setText("$remainMins:${String.format("%02d", remainSecs)}")
seekBarProgress.set((currentMill.toFloat() / totalMillis * 100).toInt())
}
}
private fun seekVideoProgress(progress: Int) {
val seekMillis = exoPlayer.duration.toFloat() * progress / 100
exoPlayer.seekTo(seekMillis.toLong())
}
And finally when you are done :
fun disposeVideoControlsObservable() {
disposablesVideoControlsDisposable.clear()
}
if you are using the player view or the player control view fully or partially(for just the buttons or something) you could set progress listener directly from it:
PlayerControlView playerControlView = miniPlayerCardView.findViewById(R.id.playerView);
ProgressBar audioProgressBar = miniPlayerCardView.findViewById(R.id.audioProgressBar);
playerControlView.setProgressUpdateListener((position, bufferedPosition) -> {
int progressBarPosition = (int) ((position*100)/player.getDuration());
int bufferedProgressBarPosition = (int) ((bufferedPosition*100)/player.getDuration());
audioProgressBar.setProgress(progressBarPosition);
audioProgressBar.setSecondaryProgress(bufferedProgressBarPosition);
});
Only use onTouchListener with the MotionEvent.ACTION_UP
SeekBar exo_progress = (SeekBar) findViewById(R.id.exo_progress);
exo_progress.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
//put your code here!!
}
return false;
}
});
This works at least with Exoplayer 2.
There is four playback states: STATE_IDLE, STATE_BUFFERING, STATE_READY and STATE_ENDED.
Checking playback state is easy to do. There is at least two solution: if-statement or switch-statement.
Whatever playback state is going on you can execute your method or set something else for example progressbar.
#Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED) {
showControls();
Toast.makeText(getApplicationContext(), "Playback ended", Toast.LENGTH_LONG).show();
}
else if (playbackState == ExoPlayer.STATE_BUFFERING)
{
progressBar.setVisibility(View.VISIBLE);
Toast.makeText(getApplicationContext(), "Buffering..", Toast.LENGTH_SHORT).show();
}
else if (playbackState == ExoPlayer.STATE_READY)
{
progressBar.setVisibility(View.INVISIBLE);
}
}
it's simple
var player = SimpleExoPlayer.Builder(context).build();
player.addListener(object:Player.Listener{
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(
Player.EVENT_IS_LOADING_CHANGED,
Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED
)) {
log(msg="progres ${player.currentPosition}")
}
}
})
and you can view com.google.android.exoplayer2.ui.PlayerControlView.java at 1356 line
I've developed an app which takes an advantage of the native Android's MediaPlayer. The source code of my class making use of Media Player is below.
The problem is that only on some devices after some miliseconds of playback (I hear only voice, the screen remains black) I keep getting error(100,0) which according to the documentation says
public static final int MEDIA_ERROR_SERVER_DIED
Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.
On forums I've found out that I need to reset the player every time I get it... but I get it after just a short moment and then it dies forever. I cannot reset the player every second since playback is useless. I cannot get why some devices have this problem and others not. The one that I know has Android OS > 4.0.
Of course, first init() and then showVideo() are getting called. The last onError with code 100 is then called. What's a potential solution to make the streams run continuously and not break?
public class NativePlayer extends Player implements OnBufferingUpdateListener,
OnCompletionListener, OnErrorListener, OnInfoListener {
private VideoView videoview;
private PlayerListener listener;
private MainActivity context;
private final Logger logger = LoggerFactory.getLogger(NativePlayer.class);
#Override
public void init(MainActivity activity) {
this.videoview = (VideoView) activity.findViewById(R.id.video);
context = activity;
}
#Override
public void showVideo(final String url, final PlayerListener _listener) {
listener = _listener;
videoview.setVisibility(View.VISIBLE);
try {
Uri video = Uri.parse(url);
videoview.setVideoURI(video);
} catch (Exception e) {
logger.error("Error playing video", e);
listener.onVideoError();
return;
}
videoview.setOnCompletionListener(this);
videoview.setOnErrorListener(this);
videoview.requestFocus();
videoview.setOnPreparedListener(new OnPreparedListener() {
public void onPrepared(MediaPlayer mp) {
videoview.start();
if (listener != null) {
listener.onVideoStarted();
}
}
});
}
#Override
public void onStop() {
stop();
}
private void stop() {
if (videoview == null) {
return;
}
if (videoview.isPlaying()) {
videoview.stopPlayback();
}
}
#Override
public void onDestroy() {
}
#Override
public void onCompletion(MediaPlayer mp) {
stop();
}
#Override
public boolean onError(MediaPlayer mp, int what, int extra) {
if (listener != null) {
listener.onVideoError();
}
return false;
}
#Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (listener != null) {
listener.onInfo(what, extra);
}
return false;
}
#Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
if (listener != null) {
listener.onBufferingUpdate(percent);
}
}
}
I had same problem (error 100, mediaplayer died, etc.).
I resolve it by using .stopPlayback(), and starting stream again.
Below is my part of code:
private void startWatchVideo(final string video_link) {
videoViewVA.setMediaController(new MediaController(this));
videoViewVA.setVideoURI(Uri.parse(video_link));
videoViewVA.requestFocus();
videoViewVA.setOnPreparedListener(new OnPreparedListener() {
public void onPrepared(MediaPlayer media) {
media.start();
}
});
videoViewVA.setOnErrorListener(new OnErrorListener() {
#Override
public boolean onError(MediaPlayer media, int what, int extra) {
if (what == 100)
{
videoViewVA.stopPlayback();
startWatchVideo(video_link);
}
return true;
}
});
}
On practice it looks like video is just slows down
I have a MediaPlayer in a Fragment which retains its instance on configuration changes. The player is playing a video loaded from my assets directory. I have the scenario set up with the goal of reproducing the YouTube app playback where the audio keeps playing during the configuration changes and the display is detached and reattached to the media player.
When I start the playback and rotate the device, the position jumps forward about 6 seconds and (necessarily) the audio cuts out when this happens. Afterwards, the playback continues normally. I have no idea what could be causing this to happen.
As requested, here is the code:
public class MainFragment extends Fragment implements SurfaceHolder.Callback, MediaController.MediaPlayerControl {
private static final String TAG = MainFragment.class.getSimpleName();
AssetFileDescriptor mVideoFd;
SurfaceView mSurfaceView;
MediaPlayer mMediaPlayer;
MediaController mMediaController;
boolean mPrepared;
boolean mShouldResumePlayback;
int mBufferingPercent;
SurfaceHolder mSurfaceHolder;
#Override
public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(activity, attrs, savedInstanceState);
final String assetFileName = "test-video.mp4";
try {
mVideoFd = activity.getAssets().openFd(assetFileName);
} catch (IOException ioe) {
Log.e(TAG, "Can't open file " + assetFileName + "!");
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
// initialize the media player
mMediaPlayer = new MediaPlayer();
try {
mMediaPlayer.setDataSource(mVideoFd.getFileDescriptor(), mVideoFd.getStartOffset(), mVideoFd.getLength());
} catch (IOException ioe) {
Log.e(TAG, "Unable to read video file when setting data source.");
throw new RuntimeException("Can't read assets file!");
}
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mp) {
mPrepared = true;
}
});
mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
#Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
mBufferingPercent = percent;
}
});
mMediaPlayer.prepareAsync();
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_main, container, false);
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface);
mSurfaceView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mMediaController.show();
}
});
mSurfaceHolder = mSurfaceView.getHolder();
if (mSurfaceHolder == null) {
throw new RuntimeException("SufraceView's holder is null");
}
mSurfaceHolder.addCallback(this);
return view;
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mMediaController = new MediaController(getActivity());
mMediaController.setEnabled(false);
mMediaController.setMediaPlayer(this);
mMediaController.setAnchorView(view);
}
#Override
public void onResume() {
super.onResume();
if (mShouldResumePlayback) {
start();
} else {
mSurfaceView.post(new Runnable() {
#Override
public void run() {
mMediaController.show();
}
});
}
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaController.setEnabled(true);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// nothing
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
mMediaPlayer.setDisplay(null);
}
#Override
public void onPause() {
if (mMediaPlayer.isPlaying() && !getActivity().isChangingConfigurations()) {
pause();
mShouldResumePlayback = true;
}
super.onPause();
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
#Override
public void onDestroyView() {
mMediaController.setAnchorView(null);
mMediaController = null;
mMediaPlayer.setDisplay(null);
mSurfaceHolder.removeCallback(this);
mSurfaceHolder = null;
mSurfaceView = null;
super.onDestroyView();
}
#Override
public void onDestroy() {
mMediaPlayer.release();
mMediaPlayer = null;
try {
mVideoFd.close();
} catch (IOException ioe) {
Log.e(TAG, "Can't close asset file..", ioe);
}
mVideoFd = null;
super.onDestroy();
}
// MediaControler methods:
#Override
public void start() {
mMediaPlayer.start();
}
#Override
public void pause() {
mMediaPlayer.pause();
}
#Override
public int getDuration() {
return mMediaPlayer.getDuration();
}
#Override
public int getCurrentPosition() {
return mMediaPlayer.getCurrentPosition();
}
#Override
public void seekTo(int pos) {
mMediaPlayer.seekTo(pos);
}
#Override
public boolean isPlaying() {
return mMediaPlayer.isPlaying();
}
#Override
public int getBufferPercentage() {
return mBufferingPercent;
}
#Override
public boolean canPause() {
return true;
}
#Override
public boolean canSeekBackward() {
return true;
}
#Override
public boolean canSeekForward() {
return true;
}
#Override
public int getAudioSessionId() {
return mMediaPlayer.getAudioSessionId();
}
}
The if block in the onPause method is not being hit.
Update:
After doing a bit more debugging, removing the interaction with the SurfaceHolder causes the problem to go away. In other words, if I don't setDisplay on the MediaPlayer the audio will work fine during the configuration change: no pause, no skip. It would seem there is some timing issue with setting the display on the MediaPlayer that is confusing the player.
Additionally, I have found that you must hide() the MediaController before you remove it during the configuration change. This improves stability but does not fix the skipping issue.
Another update:
If you care, the Android media stack looks like this:
MediaPlayer.java
-> android_media_MediaPlayer.cpp
-> MediaPlayer.cpp
-> IMediaPlayer.cpp
-> MediaPlayerService.cpp
-> BnMediaPlayerService.cpp
-> IMediaPlayerService.cpp
-> *ConcreteMediaPlayer*
-> *BaseMediaPlayer* (Stagefright, NuPlayerDriver, Midi, etc)
-> *real MediaPlayerProxy* (AwesomePlayer, NuPlayer, etc)
-> *RealMediaPlayer* (AwesomePlayerSource, NuPlayerDecoder, etc)
-> Codec
-> HW/SW decoder
Upon examining AwesomePlayer, it appears this awesome player takes the liberty of pausing itself for you when you setSurface():
status_t AwesomePlayer::setNativeWindow_l(const sp<ANativeWindow> &native) {
mNativeWindow = native;
if (mVideoSource == NULL) {
return OK;
}
ALOGV("attempting to reconfigure to use new surface");
bool wasPlaying = (mFlags & PLAYING) != 0;
pause_l();
mVideoRenderer.clear();
shutdownVideoDecoder_l();
status_t err = initVideoDecoder();
if (err != OK) {
ALOGE("failed to reinstantiate video decoder after surface change.");
return err;
}
if (mLastVideoTimeUs >= 0) {
mSeeking = SEEK;
mSeekTimeUs = mLastVideoTimeUs;
modifyFlags((AT_EOS | AUDIO_AT_EOS | VIDEO_AT_EOS), CLEAR);
}
if (wasPlaying) {
play_l();
}
return OK;
}
This reveals that setting the surface will cause the player to destroy whatever surface was previously being used as well as the video decoder along with it. While setting a surface to null should not cause the audio to stop, setting it to a new surface requires the video decoder to be reinitialized and the player to seek to the current location in the video. By convention, seeking will never take you further than you request, that is, if you overshoot a keyframe when seeking, you should land on the frame you overshot (as opposed to the next one).
My hypothesis, then, is that the Android MediaPlayer does not honor this convention and jumps forward to the next keyframe when seeking. This, coupled with a video source that has sparse keyframes, could explain the jumping I am experiencing. I have not looked at AwesomePlayer's implementation of seek, though. It was mentioned to me that jumping to the next keyframe is something that needs to happen if your MediaPlayer is developed with streaming in mind since the stream can be discarded as soon as it has been consumed. Point being, it might not be that far fetch to think the MediaPlayer would choose to jump forward as opposed to backwards.
Final Update:
While I still don't know why the playback skips when attaching a new Surface as the display for a MediaPlayer, thanks to the accepted answer, I have gotten the playback to be seamless during rotation.
Thanks to natez0r's answer, I have managed to get the setup described working. However, I use a slightly different method. I'll detail it here for reference.
I have one Fragment which I flag to be retained on configuration changes. This fragment handles both the media playback (MediaPlayer), and the standard TextureView (which provides the SurfaceTexture where the video buffer gets dumped). I initialize the media playback only once my Activity has finished onResume() and once the SurfaceTexture is available. Instead of subclassing TextureView, I simply call setSurfaceTexture (since it's public) in my fragment once I receive a reference to the SurfaceTexture. The only two things retained when a configuration change happens are the MediaPlayer reference, and the SurfaceTexture reference.
I've uploaded the source of my sample project to Github. Feel free to take a look!
I know this question is a tad old now, but I was able to get this working in my app without the skipping. The issue is the surface getting destroyed (killing whatever buffer it had in it). This may not solve all your issues because it targets API 16, but you can manage your own SurfaceTexture inside your custom TextureView where the video is drawn:
private SurfaceTexture mTexture;
private TextureView.SurfaceTextureListener mSHCallback =
new TextureView.SurfaceTextureListener() {
#Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
int height) {
mTexture = surface;
mPlayer.setSurface(new Surface(mTexture));
}
#Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
int height) {
mTexture = surface;
}
#Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mTexture = surface;
return false;
}
#Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
mTexture = surface;
}
};
the key is returning false in onSurfaceTextureDestroyed and holding onto mTexture. When the view gets re-attached to the window you can set the surfaceTexture:
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mTexture != null) {
setSurfaceTexture(mTexture);
}
}
This allows my view to continue playing video from EXACTLY where it left off.