Merge remote-tracking branch 'origin/master'
|
@ -5,7 +5,7 @@ android:
|
|||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-23.0.3
|
||||
- build-tools-25.0.0
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-25
|
||||
|
|
|
@ -8,8 +8,8 @@ android {
|
|||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
versionCode 25
|
||||
versionName "0.8.11"
|
||||
versionCode 26
|
||||
versionName "0.8.12"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -42,10 +42,10 @@ dependencies {
|
|||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
|
||||
compile 'com.google.code.gson:gson:2.4'
|
||||
compile 'com.nononsenseapps:filepicker:3.0.0'
|
||||
compile 'ch.acra:acra:4.9.0'
|
||||
compile 'com.devbrackets.android:exomedia:3.1.1'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
testCompile 'org.json:json:20160810'
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
|
@ -175,6 +176,66 @@
|
|||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PopupActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="NewPipe Popup mode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="vnd.youtube" />
|
||||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service android:name=".player.PopupVideoPlayer"/>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -85,9 +85,4 @@ public class MainActivity extends AppCompatActivity {
|
|||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
//ignore back
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* This activity is thought to open video streams form an external app using the popup playser.
|
||||
*/
|
||||
|
||||
public class PopupActivity extends Activity {
|
||||
private static final String TAG = RouterActivity.class.toString();
|
||||
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private final static String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
handleIntent(getIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private static String removeHeadingGibberish(final String input) {
|
||||
int start = 0;
|
||||
for (int i = input.indexOf("://") - 1; i >= 0; i--) {
|
||||
if (!input.substring(i, i + 1).matches("\\p{L}")) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return input.substring(start, input.length());
|
||||
}
|
||||
|
||||
private static String trim(final String input) {
|
||||
if (input == null || input.length() < 1) {
|
||||
return input;
|
||||
} else {
|
||||
String output = input;
|
||||
while (output.length() > 0 && output.substring(0, 1).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(1);
|
||||
}
|
||||
while (output.length() > 0
|
||||
&& output.substring(output.length() - 1, output.length()).matches(REGEX_REMOVE_FROM_URL)) {
|
||||
output = output.substring(0, output.length() - 1);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all Strings which look remotely like URLs from a text.
|
||||
* Used if NewPipe was called through share menu.
|
||||
*
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
private String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
for (String s : array) {
|
||||
s = trim(s);
|
||||
if (s.length() != 0) {
|
||||
if (s.matches(".+://.+")) {
|
||||
result.add(removeHeadingGibberish(s));
|
||||
} else if (s.matches(".+\\..+")) {
|
||||
result.add("http://" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(this)) {
|
||||
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
String videoUrl = "";
|
||||
StreamingService service = null;
|
||||
|
||||
// first gather data and find service
|
||||
if (intent.getData() != null) {
|
||||
// this means the video was called though another app
|
||||
videoUrl = intent.getData().toString();
|
||||
} else if (intent.getStringExtra(Intent.EXTRA_TEXT) != null) {
|
||||
//this means that vidoe was called through share menu
|
||||
String extraText = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
videoUrl = getUris(extraText)[0];
|
||||
}
|
||||
|
||||
service = NewPipe.getServiceByUrl(videoUrl);
|
||||
if (service == null) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
} else {
|
||||
Intent callIntent = new Intent();
|
||||
switch (service.getLinkTypeByUrl(videoUrl)) {
|
||||
case STREAM:
|
||||
callIntent.setClass(this, PopupVideoPlayer.class);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
Log.e(TAG, "NOT YET DEFINED");
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
callIntent.putExtra(NavStack.URL, videoUrl);
|
||||
callIntent.putExtra(NavStack.SERVICE_ID, service.getServiceId());
|
||||
startService(callIntent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,7 @@ class ActionBarHandler {
|
|||
// those are edited directly. Typically VideoItemDetailFragment will implement those callbacks.
|
||||
private OnActionListener onShareListener;
|
||||
private OnActionListener onOpenInBrowserListener;
|
||||
private OnActionListener onOpenInPopupListener;
|
||||
private OnActionListener onDownloadListener;
|
||||
private OnActionListener onPlayWithKodiListener;
|
||||
private OnActionListener onPlayAudioListener;
|
||||
|
@ -190,6 +191,12 @@ class ActionBarHandler {
|
|||
activity.startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_popup: {
|
||||
if(onOpenInPopupListener != null) {
|
||||
onOpenInPopupListener.onActionSelected(selectedVideoStream);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
Log.e(TAG, "Menu Item not known");
|
||||
}
|
||||
|
@ -208,6 +215,10 @@ class ActionBarHandler {
|
|||
onOpenInBrowserListener = listener;
|
||||
}
|
||||
|
||||
public void setOnOpenInPopupListener(OnActionListener listener) {
|
||||
onOpenInPopupListener = listener;
|
||||
}
|
||||
|
||||
public void setOnDownloadListener(OnActionListener listener) {
|
||||
onDownloadListener = listener;
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
|
@ -56,12 +54,13 @@ import org.schabi.newpipe.info_list.InfoItemBuilder;
|
|||
import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
|
@ -324,6 +323,19 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
streamThumbnail = loadedImage;
|
||||
|
||||
if (streamThumbnail != null) {
|
||||
// TODO: Change the thumbnail implementation
|
||||
|
||||
// When the thumbnail is not loaded yet, it not passes to the service in time
|
||||
// so, I can notify the service through a broadcast, but the problem is
|
||||
// when I click in another video, another thumbnail will be load, and will
|
||||
// notify again, so I send the videoUrl and compare with the service's url
|
||||
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
Intent intent = new Intent(PopupVideoPlayer.InternalListener.ACTION_UPDATE_THUMB);
|
||||
intent.putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url);
|
||||
getContext().sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -365,6 +377,28 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnOpenInPopupListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !PermissionHelper.checkSystemAlertWindowPermission(activity)) {
|
||||
Toast.makeText(activity, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
if (streamThumbnail != null)
|
||||
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
|
||||
VideoStream selectedVideoStream = info.video_streams.get(selectedStreamId);
|
||||
Intent i = new Intent(activity, PopupVideoPlayer.class);
|
||||
Toast.makeText(activity, "Starting in popup mode", Toast.LENGTH_SHORT).show();
|
||||
i.putExtra(PopupVideoPlayer.VIDEO_TITLE, info.title)
|
||||
.putExtra(PopupVideoPlayer.STREAM_URL, selectedVideoStream.url)
|
||||
.putExtra(PopupVideoPlayer.CHANNEL_NAME, info.uploader)
|
||||
.putExtra(PopupVideoPlayer.VIDEO_URL, info.webpage_url);
|
||||
activity.startService(i);
|
||||
}
|
||||
});
|
||||
|
||||
actionBarHandler.setOnPlayWithKodiListener(new ActionBarHandler.OnActionListener() {
|
||||
@Override
|
||||
public void onActionSelected(int selectedStreamId) {
|
||||
|
@ -753,13 +787,16 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
||||
|
||||
// TODO: Fix this mess
|
||||
if (streamThumbnail != null)
|
||||
ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail = streamThumbnail;
|
||||
// exo player
|
||||
|
||||
if(info.dashMpdUrl != null && !info.dashMpdUrl.isEmpty()) {
|
||||
// try dash
|
||||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setData(Uri.parse(info.dashMpdUrl))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||
.setData(Uri.parse(info.dashMpdUrl));
|
||||
//.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||
startActivity(intent);
|
||||
} else if((info.audio_streams != null && !info.audio_streams.isEmpty()) &&
|
||||
(info.video_only_streams != null && !info.video_only_streams.isEmpty())) {
|
||||
|
@ -770,7 +807,10 @@ public class VideoItemDetailFragment extends Fragment {
|
|||
Intent intent = new Intent(activity, ExoPlayerActivity.class)
|
||||
.setDataAndType(Uri.parse(selectedVideoStream.url),
|
||||
MediaFormat.getMimeById(selectedVideoStream.format))
|
||||
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER);
|
||||
|
||||
.putExtra(ExoPlayerActivity.VIDEO_TITLE, info.title)
|
||||
.putExtra(ExoPlayerActivity.CHANNEL_NAME, info.uploader);
|
||||
//.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER);
|
||||
|
||||
activity.startActivity(intent); // HERE !!!
|
||||
}
|
||||
|
|
|
@ -1,564 +1,220 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extended by Christian Schabesberger on 24.12.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3
|
||||
*
|
||||
* NewPipe 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.
|
||||
*
|
||||
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.player.exoplayer.DashRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.EventLogger;
|
||||
import org.schabi.newpipe.player.exoplayer.ExtractorRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.HlsRendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer;
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
import org.schabi.newpipe.player.exoplayer.SmoothStreamingRendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.metadata.GeobMetadata;
|
||||
import com.google.android.exoplayer.metadata.PrivMetadata;
|
||||
import com.google.android.exoplayer.metadata.TxxxMetadata;
|
||||
import com.google.android.exoplayer.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.SubtitleLayout;
|
||||
import com.google.android.exoplayer.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer.util.MimeTypes;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.View.OnKeyListener;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import android.widget.MediaController;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.PopupMenu.OnMenuItemClickListener;
|
||||
import android.widget.Toast;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.devbrackets.android.exomedia.listener.OnCompletionListener;
|
||||
import com.devbrackets.android.exomedia.listener.OnPreparedListener;
|
||||
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener;
|
||||
import com.devbrackets.android.exomedia.ui.widget.EMVideoView;
|
||||
import com.devbrackets.android.exomedia.ui.widget.VideoControlsMobile;
|
||||
|
||||
/**
|
||||
* An activity that plays media using {@link NPExoPlayer}.
|
||||
*/
|
||||
public class ExoPlayerActivity extends Activity {
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
// For use within demo app code.
|
||||
public static final String CONTENT_ID_EXTRA = "content_id";
|
||||
public static final String CONTENT_TYPE_EXTRA = "content_type";
|
||||
public static final String PROVIDER_EXTRA = "provider";
|
||||
public class ExoPlayerActivity extends Activity implements OnPreparedListener, OnCompletionListener {
|
||||
private static final String TAG = "ExoPlayerActivity";
|
||||
private static final boolean DEBUG = false;
|
||||
private EMVideoView videoView;
|
||||
private CustomVideoControls videoControls;
|
||||
|
||||
// For use when launching the demo app using adb.
|
||||
private static final String CONTENT_EXT_EXTRA = "type";
|
||||
|
||||
private static final String TAG = "PlayerActivity";
|
||||
private static final int MENU_GROUP_TRACKS = 1;
|
||||
private static final int ID_OFFSET = 2;
|
||||
|
||||
private static final CookieManager defaultCookieManager;
|
||||
static {
|
||||
defaultCookieManager = new CookieManager();
|
||||
defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
}
|
||||
|
||||
private EventLogger eventLogger;
|
||||
private MediaController mediaController;
|
||||
private View shutterView;
|
||||
private AspectRatioFrameLayout videoFrame;
|
||||
private SurfaceView surfaceView;
|
||||
private SubtitleLayout subtitleLayout;
|
||||
|
||||
private NPExoPlayer player;
|
||||
private boolean playerNeedsPrepare;
|
||||
|
||||
private long playerPosition;
|
||||
private boolean enableBackgroundAudio = true;
|
||||
|
||||
private Uri contentUri;
|
||||
private int contentType;
|
||||
private String contentId;
|
||||
private String provider;
|
||||
|
||||
private AudioCapabilitiesReceiver audioCapabilitiesReceiver;
|
||||
|
||||
|
||||
NPExoPlayer.Listener exoPlayerListener = new NPExoPlayer.Listener() {
|
||||
@Override
|
||||
public void onStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == ExoPlayer.STATE_ENDED) {
|
||||
showControls();
|
||||
}
|
||||
String text = "playWhenReady=" + playWhenReady + ", playbackState=";
|
||||
switch(playbackState) {
|
||||
case ExoPlayer.STATE_BUFFERING:
|
||||
text += "buffering";
|
||||
break;
|
||||
case ExoPlayer.STATE_ENDED:
|
||||
text += "ended";
|
||||
break;
|
||||
case ExoPlayer.STATE_IDLE:
|
||||
text += "idle";
|
||||
break;
|
||||
case ExoPlayer.STATE_PREPARING:
|
||||
text += "preparing";
|
||||
break;
|
||||
case ExoPlayer.STATE_READY:
|
||||
text += "ready";
|
||||
break;
|
||||
default:
|
||||
text += "unknown";
|
||||
break;
|
||||
}
|
||||
//todo: put text in some log
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
String errorString = null;
|
||||
if (e instanceof UnsupportedDrmException) {
|
||||
// Special case DRM failures.
|
||||
UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e;
|
||||
errorString = getString(Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
||||
: unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
|
||||
} else if (e instanceof ExoPlaybackException
|
||||
&& e.getCause() instanceof DecoderInitializationException) {
|
||||
// Special case for decoder initialization failures.
|
||||
DecoderInitializationException decoderInitializationException =
|
||||
(DecoderInitializationException) e.getCause();
|
||||
if (decoderInitializationException.decoderName == null) {
|
||||
if (decoderInitializationException.getCause() instanceof DecoderQueryException) {
|
||||
errorString = getString(R.string.error_querying_decoders);
|
||||
} else if (decoderInitializationException.secureDecoderRequired) {
|
||||
errorString = getString(R.string.error_no_secure_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
} else {
|
||||
errorString = getString(R.string.error_no_decoder,
|
||||
decoderInitializationException.mimeType);
|
||||
}
|
||||
} else {
|
||||
errorString = getString(R.string.error_instantiating_decoder,
|
||||
decoderInitializationException.decoderName);
|
||||
}
|
||||
}
|
||||
if (errorString != null) {
|
||||
Toast.makeText(getApplicationContext(), errorString, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
playerNeedsPrepare = true;
|
||||
showControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthAspectRatio) {
|
||||
shutterView.setVisibility(View.GONE);
|
||||
videoFrame.setAspectRatio(
|
||||
height == 0 ? 1 : (width * pixelWidthAspectRatio) / height);
|
||||
}
|
||||
};
|
||||
|
||||
SurfaceHolder.Callback surfaceHolderCallback = new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
if (player != null) {
|
||||
player.setSurface(holder.getSurface());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (player != null) {
|
||||
player.blockingClearSurface();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
NPExoPlayer.CaptionListener captionListener = new NPExoPlayer.CaptionListener() {
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
subtitleLayout.setCues(cues);
|
||||
}
|
||||
};
|
||||
|
||||
NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() {
|
||||
@Override
|
||||
public void onId3Metadata(Map<String, Object> metadata) {
|
||||
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
|
||||
if (TxxxMetadata.TYPE.equals(entry.getKey())) {
|
||||
TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
|
||||
TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
|
||||
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
|
||||
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
|
||||
PrivMetadata.TYPE, privMetadata.owner));
|
||||
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
|
||||
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
|
||||
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
|
||||
geobMetadata.description));
|
||||
} else {
|
||||
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AudioCapabilitiesReceiver.Listener audioCapabilitiesListener = new AudioCapabilitiesReceiver.Listener() {
|
||||
@Override
|
||||
public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
boolean backgrounded = player.getBackgrounded();
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
releasePlayer();
|
||||
preparePlayer(playWhenReady);
|
||||
player.setBackgrounded(backgrounded);
|
||||
}
|
||||
};
|
||||
|
||||
// Activity lifecycle
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
public static final String CHANNEL_NAME = "channel_name";
|
||||
private String videoTitle = "";
|
||||
private volatile String channelName = "";
|
||||
private int lastPosition;
|
||||
private boolean isFinished;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.exo_player_activity);
|
||||
View root = findViewById(R.id.root);
|
||||
root.setOnTouchListener(new OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
toggleControlsVisibility();
|
||||
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
|
||||
view.performClick();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
root.setOnKeyListener(new OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE
|
||||
|| keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
return false;
|
||||
}
|
||||
return mediaController.dispatchKeyEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
shutterView = findViewById(R.id.shutter);
|
||||
|
||||
videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
|
||||
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
|
||||
surfaceView.getHolder().addCallback(surfaceHolderCallback);
|
||||
subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles);
|
||||
|
||||
//todo: replace that creapy mediaController
|
||||
mediaController = new KeyCompatibleMediaController(this);
|
||||
mediaController.setAnchorView(root);
|
||||
|
||||
//todo: check what cookie handler does, and if we even need it
|
||||
CookieHandler currentHandler = CookieHandler.getDefault();
|
||||
if (currentHandler != defaultCookieManager) {
|
||||
CookieHandler.setDefault(defaultCookieManager);
|
||||
}
|
||||
|
||||
audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, audioCapabilitiesListener);
|
||||
audioCapabilitiesReceiver.register();
|
||||
setContentView(R.layout.activity_exo_player);
|
||||
videoView = (EMVideoView) findViewById(R.id.emVideoView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
playerPosition = 0;
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
Intent intent = getIntent();
|
||||
contentUri = intent.getData();
|
||||
contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA,
|
||||
inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA)));
|
||||
contentId = intent.getStringExtra(CONTENT_ID_EXTRA);
|
||||
provider = intent.getStringExtra(PROVIDER_EXTRA);
|
||||
configureSubtitleView();
|
||||
if (player == null) {
|
||||
if (!maybeRequestPermission()) {
|
||||
preparePlayer(true);
|
||||
}
|
||||
} else {
|
||||
player.setBackgrounded(false);
|
||||
}
|
||||
videoTitle = intent.getStringExtra(VIDEO_TITLE);
|
||||
channelName = intent.getStringExtra(CHANNEL_NAME);
|
||||
videoView.setOnPreparedListener(this);
|
||||
videoView.setOnCompletionListener(this);
|
||||
videoView.setVideoURI(intent.getData());
|
||||
|
||||
videoControls = new CustomVideoControls(this);
|
||||
videoControls.setTitle(videoTitle);
|
||||
videoControls.setSubTitle(channelName);
|
||||
|
||||
//We don't need these button until the playlist or queue is implemented
|
||||
videoControls.setNextButtonRemoved(true);
|
||||
videoControls.setPreviousButtonRemoved(true);
|
||||
|
||||
videoControls.setVisibilityListener(new VideoControlsVisibilityListener() {
|
||||
@Override
|
||||
public void onControlsShown() {
|
||||
if (DEBUG) Log.d(TAG, "------------ onControlsShown() called");
|
||||
showSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
public void onControlsHidden() {
|
||||
if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called");
|
||||
hideSystemUi();
|
||||
}
|
||||
});
|
||||
videoView.setControls(videoControls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called");
|
||||
videoView.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion() {
|
||||
if (DEBUG) Log.d(TAG, "onCompletion() called");
|
||||
// videoView.getVideoControls().setButtonListener();
|
||||
//videoView.restart();
|
||||
videoControls.setRewindButtonRemoved(true);
|
||||
videoControls.setFastForwardButtonRemoved(true);
|
||||
isFinished = true;
|
||||
videoControls.getSeekBar().setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (!enableBackgroundAudio) {
|
||||
releasePlayer();
|
||||
} else {
|
||||
player.setBackgrounded(true);
|
||||
}
|
||||
shutterView.setVisibility(View.VISIBLE);
|
||||
videoView.stopPlayback();
|
||||
lastPosition = videoView.getCurrentPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (lastPosition > 0) videoView.seekTo(lastPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
audioCapabilitiesReceiver.unregister();
|
||||
releasePlayer();
|
||||
videoView.stopPlayback();
|
||||
}
|
||||
|
||||
|
||||
// Permission request listener method
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
preparePlayer(true);
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), R.string.storage_permission_denied,
|
||||
Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
private void showSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow().getDecorView().setSystemUiVisibility(0);
|
||||
}
|
||||
|
||||
// Permission management methods
|
||||
|
||||
/**
|
||||
* Checks whether it is necessary to ask for permission to read storage. If necessary, it also
|
||||
* requests permission.
|
||||
*
|
||||
* @return true if a permission request is made. False if it is not necessary.
|
||||
*/
|
||||
@TargetApi(23)
|
||||
private boolean maybeRequestPermission() {
|
||||
if (requiresPermission(contentUri)) {
|
||||
requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
private void hideSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
||||
if (android.os.Build.VERSION.SDK_INT >= 17) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
|
||||
}
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
@TargetApi(23)
|
||||
private boolean requiresPermission(Uri uri) {
|
||||
return Util.SDK_INT >= 23
|
||||
&& Util.isLocalFileUri(uri)
|
||||
&& checkSelfPermission(permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
private class CustomVideoControls extends VideoControlsMobile {
|
||||
protected static final int FAST_FORWARD_REWIND_AMOUNT = 8000;
|
||||
|
||||
// Internal methods
|
||||
protected ImageButton fastForwardButton;
|
||||
protected ImageButton rewindButton;
|
||||
|
||||
private RendererBuilder getRendererBuilder() {
|
||||
String userAgent = Util.getUserAgent(this, "NewPipeExoPlayer");
|
||||
switch (contentType) {
|
||||
case Util.TYPE_SS:
|
||||
// default
|
||||
//return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_DASH:
|
||||
// if a dash manifest is available
|
||||
//return new DashRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_HLS:
|
||||
// for livestreams
|
||||
return new HlsRendererBuilder(this, userAgent, contentUri.toString());
|
||||
case Util.TYPE_OTHER:
|
||||
// video only streaming
|
||||
return new ExtractorRendererBuilder(this, userAgent, contentUri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + contentType);
|
||||
}
|
||||
}
|
||||
|
||||
private void preparePlayer(boolean playWhenReady) {
|
||||
if (player == null) {
|
||||
player = new NPExoPlayer(getRendererBuilder());
|
||||
player.addListener(exoPlayerListener);
|
||||
player.setCaptionListener(captionListener);
|
||||
player.setMetadataListener(id3MetadataListener);
|
||||
player.seekTo(playerPosition);
|
||||
playerNeedsPrepare = true;
|
||||
mediaController.setMediaPlayer(player.getPlayerControl());
|
||||
mediaController.setEnabled(true);
|
||||
eventLogger = new EventLogger();
|
||||
eventLogger.startSession();
|
||||
player.addListener(eventLogger);
|
||||
player.setInfoListener(eventLogger);
|
||||
player.setInternalErrorListener(eventLogger);
|
||||
}
|
||||
if (playerNeedsPrepare) {
|
||||
player.prepare();
|
||||
playerNeedsPrepare = false;
|
||||
}
|
||||
player.setSurface(surfaceView.getHolder().getSurface());
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
if (player != null) {
|
||||
playerPosition = player.getCurrentPosition();
|
||||
player.release();
|
||||
player = null;
|
||||
eventLogger.endSession();
|
||||
eventLogger = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleControlsVisibility() {
|
||||
if (mediaController.isShowing()) {
|
||||
mediaController.hide();
|
||||
} else {
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
|
||||
private void showControls() {
|
||||
mediaController.show(0);
|
||||
}
|
||||
|
||||
private void configureSubtitleView() {
|
||||
CaptionStyleCompat style;
|
||||
float fontScale;
|
||||
if (Util.SDK_INT >= 19) {
|
||||
style = getUserCaptionStyleV19();
|
||||
fontScale = getUserCaptionFontScaleV19();
|
||||
} else {
|
||||
style = CaptionStyleCompat.DEFAULT;
|
||||
fontScale = 1.0f;
|
||||
}
|
||||
subtitleLayout.setStyle(style);
|
||||
subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale);
|
||||
}
|
||||
|
||||
@TargetApi(19)
|
||||
private float getUserCaptionFontScaleV19() {
|
||||
CaptioningManager captioningManager =
|
||||
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
|
||||
return captioningManager.getFontScale();
|
||||
}
|
||||
|
||||
@TargetApi(19)
|
||||
private CaptionStyleCompat getUserCaptionStyleV19() {
|
||||
CaptioningManager captioningManager =
|
||||
(CaptioningManager) getSystemService(Context.CAPTIONING_SERVICE);
|
||||
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a best guess to infer the type from a media {@link Uri} and an optional overriding file
|
||||
* extension.
|
||||
*
|
||||
* @param uri The {@link Uri} of the media.
|
||||
* @param fileExtension An overriding file extension.
|
||||
* @return The inferred type.
|
||||
*/
|
||||
private static int inferContentType(Uri uri, String fileExtension) {
|
||||
String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension
|
||||
: uri.getLastPathSegment();
|
||||
return Util.inferContentType(lastPathSegment);
|
||||
}
|
||||
|
||||
private static final class KeyCompatibleMediaController extends MediaController {
|
||||
|
||||
private MediaController.MediaPlayerControl playerControl;
|
||||
|
||||
public KeyCompatibleMediaController(Context context) {
|
||||
public CustomVideoControls(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaPlayer(MediaController.MediaPlayerControl playerControl) {
|
||||
super.setMediaPlayer(playerControl);
|
||||
this.playerControl = playerControl;
|
||||
protected int getLayoutResource() {
|
||||
return R.layout.exomedia_custom_controls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int keyCode = event.getKeyCode();
|
||||
if (playerControl.canSeekForward() && keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
playerControl.seekTo(playerControl.getCurrentPosition() + 15000); // milliseconds
|
||||
show();
|
||||
protected void retrieveViews() {
|
||||
super.retrieveViews();
|
||||
rewindButton = (ImageButton) findViewById(R.id.exomedia_controls_frewind_btn);
|
||||
fastForwardButton = (ImageButton) findViewById(R.id.exomedia_controls_fforward_btn);
|
||||
}
|
||||
return true;
|
||||
} else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds
|
||||
show();
|
||||
|
||||
@Override
|
||||
protected void registerListeners() {
|
||||
super.registerListeners();
|
||||
rewindButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onRewindClicked();
|
||||
}
|
||||
});
|
||||
fastForwardButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onFastForwardClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean onFastForwardClicked() {
|
||||
if (videoView == null) return false;
|
||||
|
||||
int newPosition = videoView.getCurrentPosition() + FAST_FORWARD_REWIND_AMOUNT;
|
||||
if (newPosition > seekBar.getMax()) newPosition = seekBar.getMax();
|
||||
|
||||
performSeek(newPosition);
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
|
||||
public boolean onRewindClicked() {
|
||||
if (videoView == null) return false;
|
||||
|
||||
int newPosition = videoView.getCurrentPosition() - FAST_FORWARD_REWIND_AMOUNT;
|
||||
if (newPosition < 0) newPosition = 0;
|
||||
|
||||
performSeek(newPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFastForwardButtonRemoved(boolean removed) {
|
||||
fastForwardButton.setVisibility(removed ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRewindButtonRemoved(boolean removed) {
|
||||
rewindButton.setVisibility(removed ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPlayPauseClick() {
|
||||
super.onPlayPauseClick();
|
||||
if (videoView == null) return;
|
||||
if (DEBUG) Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration() + " position= " + videoView.getCurrentPosition());
|
||||
if (isFinished) {
|
||||
videoView.restart();
|
||||
setRewindButtonRemoved(false);
|
||||
setFastForwardButtonRemoved(false);
|
||||
isFinished = false;
|
||||
seekBar.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void performSeek(int newPosition) {
|
||||
internalListener.onSeekEnded(newPosition);
|
||||
}
|
||||
|
||||
public SeekBar getSeekBar() {
|
||||
return seekBar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,826 @@
|
|||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.PropertyValuesHolder;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.devbrackets.android.exomedia.listener.OnCompletionListener;
|
||||
import com.devbrackets.android.exomedia.listener.OnErrorListener;
|
||||
import com.devbrackets.android.exomedia.listener.OnPreparedListener;
|
||||
import com.devbrackets.android.exomedia.listener.OnSeekCompletionListener;
|
||||
import com.devbrackets.android.exomedia.ui.widget.EMVideoView;
|
||||
import com.devbrackets.android.exomedia.util.Repeater;
|
||||
import com.devbrackets.android.exomedia.util.TimeFormatUtil;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream_info.VideoStream;
|
||||
import org.schabi.newpipe.player.popup.PopupViewHolder;
|
||||
import org.schabi.newpipe.player.popup.StateInterface;
|
||||
import org.schabi.newpipe.util.NavStack;
|
||||
|
||||
public class PopupVideoPlayer extends Service implements StateInterface {
|
||||
private static final String TAG = ".PopupVideoPlayer";
|
||||
private static final boolean DEBUG = false;
|
||||
private static int CURRENT_STATE = -1;
|
||||
|
||||
private static final int NOTIFICATION_ID = 40028922;
|
||||
protected static final int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
|
||||
protected static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
private InternalListener internalListener;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams windowLayoutParams;
|
||||
private GestureDetector gestureDetector;
|
||||
private ValueAnimator controlViewAnimator;
|
||||
private PopupViewHolder viewHolder;
|
||||
private EMVideoView emVideoView;
|
||||
|
||||
private float screenWidth, screenHeight;
|
||||
private float popupWidth, popupHeight;
|
||||
private float currentPopupHeight = 200;
|
||||
//private float minimumHeight = 100; // TODO: Use it when implementing the resize of the popup
|
||||
|
||||
public static final String VIDEO_URL = "video_url";
|
||||
public static final String STREAM_URL = "stream_url";
|
||||
public static final String VIDEO_TITLE = "video_title";
|
||||
public static final String CHANNEL_NAME = "channel_name";
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
|
||||
private Uri streamUri;
|
||||
private String videoUrl = "";
|
||||
private String videoTitle = "";
|
||||
private volatile String channelName = "";
|
||||
|
||||
private ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
private DisplayImageOptions displayImageOptions =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
private volatile Bitmap videoThumbnail;
|
||||
|
||||
private Repeater progressPollRepeater = new Repeater();
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE));
|
||||
internalListener = new InternalListener();
|
||||
viewHolder = new PopupViewHolder(null);
|
||||
progressPollRepeater.setRepeatListener(internalListener);
|
||||
progressPollRepeater.setRepeaterDelay(500);
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this);
|
||||
initReceiver();
|
||||
}
|
||||
|
||||
private void initReceiver() {
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onReceive() called with: context = [" + context + "], intent = [" + intent + "]");
|
||||
switch (intent.getAction()) {
|
||||
case InternalListener.ACTION_CLOSE:
|
||||
internalListener.onVideoClose();
|
||||
break;
|
||||
case InternalListener.ACTION_PLAY_PAUSE:
|
||||
internalListener.onVideoPlayPause();
|
||||
break;
|
||||
case InternalListener.ACTION_OPEN_DETAIL:
|
||||
internalListener.onOpenDetail(PopupVideoPlayer.this, videoUrl);
|
||||
break;
|
||||
case InternalListener.ACTION_UPDATE_THUMB:
|
||||
internalListener.onUpdateThumbnail(intent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(InternalListener.ACTION_CLOSE);
|
||||
intentFilter.addAction(InternalListener.ACTION_PLAY_PAUSE);
|
||||
intentFilter.addAction(InternalListener.ACTION_OPEN_DETAIL);
|
||||
intentFilter.addAction(InternalListener.ACTION_UPDATE_THUMB);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
@SuppressLint({"RtlHardcoded"})
|
||||
private void initPopup() {
|
||||
if (DEBUG) Log.d(TAG, "initPopup() called");
|
||||
View rootView = View.inflate(this, R.layout.player_popup, null);
|
||||
viewHolder = new PopupViewHolder(rootView);
|
||||
viewHolder.getPlaybackSeekBar().setOnSeekBarChangeListener(internalListener);
|
||||
emVideoView = viewHolder.getVideoView();
|
||||
emVideoView.setOnPreparedListener(internalListener);
|
||||
emVideoView.setOnCompletionListener(internalListener);
|
||||
emVideoView.setOnErrorListener(internalListener);
|
||||
emVideoView.setOnSeekCompletionListener(internalListener);
|
||||
|
||||
windowLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) getMinimumVideoWidth(currentPopupHeight), (int) currentPopupHeight,
|
||||
WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
gestureDetector = new GestureDetector(this, listener);
|
||||
gestureDetector.setIsLongpressEnabled(false);
|
||||
rootView.setOnTouchListener(listener);
|
||||
updateScreenSize();
|
||||
|
||||
windowManager.addView(rootView, windowLayoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (emVideoView == null) initPopup();
|
||||
|
||||
if (intent.getStringExtra(NavStack.URL) != null) {
|
||||
Thread fetcher = new Thread(new FetcherRunnable(intent));
|
||||
fetcher.start();
|
||||
} else {
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
streamUri = Uri.parse(intent.getStringExtra(STREAM_URL));
|
||||
videoUrl = intent.getStringExtra(VIDEO_URL);
|
||||
videoTitle = intent.getStringExtra(VIDEO_TITLE);
|
||||
channelName = intent.getStringExtra(CHANNEL_NAME);
|
||||
try {
|
||||
videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
playVideo(streamUri);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private float getMinimumVideoWidth(float height) {
|
||||
float width = height * (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
|
||||
if (DEBUG) Log.d(TAG, "getMinimumVideoWidth() called with: height = [" + height + "], returned: " + width);
|
||||
return width;
|
||||
}
|
||||
|
||||
private void updateScreenSize() {
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
windowManager.getDefaultDisplay().getMetrics(metrics);
|
||||
|
||||
screenWidth = metrics.widthPixels;
|
||||
screenHeight = metrics.heightPixels;
|
||||
if (DEBUG) Log.d(TAG, "updateScreenSize() called > screenWidth = " + screenWidth + ", screenHeight = " + screenHeight);
|
||||
}
|
||||
|
||||
private void seekBy(int milliSeconds) {
|
||||
if (emVideoView == null) return;
|
||||
int progress = emVideoView.getCurrentPosition() + milliSeconds;
|
||||
emVideoView.seekTo(progress);
|
||||
}
|
||||
|
||||
private void playVideo(Uri videoURI) {
|
||||
if (DEBUG) Log.d(TAG, "playVideo() called with: streamUri = [" + streamUri + "]");
|
||||
|
||||
changeState(STATE_LOADING);
|
||||
|
||||
windowLayoutParams.width = (int) getMinimumVideoWidth(currentPopupHeight);
|
||||
windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams);
|
||||
|
||||
if (videoURI == null || emVideoView == null || viewHolder.getRootView() == null) {
|
||||
Toast.makeText(this, "Failed to play this video", Toast.LENGTH_SHORT).show();
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
if (emVideoView.isPlaying()) emVideoView.stopPlayback();
|
||||
emVideoView.setVideoURI(videoURI);
|
||||
|
||||
notBuilder = createNotification();
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
notificationManager.notify(NOTIFICATION_ID, this.notBuilder.build());
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_popup_notification);
|
||||
if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
else notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationStop,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notRemoteView.setTextViewText(R.id.notificationSongName, videoTitle);
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, channelName);
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationContent,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(InternalListener.ACTION_OPEN_DETAIL), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
|
||||
return new NotificationCompat.Builder(this)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_play_arrow_white_48dp)
|
||||
.setContent(notRemoteView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the notification, and the play/pause button in it.
|
||||
* Used for changes on the remoteView
|
||||
*
|
||||
* @param drawableId if != -1, sets the drawable with that id on the play/pause button
|
||||
*/
|
||||
private void updateNotification(int drawableId) {
|
||||
if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null || notRemoteView == null) return;
|
||||
if (drawableId != -1) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a animation, and depending on goneOnEnd, will stay on the screen or be gone
|
||||
*
|
||||
* @param drawableId the drawable that will be used to animate, pass -1 to clear any animation that is visible
|
||||
* @param goneOnEnd will set the animation view to GONE on the end of the animation
|
||||
*/
|
||||
private void showAndAnimateControl(final int drawableId, final boolean goneOnEnd) {
|
||||
if (DEBUG) Log.d(TAG, "showAndAnimateControl() called with: drawableId = [" + drawableId + "], goneOnEnd = [" + goneOnEnd + "]");
|
||||
if (controlViewAnimator != null && controlViewAnimator.isRunning()) {
|
||||
if (DEBUG) Log.d(TAG, "showAndAnimateControl: controlViewAnimator.isRunning");
|
||||
controlViewAnimator.end();
|
||||
}
|
||||
|
||||
if (drawableId == -1) {
|
||||
if (viewHolder.getControlAnimationView().getVisibility() == View.VISIBLE) {
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(),
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
|
||||
).setDuration(300);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
viewHolder.getControlAnimationView().setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float scaleFrom = goneOnEnd ? 1f : 1f, scaleTo = goneOnEnd ? 1.8f : 1.4f;
|
||||
float alphaFrom = goneOnEnd ? 1f : 0f, alphaTo = goneOnEnd ? 0f : 1f;
|
||||
|
||||
|
||||
controlViewAnimator = ObjectAnimator.ofPropertyValuesHolder(viewHolder.getControlAnimationView(),
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, alphaFrom, alphaTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, scaleFrom, scaleTo),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, scaleFrom, scaleTo)
|
||||
);
|
||||
controlViewAnimator.setDuration(goneOnEnd ? 1000 : 500);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (goneOnEnd) viewHolder.getControlAnimationView().setVisibility(View.GONE);
|
||||
else viewHolder.getControlAnimationView().setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
viewHolder.getControlAnimationView().setVisibility(View.VISIBLE);
|
||||
viewHolder.getControlAnimationView().setImageDrawable(ContextCompat.getDrawable(PopupVideoPlayer.this, drawableId));
|
||||
controlViewAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the view
|
||||
*
|
||||
* @param enterOrExit true to enter, false to exit
|
||||
* @param duration how long the animation will take, in milliseconds
|
||||
* @param delay how long the animation will wait to start, in milliseconds
|
||||
*/
|
||||
private void animateView(final View view, final boolean enterOrExit, long duration, long delay) {
|
||||
if (DEBUG) Log.d(TAG, "animateView() called with: view = [" + view + "], enterOrExit = [" + enterOrExit + "], duration = [" + duration + "], delay = [" + delay + "]");
|
||||
if (view.getVisibility() == View.VISIBLE && enterOrExit) {
|
||||
if (DEBUG) Log.d(TAG, "animateLoadingPanel() > view.getVisibility() == View.VISIBLE && enterOrExit");
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
|
||||
view.animate().setListener(null).cancel();
|
||||
view.setVisibility(View.VISIBLE);
|
||||
|
||||
if (view == viewHolder.getControlsRoot()) {
|
||||
if (enterOrExit) {
|
||||
view.setAlpha(0f);
|
||||
view.animate().alpha(1f).setDuration(duration).setStartDelay(delay).setListener(null).start();
|
||||
} else {
|
||||
view.setAlpha(1f);
|
||||
view.animate().alpha(0f)
|
||||
.setDuration(duration).setStartDelay(delay)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (enterOrExit) {
|
||||
view.setAlpha(0f);
|
||||
view.setScaleX(.8f);
|
||||
view.setScaleY(.8f);
|
||||
view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(duration).setStartDelay(delay).setListener(null).start();
|
||||
} else {
|
||||
view.setAlpha(1f);
|
||||
view.setScaleX(1f);
|
||||
view.setScaleY(1f);
|
||||
view.animate().alpha(0f).scaleX(.8f).scaleY(.8f).setDuration(duration).setStartDelay(delay)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
updateScreenSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
stopForeground(true);
|
||||
if (emVideoView != null) emVideoView.stopPlayback();
|
||||
if (imageLoader != null) imageLoader.clearMemoryCache();
|
||||
if (viewHolder.getRootView() != null) windowManager.removeView(viewHolder.getRootView());
|
||||
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
|
||||
if (progressPollRepeater != null) {
|
||||
progressPollRepeater.stop();
|
||||
progressPollRepeater.setRepeatListener(null);
|
||||
}
|
||||
if (broadcastReceiver != null) unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void changeState(int state) {
|
||||
if (DEBUG) Log.d(TAG, "changeState() called with: state = [" + state + "]");
|
||||
CURRENT_STATE = state;
|
||||
switch (state) {
|
||||
case STATE_LOADING:
|
||||
onLoading();
|
||||
break;
|
||||
case STATE_PLAYING:
|
||||
onPlaying();
|
||||
break;
|
||||
case STATE_PAUSED:
|
||||
onPaused();
|
||||
break;
|
||||
case STATE_PAUSED_SEEK:
|
||||
onPausedSeek();
|
||||
break;
|
||||
case STATE_COMPLETED:
|
||||
onCompleted();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoading() {
|
||||
if (DEBUG) Log.d(TAG, "onLoading() called");
|
||||
updateNotification(R.drawable.ic_play_arrow_white_48dp);
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
viewHolder.getPlaybackSeekBar().setEnabled(true);
|
||||
viewHolder.getPlaybackSeekBar().setProgress(0);
|
||||
viewHolder.getLoadingPanel().setBackgroundColor(Color.BLACK);
|
||||
animateView(viewHolder.getLoadingPanel(), true, 500, 0);
|
||||
viewHolder.getEndScreen().setVisibility(View.GONE);
|
||||
viewHolder.getControlsRoot().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaying() called");
|
||||
updateNotification(R.drawable.ic_pause_white_24dp);
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
viewHolder.getLoadingPanel().setVisibility(View.GONE);
|
||||
animateView(viewHolder.getControlsRoot(), false, 500, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
if (DEBUG) Log.d(TAG, "onPaused() called");
|
||||
updateNotification(R.drawable.ic_play_arrow_white_48dp);
|
||||
|
||||
showAndAnimateControl(R.drawable.ic_play_arrow_white_48dp, false);
|
||||
animateView(viewHolder.getControlsRoot(), true, 500, 100);
|
||||
viewHolder.getLoadingPanel().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
if (DEBUG) Log.d(TAG, "onPausedSeek() called");
|
||||
updateNotification(R.drawable.ic_play_arrow_white_48dp);
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
viewHolder.getLoadingPanel().setBackgroundColor(Color.TRANSPARENT);
|
||||
animateView(viewHolder.getLoadingPanel(), true, 300, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
if (DEBUG) Log.d(TAG, "onCompleted() called");
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
animateView(viewHolder.getControlsRoot(), true, 500, 0);
|
||||
animateView(viewHolder.getEndScreen(), true, 200, 0);
|
||||
viewHolder.getLoadingPanel().setVisibility(View.GONE);
|
||||
viewHolder.getPlaybackSeekBar().setEnabled(false);
|
||||
viewHolder.getPlaybackCurrentTime().setText(viewHolder.getPlaybackEndTime().getText());
|
||||
if (videoThumbnail != null) viewHolder.getEndScreen().setImageBitmap(videoThumbnail);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class joins all the necessary listeners
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess"})
|
||||
public class InternalListener implements SeekBar.OnSeekBarChangeListener, OnPreparedListener, OnSeekCompletionListener, OnCompletionListener, OnErrorListener, Repeater.RepeatListener {
|
||||
public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE";
|
||||
public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE";
|
||||
public static final String ACTION_OPEN_DETAIL = "org.schabi.newpipe.player.PopupVideoPlayer.OPEN_DETAIL";
|
||||
public static final String ACTION_UPDATE_THUMB = "org.schabi.newpipe.player.PopupVideoPlayer.UPDATE_THUMBNAIL";
|
||||
|
||||
@Override
|
||||
public void onPrepared() {
|
||||
if (DEBUG) Log.d(TAG, "onPrepared() called");
|
||||
viewHolder.getPlaybackSeekBar().setMax(emVideoView.getDuration());
|
||||
viewHolder.getPlaybackEndTime().setText(TimeFormatUtil.formatMs(emVideoView.getDuration()));
|
||||
|
||||
changeState(STATE_PLAYING);
|
||||
progressPollRepeater.start();
|
||||
emVideoView.start();
|
||||
|
||||
}
|
||||
|
||||
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||
if (viewHolder.isControlsVisible() && CURRENT_STATE != STATE_PAUSED_SEEK) {
|
||||
viewHolder.getPlaybackSeekBar().setProgress(currentProgress);
|
||||
viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(currentProgress));
|
||||
viewHolder.getPlaybackSeekBar().setSecondaryProgress((int) (viewHolder.getPlaybackSeekBar().getMax() * ((float) bufferPercent / 100)));
|
||||
}
|
||||
if (DEBUG && bufferPercent % 10 == 0) { //Limit log
|
||||
Log.d(TAG, "updateProgress() called with: isVisible = " + viewHolder.isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public void onOpenDetail(Context context, String videoUrl) {
|
||||
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
|
||||
Intent i = new Intent(context, VideoItemDetailActivity.class);
|
||||
i.putExtra(NavStack.SERVICE_ID, 0);
|
||||
i.putExtra(NavStack.URL, videoUrl);
|
||||
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(i);
|
||||
//NavStack.getInstance().openDetailActivity(context, videoUrl, 0);
|
||||
}
|
||||
|
||||
public void onUpdateThumbnail(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onUpdateThumbnail() called");
|
||||
if (!intent.getStringExtra(VIDEO_URL).equals(videoUrl)) return;
|
||||
videoThumbnail = ActivityCommunicator.getCommunicator().backgroundPlayerThumbnail;
|
||||
if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
public void onVideoClose() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoClose() called");
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
public void onVideoPlayPause() {
|
||||
if (DEBUG) Log.d(TAG, "onVideoPlayPause() called");
|
||||
if (CURRENT_STATE == STATE_COMPLETED) {
|
||||
changeState(STATE_LOADING);
|
||||
emVideoView.restart();
|
||||
return;
|
||||
}
|
||||
if (emVideoView.isPlaying()) {
|
||||
emVideoView.pause();
|
||||
progressPollRepeater.stop();
|
||||
internalListener.onRepeat();
|
||||
changeState(STATE_PAUSED);
|
||||
} else {
|
||||
emVideoView.start();
|
||||
progressPollRepeater.start();
|
||||
changeState(STATE_PLAYING);
|
||||
}
|
||||
}
|
||||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT);
|
||||
internalListener.onRepeat();
|
||||
changeState(STATE_PAUSED_SEEK);
|
||||
|
||||
showAndAnimateControl(R.drawable.ic_action_av_fast_rewind, true);
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT);
|
||||
internalListener.onRepeat();
|
||||
changeState(STATE_PAUSED_SEEK);
|
||||
|
||||
showAndAnimateControl(R.drawable.ic_action_av_fast_forward, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekComplete() {
|
||||
if (DEBUG) Log.d(TAG, "onSeekComplete() called");
|
||||
|
||||
if (!emVideoView.isPlaying()) emVideoView.start();
|
||||
changeState(STATE_PLAYING);
|
||||
/*if (emVideoView.isPlaying()) changeState(STATE_PLAYING);
|
||||
else changeState(STATE_PAUSED);*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion() {
|
||||
if (DEBUG) Log.d(TAG, "onCompletion() called");
|
||||
changeState(STATE_COMPLETED);
|
||||
progressPollRepeater.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError() {
|
||||
if (DEBUG) Log.d(TAG, "onError() called");
|
||||
stopSelf();
|
||||
return true;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// SeekBar Listener
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + progress + "], fromUser = [" + fromUser + "]");
|
||||
viewHolder.getPlaybackCurrentTime().setText(TimeFormatUtil.formatMs(progress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (DEBUG) Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
|
||||
|
||||
changeState(STATE_PAUSED_SEEK);
|
||||
if (emVideoView.isPlaying()) emVideoView.pause();
|
||||
animateView(viewHolder.getControlsRoot(), true, 300, 0);
|
||||
viewHolder.getControlsRoot().setAlpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (DEBUG) Log.d(TAG, "onProgressChanged() called with: seekBar = [" + seekBar + "], progress = [" + seekBar.getProgress() + "]");
|
||||
emVideoView.seekTo(seekBar.getProgress());
|
||||
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Repeater Listener
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Don't mistake this with anything related to the player itself, it's the {@link Repeater.RepeatListener#onRepeat}
|
||||
* It's used for pool the progress of the video
|
||||
*/
|
||||
@Override
|
||||
public void onRepeat() {
|
||||
onUpdateProgress(emVideoView.getCurrentPosition(), emVideoView.getDuration(), emVideoView.getBufferPercentage());
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private int initialPopupX, initialPopupY;
|
||||
private boolean isMoving;
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!emVideoView.isPlaying()) return false;
|
||||
if (e.getX() > popupWidth / 2) internalListener.onFastForward();
|
||||
else internalListener.onFastRewind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (emVideoView == null) return false;
|
||||
internalListener.onVideoPlayPause();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||
initialPopupX = windowLayoutParams.x;
|
||||
initialPopupY = windowLayoutParams.y;
|
||||
popupWidth = viewHolder.getRootView().getWidth();
|
||||
popupHeight = viewHolder.getRootView().getHeight();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPress(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onShowPress() called with: e = [" + e + "]");
|
||||
/*viewHolder.getControlsRoot().animate().setListener(null).cancel();
|
||||
viewHolder.getControlsRoot().setAlpha(1f);
|
||||
viewHolder.getControlsRoot().setVisibility(View.VISIBLE);*/
|
||||
animateView(viewHolder.getControlsRoot(), true, 200, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
isMoving = true;
|
||||
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
|
||||
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
|
||||
|
||||
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
|
||||
else if (posX < 0) posX = 0;
|
||||
|
||||
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
|
||||
else if (posY < 0) posY = 0;
|
||||
|
||||
windowLayoutParams.x = (int) posX;
|
||||
windowLayoutParams.y = (int) posY;
|
||||
|
||||
if (DEBUG) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]" +
|
||||
", posXy = [" + posX + ", " + posY + "]" +
|
||||
", popupWh rootView.get wh = [" + popupWidth + " x " + popupHeight + "]");
|
||||
windowManager.updateViewLayout(viewHolder.getRootView(), windowLayoutParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (viewHolder.isControlsVisible() && CURRENT_STATE == STATE_PLAYING) {
|
||||
animateView(viewHolder.getControlsRoot(), false, 300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
gestureDetector.onTouchEvent(event);
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher used if open by a link out of NewPipe
|
||||
*/
|
||||
private class FetcherRunnable implements Runnable {
|
||||
private final Intent intent;
|
||||
private final Handler mainHandler;
|
||||
private final boolean printStreams = true;
|
||||
|
||||
|
||||
FetcherRunnable(Intent intent) {
|
||||
this.intent = intent;
|
||||
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
StreamExtractor streamExtractor;
|
||||
try {
|
||||
StreamingService service = NewPipe.getService(0);
|
||||
if (service == null) return;
|
||||
streamExtractor = service.getExtractorInstance(intent.getStringExtra(NavStack.URL));
|
||||
StreamInfo info = StreamInfo.getVideoInfo(streamExtractor);
|
||||
String defaultResolution = sharedPreferences.getString(
|
||||
getResources().getString(R.string.default_resolution_key),
|
||||
getResources().getString(R.string.default_resolution_value));
|
||||
|
||||
String chosen = "", secondary = "", fallback = "";
|
||||
for (VideoStream item : info.video_streams) {
|
||||
if (DEBUG && printStreams) {
|
||||
Log.d(TAG, "StreamExtractor: current Item"
|
||||
+ ", item.resolution = " + item.resolution
|
||||
+ ", item.format = " + item.format
|
||||
+ ", item.url = " + item.url);
|
||||
}
|
||||
if (defaultResolution.equals(item.resolution)) {
|
||||
if (item.format == MediaFormat.MPEG_4.id) {
|
||||
chosen = item.url;
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "StreamExtractor: CHOSEN item"
|
||||
+ ", item.resolution = " + item.resolution
|
||||
+ ", item.format = " + item.format
|
||||
+ ", item.url = " + item.url);
|
||||
} else if (item.format == 2) secondary = item.url;
|
||||
else fallback = item.url;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!chosen.trim().isEmpty()) streamUri = Uri.parse(chosen);
|
||||
else if (!secondary.trim().isEmpty()) streamUri = Uri.parse(secondary);
|
||||
else if (!fallback.trim().isEmpty()) streamUri = Uri.parse(fallback);
|
||||
else streamUri = Uri.parse(info.video_streams.get(0).url);
|
||||
if (DEBUG && printStreams) Log.d(TAG, "StreamExtractor: chosen = " + chosen
|
||||
+ "\n, secondary = " + secondary
|
||||
+ "\n, fallback = " + fallback
|
||||
+ "\n, info.video_streams.get(0).url = " + info.video_streams.get(0).url);
|
||||
|
||||
videoUrl = info.webpage_url;
|
||||
videoTitle = info.title;
|
||||
channelName = info.uploader;
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playVideo(streamUri);
|
||||
}
|
||||
});
|
||||
imageLoader.loadImage(info.thumbnail_url, displayImageOptions, new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) {
|
||||
mainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
videoThumbnail = loadedImage;
|
||||
if (videoThumbnail != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
updateNotification(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,268 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.DefaultDashTrackSelector;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||
import com.google.android.exoplayer.dash.mpd.Period;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
|
||||
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for DASH.
|
||||
*/
|
||||
public class DashRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final String TAG = "DashRendererBuilder";
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private static final int SECURITY_LEVEL_UNKNOWN = -1;
|
||||
private static final int SECURITY_LEVEL_1 = 1;
|
||||
private static final int SECURITY_LEVEL_3 = 3;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public DashRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||
private final UriDataSource manifestDataSource;
|
||||
|
||||
private boolean canceled;
|
||||
private MediaPresentationDescription manifest;
|
||||
private long elapsedRealtimeOffset;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||
manifestDataSource = new DefaultUriDataSource(context, userAgent);
|
||||
manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(MediaPresentationDescription manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manifest = manifest;
|
||||
if (manifest.dynamic && manifest.utcTiming != null) {
|
||||
UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
|
||||
manifestFetcher.getManifestLoadCompleteTimestamp(), this);
|
||||
} else {
|
||||
buildRenderers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||
// Be optimistic and continue in the hope that the device clock is correct.
|
||||
buildRenderers();
|
||||
}
|
||||
|
||||
private void buildRenderers() {
|
||||
Period period = manifest.getPeriod(0);
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
boolean hasContentProtection = false;
|
||||
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) {
|
||||
hasContentProtection |= adaptationSet.hasContentProtection();
|
||||
}
|
||||
}
|
||||
|
||||
// Check drm support if necessary.
|
||||
boolean filterHdContent = false;
|
||||
StreamingDrmSessionManager drmSessionManager = null;
|
||||
if (hasContentProtection) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher,
|
||||
DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
|
||||
String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
|
||||
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
|
||||
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.util.VerboseLogUtil;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Logs player events using {@link Log}.
|
||||
*/
|
||||
public class EventLogger implements NPExoPlayer.Listener, NPExoPlayer.InfoListener,
|
||||
NPExoPlayer.InternalErrorListener {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
private static final NumberFormat TIME_FORMAT;
|
||||
static {
|
||||
TIME_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
TIME_FORMAT.setMinimumFractionDigits(2);
|
||||
TIME_FORMAT.setMaximumFractionDigits(2);
|
||||
}
|
||||
|
||||
private long sessionStartTimeMs;
|
||||
private long[] loadStartTimeMs;
|
||||
private long[] availableRangeValuesUs;
|
||||
|
||||
public EventLogger() {
|
||||
loadStartTimeMs = new long[NPExoPlayer.RENDERER_COUNT];
|
||||
}
|
||||
|
||||
public void startSession() {
|
||||
sessionStartTimeMs = SystemClock.elapsedRealtime();
|
||||
Log.d(TAG, "start [0]");
|
||||
}
|
||||
|
||||
public void endSession() {
|
||||
Log.d(TAG, "end [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.Listener
|
||||
|
||||
@Override
|
||||
public void onStateChanged(boolean playWhenReady, int state) {
|
||||
Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", "
|
||||
+ getStateString(state) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
Log.d(TAG, "videoSizeChanged [" + width + ", " + height + ", " + unappliedRotationDegrees
|
||||
+ ", " + pixelWidthHeightRatio + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InfoListener
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
Log.d(TAG, "bandwidth [" + getSessionTimeString() + ", " + bytes + ", "
|
||||
+ getTimeString(elapsedMs) + ", " + bitrateEstimate + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
loadStartTimeMs[sourceId] = SystemClock.elapsedRealtime();
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
Log.v(TAG, "loadStart [" + getSessionTimeString() + ", " + sourceId + ", " + type
|
||||
+ ", " + mediaStartTimeMs + ", " + mediaEndTimeMs + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (VerboseLogUtil.isTagEnabled(TAG)) {
|
||||
long downloadTime = SystemClock.elapsedRealtime() - loadStartTimeMs[sourceId];
|
||||
Log.v(TAG, "loadEnd [" + getSessionTimeString() + ", " + sourceId + ", " + downloadTime
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "videoFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs) {
|
||||
Log.d(TAG, "audioFormat [" + getSessionTimeString() + ", " + format.id + ", "
|
||||
+ Integer.toString(trigger) + "]");
|
||||
}
|
||||
|
||||
// NPExoPlayer.InternalErrorListener
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
printInternalError("loadError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRendererInitializationError(Exception e) {
|
||||
printInternalError("rendererInitError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
printInternalError("drmSessionManagerError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
printInternalError("decoderInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
printInternalError("audioTrackInitializationError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
printInternalError("audioTrackWriteError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", "
|
||||
+ elapsedSinceLastFeedMs + "]", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
printInternalError("cryptoError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
|
||||
Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0]
|
||||
+ ", " + availableRangeValuesUs[1] + "]");
|
||||
}
|
||||
|
||||
private void printInternalError(String type, Exception e) {
|
||||
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
|
||||
}
|
||||
|
||||
private String getStateString(int state) {
|
||||
switch (state) {
|
||||
case ExoPlayer.STATE_BUFFERING:
|
||||
return "B";
|
||||
case ExoPlayer.STATE_ENDED:
|
||||
return "E";
|
||||
case ExoPlayer.STATE_IDLE:
|
||||
return "I";
|
||||
case ExoPlayer.STATE_PREPARING:
|
||||
return "P";
|
||||
case ExoPlayer.STATE_READY:
|
||||
return "R";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private String getSessionTimeString() {
|
||||
return getTimeString(SystemClock.elapsedRealtime() - sessionStartTimeMs);
|
||||
}
|
||||
|
||||
private String getTimeString(long timeMs) {
|
||||
return TIME_FORMAT.format((timeMs) / 1000f);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.extractor.Extractor;
|
||||
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.Allocator;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for streams that can be read using an {@link Extractor}.
|
||||
*/
|
||||
public class ExtractorRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int BUFFER_SEGMENT_COUNT = 256;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final Uri uri;
|
||||
|
||||
public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
|
||||
|
||||
// Build the video and audio renderers.
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(),
|
||||
null);
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator,
|
||||
BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
player.getMainHandler(), player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(sampleSource, player,
|
||||
player.getMainHandler().getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.hls.DefaultHlsTrackSelector;
|
||||
import com.google.android.exoplayer.hls.HlsChunkSource;
|
||||
import com.google.android.exoplayer.hls.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylist;
|
||||
import com.google.android.exoplayer.hls.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.hls.PtsTimestampAdjusterProvider;
|
||||
import com.google.android.exoplayer.metadata.Id3Parser;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.text.eia608.Eia608TrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher.ManifestCallback;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for HLS.
|
||||
*/
|
||||
public class HlsRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int MAIN_BUFFER_SEGMENTS = 256;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public HlsRendererBuilder(Context context, String userAgent, String url) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder implements ManifestCallback<HlsPlaylist> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<HlsPlaylist> playlistFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = url;
|
||||
this.player = player;
|
||||
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||
playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException e) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(HlsPlaylist manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||
|
||||
// Build the video/audio/metadata renderers.
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
||||
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
||||
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
||||
5000, mainHandler, player, 50);
|
||||
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
||||
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
|
||||
|
||||
// Build the text renderer, preferring Webvtt where available.
|
||||
boolean preferWebvtt = false;
|
||||
if (manifest instanceof HlsMasterPlaylist) {
|
||||
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
|
||||
}
|
||||
TrackRenderer textRenderer;
|
||||
if (preferWebvtt) {
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
|
||||
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
|
||||
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
|
||||
} else {
|
||||
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
|
||||
}
|
||||
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,599 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
import com.google.android.exoplayer.CodecCounters;
|
||||
import com.google.android.exoplayer.DummyTrackRenderer;
|
||||
import com.google.android.exoplayer.ExoPlaybackException;
|
||||
import com.google.android.exoplayer.ExoPlayer;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioTrack;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.Format;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.hls.HlsSampleSource;
|
||||
import com.google.android.exoplayer.metadata.MetadataTrackRenderer.MetadataRenderer;
|
||||
import com.google.android.exoplayer.text.Cue;
|
||||
import com.google.android.exoplayer.text.TextRenderer;
|
||||
import com.google.android.exoplayer.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.util.DebugTextViewHelper;
|
||||
import com.google.android.exoplayer.util.PlayerControl;
|
||||
|
||||
import android.media.MediaCodec.CryptoException;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* A wrapper around {@link ExoPlayer} that provides a higher level interface. It can be prepared
|
||||
* with one of a number of {@link RendererBuilder} classes to suit different use cases (e.g. DASH,
|
||||
* SmoothStreaming and so on).
|
||||
*/
|
||||
public class NPExoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
|
||||
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
|
||||
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
|
||||
StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer,
|
||||
MetadataRenderer<Map<String, Object>>, DebugTextViewHelper.Provider {
|
||||
|
||||
/**
|
||||
* Builds renderers for the player.
|
||||
*/
|
||||
public interface RendererBuilder {
|
||||
/**
|
||||
* Builds renderers for playback.
|
||||
*
|
||||
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
|
||||
* should be invoked once the renderers have been built. If building fails,
|
||||
* {@link NPExoPlayer#onRenderersError} should be invoked.
|
||||
*/
|
||||
void buildRenderers(NPExoPlayer player);
|
||||
/**
|
||||
* Cancels the current build operation, if there is one. Else does nothing.
|
||||
* <p>
|
||||
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
|
||||
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
|
||||
*/
|
||||
void cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for core events.
|
||||
*/
|
||||
public interface Listener {
|
||||
void onStateChanged(boolean playWhenReady, int playbackState);
|
||||
void onError(Exception e);
|
||||
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for internal errors.
|
||||
* <p>
|
||||
* These errors are not visible to the user, and hence this listener is provided for
|
||||
* informational purposes only. Note however that an internal error may cause a fatal
|
||||
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
|
||||
* will be invoked.
|
||||
*/
|
||||
public interface InternalErrorListener {
|
||||
void onRendererInitializationError(Exception e);
|
||||
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
|
||||
void onAudioTrackWriteError(AudioTrack.WriteException e);
|
||||
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
||||
void onDecoderInitializationError(DecoderInitializationException e);
|
||||
void onCryptoError(CryptoException e);
|
||||
void onLoadError(int sourceId, IOException e);
|
||||
void onDrmSessionManagerError(Exception e);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for debugging information.
|
||||
*/
|
||||
public interface InfoListener {
|
||||
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||
void onDroppedFrames(int count, long elapsed);
|
||||
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
|
||||
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs);
|
||||
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
|
||||
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs);
|
||||
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving notifications of timed text.
|
||||
*/
|
||||
public interface CaptionListener {
|
||||
void onCues(List<Cue> cues);
|
||||
}
|
||||
|
||||
/**
|
||||
* A listener for receiving ID3 metadata parsed from the media stream.
|
||||
*/
|
||||
public interface Id3MetadataListener {
|
||||
void onId3Metadata(Map<String, Object> metadata);
|
||||
}
|
||||
|
||||
// Constants pulled into this class for convenience.
|
||||
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
||||
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
|
||||
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
|
||||
public static final int STATE_READY = ExoPlayer.STATE_READY;
|
||||
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
|
||||
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
|
||||
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
|
||||
|
||||
public static final int RENDERER_COUNT = 4;
|
||||
public static final int TYPE_VIDEO = 0;
|
||||
public static final int TYPE_AUDIO = 1;
|
||||
public static final int TYPE_TEXT = 2;
|
||||
public static final int TYPE_METADATA = 3;
|
||||
|
||||
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
||||
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
|
||||
|
||||
private final RendererBuilder rendererBuilder;
|
||||
private final ExoPlayer player;
|
||||
private final PlayerControl playerControl;
|
||||
private final Handler mainHandler;
|
||||
private final CopyOnWriteArrayList<Listener> listeners;
|
||||
|
||||
private int rendererBuildingState;
|
||||
private int lastReportedPlaybackState;
|
||||
private boolean lastReportedPlayWhenReady;
|
||||
|
||||
private Surface surface;
|
||||
private TrackRenderer videoRenderer;
|
||||
private CodecCounters codecCounters;
|
||||
private Format videoFormat;
|
||||
private int videoTrackToRestore;
|
||||
|
||||
private BandwidthMeter bandwidthMeter;
|
||||
private boolean backgrounded;
|
||||
|
||||
private CaptionListener captionListener;
|
||||
private Id3MetadataListener id3MetadataListener;
|
||||
private InternalErrorListener internalErrorListener;
|
||||
private InfoListener infoListener;
|
||||
|
||||
public NPExoPlayer(RendererBuilder rendererBuilder) {
|
||||
this.rendererBuilder = rendererBuilder;
|
||||
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
|
||||
player.addListener(this);
|
||||
playerControl = new PlayerControl(player);
|
||||
mainHandler = new Handler();
|
||||
listeners = new CopyOnWriteArrayList<>();
|
||||
lastReportedPlaybackState = STATE_IDLE;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
// Disable text initially.
|
||||
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
|
||||
}
|
||||
|
||||
public PlayerControl getPlayerControl() {
|
||||
return playerControl;
|
||||
}
|
||||
|
||||
public void addListener(Listener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void setInternalErrorListener(InternalErrorListener listener) {
|
||||
internalErrorListener = listener;
|
||||
}
|
||||
|
||||
public void setInfoListener(InfoListener listener) {
|
||||
infoListener = listener;
|
||||
}
|
||||
|
||||
public void setCaptionListener(CaptionListener listener) {
|
||||
captionListener = listener;
|
||||
}
|
||||
|
||||
public void setMetadataListener(Id3MetadataListener listener) {
|
||||
id3MetadataListener = listener;
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
this.surface = surface;
|
||||
pushSurface(false);
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return surface;
|
||||
}
|
||||
|
||||
public void blockingClearSurface() {
|
||||
surface = null;
|
||||
pushSurface(true);
|
||||
}
|
||||
|
||||
public int getTrackCount(int type) {
|
||||
return player.getTrackCount(type);
|
||||
}
|
||||
|
||||
public MediaFormat getTrackFormat(int type, int index) {
|
||||
return player.getTrackFormat(type, index);
|
||||
}
|
||||
|
||||
public int getSelectedTrack(int type) {
|
||||
return player.getSelectedTrack(type);
|
||||
}
|
||||
|
||||
public void setSelectedTrack(int type, int index) {
|
||||
player.setSelectedTrack(type, index);
|
||||
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
|
||||
captionListener.onCues(Collections.<Cue>emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getBackgrounded() {
|
||||
return backgrounded;
|
||||
}
|
||||
|
||||
public void setBackgrounded(boolean backgrounded) {
|
||||
if (this.backgrounded == backgrounded) {
|
||||
return;
|
||||
}
|
||||
this.backgrounded = backgrounded;
|
||||
if (backgrounded) {
|
||||
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
|
||||
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
|
||||
blockingClearSurface();
|
||||
} else {
|
||||
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
|
||||
player.stop();
|
||||
}
|
||||
rendererBuilder.cancel();
|
||||
videoFormat = null;
|
||||
videoRenderer = null;
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
|
||||
maybeReportPlayerState();
|
||||
rendererBuilder.buildRenderers(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked with the results from a {@link RendererBuilder}.
|
||||
*
|
||||
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
|
||||
* element may be null if there do not exist tracks of the corresponding type.
|
||||
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
|
||||
*/
|
||||
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
|
||||
for (int i = 0; i < RENDERER_COUNT; i++) {
|
||||
if (renderers[i] == null) {
|
||||
// Convert a null renderer to a dummy renderer.
|
||||
renderers[i] = new DummyTrackRenderer();
|
||||
}
|
||||
}
|
||||
// Complete preparation.
|
||||
this.videoRenderer = renderers[TYPE_VIDEO];
|
||||
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
|
||||
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
|
||||
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
|
||||
this.bandwidthMeter = bandwidthMeter;
|
||||
pushSurface(false);
|
||||
player.prepare(renderers);
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked if a {@link RendererBuilder} encounters an error.
|
||||
*
|
||||
* @param e Describes the error.
|
||||
*/
|
||||
/* package */ void onRenderersError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onRendererInitializationError(e);
|
||||
}
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(e);
|
||||
}
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
public void setPlayWhenReady(boolean playWhenReady) {
|
||||
player.setPlayWhenReady(playWhenReady);
|
||||
}
|
||||
|
||||
public void seekTo(long positionMs) {
|
||||
player.seekTo(positionMs);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
rendererBuilder.cancel();
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
surface = null;
|
||||
player.release();
|
||||
}
|
||||
|
||||
public int getPlaybackState() {
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
int playerState = player.getPlaybackState();
|
||||
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
|
||||
// This is an edge case where the renderers are built, but are still being passed to the
|
||||
// player's playback thread.
|
||||
return STATE_PREPARING;
|
||||
}
|
||||
return playerState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Format getFormat() {
|
||||
return videoFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BandwidthMeter getBandwidthMeter() {
|
||||
return bandwidthMeter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodecCounters getCodecCounters() {
|
||||
return codecCounters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCurrentPosition() {
|
||||
return player.getCurrentPosition();
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return player.getDuration();
|
||||
}
|
||||
|
||||
public int getBufferedPercentage() {
|
||||
return player.getBufferedPercentage();
|
||||
}
|
||||
|
||||
public boolean getPlayWhenReady() {
|
||||
return player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
/* package */ Looper getPlaybackLooper() {
|
||||
return player.getPlaybackLooper();
|
||||
}
|
||||
|
||||
/* package */ Handler getMainHandler() {
|
||||
return mainHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int state) {
|
||||
maybeReportPlayerState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||
for (Listener listener : listeners) {
|
||||
listener.onError(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||
float pixelWidthHeightRatio) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsed) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDroppedFrames(count, elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
|
||||
long mediaTimeMs) {
|
||||
if (infoListener == null) {
|
||||
return;
|
||||
}
|
||||
if (sourceId == TYPE_VIDEO) {
|
||||
videoFormat = format;
|
||||
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
|
||||
} else if (sourceId == TYPE_AUDIO) {
|
||||
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDrmSessionManagerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onDecoderInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackInitializationError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackWriteError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCryptoError(CryptoException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onCryptoError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||
long initializationDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(int sourceId, IOException e) {
|
||||
if (internalErrorListener != null) {
|
||||
internalErrorListener.onLoadError(sourceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCues(List<Cue> cues) {
|
||||
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
|
||||
captionListener.onCues(cues);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadata(Map<String, Object> metadata) {
|
||||
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
|
||||
id3MetadataListener.onId3Metadata(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onAvailableRangeChanged(sourceId, availableRange);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyCommitted() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawnToSurface(Surface surface) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||
if (infoListener != null) {
|
||||
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
|
||||
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCanceled(int sourceId, long bytesLoaded) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private void maybeReportPlayerState() {
|
||||
boolean playWhenReady = player.getPlayWhenReady();
|
||||
int playbackState = getPlaybackState();
|
||||
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
|
||||
for (Listener listener : listeners) {
|
||||
listener.onStateChanged(playWhenReady, playbackState);
|
||||
}
|
||||
lastReportedPlayWhenReady = playWhenReady;
|
||||
lastReportedPlaybackState = playbackState;
|
||||
}
|
||||
}
|
||||
|
||||
private void pushSurface(boolean blockForSurfacePush) {
|
||||
if (videoRenderer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockForSurfacePush) {
|
||||
player.blockingSendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
} else {
|
||||
player.sendMessage(
|
||||
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.schabi.newpipe.player.exoplayer;
|
||||
|
||||
|
||||
import org.schabi.newpipe.player.exoplayer.NPExoPlayer.RendererBuilder;
|
||||
|
||||
import com.google.android.exoplayer.DefaultLoadControl;
|
||||
import com.google.android.exoplayer.LoadControl;
|
||||
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
|
||||
import com.google.android.exoplayer.MediaCodecSelector;
|
||||
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer.smoothstreaming.DefaultSmoothStreamingTrackSelector;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingChunkSource;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest;
|
||||
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifestParser;
|
||||
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||
import com.google.android.exoplayer.upstream.DataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Handler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link RendererBuilder} for SmoothStreaming.
|
||||
*/
|
||||
public class SmoothStreamingRendererBuilder implements RendererBuilder {
|
||||
|
||||
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final String url;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
|
||||
private AsyncRendererBuilder currentAsyncBuilder;
|
||||
|
||||
public SmoothStreamingRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.url = Util.toLowerInvariant(url).endsWith("/manifest") ? url : url + "/Manifest";
|
||||
this.drmCallback = drmCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildRenderers(NPExoPlayer player) {
|
||||
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||
currentAsyncBuilder.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (currentAsyncBuilder != null) {
|
||||
currentAsyncBuilder.cancel();
|
||||
currentAsyncBuilder = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsyncRendererBuilder
|
||||
implements ManifestFetcher.ManifestCallback<SmoothStreamingManifest> {
|
||||
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final MediaDrmCallback drmCallback;
|
||||
private final NPExoPlayer player;
|
||||
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
|
||||
|
||||
private boolean canceled;
|
||||
|
||||
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.drmCallback = drmCallback;
|
||||
this.player = player;
|
||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
|
||||
parser);
|
||||
}
|
||||
|
||||
public void init() {
|
||||
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
canceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifestError(IOException exception) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.onRenderersError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleManifest(SmoothStreamingManifest manifest) {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
||||
// Check drm support if necessary.
|
||||
DrmSessionManager drmSessionManager = null;
|
||||
if (manifest.protectionElement != null) {
|
||||
if (Util.SDK_INT < 18) {
|
||||
player.onRenderersError(
|
||||
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
|
||||
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
player.onRenderersError(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the video renderer.
|
||||
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
|
||||
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_VIDEO);
|
||||
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||
drmSessionManager, true, mainHandler, player, 50);
|
||||
|
||||
// Build the audio renderer.
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
|
||||
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_AUDIO);
|
||||
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||
|
||||
// Build the text renderer.
|
||||
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||
DefaultSmoothStreamingTrackSelector.newTextInstance(),
|
||||
textDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||
NPExoPlayer.TYPE_TEXT);
|
||||
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||
mainHandler.getLooper());
|
||||
|
||||
// Invoke the callback.
|
||||
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||
player.onRenderers(renderers, bandwidthMeter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package org.schabi.newpipe.player.popup;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.devbrackets.android.exomedia.ui.widget.EMVideoView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class PopupViewHolder {
|
||||
private View rootView;
|
||||
private EMVideoView videoView;
|
||||
private View loadingPanel;
|
||||
private ImageView endScreen;
|
||||
private ImageView controlAnimationView;
|
||||
private LinearLayout controlsRoot;
|
||||
private SeekBar playbackSeekBar;
|
||||
private TextView playbackCurrentTime;
|
||||
private TextView playbackEndTime;
|
||||
|
||||
public PopupViewHolder(View rootView) {
|
||||
if (rootView == null) return;
|
||||
this.rootView = rootView;
|
||||
this.videoView = (EMVideoView) rootView.findViewById(R.id.popupVideoView);
|
||||
this.loadingPanel = rootView.findViewById(R.id.loadingPanel);
|
||||
this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen);
|
||||
this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView);
|
||||
this.controlsRoot = (LinearLayout) rootView.findViewById(R.id.playbackControlRoot);
|
||||
this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime);
|
||||
doModifications();
|
||||
}
|
||||
|
||||
private void doModifications() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
public boolean isControlsVisible() {
|
||||
return controlsRoot != null && controlsRoot.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
public boolean isVisible(View view) {
|
||||
return view != null && view.getVisibility() == View.VISIBLE;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// GETTERS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public View getRootView() {
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public EMVideoView getVideoView() {
|
||||
return videoView;
|
||||
}
|
||||
|
||||
public View getLoadingPanel() {
|
||||
return loadingPanel;
|
||||
}
|
||||
|
||||
public ImageView getEndScreen() {
|
||||
return endScreen;
|
||||
}
|
||||
|
||||
public ImageView getControlAnimationView() {
|
||||
return controlAnimationView;
|
||||
}
|
||||
|
||||
public LinearLayout getControlsRoot() {
|
||||
return controlsRoot;
|
||||
}
|
||||
|
||||
public SeekBar getPlaybackSeekBar() {
|
||||
return playbackSeekBar;
|
||||
}
|
||||
|
||||
public TextView getPlaybackCurrentTime() {
|
||||
return playbackCurrentTime;
|
||||
}
|
||||
|
||||
public TextView getPlaybackEndTime() {
|
||||
return playbackEndTime;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.schabi.newpipe.player.popup;
|
||||
|
||||
public interface StateInterface {
|
||||
int STATE_LOADING = 123;
|
||||
int STATE_PLAYING = 125;
|
||||
int STATE_PAUSED = 126;
|
||||
int STATE_PAUSED_SEEK = 127;
|
||||
int STATE_COMPLETED = 128;
|
||||
|
||||
void changeState(int state);
|
||||
|
||||
void onLoading();
|
||||
void onPlaying();
|
||||
void onPaused();
|
||||
void onPausedSeek();
|
||||
void onCompleted();
|
||||
}
|
|
@ -73,10 +73,6 @@ public class NavStack {
|
|||
return instance;
|
||||
}
|
||||
|
||||
private void addEntry(String url, Class ac, int serviceId) {
|
||||
stack.push(new NavEntry(url, serviceId));
|
||||
}
|
||||
|
||||
public void navBack(Activity activity) throws Exception {
|
||||
if(stack.size() == 0) { // if stack is already empty here, activity was probably called
|
||||
// from another app
|
||||
|
@ -120,7 +116,10 @@ public class NavStack {
|
|||
}
|
||||
|
||||
private void openActivity(Context context, String url, int serviceId, Class acitivtyClass) {
|
||||
//if last element has the same url do not push to stack again
|
||||
if(stack.isEmpty() || !stack.peek().url.equals(url)) {
|
||||
stack.push(new NavEntry(url, serviceId));
|
||||
}
|
||||
Intent i = new Intent(context, acitivtyClass);
|
||||
i.putExtra(SERVICE_ID, serviceId);
|
||||
i.putExtra(URL, url);
|
||||
|
@ -144,6 +143,7 @@ public class NavStack {
|
|||
|
||||
public void restoreSavedInstanceState(Bundle state) {
|
||||
ArrayList<String> sa = state.getStringArrayList(NAV_STACK);
|
||||
stack.clear();
|
||||
for(String url : sa) {
|
||||
stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId()));
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@ package org.schabi.newpipe.util;
|
|||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
|
@ -11,7 +15,7 @@ import android.support.v4.content.ContextCompat;
|
|||
public class PermissionHelper {
|
||||
public static final int PERMISSION_WRITE_STORAGE = 778;
|
||||
public static final int PERMISSION_READ_STORAGE = 777;
|
||||
|
||||
public static final int PERMISSION_SYSTEM_ALERT_WINDOW = 779;
|
||||
|
||||
|
||||
public static boolean checkStoragePermissions(Activity activity) {
|
||||
|
@ -65,4 +69,27 @@ public class PermissionHelper {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* In order to be able to draw over other apps, the permission android.permission.SYSTEM_ALERT_WINDOW have to be granted.
|
||||
* <p>
|
||||
* On < API 23 (MarshMallow) the permission was granted when the user installed the application (via AndroidManifest),
|
||||
* on > 23, however, it have to start a activity asking the user if he agree.
|
||||
* <p>
|
||||
* This method just return if canDraw over other apps, if it doesn't, try to get the permission,
|
||||
* it does not get the result of the startActivityForResult, if the user accept, the next time that he tries to open
|
||||
* it will return true.
|
||||
*
|
||||
* @param activity context to startActivityForResult
|
||||
* @return returns {@link Settings#canDrawOverlays(Context)}
|
||||
**/
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public static boolean checkSystemAlertWindowPermission(Activity activity) {
|
||||
if (!Settings.canDrawOverlays(activity)) {
|
||||
Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(i, PERMISSION_SYSTEM_ALERT_WINDOW);
|
||||
return false;
|
||||
}else return true;
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 575 B |
After Width: | Height: | Size: 675 B |
After Width: | Height: | Size: 277 B |
After Width: | Height: | Size: 457 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 908 B |
After Width: | Height: | Size: 889 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:startColor="#a0000000"
|
||||
android:centerColor="#26000000"
|
||||
android:endColor="#00000000"
|
||||
android:angle="90"
|
||||
/>
|
||||
</shape>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:focusable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.devbrackets.android.exomedia.ui.widget.EMVideoView
|
||||
android:id="@+id/emVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/root"
|
||||
android:focusable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true">
|
||||
|
||||
<com.google.android.exoplayer.AspectRatioFrameLayout android:id="@+id/video_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<SurfaceView android:id="@+id/surface_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<View android:id="@+id/shutter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"/>
|
||||
|
||||
<com.google.android.exoplayer.text.SubtitleLayout android:id="@+id/subtitles"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</com.google.android.exoplayer.AspectRatioFrameLayout>
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,180 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.devbrackets.android.exomedia.ui.widget.FitsSystemWindowRelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/exomedia_controls_text_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/exomedia_default_controls_text_background"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exomedia_controls_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="The Video Title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exomedia_controls_sub_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="The Video Album"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exomedia_controls_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
tools:text="The Video Artist"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/exomedia_controls_interactive_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:background="@drawable/exomedia_default_controls_interactive_background"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exomedia_controls_current_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingRight="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="1:06:29"/>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/exomedia_controls_video_seek"
|
||||
style="?android:seekBarStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/exomedia_controls_end_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingRight="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="RtlHardcoded"
|
||||
tools:text="1:23:49"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/exomedia_controls_extra_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="RtlHardcoded,UselessLeaf"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exomedia_controls_previous_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
app:srcCompat="@drawable/exomedia_ic_skip_previous_white"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exomedia_controls_frewind_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
android:src="@drawable/exomedia_ic_rewind_white"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exomedia_controls_play_pause_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
app:srcCompat="@drawable/exomedia_ic_play_arrow_white"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exomedia_controls_fforward_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
android:src="@drawable/exomedia_ic_fast_forward_white"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exomedia_controls_next_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
app:srcCompat="@drawable/exomedia_ic_skip_next_white"
|
||||
tools:ignore="ContentDescription,RtlHardcoded"
|
||||
tools:visibility="visible"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/exomedia_controls_video_loading"
|
||||
style="?android:progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"/>
|
||||
</com.devbrackets.android.exomedia.ui.widget.FitsSystemWindowRelativeLayout>
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
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"
|
||||
android:gravity="center">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/blackBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"/>
|
||||
|
||||
<com.devbrackets.android.exomedia.ui.widget.EMVideoView
|
||||
android:id="@+id/popupVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/endScreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:background="@android:color/white"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:weightSum="2">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/controlAnimationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:src="@drawable/ic_action_av_fast_rewind"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:ignore="ContentDescription"/>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playbackControlRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:gravity="bottom|center"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/popup_controls_bg"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackCurrentTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingRight="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText"
|
||||
android:text="-:--:--"/>
|
||||
|
||||
<!--style="?android:attr/progressBarStyleHorizontal"-->
|
||||
<SeekBar
|
||||
android:id="@+id/playbackSeekBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:max="100"
|
||||
android:layout_weight="1"
|
||||
android:progress="0"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playbackEndTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingRight="2dp"
|
||||
android:textColor="@android:color/white"
|
||||
tools:ignore="HardcodedText"
|
||||
android:text="-:--:--"/>
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loadingPanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:padding="20dp"
|
||||
tools:visibility="gone">
|
||||
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"/>
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/notificationContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="@color/background_notification_color"
|
||||
android:clickable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/notificationCover"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RtlHardcoded">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationSongName"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
tools:text="title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationArtist"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:maxLines="1"
|
||||
tools:text="artist"/>
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/notificationPlayPause"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="5dp"
|
||||
android:background="#00ffffff"
|
||||
android:clickable="true"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_pause_white_24dp"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/notificationStop"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="5dp"
|
||||
android:background="#00ffffff"
|
||||
android:clickable="true"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_close_white_24dp"
|
||||
tools:ignore="ContentDescription"/>
|
||||
</LinearLayout>
|
|
@ -22,6 +22,10 @@
|
|||
app:showAsAction="ifRoom"
|
||||
android:icon="?attr/cast"/>
|
||||
|
||||
<item android:id="@+id/menu_item_popup"
|
||||
app:showAsAction="never"
|
||||
android:title="@string/open_in_popup_mode"/>
|
||||
|
||||
<item android:id="@+id/menu_item_openInBrowser"
|
||||
app:showAsAction="never"
|
||||
android:title="@string/open_in_browser" />
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="cancel">Cancel</string>
|
||||
<string name="fdroid_vlc_url" translatable="false">https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc</string>
|
||||
<string name="open_in_browser">Open in browser</string>
|
||||
<string name="open_in_popup_mode">Open in popup mode</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="loading">Loading</string>
|
||||
<string name="download">Download</string>
|
||||
|
@ -188,6 +189,7 @@
|
|||
<string name="msg_copied">Copied to clipboard.</string>
|
||||
<string name="no_available_dir">Please select an available download directory.</string>
|
||||
<string name="msg_restart">You have to restart the application to apply the theme.\n\nDo you want to restart now?</string>
|
||||
<string name="msg_popup_permission">This permission is needed to\nopen in popup mode</string>
|
||||
|
||||
<!-- Checksum types -->
|
||||
<string name="md5" translatable="false">MD5</string>
|
||||
|
|