一、引言
? ? ? ? 近期,我發現電視家、火星直播等在線看電視直播的軟件都已倒閉,而我奶奶也再無法通過這些平臺看電視了。她已六十多歲,快七十歲啦。這些平臺的倒下對我來說其實沒有多大的影響,但是對于文化不多的她而言,生活中卻是少了一大樂趣。因為自己學過編程,所以我想幫她解決這個問題。她只聽得懂白話,又最愛看“廣東珠江臺”,因此,我通過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"/><applicationandroid: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;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);initView(); // initialize ui componentssetWebView(); // set webview's settingsloadVideoUrl(); // 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 instancemainWebView = new VideoEnabledWebView(this);mainWebView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));// add the webview instance to the main layoutmainNonVideoRelativeLayout.addView(mainWebView);// set general settings of webviewWebSettings 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 webviewVideoEnabledWebChromeClient videoEnabledWebChromeClient = new VideoEnabledWebChromeClient(mainNonVideoRelativeLayout, mainVideoLayout,null, mainWebView);mainWebView.setWebChromeClient(videoEnabledWebChromeClient);// create a WebViewClient for webviewmainWebView.setWebViewClient(new WebViewClient(){@Overridepublic void onPageFinished(WebView view, String url) {super.onPageFinished(view, url);// execute a javascript to automatically play the videoString 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"><RelativeLayoutandroid:id="@+id/main_rl_non_video"android:layout_width="match_parent"android:layout_height="match_parent" ></RelativeLayout><RelativeLayoutandroid: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;}@Overridepublic void onShowCustomView(View view, CustomViewCallback callback) {if (view instanceof FrameLayout) {// A video wants to be shownFrameLayout frameLayout = (FrameLayout) view;View focusedChild = frameLayout.getFocusedChild();// Save video related variablesthis.isVideoFullscreen = true;this.videoViewContainer = frameLayout;this.videoViewCallback = callback;// Hide the non-video view, add the video view, and show itactivityNonVideoView.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 eventsvideoView.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 belowif (webView != null && webView.getSettings().getJavaScriptEnabled() && focusedChild instanceof SurfaceView) {// Run javascript code that detects the video end and notifies the Javascript interfaceString 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 changeif (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 viewactivityVideoView.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 variablesisVideoFullscreen = false;videoViewContainer = null;videoViewCallback = null;// Notify full-screen changeif (toggledFullscreenCallback != null) {toggledFullscreenCallback.toggledFullscreen(false);}}}@Override// Video will start loadingpublic 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 VideoEnabledWebChromeClientpublic void notifyVideoEnd() {// This code is not executed in the UI thread, so we must force that to happennew Handler(Looper.getMainLooper()).post(new Runnable() {@Overridepublic void run() {if (videoEnabledWebChromeClient != null) {videoEnabledWebChromeClient.onHideCustomView();}}});}@android.webkit.JavascriptInterface@SuppressWarnings("unused")// Must match Javascript interface method of VideoEnabledWebChromeClientpublic void notifyVideoStart() {// This code is not executed in the UI thread, so we must force that to happennew Handler(Looper.getMainLooper()).post(new Runnable() {@Overridepublic 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);}@Overridepublic void loadData(String data, String mimeType, String encoding) {addJavascriptInterface();super.loadData(data, mimeType, encoding);}@Overridepublic void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {addJavascriptInterface();super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);}@Overridepublic void loadUrl(String url) {super.loadUrl(url);addJavascriptInterface();}@Overridepublic 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 VideoEnabledWebChromeClientaddJavascriptInterface(new JavascriptInterface(), "_VideoEnabledWebView");addedJavascriptInterface = true;}}
}
六、效果
? ? ? ? 打開App之后,經過2s的時間(對于個人而言,2s是可接受的等待時間)直接視頻全屏播放
? ? ? ? 但我發現url有時候不是特別穩定,所以有時候看不了,并建議使用電腦端訪問。
七、參考資料
1.如何在android WebView中全屏播放HTML5視頻?
2.android webview播放視頻自動全屏
八、聲明
上述代碼僅限個人的學習使用,請勿用于商業用途,請勿非法使用,謝謝。