Search code examples
androidandroid-mediaplayerapk-expansion-files

Using ZipFileProvider with MediaPlayer sometimes results in IllegalStateException


I have some trouble getting MediaPlayer to reliably play video on android 4.3 (and lower versions for that matter). Most of the code is just standard boilerplate (included for completeness).

I am using the APK Expansion library, and my obb is just a zip of som thumbnails and some m4v videofiles, along with an XML file for describing the videos.

I think there is a timing problem, because the error seems to only occur when playing small files (<30 seconds or in my case <3Mb). It happens when running mMediaPlayer.prepare() in the following code:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    setContentView(R.layout.activity_play_video);
    Intent intent = getIntent();
    mFileName = intent.getStringExtra("Video"); //which video should we play
    ...
    mHolder = mSurfaceView.getHolder();
    mHolder.addCallback(this);
}


@Override
public void surfaceCreated(SurfaceHolder holder) {
            playVideo();
}


public void playVideo() {
    String videoFilePath = Const.CONTENT_URI + "/" + mFileName + ".m4v";
    Uri zipVideo = Uri.parse(videoFilePath);

    mMediaPlayer = new MediaPlayer();
    try {
        mMediaPlayer.setDataSource(getApplicationContext(), zipVideo);
        mMediaPlayer.setDisplay(mHolder);
        mMediaPlayer.prepare();
        mMediaPlayer.setOnPreparedListener(this);
        mMediaPlayer.setOnErrorListener (new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                Log.d("ERROR", "what: " + what + " extra: " + extra);
                return false;
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    } catch (IllegalStateException e) {
        e.printStackTrace();
        finish();
    }
    ...
}

@Override
public void onPrepared(MediaPlayer mp) {
    Log.d("playback " , "onPrepared");
    start();
}

I have been investigating the problem, and it seems that setDataSource(...) doesn't set the correct state of mMediaPlayer before prepare() is called.

What makes this problem even more interesting is that when playing larger files it works flawlessly.

EDIT: Here the output from logcat.

11-01 13:19:12.483: E/MediaPlayer(26369): prepareAsync called in state 1
11-01 13:19:12.483: W/System.err(26369): java.lang.IllegalStateException
11-01 13:19:12.483: W/System.err(26369):    at android.media.MediaPlayer.prepare(Native Method)
11-01 13:19:12.483: W/System.err(26369):    at com.skilessons4u.lite.PlayVideoActivity.prepareVideo(PlayVideoActivity.java:126)
11-01 13:19:12.483: W/System.err(26369):    at com.skilessons4u.lite.PlayVideoActivity.surfaceCreated(PlayVideoActivity.java:100)
11-01 13:19:12.483: W/System.err(26369):    at android.view.SurfaceView.updateWindow(SurfaceView.java:571)
11-01 13:19:12.483: W/System.err(26369):    at android.view.SurfaceView.access$000(SurfaceView.java:86)
11-01 13:19:12.483: W/System.err(26369):    at android.view.SurfaceView$3.onPreDraw(SurfaceView.java:175)
11-01 13:19:12.483: W/System.err(26369):    at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:833)
11-01 13:19:12.483: W/System.err(26369):    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1860)
11-01 13:19:12.483: W/System.err(26369):    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1004)
11-01 13:19:12.483: W/System.err(26369):    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5481)
11-01 13:19:12.483: W/System.err(26369):    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749)
11-01 13:19:12.483: W/System.err(26369):    at android.view.Choreographer.doCallbacks(Choreographer.java:562)
11-01 13:19:12.483: W/System.err(26369):    at android.view.Choreographer.doFrame(Choreographer.java:532)
11-01 13:19:12.483: W/System.err(26369):    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735)
11-01 13:19:12.483: W/System.err(26369):    at android.os.Handler.handleCallback(Handler.java:730)
11-01 13:19:12.483: W/System.err(26369):    at android.os.Handler.dispatchMessage(Handler.java:92)
11-01 13:19:12.483: W/System.err(26369):    at android.os.Looper.loop(Looper.java:137)
11-01 13:19:12.483: W/System.err(26369):    at android.app.ActivityThread.main(ActivityThread.java:5103)
11-01 13:19:12.483: W/System.err(26369):    at java.lang.reflect.Method.invokeNative(Native Method)
11-01 13:19:12.483: W/System.err(26369):    at java.lang.reflect.Method.invoke(Method.java:525)
11-01 13:19:12.483: W/System.err(26369):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
11-01 13:19:12.483: W/System.err(26369):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
11-01 13:19:12.483: W/System.err(26369):    at dalvik.system.NativeStart.main(Native Method)

For completeness, here the commands I run in a script to generate the obb file. The thumbnails shows up fine, tough.

zip -rn .m4v:.png $ZIPFILE $XMLFILE $VIDEOSDIR $THUMBDIR
zipalign 4 $ZIPFILE $OBBFILE

Solution

  • I was able to get this working, by using an AssetFileDescriptor, instead a Uri

    The approach that eventually solved my problem was doing the following:

    AssetFileDescriptor afd = expansionFile.getAssetFileDescriptor(mFileName + ".m4v");
    FileDescriptor fd = afd.getFileDescriptor();
    long offset = afd.getStartOffset();
    long length = afd.getLength();
    
    try {
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setDataSource(fd,offset, length);
        mMediaPlayer.prepare();
    ...
    } catch (IOException e) {
        e.printStackTrace();
    } catch (IllegalStateException e) {
        e.printStackTrace();
    }
    

    I had tried that approach before, but without the offset and length parameters, and that didn't work. What you see above is was finally got it to work.

    My faulty approach before was using a "ZipFileContentProvider" like so:

    public final static String AUTHORITY = "com.example.app.provider.ZipFileContentProvider";
    public final static Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
    
    String videoFilePath = CONTENT_URI + "/" + mFileName + ".m4v";
    Uri zipVideo = Uri.parse(videoFilePath);
    mMediaPlayer = new MediaPlayer();
    mMediaPlayer.setDataSource(this, zipVideo);
    mMediaPlayer.setDisplay(mHolder);
    mMediaPlayer.prepare();
    

    However, as mentionend in the original question, this only worked in some specific instances, which was far from ideal.