一、引言
近期,我发现电视家、火星直播等在线看电视直播的软件都已倒闭,而我奶奶也再无法通过这些平台看电视了。她已六十多岁,快七十岁啦。这些平台的倒下对我来说其实没有多大的影响,但是对于文化不多的她而言,生活中却是少了一大乐趣。因为自己学过编程,所以我想帮她解决这个问题。她只听得懂白话,又最爱看“广东珠江台”,因此,我通过Android的编程技术,为她专门定制一款可以自动看广东珠江台的App,打开即用,免了点来点去的麻烦。虽说需求很小、只够她一人使用,但实现起来却并不简单呀。通过两天时间的深入钻研,最终我还是把这个小需求给实现了。为此编写一篇博客,如果日后自己还需要解决这样的问题时,我就直接ctrl+c加ctrl+v。当然,也希望这篇博客能够为你提供一些指导和帮助。
二、访问网页视频+自动播放的实现思路
由于许多的m3u8链接都已经失效,现在看电视直播只能通过一些官方网页来实现,比如央视网等等。那么,访问网页的话是可以通过Android的WebView来实现,实现的方法非常简单,就是在Activity界面之中添加一个WebView空间,然后通过下面的代码来访问网页:
main_wv = findViewById(R.id.main_wv); main_wv.setWebViewClient(new WebViewClient()); main_wv.setWebChromeClient(new WebChromeClient()); main_wv.loadUrl("https://***.***");
但是这样的话,最多也只能够竖屏中观看视频,显示十分地有限,而且还不会自动播放,如图:
所以,这里先介绍实现网页访问+自动播放的思路。WebView可以通过自定义的设置来控制它是否支持JavaScript脚本注入,即通过js来实现网页视频的自动播放。第一步是对WebView进行设置,而第二步是执行js自动播放视频的脚本,代码如下:
设置WebView支持js脚本
WebSettings settings = main_wv.getSettings(); // main_wv为WebView控件 settings.setJavaScriptEnabled(true);
执行js自动播放视频的脚本
String js = "javascript:"; js += "var videos = document.getElementsByTagName('video');"; js += "var video_last;"; js += "var video = videos[videos.length-1];"; js += "if (video != undefined && video != video_last) {"; { js += "video_last = video;"; js += "function video_start() {"; { js += "_VideoEnabledWebView.notifyVideoStart();"; } js += "}"; js += "video.addEventListener('play', video_start);"; } js += "}"; main_wv.loadUrl(js); // main_wv为WebView控件
忘了介绍,该js脚本在什么时候执行了。这里补充一下,为了使得页面自动播放视频,需要重写一个WebViewClient类,类名可以随你定义,比如“MyWebViewClient”,然后重写public void onPageFinished(WebView view, String url)方法,如下:
@Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); String js = "javascript:"; js += "var videos = document.getElementsByTagName('video');"; js += "var video_last;"; js += "var video = videos[videos.length-1];"; js += "if (video != undefined && video != video_last) {"; { js += "video_last = video;"; js += "function video_start() {"; { js += "_VideoEnabledWebView.notifyVideoStart();"; } js += "}"; js += "video.addEventListener('play', video_start);"; } js += "}"; main_wv.loadUrl(js); // main_wv为WebView控件 }
至此,还请记得改
main_wv.setWebViewClient(new WebViewClient()); // main_wv为WebView控件
为
main_wv.setWebViewClient(new MyWebViewClient()); // main_wv为WebView控件
这样,在访问网页时,视频就可以自动播放啦。
三、网页自动全屏思路
网页全屏的实现思路比较复杂,因为Android开发者的初衷是使网页设计者无法通过js的方式直接全屏地播放视频。也就是说,通过js注入的方式无法直接在WebView中实现网页的全屏播放。为什么呢?根据其他人的说法,这是因为Android开发者担心你的手机不小心访问到一个流氓网页,然后该网页直接全屏,使你的手机失控,被它播放的视频霸占许久、不能退出。我想了想,觉得这个理由倒是有些许合理的。因此执行js脚本注入无法直接在WebView中实现网页视频的全屏播放。
但是网页全屏的实现是完全没有问题的。大体的思路是:重写WebChromeClient和WebView这两个类,通过重写其中的内部方法,在加载完网页后,再调用js脚本注入,进而触发视频的全屏播放。而触发视频的全屏播放的js脚本为:
"javascript:(" + "function() { " + " var videos = document.getElementsByTagName('video'); " + " var video = videos[0]; " + " if (!document.webkitFullScreen && video.webkitEnterFullscreen) {" + " video.webkitEnterFullscreen(); " + " } " + " })()"
由于实现的细节很多,再加上我只是简单地研究了一下,所以没法更详细地展开说说了。请大家看后边“全部代码”的章节部分,了解更多细节。
四、手机默认横屏思路
手机默认横屏的思路实现起来非常简单,简单得不得了,只需要在Manifest.xml这个清单文件中对指定的Activity声明相关的属性即可。例如,用于播放的Activity为MainActivity,那么就这样设置:
<activity android:name=".MainActivity" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape" android:configChanges="orientation|screenSize|keyboardHidden" android:hardwareAccelerated="true"> </activity>
其中,最关键的两行代码为
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape"
上面一行说的是让app在手机上运行时不显示标题栏(这个可以看你个人需求,有的人喜欢留着,有的人喜欢去掉),而下面一行则是实现横屏的开关,landscape指的是风景,意为通过手机横屏的方式欣赏图片中的风景,以尽可能地使你更加清楚地目睹一张横向的风景图。
五、全部代码
项目的代码目录,其中画横线部分是重点,我从创建项目到生成可运行且有效果的App只改动过这些文件。下面我将每个框中的文件中的代码罗列出来。
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="***.***.******"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:hardwareAccelerated="true" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
MainActivity.java
package ***.***.***; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; import android.view.ViewGroup; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.LinearLayout; import android.widget.RelativeLayout; public class MainActivity extends Activity { VideoEnabledWebView mainWebView; RelativeLayout mainNonVideoRelativeLayout; ViewGroup mainVideoLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initView(); // initialize ui components setWebView(); // set webview's settings loadVideoUrl(); // load video url } private void initView() { setContentView(R.layout.activity_main); mainNonVideoRelativeLayout = (RelativeLayout) findViewById(R.id.main_rl_non_video); mainVideoLayout = (ViewGroup)findViewById(R.id.main_rl_video); } @SuppressLint("SetJavaScriptEnabled") private void setWebView() { // create a webview instance mainWebView = new VideoEnabledWebView(this); mainWebView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // add the webview instance to the main layout mainNonVideoRelativeLayout.addView(mainWebView); // set general settings of webview WebSettings settings = mainWebView.getSettings(); settings.setAllowFileAccess(true); settings.setBuiltInZoomControls(false); settings.setJavaScriptEnabled(true); settings.setBuiltInZoomControls(false); // create a WebChromeClient instance and use it to set the webview VideoEnabledWebChromeClient videoEnabledWebChromeClient = new VideoEnabledWebChromeClient(mainNonVideoRelativeLayout, mainVideoLayout,null, mainWebView); mainWebView.setWebChromeClient(videoEnabledWebChromeClient); // create a WebViewClient for webview mainWebView.setWebViewClient(new WebViewClient(){ @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); // execute a javascript to automatically play the video String js = "javascript:"; js += "var videos = document.getElementsByTagName('video');"; js += "var video_last;"; js += "var video = videos[videos.length-1];"; js += "if (video != undefined && video != video_last) {"; { js += "video_last = video;"; js += "function video_start() {"; { js += "_VideoEnabledWebView.notifyVideoStart();"; } js += "}"; js += "video.addEventListener('play', video_start);"; } js += "}"; mainWebView.loadUrl(js); } }); } private void loadVideoUrl() { mainWebView.loadUrl("https://******"); // your url that contains the video } }
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <RelativeLayout android:id="@+id/main_rl_non_video" android:layout_width="match_parent" android:layout_height="match_parent" > </RelativeLayout> <RelativeLayout android:id="@+id/main_rl_video" android:layout_width="match_parent" android:layout_height="match_parent" > </RelativeLayout> </androidx.constraintlayout.widget.ConstraintLayout>
VideoEnabledWebChromeClient.java
package ***.***.***; import android.media.MediaPlayer; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.webkit.WebChromeClient; import android.widget.FrameLayout; public class VideoEnabledWebChromeClient extends WebChromeClient implements MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { public interface ToggledFullscreenCallback { void toggledFullscreen(boolean fullscreen); } private View activityNonVideoView; private ViewGroup activityVideoView; private View loadingView; private VideoEnabledWebView webView; // Indicates if the video is being displayed using a custom view (typically full-screen) private boolean isVideoFullscreen; private FrameLayout videoViewContainer; private CustomViewCallback videoViewCallback; private ToggledFullscreenCallback toggledFullscreenCallback; /** * Never use this constructor alone. * This constructor allows this class to be defined as an inline inner class in which the user can override methods */ @SuppressWarnings("unused") public VideoEnabledWebChromeClient() { } /** * Builds a video enabled WebChromeClient. * @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen. * @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout. */ @SuppressWarnings("unused") public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView) { this.activityNonVideoView = activityNonVideoView; this.activityVideoView = activityVideoView; this.loadingView = null; this.webView = null; this.isVideoFullscreen = false; } /** * Builds a video enabled WebChromeClient. * @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen. * @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout. * @param loadingView A View to be shown while the video is loading (typically only used in API level <11). Must be already inflated and not attached to a parent view. */ @SuppressWarnings("unused") public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView, View loadingView) { this.activityNonVideoView = activityNonVideoView; this.activityVideoView = activityVideoView; this.loadingView = loadingView; this.webView = null; this.isVideoFullscreen = false; } /** * Builds a video enabled WebChromeClient. * @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen. * @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout. * @param loadingView A View to be shown while the video is loading (typically only used in API level <11). Must be already inflated and not attached to a parent view. * @param webView The owner VideoEnabledWebView. Passing it will enable the VideoEnabledWebChromeClient to detect the HTML5 video ended event and exit full-screen. * Note: The web page must only contain one video tag in order for the HTML5 video ended event to work. This could be improved if needed (see Javascript code). */ @SuppressWarnings("unused") public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView, View loadingView, VideoEnabledWebView webView) { this.activityNonVideoView = activityNonVideoView; this.activityVideoView = activityVideoView; this.loadingView = loadingView; this.webView = webView; this.isVideoFullscreen = false; } /** * Indicates if the video is being displayed using a custom view (typically full-screen) * @return true it the video is being displayed using a custom view (typically full-screen) */ public boolean isVideoFullscreen() { return isVideoFullscreen; } /** * Set a callback that will be fired when the video starts or finishes displaying using a custom view (typically full-screen) * @param callback A VideoEnabledWebChromeClient.ToggledFullscreenCallback callback */ @SuppressWarnings("unused") public void setOnToggledFullscreen(ToggledFullscreenCallback callback) { this.toggledFullscreenCallback = callback; } @Override public void onShowCustomView(View view, CustomViewCallback callback) { if (view instanceof FrameLayout) { // A video wants to be shown FrameLayout frameLayout = (FrameLayout) view; View focusedChild = frameLayout.getFocusedChild(); // Save video related variables this.isVideoFullscreen = true; this.videoViewContainer = frameLayout; this.videoViewCallback = callback; // Hide the non-video view, add the video view, and show it activityNonVideoView.setVisibility(View.INVISIBLE); activityVideoView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); activityVideoView.setVisibility(View.VISIBLE); if (focusedChild instanceof android.widget.VideoView) { // android.widget.VideoView (typically API level <11) android.widget.VideoView videoView = (android.widget.VideoView) focusedChild; // Handle all the required events videoView.setOnPreparedListener(this); videoView.setOnCompletionListener(this); videoView.setOnErrorListener(this); } else { // Other classes, including: // - android.webkit.HTML5VideoFullScreen$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 11-18) // - android.webkit.HTML5VideoFullScreen$VideoTextureView, which inherits from android.view.TextureView (typically API level 11-18) // - com.android.org.chromium.content.browser.ContentVideoView$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 19+) // Handle HTML5 video ended event only if the class is a SurfaceView // Test case: TextureView of Sony Xperia T API level 16 doesn't work fullscreen when loading the javascript below if (webView != null && webView.getSettings().getJavaScriptEnabled() && focusedChild instanceof SurfaceView) { // Run javascript code that detects the video end and notifies the Javascript interface String js = "javascript:"; js += "var _ytrp_html5_video_last;"; js += "var _ytrp_html5_video = document.getElementsByTagName('video')[0];"; js += "if (_ytrp_html5_video != undefined && _ytrp_html5_video != _ytrp_html5_video_last) {"; { js += "_ytrp_html5_video_last = _ytrp_html5_video;"; js += "function _ytrp_html5_video_ended() {"; { js += "_VideoEnabledWebView.notifyVideoEnd();"; // Must match Javascript interface name and method of VideoEnableWebView } js += "}"; js += "_ytrp_html5_video.addEventListener('ended', _ytrp_html5_video_ended);"; } js += "}"; webView.loadUrl(js); } } // Notify full-screen change if (toggledFullscreenCallback != null) { toggledFullscreenCallback.toggledFullscreen(true); } } } @Override // Available in API level 14+, deprecated in API level 18+ public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) { onShowCustomView(view, callback); } @Override // This method should be manually called on video end in all cases because it's not always called automatically. // This method must be manually called on back key press (from this class' onBackPressed() method). public void onHideCustomView() { if (isVideoFullscreen) { // Hide the video view, remove it, and show the non-video view activityVideoView.setVisibility(View.INVISIBLE); activityVideoView.removeView(videoViewContainer); activityNonVideoView.setVisibility(View.VISIBLE); // Call back (only in API level <19, because in API level 19+ with chromium webview it crashes) if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) { videoViewCallback.onCustomViewHidden(); } // Reset video related variables isVideoFullscreen = false; videoViewContainer = null; videoViewCallback = null; // Notify full-screen change if (toggledFullscreenCallback != null) { toggledFullscreenCallback.toggledFullscreen(false); } } } @Override // Video will start loading public View getVideoLoadingProgressView() { if (loadingView != null) { loadingView.setVisibility(View.VISIBLE); return loadingView; } else { return super.getVideoLoadingProgressView(); } } @Override // Video will start playing, only called in the case of android.widget.VideoView (typically API level <11) public void onPrepared(MediaPlayer mp) { if (loadingView != null) { loadingView.setVisibility(View.GONE); } } @Override // Video finished playing, only called in the case of android.widget.VideoView (typically API level <11) public void onCompletion(MediaPlayer mp) { onHideCustomView(); } @Override // Error while playing video, only called in the case of android.widget.VideoView (typically API level <11) public boolean onError(MediaPlayer mp, int what, int extra) { return false; // By returning false, onCompletion() will be called } /** * Notifies the class that the back key has been pressed by the user. * This must be called from the Activity's onBackPressed(), and if it returns false, the activity itself should handle it. Otherwise don't do anything. * @return Returns true if the event was handled, and false if was not (video view is not visible) */ @SuppressWarnings("unused") public boolean onBackPressed() { if (isVideoFullscreen) { onHideCustomView(); return true; } else { return false; } } }
VideoEnabledWebView.java
package ***.***.***; import android.annotation.SuppressLint; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.AttributeSet; import android.webkit.WebChromeClient; import android.webkit.WebView; import java.util.Map; public class VideoEnabledWebView extends WebView { public class JavascriptInterface { @android.webkit.JavascriptInterface @SuppressWarnings("unused") // Must match Javascript interface method of VideoEnabledWebChromeClient public void notifyVideoEnd() { // This code is not executed in the UI thread, so we must force that to happen new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { if (videoEnabledWebChromeClient != null) { videoEnabledWebChromeClient.onHideCustomView(); } } }); } @android.webkit.JavascriptInterface @SuppressWarnings("unused") // Must match Javascript interface method of VideoEnabledWebChromeClient public void notifyVideoStart() { // This code is not executed in the UI thread, so we must force that to happen new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { loadUrl("javascript:(" + "function() { " + " var videos = document.getElementsByTagName('video'); " + " var video = videos[0]; " + " if (!document.webkitFullScreen && video.webkitEnterFullscreen) {" + " video.webkitEnterFullscreen(); " + " } " + " })()"); } }); } } private VideoEnabledWebChromeClient videoEnabledWebChromeClient; private boolean addedJavascriptInterface; @SuppressWarnings("unused") public VideoEnabledWebView(Context context) { super(context); addedJavascriptInterface = false; } @SuppressWarnings("unused") public VideoEnabledWebView(Context context, AttributeSet attrs) { super(context, attrs); addedJavascriptInterface = false; } @SuppressWarnings("unused") public VideoEnabledWebView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); addedJavascriptInterface = false; } /** * Indicates if the video is being displayed using a custom view (typically full-screen) * * @return true it the video is being displayed using a custom view (typically full-screen) */ @SuppressWarnings("unused") public boolean isVideoFullscreen() { return videoEnabledWebChromeClient != null && videoEnabledWebChromeClient.isVideoFullscreen(); } /** * Pass only a VideoEnabledWebChromeClient instance. */ @Override @SuppressLint("SetJavaScriptEnabled") public void setWebChromeClient(WebChromeClient client) { getSettings().setJavaScriptEnabled(true); if (client instanceof VideoEnabledWebChromeClient) { this.videoEnabledWebChromeClient = (VideoEnabledWebChromeClient) client; } super.setWebChromeClient(client); } @Override public void loadData(String data, String mimeType, String encoding) { addJavascriptInterface(); super.loadData(data, mimeType, encoding); } @Override public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { addJavascriptInterface(); super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @Override public void loadUrl(String url) { super.loadUrl(url); addJavascriptInterface(); } @Override public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { addJavascriptInterface(); super.loadUrl(url, additionalHttpHeaders); } @SuppressLint("AddJavascriptInterface") private void addJavascriptInterface() { if (!addedJavascriptInterface) { // Add javascript interface to be called when the video ends (must be done before page load) // Must match Javascript interface name of VideoEnabledWebChromeClient addJavascriptInterface(new JavascriptInterface(), "_VideoEnabledWebView"); addedJavascriptInterface = true; } } }
六、效果
打开App之后,经过2s的时间(对于个人而言,2s是可接受的等待时间)直接视频全屏播放
但我发现url有时候不是特别稳定,所以有时候看不了,并建议使用电脑端访问。
七、参考资料
1.如何在android WebView中全屏播放HTML5视频?
八、声明
上述代码仅限个人的学习使用,请勿用于商业用途,请勿非法使用,谢谢。