diff --git a/app/build.gradle b/app/build.gradle index c1f1fb00f..423cb518f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,4 +42,5 @@ dependencies { compile 'de.hdodenhof:circleimageview:2.0.0' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' compile 'com.github.nirhart:parallaxscroll:1.0' + compile 'com.google.android.exoplayer:exoplayer:r1.5.5' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 323da8bd6..bf10d6e05 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,6 +81,24 @@ android:theme="@style/VideoPlayerTheme" android:parentActivityName=".VideoItemDetailActivity" tools:ignore="UnusedAttribute"> + + + + + + + + + + + + + , 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/exoplayer/EventLogger.java b/app/src/main/java/org/schabi/newpipe/exoplayer/EventLogger.java new file mode 100644 index 000000000..f2de9033e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.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/exoplayer/ExoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/exoplayer/ExoPlayerActivity.java new file mode 100644 index 000000000..5b9f010f8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/exoplayer/ExoPlayerActivity.java @@ -0,0 +1,729 @@ +/* + * 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.exoplayer; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.exoplayer.NPExoPlayer.RendererBuilder; + +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.OnClickListener; +import android.view.View.OnKeyListener; +import android.view.View.OnTouchListener; +import android.view.accessibility.CaptioningManager; +import android.widget.Button; +import android.widget.MediaController; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.TextView; +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 implements SurfaceHolder.Callback, OnClickListener, + NPExoPlayer.Listener, NPExoPlayer.CaptionListener, NPExoPlayer.Id3MetadataListener, + AudioCapabilitiesReceiver.Listener { + + // 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 debugRootView; + private View shutterView; + private AspectRatioFrameLayout videoFrame; + private SurfaceView surfaceView; + private TextView debugTextView; + private TextView playerStateTextView; + private SubtitleLayout subtitleLayout; + private Button videoButton; + private Button audioButton; + private Button textButton; + private Button retryButton; + + private NPExoPlayer player; + private DebugTextViewHelper debugViewHelper; + private boolean playerNeedsPrepare; + + private long playerPosition; + private boolean enableBackgroundAudio; + + private Uri contentUri; + private int contentType; + private String contentId; + private String provider; + + private AudioCapabilitiesReceiver audioCapabilitiesReceiver; + + // 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); + debugRootView = findViewById(R.id.controls_root); + + videoFrame = (AspectRatioFrameLayout) findViewById(R.id.video_frame); + surfaceView = (SurfaceView) findViewById(R.id.surface_view); + surfaceView.getHolder().addCallback(this); + debugTextView = (TextView) findViewById(R.id.debug_text_view); + + playerStateTextView = (TextView) findViewById(R.id.player_state_view); + subtitleLayout = (SubtitleLayout) findViewById(R.id.subtitles); + + mediaController = new KeyCompatibleMediaController(this); + mediaController.setAnchorView(root); + retryButton = (Button) findViewById(R.id.retry_button); + retryButton.setOnClickListener(this); + videoButton = (Button) findViewById(R.id.video_controls); + audioButton = (Button) findViewById(R.id.audio_controls); + textButton = (Button) findViewById(R.id.text_controls); + + CookieHandler currentHandler = CookieHandler.getDefault(); + if (currentHandler != defaultCookieManager) { + CookieHandler.setDefault(defaultCookieManager); + } + + audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this, this); + 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(); + } + + // OnClickListener methods + + @Override + public void onClick(View view) { + if (view == retryButton) { + preparePlayer(true); + } + } + + // AudioCapabilitiesReceiver.Listener methods + + @Override + public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + if (player == null) { + return; + } + boolean backgrounded = player.getBackgrounded(); + boolean playWhenReady = player.getPlayWhenReady(); + releasePlayer(); + preparePlayer(playWhenReady); + player.setBackgrounded(backgrounded); + } + + // 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: + return new SmoothStreamingRendererBuilder(this, userAgent, contentUri.toString(), + new SmoothStreamingTestMediaDrmCallback()); + case Util.TYPE_DASH: + return new DashRendererBuilder(this, userAgent, contentUri.toString(), + new WidevineTestMediaDrmCallback(contentId, provider)); + case Util.TYPE_HLS: + return new HlsRendererBuilder(this, userAgent, contentUri.toString()); + case Util.TYPE_OTHER: + 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(this); + player.setCaptionListener(this); + player.setMetadataListener(this); + 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); + debugViewHelper = new DebugTextViewHelper(player, debugTextView); + playerStateTextView.setVisibility(View.GONE); + debugTextView.setVisibility(View.GONE); + debugViewHelper.start(); + } + if (playerNeedsPrepare) { + player.prepare(); + playerNeedsPrepare = false; + updateButtonVisibilities(); + } + player.setSurface(surfaceView.getHolder().getSurface()); + player.setPlayWhenReady(playWhenReady); + } + + private void releasePlayer() { + if (player != null) { + debugViewHelper.stop(); + debugViewHelper = null; + playerPosition = player.getCurrentPosition(); + player.release(); + player = null; + eventLogger.endSession(); + eventLogger = null; + } + } + + // NPExoPlayer.Listener implementation + + @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; + } + playerStateTextView.setText(text); + updateButtonVisibilities(); + } + + @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; + updateButtonVisibilities(); + 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); + } + + // User controls + + private void updateButtonVisibilities() { + retryButton.setVisibility(playerNeedsPrepare ? View.VISIBLE : View.GONE); + videoButton.setVisibility(haveTracks(NPExoPlayer.TYPE_VIDEO) ? View.VISIBLE : View.GONE); + audioButton.setVisibility(haveTracks(NPExoPlayer.TYPE_AUDIO) ? View.VISIBLE : View.GONE); + textButton.setVisibility(haveTracks(NPExoPlayer.TYPE_TEXT) ? View.VISIBLE : View.GONE); + } + + private boolean haveTracks(int type) { + return player != null && player.getTrackCount(type) > 0; + } + + public void showVideoPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + configurePopupWithTracks(popup, null, NPExoPlayer.TYPE_VIDEO); + popup.show(); + } + + public void showAudioPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + Menu menu = popup.getMenu(); + menu.add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.enable_background_audio); + final MenuItem backgroundAudioItem = menu.findItem(0); + backgroundAudioItem.setCheckable(true); + backgroundAudioItem.setChecked(enableBackgroundAudio); + OnMenuItemClickListener clickListener = new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item == backgroundAudioItem) { + enableBackgroundAudio = !item.isChecked(); + return true; + } + return false; + } + }; + configurePopupWithTracks(popup, clickListener, NPExoPlayer.TYPE_AUDIO); + popup.show(); + } + + public void showTextPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + configurePopupWithTracks(popup, null, NPExoPlayer.TYPE_TEXT); + popup.show(); + } + + public void showVerboseLogPopup(View v) { + PopupMenu popup = new PopupMenu(this, v); + Menu menu = popup.getMenu(); + menu.add(Menu.NONE, 0, Menu.NONE, R.string.logging_normal); + menu.add(Menu.NONE, 1, Menu.NONE, R.string.logging_verbose); + menu.setGroupCheckable(Menu.NONE, true, true); + menu.findItem((VerboseLogUtil.areAllTagsEnabled()) ? 1 : 0).setChecked(true); + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == 0) { + VerboseLogUtil.setEnableAllTags(false); + } else { + VerboseLogUtil.setEnableAllTags(true); + } + return true; + } + }); + popup.show(); + } + + private void configurePopupWithTracks(PopupMenu popup, + final OnMenuItemClickListener customActionClickListener, + final int trackType) { + if (player == null) { + return; + } + int trackCount = player.getTrackCount(trackType); + if (trackCount == 0) { + return; + } + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return (customActionClickListener != null + && customActionClickListener.onMenuItemClick(item)) + || onTrackItemClick(item, trackType); + } + }); + Menu menu = popup.getMenu(); + // ID_OFFSET ensures we avoid clashing with Menu.NONE (which equals 0). + menu.add(MENU_GROUP_TRACKS, NPExoPlayer.TRACK_DISABLED + ID_OFFSET, Menu.NONE, R.string.off); + for (int i = 0; i < trackCount; i++) { + menu.add(MENU_GROUP_TRACKS, i + ID_OFFSET, Menu.NONE, + buildTrackName(player.getTrackFormat(trackType, i))); + } + menu.setGroupCheckable(MENU_GROUP_TRACKS, true, true); + menu.findItem(player.getSelectedTrack(trackType) + ID_OFFSET).setChecked(true); + } + + private static String buildTrackName(MediaFormat format) { + if (format.adaptive) { + return "auto"; + } + String trackName; + if (MimeTypes.isVideo(format.mimeType)) { + trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), + buildBitrateString(format)), buildTrackIdString(format)); + } else if (MimeTypes.isAudio(format.mimeType)) { + trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildAudioPropertyString(format)), buildBitrateString(format)), + buildTrackIdString(format)); + } else { + trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), + buildBitrateString(format)), buildTrackIdString(format)); + } + return trackName.length() == 0 ? "unknown" : trackName; + } + + private static String buildResolutionString(MediaFormat format) { + return format.width == MediaFormat.NO_VALUE || format.height == MediaFormat.NO_VALUE + ? "" : format.width + "x" + format.height; + } + + private static String buildAudioPropertyString(MediaFormat format) { + return format.channelCount == MediaFormat.NO_VALUE || format.sampleRate == MediaFormat.NO_VALUE + ? "" : format.channelCount + "ch, " + format.sampleRate + "Hz"; + } + + private static String buildLanguageString(MediaFormat format) { + return TextUtils.isEmpty(format.language) || "und".equals(format.language) ? "" + : format.language; + } + + private static String buildBitrateString(MediaFormat format) { + return format.bitrate == MediaFormat.NO_VALUE ? "" + : String.format(Locale.US, "%.2fMbit", format.bitrate / 1000000f); + } + + private static String joinWithSeparator(String first, String second) { + return first.length() == 0 ? second : (second.length() == 0 ? first : first + ", " + second); + } + + private static String buildTrackIdString(MediaFormat format) { + return format.trackId == null ? "" : " (" + format.trackId + ")"; + } + + private boolean onTrackItemClick(MenuItem item, int type) { + if (player == null || item.getGroupId() != MENU_GROUP_TRACKS) { + return false; + } + player.setSelectedTrack(type, item.getItemId() - ID_OFFSET); + return true; + } + + private void toggleControlsVisibility() { + if (mediaController.isShowing()) { + mediaController.hide(); + debugRootView.setVisibility(View.GONE); + playerStateTextView.setVisibility(View.GONE); + debugTextView.setVisibility(View.GONE); + } else { + showControls(); + } + } + + private void showControls() { + mediaController.show(0); + debugRootView.setVisibility(View.VISIBLE); + playerStateTextView.setVisibility(View.VISIBLE); + debugTextView.setVisibility(View.VISIBLE); + } + + // NPExoPlayer.CaptionListener implementation + + @Override + public void onCues(List cues) { + subtitleLayout.setCues(cues); + } + + // NPExoPlayer.MetadataListener implementation + + @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())); + } + } + } + + // SurfaceHolder.Callback implementation + + @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(); + } + } + + 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/exoplayer/ExtractorRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/exoplayer/ExtractorRendererBuilder.java new file mode 100644 index 000000000..0ba716bcb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.exoplayer; + +import org.schabi.newpipe.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/exoplayer/HlsRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/exoplayer/HlsRendererBuilder.java new file mode 100644 index 000000000..8c0fae97d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.exoplayer; + +import org.schabi.newpipe.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/exoplayer/NPExoPlayer.java b/app/src/main/java/org/schabi/newpipe/exoplayer/NPExoPlayer.java new file mode 100644 index 000000000..377e2e7b8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.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/exoplayer/SmoothStreamingRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingRendererBuilder.java new file mode 100644 index 000000000..524e2d5c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.exoplayer; + + +import org.schabi.newpipe.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/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java new file mode 100644 index 000000000..3fe1db139 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java @@ -0,0 +1,64 @@ +/* + * 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.exoplayer; + +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Demo {@link StreamingDrmSessionManager} for smooth streaming test content. + */ +@TargetApi(18) +public class SmoothStreamingTestMediaDrmCallback implements MediaDrmCallback { + + private static final String PLAYREADY_TEST_DEFAULT_URI = + "http://playready.directtaps.net/pr/svc/rightsmanager.asmx"; + private static final Map KEY_REQUEST_PROPERTIES; + static { + HashMap keyRequestProperties = new HashMap<>(); + keyRequestProperties.put("Content-Type", "text/xml"); + keyRequestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); + KEY_REQUEST_PROPERTIES = keyRequestProperties; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return Util.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = PLAYREADY_TEST_DEFAULT_URI; + } + return Util.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java b/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java new file mode 100644 index 000000000..e23f3fc30 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java @@ -0,0 +1,60 @@ +/* + * 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.exoplayer; + +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} for Widevine test content. + */ +@TargetApi(18) +public class WidevineTestMediaDrmCallback implements MediaDrmCallback { + + private static final String WIDEVINE_GTS_DEFAULT_BASE_URI = + "https://proxy.uat.widevine.com/proxy"; + + private final String defaultUri; + + public WidevineTestMediaDrmCallback(String contentId, String provider) { + String params = "?video_id=" + contentId + "&provider=" + provider; + defaultUri = WIDEVINE_GTS_DEFAULT_BASE_URI + params; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return Util.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws IOException { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = defaultUri; + } + return Util.executePost(url, request.getData(), null); + } + +} diff --git a/app/src/main/res/layout/exo_player_activity.xml b/app/src/main/res/layout/exo_player_activity.xml new file mode 100644 index 000000000..041617f80 --- /dev/null +++ b/app/src/main/res/layout/exo_player_activity.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0eb887da1..3c932d561 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,8 @@ + + 16dp + 16dp @@ -36,4 +39,4 @@ 6sp 20dp - \ 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 a4be457fc..930d7ea7e 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 @@ -101,4 +101,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/exoplayer/SmoothStreamingRendererBuilder.java b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingRendererBuilder.java new file mode 100644 index 000000000..524e2d5c5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/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.exoplayer; + + +import org.schabi.newpipe.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/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java new file mode 100644 index 000000000..3fe1db139 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/exoplayer/SmoothStreamingTestMediaDrmCallback.java @@ -0,0 +1,64 @@ +/* + * 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.exoplayer; + +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.drm.StreamingDrmSessionManager; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Demo {@link StreamingDrmSessionManager} for smooth streaming test content. + */ +@TargetApi(18) +public class SmoothStreamingTestMediaDrmCallback implements MediaDrmCallback { + + private static final String PLAYREADY_TEST_DEFAULT_URI = + "http://playready.directtaps.net/pr/svc/rightsmanager.asmx"; + private static final Map KEY_REQUEST_PROPERTIES; + static { + HashMap keyRequestProperties = new HashMap<>(); + keyRequestProperties.put("Content-Type", "text/xml"); + keyRequestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); + KEY_REQUEST_PROPERTIES = keyRequestProperties; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return Util.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = PLAYREADY_TEST_DEFAULT_URI; + } + return Util.executePost(url, request.getData(), KEY_REQUEST_PROPERTIES); + } + +} diff --git a/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java b/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java new file mode 100644 index 000000000..e23f3fc30 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/exoplayer/WidevineTestMediaDrmCallback.java @@ -0,0 +1,60 @@ +/* + * 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.exoplayer; + +import com.google.android.exoplayer.drm.MediaDrmCallback; +import com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.ProvisionRequest; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} for Widevine test content. + */ +@TargetApi(18) +public class WidevineTestMediaDrmCallback implements MediaDrmCallback { + + private static final String WIDEVINE_GTS_DEFAULT_BASE_URI = + "https://proxy.uat.widevine.com/proxy"; + + private final String defaultUri; + + public WidevineTestMediaDrmCallback(String contentId, String provider) { + String params = "?video_id=" + contentId + "&provider=" + provider; + defaultUri = WIDEVINE_GTS_DEFAULT_BASE_URI + params; + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + return Util.executePost(url, null, null); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws IOException { + String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = defaultUri; + } + return Util.executePost(url, request.getData(), null); + } + +} diff --git a/app/src/main/res/layout/exo_player_activity.xml b/app/src/main/res/layout/exo_player_activity.xml new file mode 100644 index 000000000..041617f80 --- /dev/null +++ b/app/src/main/res/layout/exo_player_activity.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0eb887da1..3c932d561 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,5 +1,8 @@ + + 16dp + 16dp @@ -36,4 +39,4 @@ 6sp 20dp - \ 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 a4be457fc..930d7ea7e 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 @@ -101,4 +101,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"/> +