From abff1f537b6713a83795f5d4b96c28ac010fde8c Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Thu, 9 Mar 2017 01:44:00 -0300 Subject: [PATCH 1/6] Add ExoMedia and remove ExoPlayer - Remove ExoPlayer files - Added ExoMedia library --- app/build.gradle | 2 +- .../newpipe/player/ExoPlayerActivity.java | 655 +++++------------- .../player/exoplayer/DashRendererBuilder.java | 268 ------- .../newpipe/player/exoplayer/EventLogger.java | 214 ------ .../exoplayer/ExtractorRendererBuilder.java | 89 --- .../player/exoplayer/HlsRendererBuilder.java | 180 ----- .../newpipe/player/exoplayer/NPExoPlayer.java | 599 ---------------- .../SmoothStreamingRendererBuilder.java | 206 ------ .../main/res/layout/activity_exo_player.xml | 15 + .../main/res/layout/exo_player_activity.xml | 44 -- .../res/layout/exomedia_custom_controls.xml | 180 +++++ 11 files changed, 369 insertions(+), 2083 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java create mode 100644 app/src/main/res/layout/activity_exo_player.xml delete mode 100644 app/src/main/res/layout/exo_player_activity.xml create mode 100644 app/src/main/res/layout/exomedia_custom_controls.xml diff --git a/app/build.gradle b/app/build.gradle index f2f467326..a1da6a7f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index c38e63545..7fe97d0dc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -16,549 +16,240 @@ /** * Extended by Christian Schabesberger on 24.12.15. - * + *

* Copyright (C) Christian Schabesberger 2015 * 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 . */ 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 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 cues) { - subtitleLayout.setCues(cues); - } - }; - - NPExoPlayer.Id3MetadataListener id3MetadataListener = new NPExoPlayer.Id3MetadataListener() { - @Override - public void onId3Metadata(Map metadata) { - for (Map.Entry 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); + 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() { + Log.d(TAG, "------------ onControlsShown() called"); + showSystemUi(); } - } else { - player.setBackgrounded(false); - } + + @Override + public void onControlsHidden() { + Log.d(TAG, "------------ onControlsHidden() called"); + hideSystemUi(); + } + }); + videoView.setControls(videoControls); } @Override - public void onPause() { + public void onPrepared() { + Log.d(TAG, "onPrepared() called"); + videoView.start(); + } + + @Override + public void onCompletion() { + 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(); } + private void showSystemUi() { + Log.d(TAG, "showSystemUi() called"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().getDecorView().setSystemUiVisibility(0); + } - // 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 hideSystemUi() { + 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); } - // Permission management methods + private class CustomVideoControls extends VideoControlsMobile { + protected static final int FAST_FORWARD_REWIND_AMOUNT = 8000; - /** - * 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; - } - } + protected ImageButton fastForwardButton; + protected ImageButton rewindButton; - @TargetApi(23) - private boolean requiresPermission(Uri uri) { - return Util.SDK_INT >= 23 - && Util.isLocalFileUri(uri) - && checkSelfPermission(permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED; - } - - // Internal methods - - 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); + } + + @Override + protected void registerListeners() { + super.registerListeners(); + rewindButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onRewindClicked(); } - return true; - } else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { - if (event.getAction() == KeyEvent.ACTION_DOWN) { - playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds - show(); + }); + fastForwardButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onFastForwardClicked(); } - return true; + }); + } + + 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; + } + + 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; + Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration()+" position= "+ videoView.getCurrentPosition()); + if (isFinished) { + videoView.restart(); + setRewindButtonRemoved(false); + setFastForwardButtonRemoved(false); + isFinished = false; + seekBar.setEnabled(true); } - return super.dispatchKeyEvent(event); + } + + private void performSeek(int newPosition) { + internalListener.onSeekEnded(newPosition); + } + + public SeekBar getSeekBar() { + return seekBar; } } - } diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java deleted file mode 100644 index f12dc8975..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java +++ /dev/null @@ -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, UtcTimingCallback { - - private final Context context; - private final String userAgent; - private final MediaDrmCallback drmCallback; - private final NPExoPlayer player; - private final ManifestFetcher 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; - } - - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java deleted file mode 100644 index 62553ab3b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java deleted file mode 100644 index a74c33bf8..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java +++ /dev/null @@ -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. - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java deleted file mode 100644 index 8e6c2d9f5..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java +++ /dev/null @@ -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 { - - private final Context context; - private final String userAgent; - private final String url; - private final NPExoPlayer player; - private final ManifestFetcher 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> 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); - } - - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java deleted file mode 100644 index 63a6a9261..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java +++ /dev/null @@ -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>, 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. - *

- * 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. - *

- * 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 cues); - } - - /** - * A listener for receiving ID3 metadata parsed from the media stream. - */ - public interface Id3MetadataListener { - void onId3Metadata(Map 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 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.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 cues) { - if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) { - captionListener.onCues(cues); - } - } - - @Override - public void onMetadata(Map 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); - } - } - -} diff --git a/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java deleted file mode 100644 index 55b59c276..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java +++ /dev/null @@ -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 { - - private final Context context; - private final String userAgent; - private final MediaDrmCallback drmCallback; - private final NPExoPlayer player; - private final ManifestFetcher 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); - } - - } - -} diff --git a/app/src/main/res/layout/activity_exo_player.xml b/app/src/main/res/layout/activity_exo_player.xml new file mode 100644 index 000000000..5653532ec --- /dev/null +++ b/app/src/main/res/layout/activity_exo_player.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/layout/exo_player_activity.xml b/app/src/main/res/layout/exo_player_activity.xml deleted file mode 100644 index c7d3a7c95..000000000 --- a/app/src/main/res/layout/exo_player_activity.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/exomedia_custom_controls.xml b/app/src/main/res/layout/exomedia_custom_controls.xml new file mode 100644 index 000000000..dedaf7908 --- /dev/null +++ b/app/src/main/res/layout/exomedia_custom_controls.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From a37d8f083a336ae43d9d7e11d94b7e2eaca05461 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Thu, 9 Mar 2017 04:42:40 -0300 Subject: [PATCH 2/6] Implement popup mode - Add icons replay, fast_forward - Add strings - Add menu entry - Add as option to open link directly to popup mode --- app/src/main/AndroidManifest.xml | 61 ++ .../org/schabi/newpipe/PopupActivity.java | 137 +++ .../newpipe/detail/ActionBarHandler.java | 11 + .../detail/VideoItemDetailFragment.java | 54 +- .../newpipe/player/ExoPlayerActivity.java | 51 +- .../newpipe/player/PopupVideoPlayer.java | 826 ++++++++++++++++++ .../newpipe/player/popup/PopupViewHolder.java | 93 ++ .../newpipe/player/popup/StateInterface.java | 17 + .../schabi/newpipe/util/PermissionHelper.java | 29 +- .../ic_action_av_fast_forward.png | Bin 0 -> 575 bytes .../res/drawable-hdpi/ic_replay_white.png | Bin 0 -> 675 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 277 bytes .../res/drawable-mdpi/ic_replay_white.png | Bin 0 -> 457 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 574 bytes .../res/drawable-xhdpi/ic_replay_white.png | Bin 0 -> 908 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 889 bytes .../res/drawable-xxhdpi/ic_replay_white.png | Bin 0 -> 1390 bytes .../ic_action_av_fast_forward.png | Bin 0 -> 1381 bytes .../res/drawable-xxxhdpi/ic_replay_white.png | Bin 0 -> 1885 bytes .../main/res/drawable/popup_controls_bg.xml | 9 + app/src/main/res/layout/player_popup.xml | 110 +++ .../res/layout/player_popup_notification.xml | 69 ++ app/src/main/res/menu/videoitem_detail.xml | 4 + app/src/main/res/values/strings.xml | 2 + 24 files changed, 1422 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/PopupActivity.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java create mode 100644 app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-hdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-mdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_replay_white.png create mode 100644 app/src/main/res/drawable/popup_controls_bg.xml create mode 100644 app/src/main/res/layout/player_popup.xml create mode 100644 app/src/main/res/layout/player_popup_notification.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc050f2ce..4e8ca0676 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/PopupActivity.java b/app/src/main/java/org/schabi/newpipe/PopupActivity.java new file mode 100644 index 000000000..59763070b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/PopupActivity.java @@ -0,0 +1,137 @@ +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 Acitivty is designed to route share/open intents to the specified service, and + * to the part of the service which can handle the url. + */ + +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 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); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java index 30ddf3da9..eccfc0919 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java +++ b/app/src/main/java/org/schabi/newpipe/detail/ActionBarHandler.java @@ -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; } diff --git a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java index 8dc403f11..a221e4e94 100644 --- a/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/detail/VideoItemDetailFragment.java @@ -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 !!! } diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java index 7fe97d0dc..c868bb722 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -1,39 +1,3 @@ -/* - * 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 - * 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 . - */ - package org.schabi.newpipe.player; import android.app.Activity; @@ -56,6 +20,7 @@ import org.schabi.newpipe.R; 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; @@ -94,13 +59,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O videoControls.setVisibilityListener(new VideoControlsVisibilityListener() { @Override public void onControlsShown() { - Log.d(TAG, "------------ onControlsShown() called"); + if (DEBUG) Log.d(TAG, "------------ onControlsShown() called"); showSystemUi(); } @Override public void onControlsHidden() { - Log.d(TAG, "------------ onControlsHidden() called"); + if (DEBUG) Log.d(TAG, "------------ onControlsHidden() called"); hideSystemUi(); } }); @@ -109,13 +74,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O @Override public void onPrepared() { - Log.d(TAG, "onPrepared() called"); + if (DEBUG) Log.d(TAG, "onPrepared() called"); videoView.start(); } @Override public void onCompletion() { - Log.d(TAG, "onCompletion() called"); + if (DEBUG) Log.d(TAG, "onCompletion() called"); // videoView.getVideoControls().setButtonListener(); //videoView.restart(); videoControls.setRewindButtonRemoved(true); @@ -144,13 +109,13 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O } private void showSystemUi() { - Log.d(TAG, "showSystemUi() called"); + if (DEBUG) Log.d(TAG, "showSystemUi() called"); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().getDecorView().setSystemUiVisibility(0); } private void hideSystemUi() { - Log.d(TAG, "hideSystemUi() called"); + 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 @@ -234,7 +199,7 @@ public class ExoPlayerActivity extends Activity implements OnPreparedListener, O protected void onPlayPauseClick() { super.onPlayPauseClick(); if (videoView == null) return; - Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration()+" position= "+ videoView.getCurrentPosition()); + if (DEBUG) Log.d(TAG, "onPlayPauseClick() called" + videoView.getDuration() + " position= " + videoView.getCurrentPosition()); if (isFinished) { videoView.restart(); setRewindButtonRemoved(false); diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java new file mode 100644 index 000000000..7d211ecdb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -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(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java b/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java new file mode 100644 index 000000000..22895668e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/popup/PopupViewHolder.java @@ -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; + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java b/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java new file mode 100644 index 000000000..94ea41470 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/popup/StateInterface.java @@ -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(); +} \ No newline at end of file diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 4c43426c5..a5707ecb2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -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. + *

+ * 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. + *

+ * 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; + } } diff --git a/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-hdpi/ic_action_av_fast_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..672ee37bcbf0fd54d6d6351d6166bea7860c6297 GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmSQK*5Dp-y;YjIVU|{^_>EalY z;rw>`eti~4k>mNTE{iX8dMQrVJ|e$h`PJ21n!;AN#5{Fh?6AmLlS^n~M^K0rmlc=e z5z$BP6Qbu&zjJEVn|Ei{MXP@@`}riT{9gI9z3#c6zQu#z zb>=xvnrCZ{^dA;5S$^@V?KGCIfJS49Hnr)V9nlTx5p8Jthx$pk6lS}7!9%l7$3cb;6_u-M+QvH~Q7kP<-rzm4DK`svG91Z`N3zFBqJ%Q~%O_#?LBOGCO&9MQ8LJ;+w_TVe8-b*tOko!)Y#- zb*G+lPC2lRDe)|``Q!^?bB}&sUb&{>^d3*cx}fDZ#ec3%WJ}#q@3NV#d*1Wd$IDI& zgf{gp;9-9;$E8xFz>!IKgSXJ6DW|{sUC{DbymALe(~K&y4~G^+8@LNC*POINsf=se z#l6ceEagwtJ|g?5?eB!YC9Em)-!;FvoqWC4`;F|0X_1b_lkc7C;YwzGlA0&B#@_D8 zpPkcp>a4%)FLLR_^_OevGmd%e`&gf0^v-4~dQAMk(&&Hx!IJA!w({BnQv`#jtDnm{ Hr-UW|6-NQP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white.png b/app/src/main/res/drawable-hdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..fcddcf02ddb58ee1680889e7315757d76419b4b2 GIT binary patch literal 675 zcmV;U0$lxxP)i^4O#T$_9m82|5VvpF*wP4OP zM$UZi@^>7mIp;Yu=T~<=&tBi0Ip_J;@t6l6eCkO56W1~dg3&=h1q zQ;-2oK?XDh8PF7DKvR$bO+kjAeDJ~pr|dFMzEwenMqRT?w%I|3a4PK3rz1$XY>?9M z$o%dYv=53msmx64U;8dHjIzKUk9_ChB>7>S;1G`>C)ufEh%>(Om`-|fb*SR^#z4}` zk1=8_ANv5(l+5L85*Xb37G?llB;BT5>A(ui94pbtcshM*TjCYGR_ z-6MNIjB5B%pCT4Cy!N|@y;y<{>^_Y1h{sr_j%UPd_@F#~6P7u_$u$?{J9LdWv<^B( zTt-`dtRg&yta3*15X;emvQ!XfRzVjCmrT?J+75nB>W0d9%nhW=(93#VAeC5$p_bT;=ksFy7&G{R+*d}}W7vaE1LouAaX zWSLB>FM)fQW1AE1DN&IEF;H zzYVeEJLJIAqoJYk(;?%BL(pesxdY8XPo1Z{T&a<9(MT_hOWRs*8a*xy>u=*T0s-0jFCp&d^=VN2rM(KhX;>I$o zX1Z$~$>r#_zL0Kr_~Vw9F$#IkHVxVv(k()a`?ml&dm05_Fj%X{rJTEXdHWr`Umt}l zbu{lc6_ti=KfsqbU6!lu&!e2O%>wJIc-47=pDJcu+;J~>Pv!d%4fZDuGC$PW?OxQq Vp1nUR?JUq=44$rjF6*2UngIOgXq*55 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white.png b/app/src/main/res/drawable-mdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..3b41913257dbe14e552f8203ba4612c9e49fa7a7 GIT binary patch literal 457 zcmV;)0XF`LP)FTt0oJ-PWqJo#>PDsS z`vtg-Yj!6pzlRYn$|uYfju>o)RHCdaOepvx$Asb!d`#= zj9GfM%UQ%Z_%ASSS)uvl>Ee}7Z>bB!D0o=6hwbXB`dMu+HI^KKV^PCw@Y7Xhr@(2=3F!9#Q0L{xh1dvWPHv1m9_7Drj_M4$wROC zE?c)u6RO!;BcznD;^CRl<==#*r(c++^5K)`?B2u@rw?35jLo(!2^Jmnt%V;%zvL6!?*ZKQtC7XY_3Tm%(Wz`S|H%frVxvmCv38R_KRZJgpw0eA>|UgYlP- zKa7`r+UR3+T~*<94NvceC#g51W>4?mT32yris*&g;!1n#RIlBhzUXhAuH3)Y2Z2Hg gj5ttZy~hJ~b~dH59^PfLzy!hI>FVdQ&MBb@08(@E`Tzg` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white.png b/app/src/main/res/drawable-xhdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..1573fb111b535e5e0df9371ff7f804ce2c40a0fa GIT binary patch literal 908 zcmV;719SX|P)7!(!0)U7tJ0c*i|=W=`GD{Rhrh*S+VSbLJ}_yb|Y} z2=nyPP+9@g9^_WikSR{kYsy-hy zEku#&*XNV6oCC6@Y0~GT-ej9>Czv`dtH=_DbXwRliC~qaN~fj83SwBz>9x4qN(Rvr ze!Z60m#~`DYlFi_98nBIuPvr=tUC1CrC;Y$3W(^FgzQC`p+k!}pOw6jBq=iPfkh0TWmS)dUz^Bf7(L-mV&-4}J$XOF7sBlc6Scm>hhDc(^Uj{LXd-%JWIiC2AM`rMs#_tFGOcEm> zQ6{J?t)HM6bg;rDU%4Peu#|sCR!~qE~FTC?knj22pV}>5;)E~ds^l}5-05`x5 ia0A=`H^2>0aQz3T@7w=K#~w`p0000Eakt z5%+f1-qpg6B1hwoxH*PwxZvj^?YK5aC+9Ewn>D&FYvNP0^VPRCDI}#X?p?ZI?t-le ziegPs8jr$lgg+(ldZv}7zq={^M>l5E?48BuXWoC~US=L0ts~IDz@osw$l<`iL^3nL zA;D_yy)YSFkF|bvuCe?cWx!?Pn$)*{kDIt0cCwH;Zc^>SLOs~uW+y4 zKQG{t%b%be?Qh>rUtf9kI(N$-#(+yIOPix|`woc$1!}@$G_RIqO|6o$ zT`%M{t7U$ddROz#gBDwh;=FDjmgAlxrE%i+>N(zB&zWjAD(qJ~x-#_f<~{k%dyQmS z?>?Po_D#3=!Ih|G_D^=dnmvC`M$@7>&O7(adn-SsKFw%m&6{ty@t%@Zx_I@bL+N!j5rMpAPtH;?_?f`@5^7+?YOV3wd<2|-=>$&6WvR-pOUHZwNU$y}fq)r+ykDi_zRSXO9 zb6X9bT*-QPJxH1P>B3w6TZ|rE$$DzJ=Ny;DA5+6Qe_|36nJ&#Qj@RgOe__2<=}-HW zHx8cVTgpWf^B>NMQrw`_b#CFFtIr?IpT2b8oT#Pq+|CP6`69IUW^38qhB;BMYc8;n immopu_`!eW58@w6gtYIy4&DaLOAMZ_elF{r5}E)|O?+km literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white.png new file mode 100644 index 0000000000000000000000000000000000000000..5105c2251b72ccf4968f659b10e98200b6a21acd GIT binary patch literal 1390 zcmV-!1(EuRP)BgE0Bosd;&l!5{GB}Nw&aWDv$#(+==h%zxz{0{`dg+v3oNsZEs1O#b7TU=z3 zN+U>6X{oItF;)nPJ2M7_WMc9%Um4!^-ghT8^Ukw#zTbcF!Mpd}bI-XHnCAtLxJ@r7 z*+WWFkh~r@MJBk;QR;*R$?I{j$^*{OAS_5;j{}dp93n0(NM4VF=X6pdEJ$9DgITg9 zR8TMzWb_kr@e}pBNC)$pcHQP(8I2%dRM20TlCA~t5IK}Qkj#7O)Q^3r9eOSQH4a|6K{m=!i&(jnw*l3S} zAo;|Y#mrR|c`AQ73Aj>W`=NK1ad*g$rgcHjacN zsE$?43`9cEp#0VI1I#Q^C$1+=0W+iFH7}gO##wPe=dsZho}dOi%*>M#)$=KDF|$HC z%=VYN*tj4n=n^*Wgxi952pdam7t`|<%h)&=uAn&2u`wd{|1*k>Nn)W2a<^>@q#HZO z!*3+7VHO(;>=dXW7O*kRCla0ZS?o+w8>*fZ|6u2s%rHxW3G57$2u;t&j9}+4Hb_nT zWC$WhETqMGkRZO19H<*2h`taM#pox9o3bOB^}HsC9El2lT!REL$7j+rtL+3cT*2i! z#R$QC8-=m(9KlS|RH2RiIl)|u#=`OrwtD`Z#efJ`%0Sgyv2>j;V3?^)IpJQ zC~$)oVryJ;_A*F;awzhBRG;YTz@uDdxXb~P?psnaT;V0<;<@!m_9(Wo#9CP45&fK^ zm0dKDWCIB{)4(oTIZZ#0Sz#>{I2`q70$a!<#VdblmKEeqBTtav5BJ6{FUU`l0m9~* z`?oM!L%nU}35z-RnM+)dA!%+A1~=V*R#Jl8+0QtYYudtsG%831sUQ`k wf>e+SQb8(6qk>eB3Q|ETNCl}N6{IobUk;_uiK)$-$N&HU07*qoM6N<$g8NE^b^rhX literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png b/app/src/main/res/drawable-xxxhdpi/ic_action_av_fast_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..599c66f70570b6f2668b853354b1d4327aeee514 GIT binary patch literal 1381 zcmcgs`&W_&82(IA!cjSurnWMh;jP-j*$GX|De_@yL#D^iHZL`%8D5epN)pj3uQR;V zHY)6bm!%!$sDVS#WGR}MA1yT#DUXgy@vAyTK@ALfdJnv7>c?yq3gkdqx z7ytmUASHxmM&7@Rwl?3Mz{CMFSY-u+bhLTSqE8k80BRc;a+uEkX8wA3-;uMq0Cij@x^RFq4yB4zG4>S$B9nwmBy^`V?8eq?$ne0)H3 zf}y^SbPgRv{FhCgS^CGhlO}|7^W6=?cZ>+}!wnYbAYj859$@7P{O2^J8fuIXWP_Rj z;H|#bvf}t5^s3Kn6)PU{#16Ig>@MXX8fDtsoDqwWQ_B}v6lXi{{psAKO!TvK%M9-g zaSLFU_*Qqx()V`*QwcOtm6RLYyF;$9(vja^Y>CAXUD$|yz|LF##?2Yt{2_Qx3fUuG zYy6?eA0i#)#o3Mic^uk{9T=}$UmXeW+OhSV$r`E1kq?ij+evSo+p|p7Aind1kvE&$ zlYhhNqOC~cUf8W3Uk1q6RJ$HM^0WCoP{$sZs|9;0E|m=%1s!>ay;E4x!uYn#ygbG9 z?KVC5vt#oClINASd1u{zM`F+`xxt22`I^%)bh8Ea)5>m3zja*rwY!a8{7k2oG0=ULz`u+MRjioMCc1Ogm-{t-Q-hJSw$)eboS=jI=40%RP=y^I zlx0oT8VEu8YI5p{%dD`_CfA;mHO&5tqBJicyfS|vxs4SS+N9~;n8YGYL<6nEM6$Py zc*={vmU21cQg0}tX+$N+Ts+5S&@pr?7qM>g@M(`~4{o~*IAv5MPv4_6DU^o6YVIXv z5qwx-z5dJ7ag7FfVob&rq@?mhTNv%FKp92P9 zg4BYE;rA@fL0;+7E_gp)mGiPTj={rIjd`=3<)%Z*Cu#6y7t*&P3xjg5vV~Hpc;mXJ zHPZV7ECarJRYt{Et-d=-K3OoKH5z}Em7o@*GAFJW+D!y^(j3P>cTry(-+ZkR(0aBL zU<>>mtx&{OJt&np@Jb~dIMMy@b)H8LNjDhjc3$Gp1%^svBz7~2Z}oTqD6s7!j}cPJ z$nU3>=$?7=Cvjh~(eDY@u_an!+Kw|PB8jGS(zUvcGYd!h-er6|DHL7(FrGsbV7h8X z!{BVy{nxo{&WtKDu5ce@Ar~b2Hxp}=3W{S&n@uiKL2P$>3}mC26|69oNkD`Ro0jyo zclrXh^76^txaVf2!K455rWHM2SeMjxJ6-q5L}h)pP+Fa{X<3hFr16(-057<|6QU%O zPf=gX*ETwzEm1r36o0G)dcGdq`DV)OBuDU9Vr^ce;W~mb#OVXpZw4dpC}5_obYd^J p*mwAg46Tu!z_G0000LhNkl?*|(>Q-}x=BZLQ4&YpE&mJ&MS+o(x;tb+l&fx65xW;&^oP6*;;qZ`277j0&B;g2y13QyVxcB2x=EDkM`q92JwP7LF>&R0l^@ zWU7IqT37j=K`wEM_jsFaJi{v5DanN4NXjtpvz;}}lo=0>dN|9gbkZzyEgbbU#4aA8 zQRXK&((omlnJeIN0diU~(n^z$bb4~{H3$n7c`99eROtxQ(2;K-J9 ztWdGw$ePz^QIX)tno}%Rk>JRh5uQ|$;K-g=Xi#zB$etcrRU9~~)5H{JvygjP&5P`! zk0Gk{A=6bHII5uQP@;q9+0P)AJIgE;2abwqTr_YWuQNbpF3_&xz)>Lu(9CLjC==fW zW~n%ER78=mfZdD{*IA~kIB--zkujGyxI#Srw5m98#HLv2;281r(4gYL5tCw~#8Z4l z99=3B9I>bZ+{!z|u}(#TBL-E7XBj1`5f-UPa73nxaTn)^>JwU2Bsd~c6_`pNQEgYT z;D|(3p_Ri#Q|2KR3yug>B^v1_n$Ngd#eyS9Rii{V(fmV2gCjsSKqH5V;xfNi(cth> zP0&goQ5;b5;P6n5(8hV9SgGQ{F)q~%cQZ=Z2^A5Jaj1rPo+utw5#hKY)fD>(>sK-1 zxB=A^Q}~*&2UScsuA>^`X~KF{R5*S^HAad55K?BIiVDXyRC9DNM#xJlE*!t21h9jU zFPNm_!to0wfVqqj^00~w$Ip}qx(Rtp#fIZYN(2ij<2OK)iVepPln{CeS*fDKffB+R zLb_ydN(e0s;&(s}pO7bHl0ySS_;tx7i39i@lSvXU;5R6fB<>_+woGy; zF^pfQOp@rwZ@o;C=*Dl0Op@4$-(Hy{u@=80GD%`7ekWy;Lqo1 zGD)HtzcHnatN1l3$7sfHOljkL{H7?!Xv1$rX=4z-*~&5I;P;2zgmqK_?+S${7w4vO-xwGeh{5S)d$Y5oP=aXi|2ti;#aRN7zHiPGt#?5Hd!) za)S9>C8SeXLL*-i@`iGPorGLslCp)(gj`{RC89@{O zCW=w!Df`$;G`lG&11PbFXf`Pin9Mn%*-fKTMiYC8=2Kdf53C@n!?Y?z+`_+zrpyD% z4_+gxKH8KLrttw$btzA1;WSa5=WeBdd-$BFKBigu!eT~ zNgVr_qB>zJ?-Ivqp|mlQH7yW3uv%>6|65F}hix8e$Q97$dI#F-`f$EG`gVnO@e=qAJnCIu24M zzVpn~#6UY2sLUWcxSs}9fJrRlO$MpVdFE*1U>0Yo+zyvz!kWGch&c!DmDF+?1L9Az7iGef3Um`x|^ z*}`6qaFUA*Fv1vPjPNZNImr?BvW4|@a+{1X#u#IaF~%5Uj4{R-V~jDz7-NhvCN=*D X>#M$jzV5xv00000NkvXXu0mjfXY*@a literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/popup_controls_bg.xml b/app/src/main/res/drawable/popup_controls_bg.xml new file mode 100644 index 000000000..d04812bd8 --- /dev/null +++ b/app/src/main/res/drawable/popup_controls_bg.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_popup.xml b/app/src/main/res/layout/player_popup.xml new file mode 100644 index 000000000..6d1860408 --- /dev/null +++ b/app/src/main/res/layout/player_popup.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_popup_notification.xml b/app/src/main/res/layout/player_popup_notification.xml new file mode 100644 index 000000000..188589bef --- /dev/null +++ b/app/src/main/res/layout/player_popup_notification.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/videoitem_detail.xml b/app/src/main/res/menu/videoitem_detail.xml index 06184da1d..c4f2d5fb1 100644 --- a/app/src/main/res/menu/videoitem_detail.xml +++ b/app/src/main/res/menu/videoitem_detail.xml @@ -22,6 +22,10 @@ app:showAsAction="ifRoom" android:icon="?attr/cast"/> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71c11fd50..263c6bc14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Cancel https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc Open in browser + Open in popup mode Share Loading Download @@ -188,6 +189,7 @@ Copied to clipboard. Please select an available download directory. You have to restart the application to apply the theme.\n\nDo you want to restart now? + This permission is needed to\nopen in popup mode MD5 From c02c511e31fb233f66750789081bc0be7a1829b5 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 9 Mar 2017 15:41:10 +0100 Subject: [PATCH 3/6] Update .travis.yml update build-tools version for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 32629b1cb..36b71b8ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 From a8ff4b0744510773e1f56d05eba4d13f1e559b7d Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 9 Mar 2017 16:01:09 +0100 Subject: [PATCH 4/6] Fix backbutton behaviour - close on MainActivity when back pressed - clear NavStack on rotation --- app/src/main/java/org/schabi/newpipe/MainActivity.java | 5 ----- .../main/java/org/schabi/newpipe/util/NavStack.java | 10 +++++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index eb9fd313c..781460f0a 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -85,9 +85,4 @@ public class MainActivity extends AppCompatActivity { return super.onOptionsItemSelected(item); } } - - @Override - public void onBackPressed() { - //ignore back - } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavStack.java b/app/src/main/java/org/schabi/newpipe/util/NavStack.java index f84c5725e..a5941ad83 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavStack.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavStack.java @@ -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) { - stack.push(new NavEntry(url, serviceId)); + //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 sa = state.getStringArrayList(NAV_STACK); + stack.clear(); for(String url : sa) { stack.push(new NavEntry(url, NewPipe.getServiceByUrl(url).getServiceId())); } From 095a2be748d23606c19501b5929d8360504f438d Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 9 Mar 2017 16:14:49 +0100 Subject: [PATCH 5/6] change popuplayer class description --- app/src/main/java/org/schabi/newpipe/PopupActivity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/PopupActivity.java b/app/src/main/java/org/schabi/newpipe/PopupActivity.java index 59763070b..f0f1c1ea8 100644 --- a/app/src/main/java/org/schabi/newpipe/PopupActivity.java +++ b/app/src/main/java/org/schabi/newpipe/PopupActivity.java @@ -17,8 +17,7 @@ import java.util.Collection; import java.util.HashSet; /** - * This Acitivty is designed to route share/open intents to the specified service, and - * to the part of the service which can handle the url. + * This activity is thought to open video streams form an external app using the popup playser. */ public class PopupActivity extends Activity { From e5ce3f30074f4c07ee2a135c7df2882c3359f4f3 Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 9 Mar 2017 16:21:20 +0100 Subject: [PATCH 6/6] moved on to version 0.8.12 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7ca567349..73f11aab0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 {