Camera frames are never captured in background service (Android) - android

I want to run the front camera of a device from a background service, getting the captured frames by the camera in the background service using the callback that's called whenever a new frame is captured by the camera (eg, onPreviewFrame), and apply some real time processing on the obtained frames.
After searching I understand that there are two ways to run the camera in the background:
1- setting a SurfaceView with size 1 X 1.
2- Using SurfaceTexture (This does not require any view which seems more efficient.)
I tried both ways, I could only run the camera from the background, but the callbacks for getting frames were never called, though I tried different tricks.
The code I used for method 1 is available here:
https://stackoverflow.com/questions/35160911/why-onpreviewframe-is-not-being-called-from-a-background-service
Note that I've also tried to call onPreviewFrame inside surfaceChanged, but I always get that the camera is null when trying to use it to set the callback! I even tried to re-instantiate it as camera = Camer.open(), but I got an error saying "Fail to connect to camera service."
Here is the code for method 2:
public class BackgroundService extends Service implements TextureView.SurfaceTextureListener {
private Camera camera = null;
private int NOTIFICATION_ID= 1;
private static final String TAG = "OCVSample::Activity";
// Binder given to clients
private final IBinder mBinder = new LocalBinder();
private WindowManager windowManager;
private SurfaceTexture mSurfaceTexture= new SurfaceTexture (10);
public class LocalBinder extends Binder {
BackgroundService getService() {
// Return this instance of this service so clients can call public methods
return BackgroundService.this;
}
}//end inner class that returns an instance of the service.
#Override
public IBinder onBind(Intent intent) {
return mBinder;
}//end onBind.
#Override
public void onCreate() {
// Start foreground service to avoid unexpected kill
startForeground(NOTIFICATION_ID, buildNotification());
}
private Notification buildNotification () {
NotificationCompat.Builder notificationBuilder=new NotificationCompat.Builder(this);
notificationBuilder.setOngoing(true); //this notification should be ongoing
notificationBuilder.setContentTitle(getString(R.string.notification_title))
.setContentText(getString (R.string.notification_text_and_ticker))
.setSmallIcon(R.drawable.vecsat_logo)
.setTicker(getString(R.string.notification_text_and_ticker));
return(notificationBuilder.build());
}
#Override
public void onDestroy() {
Log.i(TAG, "surfaceDestroyed method");
}
/***********Ok now all service related methods were implemented**************/
/************now implement surface texture related methods*****************/
#Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i2) {
//Now the surfaceTexture available, so we can create the camera.
camera = Camera.open(1);
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
#Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.e(TAG, "frame captured");
}
});
//now try to set the preview texture of the camera which is actually the surfaceTexture that has just been created.
try {
camera.setPreviewTexture(mSurfaceTexture);
}
catch (IOException e){
Log.e(TAG, "Error in setting the camera surface texture");
}
camera.setPreviewCallback(new Camera.PreviewCallback() {
#Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
Log.e(TAG, "frame captured");
}
});
// the preview was set, now start it
camera.startPreview();
}
#Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i2) {
//super implementation is sufficient.
}
#Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
//stop the preview.
camera.stopPreview();
//release camera resource:
camera.release();
return false;
}
#Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
#Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.e(TAG, "frame captured from update");
}
});
camera.stopPreview(); //stop current preview.
try {
camera.setPreviewTexture(mSurfaceTexture);
}
catch (IOException e){
Log.e(TAG, "Error in setting the camera surface texture");
}
camera.setPreviewCallback(new Camera.PreviewCallback() {
#Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
Log.e(TAG, "frame captured");
}
});
camera.startPreview(); // start the preview again.
}
Here is how I call the service from an activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
//call the method in the super class..
super.onCreate(savedInstanceState);
//inflate the xml layout.
setContentView(R.layout.main);
//prepare buttons.
ImageButton startButton = (ImageButton) findViewById(R.id.start_btn);
ImageButton stopButton = (ImageButton) findViewById(R.id.stop_btn);
//prepare service intent:
final Intent serviceIntent = new Intent(getApplicationContext(), BackgroundService.class);
//set on click listeners for buttons (this should respond to normal touch events and simulated ones):
startButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//startActivity(serviceIntent);
//start eye gazing mode as a service running in the background:
isBound= getApplicationContext().bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
}
});
stopButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//stop eye gazing mode as a service running in the background:
getApplicationContext().unbindService(mConnection);
}
});
}//end onCreate.
private final ServiceConnection mConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName className, IBinder service) {
BackgroundService.LocalBinder binder = (BackgroundService.LocalBinder) service;
mService = binder.getService();
isBound = true;
}
#Override
public void onServiceDisconnected(ComponentName className) {
isBound = false;
}
};
Please help, how can I get the frames callback to work? I tried a lot but I couldn't figure it out.
Any help is highly appreciated whether it uses method 1 or 2.
Thanks.

Related

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.

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.

How to play music in the background through out the App continuously?

I have 4 activities in my android app.When the first activity is created, it starts music in the background. Now when the user goes from 1st activity to the 2nd activity I want the song to continue without any interruption. The song should stop only when the user is out of the app.
Right now the music stops when I am going out of one activity and starts from the beginning in the next activity.
Keep the player in the background as a static reference. Then let it know if you are moving within the app or out of it. Here is how I would do it. I am using a class named DJ for this purpose.
public class DJ {
private static MediaPlayer player;
private static boolean keepMusicOn;
public static void iAmIn(Context context){
if (player == null){
player = MediaPlayer.create(context, R.raw.music1);
player.setLooping(true);
try{
player.prepare();
}
catch (IllegalStateException e){}
catch (IOException e){}
}
if(!player.isPlaying()){
player.start();
}
keepMusicOn= false;
}
public static void keepMusicOn(){
keepMusicOn= true;
}
public static void iAmLeaving(){
if(!keepMusicOn){
player.pause();
}
}
}
Now from your Activity call the DJ like this.(Let him know if you would like to keep the music on)
public void onPause() {
super.onPause();
DJ.iAmLeaving();
}
public void onResume(){
super.onResume();
DJ.iAmIn(this);
}
public void buttonOnClick(View view){
DJ.keepMusicOn();
Intent intent = new Intent(this, TheOtherActivity.class);
startActivity(intent);
}
I did it this way and I'm pleased with the result:
1st create the service:
public class LocalService extends Service
{
// This is the object that receives interactions from clients. See RemoteService for a more complete example.
private final IBinder mBinder = new LocalBinder();
private MediaPlayer player;
/**
* Class for clients to access. Because we know this service always runs in
* the same process as its clients, we don't need to deal with IPC.
*/
public class LocalBinder extends Binder
{
LocalService getService()
{
return LocalService.this;
}
}
#Override
public void onCreate()
{
}
#Override
public int onStartCommand(Intent intent, int flags, int startId)
{
// We want this service to continue running until it is explicitly stopped, so return sticky.
return START_STICKY;
}
#Override
public void onDestroy()
{
destroy();
}
#Override
public IBinder onBind(Intent intent)
{
return mBinder;
}
public void play(int res)
{
try
{
player = MediaPlayer.create(this, res);
player.setLooping(true);
player.setVolume(0.1f, 0.1f);
player.start();
}
catch(Exception e)
{
e.printStackTrace();
}
}
public void pause()
{
if(null != player && player.isPlaying())
{
player.pause();
player.seekTo(0);
}
}
public void resume()
{
try
{
if(null != player && !player.isPlaying())
{
player.start();
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
public void destroy()
{
if(null != player)
{
if(player.isPlaying())
{
player.stop();
}
player.release();
player = null;
}
}
}
2nd, create a base activity and extend all your activities in witch you wish to play the background music from it:
public class ActivityBase extends Activity
{
private Context context = ActivityBase.this;
private final int [] background_sound = { R.raw.azilum_2, R.raw.bg_sound_5 };
private LocalService mBoundService;
private boolean mIsBound = false;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
doBindService();
}
#Override
protected void onStart()
{
super.onStart();
try
{
if(null != mBoundService)
{
Random rand = new Random();
int what = background_sound[rand.nextInt(background_sound.length)];
mBoundService.play(what);
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
#Override
protected void onStop()
{
super.onStop();
basePause();
}
protected void baseResume()
{
try
{
if(null != mBoundService)
{
mBoundService.resume();
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
protected void basePause()
{
try
{
if(null != mBoundService)
{
mBoundService.pause();
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
private ServiceConnection mConnection = new ServiceConnection()
{
public void onServiceConnected(ComponentName className, IBinder service)
{
// This is called when the connection with the service has been
// established, giving us the service object we can use to
// interact with the service. Because we have bound to a explicit
// service that we know is running in our own process, we can
// cast its IBinder to a concrete class and directly access it.
mBoundService = ((LocalService.LocalBinder) service).getService();
if(null != mBoundService)
{
Random rand = new Random();
int what = background_sound[rand.nextInt(background_sound.length)];
mBoundService.play(what);
}
}
public void onServiceDisconnected(ComponentName className)
{
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
// Because it is running in our same process, we should never
// see this happen.
mBoundService = null;
if(null != mBoundService)
{
mBoundService.destroy();
}
}
};
private void doBindService()
{
// Establish a connection with the service. We use an explicit
// class name because we want a specific service implementation that
// we know will be running in our own process (and thus won't be
// supporting component replacement by other applications).
Intent i = new Intent(getApplicationContext(), LocalService.class);
bindService(i, mConnection, Context.BIND_AUTO_CREATE);
mIsBound = true;
}
private void doUnbindService()
{
if (mIsBound)
{
// Detach our existing connection.
unbindService(mConnection);
mIsBound = false;
}
}
#Override
protected void onDestroy()
{
super.onDestroy();
doUnbindService();
}
}
And that's it, now you have background sound in all the activities that are extended from ActivityBase.
You can even control the pause / resume functionality by calling basePause() / baseResume().
Don't forget to declare the service in manifest:
<service android:name="com.gga.screaming.speech.LocalService" />
The idea is that you should not play music from the activity itself. On Android, Activities, and other contexts, have life cycles. It means they will live...and die. And when dead, they can't do anything any more.
So you gotta find something with a lifecycle that lasts more than a single activity if you want the music to live longer.
The easiest solution is an Android service. You can find a good thread here : Android background music service

End SurfaceView and GameThread on exiting app

I've got a surfaceview and a gameThread class.
The gameThread keeps updating and painting on the SurfaceView class.
Now when I exit the app (By pressing the home or back button) I get a message that the app force closed. That's because the GameThread still tries to draw on the erased surfaceview...
So how do i properly end the app without getting this force close notification? I would like the GameThread class to stop when the back button is pressed. It should pause when pressing on home and running in the background. When re-entering the still running game it should resume....
Any ideas?
This is my GameThread class:
public class GameThread extends Thread{
private GameView view;
public boolean isRunning = false;
public GameThread(GameView view) {
this.view = view;
}
public void setRunning(boolean setRunning) {
isRunning = setRunning;
}
public void run() {
while(isRunning) {
Canvas c = null;
view.update();
try {
c = view.getHolder().lockCanvas();
synchronized (view.getHolder()) {
view.draw(c);
}
}finally {
if(c != null) {
view.getHolder().unlockCanvasAndPost(c);
}
}
}
}
It keeps updating my GameView class:
public class GameView extends SurfaceView{
private GameThread gameThread;
public GameView(Context context, Activity activity) {
super(context);
gameThread = new GameThread(this);
init();
holder = getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
gameThread.setRunning(true);
gameThread.start();
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
});
}
public void init() {
}
public void update() {
}
public void draw(Canvas canvas) {
super.draw(canvas);
}
}
When pressing home my logcat shows this up:
02-24 18:24:59.336: E/SurfaceHolder(839): Exception locking surface
02-24 18:24:59.336: E/SurfaceHolder(839): java.lang.IllegalStateException: Surface has already been released.
02-24 18:24:59.336: E/SurfaceHolder(839): at android.view.Surface.checkNotReleasedLocked(Surface.java:437)
02-24 18:24:59.336: E/SurfaceHolder(839): at android.view.Surface.lockCanvas(Surface.java:245)
02-24 18:24:59.336: E/SurfaceHolder(839): at android.view.SurfaceView$4.internalLockCanvas(SurfaceView.java:872)
Main activity:
public class MainActivity extends Activity {
private RelativeLayout relativeLayout;
private GameView gameView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new GameView(this, this);
setContentView(R.layout.activity_main);
relativeLayout = (RelativeLayout)findViewById(R.id.mainView);
relativeLayout.addView(gameView);
}
#Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
gameView.onBackPressed();
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
Well you can make changes to your code like this.
In GameView
#Override
public void surfaceCreated(SurfaceHolder holder) {
MainActivity obj=new MainActivity();
stop=obj.getBoolean(); //receive status of boolean from main activity
//stop is boolean set if backPressed in main activity
if(!stop){
gameThread.setRunning(true);
gameThread.start();
}
else
gameThread.setRunning(false);
}
In MainActivity
public Boolean getBoolean(){
return stop;
}
public void setBoolean(Boolean bools){
stop=bools;
}
#Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
stop=true;
setBoolean(stop);
try{
Thread.sleep(200); // Be safeside and wait for Game thread to finish
}
catch(Exception e){
e.printStackTrace();
}
MainActivity.finish();
}
Call setBoolean once before backPress else it will give error. Make amendments to code as per your needs.
Hope it helps. Cheers. :)
The Surface underlying the SurfaceView gets destroyed between onPause() and onDestroy() in the app lifecycle. Send a message to your renderer thread in onPause(), and wait for the thread to shut down.
For an example, see the "Hardware scaler exerciser" activity in Grafika. It starts the render thread in the SurfaceHolder's surfaceCreated() callback.
Update: it felt weird to start the thread in surfaceCreated() and stop it in onPause(), so after a bit of reshuffling HardwareScalerActivity now stops the thread in surfaceDestroyed(). (The SurfaceView documentation is a bit ambiguous -- it says the Surface is valid "between surfaceCreated() and surfaceDestroyed()" -- but looking at the SurfaceView implementation this appears to be the expected usage.)
Update 2: I updated the code some more after finding cases it didn't handle well. There's now a 60-line megacomment in the sources, which can also been seen in my answer to a similar question.
Update 3: The mega-comment turned into an architecture doc appendix. Grafika's "hardware scaler exerciser" activity demonstrates approach #2, while "texture from camera" demonstrates approach #1.

MediaPlayer skips forward about 6 seconds on rotation

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.

Categories

Resources