Android WebView Playing HTML5/h.264/mp4 Video, How

2019-01-24 00:05发布

问题:

I have an Activity which is a WebView. I have a WebChromeClient inside it. Inside that, there are several callbacks which are meant to return the MediaPlayer handling the video bits. For example:

@Override
public void onPrepared(MediaPlayer mp) {
    Log.i(TAG, " -------------> onPrepared");
}

These fail to fire when I load an MP4 stream into the WebView using HTML <video> tags (via injection).

When I finish() the activity, the logcat reports this:

09-13 23:55:24.590: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.
09-13 23:55:24.680: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.
09-13 23:55:24.680: E/MediaPlayer(7949): mOnVideoSizeChangedListener is null. Failed to send MEDIA_SET_VIDEO_SIZE message.
09-13 23:55:25.675: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.
09-13 23:55:26.735: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.
09-13 23:55:27.755: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.
09-13 23:55:28.705: E/MediaPlayer(7949): mOnBufferingUpdateListener is null. Failed to send MEDIA_BUFFERING_UPDATE message.

Try as I may, I can't get it to stop, even though the WebView is cleared, and then destroyed. When loading video using <video> tags, I don't know of any way to force the WebChromeClient to use a particular MediaPlayer that I create for it. It seems determined to use some hidden one, which reports the above. Is there a way to locate the MediaPlayer created from via a <video> tag on a WebView?

--Update

Here is the code for initializing the WebView:

        mWebView = new WebView(mContext);

        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.getSettings().setPluginState(PluginState.OFF);
        mWebView.setVisibility(View.INVISIBLE);
        mWebView.getSettings().setDefaultZoom(WebSettings.ZoomDensity.FAR);
        mWebView.getSettings().setBuiltInZoomControls(false);
        mWebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
        mWebView.getSettings().setLoadWithOverviewMode(true);
        mWebView.getSettings().setUseWideViewPort(true);
        mWebView.clearHistory();
        mWebView.clearFormData();
        mWebView.clearCache(true);
        mWebView.getSettings().setAllowFileAccess(true);
        mWebView.getSettings().setUserAgentString("Android Mozilla/5.0 AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30");

        chrome = new MyWebChromeClient();
        mWebView.setWebChromeClient(chrome);

        wvc = new MyWebViewClient();
        mWebView.setWebViewClient(wvc);

        mDomain = "http://foo.bar.com";
        mWebView.requestFocus(View.FOCUS_DOWN);

        String meat = genMainHTML(R.raw.frame);

        mWebView.loadDataWithBaseURL(mDomain, meat, "text/html", "utf-8", null);

The "frame" code is an iframe to launch some video (both Vimeo and YouTube seem to adopt this approach, at least). I have pruned this a bit to avoid cruft:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>
     <iframe src="-target.url.with.params-" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen>
     </iframe>
</div>
</body>
</html>

Within that WebView class, there exists this class:

private class MyWebViewClient extends WebViewClient {

    @Override
    public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            String injection = injectPageMonitor();
            if(injection != null && !injectionComplete) {
                // Log.i(TAG, " ---------------> Page Loaded . . .");
                view.loadUrl(injection);
                injectionComplete = true;
        }
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            Log.d(TAG, "*** Error ["+description+"] ["+failingUrl+"]");
        Toast.makeText(mContext, description, Toast.LENGTH_SHORT).show();
    }

}

And also this class:

    private class MyWebChromeClient extends WebChromeClient implements MediaPlayer.OnInfoListener, MediaPlayer.OnSeekCompleteListener, MediaPlayer.OnErrorListener, MediaPlayer.OnVideoSizeChangedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnBufferingUpdateListener {

       @Override
       public void onProgressChanged(WebView view, int progress) {
           //Log.e(TAG, " -------------> Progress Changed . . . . ["+progress+"] mWebView ["+mWebView+"] ["+view+"]");
           if(progress == 100) {
                               // Do something really interesting
           }  else  {
               updateBuffering(UPDATE_PERCENT_LAUNCH_EXTRACTOR + (progress / 2));
           }
       }

       @Override
       public boolean onConsoleMessage(ConsoleMessage cm) {
                       // Spit out lots of console messages here
           return(true);
       }
   }

And I have all the usual suspects overridden, just in case I get lucky and get some response out of the MediaPlayer:

        @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        // Jump up and down because we got a mp!
                    // never happens, so no jumping
    }

As a side-note, I did dig around using reflection and got ahold of the MediaPlayer that way, but I always feel a bit ookey after doing that kind of thing. And even though I have the MediaPlayer object, I'm reluctant to just release it. A little studying of the source code for MP suggests that nothing bad would happen, but . . .

This does not happen on every device, I have noticed. HTC seems to behave better than Samsung, for example ("same" OS/API level -- I'm not really ambitious enough to compare the two source trees).

The behavior noted below is annoying, but only for me (who is watching the logcat output. For the user or application it seems to have no effect. It's just that I really dislike the notion that I'm kicking off a MediaPlayer instance, setting it busily caching away, and then (if/when the user terminates my app) leaving that dingleberry hanging out there. I have a lot of money tied up in therapy at this point, and it doesn't seem to be helping.

Thanks in advance.

回答1:

There is no way to access the MediaPlayer without reflection - the WebViewClient and WebChromeClient do not intentionally expose it. And you are correct in saying that using reflection could cause you to break your app or the WebView as the code can vary across Android versions, and even across OEMs.

If you would like to do this, you will have to locate the instance of HTML5VideoView that corresponds to your video element. This object creates and maintains an instance to the MediaPlayer as "mPlayer". Please see https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/core/java/android/webkit/HTML5VideoView.java.



回答2:

in your class

private class MyWebChromeClient extends WebChromeClient 
                                implements ..... MediaPlayer.onCompletion {
}

adding

@Override
public void onCompletion(MediaPlayer mp) {
    // TODO Auto-generated method stub
    mp.release();           
}

gets rid of the error messages for me after the webview closes.

Now if only I could get the webview to play/stream vimeo videos :(