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
Related
i am trying to make a video play option in recyclerview adapter in my app i can successfully play video in textureview now i am trying to add mediacontroller and thumbnail in to textureview video . i have tried by googling but i couldnt do.please need help.
this is my textureview interface method implementation:
#Override
public void onSurfaceTextureAvailable(final SurfaceTexture surfaceTexture, int i, int i1) {
try {
if(mediaPlayer!=null) {
final Surface surface = new Surface(surfaceTexture);
mediaPlayer.setDataSource(context, Uri.parse("https://www.w3schools.com/html/mov_bbb.mp4"));
mediaPlayer.setSurface(surface);
mediaPlayer.prepareAsync();
final MediaController mediaController = new MediaController(context);
mediaController.setMediaPlayer(this);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(final MediaPlayer mediaPlayer) {
// mediaPlayer.start();
}
});
}
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {
}
#Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
mediaPlayer.reset();
return true;
}
#Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
}
I use MediaPlayer for displaying video. I need to loop it. I use native method setLooping(), but it doesn't work. Video finished and that's all. Here is my code:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_full_screen);
TextureView videoViewFullScreen = (TextureView) findViewById(R.id.video_screen);
path = getIntent().getExtras().getString(SettingsActivity.FILE_PATH);
cycle = getIntent().getExtras().getBoolean(SettingsActivity.CYCLE);
position = getIntent().getIntExtra(FullScreenActivity.POSITION, -1);
videoViewFullScreen.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
#Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
final Surface surf = new Surface(surface);
try {
mediaPlayerFullScreen = new MediaPlayer();
mediaPlayerFullScreen.setDataSource(path);
mediaPlayerFullScreen.setSurface(surf);
if (cycle) {
mediaPlayerFullScreen.setLooping(true);
}
mediaPlayerFullScreen.prepareAsync();
mediaPlayerFullScreen.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
if (position != -1) {
Log.d("Position", "Position Full " + position);
mediaPlayerFullScreen.seekTo(position);
}
mediaPlayer.start();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
#Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
#Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mediaPlayerFullScreen.stop();
mediaPlayerFullScreen.release();
mediaPlayerFullScreen = null;
return true;
}
#Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
});
}
#Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayerFullScreen != null) {
mediaPlayerFullScreen.stop();
mediaPlayerFullScreen.release();
mediaPlayerFullScreen = null;
}
}
I looked all similar questions and tried all answers that find here, but none of them helped me.
I have Android version 4.4.2, Firmware version 4.5, Kernel version 3.3.0
Could someone help me? Is there another way to loop video?
I find replacement for setLooping(true), this works for me:
mediaPlayerFullScreen.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
#Override
public void onCompletion(MediaPlayer mp) {
mediaPlayerFullScreen.reset();
try {
mediaPlayerFullScreen.setDataSource(path);
} catch (IOException e) {
e.printStackTrace();
}
mediaPlayerFullScreen.setSurface(surf);
mediaPlayerFullScreen.prepareAsync();
mediaPlayerFullScreen.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
#Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayer.start();
}
});
}
});
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.
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.