Added ExoPlayer support
This commit is contained in:
parent
352a883b5d
commit
3a8611ebf8
21 changed files with 2633 additions and 137 deletions
|
@ -42,4 +42,5 @@ dependencies {
|
||||||
compile 'de.hdodenhof:circleimageview:2.0.0'
|
compile 'de.hdodenhof:circleimageview:2.0.0'
|
||||||
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||||
compile 'com.github.nirhart:parallaxscroll:1.0'
|
compile 'com.github.nirhart:parallaxscroll:1.0'
|
||||||
|
compile 'com.google.android.exoplayer:exoplayer:r1.5.5'
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,24 @@
|
||||||
android:theme="@style/VideoPlayerTheme"
|
android:theme="@style/VideoPlayerTheme"
|
||||||
android:parentActivityName=".VideoItemDetailActivity"
|
android:parentActivityName=".VideoItemDetailActivity"
|
||||||
tools:ignore="UnusedAttribute">
|
tools:ignore="UnusedAttribute">
|
||||||
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".exoplayer.ExoPlayerActivity"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:theme="@style/PlayerTheme">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.schabi.newpipe.exoplayer.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="content" />
|
||||||
|
<data android:scheme="asset" />
|
||||||
|
<data android:scheme="file" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
android:name=".BackgroundPlayer"
|
android:name=".BackgroundPlayer"
|
||||||
|
|
|
@ -38,6 +38,7 @@ import android.widget.Toast;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||||
|
@ -54,6 +55,7 @@ import org.schabi.newpipe.crawler.VideoPreviewInfo;
|
||||||
import org.schabi.newpipe.crawler.StreamingService;
|
import org.schabi.newpipe.crawler.StreamingService;
|
||||||
import org.schabi.newpipe.crawler.VideoInfo;
|
import org.schabi.newpipe.crawler.VideoInfo;
|
||||||
import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor;
|
import org.schabi.newpipe.crawler.services.youtube.YoutubeStreamExtractor;
|
||||||
|
import org.schabi.newpipe.exoplayer.ExoPlayerActivity;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -442,7 +444,7 @@ public class VideoItemDetailFragment extends Fragment {
|
||||||
info.audio_streams.get(getPreferredAudioStreamId(info));
|
info.audio_streams.get(getPreferredAudioStreamId(info));
|
||||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 18) {
|
||||||
//internal music player: explicit intent
|
//internal music player: explicit intent
|
||||||
if (!BackgroundPlayer.isRunning && videoThumbnail != null) {
|
if (!BackgroundPlayer.isRunning && videoThumbnail != null) {
|
||||||
ActivityCommunicator.getCommunicator()
|
ActivityCommunicator.getCommunicator()
|
||||||
.backgroundPlayerThumbnail = videoThumbnail;
|
.backgroundPlayerThumbnail = videoThumbnail;
|
||||||
intent = new Intent(activity, BackgroundPlayer.class);
|
intent = new Intent(activity, BackgroundPlayer.class);
|
||||||
|
@ -719,13 +721,21 @@ public class VideoItemDetailFragment extends Fragment {
|
||||||
builder.create().show();
|
builder.create().show();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Internal Player
|
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
Intent intent = new Intent(activity, PlayVideoActivity.class);
|
.getBoolean(activity.getString(R.string.use_exoplayer_key), false)) {
|
||||||
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title);
|
Intent mpdIntent = new Intent(activity, ExoPlayerActivity.class)
|
||||||
intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url);
|
.setData(Uri.parse(info.dashMpdUrl))
|
||||||
intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url);
|
.putExtra(ExoPlayerActivity.CONTENT_TYPE_EXTRA, Util.TYPE_DASH);
|
||||||
intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
startActivity(mpdIntent);
|
||||||
activity.startActivity(intent); //also HERE !!!
|
} else {
|
||||||
|
// Internal Player
|
||||||
|
Intent intent = new Intent(activity, PlayVideoActivity.class);
|
||||||
|
intent.putExtra(PlayVideoActivity.VIDEO_TITLE, info.title);
|
||||||
|
intent.putExtra(PlayVideoActivity.STREAM_URL, selectedVideoStream.url);
|
||||||
|
intent.putExtra(PlayVideoActivity.VIDEO_URL, info.webpage_url);
|
||||||
|
intent.putExtra(PlayVideoActivity.START_POSITION, info.start_position);
|
||||||
|
activity.startActivity(intent); //also HERE !!!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------
|
// --------------------------------------------
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.schabi.newpipe.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.dash.DashChunkSource;
|
||||||
|
import com.google.android.exoplayer.dash.DefaultDashTrackSelector;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionParser;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.Period;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.UtcTimingElement;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.UtcTimingElementResolver.UtcTimingCallback;
|
||||||
|
import com.google.android.exoplayer.drm.MediaDrmCallback;
|
||||||
|
import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
|
||||||
|
import com.google.android.exoplayer.drm.UnsupportedDrmException;
|
||||||
|
import com.google.android.exoplayer.text.TextTrackRenderer;
|
||||||
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.DefaultAllocator;
|
||||||
|
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
|
||||||
|
import com.google.android.exoplayer.upstream.DefaultUriDataSource;
|
||||||
|
import com.google.android.exoplayer.upstream.UriDataSource;
|
||||||
|
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.MediaCodec;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RendererBuilder} for DASH.
|
||||||
|
*/
|
||||||
|
public class DashRendererBuilder implements RendererBuilder {
|
||||||
|
|
||||||
|
private static final String TAG = "DashRendererBuilder";
|
||||||
|
|
||||||
|
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
|
||||||
|
private static final int VIDEO_BUFFER_SEGMENTS = 200;
|
||||||
|
private static final int AUDIO_BUFFER_SEGMENTS = 54;
|
||||||
|
private static final int TEXT_BUFFER_SEGMENTS = 2;
|
||||||
|
private static final int LIVE_EDGE_LATENCY_MS = 30000;
|
||||||
|
|
||||||
|
private static final int SECURITY_LEVEL_UNKNOWN = -1;
|
||||||
|
private static final int SECURITY_LEVEL_1 = 1;
|
||||||
|
private static final int SECURITY_LEVEL_3 = 3;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final String userAgent;
|
||||||
|
private final String url;
|
||||||
|
private final MediaDrmCallback drmCallback;
|
||||||
|
|
||||||
|
private AsyncRendererBuilder currentAsyncBuilder;
|
||||||
|
|
||||||
|
public DashRendererBuilder(Context context, String userAgent, String url,
|
||||||
|
MediaDrmCallback drmCallback) {
|
||||||
|
this.context = context;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.url = url;
|
||||||
|
this.drmCallback = drmCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void buildRenderers(NPExoPlayer player) {
|
||||||
|
currentAsyncBuilder = new AsyncRendererBuilder(context, userAgent, url, drmCallback, player);
|
||||||
|
currentAsyncBuilder.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
if (currentAsyncBuilder != null) {
|
||||||
|
currentAsyncBuilder.cancel();
|
||||||
|
currentAsyncBuilder = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AsyncRendererBuilder
|
||||||
|
implements ManifestFetcher.ManifestCallback<MediaPresentationDescription>, UtcTimingCallback {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final String userAgent;
|
||||||
|
private final MediaDrmCallback drmCallback;
|
||||||
|
private final NPExoPlayer player;
|
||||||
|
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
|
||||||
|
private final UriDataSource manifestDataSource;
|
||||||
|
|
||||||
|
private boolean canceled;
|
||||||
|
private MediaPresentationDescription manifest;
|
||||||
|
private long elapsedRealtimeOffset;
|
||||||
|
|
||||||
|
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||||
|
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||||
|
this.context = context;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.drmCallback = drmCallback;
|
||||||
|
this.player = player;
|
||||||
|
MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser();
|
||||||
|
manifestDataSource = new DefaultUriDataSource(context, userAgent);
|
||||||
|
manifestFetcher = new ManifestFetcher<>(url, manifestDataSource, parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
canceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifest(MediaPresentationDescription manifest) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manifest = manifest;
|
||||||
|
if (manifest.dynamic && manifest.utcTiming != null) {
|
||||||
|
UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming,
|
||||||
|
manifestFetcher.getManifestLoadCompleteTimestamp(), this);
|
||||||
|
} else {
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifestError(IOException e) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.onRenderersError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimestampResolved(UtcTimingElement utcTiming, long elapsedRealtimeOffset) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimestampError(UtcTimingElement utcTiming, IOException e) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(TAG, "Failed to resolve UtcTiming element [" + utcTiming + "]", e);
|
||||||
|
// Be optimistic and continue in the hope that the device clock is correct.
|
||||||
|
buildRenderers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildRenderers() {
|
||||||
|
Period period = manifest.getPeriod(0);
|
||||||
|
Handler mainHandler = player.getMainHandler();
|
||||||
|
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||||
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||||
|
|
||||||
|
boolean hasContentProtection = false;
|
||||||
|
for (int i = 0; i < period.adaptationSets.size(); i++) {
|
||||||
|
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||||
|
if (adaptationSet.type != AdaptationSet.TYPE_UNKNOWN) {
|
||||||
|
hasContentProtection |= adaptationSet.hasContentProtection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check drm support if necessary.
|
||||||
|
boolean filterHdContent = false;
|
||||||
|
StreamingDrmSessionManager drmSessionManager = null;
|
||||||
|
if (hasContentProtection) {
|
||||||
|
if (Util.SDK_INT < 18) {
|
||||||
|
player.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
drmSessionManager = StreamingDrmSessionManager.newWidevineInstance(
|
||||||
|
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||||
|
filterHdContent = getWidevineSecurityLevel(drmSessionManager) != SECURITY_LEVEL_1;
|
||||||
|
} catch (UnsupportedDrmException e) {
|
||||||
|
player.onRenderersError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the video renderer.
|
||||||
|
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
|
||||||
|
DefaultDashTrackSelector.newVideoInstance(context, true, filterHdContent),
|
||||||
|
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS,
|
||||||
|
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||||
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_VIDEO);
|
||||||
|
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||||
|
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||||
|
drmSessionManager, true, mainHandler, player, 50);
|
||||||
|
|
||||||
|
// Build the audio renderer.
|
||||||
|
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource audioChunkSource = new DashChunkSource(manifestFetcher,
|
||||||
|
DefaultDashTrackSelector.newAudioInstance(), audioDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||||
|
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_AUDIO);
|
||||||
|
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||||
|
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_AUDIO);
|
||||||
|
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||||
|
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||||
|
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||||
|
|
||||||
|
// Build the text renderer.
|
||||||
|
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource textChunkSource = new DashChunkSource(manifestFetcher,
|
||||||
|
DefaultDashTrackSelector.newTextInstance(), textDataSource, null, LIVE_EDGE_LATENCY_MS,
|
||||||
|
elapsedRealtimeOffset, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||||
|
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
|
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_TEXT);
|
||||||
|
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||||
|
mainHandler.getLooper());
|
||||||
|
|
||||||
|
// Invoke the callback.
|
||||||
|
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||||
|
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||||
|
player.onRenderers(renderers, bandwidthMeter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) {
|
||||||
|
String securityLevelProperty = sessionManager.getPropertyString("securityLevel");
|
||||||
|
return securityLevelProperty.equals("L1") ? SECURITY_LEVEL_1 : securityLevelProperty
|
||||||
|
.equals("L3") ? SECURITY_LEVEL_3 : SECURITY_LEVEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
214
app/src/main/java/org/schabi/newpipe/exoplayer/EventLogger.java
Normal file
214
app/src/main/java/org/schabi/newpipe/exoplayer/EventLogger.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<Cue> cues) {
|
||||||
|
subtitleLayout.setCues(cues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPExoPlayer.MetadataListener implementation
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onId3Metadata(Map<String, Object> metadata) {
|
||||||
|
for (Map.Entry<String, Object> entry : metadata.entrySet()) {
|
||||||
|
if (TxxxMetadata.TYPE.equals(entry.getKey())) {
|
||||||
|
TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue();
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s",
|
||||||
|
TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value));
|
||||||
|
} else if (PrivMetadata.TYPE.equals(entry.getKey())) {
|
||||||
|
PrivMetadata privMetadata = (PrivMetadata) entry.getValue();
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s",
|
||||||
|
PrivMetadata.TYPE, privMetadata.owner));
|
||||||
|
} else if (GeobMetadata.TYPE.equals(entry.getKey())) {
|
||||||
|
GeobMetadata geobMetadata = (GeobMetadata) entry.getValue();
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
|
||||||
|
GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename,
|
||||||
|
geobMetadata.description));
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<HlsPlaylist> {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final String userAgent;
|
||||||
|
private final String url;
|
||||||
|
private final NPExoPlayer player;
|
||||||
|
private final ManifestFetcher<HlsPlaylist> playlistFetcher;
|
||||||
|
|
||||||
|
private boolean canceled;
|
||||||
|
|
||||||
|
public AsyncRendererBuilder(Context context, String userAgent, String url, NPExoPlayer player) {
|
||||||
|
this.context = context;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.url = url;
|
||||||
|
this.player = player;
|
||||||
|
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||||
|
playlistFetcher = new ManifestFetcher<>(url, new DefaultUriDataSource(context, userAgent),
|
||||||
|
parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
canceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifestError(IOException e) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.onRenderersError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifest(HlsPlaylist manifest) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler mainHandler = player.getMainHandler();
|
||||||
|
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||||
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||||
|
PtsTimestampAdjusterProvider timestampAdjusterProvider = new PtsTimestampAdjusterProvider();
|
||||||
|
|
||||||
|
// Build the video/audio/metadata renderers.
|
||||||
|
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
HlsChunkSource chunkSource = new HlsChunkSource(true /* isMaster */, dataSource, url,
|
||||||
|
manifest, DefaultHlsTrackSelector.newDefaultInstance(context), bandwidthMeter,
|
||||||
|
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||||
|
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, loadControl,
|
||||||
|
MAIN_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_VIDEO);
|
||||||
|
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context,
|
||||||
|
sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
|
||||||
|
5000, mainHandler, player, 50);
|
||||||
|
MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource,
|
||||||
|
MediaCodecSelector.DEFAULT, null, true, player.getMainHandler(), player,
|
||||||
|
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||||
|
MetadataTrackRenderer<Map<String, Object>> id3Renderer = new MetadataTrackRenderer<>(
|
||||||
|
sampleSource, new Id3Parser(), player, mainHandler.getLooper());
|
||||||
|
|
||||||
|
// Build the text renderer, preferring Webvtt where available.
|
||||||
|
boolean preferWebvtt = false;
|
||||||
|
if (manifest instanceof HlsMasterPlaylist) {
|
||||||
|
preferWebvtt = !((HlsMasterPlaylist) manifest).subtitles.isEmpty();
|
||||||
|
}
|
||||||
|
TrackRenderer textRenderer;
|
||||||
|
if (preferWebvtt) {
|
||||||
|
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
HlsChunkSource textChunkSource = new HlsChunkSource(false /* isMaster */, textDataSource,
|
||||||
|
url, manifest, DefaultHlsTrackSelector.newVttInstance(), bandwidthMeter,
|
||||||
|
timestampAdjusterProvider, HlsChunkSource.ADAPTIVE_MODE_SPLICE);
|
||||||
|
HlsSampleSource textSampleSource = new HlsSampleSource(textChunkSource, loadControl,
|
||||||
|
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player, NPExoPlayer.TYPE_TEXT);
|
||||||
|
textRenderer = new TextTrackRenderer(textSampleSource, player, mainHandler.getLooper());
|
||||||
|
} else {
|
||||||
|
textRenderer = new Eia608TrackRenderer(sampleSource, player, mainHandler.getLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||||
|
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_METADATA] = id3Renderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||||
|
player.onRenderers(renderers, bandwidthMeter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
599
app/src/main/java/org/schabi/newpipe/exoplayer/NPExoPlayer.java
Normal file
599
app/src/main/java/org/schabi/newpipe/exoplayer/NPExoPlayer.java
Normal file
|
@ -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<Map<String, Object>>, DebugTextViewHelper.Provider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds renderers for the player.
|
||||||
|
*/
|
||||||
|
public interface RendererBuilder {
|
||||||
|
/**
|
||||||
|
* Builds renderers for playback.
|
||||||
|
*
|
||||||
|
* @param player The player for which renderers are being built. {@link NPExoPlayer#onRenderers}
|
||||||
|
* should be invoked once the renderers have been built. If building fails,
|
||||||
|
* {@link NPExoPlayer#onRenderersError} should be invoked.
|
||||||
|
*/
|
||||||
|
void buildRenderers(NPExoPlayer player);
|
||||||
|
/**
|
||||||
|
* Cancels the current build operation, if there is one. Else does nothing.
|
||||||
|
* <p>
|
||||||
|
* A canceled build operation must not invoke {@link NPExoPlayer#onRenderers} or
|
||||||
|
* {@link NPExoPlayer#onRenderersError} on the player, which may have been released.
|
||||||
|
*/
|
||||||
|
void cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener for core events.
|
||||||
|
*/
|
||||||
|
public interface Listener {
|
||||||
|
void onStateChanged(boolean playWhenReady, int playbackState);
|
||||||
|
void onError(Exception e);
|
||||||
|
void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||||
|
float pixelWidthHeightRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener for internal errors.
|
||||||
|
* <p>
|
||||||
|
* These errors are not visible to the user, and hence this listener is provided for
|
||||||
|
* informational purposes only. Note however that an internal error may cause a fatal
|
||||||
|
* error if the player fails to recover. If this happens, {@link Listener#onError(Exception)}
|
||||||
|
* will be invoked.
|
||||||
|
*/
|
||||||
|
public interface InternalErrorListener {
|
||||||
|
void onRendererInitializationError(Exception e);
|
||||||
|
void onAudioTrackInitializationError(AudioTrack.InitializationException e);
|
||||||
|
void onAudioTrackWriteError(AudioTrack.WriteException e);
|
||||||
|
void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
|
||||||
|
void onDecoderInitializationError(DecoderInitializationException e);
|
||||||
|
void onCryptoError(CryptoException e);
|
||||||
|
void onLoadError(int sourceId, IOException e);
|
||||||
|
void onDrmSessionManagerError(Exception e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener for debugging information.
|
||||||
|
*/
|
||||||
|
public interface InfoListener {
|
||||||
|
void onVideoFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||||
|
void onAudioFormatEnabled(Format format, int trigger, long mediaTimeMs);
|
||||||
|
void onDroppedFrames(int count, long elapsed);
|
||||||
|
void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate);
|
||||||
|
void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||||
|
long mediaStartTimeMs, long mediaEndTimeMs);
|
||||||
|
void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||||
|
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
|
||||||
|
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||||
|
long initializationDurationMs);
|
||||||
|
void onAvailableRangeChanged(int sourceId, TimeRange availableRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener for receiving notifications of timed text.
|
||||||
|
*/
|
||||||
|
public interface CaptionListener {
|
||||||
|
void onCues(List<Cue> cues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener for receiving ID3 metadata parsed from the media stream.
|
||||||
|
*/
|
||||||
|
public interface Id3MetadataListener {
|
||||||
|
void onId3Metadata(Map<String, Object> metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants pulled into this class for convenience.
|
||||||
|
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
|
||||||
|
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
|
||||||
|
public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING;
|
||||||
|
public static final int STATE_READY = ExoPlayer.STATE_READY;
|
||||||
|
public static final int STATE_ENDED = ExoPlayer.STATE_ENDED;
|
||||||
|
public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED;
|
||||||
|
public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT;
|
||||||
|
|
||||||
|
public static final int RENDERER_COUNT = 4;
|
||||||
|
public static final int TYPE_VIDEO = 0;
|
||||||
|
public static final int TYPE_AUDIO = 1;
|
||||||
|
public static final int TYPE_TEXT = 2;
|
||||||
|
public static final int TYPE_METADATA = 3;
|
||||||
|
|
||||||
|
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
|
||||||
|
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
|
||||||
|
private static final int RENDERER_BUILDING_STATE_BUILT = 3;
|
||||||
|
|
||||||
|
private final RendererBuilder rendererBuilder;
|
||||||
|
private final ExoPlayer player;
|
||||||
|
private final PlayerControl playerControl;
|
||||||
|
private final Handler mainHandler;
|
||||||
|
private final CopyOnWriteArrayList<Listener> listeners;
|
||||||
|
|
||||||
|
private int rendererBuildingState;
|
||||||
|
private int lastReportedPlaybackState;
|
||||||
|
private boolean lastReportedPlayWhenReady;
|
||||||
|
|
||||||
|
private Surface surface;
|
||||||
|
private TrackRenderer videoRenderer;
|
||||||
|
private CodecCounters codecCounters;
|
||||||
|
private Format videoFormat;
|
||||||
|
private int videoTrackToRestore;
|
||||||
|
|
||||||
|
private BandwidthMeter bandwidthMeter;
|
||||||
|
private boolean backgrounded;
|
||||||
|
|
||||||
|
private CaptionListener captionListener;
|
||||||
|
private Id3MetadataListener id3MetadataListener;
|
||||||
|
private InternalErrorListener internalErrorListener;
|
||||||
|
private InfoListener infoListener;
|
||||||
|
|
||||||
|
public NPExoPlayer(RendererBuilder rendererBuilder) {
|
||||||
|
this.rendererBuilder = rendererBuilder;
|
||||||
|
player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000);
|
||||||
|
player.addListener(this);
|
||||||
|
playerControl = new PlayerControl(player);
|
||||||
|
mainHandler = new Handler();
|
||||||
|
listeners = new CopyOnWriteArrayList<>();
|
||||||
|
lastReportedPlaybackState = STATE_IDLE;
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||||
|
// Disable text initially.
|
||||||
|
player.setSelectedTrack(TYPE_TEXT, TRACK_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerControl getPlayerControl() {
|
||||||
|
return playerControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Listener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeListener(Listener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInternalErrorListener(InternalErrorListener listener) {
|
||||||
|
internalErrorListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfoListener(InfoListener listener) {
|
||||||
|
infoListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCaptionListener(CaptionListener listener) {
|
||||||
|
captionListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetadataListener(Id3MetadataListener listener) {
|
||||||
|
id3MetadataListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSurface(Surface surface) {
|
||||||
|
this.surface = surface;
|
||||||
|
pushSurface(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Surface getSurface() {
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void blockingClearSurface() {
|
||||||
|
surface = null;
|
||||||
|
pushSurface(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTrackCount(int type) {
|
||||||
|
return player.getTrackCount(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaFormat getTrackFormat(int type, int index) {
|
||||||
|
return player.getTrackFormat(type, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectedTrack(int type) {
|
||||||
|
return player.getSelectedTrack(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedTrack(int type, int index) {
|
||||||
|
player.setSelectedTrack(type, index);
|
||||||
|
if (type == TYPE_TEXT && index < 0 && captionListener != null) {
|
||||||
|
captionListener.onCues(Collections.<Cue>emptyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBackgrounded() {
|
||||||
|
return backgrounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBackgrounded(boolean backgrounded) {
|
||||||
|
if (this.backgrounded == backgrounded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.backgrounded = backgrounded;
|
||||||
|
if (backgrounded) {
|
||||||
|
videoTrackToRestore = getSelectedTrack(TYPE_VIDEO);
|
||||||
|
setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED);
|
||||||
|
blockingClearSurface();
|
||||||
|
} else {
|
||||||
|
setSelectedTrack(TYPE_VIDEO, videoTrackToRestore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepare() {
|
||||||
|
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
|
||||||
|
player.stop();
|
||||||
|
}
|
||||||
|
rendererBuilder.cancel();
|
||||||
|
videoFormat = null;
|
||||||
|
videoRenderer = null;
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
|
||||||
|
maybeReportPlayerState();
|
||||||
|
rendererBuilder.buildRenderers(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked with the results from a {@link RendererBuilder}.
|
||||||
|
*
|
||||||
|
* @param renderers Renderers indexed by {@link NPExoPlayer} TYPE_* constants. An individual
|
||||||
|
* element may be null if there do not exist tracks of the corresponding type.
|
||||||
|
* @param bandwidthMeter Provides an estimate of the currently available bandwidth. May be null.
|
||||||
|
*/
|
||||||
|
/* package */ void onRenderers(TrackRenderer[] renderers, BandwidthMeter bandwidthMeter) {
|
||||||
|
for (int i = 0; i < RENDERER_COUNT; i++) {
|
||||||
|
if (renderers[i] == null) {
|
||||||
|
// Convert a null renderer to a dummy renderer.
|
||||||
|
renderers[i] = new DummyTrackRenderer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Complete preparation.
|
||||||
|
this.videoRenderer = renderers[TYPE_VIDEO];
|
||||||
|
this.codecCounters = videoRenderer instanceof MediaCodecTrackRenderer
|
||||||
|
? ((MediaCodecTrackRenderer) videoRenderer).codecCounters
|
||||||
|
: renderers[TYPE_AUDIO] instanceof MediaCodecTrackRenderer
|
||||||
|
? ((MediaCodecTrackRenderer) renderers[TYPE_AUDIO]).codecCounters : null;
|
||||||
|
this.bandwidthMeter = bandwidthMeter;
|
||||||
|
pushSurface(false);
|
||||||
|
player.prepare(renderers);
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_BUILT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked if a {@link RendererBuilder} encounters an error.
|
||||||
|
*
|
||||||
|
* @param e Describes the error.
|
||||||
|
*/
|
||||||
|
/* package */ void onRenderersError(Exception e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onRendererInitializationError(e);
|
||||||
|
}
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onError(e);
|
||||||
|
}
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||||
|
maybeReportPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayWhenReady(boolean playWhenReady) {
|
||||||
|
player.setPlayWhenReady(playWhenReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seekTo(long positionMs) {
|
||||||
|
player.seekTo(positionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
rendererBuilder.cancel();
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||||
|
surface = null;
|
||||||
|
player.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPlaybackState() {
|
||||||
|
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) {
|
||||||
|
return STATE_PREPARING;
|
||||||
|
}
|
||||||
|
int playerState = player.getPlaybackState();
|
||||||
|
if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) {
|
||||||
|
// This is an edge case where the renderers are built, but are still being passed to the
|
||||||
|
// player's playback thread.
|
||||||
|
return STATE_PREPARING;
|
||||||
|
}
|
||||||
|
return playerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Format getFormat() {
|
||||||
|
return videoFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BandwidthMeter getBandwidthMeter() {
|
||||||
|
return bandwidthMeter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CodecCounters getCodecCounters() {
|
||||||
|
return codecCounters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCurrentPosition() {
|
||||||
|
return player.getCurrentPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDuration() {
|
||||||
|
return player.getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBufferedPercentage() {
|
||||||
|
return player.getBufferedPercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPlayWhenReady() {
|
||||||
|
return player.getPlayWhenReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ Looper getPlaybackLooper() {
|
||||||
|
return player.getPlaybackLooper();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* package */ Handler getMainHandler() {
|
||||||
|
return mainHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, int state) {
|
||||||
|
maybeReportPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(ExoPlaybackException exception) {
|
||||||
|
rendererBuildingState = RENDERER_BUILDING_STATE_IDLE;
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
|
||||||
|
float pixelWidthHeightRatio) {
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDroppedFrames(int count, long elapsed) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onDroppedFrames(count, elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onBandwidthSample(elapsedMs, bytes, bitrateEstimate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownstreamFormatChanged(int sourceId, Format format, int trigger,
|
||||||
|
long mediaTimeMs) {
|
||||||
|
if (infoListener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sourceId == TYPE_VIDEO) {
|
||||||
|
videoFormat = format;
|
||||||
|
infoListener.onVideoFormatEnabled(format, trigger, mediaTimeMs);
|
||||||
|
} else if (sourceId == TYPE_AUDIO) {
|
||||||
|
infoListener.onAudioFormatEnabled(format, trigger, mediaTimeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrmKeysLoaded() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrmSessionManagerError(Exception e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onDrmSessionManagerError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDecoderInitializationError(DecoderInitializationException e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onDecoderInitializationError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioTrackInitializationError(AudioTrack.InitializationException e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onAudioTrackInitializationError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioTrackWriteError(AudioTrack.WriteException e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onAudioTrackWriteError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCryptoError(CryptoException e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onCryptoError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
||||||
|
long initializationDurationMs) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onDecoderInitialized(decoderName, elapsedRealtimeMs, initializationDurationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadError(int sourceId, IOException e) {
|
||||||
|
if (internalErrorListener != null) {
|
||||||
|
internalErrorListener.onLoadError(sourceId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCues(List<Cue> cues) {
|
||||||
|
if (captionListener != null && getSelectedTrack(TYPE_TEXT) != TRACK_DISABLED) {
|
||||||
|
captionListener.onCues(cues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMetadata(Map<String, Object> metadata) {
|
||||||
|
if (id3MetadataListener != null && getSelectedTrack(TYPE_METADATA) != TRACK_DISABLED) {
|
||||||
|
id3MetadataListener.onId3Metadata(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAvailableRangeChanged(int sourceId, TimeRange availableRange) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onAvailableRangeChanged(sourceId, availableRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayWhenReadyCommitted() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDrawnToSurface(Surface surface) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format,
|
||||||
|
long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onLoadStarted(sourceId, length, type, trigger, format, mediaStartTimeMs,
|
||||||
|
mediaEndTimeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format,
|
||||||
|
long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
|
||||||
|
if (infoListener != null) {
|
||||||
|
infoListener.onLoadCompleted(sourceId, bytesLoaded, type, trigger, format, mediaStartTimeMs,
|
||||||
|
mediaEndTimeMs, elapsedRealtimeMs, loadDurationMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadCanceled(int sourceId, long bytesLoaded) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs) {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeReportPlayerState() {
|
||||||
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
|
int playbackState = getPlaybackState();
|
||||||
|
if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) {
|
||||||
|
for (Listener listener : listeners) {
|
||||||
|
listener.onStateChanged(playWhenReady, playbackState);
|
||||||
|
}
|
||||||
|
lastReportedPlayWhenReady = playWhenReady;
|
||||||
|
lastReportedPlaybackState = playbackState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushSurface(boolean blockForSurfacePush) {
|
||||||
|
if (videoRenderer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockForSurfacePush) {
|
||||||
|
player.blockingSendMessage(
|
||||||
|
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||||
|
} else {
|
||||||
|
player.sendMessage(
|
||||||
|
videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<SmoothStreamingManifest> {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final String userAgent;
|
||||||
|
private final MediaDrmCallback drmCallback;
|
||||||
|
private final NPExoPlayer player;
|
||||||
|
private final ManifestFetcher<SmoothStreamingManifest> manifestFetcher;
|
||||||
|
|
||||||
|
private boolean canceled;
|
||||||
|
|
||||||
|
public AsyncRendererBuilder(Context context, String userAgent, String url,
|
||||||
|
MediaDrmCallback drmCallback, NPExoPlayer player) {
|
||||||
|
this.context = context;
|
||||||
|
this.userAgent = userAgent;
|
||||||
|
this.drmCallback = drmCallback;
|
||||||
|
this.player = player;
|
||||||
|
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||||
|
manifestFetcher = new ManifestFetcher<>(url, new DefaultHttpDataSource(userAgent, null),
|
||||||
|
parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
canceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifestError(IOException exception) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.onRenderersError(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleManifest(SmoothStreamingManifest manifest) {
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler mainHandler = player.getMainHandler();
|
||||||
|
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||||
|
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||||
|
|
||||||
|
// Check drm support if necessary.
|
||||||
|
DrmSessionManager drmSessionManager = null;
|
||||||
|
if (manifest.protectionElement != null) {
|
||||||
|
if (Util.SDK_INT < 18) {
|
||||||
|
player.onRenderersError(
|
||||||
|
new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
drmSessionManager = new StreamingDrmSessionManager(manifest.protectionElement.uuid,
|
||||||
|
player.getPlaybackLooper(), drmCallback, null, player.getMainHandler(), player);
|
||||||
|
} catch (UnsupportedDrmException e) {
|
||||||
|
player.onRenderersError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the video renderer.
|
||||||
|
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||||
|
DefaultSmoothStreamingTrackSelector.newVideoInstance(context, true, false),
|
||||||
|
videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS);
|
||||||
|
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
|
||||||
|
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_VIDEO);
|
||||||
|
TrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(context, videoSampleSource,
|
||||||
|
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000,
|
||||||
|
drmSessionManager, true, mainHandler, player, 50);
|
||||||
|
|
||||||
|
// Build the audio renderer.
|
||||||
|
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource audioChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||||
|
DefaultSmoothStreamingTrackSelector.newAudioInstance(),
|
||||||
|
audioDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||||
|
ChunkSampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||||
|
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_AUDIO);
|
||||||
|
TrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource,
|
||||||
|
MediaCodecSelector.DEFAULT, drmSessionManager, true, mainHandler, player,
|
||||||
|
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
|
||||||
|
|
||||||
|
// Build the text renderer.
|
||||||
|
DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||||
|
ChunkSource textChunkSource = new SmoothStreamingChunkSource(manifestFetcher,
|
||||||
|
DefaultSmoothStreamingTrackSelector.newTextInstance(),
|
||||||
|
textDataSource, null, LIVE_EDGE_LATENCY_MS);
|
||||||
|
ChunkSampleSource textSampleSource = new ChunkSampleSource(textChunkSource, loadControl,
|
||||||
|
TEXT_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, mainHandler, player,
|
||||||
|
NPExoPlayer.TYPE_TEXT);
|
||||||
|
TrackRenderer textRenderer = new TextTrackRenderer(textSampleSource, player,
|
||||||
|
mainHandler.getLooper());
|
||||||
|
|
||||||
|
// Invoke the callback.
|
||||||
|
TrackRenderer[] renderers = new TrackRenderer[NPExoPlayer.RENDERER_COUNT];
|
||||||
|
renderers[NPExoPlayer.TYPE_VIDEO] = videoRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_AUDIO] = audioRenderer;
|
||||||
|
renderers[NPExoPlayer.TYPE_TEXT] = textRenderer;
|
||||||
|
player.onRenderers(renderers, bandwidthMeter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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<String, String> KEY_REQUEST_PROPERTIES;
|
||||||
|
static {
|
||||||
|
HashMap<String, String> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
116
app/src/main/res/layout/exo_player_activity.xml
Normal file
116
app/src/main/res/layout/exo_player_activity.xml
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:focusable="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:keepScreenOn="true">
|
||||||
|
|
||||||
|
<com.google.android.exoplayer.AspectRatioFrameLayout android:id="@+id/video_frame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
<SurfaceView android:id="@+id/surface_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
|
<View android:id="@+id/shutter"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@android:color/black"/>
|
||||||
|
|
||||||
|
<com.google.android.exoplayer.text.SubtitleLayout android:id="@+id/subtitles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
</com.google.android.exoplayer.AspectRatioFrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="#88000000"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView android:id="@+id/player_state_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="4dp"
|
||||||
|
android:paddingRight="4dp"
|
||||||
|
android:textSize="10sp"
|
||||||
|
tools:ignore="SmallSp"/>
|
||||||
|
|
||||||
|
<TextView android:id="@+id/debug_text_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="4dp"
|
||||||
|
android:paddingRight="4dp"
|
||||||
|
android:textSize="10sp"
|
||||||
|
tools:ignore="SmallSp"/>
|
||||||
|
|
||||||
|
<LinearLayout android:id="@+id/controls_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<Button android:id="@+id/video_controls"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/video"
|
||||||
|
style="@style/ExoPlayerButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:onClick="showVideoPopup"/>
|
||||||
|
|
||||||
|
<Button android:id="@+id/audio_controls"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/audio"
|
||||||
|
style="@style/ExoPlayerButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:onClick="showAudioPopup"/>
|
||||||
|
|
||||||
|
<Button android:id="@+id/text_controls"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/text"
|
||||||
|
style="@style/ExoPlayerButton"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:onClick="showTextPopup"/>
|
||||||
|
|
||||||
|
<Button android:id="@+id/verbose_log_controls"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/logging"
|
||||||
|
style="@style/ExoPlayerButton"
|
||||||
|
android:onClick="showVerboseLogPopup"/>
|
||||||
|
|
||||||
|
<Button android:id="@+id/retry_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/retry"
|
||||||
|
android:visibility="gone"
|
||||||
|
style="@style/ExoPlayerButton"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -1,75 +0,0 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<resources><string name="upload_date_text">Publikováno %1$s</string>
|
|
||||||
<string name="no_player_found">Žádný přehrávač nenalezen. Nainstalovat VLC?</string>
|
|
||||||
<string name="install">Instalovat</string>
|
|
||||||
<string name="cancel">Zrušit</string>
|
|
||||||
<string name="open_in_browser">Otevřít v prohlížeči</string>
|
|
||||||
<string name="share">Sdílet</string>
|
|
||||||
<string name="loading">Načítám</string>
|
|
||||||
<string name="download">Stáhnout</string>
|
|
||||||
<string name="search">Vyhledat</string>
|
|
||||||
<string name="settings">Nastavení</string>
|
|
||||||
<string name="did_you_mean">"Měli jste na mysli: "</string>
|
|
||||||
<string name="search_page">"Vyhledat stránku: "</string>
|
|
||||||
<string name="share_dialog_title">Sdílet s:</string>
|
|
||||||
<string name="choose_browser">Vybrat prohlížeč:</string>
|
|
||||||
<string name="screen_rotation">otočení</string>
|
|
||||||
<string name="settings_activity_title">Nastavení</string>
|
|
||||||
<string name="use_external_video_player_title">Použít externí video přehrávač</string>
|
|
||||||
<string name="use_external_audio_player_title">Použít externí audio přehrávač</string>
|
|
||||||
|
|
||||||
<string name="download_path_audio_summary">Cesta, kde se uloží audio po stažení.</string>
|
|
||||||
<string name="download_path_audio_dialog_title">Zadejte umístění pro uložení audio souborů.</string>
|
|
||||||
|
|
||||||
<string name="download_path_audio_title">Umístění pro stažené audio</string>
|
|
||||||
<string name="autoplay_through_intent_title">Automatické přehrávání skrze Intent</string>
|
|
||||||
<string name="autoplay_through_intent_summary">Automaticky přehrávat video, jestliže je volané z jiné aplikace.</string>
|
|
||||||
<string name="default_resolution_title">Výchozí rozlišení</string>
|
|
||||||
<string name="play_with_kodi_title">Přehrát s Kodi</string>
|
|
||||||
<string name="kore_not_found">Aplikace Kore nenalezena. Nainstalovat Kore?</string>
|
|
||||||
<string name="view_count_text">%1$s zhlédnutí</string>
|
|
||||||
<string name="background_player_name">NewPipe Přehrávač na pozadí</string>
|
|
||||||
<string name="download_path_title">Umístění pro stažené video</string>
|
|
||||||
<string name="download_path_summary">Cesta, kde se uloží video po stažení.</string>
|
|
||||||
<string name="download_path_dialog_title">Zadejte umístění pro uložená videa</string>
|
|
||||||
|
|
||||||
<string name="show_play_with_kodi_title">Zobrazit možnost \"Přehrát s Kodi\"</string>
|
|
||||||
<string name="show_play_with_kodi_summary">Zobrazit možnost přehrát video s multimediálním centrem Kodi.</string>
|
|
||||||
<string name="play_audio">Audio</string>
|
|
||||||
<string name="default_audio_format_title">Výchozí audio formát</string>
|
|
||||||
<string name="webm_description">WebM — svobodný formát</string>
|
|
||||||
<string name="m4a_description">m4a — lepší kvalita</string>
|
|
||||||
<string name="theme_title">Vzhled</string>
|
|
||||||
<string name="dark_theme_title">Tmavý</string>
|
|
||||||
<string name="light_theme_title">Světlý</string>
|
|
||||||
|
|
||||||
<string name="download_dialog_title">Stažení</string>
|
|
||||||
<string name="next_video_title">Následující video</string>
|
|
||||||
<string name="show_next_and_similar_title">Zobrazit následující a související videa</string>
|
|
||||||
<string name="url_not_supported_toast">URL není podporováno</string>
|
|
||||||
<string name="similar_videos_btn_text">Související videa</string>
|
|
||||||
<string name="search_language_title">Preferovaný jazyk obsahu</string>
|
|
||||||
<string name="settings_category_video_audio_title">Video & audio</string>
|
|
||||||
<string name="settings_category_appearance_title">Vzhled</string>
|
|
||||||
<string name="settings_category_other_title">Ostatní</string>
|
|
||||||
<string name="background_player_playing_toast">Přehrávám na pozadí</string>
|
|
||||||
<string name="play_btn_text">Přehrát</string>
|
|
||||||
<string name="general_error">Chyba</string>
|
|
||||||
<string name="network_error">Chyba sítě</string>
|
|
||||||
<string name="could_not_load_thumbnails">Nebylo možné stáhnout všechny náhledy</string>
|
|
||||||
<string name="youtube_signature_decryption_error">Nebylo možné dekódovat URL videa.</string>
|
|
||||||
<string name="parsing_error">Nebylo možné analyzovat webovou stránku.</string>
|
|
||||||
<string name="content_not_available">Obsah není k dispozici.</string>
|
|
||||||
<string name="blocked_by_gema">Obsah blokuje GEMA.</string>
|
|
||||||
|
|
||||||
<string name="list_thumbnail_view_description">Náhled videa</string>
|
|
||||||
<string name="detail_thumbnail_view_description">Náhled videa</string>
|
|
||||||
<string name="detail_uploader_thumbnail_view_description">Náhled uploadera</string>
|
|
||||||
<string name="detail_likes_img_view_description">To se mi líbí</string>
|
|
||||||
<string name="detail_dislikes_img_view_description">To se mi nelíbí</string>
|
|
||||||
<string name="use_tor_title">Použít Tor</string>
|
|
||||||
<string name="use_tor_summary">Vynutit stahování skrz Tor pro zvýšené soukromí (streaming není zatím podporován)</string>
|
|
||||||
|
|
||||||
<string name="err_dir_create">Nebylo možné vytvořit složku pro stažené soubory \'%1$s\'</string>
|
|
||||||
<string name="info_dir_created">Vytvořena složka pro stažené soubory \'%1$s\'</string>
|
|
||||||
</resources>
|
|
|
@ -1,50 +0,0 @@
|
||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<resources><string name="background_player_name">NewPipe-fonludilo</string>
|
|
||||||
<string name="view_count_text">%1$s vidoj</string>
|
|
||||||
<string name="upload_date_text">Alŝultita je %1$s</string>
|
|
||||||
<string name="install">Instali</string>
|
|
||||||
<string name="cancel">Nuligi</string>
|
|
||||||
<string name="open_in_browser">Malfermi per retumilo</string>
|
|
||||||
<string name="share">Konigi</string>
|
|
||||||
<string name="loading">Ŝargado</string>
|
|
||||||
<string name="download">Elŝuti</string>
|
|
||||||
<string name="search">Serĉi</string>
|
|
||||||
<string name="settings">Agordoj</string>
|
|
||||||
<string name="did_you_mean">"Ĉu vi intencis: "</string>
|
|
||||||
<string name="search_page">"Serĉpaĝo: "</string>
|
|
||||||
<string name="share_dialog_title">Konigi kun:</string>
|
|
||||||
<string name="choose_browser">Elekti retumilon:</string>
|
|
||||||
<string name="screen_rotation">turno</string>
|
|
||||||
<string name="settings_activity_title">Agordoj</string>
|
|
||||||
<string name="use_external_video_player_title">Uzi eksteran videoludilon</string>
|
|
||||||
<string name="use_external_audio_player_title">Uzi eksteran sonludilon</string>
|
|
||||||
|
|
||||||
<string name="default_resolution_title">Defaŭlta distingivo</string>
|
|
||||||
<string name="play_with_kodi_title">Ludi per Kodi</string>
|
|
||||||
<string name="show_play_with_kodi_title">Montri \"Ludi per Kodi\"-opcion</string>
|
|
||||||
<string name="play_audio">Sono</string>
|
|
||||||
<string name="default_audio_format_title">Defaŭlta sondosierformo</string>
|
|
||||||
<string name="webm_description">WebM — libera dosierformo</string>
|
|
||||||
<string name="m4a_description">m4a — pli bona kvalito</string>
|
|
||||||
<string name="theme_title">Etoso</string>
|
|
||||||
<string name="dark_theme_title">Malluma</string>
|
|
||||||
<string name="light_theme_title">Luma</string>
|
|
||||||
|
|
||||||
<string name="download_dialog_title">Elŝuti</string>
|
|
||||||
<string name="next_video_title">Sekva video</string>
|
|
||||||
<string name="url_not_supported_toast">Ligilo ne subtenita</string>
|
|
||||||
<string name="similar_videos_btn_text">Similaj videoj</string>
|
|
||||||
<string name="search_language_title">Preferata enhavlingvo</string>
|
|
||||||
<string name="settings_category_video_audio_title">Video kaj sono</string>
|
|
||||||
<string name="settings_category_appearance_title">Apero</string>
|
|
||||||
<string name="settings_category_other_title">Alia</string>
|
|
||||||
<string name="background_player_playing_toast">Ludado fone</string>
|
|
||||||
<string name="play_btn_text">Ludi</string>
|
|
||||||
<string name="general_error">Eraro</string>
|
|
||||||
<string name="network_error">Reteraro</string>
|
|
||||||
<string name="content_not_available">Enhavo ne estas disponebla.</string>
|
|
||||||
<string name="blocked_by_gema">Blokita de GEMA.</string>
|
|
||||||
|
|
||||||
<string name="detail_likes_img_view_description">Ŝatoj</string>
|
|
||||||
<string name="detail_dislikes_img_view_description">Malŝatoj</string>
|
|
||||||
</resources>
|
|
22
app/src/main/res/values/constants.xml
Normal file
22
app/src/main/res/values/constants.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<resources xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- The minimum subtitle font size. -->
|
||||||
|
<dimen name="subtitle_minimum_font_size">13sp</dimen>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -1,5 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
|
||||||
<!-- Video Item Search View Dimensions-->
|
<!-- Video Item Search View Dimensions-->
|
||||||
<!-- Text Size -->
|
<!-- Text Size -->
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<string name="use_external_video_player_key">use_external_video_player</string>
|
<string name="use_external_video_player_key">use_external_video_player</string>
|
||||||
<string name="use_external_audio_player_key">use_external_audio_player</string>
|
<string name="use_external_audio_player_key">use_external_audio_player</string>
|
||||||
<string name="autoplay_through_intent_key">autoplay_through_intent</string>
|
<string name="autoplay_through_intent_key">autoplay_through_intent</string>
|
||||||
|
<string name="use_exoplayer_key">use_exoplayer</string>
|
||||||
|
|
||||||
<string name="default_resolution_key">default_resolution_preference</string>
|
<string name="default_resolution_key">default_resolution_preference</string>
|
||||||
<string name="default_resolution_value">360p</string>
|
<string name="default_resolution_value">360p</string>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<resources>
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="app_name" translatable="false">NewPipe</string>
|
<string name="app_name" translatable="false">NewPipe</string>
|
||||||
<string name="background_player_name">NewPipe Background Player</string>
|
<string name="background_player_name">NewPipe Background Player</string>
|
||||||
<string name="title_videoitem_detail" translatable="false">NewPipe</string>
|
<string name="title_videoitem_detail" translatable="false">NewPipe</string>
|
||||||
|
@ -84,4 +84,24 @@
|
||||||
|
|
||||||
<string name="err_dir_create">Cannot create download directory \'%1$s\'</string>
|
<string name="err_dir_create">Cannot create download directory \'%1$s\'</string>
|
||||||
<string name="info_dir_created">Created download directory \'%1$s\'</string>
|
<string name="info_dir_created">Created download directory \'%1$s\'</string>
|
||||||
|
|
||||||
|
<string name="enable_background_audio">Play in background</string>
|
||||||
|
<string name="video">Video</string>
|
||||||
|
<string name="audio">Audio</string>
|
||||||
|
<string name="text">Text</string>
|
||||||
|
<string name="logging">Logging</string>
|
||||||
|
<string name="logging_normal">Normal</string>
|
||||||
|
<string name="logging_verbose">Verbose</string>
|
||||||
|
<string name="retry">Retry</string>
|
||||||
|
<string name="off">[off]</string>
|
||||||
|
<string name="error_drm_not_supported">Protected content not supported on API levels below 18</string>
|
||||||
|
<string name="error_drm_unsupported_scheme">This device does not support the required DRM scheme</string>
|
||||||
|
<string name="error_drm_unknown">An unknown DRM error occurred</string>
|
||||||
|
<string name="error_no_decoder">This device does not provide a decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||||
|
<string name="error_no_secure_decoder">This device does not provide a secure decoder for <xliff:g id="mime_type">%1$s</xliff:g></string>
|
||||||
|
<string name="error_querying_decoders">Unable to query device decoders</string>
|
||||||
|
<string name="error_instantiating_decoder">Unable to instantiate decoder <xliff:g id="decoder_name">%1$s</xliff:g></string>
|
||||||
|
<string name="storage_permission_denied">Permission to access storage was denied</string>
|
||||||
|
<string name="use_exoplayer_title">Use ExoPlayer</string>
|
||||||
|
<string name="use_exoplayer_summary">Experimental</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,4 +1,19 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
|
<style name="RootTheme" parent="android:Theme.Holo">
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PlayerTheme" parent="@style/RootTheme">
|
||||||
|
<item name="android:windowNoTitle">true</item>
|
||||||
|
<item name="android:windowBackground">@android:color/black</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="ExoPlayerButton">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:minWidth">40dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light">
|
<style name="AppTheme" parent="Theme.AppCompat.Light">
|
||||||
<item name="android:actionBarStyle">@style/NewPipeActionbarTheme</item>
|
<item name="android:actionBarStyle">@style/NewPipeActionbarTheme</item>
|
||||||
|
@ -32,4 +47,5 @@
|
||||||
<item name="android:background">@color/video_overlay_color</item>
|
<item name="android:background">@color/video_overlay_color</item>
|
||||||
<item name="background">@color/video_overlay_color</item>
|
<item name="background">@color/video_overlay_color</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -33,6 +33,11 @@
|
||||||
android:entryValues="@array/audio_format_list"
|
android:entryValues="@array/audio_format_list"
|
||||||
android:defaultValue="@string/default_audio_format_value"/>
|
android:defaultValue="@string/default_audio_format_value"/>
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:key="@string/use_exoplayer_key"
|
||||||
|
android:title="@string/use_exoplayer_title"
|
||||||
|
android:summary="@string/use_exoplayer_summary"
|
||||||
|
android:defaultValue="false"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="@string/settings_category_appearance"
|
android:key="@string/settings_category_appearance"
|
||||||
|
|
Loading…
Reference in a new issue