MediaPlayer skips forward about 6 seconds on rotation - android

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.

Related

MediaPlayer and SurfaceView over multiple activities

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.

What exactly is a "Surface" class in Android?

I have been trying for a long time to render a video on a "Surface" class using MediaPlayer class. It was playing audio, but not the video. Everywhere I search, people talk about SurfaceView and SurfaceHolder but I have only a Surface object. How to crack this blocker?
This is how I tried,
public class SampleVideoPlayer{
private Uri mUrl;
private Surface mSurface;
private MediaPlayer mMediaPlayer;
private Context mContext;
public SampleVideoPlayer(Context context, String url, Surface surface){
mUrl = Uri.parse(url);
mSurface = surface;
mMediaPlayer = new MediaPlayer();
mContext = context;
}
public void playVideo() throws IOException {
mMediaPlayer.setDataSource(mContext, mUrl);
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener(){
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayer.start();
}
});
mMediaPlayer.prepareAsync();
}
}
Adding the Session Object I am passing,
public class MyTvSession extends TvInputService.Session implements Handler.Callback {
Context mContext;
String vidUrl;
Surface mSurface;
SampleVideoPlayer player = null;
SampleMediaPlayer mediaPlayer;
public MyTvSession(Context context){
super(context);
ChannelXmlReader reader = new ChannelXmlReader(context);
ArrayList<Channel> channels = reader.ReadXml();
mContext = context;
vidUrl = channels.get(0).url;
}
#Override
public boolean handleMessage(Message message) {
Log.d("HANDLE MESSAGE", message.toString());
return true;
}
#Override
public void onRelease() {
}
#Override
public boolean onSetSurface(Surface surface) {
if(surface != null)
Log.d("NOT NULL from SESSION", "NOTNULL");
mSurface = surface;
return true;
}
#Override
public void onSurfaceChanged(int format, int width, int height) {
super.onSurfaceChanged(format, width, height);
if(mediaPlayer != null)
mediaPlayer.mMediaPlayer.setSurface(mSurface);
Log.d("ONSURFACECHANGED", "Event");
}
#Override
public void onSetStreamVolume(float v) {
}
#Override
public boolean onTune(Uri uri) {
Log.d("TUNING CHANNEL", uri.toString());
try {
mediaPlayer = new SampleMediaPlayer(mContext, vidUrl, mSurface);
mediaPlayer.playVideo();
}catch(Exception e){
Log.d("MPEXCEPTION", Log.getStackTraceString(e));
}
return true;
}
#Override
public void onSetCaptionEnabled(boolean b) {
}
}
The Surface class is a thin wrapper around a buffer list shared with the backing surfaceflinger process, which is responsible for rendering to the display.
You can get one of these using the SurfaceView and its SurfaceHolder, which are tied to the lifecycle of the view. So be sure to get it after being called back when the surface has been created.
Alternatively, you can use a SurfaceTexture which is created using your own custom OpenGL context. With this approach you can render using your own OpenGL code or even pass it off to the media engine for rendering. You can also get a SurfaceTexture tied to the view subsystem by using TextureView (but like SurfaceView you'll need to use it at the appropriate time in its lifecycle.)
I have exactly the same problem. But it only happens on Philips TV. The same code runs fine on every other Android TV devices. The surface I get in onSetSurface is valid, sound is playing, but picture is black. When I close the app, the video is visible for a second. It seems to be in the background.

MediaPlayer doesn't resume video after activity restart

I have a MediaPlayer object that uses a SurfaceHolder object as a surface. There is a button on top of the video that takes me out of the video to a website. When that happens, I pause the player with player.pause(). When I return from the website, I resume the player with player.start(). I know that the surface gets destroyed when the activity is not displayed anymore, and it gets recreated as soon as the activity is restarted. In my surfaceCreated(), I set the surface for the player again (since it no longer has a surface at that point), and then resume. However, the player simply restarts the video from the beginning.
I've tried commenting out the line that takes me to the website, just to see if pause/start works properly and resumes from last spot. It does. I'm not sure why this behaviour doesn't happen when I leave and re-enter the video activity though.
I also tried using the player.seekTo() call. There was no difference. In fact, when I disabled the button taking me to a site to just pausing the video, with the seekTo() call the video ALSO started from the beginning despite position being not 0.
The player object is the same all the way throughout.
Just because the surface is a new one on restart, it doesn't know or care of its contents, does it? The player should be managing that, right?
I'm out of ideas at this point. Can anyone please offer any tips?
UPDATE: So I threw together a quick app just to eliminate any other external factors. Here's the full code for the video class (other class is just an activity with a play button):
public class VideoPlayer extends Activity implements MediaPlayer.OnCompletionListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnVideoSizeChangedListener,
SurfaceHolder.Callback {
private MediaPlayer player;
private SurfaceHolder mSurfaceHolder;
private SurfaceView mSurfaceView;
private Button leaveVideoButton;
private boolean isPaused = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.video_layout);
leaveVideoButton = (Button) findViewById(R.id.go_to_web);
leaveVideoButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Uri uri = Uri.parse("http://www.google.com");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
pauseSteps();
startActivity(intent);
}
});
createPlayer();
createSurface();
}
private void createSurface() {
mSurfaceView = (SurfaceView) findViewById(R.id.surface);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
private void createPlayer() {
player = new MediaPlayer();
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
player.setOnPreparedListener(this);
player.setOnVideoSizeChangedListener(this);
player.setOnSeekCompleteListener(this);
}
private void pauseSteps() {
if(player.isPlaying()) {
player.pause();
isPaused = true;
}
}
private void playSteps() {
if(isPaused) {
isPaused = false;
player.start();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
if (player.isPlaying()) {
player.stop();
}
player.reset();
player.release();
player = null;
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
player.setDisplay(holder);
if (!isPaused) {
try {
// player.setDataSource(path);
AssetFileDescriptor afd = getResources().openRawResourceFd(R.raw.video);
if (afd == null) return;
player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
afd.close();
player.prepare();
} catch (IOException e) {
e.printStackTrace();
}
} else {
playSteps();
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
#Override
public void onCompletion(MediaPlayer mp) {
}
#Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false;
}
#Override
public void onPrepared(MediaPlayer mp) {
player.start();
}
#Override
public void onSeekComplete(MediaPlayer mp) {
}
#Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
}
}
UPDATE 2: So I tried a different video and it resumed just fine from the same spot. This must be some encoding issue.

SurfaceView goes black -- MediaPlayer Android

I am using MediaPlayer and SurfaceView to stream a video from a server. The video plays fine however if the activity goes in "Paused" state, eg. when the user taps home button or recent button, On resuming the player activity the SurfaceView becomes black. I know when you leave the activity, the surfaceView is destroyed and upon resuming it is created again, so as workaround I saved the player's current position in onPause() of the activity and when the activity resumes, I seek the player to that position. This didn't work either.
So my question is how can I make the surfaceView keep the frame/picture it was displaying when the user goes out of the activity?
public class VideoPlayer extends Activity implements SurfaceHolder.Callback, OnPreparedListener {
private long PREV_PLAYER_POS;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_videoplayer);
mDecorView = getWindow().getDecorView();
mediaController = new VideoControllerView(this, mDecorView);
handler = new Handler();
Intent intent = getIntent();
usedURL = intent.getExtras().getString("vidURL");
vidID = intent.getExtras().getString("id");
mProgressBar = (ProgressBar) findViewById(R.id.progressBar1);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
mContext = this;
surfView = (SurfaceView) findViewById(R.id.videoView4);
SurfaceHolder holder = surfView.getHolder();
holder.addCallback(this);
mMediaPlayer = new MediaPlayer();
mMediaPlayerControl = new VideoControllerView.MediaPlayerControl() {
……
}
mMediaPlayer.setOnPreparedListener(this);
mediaController.setOnSeekStartListener(new onSeekStartListener() {
#Override
public void onSeekStarted() {
}
});
mediaController.setMediaPlayer(mMediaPlayerControl);
mediaController.setAnchorView((ViewGroup) findViewById(R.id.container4));
mediaController.setEnabled(true);
mediaController.show();
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
mMediaPlayer.setDisplay(holder);
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
mMediaPlayer.setDisplay(holder);
try {
mMediaPlayer.setDataSource(usedURL);
mMediaPlayer.prepareAsync();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
#Override
public void onPrepared(MediaPlayer mp) {
surfView.requestLayout();
surfView.invalidate();
mMediaPlayer.start();
}
#Override
public void onSurfaceViewSizeChanged(int width, int height) {
}
#Override
protected void onResume() {
super.onResume();
mMediaPlayer.seekTo((int)PREV_PLAYER_POS);
}
#Override
public void onBackPressed() {
super.onBackPressed();
mMediaPlayer.stop();
}
#Override
protected void onPause() {
super.onPause();
PREV_PLAYER_POS = mMediaPlayer.getCurrentPosition();
mMediaPlayer.pause();
}
#Override
protected void onDestroy() {
super.onDestroy();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
Best Regards
Start your video in onResume,
#Override
protected void onResume() {
super.onResume();
mMediaPlayer.start();
mMediaPlayer.seekTo(length);
}
Hope it ll work..
The SurfaceView's surface may or may not be destroyed when you bring something up in front of the activity. See this section in the graphics architecture document for an overview. If it didn't get destroyed, then surfaceCreated() won't be called again, and your app will never call mMediaPlayer.setDisplay(holder) to connect the MediaPlayer to the surface.
I would guess that, if you brought up "recents" and then rotated the device, things would work when you returned (because the device rotation forces the surface to be recreated). You may want to add some logging to the various callbacks to see when they fire.
You will need to have a static variable that tracks whether or not the surface has been destroyed (e.g. static bool haveSurface = false, set to true in surfaceCreated(), false in surfaceDestroyed(). If it's true in onCreate(), call mMediaPlayer.setDisplay(holder) immediately.
Grafika has some examples of working with the odd SurfaceView-vs-Activity lifecycle issues, though I don't think any of them quite fit your use case.

Disable Android's VideoView requestAudioFocus when playing a video?

I'm building an app that records and plays back video. I would like to do so without affecting background music playback, i.e. if I begin playing a video, I do not want to pause other apps' audio. However, on Lollipop, Android's VideoView class automatically requests audio focus when the private method VideoView.openVideo() is called:
AudioManager am = (AudioManager) super.getSystemService(name);
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
Any suggestions on how to get around this?
Starting with Android SDK 26 you may want to use VideoView and
if(Build.VERSION.SDK_INT >= Build.Version_CODES.O){
//set this BEFORE start playback
videoView.setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE)
}
For older version, there's a workaround described here: https://stackoverflow.com/a/31569930/993439
Basically, copy source code of VideoView and uncomment following lines
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
I got around this with a stupid solution by copying whole source code of android.widget.VideoView of Lollipop and removing line you mentioned.
Make your own VideoView class. don't use extends VideoView since you can't override openVideo() method.
I don't recommend this as I thinking it's a temporary solution. VideoView Changed a lot between 4.1-5.0 so this can make RuntimeException on Android version other than Lollipop
Edit
I made approach MediaPlayer + SurfaceView as pinxue told us;
It respects aspect ratio within viewWidth and viewHeight.
final String finalFilePath = filePath;
final SurfaceHolder surfaceHolder = sv.getHolder();
final MediaPlayer mediaPlayer = new MediaPlayer();
final LinearLayout.LayoutParams svLayoutParams = new LinearLayout.LayoutParams(viewWidth,viewHeight);
surfaceHolder.addCallback(new SurfaceHolder.Callback(){
#Override
public void surfaceCreated(SurfaceHolder holder) {
try {
if(isDebug) {
System.out.println("setting VideoPath to VideoView: "+finalFilePath);
}
mediaPlayer.setDataSource(finalFilePath);
}catch (IOException ioe){
if(isDebug){
ioe.printStackTrace();
}
//mediaPlayer = null;
}
mediaPlayer.setDisplay(surfaceHolder);
mediaPlayer.prepareAsync();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mp) {
if(isDebug){
System.out.println("Video is starting...");
}
// for compatibility, we adjust size based on aspect ratio
if ( mp.getVideoWidth() * svLayoutParams.height < svLayoutParams.width * mp.getVideoHeight() ) {
//Log.i("###", "image too wide, correcting");
svLayoutParams.width = svLayoutParams.height * mp.getVideoWidth() / mp.getVideoHeight();
} else if ( mp.getVideoWidth() * svLayoutParams.height > svLayoutParams.width * mp.getVideoHeight() ) {
//Log.i("###", "image too tall, correcting");
svLayoutParams.height = svLayoutParams.width * mp.getVideoHeight() / mp.getVideoWidth();
}
sv.post(new Runnable(){
#Override
public void run() {
sv.setLayoutParams(svLayoutParams);
}
});
mp.start();
}
});
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if(isDebug){
System.out.println("surfaceChanged(holder, "+format+", "+width+", "+height+")");
}
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
try {
mediaPlayer.setDataSource("");
}catch (IOException ioe){
if(isDebug){
ioe.printStackTrace();
}
}
}
});
if(sv.post(new Runnable() {
#Override
public void run() {
sv.setLayoutParams(svLayoutParams);///
sv.setVisibility(View.VISIBLE);
}})){
if(isDebug) {
System.out.println("post Succeded");
}
}else{
if(isDebug) {
System.out.println("post Failed");
}
}
The accepted solution does not guarantee compatibility across all Android versions and is a dirty hack more than a true solution. I've tried all forms of hacks to get this working, yet none have worked to my satisfaction.
I have come up with a much better solution though - switch from a VideoView to a TextureView and load it with a MediaPlayer. There is no difference from the user's perspective, just no more audio stoppage.
Here's my use case for playing an MP4 looping:
private TextureView _introVideoTextureView;
private MediaPlayer _introMediaPlayer;
...
#Override
public void onCreate(...) {
_introVideoTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
#Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
try {
destoryIntroVideo();
_introMediaPlayer = MediaPlayer.create(SignInActivity.this, R.raw.intro_video);
_introMediaPlayer.setSurface(new Surface(surfaceTexture));
_introMediaPlayer.setLooping(true);
_introMediaPlayer.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
_introMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayer.start();
}
});
} catch (Exception e) {
System.err.println("Error playing intro video: " + e.getMessage());
}
}
#Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {}
#Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
return false;
}
#Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {}
});
}
#Override
public void onDestroy() {
super.onDestroy();
destoryIntroVideo();
}
private void destoryIntroVideo() {
if (_introMediaPlayer != null) {
_introMediaPlayer.stop();
_introMediaPlayer.release();
_introMediaPlayer = null;
}
}
You may use MediaPlayer + SurfaceView instead.
use audioManager.abandonAudioFocus(null)
If you look at the VideoView code you will notice it calls the method audioManager.requestAudioFocus with null for the OnAudioFocusChangeListener. When you register a listener with the AudioManager it uses this method to make an ID for the listener
private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) {
if (l == null) {
return new String(this.toString());
} else {
return new String(this.toString() + l.toString());
}
}
which generates the same ID every time you use null. So if you call abandonAudioFocus with null it will remove any listener that was added with null as the parameter for the OnAudioFocusChangeListener

Categories

Resources