diff --git a/app/build.gradle b/app/build.gradle index 10e4df714..b6f963973 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,4 +43,5 @@ dependencies { compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' compile 'com.github.nirhart:parallaxscroll:1.0' compile 'org.apache.directory.studio:org.apache.commons.lang:2.6' + compile 'com.google.android.exoplayer:exoplayer:r1.5.5' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64b7aec4f..f3160f28e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,20 +79,39 @@ - + tools:ignore="UnusedAttribute"/> + android:label="@string/background_player_name"/> + + + + + + + + + + + + + + android:label="@string/settings_activity_title" /> - + - diff --git a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java b/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java index 14c495dbe..84d0dc21b 100644 --- a/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/VideoItemDetailFragment.java @@ -38,6 +38,7 @@ import android.widget.Toast; import java.io.IOException; +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; @@ -46,6 +47,7 @@ import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import java.util.ArrayList; import java.util.Vector; + import org.schabi.newpipe.extractor.AudioStream; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.ParsingException; @@ -56,6 +58,9 @@ import org.schabi.newpipe.extractor.StreamPreviewInfo; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.VideoStream; import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor; +import org.schabi.newpipe.player.BackgroundPlayer; +import org.schabi.newpipe.player.PlayVideoActivity; +import org.schabi.newpipe.player.ExoPlayerActivity; /** @@ -858,12 +863,11 @@ public class VideoItemDetailFragment extends Fragment { // External Player Intent intent = new Intent(); try { - intent.setAction(Intent.ACTION_VIEW); - - intent.setDataAndType(Uri.parse(selectedVideoStream.url), - MediaFormat.getMimeById(selectedVideoStream.format)); - intent.putExtra(Intent.EXTRA_TITLE, info.title); - intent.putExtra("title", info.title); + intent.setAction(Intent.ACTION_VIEW) + .setDataAndType(Uri.parse(selectedVideoStream.url), + MediaFormat.getMimeById(selectedVideoStream.format)) + .putExtra(Intent.EXTRA_TITLE, info.title) + .putExtra("title", info.title); activity.startActivity(intent); // HERE !!! } catch (Exception e) { @@ -873,9 +877,9 @@ public class VideoItemDetailFragment extends Fragment { .setPositiveButton(R.string.install, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); + Intent intent = new Intent() + .setAction(Intent.ACTION_VIEW) + .setData(Uri.parse(activity.getString(R.string.fdroid_vlc_url))); activity.startActivity(intent); } }) @@ -888,13 +892,41 @@ public class VideoItemDetailFragment extends Fragment { builder.create().show(); } } else { - // Internal Player - Intent intent = new Intent(activity, PlayVideoActivity.class); - intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title); - intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url); - intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url); - intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position); - activity.startActivity(intent); //also HERE !!! + if (PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(activity.getString(R.string.use_exoplayer_key), false)) { + + // 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); + startActivity(intent); + } else if((info.audio_streams != null && !info.audio_streams.isEmpty()) && + (info.video_only_streams != null && !info.video_only_streams.isEmpty())) { + // try smooth streaming + + } else { + //default streaming + Intent intent = new Intent(activity, ExoPlayerActivity.class) + .setDataAndType(Uri.parse(selectedVideoStream.url), + MediaFormat.getMimeById(selectedVideoStream.format)) + .putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_OTHER); + + activity.startActivity(intent); // HERE !!! + } + //------------- + + } else { + // Internal Player + Intent intent = new Intent(activity, PlayVideoActivity.class) + .putExtra(PlayVideoActivity.VIDEO_TITLE, info.title) + .putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url) + .putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url) + .putExtra(PlayVideoActivity.START_POSITION, info.start_position); + activity.startActivity(intent); //also HERE !!! + } } // -------------------------------------------- diff --git a/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java similarity index 98% rename from app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java rename to app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 47afac5b1..21d3d6e0d 100644 --- a/app/src/main/java/org/schabi/newpipe/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.player; import android.app.Notification; import android.app.NotificationManager; @@ -20,6 +20,12 @@ import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; +import org.schabi.newpipe.ActivityCommunicator; +import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.R; +import org.schabi.newpipe.VideoItemDetailActivity; +import org.schabi.newpipe.VideoItemDetailFragment; + import java.io.IOException; /** diff --git a/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java new file mode 100644 index 000000000..d0aa7967e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/ExoPlayerActivity.java @@ -0,0 +1,565 @@ +/* + * 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. + */ + +/** + * Created by Christian Schabesberger on 24.12.15. + * + * Copyright (C) Christian Schabesberger 2015 + * ExoPlayerActivity.java is part of NewPipe. all changes are under GPL3 + * 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 java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * An activity that plays media using {@link NPExoPlayer}. + */ +public class ExoPlayerActivity extends Activity { + + // 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"; + + // 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 + + @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(); + } + + @Override + public void onNewIntent(Intent intent) { + releasePlayer(); + playerPosition = 0; + setIntent(intent); + } + + @Override + public void onResume() { + super.onResume(); + Intent intent = getIntent(); + contentUri = intent.getData(); + contentType = intent.getIntExtra(CONTENT_TYPE_EXTRA, + inferContentType(contentUri, intent.getStringExtra(CONTENT_EXT_EXTRA))); + contentId = intent.getStringExtra(CONTENT_ID_EXTRA); + provider = intent.getStringExtra(PROVIDER_EXTRA); + configureSubtitleView(); + if (player == null) { + if (!maybeRequestPermission()) { + preparePlayer(true); + } + } else { + player.setBackgrounded(false); + } + } + + @Override + public void onPause() { + super.onPause(); + if (!enableBackgroundAudio) { + releasePlayer(); + } else { + player.setBackgrounded(true); + } + shutterView.setVisibility(View.VISIBLE); + } + + @Override + public void onDestroy() { + super.onDestroy(); + audioCapabilitiesReceiver.unregister(); + releasePlayer(); + } + + + // 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(); + } + } + + // Permission management methods + + /** + * Checks whether it is necessary to ask for permission to read storage. If necessary, it also + * requests permission. + * + * @return true if a permission request is made. False if it is not necessary. + */ + @TargetApi(23) + private boolean maybeRequestPermission() { + if (requiresPermission(contentUri)) { + requestPermissions(new String[] {permission.READ_EXTERNAL_STORAGE}, 0); + return true; + } else { + return false; + } + } + + @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) { + super(context); + } + + @Override + public void setMediaPlayer(MediaController.MediaPlayerControl playerControl) { + super.setMediaPlayer(playerControl); + this.playerControl = playerControl; + } + + @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(); + } + return true; + } else if (playerControl.canSeekBackward() && keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + playerControl.seekTo(playerControl.getCurrentPosition() - 5000); // milliseconds + show(); + } + return true; + } + return super.dispatchKeyEvent(event); + } + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java similarity index 99% rename from app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java rename to app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java index 823fb762c..91ba36f19 100644 --- a/app/src/main/java/org/schabi/newpipe/PlayVideoActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayVideoActivity.java @@ -1,4 +1,4 @@ -package org.schabi.newpipe; +package org.schabi.newpipe.player; import android.content.Context; import android.content.Intent; @@ -27,6 +27,9 @@ import android.widget.MediaController; import android.widget.ProgressBar; import android.widget.VideoView; +import org.schabi.newpipe.App; +import org.schabi.newpipe.R; + /** * Copyright (C) Christian Schabesberger 2015 * PlayVideoActivity.java is part of NewPipe. @@ -191,7 +194,6 @@ public class PlayVideoActivity extends AppCompatActivity { @Override public void onResume() { super.onResume(); - App.checkStartTor(this); } @Override 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 new file mode 100644 index 000000000..f12dc8975 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/DashRendererBuilder.java @@ -0,0 +1,268 @@ +/* + * 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 new file mode 100644 index 000000000..62553ab3b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/EventLogger.java @@ -0,0 +1,214 @@ +/* + * 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 new file mode 100644 index 000000000..a74c33bf8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/ExtractorRendererBuilder.java @@ -0,0 +1,89 @@ +/* + * 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 new file mode 100644 index 000000000..8e6c2d9f5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/HlsRendererBuilder.java @@ -0,0 +1,180 @@ +/* + * 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 new file mode 100644 index 000000000..63a6a9261 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/NPExoPlayer.java @@ -0,0 +1,599 @@ +/* + * 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 new file mode 100644 index 000000000..55b59c276 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java @@ -0,0 +1,206 @@ +/* + * 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_play_video.xml b/app/src/main/res/layout/activity_play_video.xml index 117006a1a..bdedb2c2c 100644 --- a/app/src/main/res/layout/activity_play_video.xml +++ b/app/src/main/res/layout/activity_play_video.xml @@ -2,7 +2,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="org.schabi.newpipe.PlayVideoActivity" + tools:context=".player.PlayVideoActivity" android:gravity="center"> + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml new file mode 100644 index 000000000..e13926f59 --- /dev/null +++ b/app/src/main/res/values/constants.xml @@ -0,0 +1,22 @@ + + + + + + + 13sp + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index cba909e4a..afb085f6d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,5 @@ - 14sp @@ -39,4 +38,4 @@ 16dp 16dp - \ No newline at end of file + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 18297c34b..e17179117 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -11,6 +11,7 @@ use_external_video_player use_external_audio_player autoplay_through_intent + use_exoplayer default_resolution_preference 360p diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 856225321..312e1f9a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - - + + NewPipe NewPipe Background Player NewPipe @@ -116,4 +116,24 @@ Cannot create download directory \'%1$s\' Created download directory \'%1$s\' - + + Play in background + Video + Audio + Text + Logging + Normal + Verbose + Retry + [off] + Protected content not supported on API levels below 18 + This device does not support the required DRM scheme + An unknown DRM error occurred + This device does not provide a decoder for %1$s + This device does not provide a secure decoder for %1$s + Unable to query device decoders + Unable to instantiate decoder %1$s + Permission to access storage was denied + Use ExoPlayer + Experimental + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8abacc2e8..cb0233f00 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,19 @@ + + + + + + + - \ No newline at end of file + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index a9a0040e2..7ecf1a04f 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -33,6 +33,11 @@ android:entryValues="@array/audio_format_list" android:defaultValue="@string/default_audio_format_value"/> +
+ * 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 new file mode 100644 index 000000000..55b59c276 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/exoplayer/SmoothStreamingRendererBuilder.java @@ -0,0 +1,206 @@ +/* + * 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_play_video.xml b/app/src/main/res/layout/activity_play_video.xml index 117006a1a..bdedb2c2c 100644 --- a/app/src/main/res/layout/activity_play_video.xml +++ b/app/src/main/res/layout/activity_play_video.xml @@ -2,7 +2,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="org.schabi.newpipe.PlayVideoActivity" + tools:context=".player.PlayVideoActivity" android:gravity="center"> + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml new file mode 100644 index 000000000..e13926f59 --- /dev/null +++ b/app/src/main/res/values/constants.xml @@ -0,0 +1,22 @@ + + + + + + + 13sp + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index cba909e4a..afb085f6d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,6 +1,5 @@ - 14sp @@ -39,4 +38,4 @@ 16dp 16dp - \ No newline at end of file + diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index 18297c34b..e17179117 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -11,6 +11,7 @@ use_external_video_player use_external_audio_player autoplay_through_intent + use_exoplayer default_resolution_preference 360p diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 856225321..312e1f9a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - - + + NewPipe NewPipe Background Player NewPipe @@ -116,4 +116,24 @@ Cannot create download directory \'%1$s\' Created download directory \'%1$s\' - + + Play in background + Video + Audio + Text + Logging + Normal + Verbose + Retry + [off] + Protected content not supported on API levels below 18 + This device does not support the required DRM scheme + An unknown DRM error occurred + This device does not provide a decoder for %1$s + This device does not provide a secure decoder for %1$s + Unable to query device decoders + Unable to instantiate decoder %1$s + Permission to access storage was denied + Use ExoPlayer + Experimental + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8abacc2e8..cb0233f00 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,4 +1,19 @@ + + + + + + + - \ No newline at end of file + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index a9a0040e2..7ecf1a04f 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -33,6 +33,11 @@ android:entryValues="@array/audio_format_list" android:defaultValue="@string/default_audio_format_value"/> +