I want to add "Fullscreen" button to the standard SimpleExoPlayer implementation from Google. I added the ImageButton and everything works well so far.
Then I decided that fullscreen will work in next way: after pressing on button, I open new activity with fullscreen SimpleExoPlayerView, pass there video uri and current position, initialize player and seek for given position. It works, but the player re-initialization takes 1-3 seconds depending on a device what don't want to have.
Now I would like to have an instance of player inside intent service and just reattach existing player to any view that wants to show it (preview or fullscreen view), like this:
mPlayerView.setPlayer(mPlayer);
The problem is that service will have the player and activity will have the view. No one of them will have both to be able to attach player to view. As a workaround, I think about making that the service class may have a static link to the player, so activity will be able to get it via static reference. But this seems like some code smell and I don't know if there won't be problems with communications between threads.
So, how can I pass the player from Service (that is not Serializable or Parcelable) to Activity or how can I pass a player view to the Service?
I have faced a similar issue with Video Playback using MediaPlayer. I have bound to MediaBrowserService using IBinder and then fetched the MediaPlayer instance. In your service provide a method that returns a reference to MediaPlayer. Something like this:
private MediaPlayer mediaPlayer;
#Override
public IBinder onBind(Intent intent) {
if (intent.getAction().equals("YOUR_INTENT")) {
return new LocalBinder();
}
return super.onBind(intent);
}
public class LocalBinder extends Binder{
public AudioService getService(){
return AudioService.this;
}
}
public MediaPlayer getMediaPlayer() {
return mediaPlayer;
}
In your Activity/Fragment then bind to MediaBrowserService using IBinder. In my implementation, I have used MediaPlayer, but I think in similar ways it can be used for Exoplayer.
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback{
private MediaPlayer mediaPlayer;
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private boolean isServiceBounded;
private boolean isSurfaceReady;
private ServiceConnection serviceConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName name, IBinder service) {
isServiceBounded = true;
mediaPlayer = ((AudioService)service).getMediaPlayer();
if (isSurfaceReady) {
mediaPlayer.setDisplay(surfaceHolder);
}
}
#Override
public void onServiceDisconnected(ComponentName name) {
isServiceBounded = false;
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView = findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
bindService(new Intent("YOUR_INTENT"), serviceConnection, BIND_AUTO_CREATE);
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
surfaceHolder = holder;
isSurfaceReady = true;
if (mediaPlayer != null) {
mediaPlayer.setDisplay(holder);
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
isSurfaceReady = false;
if (mediaPlayer != null) {
mediaPlayer.setDisplay(null);
}
surfaceHolder = null;
}
#Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);
}
}
exoPlayer handle fullscreen good enough - try to change your layout params of playerView to match the screen on rotation or pressing button.
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int width;
int height;
View decorView = getWindow().getDecorView();
int uiOptions;
if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Resources r = getResources();
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, r.getDisplayMetrics());
width = CoordinatorLayout.LayoutParams.MATCH_PARENT;
height = (int)px;
uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
} else {
uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
decorView.setSystemUiVisibility(uiOptions);
final CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(width, height);
mAppBar.setLayoutParams(lp);
}
Also don't forget to add this to your activity manifest to prevent activity reload:
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
Related
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.
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 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.
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.
I'm creating a music streaming app. As per android's MediaPlayer guide, I'm controlling the MediaPlayer from a Service. This all works fine and now I'm trying to add a MediaController to control playback. To do so, I'm having my Service implement MediaController.MediaPlayerControl, and having my Activity bind to my Service, and then instantiating the MediaController from the Activity with the Service context from the ServiceConnection.
Player.java
public class Player extends Activity implements OnClickListener, OnItemClickListener, MediaController.MediaPlayerControl {
private MediaController mediaController;
private ServiceConnection mConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName className, IBinder service) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
LocalBinder binder = (LocalBinder) service;
mService = binder.getService();
mBound = true;
showMediaController();
}
#Override
public void onServiceDisconnected(ComponentName arg0) {
mBound = false;
}
};
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
startService(
new Intent(this, PlayerService.class)
.setAction("com.limastreamer.action.NEXTSHOW"));
bindService(
new Intent(this, PlayerService.class),
mConnection,
Context.BIND_AUTO_CREATE);
}
public void showMediaController() {
if (mBound) {
mediaController = new MediaController(this);
mediaController.setAnchorView(
findViewById(R.id.player)
);
mediaController.setMediaPlayer(mService);
mediaController.setEnabled(true);
mediaController.show(0);
}
}
}
PlayerService.java
public class PlayerService extends Service implements MediaController.MediaPlayerControl {
private MediaPlayer mMediaPlayer;
public class LocalBinder extends Binder {
PlayerService getService() {
return PlayerService.this;
}
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
String action = intent.getAction();
if (action.equals("com.limastreamer.action.NEXTSHOW")) {
if (mMediaPlayer == null)
{
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setLooping(false);
}
try
{
mMediaPlayer.reset();
mMediaPlayer.setDataSource(url);
mMediaPlayer.prepareAsync(); // prepare async to not block main thread
}
catch (Exception ex)
{
Toast.makeText(getApplicationContext(), "Failed to prepare MediaPlayer", Toast.LENGTH_SHORT).show();
}
}
}
#Override
public boolean canPause() {
return true;
}
#Override
public boolean canSeekBackward() {
return true;
}
#Override
public boolean canSeekForward() {
return true;
}
#Override
public int getBufferPercentage() {
return 0;
}
#Override
public int getCurrentPosition() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying())
return mMediaPlayer.getCurrentPosition();
else
return 0;
}
#Override
public int getDuration() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying())
return mMediaPlayer.getDuration();
else
return 0;
}
#Override
public boolean isPlaying() {
if (mMediaPlayer != null)
return mMediaPlayer.isPlaying();
else
return false;
}
#Override
public void pause() {
if (mMediaPlayer != null)
mMediaPlayer.pause();
}
#Override
public void seekTo(int msec) {
if (mMediaPlayer != null)
mMediaPlayer.seekTo(msec);
}
#Override
public void start() {
if (mMediaPlayer != null)
mMediaPlayer.start();
}
}
R.id.player refers to the root element of my xml layout.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".Player" >
On calling mediaController.show(); the app bombs out with the exception: Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
From looking at other questions on SO (for example), it seems that this is caused by using the wrong context here: mediaController = new MediaController(this);, ie using something other than the Activity context. But as far as I can tell, I am using the Activity context.
I've tried:
Using other views in the layout as the anchor view (even tho the doc says you can use the Activity's main view)
Putting the MediaController in a fragment, and using getActivity() as the context, as shown here
Putting the MediaController in the xml layout instead of instantiating it programatically.
Setting a VideoView as the anchor view (some people say it only works with a VideoView).
Creating a new class that extends VideoView and implements MediaPlayerControl, and instantiating the MediaController in that class, using the saved context that was passed to the class when it was initialized as the context, and this as the anchor view.
Your activity should implement MediaPlayer.OnPreparedListener and set onPreparedListener of mediaPlayer of your service to the player activity.
public class MyMediaPlayer extends Activity implements
MediaController.MediaPlayerControl,MediaPlayer.OnPreparedListener {
...
public void onCreate(Bundle savedInstanceState) {
...
//this mediaPlayer is the reference of your media player inside your service
mediaPlayer.setOnPreparedListener(this);
...
}
...
}
also you have to start your service a little after you created your activity
Intent in=new Intent(MainPlayer.this,MyMediaPlayer.class);
startActivity(in);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//START YOUR SERVICE TO PREPARE YOUR PLAYER
this works for me.