Built in player

This commit is contained in:
Thomas 2020-04-04 19:09:17 +02:00
parent 1632b6f57e
commit dccfd0e1ae
10 changed files with 413 additions and 1 deletions

View File

@ -29,8 +29,10 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0'
implementation 'org.jsoup:jsoup:1.13.1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View File

@ -182,6 +182,11 @@
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:theme="@style/AppTheme" />
<activity android:name="app.fedilab.nitterizeme.WebviewPlayerActivity"
android:label="@string/app_name"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen"/>
</application>
</manifest>

View File

@ -0,0 +1,182 @@
package app.fedilab.nitterizeme;
/* Copyright 2020 Thomas Schneider
*
* This file is a part of NitterizeMe
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* NitterizeMe is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with NitterizeMe; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.Activity;
import android.media.MediaPlayer;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
/**
* Custom WebChromeClient
*/
public class PlayerChromeClient extends WebChromeClient implements MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener {
private FrameLayout videoViewContainer;
private CustomViewCallback videoViewCallback;
private ToggledFullscreenCallback toggledFullscreenCallback;
private WebView webView;
private View activityNonVideoView;
private ViewGroup activityVideoView;
private boolean isVideoFullscreen;
private Activity activity;
PlayerChromeClient(Activity activity, WebView webView, FrameLayout activityNonVideoView, ViewGroup activityVideoView) {
this.activity = activity;
this.isVideoFullscreen = false;
this.webView = webView;
this.activityNonVideoView = activityNonVideoView;
this.activityVideoView = activityVideoView;
}
/**
* 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
*/
void setOnToggledFullscreen(ToggledFullscreenCallback callback) {
this.toggledFullscreenCallback = callback;
}
//FULLSCREEN VIDEO
//Code from https://stackoverflow.com/a/16179544/3197259
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
if (view instanceof FrameLayout) {
if (((AppCompatActivity) activity).getSupportActionBar() != null)
//noinspection ConstantConditions
((AppCompatActivity) activity).getSupportActionBar().hide();
// A video wants to be shown
FrameLayout frameLayout = (FrameLayout) view;
View focusedChild = frameLayout.getFocusedChild();
// Save video related variables
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.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);
}
}
}
// Available in API level 14+, deprecated in API level 18+
@Override
public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) {
onShowCustomView(view, callback);
}
@Override
public void onHideCustomView() {
if (((AppCompatActivity) activity).getSupportActionBar() != null)
//noinspection ConstantConditions
((AppCompatActivity) activity).getSupportActionBar().show();
// 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).
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);
}
}
}
// Video will start loading
@Override
public View getVideoLoadingProgressView() {
return super.getVideoLoadingProgressView();
}
// Video finished playing, only called in the case of android.widget.VideoView (typically API level <11)
@Override
public void onCompletion(MediaPlayer mp) {
onHideCustomView();
}
// Error while playing video, only called in the case of android.widget.VideoView (typically API level <11)
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false; // By returning false, onCompletion() will be called
}
public interface ToggledFullscreenCallback {
void toggledFullscreen(boolean fullscreen);
}
}

View File

@ -35,6 +35,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.net.MalformedURLException;
import java.net.URL;
@ -420,7 +421,20 @@ public class TransformActivity extends Activity {
targetIntent.setComponent(new ComponentName("org.schabi.newpipe", "org.schabi.newpipe.RouterActivity"));
targetIntents.add(targetIntent);
}
if (targetIntents.size() > 0) {
if (Arrays.asList(invidious_instances).contains(Objects.requireNonNull(i.getData()).getHost())) {
if( !i.getData().toString().contains("videoplayback")){
Intent intentPlayer = new Intent(TransformActivity.this, WebviewPlayerActivity.class);
intentPlayer.putExtra("url", i.getData().toString());
startActivity(intentPlayer);
}else{
Intent intentStreamingUrl = new Intent(Utils.RECEIVE_STREAMING_URL);
Bundle b = new Bundle();
b.putString("streaming_url", i.getData().toString());
intentStreamingUrl.putExtras(b);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intentStreamingUrl);
}
}else if (targetIntents.size() > 0) {
Intent chooserIntent = Intent.createChooser(targetIntents.remove(0), getString(R.string.open_with));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[]{}));
startActivity(chooserIntent);

View File

@ -31,6 +31,8 @@ import static app.fedilab.nitterizeme.MainActivity.shortener_domains;
class Utils {
public static final String RECEIVE_STREAMING_URL = "receive_streaming_url";
private static final String[] UTM_PARAMS = {
"utm_\\w+",
"ga_source",

View File

@ -0,0 +1,146 @@
package app.fedilab.nitterizeme;
/* Copyright 2020 Thomas Schneider
*
* This file is a part of NitterizeMe
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* NitterizeMe is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with NitterizeMe; if not,
* see <http://www.gnu.org/licenses>. */
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.IOException;
public class WebviewPlayerActivity extends AppCompatActivity {
private String videoUrl;
private WebView webView;
private BroadcastReceiver receive_data;
@SuppressLint("SetJavaScriptEnabled")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String url = null;
Bundle b = getIntent().getExtras();
if (b != null) {
url = b.getString("url", null);
}
if( url == null){
finish();
}
setContentView(R.layout.activity_webview_player);
final ViewGroup videoLayout = findViewById(R.id.videoLayout);
webView = findViewById(R.id.webview);
FrameLayout webview_container = findViewById(R.id.webview_container);
webView.getSettings().setJavaScriptEnabled(true);
PlayerChromeClient playerChromeClient = new PlayerChromeClient(WebviewPlayerActivity.this, webView, webview_container, videoLayout);
playerChromeClient.setOnToggledFullscreen(fullscreen -> {
if (fullscreen) {
videoLayout.setVisibility(View.VISIBLE);
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
} else {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
videoLayout.setVisibility(View.GONE);
}
});
receive_data = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
assert b != null;
String streaming_url = b.getString("streaming_url", null);
if (streaming_url != null ) {
webView.stopLoading();
webView.loadUrl(streaming_url);
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
videoLayout.setVisibility(View.GONE);
}
}
};
webView.setWebChromeClient(playerChromeClient);
LocalBroadcastManager.getInstance(WebviewPlayerActivity.this).registerReceiver(receive_data, new IntentFilter(Utils.RECEIVE_STREAMING_URL));
String finalUrl = url;
AsyncTask.execute(() -> {
try {
Document document = Jsoup
.connect(finalUrl).ignoreContentType(true).get();
Element video = document.select("video").first();
if( video != null ){
Element source = video.select("source").first();
if( source != null ) {
videoUrl = source.absUrl("src");
runOnUiThread(() -> {
webView.loadUrl(videoUrl);
});
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
@Override
public void onPause(){
super.onPause();
if( webView != null ){
webView.onPause();
}
}
@Override
public void onResume(){
super.onResume();
if( webView != null ){
webView.onResume();
}
}
@Override
public void onDestroy(){
super.onDestroy();
if (receive_data != null)
LocalBroadcastManager.getInstance(WebviewPlayerActivity.this).unregisterReceiver(receive_data);
}
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?><!--
/* Copyright 2020 Thomas Schneider
*
* This file is a part of NitterizeMe
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* NitterizeMe is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with NitterizeMe; if not,
* see <http://www.gnu.org/licenses>. */
-->
<RelativeLayout
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=".WebviewPlayerActivity">
<FrameLayout
android:id="@+id/webview_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView
android:background="@android:color/black"
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>
<!-- View where the video will be shown when video goes fullscreen -->
<RelativeLayout
android:id="@+id/videoLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</RelativeLayout>

View File

@ -258,6 +258,17 @@
app:layout_constraintStart_toEndOf="@id/label_instance_invidious"
app:layout_constraintTop_toTopOf="@id/guide_invidious" />
<CheckBox
android:id="@+id/enable_embed_player"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="@string/use_built_in_player"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier_invidious" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_invidious"
android:layout_width="wrap_content"

View File

@ -52,4 +52,5 @@
<string name="instance_info">Information about listed instances</string>
<string name="about_instances">The list of instances comes from\n\n <a href="%1$s">%2$s</a>\n\nYou can contribute to this list by adding or removing instances.</string>
<string name="about_instances_title">About instances</string>
<string name="use_built_in_player">Use the built-in player</string>
</resources>

View File

@ -40,4 +40,12 @@
<item name="cardCornerRadius">5dp</item>
</style>
<style name="Theme.AppCompat.Light.NoActionBar.FullScreen" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>