Add support of other delivery methods than progressive HTTP (in the player only)
Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
This commit is contained in:
parent
a59660f421
commit
210834fbe9
27 changed files with 2417 additions and 539 deletions
|
|
@ -1744,24 +1744,9 @@ public final class Player implements
|
|||
if (exoPlayerIsNull()) {
|
||||
return;
|
||||
}
|
||||
// Use duration of currentItem for non-live streams,
|
||||
// because HLS streams are fragmented
|
||||
// and thus the whole duration is not available to the player
|
||||
// TODO: revert #6307 when introducing proper HLS support
|
||||
final int duration;
|
||||
if (currentItem != null
|
||||
&& !StreamTypeUtil.isLiveStream(currentItem.getStreamType())
|
||||
) {
|
||||
// convert seconds to milliseconds
|
||||
duration = (int) (currentItem.getDuration() * 1000);
|
||||
} else {
|
||||
duration = (int) simpleExoPlayer.getDuration();
|
||||
}
|
||||
onUpdateProgress(
|
||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
duration,
|
||||
simpleExoPlayer.getBufferedPercentage()
|
||||
);
|
||||
|
||||
onUpdateProgress(Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
(int) simpleExoPlayer.getDuration(), simpleExoPlayer.getBufferedPercentage());
|
||||
}
|
||||
|
||||
private Disposable getProgressUpdateDisposable() {
|
||||
|
|
@ -3399,6 +3384,7 @@ public final class Player implements
|
|||
|
||||
switch (info.getStreamType()) {
|
||||
case AUDIO_STREAM:
|
||||
case POST_LIVE_AUDIO_STREAM:
|
||||
binding.surfaceView.setVisibility(View.GONE);
|
||||
binding.endScreen.setVisibility(View.VISIBLE);
|
||||
binding.playbackEndTime.setVisibility(View.VISIBLE);
|
||||
|
|
@ -3417,6 +3403,7 @@ public final class Player implements
|
|||
break;
|
||||
|
||||
case VIDEO_STREAM:
|
||||
case POST_LIVE_STREAM:
|
||||
if (currentMetadata == null
|
||||
|| !currentMetadata.getMaybeQuality().isPresent()
|
||||
|| (info.getVideoStreams().isEmpty()
|
||||
|
|
@ -3484,10 +3471,10 @@ public final class Player implements
|
|||
for (int i = 0; i < availableStreams.size(); i++) {
|
||||
final VideoStream videoStream = availableStreams.get(i);
|
||||
qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
|
||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
||||
.getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
|
||||
}
|
||||
if (getSelectedVideoStream() != null) {
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||
}
|
||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||
qualityPopupMenu.setOnDismissListener(this);
|
||||
|
|
@ -3605,7 +3592,7 @@ public final class Player implements
|
|||
}
|
||||
|
||||
saveStreamProgressState(); //TODO added, check if good
|
||||
final String newResolution = availableStreams.get(menuItemIndex).resolution;
|
||||
final String newResolution = availableStreams.get(menuItemIndex).getResolution();
|
||||
setRecovery();
|
||||
setPlaybackQuality(newResolution);
|
||||
reloadPlayQueueManager();
|
||||
|
|
@ -3633,7 +3620,7 @@ public final class Player implements
|
|||
}
|
||||
isSomePopupMenuVisible = false; //TODO check if this works
|
||||
if (getSelectedVideoStream() != null) {
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
|
||||
}
|
||||
if (isPlaying()) {
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
|
|
@ -4250,7 +4237,8 @@ public final class Player implements
|
|||
} else {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (streamType == StreamType.AUDIO_STREAM
|
||||
|| streamType == StreamType.AUDIO_LIVE_STREAM) {
|
||||
|| streamType == StreamType.AUDIO_LIVE_STREAM
|
||||
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM) {
|
||||
// Nothing to do more than setting the recovery position
|
||||
setRecovery();
|
||||
return;
|
||||
|
|
@ -4285,13 +4273,15 @@ public final class Player implements
|
|||
* the content is not an audio content, but also if none of the following cases is met:
|
||||
*
|
||||
* <ul>
|
||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream} or an
|
||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream};</li>
|
||||
* <li>the content is an {@link StreamType#AUDIO_STREAM audio stream}, an
|
||||
* {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a
|
||||
* {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};</li>
|
||||
* <li>the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a
|
||||
* {@link SourceType#LIVE_STREAM live source};</li>
|
||||
* <li>the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream
|
||||
* with a separated audio source} or has no audio-only streams available <b>and</b> is a
|
||||
* {@link StreamType#LIVE_STREAM live stream} or a
|
||||
* {@link StreamType#VIDEO_STREAM video stream}, an
|
||||
* {@link StreamType#POST_LIVE_STREAM ended live stream}, or a
|
||||
* {@link StreamType#LIVE_STREAM live stream}.
|
||||
* </li>
|
||||
* </ul>
|
||||
|
|
@ -4309,14 +4299,17 @@ public final class Player implements
|
|||
final StreamType streamType = streamInfo.getStreamType();
|
||||
|
||||
if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM
|
||||
&& streamType != StreamType.AUDIO_LIVE_STREAM) {
|
||||
&& streamType != StreamType.AUDIO_LIVE_STREAM
|
||||
&& streamType != StreamType.POST_LIVE_AUDIO_STREAM) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The content is an audio stream, an audio live stream, or a live stream with a live
|
||||
// source: it's not needed to reload the play queue manager because the stream source will
|
||||
// be the same
|
||||
if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM)
|
||||
if ((streamType == StreamType.AUDIO_STREAM
|
||||
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM
|
||||
|| streamType == StreamType.AUDIO_LIVE_STREAM)
|
||||
|| (streamType == StreamType.LIVE_STREAM
|
||||
&& sourceType == SourceType.LIVE_STREAM)) {
|
||||
return false;
|
||||
|
|
@ -4331,8 +4324,10 @@ public final class Player implements
|
|||
|| (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY
|
||||
&& isNullOrEmpty(streamInfo.getAudioStreams()))) {
|
||||
// It's not needed to reload the play queue manager only if the content's stream type
|
||||
// is a video stream or a live stream
|
||||
return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM;
|
||||
// is a video stream, a live stream or an ended live stream
|
||||
return streamType != StreamType.VIDEO_STREAM
|
||||
&& streamType != StreamType.LIVE_STREAM
|
||||
&& streamType != StreamType.POST_LIVE_STREAM;
|
||||
}
|
||||
|
||||
// Other cases: the play queue manager reload is needed
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,9 @@ package org.schabi.newpipe.player.helper;
|
|||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
|
|
@ -14,45 +17,58 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
|||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
|
||||
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/* package-private */ class CacheFactory implements DataSource.Factory {
|
||||
private static final String TAG = "CacheFactory";
|
||||
/* package-private */ final class CacheFactory implements DataSource.Factory {
|
||||
private static final String TAG = CacheFactory.class.getSimpleName();
|
||||
|
||||
private static final String CACHE_FOLDER_NAME = "exoplayer";
|
||||
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE
|
||||
| CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final File cacheDir;
|
||||
private final long maxFileSize;
|
||||
|
||||
// Creating cache on every instance may cause problems with multiple players when
|
||||
// sources are not ExtractorMediaSource
|
||||
// see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer
|
||||
// todo: make this a singleton?
|
||||
private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
||||
private static SimpleCache cache;
|
||||
|
||||
CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener transferListener) {
|
||||
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(),
|
||||
PlayerHelper.getPreferredFileSize());
|
||||
private final long maxFileSize;
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final TransferListener transferListener;
|
||||
private final DataSource.Factory upstreamDataSourceFactory;
|
||||
|
||||
public static class Builder {
|
||||
private final Context context;
|
||||
private final String userAgent;
|
||||
private final TransferListener transferListener;
|
||||
private DataSource.Factory upstreamDataSourceFactory;
|
||||
|
||||
Builder(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener transferListener) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.transferListener = transferListener;
|
||||
}
|
||||
|
||||
public void setUpstreamDataSourceFactory(
|
||||
@Nullable final DataSource.Factory upstreamDataSourceFactory) {
|
||||
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
|
||||
}
|
||||
|
||||
public CacheFactory build() {
|
||||
return new CacheFactory(context, userAgent, transferListener,
|
||||
upstreamDataSourceFactory);
|
||||
}
|
||||
}
|
||||
|
||||
private CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener transferListener,
|
||||
final long maxCacheSize,
|
||||
final long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
@Nullable final DataSource.Factory upstreamDataSourceFactory) {
|
||||
this.context = context;
|
||||
this.userAgent = userAgent;
|
||||
this.transferListener = transferListener;
|
||||
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
|
||||
|
||||
dataSourceFactory = new DefaultDataSource
|
||||
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
||||
.setTransferListener(transferListener);
|
||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
cacheDir.mkdir();
|
||||
|
|
@ -60,37 +76,43 @@ import androidx.annotation.NonNull;
|
|||
|
||||
if (cache == null) {
|
||||
final LeastRecentlyUsedCacheEvictor evictor
|
||||
= new LeastRecentlyUsedCacheEvictor(maxCacheSize);
|
||||
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
|
||||
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
|
||||
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
maxFileSize = PlayerHelper.getPreferredFileSize();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource createDataSource() {
|
||||
Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath());
|
||||
|
||||
final DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
final DataSource.Factory upstreamDataSourceFactoryToUse;
|
||||
if (upstreamDataSourceFactory == null) {
|
||||
upstreamDataSourceFactoryToUse = new DefaultHttpDataSource.Factory()
|
||||
.setUserAgent(userAgent);
|
||||
} else {
|
||||
if (upstreamDataSourceFactory instanceof DefaultHttpDataSource.Factory) {
|
||||
upstreamDataSourceFactoryToUse =
|
||||
((DefaultHttpDataSource.Factory) upstreamDataSourceFactory)
|
||||
.setUserAgent(userAgent);
|
||||
} else if (upstreamDataSourceFactory instanceof YoutubeHttpDataSource.Factory) {
|
||||
upstreamDataSourceFactoryToUse =
|
||||
((YoutubeHttpDataSource.Factory) upstreamDataSourceFactory)
|
||||
.setUserAgentForNonMobileStreams(userAgent);
|
||||
} else {
|
||||
upstreamDataSourceFactoryToUse = upstreamDataSourceFactory;
|
||||
}
|
||||
}
|
||||
|
||||
final DefaultDataSource dataSource = new DefaultDataSource.Factory(context,
|
||||
upstreamDataSourceFactoryToUse)
|
||||
.setTransferListener(transferListener)
|
||||
.createDataSource();
|
||||
|
||||
final FileDataSource fileSource = new FileDataSource();
|
||||
final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize);
|
||||
|
||||
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
|
||||
}
|
||||
|
||||
public void tryDeleteCacheFiles() {
|
||||
if (!cacheDir.exists() || !cacheDir.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (final File file : cacheDir.listFiles()) {
|
||||
final String filePath = file.getAbsolutePath();
|
||||
final boolean deleteSuccessful = file.delete();
|
||||
|
||||
Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to delete file.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMultivariantPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
|
||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* A {@link HlsPlaylistParserFactory} for non-URI HLS sources.
|
||||
*/
|
||||
public final class NonUriHlsPlaylistParserFactory implements HlsPlaylistParserFactory {
|
||||
|
||||
private final HlsPlaylist hlsPlaylist;
|
||||
|
||||
public NonUriHlsPlaylistParserFactory(final HlsPlaylist hlsPlaylist) {
|
||||
this.hlsPlaylist = hlsPlaylist;
|
||||
}
|
||||
|
||||
private final class NonUriHlsPlayListParser implements ParsingLoadable.Parser<HlsPlaylist> {
|
||||
|
||||
@Override
|
||||
public HlsPlaylist parse(final Uri uri,
|
||||
final InputStream inputStream) throws IOException {
|
||||
return hlsPlaylist;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser() {
|
||||
return new NonUriHlsPlayListParser();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ParsingLoadable.Parser<HlsPlaylist> createPlaylistParser(
|
||||
@NonNull final HlsMultivariantPlaylist multivariantPlaylist,
|
||||
@Nullable final HlsMediaPlaylist previousMediaPlaylist) {
|
||||
return new NonUriHlsPlayListParser();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,21 +2,27 @@ package org.schabi.newpipe.player.helper;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParserFactory;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
|
||||
import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource;
|
||||
|
||||
public class PlayerDataSource {
|
||||
|
||||
|
|
@ -29,79 +35,120 @@ public class PlayerDataSource {
|
|||
* early.
|
||||
*/
|
||||
private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15;
|
||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* The maximum number of generated manifests per cache, in
|
||||
* {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and
|
||||
* {@link YoutubePostLiveStreamDvrDashManifestCreator}.
|
||||
*/
|
||||
private static final int MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE = 500;
|
||||
|
||||
private final int continueLoadingCheckIntervalBytes;
|
||||
private final DataSource.Factory cacheDataSourceFactory;
|
||||
private final CacheFactory.Builder cacheDataSourceFactoryBuilder;
|
||||
private final DataSource.Factory cachelessDataSourceFactory;
|
||||
|
||||
public PlayerDataSource(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener transferListener) {
|
||||
continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context);
|
||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
||||
cachelessDataSourceFactory = new DefaultDataSource
|
||||
.Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
||||
cacheDataSourceFactoryBuilder = new CacheFactory.Builder(context, userAgent,
|
||||
transferListener);
|
||||
cachelessDataSourceFactory = new DefaultDataSource.Factory(context,
|
||||
new DefaultHttpDataSource.Factory().setUserAgent(userAgent))
|
||||
.setTransferListener(transferListener);
|
||||
|
||||
YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(
|
||||
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
|
||||
YoutubeOtfDashManifestCreator.getCache().setMaximumSize(
|
||||
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
|
||||
YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize(
|
||||
MAXIMUM_SIZE_CACHED_GENERATED_MANIFESTS_PER_CACHE);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(
|
||||
MANIFEST_MINIMUM_RETRY))
|
||||
.setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory) ->
|
||||
new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy,
|
||||
playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)
|
||||
);
|
||||
playlistParserFactory,
|
||||
PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT));
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory
|
||||
)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
|
||||
cachelessDataSourceFactory);
|
||||
}
|
||||
|
||||
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||
final DataSource.Factory dataSourceFactory
|
||||
) {
|
||||
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cacheDataSourceFactory);
|
||||
public HlsMediaSource.Factory getHlsMediaSourceFactory(
|
||||
@Nullable final HlsPlaylistParserFactory hlsPlaylistParserFactory) {
|
||||
final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(
|
||||
cacheDataSourceFactoryBuilder.build());
|
||||
if (hlsPlaylistParserFactory != null) {
|
||||
factory.setPlaylistParserFactory(hlsPlaylistParserFactory);
|
||||
}
|
||||
return factory;
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
|
||||
cacheDataSourceFactory
|
||||
);
|
||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
|
||||
cacheDataSourceFactoryBuilder.build());
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
|
||||
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes)
|
||||
.setLoadErrorHandlingPolicy(
|
||||
new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
|
||||
public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() {
|
||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
|
||||
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
||||
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
|
||||
public SsMediaSource.Factory getSSMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(
|
||||
new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
|
||||
cachelessDataSourceFactory);
|
||||
}
|
||||
|
||||
public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() {
|
||||
return new SingleSampleMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() {
|
||||
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
|
||||
getYoutubeHttpDataSourceFactory(true, true));
|
||||
return new DashMediaSource.Factory(
|
||||
getDefaultDashChunkSourceFactory(cacheDataSourceFactoryBuilder.build()),
|
||||
cacheDataSourceFactoryBuilder.build());
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() {
|
||||
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
|
||||
getYoutubeHttpDataSourceFactory(false, false));
|
||||
return new HlsMediaSource.Factory(cacheDataSourceFactoryBuilder.build());
|
||||
}
|
||||
|
||||
public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() {
|
||||
cacheDataSourceFactoryBuilder.setUpstreamDataSourceFactory(
|
||||
getYoutubeHttpDataSourceFactory(false, true));
|
||||
return new ProgressiveMediaSource.Factory(cacheDataSourceFactoryBuilder.build())
|
||||
.setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
|
||||
final DataSource.Factory dataSourceFactory) {
|
||||
return new DefaultDashChunkSource.Factory(dataSourceFactory);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory(
|
||||
final boolean rangeParameterEnabled,
|
||||
final boolean rnParameterEnabled) {
|
||||
return new YoutubeHttpDataSource.Factory()
|
||||
.setRangeParameterEnabled(rangeParameterEnabled)
|
||||
.setRnParameterEnabled(rnParameterEnabled);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package org.schabi.newpipe.player.helper;
|
|||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
|
||||
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
|
||||
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||
import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN;
|
||||
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
|
||||
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
|
||||
|
|
@ -110,12 +112,14 @@ public final class PlayerHelper {
|
|||
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
|
||||
}
|
||||
|
||||
private PlayerHelper() { }
|
||||
private PlayerHelper() {
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Exposed helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@NonNull
|
||||
public static String getTimeString(final int milliSeconds) {
|
||||
final int seconds = (milliSeconds % 60000) / 1000;
|
||||
final int minutes = (milliSeconds % 3600000) / 60000;
|
||||
|
|
@ -131,15 +135,18 @@ public final class PlayerHelper {
|
|||
).toString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String formatSpeed(final double speed) {
|
||||
return SPEED_FORMATTER.format(speed);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String formatPitch(final double pitch) {
|
||||
return PITCH_FORMATTER.format(pitch);
|
||||
}
|
||||
|
||||
public static String subtitleMimeTypesOf(final MediaFormat format) {
|
||||
@NonNull
|
||||
public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) {
|
||||
switch (format) {
|
||||
case VTT:
|
||||
return MimeTypes.TEXT_VTT;
|
||||
|
|
@ -192,14 +199,48 @@ public final class PlayerHelper {
|
|||
|
||||
@NonNull
|
||||
public static String cacheKeyOf(@NonNull final StreamInfo info,
|
||||
@NonNull final VideoStream video) {
|
||||
return info.getUrl() + video.getResolution() + video.getFormat().getName();
|
||||
@NonNull final VideoStream videoStream) {
|
||||
String cacheKey = info.getUrl() + " " + videoStream.getId();
|
||||
|
||||
final String resolution = videoStream.getResolution();
|
||||
final MediaFormat mediaFormat = videoStream.getFormat();
|
||||
if (resolution.equals(RESOLUTION_UNKNOWN) && mediaFormat == null) {
|
||||
// The hash code is only used in the cache key in the case when the resolution and the
|
||||
// media format are unknown
|
||||
cacheKey += " " + videoStream.hashCode();
|
||||
} else {
|
||||
if (mediaFormat != null) {
|
||||
cacheKey += " " + videoStream.getFormat().getName();
|
||||
}
|
||||
if (!resolution.equals(RESOLUTION_UNKNOWN)) {
|
||||
cacheKey += " " + resolution;
|
||||
}
|
||||
}
|
||||
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String cacheKeyOf(@NonNull final StreamInfo info,
|
||||
@NonNull final AudioStream audio) {
|
||||
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
|
||||
@NonNull final AudioStream audioStream) {
|
||||
String cacheKey = info.getUrl() + " " + audioStream.getId();
|
||||
|
||||
final int averageBitrate = audioStream.getAverageBitrate();
|
||||
final MediaFormat mediaFormat = audioStream.getFormat();
|
||||
if (averageBitrate == UNKNOWN_BITRATE && mediaFormat == null) {
|
||||
// The hash code is only used in the cache key in the case when the resolution and the
|
||||
// media format are unknown
|
||||
cacheKey += " " + audioStream.hashCode();
|
||||
} else {
|
||||
if (mediaFormat != null) {
|
||||
cacheKey += " " + audioStream.getFormat().getName();
|
||||
}
|
||||
if (averageBitrate != UNKNOWN_BITRATE) {
|
||||
cacheKey += " " + averageBitrate;
|
||||
}
|
||||
}
|
||||
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,7 +274,7 @@ public final class PlayerHelper {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem
|
||||
if (relatedItems.get(0) instanceof StreamInfoItem
|
||||
&& !urls.contains(relatedItems.get(0).getUrl())) {
|
||||
return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0));
|
||||
}
|
||||
|
|
@ -335,6 +376,7 @@ public final class PlayerHelper {
|
|||
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ExoTrackSelection.Factory getQualitySelector() {
|
||||
return new AdaptiveTrackSelection.Factory(
|
||||
1000,
|
||||
|
|
@ -389,7 +431,7 @@ public final class PlayerHelper {
|
|||
/**
|
||||
* @param context the Android context
|
||||
* @return the screen brightness to use. A value less than 0 (the default) means to use the
|
||||
* preferred screen brightness
|
||||
* preferred screen brightness
|
||||
*/
|
||||
public static float getScreenBrightness(@NonNull final Context context) {
|
||||
final SharedPreferences sp = getPreferences(context);
|
||||
|
|
@ -480,7 +522,8 @@ public final class PlayerHelper {
|
|||
return REPEAT_MODE_ONE;
|
||||
case REPEAT_MODE_ONE:
|
||||
return REPEAT_MODE_ALL;
|
||||
case REPEAT_MODE_ALL: default:
|
||||
case REPEAT_MODE_ALL:
|
||||
default:
|
||||
return REPEAT_MODE_OFF;
|
||||
}
|
||||
}
|
||||
|
|
@ -548,7 +591,7 @@ public final class PlayerHelper {
|
|||
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
|
||||
final float popupWidth = popupRememberSizeAndPos
|
||||
? player.getPrefs().getFloat(player.getContext().getString(
|
||||
R.string.popup_saved_width_key), defaultSize)
|
||||
R.string.popup_saved_width_key), defaultSize)
|
||||
: defaultSize;
|
||||
final float popupHeight = getMinimumVideoHeight(popupWidth);
|
||||
|
||||
|
|
@ -564,10 +607,10 @@ public final class PlayerHelper {
|
|||
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
|
||||
popupLayoutParams.x = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_x_key), centerX) : centerX;
|
||||
R.string.popup_saved_x_key), centerX) : centerX;
|
||||
popupLayoutParams.y = popupRememberSizeAndPos
|
||||
? player.getPrefs().getInt(player.getContext().getString(
|
||||
R.string.popup_saved_y_key), centerY) : centerY;
|
||||
R.string.popup_saved_y_key), centerY) : centerY;
|
||||
|
||||
return popupLayoutParams;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class QualityClickListener(
|
|||
val videoStream = player.selectedVideoStream
|
||||
if (videoStream != null) {
|
||||
player.binding.qualityTextView.text =
|
||||
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution
|
||||
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
|
||||
}
|
||||
|
||||
player.saveWasPlaying()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
package org.schabi.newpipe.player.resolver;
|
||||
|
||||
import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
|
|
@ -16,7 +18,13 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
|||
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioPlaybackResolver implements PlaybackResolver {
|
||||
private static final String TAG = AudioPlaybackResolver.class.getSimpleName();
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
@NonNull
|
||||
|
|
@ -31,19 +39,28 @@ public class AudioPlaybackResolver implements PlaybackResolver {
|
|||
@Override
|
||||
@Nullable
|
||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
||||
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
||||
if (liveSource != null) {
|
||||
return liveSource;
|
||||
}
|
||||
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||
final List<AudioStream> audioStreams = new ArrayList<>(info.getAudioStreams());
|
||||
removeTorrentStreams(audioStreams);
|
||||
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, audioStreams);
|
||||
if (index < 0 || index >= info.getAudioStreams().size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final AudioStream audio = info.getAudioStreams().get(index);
|
||||
final MediaItemTag tag = StreamInfoTag.of(info);
|
||||
return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
||||
|
||||
try {
|
||||
return PlaybackResolver.buildMediaSource(
|
||||
dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Unable to create audio source:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,38 @@
|
|||
package org.schabi.newpipe.player.resolver;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
|
||||
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator;
|
||||
import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.NonUriHlsPlaylistParserFactory;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
||||
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||
|
|
@ -18,13 +41,17 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
||||
String TAG = PlaybackResolver.class.getSimpleName();
|
||||
|
||||
@Nullable
|
||||
default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final StreamInfo info) {
|
||||
static MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final StreamInfo info) {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (!StreamTypeUtil.isLiveStream(streamType)) {
|
||||
return null;
|
||||
|
|
@ -41,10 +68,10 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final String sourceUrl,
|
||||
@C.ContentType final int type,
|
||||
@NonNull final MediaItemTag metadata) {
|
||||
static MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final String sourceUrl,
|
||||
@C.ContentType final int type,
|
||||
@NonNull final MediaItemTag metadata) {
|
||||
final MediaSource.Factory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
|
|
@ -67,46 +94,342 @@ public interface PlaybackResolver extends Resolver<StreamInfo, MediaSource> {
|
|||
.setLiveConfiguration(
|
||||
new MediaItem.LiveConfiguration.Builder()
|
||||
.setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
);
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final String sourceUrl,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final String overrideExtension,
|
||||
@NonNull final MediaItemTag metadata) {
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
|
||||
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||
|
||||
final MediaSource.Factory factory;
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
factory = dataSource.getLiveSsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_DASH:
|
||||
factory = dataSource.getDashMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_HLS:
|
||||
factory = dataSource.getHlsMediaSourceFactory();
|
||||
break;
|
||||
case C.TYPE_OTHER:
|
||||
factory = dataSource.getExtractorMediaSourceFactory();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
static MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final Stream stream,
|
||||
@NonNull final StreamInfo streamInfo,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata)
|
||||
throws IOException {
|
||||
if (streamInfo.getService() == ServiceList.YouTube) {
|
||||
return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata);
|
||||
}
|
||||
|
||||
return factory.createMediaSource(
|
||||
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
|
||||
switch (deliveryMethod) {
|
||||
case PROGRESSIVE_HTTP:
|
||||
return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata);
|
||||
case DASH:
|
||||
return buildDashMediaSource(dataSource, stream, cacheKey, metadata);
|
||||
case HLS:
|
||||
return buildHlsMediaSource(dataSource, stream, cacheKey, metadata);
|
||||
case SS:
|
||||
return buildSSMediaSource(dataSource, stream, cacheKey, metadata);
|
||||
// Torrent streams are not supported by ExoPlayer
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported delivery type: " + deliveryMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> ProgressiveMediaSource buildProgressiveMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) throws IOException {
|
||||
final String url = stream.getContent();
|
||||
|
||||
if (isNullOrEmpty(url)) {
|
||||
throw new IOException(
|
||||
"Try to generate a progressive media source from an empty string or from a "
|
||||
+ "null object");
|
||||
} else {
|
||||
return dataSource.getProgressiveMediaSourceFactory().createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(url))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> DashMediaSource buildDashMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) throws IOException {
|
||||
final boolean isUrlStream = stream.isUrl();
|
||||
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
|
||||
throw new IOException("Try to generate a DASH media source from an empty string or "
|
||||
+ "from a null object");
|
||||
}
|
||||
|
||||
if (isUrlStream) {
|
||||
return dataSource.getDashMediaSourceFactory().createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
} else {
|
||||
String baseUrl = stream.getManifestUrl();
|
||||
if (baseUrl == null) {
|
||||
baseUrl = "";
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse(baseUrl);
|
||||
|
||||
return dataSource.getDashMediaSourceFactory().createMediaSource(
|
||||
createDashManifest(stream.getContent(), stream),
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(uri)
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> DashManifest createDashManifest(
|
||||
@NonNull final String manifestContent,
|
||||
@NonNull final T stream) throws IOException {
|
||||
try {
|
||||
final ByteArrayInputStream dashManifestInput = new ByteArrayInputStream(
|
||||
manifestContent.getBytes(StandardCharsets.UTF_8));
|
||||
String baseUrl = stream.getManifestUrl();
|
||||
if (baseUrl == null) {
|
||||
baseUrl = "";
|
||||
}
|
||||
|
||||
return new DashManifestParser().parse(Uri.parse(baseUrl), dashManifestInput);
|
||||
} catch (final IOException e) {
|
||||
throw new IOException("Error when parsing manual DASH manifest", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> HlsMediaSource buildHlsMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) throws IOException {
|
||||
final boolean isUrlStream = stream.isUrl();
|
||||
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
|
||||
throw new IOException("Try to generate an HLS media source from an empty string or "
|
||||
+ "from a null object");
|
||||
}
|
||||
|
||||
if (isUrlStream) {
|
||||
return dataSource.getHlsMediaSourceFactory(null).createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
} else {
|
||||
String baseUrl = stream.getManifestUrl();
|
||||
if (baseUrl == null) {
|
||||
baseUrl = "";
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse(baseUrl);
|
||||
|
||||
final HlsPlaylist hlsPlaylist;
|
||||
try {
|
||||
final ByteArrayInputStream hlsManifestInput = new ByteArrayInputStream(
|
||||
stream.getContent().getBytes(StandardCharsets.UTF_8));
|
||||
hlsPlaylist = new HlsPlaylistParser().parse(uri, hlsManifestInput);
|
||||
} catch (final IOException e) {
|
||||
throw new IOException("Error when parsing manual HLS manifest", e);
|
||||
}
|
||||
|
||||
return dataSource.getHlsMediaSourceFactory(
|
||||
new NonUriHlsPlaylistParserFactory(hlsPlaylist))
|
||||
.createMediaSource(new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> SsMediaSource buildSSMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) throws IOException {
|
||||
final boolean isUrlStream = stream.isUrl();
|
||||
if (isUrlStream && isNullOrEmpty(stream.getContent())) {
|
||||
throw new IOException("Try to generate an SmoothStreaming media source from an empty "
|
||||
+ "string or from a null object");
|
||||
}
|
||||
|
||||
if (isUrlStream) {
|
||||
return dataSource.getSSMediaSourceFactory().createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
} else {
|
||||
String baseUrl = stream.getManifestUrl();
|
||||
if (baseUrl == null) {
|
||||
baseUrl = "";
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse(baseUrl);
|
||||
|
||||
final SsManifest smoothStreamingManifest;
|
||||
try {
|
||||
final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream(
|
||||
stream.getContent().getBytes(StandardCharsets.UTF_8));
|
||||
smoothStreamingManifest = new SsManifestParser().parse(uri,
|
||||
smoothStreamingManifestInput);
|
||||
} catch (final IOException e) {
|
||||
throw new IOException("Error when parsing manual SmoothStreaming manifest", e);
|
||||
}
|
||||
|
||||
return dataSource.getSSMediaSourceFactory().createMediaSource(
|
||||
smoothStreamingManifest,
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(uri)
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends Stream> MediaSource createYoutubeMediaSource(
|
||||
final T stream,
|
||||
final StreamInfo streamInfo,
|
||||
final PlayerDataSource dataSource,
|
||||
final String cacheKey,
|
||||
final MediaItemTag metadata) throws IOException {
|
||||
if (!(stream instanceof AudioStream || stream instanceof VideoStream)) {
|
||||
throw new IOException("Try to generate a DASH manifest of a YouTube "
|
||||
+ stream.getClass() + " " + stream.getContent());
|
||||
}
|
||||
|
||||
final StreamType streamType = streamInfo.getStreamType();
|
||||
if (streamType == StreamType.VIDEO_STREAM) {
|
||||
return createYoutubeMediaSourceOfVideoStreamType(dataSource, stream, streamInfo,
|
||||
cacheKey, metadata);
|
||||
} else if (streamType == StreamType.POST_LIVE_STREAM) {
|
||||
// If the content is not an URL, uses the DASH delivery method and if the stream type
|
||||
// of the stream is a post live stream, it means that the content is an ended
|
||||
// livestream so we need to generate the manifest corresponding to the content
|
||||
// (which is the last segment of the stream)
|
||||
|
||||
try {
|
||||
final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem());
|
||||
final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator
|
||||
.fromPostLiveStreamDvrStreamingUrl(stream.getContent(),
|
||||
itagItem,
|
||||
itagItem.getTargetDurationSec(),
|
||||
streamInfo.getDuration());
|
||||
return buildYoutubeManualDashMediaSource(dataSource,
|
||||
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||
metadata);
|
||||
} catch (final CreationException | NullPointerException e) {
|
||||
Log.e(TAG, "Error when generating the DASH manifest of YouTube ended live stream",
|
||||
e);
|
||||
throw new IOException("Error when generating the DASH manifest of YouTube ended "
|
||||
+ "live stream " + stream.getContent(), e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("DASH manifest generation of YouTube livestreams is "
|
||||
+ "not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends Stream> MediaSource createYoutubeMediaSourceOfVideoStreamType(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final StreamInfo streamInfo,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) throws IOException {
|
||||
final DeliveryMethod deliveryMethod = stream.getDeliveryMethod();
|
||||
switch (deliveryMethod) {
|
||||
case PROGRESSIVE_HTTP:
|
||||
if ((stream instanceof VideoStream && ((VideoStream) stream).isVideoOnly())
|
||||
|| stream instanceof AudioStream) {
|
||||
try {
|
||||
final String manifestString = YoutubeProgressiveDashManifestCreator
|
||||
.fromProgressiveStreamingUrl(stream.getContent(),
|
||||
Objects.requireNonNull(stream.getItagItem()),
|
||||
streamInfo.getDuration());
|
||||
return buildYoutubeManualDashMediaSource(dataSource,
|
||||
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||
metadata);
|
||||
} catch (final CreationException | IOException | NullPointerException e) {
|
||||
Log.w(TAG, "Error when generating or parsing DASH manifest of "
|
||||
+ "YouTube progressive stream, falling back to a "
|
||||
+ "ProgressiveMediaSource.", e);
|
||||
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
|
||||
metadata);
|
||||
}
|
||||
} else {
|
||||
// Legacy progressive streams, subtitles are handled by
|
||||
// VideoPlaybackResolver
|
||||
return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey,
|
||||
metadata);
|
||||
}
|
||||
case DASH:
|
||||
// If the content is not a URL, uses the DASH delivery method and if the stream
|
||||
// type of the stream is a video stream, it means the content is an OTF stream
|
||||
// so we need to generate the manifest corresponding to the content (which is
|
||||
// the base URL of the OTF stream).
|
||||
|
||||
try {
|
||||
final String manifestString = YoutubeOtfDashManifestCreator
|
||||
.fromOtfStreamingUrl(stream.getContent(),
|
||||
Objects.requireNonNull(stream.getItagItem()),
|
||||
streamInfo.getDuration());
|
||||
return buildYoutubeManualDashMediaSource(dataSource,
|
||||
createDashManifest(manifestString, stream), stream, cacheKey,
|
||||
metadata);
|
||||
} catch (final CreationException | NullPointerException e) {
|
||||
Log.e(TAG,
|
||||
"Error when generating the DASH manifest of YouTube OTF stream", e);
|
||||
throw new IOException(
|
||||
"Error when generating the DASH manifest of YouTube OTF stream "
|
||||
+ stream.getContent(), e);
|
||||
}
|
||||
case HLS:
|
||||
return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource(
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
default:
|
||||
throw new IOException("Unsupported delivery method for YouTube contents: "
|
||||
+ deliveryMethod);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> DashMediaSource buildYoutubeManualDashMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final DashManifest dashManifest,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) {
|
||||
return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest,
|
||||
new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(uri)
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build()
|
||||
);
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static <T extends Stream> ProgressiveMediaSource buildYoutubeProgressiveMediaSource(
|
||||
@NonNull final PlayerDataSource dataSource,
|
||||
@NonNull final T stream,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final MediaItemTag metadata) {
|
||||
return dataSource.getYoutubeProgressiveMediaSourceFactory()
|
||||
.createMediaSource(new MediaItem.Builder()
|
||||
.setTag(metadata)
|
||||
.setUri(Uri.parse(stream.getContent()))
|
||||
.setCustomCacheKey(cacheKey)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver;
|
|||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -22,13 +23,18 @@ import org.schabi.newpipe.player.mediaitem.MediaItemTag;
|
|||
import org.schabi.newpipe.player.mediaitem.StreamInfoTag;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||
import static org.schabi.newpipe.util.ListHelper.removeNonUrlAndTorrentStreams;
|
||||
import static org.schabi.newpipe.util.ListHelper.removeTorrentStreams;
|
||||
|
||||
public class VideoPlaybackResolver implements PlaybackResolver {
|
||||
private static final String TAG = VideoPlaybackResolver.class.getSimpleName();
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
@NonNull
|
||||
|
|
@ -57,17 +63,22 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
@Override
|
||||
@Nullable
|
||||
public MediaSource resolve(@NonNull final StreamInfo info) {
|
||||
final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info);
|
||||
final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info);
|
||||
if (liveSource != null) {
|
||||
streamSourceType = SourceType.LIVE_STREAM;
|
||||
return liveSource;
|
||||
}
|
||||
|
||||
final List<MediaSource> mediaSources = new ArrayList<>();
|
||||
final List<VideoStream> videoStreams = new ArrayList<>(info.getVideoStreams());
|
||||
final List<VideoStream> videoOnlyStreams = new ArrayList<>(info.getVideoOnlyStreams());
|
||||
|
||||
removeTorrentStreams(videoStreams);
|
||||
removeTorrentStreams(videoOnlyStreams);
|
||||
|
||||
// Create video stream source
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false, true);
|
||||
videoStreams, videoOnlyStreams, false, true);
|
||||
final int index;
|
||||
if (videos.isEmpty()) {
|
||||
index = -1;
|
||||
|
|
@ -82,24 +93,34 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
.orElse(null);
|
||||
|
||||
if (video != null) {
|
||||
final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, video),
|
||||
MediaFormat.getSuffixById(video.getFormatId()), tag);
|
||||
mediaSources.add(streamSource);
|
||||
try {
|
||||
final MediaSource streamSource = PlaybackResolver.buildMediaSource(
|
||||
dataSource, video, info, PlayerHelper.cacheKeyOf(info, video), tag);
|
||||
mediaSources.add(streamSource);
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Unable to create video source:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Create optional audio stream source
|
||||
final List<AudioStream> audioStreams = info.getAudioStreams();
|
||||
removeTorrentStreams(audioStreams);
|
||||
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
||||
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
||||
|
||||
// Use the audio stream if there is no video stream, or
|
||||
// Merge with audio stream in case if video does not contain audio
|
||||
if (audio != null && (video == null || video.isVideoOnly)) {
|
||||
final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()), tag);
|
||||
mediaSources.add(audioSource);
|
||||
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||
// merge with audio stream in case if video does not contain audio
|
||||
if (audio != null && (video == null || video.isVideoOnly())) {
|
||||
try {
|
||||
final MediaSource audioSource = PlaybackResolver.buildMediaSource(
|
||||
dataSource, audio, info, PlayerHelper.cacheKeyOf(info, audio), tag);
|
||||
mediaSources.add(audioSource);
|
||||
streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO;
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Unable to create audio source:", e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY;
|
||||
}
|
||||
|
|
@ -111,33 +132,35 @@ public class VideoPlaybackResolver implements PlaybackResolver {
|
|||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
if (info.getSubtitles() != null) {
|
||||
for (final SubtitlesStream subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat());
|
||||
if (mimeType == null) {
|
||||
continue;
|
||||
final List<SubtitlesStream> subtitlesStreams = info.getSubtitles();
|
||||
if (subtitlesStreams != null) {
|
||||
// Torrent and non URL subtitles are not supported by ExoPlayer
|
||||
final List<SubtitlesStream> nonTorrentAndUrlStreams = removeNonUrlAndTorrentStreams(
|
||||
subtitlesStreams);
|
||||
for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) {
|
||||
final MediaFormat mediaFormat = subtitle.getFormat();
|
||||
if (mediaFormat != null) {
|
||||
@C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated()
|
||||
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
|
||||
: C.ROLE_FLAG_CAPTION;
|
||||
final MediaItem.SubtitleConfiguration textMediaItem =
|
||||
new MediaItem.SubtitleConfiguration.Builder(
|
||||
Uri.parse(subtitle.getContent()))
|
||||
.setMimeType(mediaFormat.getMimeType())
|
||||
.setRoleFlags(textRoleFlag)
|
||||
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
|
||||
.build();
|
||||
final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory()
|
||||
.createMediaSource(textMediaItem, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated()
|
||||
? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND
|
||||
: C.ROLE_FLAG_CAPTION;
|
||||
final MediaItem.SubtitleConfiguration textMediaItem =
|
||||
new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl()))
|
||||
.setMimeType(mimeType)
|
||||
.setRoleFlags(textRoleFlag)
|
||||
.setLanguage(PlayerHelper.captionLanguageOf(context, subtitle))
|
||||
.build();
|
||||
final MediaSource textSource = dataSource
|
||||
.getSampleMediaSourceFactory()
|
||||
.createMediaSource(textMediaItem, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSources.size() == 1) {
|
||||
return mediaSources.get(0);
|
||||
} else {
|
||||
return new MergingMediaSource(mediaSources.toArray(
|
||||
new MediaSource[0]));
|
||||
return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue