Improve documentation and adress most of the requested changes
Also fix some issues in several places, in the code and the documentation.
This commit is contained in:
parent
6985167e63
commit
aa4c10e751
20 changed files with 759 additions and 522 deletions
|
@ -120,9 +120,9 @@ public class BandcampStreamExtractor extends StreamExtractor {
|
|||
public String getThumbnailUrl() throws ParsingException {
|
||||
if (albumJson.isNull("art_id")) {
|
||||
return EMPTY_STRING;
|
||||
} else {
|
||||
return getImageUrl(albumJson.getLong("art_id"), true);
|
||||
}
|
||||
|
||||
return getImageUrl(albumJson.getLong("art_id"), true);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
|
|
@ -12,15 +12,16 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
|||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
@ -58,9 +59,9 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
|||
final JsonObject roomObject = rooms.getObject(r);
|
||||
if (getId().equals(conferenceObject.getString("slug") + "/"
|
||||
+ roomObject.getString("slug"))) {
|
||||
this.conference = conferenceObject;
|
||||
this.group = groupObject;
|
||||
this.room = roomObject;
|
||||
conference = conferenceObject;
|
||||
group = groupObject;
|
||||
room = roomObject;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -109,122 +110,120 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
|||
* Get the URL of the first DASH stream found.
|
||||
*
|
||||
* <p>
|
||||
* There can be several DASH streams, so the URL of the first found is returned by this method.
|
||||
* There can be several DASH streams, so the URL of the first one found is returned by this
|
||||
* method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* You can find the other video DASH streams by using {@link #getVideoStreams()}
|
||||
* You can find the other DASH video streams by using {@link #getVideoStreams()}
|
||||
* </p>
|
||||
*/
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getDashMpdUrl() throws ParsingException {
|
||||
|
||||
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
|
||||
final JsonObject stream = room.getArray(STREAMS).getObject(s);
|
||||
final JsonObject urls = stream.getObject(URLS);
|
||||
if (urls.has("dash")) {
|
||||
return urls.getObject("dash").getString(URL, EMPTY_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
return EMPTY_STRING;
|
||||
return getManifestOfDeliveryMethodWanted("dash");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the first HLS stream found.
|
||||
*
|
||||
* <p>
|
||||
* There can be several HLS streams, so the URL of the first found is returned by this method.
|
||||
* There can be several HLS streams, so the URL of the first one found is returned by this
|
||||
* method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* You can find the other video HLS streams by using {@link #getVideoStreams()}
|
||||
* You can find the other HLS video streams by using {@link #getVideoStreams()}
|
||||
* </p>
|
||||
*/
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getHlsUrl() {
|
||||
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
|
||||
final JsonObject stream = room.getArray(STREAMS).getObject(s);
|
||||
final JsonObject urls = stream.getObject(URLS);
|
||||
if (urls.has("hls")) {
|
||||
return urls.getObject("hls").getString(URL, EMPTY_STRING);
|
||||
}
|
||||
}
|
||||
return EMPTY_STRING;
|
||||
return getManifestOfDeliveryMethodWanted("hls");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryMethod) {
|
||||
return room.getArray(STREAMS).stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(streamObject -> streamObject.getObject(URLS))
|
||||
.filter(urls -> urls.has(deliveryMethod))
|
||||
.map(urls -> urls.getObject(deliveryMethod).getString(URL, EMPTY_STRING))
|
||||
.findFirst()
|
||||
.orElse(EMPTY_STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
|
||||
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||
IntStream.range(0, room.getArray(STREAMS).size())
|
||||
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
|
||||
.filter(streamJsonObject -> streamJsonObject.getString("type").equals("audio"))
|
||||
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
|
||||
.forEach(type -> {
|
||||
final JsonObject urlObject = streamJsonObject.getObject(URLS)
|
||||
.getObject(type);
|
||||
// The DASH manifest will be extracted with getDashMpdUrl
|
||||
if (!type.equals("dash")) {
|
||||
final AudioStream.Builder builder = new AudioStream.Builder()
|
||||
.setId(urlObject.getString("tech", ID_UNKNOWN))
|
||||
.setContent(urlObject.getString(URL), true)
|
||||
.setAverageBitrate(UNKNOWN_BITRATE);
|
||||
if (type.equals("hls")) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
builder.setDeliveryMethod(DeliveryMethod.HLS);
|
||||
} else {
|
||||
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
|
||||
}
|
||||
return getStreams("audio",
|
||||
dto -> {
|
||||
final AudioStream.Builder builder = new AudioStream.Builder()
|
||||
.setId(dto.getUrlValue().getString("tech", ID_UNKNOWN))
|
||||
.setContent(dto.getUrlValue().getString(URL), true)
|
||||
.setAverageBitrate(UNKNOWN_BITRATE);
|
||||
|
||||
audioStreams.add(builder.build());
|
||||
}
|
||||
}));
|
||||
if ("hls".equals(dto.getUrlKey())) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
return builder.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build();
|
||||
}
|
||||
|
||||
return audioStreams;
|
||||
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey()))
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
|
||||
final List<VideoStream> videoStreams = new ArrayList<>();
|
||||
IntStream.range(0, room.getArray(STREAMS).size())
|
||||
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
|
||||
.filter(stream -> stream.getString("type").equals("video"))
|
||||
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
|
||||
.forEach(type -> {
|
||||
final String resolution =
|
||||
streamJsonObject.getArray("videoSize").getInt(0)
|
||||
+ "x"
|
||||
+ streamJsonObject.getArray("videoSize").getInt(1);
|
||||
final JsonObject urlObject = streamJsonObject.getObject(URLS)
|
||||
.getObject(type);
|
||||
// The DASH manifest will be extracted with getDashMpdUrl
|
||||
if (!type.equals("dash")) {
|
||||
final VideoStream.Builder builder = new VideoStream.Builder()
|
||||
.setId(urlObject.getString("tech", ID_UNKNOWN))
|
||||
.setContent(urlObject.getString(URL), true)
|
||||
.setIsVideoOnly(false)
|
||||
.setResolution(resolution);
|
||||
return getStreams("video",
|
||||
dto -> {
|
||||
final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize");
|
||||
|
||||
if (type.equals("hls")) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
builder.setDeliveryMethod(DeliveryMethod.HLS);
|
||||
} else {
|
||||
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
|
||||
}
|
||||
final VideoStream.Builder builder = new VideoStream.Builder()
|
||||
.setId(dto.getUrlValue().getString("tech", ID_UNKNOWN))
|
||||
.setContent(dto.getUrlValue().getString(URL), true)
|
||||
.setIsVideoOnly(false)
|
||||
.setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1));
|
||||
|
||||
videoStreams.add(builder.build());
|
||||
}
|
||||
}));
|
||||
if ("hls".equals(dto.getUrlKey())) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
return builder.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build();
|
||||
}
|
||||
|
||||
return videoStreams;
|
||||
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey()))
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
private <T extends Stream> List<T> getStreams(
|
||||
@Nonnull final String streamType,
|
||||
@Nonnull final Function<MediaCCCLiveStreamMapperDTO, T> converter) {
|
||||
return room.getArray(STREAMS).stream()
|
||||
// Ensure that we use only process JsonObjects
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
// Only process audio streams
|
||||
.filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type")))
|
||||
// Flatmap Urls and ensure that we use only process JsonObjects
|
||||
.flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream()
|
||||
.filter(e -> e.getValue() instanceof JsonObject)
|
||||
.map(e -> new MediaCCCLiveStreamMapperDTO(
|
||||
streamJsonObj,
|
||||
e.getKey(),
|
||||
(JsonObject) e.getValue())))
|
||||
// The DASH manifest will be extracted with getDashMpdUrl
|
||||
.filter(dto -> !"dash".equals(dto.getUrlKey()))
|
||||
// Convert
|
||||
.map(converter)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
final class MediaCCCLiveStreamMapperDTO {
|
||||
private final JsonObject streamJsonObj;
|
||||
private final String urlKey;
|
||||
private final JsonObject urlValue;
|
||||
|
||||
MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj,
|
||||
final String urlKey,
|
||||
final JsonObject urlValue) {
|
||||
this.streamJsonObj = streamJsonObj;
|
||||
this.urlKey = urlKey;
|
||||
this.urlValue = urlValue;
|
||||
}
|
||||
|
||||
JsonObject getStreamJsonObj() {
|
||||
return streamJsonObj;
|
||||
}
|
||||
|
||||
String getUrlKey() {
|
||||
return urlKey;
|
||||
}
|
||||
|
||||
JsonObject getUrlValue() {
|
||||
return urlValue;
|
||||
}
|
||||
}
|
|
@ -102,7 +102,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
|||
final JsonObject recording = recordings.getObject(i);
|
||||
final String mimeType = recording.getString("mime_type");
|
||||
if (mimeType.startsWith("audio")) {
|
||||
// First we need to resolve the actual video data from CDN
|
||||
// First we need to resolve the actual video data from the CDN
|
||||
final MediaFormat mediaFormat;
|
||||
if (mimeType.endsWith("opus")) {
|
||||
mediaFormat = MediaFormat.OPUS;
|
||||
|
@ -115,7 +115,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
|
||||
// Don't use the containsSimilarStream method because it will always return
|
||||
// false so if there are multiples audio streams available, only the first will
|
||||
// false. So if there are multiple audio streams available, only the first one will
|
||||
// be extracted in this case.
|
||||
audioStreams.add(new AudioStream.Builder()
|
||||
.setId(recording.getString("filename", ID_UNKNOWN))
|
||||
|
@ -136,7 +136,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
|||
final JsonObject recording = recordings.getObject(i);
|
||||
final String mimeType = recording.getString("mime_type");
|
||||
if (mimeType.startsWith("video")) {
|
||||
// First we need to resolve the actual video data from CDN
|
||||
// First we need to resolve the actual video data from the CDN
|
||||
|
||||
final MediaFormat mediaFormat;
|
||||
if (mimeType.endsWith("webm")) {
|
||||
|
@ -148,7 +148,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
|
||||
// Don't use the containsSimilarStream method because it will remove the
|
||||
// extraction of some video versions (mostly languages)
|
||||
// extraction of some video versions (mostly languages). So if there are multiple
|
||||
// video streams available, only the first one will be extracted in this case.
|
||||
videoStreams.add(new VideoStream.Builder()
|
||||
.setId(recording.getString("filename", ID_UNKNOWN))
|
||||
.setContent(recording.getString("recording_url"), true)
|
||||
|
|
|
@ -220,10 +220,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
if (getStreamType() == StreamType.VIDEO_STREAM
|
||||
&& !isNullOrEmpty(json.getObject(FILES))) {
|
||||
return json.getObject(FILES).getString(PLAYLIST_URL, EMPTY_STRING);
|
||||
} else {
|
||||
return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL,
|
||||
EMPTY_STRING);
|
||||
}
|
||||
|
||||
return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL,
|
||||
EMPTY_STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -231,7 +231,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
assertPageFetched();
|
||||
|
||||
/*
|
||||
Some videos have audio streams, some videos don't have audio streams.
|
||||
Some videos have audio streams; others don't.
|
||||
So an audio stream may be available if a video stream is available.
|
||||
Audio streams are also not returned as separated streams for livestreams.
|
||||
That's why the extraction of audio streams is only run when there are video streams
|
||||
|
@ -435,23 +435,21 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
private void extractLiveVideoStreams() throws ParsingException {
|
||||
try {
|
||||
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
|
||||
for (final Object s : streamingPlaylists) {
|
||||
if (!(s instanceof JsonObject)) {
|
||||
continue;
|
||||
}
|
||||
final JsonObject stream = (JsonObject) s;
|
||||
// Don't use the containsSimilarStream method because it will always return false
|
||||
// so if there are multiples HLS URLs returned, only the first will be extracted in
|
||||
// this case.
|
||||
videoStreams.add(new VideoStream.Builder()
|
||||
.setId(String.valueOf(stream.getInt("id", -1)))
|
||||
.setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true)
|
||||
.setIsVideoOnly(false)
|
||||
.setResolution(EMPTY_STRING)
|
||||
.setMediaFormat(MediaFormat.MPEG_4)
|
||||
.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build());
|
||||
}
|
||||
streamingPlaylists.stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(stream -> new VideoStream.Builder()
|
||||
.setId(String.valueOf(stream.getInt("id", -1)))
|
||||
.setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true)
|
||||
.setIsVideoOnly(false)
|
||||
.setResolution(EMPTY_STRING)
|
||||
.setMediaFormat(MediaFormat.MPEG_4)
|
||||
.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build())
|
||||
// Don't use the containsSimilarStream method because it will always return
|
||||
// false so if there are multiples HLS URLs returned, only the first will be
|
||||
// extracted in this case.
|
||||
.forEachOrdered(videoStreams::add);
|
||||
} catch (final Exception e) {
|
||||
throw new ParsingException("Could not get video streams", e);
|
||||
}
|
||||
|
@ -463,14 +461,11 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
|
||||
// HLS streams
|
||||
try {
|
||||
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
|
||||
for (final Object p : streamingPlaylists) {
|
||||
if (!(p instanceof JsonObject)) {
|
||||
continue;
|
||||
}
|
||||
final JsonObject playlist = (JsonObject) p;
|
||||
final String playlistUrl = playlist.getString(PLAYLIST_URL);
|
||||
getStreamsFromArray(playlist.getArray(FILES), playlistUrl);
|
||||
for (final JsonObject playlist : json.getArray(STREAMING_PLAYLISTS).stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.collect(Collectors.toList())) {
|
||||
getStreamsFromArray(playlist.getArray(FILES), playlist.getString(PLAYLIST_URL));
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
throw new ParsingException("Could not get streams", e);
|
||||
|
@ -481,39 +476,31 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
final String playlistUrl) throws ParsingException {
|
||||
try {
|
||||
/*
|
||||
Starting with version 3.4.0 of PeerTube, HLS playlist of stream resolutions contain the
|
||||
UUID of the stream, so we can't use the same system to get HLS playlist URL of streams
|
||||
without fetching the master playlist.
|
||||
These UUIDs are the same that the ones returned into the fileUrl and fileDownloadUrl
|
||||
Starting with version 3.4.0 of PeerTube, the HLS playlist of stream resolutions
|
||||
contains the UUID of the streams, so we can't use the same method to get the URL of
|
||||
the HLS playlist without fetching the master playlist.
|
||||
These UUIDs are the same as the ones returned into the fileUrl and fileDownloadUrl
|
||||
strings.
|
||||
*/
|
||||
final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl)
|
||||
&& playlistUrl.endsWith("-master.m3u8");
|
||||
|
||||
for (final Object s : streams) {
|
||||
if (!(s instanceof JsonObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final JsonObject stream = (JsonObject) s;
|
||||
final String resolution = JsonUtils.getString(stream, "resolution.label");
|
||||
final String url;
|
||||
final String idSuffix;
|
||||
for (final JsonObject stream : streams.stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.collect(Collectors.toList())) {
|
||||
|
||||
// Extract stream version of streams first
|
||||
if (stream.has(FILE_URL)) {
|
||||
url = JsonUtils.getString(stream, FILE_URL);
|
||||
idSuffix = FILE_URL;
|
||||
} else {
|
||||
url = JsonUtils.getString(stream, FILE_DOWNLOAD_URL);
|
||||
idSuffix = FILE_DOWNLOAD_URL;
|
||||
}
|
||||
|
||||
final String url = JsonUtils.getString(stream,
|
||||
stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL);
|
||||
if (isNullOrEmpty(url)) {
|
||||
// Not a valid stream URL
|
||||
return;
|
||||
}
|
||||
|
||||
final String resolution = JsonUtils.getString(stream, "resolution.label");
|
||||
final String idSuffix = stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL;
|
||||
|
||||
if (resolution.toLowerCase().contains("audio")) {
|
||||
// An audio stream
|
||||
addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution,
|
||||
|
@ -535,12 +522,9 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
@Nonnull final String idSuffix,
|
||||
@Nonnull final String format,
|
||||
@Nonnull final String url) throws ParsingException {
|
||||
final String streamUrl;
|
||||
if (FILE_DOWNLOAD_URL.equals(idSuffix)) {
|
||||
streamUrl = JsonUtils.getString(streamJsonObject, FILE_URL);
|
||||
} else {
|
||||
streamUrl = url;
|
||||
}
|
||||
final String streamUrl = FILE_DOWNLOAD_URL.equals(idSuffix)
|
||||
? JsonUtils.getString(streamJsonObject, FILE_URL)
|
||||
: url;
|
||||
return streamUrl.replace("-fragmented." + format, ".m3u8");
|
||||
}
|
||||
|
||||
|
@ -593,7 +577,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
// Add finally torrent URLs
|
||||
// Finally, add torrent URLs
|
||||
final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl");
|
||||
if (!isNullOrEmpty(torrentUrl)) {
|
||||
audioStreams.add(new AudioStream.Builder()
|
||||
|
@ -627,14 +611,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
|
|||
|
||||
// Then add HLS streams
|
||||
if (!isNullOrEmpty(playlistUrl)) {
|
||||
final String hlsStreamUrl;
|
||||
if (isInstanceUsingRandomUuidsForHlsStreams) {
|
||||
hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix,
|
||||
extension, url);
|
||||
} else {
|
||||
hlsStreamUrl = playlistUrl.replace("master", JsonUtils.getNumber(
|
||||
streamJsonObject, RESOLUTION_ID).toString());
|
||||
}
|
||||
final String hlsStreamUrl = isInstanceUsingRandomUuidsForHlsStreams
|
||||
? getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, extension,
|
||||
url)
|
||||
: getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl);
|
||||
|
||||
final VideoStream videoStream = new VideoStream.Builder()
|
||||
.setId(id + "-" + DeliveryMethod.HLS)
|
||||
|
|
|
@ -233,11 +233,10 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
@Nullable
|
||||
private String getDownloadUrl(@Nonnull final String trackId)
|
||||
throws IOException, ExtractionException {
|
||||
final Downloader dl = NewPipe.getDownloader();
|
||||
final JsonObject downloadJsonObject;
|
||||
final String response = NewPipe.getDownloader().get(SOUNDCLOUD_API_V2_URL + "tracks/"
|
||||
+ trackId + "/download" + "?client_id=" + clientId()).responseBody();
|
||||
|
||||
final String response = dl.get(SOUNDCLOUD_API_V2_URL + "tracks/" + trackId
|
||||
+ "/download" + "?client_id=" + clientId()).responseBody();
|
||||
final JsonObject downloadJsonObject;
|
||||
try {
|
||||
downloadJsonObject = JsonParser.object().from(response);
|
||||
} catch (final JsonParserException e) {
|
||||
|
@ -293,7 +292,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
}
|
||||
} catch (final Exception ignored) {
|
||||
// Something went wrong when parsing this transcoding, don't add it to the
|
||||
// Something went wrong when parsing this transcoding URL, so don't add it to the
|
||||
// audioStreams
|
||||
}
|
||||
}
|
||||
|
@ -304,11 +303,15 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
*
|
||||
* <p>
|
||||
* A track can have the {@code downloadable} boolean set to {@code true}, but it doesn't mean
|
||||
* we can download it: if the value of the {@code has_download_left} boolean is true, the track
|
||||
* can be downloaded; otherwise not.
|
||||
* we can download it.
|
||||
* </p>
|
||||
*
|
||||
* @param audioStreams the audio streams to which add the downloadable file
|
||||
* <p>
|
||||
* If the value of the {@code has_download_left} boolean is {@code true}, the track can be
|
||||
* downloaded, and not otherwise.
|
||||
* </p>
|
||||
*
|
||||
* @param audioStreams the audio streams to which the downloadable file is added
|
||||
*/
|
||||
public void extractDownloadableFileIfAvailable(final List<AudioStream> audioStreams) {
|
||||
if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) {
|
||||
|
@ -332,9 +335,9 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
|
|||
* Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
|
||||
*
|
||||
* <p>
|
||||
* This method downloads the provided manifest URL, find all web occurrences in the manifest,
|
||||
* get the last segment URL, changes its segment range to {@code 0/track-length} and return
|
||||
* this string.
|
||||
* This method downloads the provided manifest URL, finds all web occurrences in the manifest,
|
||||
* gets the last segment URL, changes its segment range to {@code 0/track-length}, and return
|
||||
* this as a string.
|
||||
* </p>
|
||||
*
|
||||
* @param hlsManifestUrl the URL of the manifest to be parsed
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
|||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
@ -52,10 +51,26 @@ import javax.xml.transform.TransformerFactory;
|
|||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
|
||||
/**
|
||||
* Class to extract streams from a DASH manifest.
|
||||
*
|
||||
* <p>
|
||||
* Note that this class relies on the YouTube's {@link ItagItem} class and should be made generic
|
||||
* in order to be used on other services.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This class is not used by the extractor itself, as all streams are supported by the extractor.
|
||||
* </p>
|
||||
*/
|
||||
public final class DashMpdParser {
|
||||
private DashMpdParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception class which is thrown when something went wrong when using
|
||||
* {@link DashMpdParser#getStreams(String)}.
|
||||
*/
|
||||
public static class DashMpdParsingException extends ParsingException {
|
||||
|
||||
DashMpdParsingException(final String message, final Exception e) {
|
||||
|
@ -63,15 +78,21 @@ public final class DashMpdParser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which represents the result of a DASH MPD file parsing by {@link DashMpdParser}.
|
||||
*
|
||||
* <p>
|
||||
* The result contains video, video-only and audio streams.
|
||||
* </p>
|
||||
*/
|
||||
public static class Result {
|
||||
private final List<VideoStream> videoStreams;
|
||||
private final List<VideoStream> videoOnlyStreams;
|
||||
private final List<AudioStream> audioStreams;
|
||||
|
||||
|
||||
public Result(final List<VideoStream> videoStreams,
|
||||
final List<VideoStream> videoOnlyStreams,
|
||||
final List<AudioStream> audioStreams) {
|
||||
Result(final List<VideoStream> videoStreams,
|
||||
final List<VideoStream> videoOnlyStreams,
|
||||
final List<AudioStream> audioStreams) {
|
||||
this.videoStreams = videoStreams;
|
||||
this.videoOnlyStreams = videoOnlyStreams;
|
||||
this.audioStreams = audioStreams;
|
||||
|
@ -90,19 +111,22 @@ public final class DashMpdParser {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Make this class generic and decouple from YouTube's ItagItem class.
|
||||
|
||||
/**
|
||||
* Will try to download and parse the DASH manifest (using {@link StreamInfo#getDashMpdUrl()}),
|
||||
* adding items that are listed in the {@link ItagItem} class.
|
||||
* <p>
|
||||
* It has video, video only and audio streams.
|
||||
* <p>
|
||||
* Info about DASH MPD can be found here
|
||||
* This method will try to download and parse the YouTube DASH MPD manifest URL provided to get
|
||||
* supported {@link AudioStream}s and {@link VideoStream}s.
|
||||
*
|
||||
* @param dashMpdUrl URL to the DASH MPD
|
||||
* <p>
|
||||
* The parser supports video, video-only and audio streams.
|
||||
* </p>
|
||||
*
|
||||
* @param dashMpdUrl the URL of the DASH MPD manifest
|
||||
* @return a {@link Result} which contains all video, video-only and audio streams extracted
|
||||
* and supported by the extractor (so the ones for which {@link ItagItem#isSupported(int)}
|
||||
* returns {@code true}).
|
||||
* @throws DashMpdParsingException if something went wrong when downloading or parsing the
|
||||
* manifest
|
||||
* @see <a href="https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html">
|
||||
* www.brendanlog.com</a>
|
||||
* www.brendanlong.com's page about the structure of an MPEG-DASH MPD manifest</a>
|
||||
*/
|
||||
@Nonnull
|
||||
public static Result getStreams(final String dashMpdUrl)
|
||||
|
@ -188,7 +212,7 @@ public final class DashMpdParser {
|
|||
throws TransformerException {
|
||||
final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0);
|
||||
|
||||
// Clone element so we can freely modify it
|
||||
// Clone the element so we can freely modify it
|
||||
final Element adaptationSet = (Element) representation.getParentNode();
|
||||
final Element adaptationSetClone = (Element) adaptationSet.cloneNode(true);
|
||||
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Serializable;
|
||||
|
||||
public final class ItagInfo implements Serializable {
|
||||
|
||||
@Nonnull
|
||||
private final String content;
|
||||
@Nonnull
|
||||
private final ItagItem itagItem;
|
||||
private boolean isUrl;
|
||||
|
||||
public ItagInfo(@Nonnull final String content,
|
||||
@Nonnull final ItagItem itagItem) {
|
||||
this.content = content;
|
||||
this.itagItem = itagItem;
|
||||
}
|
||||
|
||||
public void setIsUrl(final boolean isUrl) {
|
||||
this.isUrl = isUrl;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ItagItem getItagItem() {
|
||||
return itagItem;
|
||||
}
|
||||
|
||||
public boolean getIsUrl() {
|
||||
return isUrl;
|
||||
}
|
||||
}
|
|
@ -237,11 +237,15 @@ public class ItagItem implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the frame rate per second.
|
||||
* Get the frame rate.
|
||||
*
|
||||
* <p>
|
||||
* It defaults to the standard value associated with this itag and is set to the {@code fps}
|
||||
* value returned in the corresponding itag in the YouTube player response.
|
||||
* It is set to the {@code fps} value returned in the corresponding itag in the YouTube player
|
||||
* response.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It defaults to the standard value associated with this itag.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -249,28 +253,24 @@ public class ItagItem implements Serializable {
|
|||
* #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags.
|
||||
* </p>
|
||||
*
|
||||
* @return the frame rate per second or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
|
||||
* @return the frame rate or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
|
||||
*/
|
||||
public int getFps() {
|
||||
return fps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the frame rate per second.
|
||||
* Set the frame rate.
|
||||
*
|
||||
* <p>
|
||||
* It is only known for video itags, so {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} is set/used for
|
||||
* non video itags or if the sample rate value is less than or equal to 0.
|
||||
* </p>
|
||||
*
|
||||
* @param fps the frame rate per second
|
||||
* @param fps the frame rate
|
||||
*/
|
||||
public void setFps(final int fps) {
|
||||
if (fps > 0) {
|
||||
this.fps = fps;
|
||||
} else {
|
||||
this.fps = FPS_NOT_APPLICABLE_OR_UNKNOWN;
|
||||
}
|
||||
this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN;
|
||||
}
|
||||
|
||||
public int getInitStart() {
|
||||
|
@ -314,13 +314,13 @@ public class ItagItem implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the resolution string associated to this {@code ItagItem}.
|
||||
* Get the resolution string associated with this {@code ItagItem}.
|
||||
*
|
||||
* <p>
|
||||
* It is only known for video itags.
|
||||
* </p>
|
||||
*
|
||||
* @return the resolution string associated to this {@code ItagItem} or
|
||||
* @return the resolution string associated with this {@code ItagItem} or
|
||||
* {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
|
@ -361,7 +361,7 @@ public class ItagItem implements Serializable {
|
|||
*
|
||||
* <p>
|
||||
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is returned for non audio
|
||||
* itags or if the sample rate is unknown.
|
||||
* itags, or if the sample rate is unknown.
|
||||
* </p>
|
||||
*
|
||||
* @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN}
|
||||
|
@ -374,8 +374,8 @@ public class ItagItem implements Serializable {
|
|||
* Set the sample rate.
|
||||
*
|
||||
* <p>
|
||||
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non video
|
||||
* itags or if the sample rate value is less than or equal to 0.
|
||||
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non audio
|
||||
* itags, or if the sample rate value is less than or equal to 0.
|
||||
* </p>
|
||||
*
|
||||
* @param sampleRate the sample rate of an audio itag
|
||||
|
@ -392,8 +392,8 @@ public class ItagItem implements Serializable {
|
|||
* Get the number of audio channels.
|
||||
*
|
||||
* <p>
|
||||
* It is only known for audio streams, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
|
||||
* returned for video streams or if it is unknown.
|
||||
* It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
|
||||
* returned for non audio itags, or if it is unknown.
|
||||
* </p>
|
||||
*
|
||||
* @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN}
|
||||
|
@ -406,28 +406,26 @@ public class ItagItem implements Serializable {
|
|||
* Set the number of audio channels.
|
||||
*
|
||||
* <p>
|
||||
* It is only known for audio itag, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
|
||||
* set/used for non audio itags or if the {@code audioChannels} value is less than or equal to
|
||||
* It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
|
||||
* set/used for non audio itags, or if the {@code audioChannels} value is less than or equal to
|
||||
* 0.
|
||||
* </p>
|
||||
*
|
||||
* @param audioChannels the number of audio channels of an audio itag
|
||||
*/
|
||||
public void setAudioChannels(final int audioChannels) {
|
||||
if (audioChannels > 0) {
|
||||
this.audioChannels = audioChannels;
|
||||
} else {
|
||||
this.audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
|
||||
}
|
||||
this.audioChannels = audioChannels > 0
|
||||
? audioChannels
|
||||
: AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code targetDurationSec} value.
|
||||
*
|
||||
* <p>
|
||||
* This value is an average time in seconds of sequences duration of livestreams and ended
|
||||
* livestreams. It is only returned for these stream types by YouTube and makes no sense for
|
||||
* videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for video streams.
|
||||
* This value is the average time in seconds of the duration of sequences of livestreams and
|
||||
* ended livestreams. It is only returned by YouTube for these stream types, and makes no sense
|
||||
* for videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for those.
|
||||
* </p>
|
||||
*
|
||||
* @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN}
|
||||
|
@ -440,25 +438,23 @@ public class ItagItem implements Serializable {
|
|||
* Set the {@code targetDurationSec} value.
|
||||
*
|
||||
* <p>
|
||||
* This value is an average time in seconds of sequences duration of livestreams and ended
|
||||
* livestreams.
|
||||
* This value is the average time in seconds of the duration of sequences of livestreams and
|
||||
* ended livestreams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It is only returned for these stream types by YouTube and makes no sense for
|
||||
* videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if
|
||||
* this value is less than or equal to 0.
|
||||
* It is only returned for these stream types by YouTube and makes no sense for videos, so
|
||||
* {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if this value is
|
||||
* less than or equal to 0.
|
||||
* </p>
|
||||
*
|
||||
* @param targetDurationSec the target duration of a segment of streams which are using the
|
||||
* live delivery method type
|
||||
*/
|
||||
public void setTargetDurationSec(final int targetDurationSec) {
|
||||
if (targetDurationSec > 0) {
|
||||
this.targetDurationSec = targetDurationSec;
|
||||
} else {
|
||||
this.targetDurationSec = TARGET_DURATION_SEC_UNKNOWN;
|
||||
}
|
||||
this.targetDurationSec = targetDurationSec > 0
|
||||
? targetDurationSec
|
||||
: TARGET_DURATION_SEC_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -487,11 +483,9 @@ public class ItagItem implements Serializable {
|
|||
* milliseconds
|
||||
*/
|
||||
public void setApproxDurationMs(final long approxDurationMs) {
|
||||
if (approxDurationMs > 0) {
|
||||
this.approxDurationMs = approxDurationMs;
|
||||
} else {
|
||||
this.approxDurationMs = APPROX_DURATION_MS_UNKNOWN;
|
||||
}
|
||||
this.approxDurationMs = approxDurationMs > 0
|
||||
? approxDurationMs
|
||||
: APPROX_DURATION_MS_UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -519,10 +513,6 @@ public class ItagItem implements Serializable {
|
|||
* @param contentLength the content length of a DASH progressive stream
|
||||
*/
|
||||
public void setContentLength(final long contentLength) {
|
||||
if (contentLength > 0) {
|
||||
this.contentLength = contentLength;
|
||||
} else {
|
||||
this.contentLength = CONTENT_LENGTH_UNKNOWN;
|
||||
}
|
||||
this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.*;
|
|||
* It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages.
|
||||
* </p>
|
||||
*/
|
||||
@SuppressWarnings({"ConstantConditions", "unused"})
|
||||
public final class YoutubeDashManifestCreator {
|
||||
|
||||
/**
|
||||
|
@ -115,6 +114,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* </p>
|
||||
*/
|
||||
PROGRESSIVE,
|
||||
|
||||
/**
|
||||
* YouTube's OTF delivery method which uses a sequence parameter to get segments of
|
||||
* streams.
|
||||
|
@ -124,12 +124,14 @@ public final class YoutubeDashManifestCreator {
|
|||
* metadata needed to build the stream source (sidx boxes, segment length, segment count,
|
||||
* duration, ...)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Only used for videos; mostly those with a small amount of views, or ended livestreams
|
||||
* which have just been re-encoded as normal videos.
|
||||
* </p>
|
||||
*/
|
||||
OTF,
|
||||
|
||||
/**
|
||||
* YouTube's delivery method for livestreams which uses a sequence parameter to get
|
||||
* segments of streams.
|
||||
|
@ -139,6 +141,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* metadata (sidx boxes, segment length, ...), which make no need of an initialization
|
||||
* segment.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Only used for livestreams (ended or running).
|
||||
* </p>
|
||||
|
@ -225,27 +228,27 @@ public final class YoutubeDashManifestCreator {
|
|||
*/
|
||||
@Nonnull
|
||||
public static String createDashManifestFromOtfStreamingUrl(
|
||||
@Nonnull String otfBaseStreamingUrl,
|
||||
@Nonnull final String otfBaseStreamingUrl,
|
||||
@Nonnull final ItagItem itagItem,
|
||||
final long durationSecondsFallback)
|
||||
throws YoutubeDashManifestCreationException {
|
||||
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
|
||||
if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) {
|
||||
return GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl).getSecond();
|
||||
return Objects.requireNonNull(GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl))
|
||||
.getSecond();
|
||||
}
|
||||
|
||||
final String originalOtfBaseStreamingUrl = otfBaseStreamingUrl;
|
||||
String realOtfBaseStreamingUrl = otfBaseStreamingUrl;
|
||||
// Try to avoid redirects when streaming the content by saving the last URL we get
|
||||
// from video servers.
|
||||
final Response response = getInitializationResponse(otfBaseStreamingUrl,
|
||||
final Response response = getInitializationResponse(realOtfBaseStreamingUrl,
|
||||
itagItem, DeliveryType.OTF);
|
||||
otfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
|
||||
realOtfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
|
||||
.replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING);
|
||||
|
||||
final int responseCode = response.responseCode();
|
||||
if (responseCode != 200) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Unable to create the DASH manifest: could not get the initialization URL of the OTF stream: response code "
|
||||
+ responseCode);
|
||||
"Unable to create the DASH manifest: could not get the initialization URL of "
|
||||
+ "the OTF stream: response code " + responseCode);
|
||||
}
|
||||
|
||||
final String[] segmentDuration;
|
||||
|
@ -266,7 +269,8 @@ public final class YoutubeDashManifestCreator {
|
|||
}
|
||||
} catch (final Exception e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Unable to generate the DASH manifest: could not get the duration of segments", e);
|
||||
"Unable to generate the DASH manifest: could not get the duration of segments",
|
||||
e);
|
||||
}
|
||||
|
||||
final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF,
|
||||
|
@ -278,7 +282,7 @@ public final class YoutubeDashManifestCreator {
|
|||
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
|
||||
generateAudioChannelConfigurationElement(document, itagItem);
|
||||
}
|
||||
generateSegmentTemplateElement(document, otfBaseStreamingUrl, DeliveryType.OTF);
|
||||
generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF);
|
||||
generateSegmentTimelineElement(document);
|
||||
collectSegmentsData(segmentDuration);
|
||||
generateSegmentElementsForOtfStreams(document);
|
||||
|
@ -286,7 +290,7 @@ public final class YoutubeDashManifestCreator {
|
|||
SEGMENTS_DURATION.clear();
|
||||
DURATION_REPETITIONS.clear();
|
||||
|
||||
return buildResult(originalOtfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS);
|
||||
return buildResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -358,36 +362,37 @@ public final class YoutubeDashManifestCreator {
|
|||
*/
|
||||
@Nonnull
|
||||
public static String createDashManifestFromPostLiveStreamDvrStreamingUrl(
|
||||
@Nonnull String postLiveStreamDvrStreamingUrl,
|
||||
@Nonnull final String postLiveStreamDvrStreamingUrl,
|
||||
@Nonnull final ItagItem itagItem,
|
||||
final int targetDurationSec,
|
||||
final long durationSecondsFallback)
|
||||
throws YoutubeDashManifestCreationException {
|
||||
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
|
||||
if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) {
|
||||
return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(postLiveStreamDvrStreamingUrl)
|
||||
.getSecond();
|
||||
return Objects.requireNonNull(GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(
|
||||
postLiveStreamDvrStreamingUrl)).getSecond();
|
||||
}
|
||||
final String originalPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl;
|
||||
String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl;
|
||||
final String streamDuration;
|
||||
final String segmentCount;
|
||||
|
||||
if (targetDurationSec <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the targetDurationSec value is less than or equal to 0 (" + targetDurationSec + ")");
|
||||
"Could not generate the DASH manifest: the targetDurationSec value is less "
|
||||
+ "than or equal to 0 (" + targetDurationSec + ")");
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to avoid redirects when streaming the content by saving the latest URL we get
|
||||
// from video servers.
|
||||
final Response response = getInitializationResponse(postLiveStreamDvrStreamingUrl,
|
||||
final Response response = getInitializationResponse(realPostLiveStreamDvrStreamingUrl,
|
||||
itagItem, DeliveryType.LIVE);
|
||||
postLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
|
||||
realPostLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
|
||||
.replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING);
|
||||
|
||||
final int responseCode = response.responseCode();
|
||||
if (responseCode != 200) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code "
|
||||
"Could not generate the DASH manifest: could not get the initialization "
|
||||
+ "segment of the post-live-DVR stream: response code "
|
||||
+ responseCode);
|
||||
}
|
||||
|
||||
|
@ -396,15 +401,18 @@ public final class YoutubeDashManifestCreator {
|
|||
segmentCount = responseHeaders.get("X-Head-Seqnum").get(0);
|
||||
} catch (final IndexOutOfBoundsException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR streaming URL", e);
|
||||
"Could not generate the DASH manifest: could not get the value of the "
|
||||
+ "X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR"
|
||||
+ "streaming URL", e);
|
||||
}
|
||||
|
||||
if (isNullOrEmpty(segmentCount)) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the number of segments of the post-live-DVR stream");
|
||||
"Could not generate the DASH manifest: could not get the number of segments of"
|
||||
+ "the post-live-DVR stream");
|
||||
}
|
||||
|
||||
final Document document = generateDocumentAndMpdElement(new String[]{streamDuration},
|
||||
final Document document = generateDocumentAndMpdElement(new String[] {streamDuration},
|
||||
DeliveryType.LIVE, itagItem, durationSecondsFallback);
|
||||
generatePeriodElement(document);
|
||||
generateAdaptationSetElement(document, itagItem);
|
||||
|
@ -413,11 +421,12 @@ public final class YoutubeDashManifestCreator {
|
|||
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
|
||||
generateAudioChannelConfigurationElement(document, itagItem);
|
||||
}
|
||||
generateSegmentTemplateElement(document, postLiveStreamDvrStreamingUrl, DeliveryType.LIVE);
|
||||
generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl,
|
||||
DeliveryType.LIVE);
|
||||
generateSegmentTimelineElement(document);
|
||||
generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount);
|
||||
|
||||
return buildResult(originalPostLiveStreamDvrStreamingUrl, document,
|
||||
return buildResult(postLiveStreamDvrStreamingUrl, document,
|
||||
GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS);
|
||||
}
|
||||
|
||||
|
@ -486,13 +495,14 @@ public final class YoutubeDashManifestCreator {
|
|||
@Nonnull final ItagItem itagItem,
|
||||
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
|
||||
if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) {
|
||||
return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(progressiveStreamingBaseUrl)
|
||||
.getSecond();
|
||||
return Objects.requireNonNull(GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(
|
||||
progressiveStreamingBaseUrl)).getSecond();
|
||||
}
|
||||
|
||||
if (durationSecondsFallback <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the durationSecondsFallback value is less than or equal to 0 (" + durationSecondsFallback + ")");
|
||||
"Could not generate the DASH manifest: the durationSecondsFallback value is"
|
||||
+ "less than or equal to 0 (" + durationSecondsFallback + ")");
|
||||
}
|
||||
|
||||
final Document document = generateDocumentAndMpdElement(new String[]{},
|
||||
|
@ -508,7 +518,8 @@ public final class YoutubeDashManifestCreator {
|
|||
generateSegmentBaseElement(document, itagItem);
|
||||
generateInitializationElement(document, itagItem);
|
||||
|
||||
return buildResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS);
|
||||
return buildResult(progressiveStreamingBaseUrl, document,
|
||||
GENERATED_PROGRESSIVE_STREAMS_MANIFESTS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -564,7 +575,8 @@ public final class YoutubeDashManifestCreator {
|
|||
return downloader.post(baseStreamingUrl, headers, emptyBody);
|
||||
} catch (final IOException | ExtractionException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: error when trying to get the ANDROID streaming post-live-DVR URL response", e);
|
||||
"Could not generate the DASH manifest: error when trying to get the "
|
||||
+ "ANDROID streaming post-live-DVR URL response", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,10 +591,12 @@ public final class YoutubeDashManifestCreator {
|
|||
} catch (final IOException | ExtractionException e) {
|
||||
if (isAnAndroidStreamingUrl) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: error when trying to get the ANDROID streaming URL response", e);
|
||||
"Could not generate the DASH manifest: error when trying to get the "
|
||||
+ "ANDROID streaming URL response", e);
|
||||
} else {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: error when trying to get the streaming URL response", e);
|
||||
"Could not generate the DASH manifest: error when trying to get the "
|
||||
+ "streaming URL response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -658,16 +672,18 @@ public final class YoutubeDashManifestCreator {
|
|||
if (responseCode != 200) {
|
||||
if (deliveryType == DeliveryType.LIVE) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code "
|
||||
+ responseCode);
|
||||
"Could not generate the DASH manifest: could not get the "
|
||||
+ "initialization URL of the post-live-DVR stream: "
|
||||
+ "response code " + responseCode);
|
||||
} else if (deliveryType == DeliveryType.OTF) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the initialization URL of the OTF stream: response code "
|
||||
"Could not generate the DASH manifest: could not get the "
|
||||
+ "initialization URL of the OTF stream: response code "
|
||||
+ responseCode);
|
||||
} else {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not fetch the URL of the progressive stream: response code "
|
||||
+ responseCode);
|
||||
"Could not generate the DASH manifest: could not fetch the URL of "
|
||||
+ "the progressive stream: response code " + responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -678,7 +694,8 @@ public final class YoutubeDashManifestCreator {
|
|||
"Content-Type"));
|
||||
} catch (final NullPointerException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: could not get the Content-Type header from the streaming URL", e);
|
||||
"Could not generate the DASH manifest: could not get the Content-Type "
|
||||
+ "header from the streaming URL", e);
|
||||
}
|
||||
|
||||
// The response body is the redirection URL
|
||||
|
@ -692,16 +709,19 @@ public final class YoutubeDashManifestCreator {
|
|||
|
||||
if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: too many redirects when trying to get the WEB streaming URL response");
|
||||
"Could not generate the DASH manifest: too many redirects when trying to "
|
||||
+ "get the WEB streaming URL response");
|
||||
}
|
||||
|
||||
// This should never be reached, but is required because we don't want to return null
|
||||
// here
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: error when trying to get the WEB streaming URL response");
|
||||
"Could not generate the DASH manifest: error when trying to get the WEB "
|
||||
+ "streaming URL response");
|
||||
} catch (final IOException | ExtractionException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: error when trying to get the WEB streaming URL response", e);
|
||||
"Could not generate the DASH manifest: error when trying to get the WEB "
|
||||
+ "streaming URL response", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -731,7 +751,8 @@ public final class YoutubeDashManifestCreator {
|
|||
}
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: unable to get the segments of the stream", e);
|
||||
"Could not generate the DASH manifest: unable to get the segments of the "
|
||||
+ "stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,7 +788,8 @@ public final class YoutubeDashManifestCreator {
|
|||
return streamLengthMs;
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: unable to get the length of the stream", e);
|
||||
"Could not generate the DASH manifest: unable to get the length of the stream",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -778,6 +800,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* The generated {@code <MPD>} element looks like the manifest returned into the player
|
||||
* response of videos with OTF streams:
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* {@code <MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
* xmlns="urn:mpeg:DASH:schema:MPD:2011"
|
||||
|
@ -787,6 +810,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* (where {@code $duration$} represents the duration in seconds (a number with 3 digits after
|
||||
* the decimal point)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If the duration is an integer or a double with less than 3 digits after the decimal point,
|
||||
* it will be converted into a double with 3 digits after the decimal point.
|
||||
|
@ -859,8 +883,10 @@ public final class YoutubeDashManifestCreator {
|
|||
streamDuration = durationSecondsFallback * 1000;
|
||||
} else {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the MPD element of the DASH manifest to the document: "
|
||||
+ "the duration of the stream could not be determined and the durationSecondsFallback is less than or equal to 0");
|
||||
"Could not generate or append the MPD element of the DASH "
|
||||
+ "manifest to the document: the duration of the stream "
|
||||
+ "could not be determined and the "
|
||||
+ "durationSecondsFallback is less than or equal to 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -870,7 +896,8 @@ public final class YoutubeDashManifestCreator {
|
|||
mpdElement.setAttributeNode(mediaPresentationDurationAttribute);
|
||||
} catch (final Exception e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the MPD element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the MPD element of the DASH manifest to the "
|
||||
+ "document", e);
|
||||
}
|
||||
|
||||
return document;
|
||||
|
@ -898,7 +925,8 @@ public final class YoutubeDashManifestCreator {
|
|||
mpdElement.appendChild(periodElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the Period element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the Period element of the DASH manifest to the "
|
||||
+ "document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -921,7 +949,8 @@ public final class YoutubeDashManifestCreator {
|
|||
@Nonnull final ItagItem itagItem)
|
||||
throws YoutubeDashManifestCreationException {
|
||||
try {
|
||||
final Element periodElement = (Element) document.getElementsByTagName("Period").item(0);
|
||||
final Element periodElement = (Element) document.getElementsByTagName("Period")
|
||||
.item(0);
|
||||
final Element adaptationSetElement = document.createElement("AdaptationSet");
|
||||
|
||||
final Attr idAttribute = document.createAttribute("id");
|
||||
|
@ -931,21 +960,25 @@ public final class YoutubeDashManifestCreator {
|
|||
final MediaFormat mediaFormat = itagItem.getMediaFormat();
|
||||
if (mediaFormat == null || isNullOrEmpty(mediaFormat.mimeType)) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the AdaptationSet element of the DASH manifest to the document: the MediaFormat or the mime type of the MediaFormat of the ItagItem is null or empty");
|
||||
"Could not generate the AdaptationSet element of the DASH manifest to the "
|
||||
+ "document: the MediaFormat or the mime type of the MediaFormat "
|
||||
+ "of the ItagItem is null or empty");
|
||||
}
|
||||
|
||||
final Attr mimeTypeAttribute = document.createAttribute("mimeType");
|
||||
mimeTypeAttribute.setValue(mediaFormat.mimeType);
|
||||
adaptationSetElement.setAttributeNode(mimeTypeAttribute);
|
||||
|
||||
final Attr subsegmentAlignmentAttribute = document.createAttribute("subsegmentAlignment");
|
||||
final Attr subsegmentAlignmentAttribute = document.createAttribute(
|
||||
"subsegmentAlignment");
|
||||
subsegmentAlignmentAttribute.setValue("true");
|
||||
adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute);
|
||||
|
||||
periodElement.appendChild(adaptationSetElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the AdaptationSet element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the AdaptationSet element of the DASH manifest "
|
||||
+ "to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -956,9 +989,11 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This element, with its attributes and values, is:
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>}
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <AdaptationSet>} element needs to be generated before this element with
|
||||
* {@link #generateAdaptationSetElement(Document, ItagItem)}).
|
||||
|
@ -967,7 +1002,8 @@ public final class YoutubeDashManifestCreator {
|
|||
* @param document the {@link Document} on which the the {@code <Role>} element will be
|
||||
* appended
|
||||
* @throws YoutubeDashManifestCreationException if something goes wrong when generating or
|
||||
* appending the {@code <Role>} element to the document
|
||||
* appending the {@code <Role>} element to the
|
||||
* document
|
||||
*/
|
||||
private static void generateRoleElement(@Nonnull final Document document)
|
||||
throws YoutubeDashManifestCreationException {
|
||||
|
@ -987,7 +1023,8 @@ public final class YoutubeDashManifestCreator {
|
|||
adaptationSetElement.appendChild(roleElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the Role element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the Role element of the DASH manifest to the "
|
||||
+ "document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1018,7 +1055,9 @@ public final class YoutubeDashManifestCreator {
|
|||
final int id = itagItem.id;
|
||||
if (id <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the Representation element of the DASH manifest to the document: the id of the ItagItem is less than or equal to 0");
|
||||
"Could not generate the Representation element of the DASH manifest to "
|
||||
+ "the document: the id of the ItagItem is less than or equal to "
|
||||
+ "0");
|
||||
}
|
||||
final Attr idAttribute = document.createAttribute("id");
|
||||
idAttribute.setValue(String.valueOf(id));
|
||||
|
@ -1027,7 +1066,8 @@ public final class YoutubeDashManifestCreator {
|
|||
final String codec = itagItem.getCodec();
|
||||
if (isNullOrEmpty(codec)) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the AdaptationSet element of the DASH manifest to the document: the codecs value is null or empty");
|
||||
"Could not generate the AdaptationSet element of the DASH manifest to the "
|
||||
+ "document: the codec value is null or empty");
|
||||
}
|
||||
final Attr codecsAttribute = document.createAttribute("codecs");
|
||||
codecsAttribute.setValue(codec);
|
||||
|
@ -1044,7 +1084,9 @@ public final class YoutubeDashManifestCreator {
|
|||
final int bitrate = itagItem.getBitrate();
|
||||
if (bitrate <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the Representation element of the DASH manifest to the document: the bitrate of the ItagItem is less than or equal to 0");
|
||||
"Could not generate the Representation element of the DASH manifest to "
|
||||
+ "the document: the bitrate of the ItagItem is less than or "
|
||||
+ "equal to 0");
|
||||
}
|
||||
final Attr bandwidthAttribute = document.createAttribute("bandwidth");
|
||||
bandwidthAttribute.setValue(String.valueOf(bitrate));
|
||||
|
@ -1057,7 +1099,9 @@ public final class YoutubeDashManifestCreator {
|
|||
final int width = itagItem.getWidth();
|
||||
if (height <= 0 && width <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the Representation element of the DASH manifest to the document: the width and the height of the ItagItem are less than or equal to 0");
|
||||
"Could not generate the Representation element of the DASH manifest "
|
||||
+ "to the document: the width and the height of the ItagItem "
|
||||
+ "are less than or equal to 0");
|
||||
}
|
||||
|
||||
if (width > 0) {
|
||||
|
@ -1087,7 +1131,8 @@ public final class YoutubeDashManifestCreator {
|
|||
adaptationSetElement.appendChild(representationElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the Representation element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the Representation element of the DASH manifest "
|
||||
+ "to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1098,6 +1143,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This method is only used when generating DASH manifests of audio streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It will produce the following element:
|
||||
* <br>
|
||||
|
@ -1108,6 +1154,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second
|
||||
* parameter of this method)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <Representation>} element needs to be generated before this element with
|
||||
* {@link #generateRepresentationElement(Document, ItagItem)}).
|
||||
|
@ -1139,7 +1186,8 @@ public final class YoutubeDashManifestCreator {
|
|||
final int audioChannels = itagItem.getAudioChannels();
|
||||
if (audioChannels <= 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the audioChannels value is less than or equal to 0 (" + audioChannels + ")");
|
||||
"Could not generate the DASH manifest: the audioChannels value is less "
|
||||
+ "than or equal to 0 (" + audioChannels + ")");
|
||||
}
|
||||
valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels()));
|
||||
audioChannelConfigurationElement.setAttributeNode(valueAttribute);
|
||||
|
@ -1147,7 +1195,8 @@ public final class YoutubeDashManifestCreator {
|
|||
representationElement.appendChild(audioChannelConfigurationElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the AudioChannelConfiguration element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the AudioChannelConfiguration element of the "
|
||||
+ "DASH manifest to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1158,6 +1207,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This method is only used when generating DASH manifests from progressive streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <Representation>} element needs to be generated before this element with
|
||||
* {@link #generateRepresentationElement(Document, ItagItem)}).
|
||||
|
@ -1182,7 +1232,8 @@ public final class YoutubeDashManifestCreator {
|
|||
representationElement.appendChild(baseURLElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the BaseURL element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the BaseURL element of the DASH manifest to the "
|
||||
+ "document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1193,6 +1244,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This method is only used when generating DASH manifests from progressive streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It generates the following element:
|
||||
* <br>
|
||||
|
@ -1201,6 +1253,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
|
||||
* as the second parameter)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <Representation>} element needs to be generated before this element with
|
||||
* {@link #generateRepresentationElement(Document, ItagItem)}).
|
||||
|
@ -1227,12 +1280,14 @@ public final class YoutubeDashManifestCreator {
|
|||
final int indexStart = itagItem.getIndexStart();
|
||||
if (indexStart < 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the indexStart value of the ItagItem is less than to 0 (" + indexStart + ")");
|
||||
"Could not generate the DASH manifest: the indexStart value of the "
|
||||
+ "ItagItem is less than to 0 (" + indexStart + ")");
|
||||
}
|
||||
final int indexEnd = itagItem.getIndexEnd();
|
||||
if (indexEnd < 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the indexEnd value of the ItagItem is less than to 0 (" + indexStart + ")");
|
||||
"Could not generate the DASH manifest: the indexEnd value of the ItagItem "
|
||||
+ "is less than to 0 (" + indexStart + ")");
|
||||
}
|
||||
|
||||
indexRangeAttribute.setValue(indexStart + "-" + indexEnd);
|
||||
|
@ -1241,7 +1296,8 @@ public final class YoutubeDashManifestCreator {
|
|||
representationElement.appendChild(segmentBaseElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the SegmentBase element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the SegmentBase element of the DASH manifest to "
|
||||
+ "the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1252,6 +1308,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This method is only used when generating DASH manifests from progressive streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It generates the following element:
|
||||
* <br>
|
||||
|
@ -1260,6 +1317,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
|
||||
* as the second parameter)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <SegmentBase>} element needs to be generated before this element with
|
||||
* {@link #generateSegmentBaseElement(Document, ItagItem)}).
|
||||
|
@ -1286,12 +1344,14 @@ public final class YoutubeDashManifestCreator {
|
|||
final int initStart = itagItem.getInitStart();
|
||||
if (initStart < 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the initStart value of the ItagItem is less than to 0 (" + initStart + ")");
|
||||
"Could not generate the DASH manifest: the initStart value of the "
|
||||
+ "ItagItem is less than to 0 (" + initStart + ")");
|
||||
}
|
||||
final int initEnd = itagItem.getInitEnd();
|
||||
if (initEnd < 0) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate the DASH manifest: the initEnd value of the ItagItem is less than to 0 (" + initEnd + ")");
|
||||
"Could not generate the DASH manifest: the initEnd value of the ItagItem "
|
||||
+ "is less than to 0 (" + initEnd + ")");
|
||||
}
|
||||
|
||||
rangeAttribute.setValue(initStart + "-" + initEnd);
|
||||
|
@ -1300,7 +1360,8 @@ public final class YoutubeDashManifestCreator {
|
|||
segmentBaseElement.appendChild(initializationElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the Initialization element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the Initialization element of the DASH manifest "
|
||||
+ "to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1311,6 +1372,7 @@ public final class YoutubeDashManifestCreator {
|
|||
* <p>
|
||||
* This method is only used when generating DASH manifests from OTF and post-live-DVR streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It will produce a {@code <SegmentTemplate>} element with the following attributes:
|
||||
* <ul>
|
||||
|
@ -1372,7 +1434,8 @@ public final class YoutubeDashManifestCreator {
|
|||
representationElement.appendChild(segmentTemplateElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the SegmentTemplate element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the SegmentTemplate element of the DASH "
|
||||
+ "manifest to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1401,7 +1464,8 @@ public final class YoutubeDashManifestCreator {
|
|||
segmentTemplateElement.appendChild(segmentTimelineElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the SegmentTimeline element of the DASH manifest to the document", e);
|
||||
"Could not generate or append the SegmentTimeline element of the DASH "
|
||||
+ "manifest to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1413,16 +1477,20 @@ public final class YoutubeDashManifestCreator {
|
|||
* so we just have to loop into {@link #SEGMENTS_DURATION} and {@link #DURATION_REPETITIONS}
|
||||
* to generate the following element for each duration:
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* {@code <S d="segmentDuration" r="durationRepetition" />}
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* If there is no repetition of the duration between two segments, the {@code r} attribute is
|
||||
* not added to the {@code S} element.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* These elements will be appended as children of the {@code <SegmentTimeline>} element.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@code <SegmentTimeline>} element needs to be generated before this element with
|
||||
* {@link #generateSegmentTimelineElement(Document)}.
|
||||
|
@ -1462,7 +1530,8 @@ public final class YoutubeDashManifestCreator {
|
|||
|
||||
} catch (final DOMException | IllegalStateException | IndexOutOfBoundsException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the segment (S) elements of the DASH manifest to the document", e);
|
||||
"Could not generate or append the segment (S) elements of the DASH manifest "
|
||||
+ "to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1507,7 +1576,8 @@ public final class YoutubeDashManifestCreator {
|
|||
segmentTimelineElement.appendChild(sElement);
|
||||
} catch (final DOMException e) {
|
||||
throw new YoutubeDashManifestCreationException(
|
||||
"Could not generate or append the segment (S) elements of the DASH manifest to the document", e);
|
||||
"Could not generate or append the segment (S) elements of the DASH manifest "
|
||||
+ "to the document", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1596,9 +1596,9 @@ public final class YoutubeParsingHelper {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check if the streaming URL is a URL from the YouTube {@code WEB} client.
|
||||
* Check if the streaming URL is from the YouTube {@code WEB} client.
|
||||
*
|
||||
* @param url the streaming URL on which check if it's a {@code WEB} streaming URL.
|
||||
* @param url the streaming URL to be checked.
|
||||
* @return true if it's a {@code WEB} streaming URL, false otherwise
|
||||
*/
|
||||
public static boolean isWebStreamingUrl(@Nonnull final String url) {
|
||||
|
@ -1620,7 +1620,7 @@ public final class YoutubeParsingHelper {
|
|||
/**
|
||||
* Check if the streaming URL is a URL from the YouTube {@code ANDROID} client.
|
||||
*
|
||||
* @param url the streaming URL on which check if it's a {@code ANDROID} streaming URL.
|
||||
* @param url the streaming URL to be checked.
|
||||
* @return true if it's a {@code ANDROID} streaming URL, false otherwise
|
||||
*/
|
||||
public static boolean isAndroidStreamingUrl(@Nonnull final String url) {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
package org.schabi.newpipe.extractor.services.youtube.extractors;
|
||||
|
||||
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Class to build easier {@link org.schabi.newpipe.extractor.stream.Stream}s for
|
||||
* {@link YoutubeStreamExtractor}.
|
||||
*
|
||||
* <p>
|
||||
* It stores, per stream:
|
||||
* <ul>
|
||||
* <li>its content (the URL/the base URL of streams);</li>
|
||||
* <li>whether its content is the URL the content itself or the base URL;</li>
|
||||
* <li>its associated {@link ItagItem}.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
final class ItagInfo implements Serializable {
|
||||
@Nonnull
|
||||
private final String content;
|
||||
@Nonnull
|
||||
private final ItagItem itagItem;
|
||||
private boolean isUrl;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ItagInfo} instance.
|
||||
*
|
||||
* @param content the content of the stream, which must be not null
|
||||
* @param itagItem the {@link ItagItem} associated with the stream, which must be not null
|
||||
*/
|
||||
ItagInfo(@Nonnull final String content,
|
||||
@Nonnull final ItagItem itagItem) {
|
||||
this.content = content;
|
||||
this.itagItem = itagItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the stream is a URL.
|
||||
*
|
||||
* @param isUrl whether the content is a URL
|
||||
*/
|
||||
void setIsUrl(final boolean isUrl) {
|
||||
this.isUrl = isUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content stored in this {@code ItagInfo} instance, which is either the URL to the
|
||||
* content itself or the base URL.
|
||||
*
|
||||
* @return the content stored in this {@code ItagInfo} instance
|
||||
*/
|
||||
@Nonnull
|
||||
String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItagItem} associated with this {@code ItagInfo} instance.
|
||||
*
|
||||
* @return the {@link ItagItem} associated with this {@code ItagInfo} instance, which is not
|
||||
* null
|
||||
*/
|
||||
@Nonnull
|
||||
ItagItem getItagItem() {
|
||||
return itagItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the content stored is the URL to the content itself or the base URL of it.
|
||||
*
|
||||
* @return whether the content stored is the URL to the content itself or the base URL of it
|
||||
* @see #getContent() for more details
|
||||
*/
|
||||
boolean getIsUrl() {
|
||||
return isUrl;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
|
|||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.VIDEO_ID;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.createDesktopPlayerBody;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.ItagItem.CONTENT_LENGTH_UNKNOWN;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce;
|
||||
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter;
|
||||
|
@ -66,7 +67,6 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
|
|||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
|
||||
import org.schabi.newpipe.extractor.services.youtube.ItagInfo;
|
||||
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
|
||||
|
@ -666,9 +666,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
|
||||
private void setStreamType() {
|
||||
if (playerResponse.getObject("playabilityStatus").has("liveStreamability")
|
||||
|| playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) {
|
||||
if (playerResponse.getObject("playabilityStatus").has("liveStreamability")) {
|
||||
streamType = StreamType.LIVE_STREAM;
|
||||
} else if (playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) {
|
||||
streamType = StreamType.POST_LIVE_STREAM;
|
||||
} else {
|
||||
streamType = StreamType.VIDEO_STREAM;
|
||||
}
|
||||
|
@ -1171,7 +1172,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
|
||||
for (final Pair<JsonObject, String> pair : streamingDataAndCpnLoopList) {
|
||||
itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey,
|
||||
itagTypeWanted, streamType, pair.getSecond()));
|
||||
itagTypeWanted, pair.getSecond()));
|
||||
}
|
||||
|
||||
final List<T> streamList = new ArrayList<>();
|
||||
|
@ -1189,6 +1190,32 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link StreamBuilderHelper} which will be used to build {@link AudioStream}s in
|
||||
* {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)}
|
||||
*
|
||||
* <p>
|
||||
* The {@code StreamBuilderHelper} will set the following attributes in the
|
||||
* {@link AudioStream}s built:
|
||||
* <ul>
|
||||
* <li>the {@link ItagItem}'s id of the stream as its id;</li>
|
||||
* <li>{@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and
|
||||
* and as the value of {@code isUrl};</li>
|
||||
* <li>the media format returned by the {@link ItagItem} as its media format;</li>
|
||||
* <li>its average bitrate with the value returned by {@link
|
||||
* ItagItem#getAverageBitrate()};</li>
|
||||
* <li>the {@link ItagItem};</li>
|
||||
* <li>the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams
|
||||
* and ended streams.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
|
||||
* </p>
|
||||
*
|
||||
* @return a {@link StreamBuilderHelper} to build {@link AudioStream}s
|
||||
*/
|
||||
@Nonnull
|
||||
private StreamBuilderHelper<AudioStream> getAudioStreamBuilderHelper() {
|
||||
return new StreamBuilderHelper<AudioStream>() {
|
||||
|
@ -1203,9 +1230,11 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
.setAverageBitrate(itagItem.getAverageBitrate())
|
||||
.setItagItem(itagItem);
|
||||
|
||||
if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) {
|
||||
// YouTube uses the DASH delivery method for videos on OTF streams and
|
||||
// for all streams of post-live streams and live streams
|
||||
if (streamType == StreamType.LIVE_STREAM
|
||||
|| streamType == StreamType.POST_LIVE_STREAM
|
||||
|| !itagInfo.getIsUrl()) {
|
||||
// For YouTube videos on OTF streams and for all streams of post-live streams
|
||||
// and live streams, only the DASH delivery method can be used.
|
||||
builder.setDeliveryMethod(DeliveryMethod.DASH);
|
||||
}
|
||||
|
||||
|
@ -1214,6 +1243,40 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link StreamBuilderHelper} which will be used to build {@link VideoStream}s in
|
||||
* {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)}
|
||||
*
|
||||
* <p>
|
||||
* The {@code StreamBuilderHelper} will set the following attributes in the
|
||||
* {@link VideoStream}s built:
|
||||
* <ul>
|
||||
* <li>the {@link ItagItem}'s id of the stream as its id;</li>
|
||||
* <li>{@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and
|
||||
* and as the value of {@code isUrl};</li>
|
||||
* <li>the media format returned by the {@link ItagItem} as its media format;</li>
|
||||
* <li>whether it is video-only with the {@code areStreamsVideoOnly} parameter</li>
|
||||
* <li>the {@link ItagItem};</li>
|
||||
* <li>the resolution, by trying to use, in this order:
|
||||
* <ol>
|
||||
* <li>the height returned by the {@link ItagItem} + {@code p} + the frame rate if
|
||||
* it is more than 30;</li>
|
||||
* <li>the default resolution string from the {@link ItagItem};</li>
|
||||
* <li>an {@link Utils#EMPTY_STRING empty string}.</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li>the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams
|
||||
* and ended streams.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
|
||||
* </p>
|
||||
*
|
||||
* @param areStreamsVideoOnly whether the {@link StreamBuilderHelper} will set the video
|
||||
* streams as video-only streams
|
||||
* @return a {@link StreamBuilderHelper} to build {@link VideoStream}s
|
||||
*/
|
||||
@Nonnull
|
||||
private StreamBuilderHelper<VideoStream> getVideoStreamBuilderHelper(
|
||||
final boolean areStreamsVideoOnly) {
|
||||
|
@ -1241,12 +1304,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
builder.setResolution(stringBuilder.toString());
|
||||
} else {
|
||||
final String resolutionString = itagItem.getResolutionString();
|
||||
builder.setResolution(resolutionString != null ? resolutionString : "");
|
||||
builder.setResolution(resolutionString != null ? resolutionString
|
||||
: EMPTY_STRING);
|
||||
}
|
||||
|
||||
if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) {
|
||||
// YouTube uses the DASH delivery method for videos on OTF streams and
|
||||
// for all streams of post-live streams and live streams
|
||||
// For YouTube videos on OTF streams and for all streams of post-live streams
|
||||
// and live streams, only the DASH delivery method can be used.
|
||||
builder.setDeliveryMethod(DeliveryMethod.DASH);
|
||||
}
|
||||
|
||||
|
@ -1260,15 +1324,15 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
final JsonObject streamingData,
|
||||
final String streamingDataKey,
|
||||
@Nonnull final ItagItem.ItagType itagTypeWanted,
|
||||
@Nonnull final StreamType contentStreamType,
|
||||
@Nonnull final String contentPlaybackNonce) {
|
||||
@Nonnull final String contentPlaybackNonce) throws ParsingException {
|
||||
if (streamingData == null || !streamingData.has(streamingDataKey)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final String videoId = getId();
|
||||
final List<ItagInfo> itagInfos = new ArrayList<>();
|
||||
final JsonArray formats = streamingData.getArray(streamingDataKey);
|
||||
for (int i = 0; i < formats.size(); i++) {
|
||||
for (int i = 0; i != formats.size(); ++i) {
|
||||
final JsonObject formatData = formats.getObject(i);
|
||||
final int itag = formatData.getInt("itag");
|
||||
|
||||
|
@ -1279,79 +1343,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
try {
|
||||
final ItagItem itagItem = ItagItem.getItag(itag);
|
||||
final ItagItem.ItagType itagType = itagItem.itagType;
|
||||
if (itagItem.itagType != itagTypeWanted) {
|
||||
continue;
|
||||
if (itagType == itagTypeWanted) {
|
||||
buildAndAddItagInfoToList(videoId, itagInfos, formatData, itagItem,
|
||||
itagType, contentPlaybackNonce);
|
||||
}
|
||||
String streamUrl;
|
||||
if (formatData.has("url")) {
|
||||
streamUrl = formatData.getString("url") + "&cpn="
|
||||
+ contentPlaybackNonce;
|
||||
} else {
|
||||
// This url has an obfuscated signature
|
||||
final String cipherString = formatData.has(CIPHER)
|
||||
? formatData.getString(CIPHER)
|
||||
: formatData.getString(SIGNATURE_CIPHER);
|
||||
final Map<String, String> cipher = Parser.compatParseMap(
|
||||
cipherString);
|
||||
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
|
||||
+ deobfuscateSignature(cipher.get("s"));
|
||||
}
|
||||
|
||||
if (isWebStreamingUrl(streamUrl)) {
|
||||
streamUrl = tryDecryptUrl(streamUrl, getId()) + "&cver="
|
||||
+ getClientVersion();
|
||||
}
|
||||
|
||||
final JsonObject initRange = formatData.getObject("initRange");
|
||||
final JsonObject indexRange = formatData.getObject("indexRange");
|
||||
final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
|
||||
final String codec = mimeType.contains("codecs")
|
||||
? mimeType.split("\"")[1] : EMPTY_STRING;
|
||||
|
||||
itagItem.setBitrate(formatData.getInt("bitrate"));
|
||||
itagItem.setWidth(formatData.getInt("width"));
|
||||
itagItem.setHeight(formatData.getInt("height"));
|
||||
itagItem.setInitStart(Integer.parseInt(initRange.getString("start",
|
||||
"-1")));
|
||||
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end",
|
||||
"-1")));
|
||||
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start",
|
||||
"-1")));
|
||||
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end",
|
||||
"-1")));
|
||||
itagItem.setQuality(formatData.getString("quality"));
|
||||
itagItem.setCodec(codec);
|
||||
if (contentStreamType != StreamType.VIDEO_STREAM) {
|
||||
itagItem.setTargetDurationSec(formatData.getInt(
|
||||
"targetDurationSec"));
|
||||
}
|
||||
if (itagType == ItagItem.ItagType.VIDEO
|
||||
|| itagType == ItagItem.ItagType.VIDEO_ONLY) {
|
||||
itagItem.setFps(formatData.getInt("fps"));
|
||||
}
|
||||
if (itagType == ItagItem.ItagType.AUDIO) {
|
||||
itagItem.setSampleRate(Integer.parseInt(formatData.getString(
|
||||
"audioSampleRate")));
|
||||
itagItem.setAudioChannels(formatData.getInt("audioChannels"));
|
||||
}
|
||||
itagItem.setContentLength(Long.parseLong(formatData.getString(
|
||||
"contentLength", "-1")));
|
||||
|
||||
final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem);
|
||||
|
||||
if (contentStreamType == StreamType.VIDEO_STREAM) {
|
||||
itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING)
|
||||
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF"));
|
||||
} else {
|
||||
// We are currently not able to generate DASH manifests for running
|
||||
// livestreams, so because of the requirements of StreamInfo
|
||||
// objects, return these streams as DASH URL streams (even if they
|
||||
// are not playable).
|
||||
// Ended livestreams are returned as non URL streams
|
||||
itagInfo.setIsUrl(contentStreamType != StreamType.POST_LIVE_STREAM);
|
||||
}
|
||||
|
||||
itagInfos.add(itagInfo);
|
||||
} catch (final IOException | ExtractionException ignored) {
|
||||
}
|
||||
}
|
||||
|
@ -1359,6 +1354,83 @@ public class YoutubeStreamExtractor extends StreamExtractor {
|
|||
return itagInfos;
|
||||
}
|
||||
|
||||
private void buildAndAddItagInfoToList(
|
||||
@Nonnull final String videoId,
|
||||
@Nonnull final List<ItagInfo> itagInfos,
|
||||
@Nonnull final JsonObject formatData,
|
||||
@Nonnull final ItagItem itagItem,
|
||||
@Nonnull final ItagItem.ItagType itagType,
|
||||
@Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
|
||||
String streamUrl;
|
||||
if (formatData.has("url")) {
|
||||
streamUrl = formatData.getString("url");
|
||||
} else {
|
||||
// This url has an obfuscated signature
|
||||
final String cipherString = formatData.has(CIPHER)
|
||||
? formatData.getString(CIPHER)
|
||||
: formatData.getString(SIGNATURE_CIPHER);
|
||||
final Map<String, String> cipher = Parser.compatParseMap(
|
||||
cipherString);
|
||||
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
|
||||
+ deobfuscateSignature(cipher.get("s"));
|
||||
}
|
||||
|
||||
// Add the content playback nonce to the stream URL
|
||||
streamUrl += "&" + CPN + "=" + contentPlaybackNonce;
|
||||
|
||||
if (isWebStreamingUrl(streamUrl)) {
|
||||
streamUrl = tryDecryptUrl(streamUrl, videoId) + "&cver=" + getClientVersion();
|
||||
}
|
||||
|
||||
final JsonObject initRange = formatData.getObject("initRange");
|
||||
final JsonObject indexRange = formatData.getObject("indexRange");
|
||||
final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
|
||||
final String codec = mimeType.contains("codecs")
|
||||
? mimeType.split("\"")[1] : EMPTY_STRING;
|
||||
|
||||
itagItem.setBitrate(formatData.getInt("bitrate"));
|
||||
itagItem.setWidth(formatData.getInt("width"));
|
||||
itagItem.setHeight(formatData.getInt("height"));
|
||||
itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1")));
|
||||
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1")));
|
||||
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1")));
|
||||
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1")));
|
||||
itagItem.setQuality(formatData.getString("quality"));
|
||||
itagItem.setCodec(codec);
|
||||
|
||||
if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) {
|
||||
itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec"));
|
||||
}
|
||||
|
||||
if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) {
|
||||
itagItem.setFps(formatData.getInt("fps"));
|
||||
}
|
||||
if (itagType == ItagItem.ItagType.AUDIO) {
|
||||
// YouTube return the audio sample rate as a string
|
||||
itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate")));
|
||||
itagItem.setAudioChannels(formatData.getInt("audioChannels"));
|
||||
}
|
||||
|
||||
// YouTube return the content length as a string
|
||||
itagItem.setContentLength(Long.parseLong(formatData.getString("contentLength",
|
||||
String.valueOf(CONTENT_LENGTH_UNKNOWN))));
|
||||
|
||||
final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem);
|
||||
|
||||
if (streamType == StreamType.VIDEO_STREAM) {
|
||||
itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING)
|
||||
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF"));
|
||||
} else {
|
||||
// We are currently not able to generate DASH manifests for running
|
||||
// livestreams, so because of the requirements of StreamInfo
|
||||
// objects, return these streams as DASH URL streams (even if they
|
||||
// are not playable).
|
||||
// Ended livestreams are returned as non URL streams
|
||||
itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM);
|
||||
}
|
||||
|
||||
itagInfos.add(itagInfo);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
|
|
|
@ -68,10 +68,10 @@ public final class AudioStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the identifier of the {@link SubtitlesStream}.
|
||||
* Set the identifier of the {@link AudioStream}.
|
||||
*
|
||||
* <p>
|
||||
* It <b>must be not null</b> and should be non empty.
|
||||
* It <b>must not be null</b> and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -79,7 +79,7 @@ public final class AudioStream extends Stream {
|
|||
* Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
|
||||
* </p>
|
||||
*
|
||||
* @param id the identifier of the {@link SubtitlesStream}, which must be not null
|
||||
* @param id the identifier of the {@link AudioStream}, which must not be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setId(@Nonnull final String id) {
|
||||
|
@ -91,7 +91,7 @@ public final class AudioStream extends Stream {
|
|||
* Set the content of the {@link AudioStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be non null and should be non empty.
|
||||
* It must not be null, and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* @param content the content of the {@link AudioStream}
|
||||
|
@ -111,8 +111,8 @@ public final class AudioStream extends Stream {
|
|||
* <p>
|
||||
* It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A},
|
||||
* {@link MediaFormat#WEBMA WEBMA}, {@link MediaFormat#MP3 MP3}, {@link MediaFormat#OPUS
|
||||
* OPUS}, {@link MediaFormat#OGG OGG}, {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but can
|
||||
* be {@code null} if the media format could not be determined.
|
||||
* OPUS}, {@link MediaFormat#OGG OGG}, or {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but
|
||||
* can be {@code null} if the media format could not be determined.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -131,7 +131,7 @@ public final class AudioStream extends Stream {
|
|||
* Set the {@link DeliveryMethod} of the {@link AudioStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be not null.
|
||||
* It must not be null.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -139,7 +139,7 @@ public final class AudioStream extends Stream {
|
|||
* </p>
|
||||
*
|
||||
* @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must
|
||||
* be not null
|
||||
* not be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
|
||||
|
@ -151,8 +151,8 @@ public final class AudioStream extends Stream {
|
|||
* Set the base URL of the {@link AudioStream}.
|
||||
*
|
||||
* <p>
|
||||
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
|
||||
* they have been parsed.
|
||||
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
|
||||
* from which the URLs have been parsed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -213,7 +213,7 @@ public final class AudioStream extends Stream {
|
|||
*
|
||||
* @return a new {@link AudioStream} using the builder's current values
|
||||
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or
|
||||
* {@code deliveryMethod} have been not set or set as {@code null}
|
||||
* {@code deliveryMethod} have been not set, or have been set as {@code null}
|
||||
*/
|
||||
@Nonnull
|
||||
public AudioStream build() {
|
||||
|
@ -244,8 +244,8 @@ public final class AudioStream extends Stream {
|
|||
/**
|
||||
* Create a new audio stream.
|
||||
*
|
||||
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
|
||||
* would be the itag
|
||||
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
|
||||
* this would be the itag
|
||||
* @param content the content or the URL of the stream, depending on whether isUrl is
|
||||
* true
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
|
||||
|
@ -258,6 +258,7 @@ public final class AudioStream extends Stream {
|
|||
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
|
||||
* information)
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
private AudioStream(@Nonnull final String id,
|
||||
@Nonnull final String content,
|
||||
final boolean isUrl,
|
||||
|
|
|
@ -13,25 +13,43 @@ public enum DeliveryMethod {
|
|||
PROGRESSIVE_HTTP,
|
||||
|
||||
/**
|
||||
* Enum constant which represents the use of the DASH adaptive streaming method to fetch a
|
||||
* {@link Stream stream}.
|
||||
* Enum constant which represents the use of the DASH (Dynamic Adaptive Streaming over HTTP)
|
||||
* adaptive streaming method to fetch a {@link Stream stream}.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP">the
|
||||
* Dynamic Adaptive Streaming over HTTP Wikipedia page</a> and <a href="https://dashif.org/">
|
||||
* DASH Industry Forum's website</a> for more information about the DASH delivery method
|
||||
*/
|
||||
DASH,
|
||||
|
||||
/**
|
||||
* Enum constant which represents the use of the HLS adaptive streaming method to fetch a
|
||||
* {@link Stream stream}.
|
||||
* Enum constant which represents the use of the HLS (HTTP Live Streaming) adaptive streaming
|
||||
* method to fetch a {@link Stream stream}.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/HTTP_Live_Streaming">the HTTP Live Streaming
|
||||
* page</a> and <a href="https://developer.apple.com/streaming">Apple's developers website page
|
||||
* about HTTP Live Streaming</a> for more information about the HLS delivery method
|
||||
*/
|
||||
HLS,
|
||||
|
||||
/**
|
||||
* Enum constant which represents the use of the SmoothStreaming adaptive streaming method to
|
||||
* fetch a {@link Stream stream}.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming
|
||||
* #Microsoft_Smooth_Streaming_(MSS)">Wikipedia's page about adaptive bitrate streaming,
|
||||
* section <i>Microsoft Smooth Streaming (MSS)</i></a> for more information about the
|
||||
* SmoothStreaming delivery method
|
||||
*/
|
||||
SS,
|
||||
|
||||
/**
|
||||
* Enum constant which represents the use of a torrent to fetch a {@link Stream stream}.
|
||||
* Enum constant which represents the use of a torrent file to fetch a {@link Stream stream}.
|
||||
*
|
||||
* @see <a href="https://en.wikipedia.org/wiki/BitTorrent">Wikipedia's BitTorrent's page</a>,
|
||||
* <a href="https://en.wikipedia.org/wiki/Torrent_file">Wikipedia's page about torrent files
|
||||
* </a> and <a href=""https://www.bittorrent.org/></a> for more information about the
|
||||
* BitTorrent protocol
|
||||
*/
|
||||
TORRENT
|
||||
}
|
||||
|
|
|
@ -19,11 +19,11 @@ public abstract class Stream implements Serializable {
|
|||
public static final String ID_UNKNOWN = " ";
|
||||
|
||||
/**
|
||||
* An integer to represent that the itag id returned is not available (only for YouTube, this
|
||||
* An integer to represent that the itag ID returned is not available (only for YouTube; this
|
||||
* should never happen) or not applicable (for other services than YouTube).
|
||||
*
|
||||
* <p>
|
||||
* An itag should not have a negative value so {@code -1} is used for this constant.
|
||||
* An itag should not have a negative value, so {@code -1} is used for this constant.
|
||||
* </p>
|
||||
*/
|
||||
public static final int ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE = -1;
|
||||
|
@ -38,8 +38,8 @@ public abstract class Stream implements Serializable {
|
|||
/**
|
||||
* Instantiates a new {@code Stream} object.
|
||||
*
|
||||
* @param id the ID which uniquely identifies the file, e.g. for YouTube this would
|
||||
* be the itag
|
||||
* @param id the identifier which uniquely identifies the file, e.g. for YouTube
|
||||
* this would be the itag
|
||||
* @param content the content or URL, depending on whether isUrl is true
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
|
||||
* manifest
|
||||
|
@ -63,10 +63,10 @@ public abstract class Stream implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if the list already contains one stream with equals stats.
|
||||
* Checks if the list already contains a stream with the same statistics.
|
||||
*
|
||||
* @param stream the stream which will be compared to the streams in the stream list
|
||||
* @param streamList the list of {@link Stream Streams} which will be compared
|
||||
* @param stream the stream to be compared against the streams in the stream list
|
||||
* @param streamList the list of {@link Stream}s which will be compared
|
||||
* @return whether the list already contains one stream with equals stats
|
||||
*/
|
||||
public static boolean containSimilarStream(final Stream stream,
|
||||
|
@ -83,16 +83,16 @@ public abstract class Stream implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reveals whether two streams have the same stats ({@link MediaFormat media format} and
|
||||
* Reveals whether two streams have the same statistics ({@link MediaFormat media format} and
|
||||
* {@link DeliveryMethod delivery method}).
|
||||
*
|
||||
* <p>
|
||||
* If the {@link MediaFormat media format} of the stream is unknown, the streams are compared
|
||||
* by only using the {@link DeliveryMethod delivery method} and their id.
|
||||
* by using only the {@link DeliveryMethod delivery method} and their ID.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Note: This method always returns always false if the stream passed is null.
|
||||
* Note: This method always returns false if the stream passed is null.
|
||||
* </p>
|
||||
*
|
||||
* @param cmp the stream object to be compared to this stream object
|
||||
|
@ -118,9 +118,12 @@ public abstract class Stream implements Serializable {
|
|||
/**
|
||||
* Reveals whether two streams are equal.
|
||||
*
|
||||
* @param cmp the stream object to be compared to this stream object
|
||||
* @return whether streams are equal
|
||||
* @param cmp a {@link Stream} object to be compared to this {@link Stream} instance.
|
||||
* @return whether the compared streams are equal
|
||||
* @deprecated Use {@link #equalStats(Stream)} to compare statistics of two streams and
|
||||
* {@link #equals(Object)} to compare the equality of two streams instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean equals(final Stream cmp) {
|
||||
return equalStats(cmp) && content.equals(cmp.content);
|
||||
}
|
||||
|
@ -129,19 +132,19 @@ public abstract class Stream implements Serializable {
|
|||
* Gets the identifier of this stream, e.g. the itag for YouTube.
|
||||
*
|
||||
* <p>
|
||||
* It should be normally unique but {@link #ID_UNKNOWN} may be returned as the identifier if
|
||||
* one used by the stream extractor cannot be extracted, if the extractor uses a value from a
|
||||
* streaming service.
|
||||
* It should normally be unique, but {@link #ID_UNKNOWN} may be returned as the identifier if
|
||||
* the one used by the stream extractor cannot be extracted, which could happen if the
|
||||
* extractor uses a value from a streaming service.
|
||||
* </p>
|
||||
*
|
||||
* @return the id (which may be {@link #ID_UNKNOWN})
|
||||
* @return the identifier (which may be {@link #ID_UNKNOWN})
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of this stream if the content is a URL, or {@code null} if that's the not case.
|
||||
* Gets the URL of this stream if the content is a URL, or {@code null} otherwise.
|
||||
*
|
||||
* @return the URL if the content is a URL, {@code null} otherwise
|
||||
* @deprecated Use {@link #getContent()} instead.
|
||||
|
@ -162,10 +165,10 @@ public abstract class Stream implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns if the content is a URL or not.
|
||||
* Returns whether the content is a URL or not.
|
||||
*
|
||||
* @return {@code true} if the content of this stream content is a URL, {@code false}
|
||||
* if it is the actual content
|
||||
* @return {@code true} if the content of this stream is a URL, {@code false} if it's the
|
||||
* actual content
|
||||
*/
|
||||
public boolean isUrl() {
|
||||
return isUrl;
|
||||
|
@ -182,9 +185,9 @@ public abstract class Stream implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the format id, which can be unknown.
|
||||
* Gets the format ID, which can be unknown.
|
||||
*
|
||||
* @return the format id or {@link #FORMAT_ID_UNKNOWN}
|
||||
* @return the format ID or {@link #FORMAT_ID_UNKNOWN}
|
||||
*/
|
||||
public int getFormatId() {
|
||||
if (mediaFormat != null) {
|
||||
|
@ -208,7 +211,7 @@ public abstract class Stream implements Serializable {
|
|||
*
|
||||
* <p>
|
||||
* If the stream is not a DASH stream or an HLS stream, this value will always be null.
|
||||
* It may be also null for these streams too.
|
||||
* It may also be null for these streams too.
|
||||
* </p>
|
||||
*
|
||||
* @return the base URL of the stream or {@code null}
|
||||
|
@ -222,7 +225,7 @@ public abstract class Stream implements Serializable {
|
|||
* Gets the {@link ItagItem} of a stream.
|
||||
*
|
||||
* <p>
|
||||
* If the stream is not a YouTube stream, this value will always be null.
|
||||
* If the stream is not from YouTube, this value will always be null.
|
||||
* </p>
|
||||
*
|
||||
* @return the {@link ItagItem} of the stream or {@code null}
|
||||
|
@ -242,11 +245,14 @@ public abstract class Stream implements Serializable {
|
|||
|
||||
final Stream stream = (Stream) obj;
|
||||
return id.equals(stream.id) && mediaFormat == stream.mediaFormat
|
||||
&& deliveryMethod == stream.deliveryMethod;
|
||||
&& deliveryMethod == stream.deliveryMethod
|
||||
&& content.equals(stream.content)
|
||||
&& isUrl == stream.isUrl
|
||||
&& Objects.equals(baseUrl, stream.baseUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, mediaFormat, deliveryMethod);
|
||||
return Objects.hash(id, mediaFormat, deliveryMethod, content, isUrl, baseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ public enum StreamType {
|
|||
NONE,
|
||||
|
||||
/**
|
||||
* Enum constant to indicate that the stream type of stream content is a video.
|
||||
* Enum constant to indicate that the stream type of stream content is a live video.
|
||||
*
|
||||
* <p>
|
||||
* Note that contents <strong>can contain audio streams</strong> even if they also contain
|
||||
* Note that contents <strong>may contain audio streams</strong> even if they also contain
|
||||
* video streams (video-only or video with audio, depending of the stream/the content/the
|
||||
* service).
|
||||
* </p>
|
||||
|
@ -46,23 +46,22 @@ public enum StreamType {
|
|||
*
|
||||
* <p>
|
||||
* Note that contents <strong>can contain audio live streams</strong> even if they also contain
|
||||
* live video streams (video-only or video with audio, depending of the stream/the content/the
|
||||
* service).
|
||||
* live video streams (so video-only or video with audio, depending on the stream/the content/
|
||||
* the service).
|
||||
* </p>
|
||||
*/
|
||||
LIVE_STREAM,
|
||||
|
||||
/**
|
||||
* Enum constant to indicate that the stream type of stream content is a live audio content.
|
||||
* Enum constant to indicate that the stream type of stream content is a live audio.
|
||||
*
|
||||
* <p>
|
||||
* Note that contents returned as live audio streams should not return live video streams.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* So, in order to prevent unexpected behaviors, stream extractors which are returning this
|
||||
* stream type for a content should ensure that no live video stream is returned for this
|
||||
* content.
|
||||
* To prevent unexpected behavior, stream extractors which are returning this stream type for a
|
||||
* content should ensure that no live video stream is returned along with it.
|
||||
* </p>
|
||||
*/
|
||||
AUDIO_LIVE_STREAM,
|
||||
|
@ -72,10 +71,10 @@ public enum StreamType {
|
|||
* ended live video stream.
|
||||
*
|
||||
* <p>
|
||||
* Note that most of ended live video (or audio) contents may be extracted as
|
||||
* {@link #VIDEO_STREAM regular video contents} (or
|
||||
* {@link #AUDIO_STREAM regular audio contents}) later, because the service may encode them
|
||||
* again later as normal video/audio streams. That's the case for example on YouTube.
|
||||
* Note that most of the content of an ended live video (or audio) may be extracted as {@link
|
||||
* #VIDEO_STREAM regular video contents} (or {@link #AUDIO_STREAM regular audio contents})
|
||||
* later, because the service may encode them again later as normal video/audio streams. That's
|
||||
* the case on YouTube, for example.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
|
|
@ -35,7 +35,7 @@ public final class SubtitlesStream extends Stream {
|
|||
private Boolean autoGenerated;
|
||||
|
||||
/**
|
||||
* Create a new {@link Builder} instance with its default values.
|
||||
* Create a new {@link Builder} instance with default values.
|
||||
*/
|
||||
public Builder() {
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public final class SubtitlesStream extends Stream {
|
|||
/**
|
||||
* Set the identifier of the {@link SubtitlesStream}.
|
||||
*
|
||||
* @param id the identifier of the {@link SubtitlesStream}, which should be not null
|
||||
* @param id the identifier of the {@link SubtitlesStream}, which should not be null
|
||||
* (otherwise the fallback to create the identifier will be used when building
|
||||
* the builder)
|
||||
* @return this {@link Builder} instance
|
||||
|
@ -57,10 +57,10 @@ public final class SubtitlesStream extends Stream {
|
|||
* Set the content of the {@link SubtitlesStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be non null and should be non empty.
|
||||
* It must not be null, and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* @param content the content of the {@link SubtitlesStream}
|
||||
* @param content the content of the {@link SubtitlesStream}, which must not be null
|
||||
* @param isUrl whether the content is a URL
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
|
@ -78,7 +78,7 @@ public final class SubtitlesStream extends Stream {
|
|||
* It should be one of the subtitles {@link MediaFormat}s ({@link MediaFormat#SRT SRT},
|
||||
* {@link MediaFormat#TRANSCRIPT1 TRANSCRIPT1}, {@link MediaFormat#TRANSCRIPT2
|
||||
* TRANSCRIPT2}, {@link MediaFormat#TRANSCRIPT3 TRANSCRIPT3}, {@link MediaFormat#TTML
|
||||
* TTML}, {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could
|
||||
* TTML}, or {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could
|
||||
* not be determined.
|
||||
* </p>
|
||||
*
|
||||
|
@ -99,7 +99,7 @@ public final class SubtitlesStream extends Stream {
|
|||
* Set the {@link DeliveryMethod} of the {@link SubtitlesStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be not null.
|
||||
* It must not be null.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -107,7 +107,7 @@ public final class SubtitlesStream extends Stream {
|
|||
* </p>
|
||||
*
|
||||
* @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which
|
||||
* must be not null
|
||||
* must not be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
|
||||
|
@ -119,8 +119,8 @@ public final class SubtitlesStream extends Stream {
|
|||
* Set the base URL of the {@link SubtitlesStream}.
|
||||
*
|
||||
* <p>
|
||||
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
|
||||
* they have been parsed.
|
||||
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
|
||||
* from which the URLs have been parsed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -139,7 +139,7 @@ public final class SubtitlesStream extends Stream {
|
|||
* Set the language code of the {@link SubtitlesStream}.
|
||||
*
|
||||
* <p>
|
||||
* It <b>must be not null</b> and should be not an empty string.
|
||||
* It <b>must not be null</b> and should not be an empty string.
|
||||
* </p>
|
||||
*
|
||||
* @param languageCode the language code of the {@link SubtitlesStream}
|
||||
|
@ -151,10 +151,10 @@ public final class SubtitlesStream extends Stream {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set whether the subtitles have been generated by the streaming service.
|
||||
* Set whether the subtitles have been auto-generated by the streaming service.
|
||||
*
|
||||
* @param autoGenerated whether the subtitles have been generated by the streaming
|
||||
* service
|
||||
* service
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setAutoGenerated(final boolean autoGenerated) {
|
||||
|
@ -172,13 +172,13 @@ public final class SubtitlesStream extends Stream {
|
|||
*
|
||||
* <p>
|
||||
* If no identifier has been set, an identifier will be generated using the language code
|
||||
* and the media format suffix if the media format is known
|
||||
* and the media format suffix, if the media format is known.
|
||||
* </p>
|
||||
*
|
||||
* @return a new {@link SubtitlesStream} using the builder's current values
|
||||
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}),
|
||||
* {@code deliveryMethod}, {@code languageCode} or the {@code isAutogenerated} have been
|
||||
* not set or set as {@code null}
|
||||
* not set, or have been set as {@code null}
|
||||
*/
|
||||
@Nonnull
|
||||
public SubtitlesStream build() {
|
||||
|
@ -219,28 +219,29 @@ public final class SubtitlesStream extends Stream {
|
|||
/**
|
||||
* Create a new subtitles stream.
|
||||
*
|
||||
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
|
||||
* would be the itag
|
||||
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
|
||||
* this would be the itag
|
||||
* @param content the content or the URL of the stream, depending on whether isUrl is
|
||||
* true
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
|
||||
* manifest
|
||||
* @param format the {@link MediaFormat} used by the stream
|
||||
* @param mediaFormat the {@link MediaFormat} used by the stream
|
||||
* @param deliveryMethod the {@link DeliveryMethod} of the stream
|
||||
* @param languageCode the language code of the stream
|
||||
* @param autoGenerated whether the subtitles are auto-generated by the streaming service
|
||||
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
|
||||
* information)
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
private SubtitlesStream(@Nonnull final String id,
|
||||
@Nonnull final String content,
|
||||
final boolean isUrl,
|
||||
@Nullable final MediaFormat format,
|
||||
@Nullable final MediaFormat mediaFormat,
|
||||
@Nonnull final DeliveryMethod deliveryMethod,
|
||||
@Nonnull final String languageCode,
|
||||
final boolean autoGenerated,
|
||||
@Nullable final String baseUrl) {
|
||||
super(id, content, isUrl, format, deliveryMethod, baseUrl);
|
||||
super(id, content, isUrl, mediaFormat, deliveryMethod, baseUrl);
|
||||
|
||||
/*
|
||||
* Locale.forLanguageTag only for Android API >= 21
|
||||
|
@ -262,7 +263,7 @@ public final class SubtitlesStream extends Stream {
|
|||
}
|
||||
|
||||
this.code = languageCode;
|
||||
this.format = format;
|
||||
this.format = mediaFormat;
|
||||
this.autoGenerated = autoGenerated;
|
||||
}
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ public final class VideoStream extends Stream {
|
|||
* Set the identifier of the {@link VideoStream}.
|
||||
*
|
||||
* <p>
|
||||
* It <b>must be not null</b> and should be non empty.
|
||||
* It must not be null, and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -89,7 +89,7 @@ public final class VideoStream extends Stream {
|
|||
* Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
|
||||
* </p>
|
||||
*
|
||||
* @param id the identifier of the {@link VideoStream}, which must be not null
|
||||
* @param id the identifier of the {@link VideoStream}, which must not be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setId(@Nonnull final String id) {
|
||||
|
@ -101,7 +101,7 @@ public final class VideoStream extends Stream {
|
|||
* Set the content of the {@link VideoStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be non null and should be non empty.
|
||||
* It must not be null, and should be non empty.
|
||||
* </p>
|
||||
*
|
||||
* @param content the content of the {@link VideoStream}
|
||||
|
@ -120,8 +120,8 @@ public final class VideoStream extends Stream {
|
|||
*
|
||||
* <p>
|
||||
* It should be one of the video {@link MediaFormat}s ({@link MediaFormat#MPEG_4 MPEG_4},
|
||||
* {@link MediaFormat#v3GPP v3GPP}, {@link MediaFormat#WEBM WEBM}) but can be {@code null}
|
||||
* if the media format could not be determined.
|
||||
* {@link MediaFormat#v3GPP v3GPP}, or {@link MediaFormat#WEBM WEBM}) but can be {@code
|
||||
* null} if the media format could not be determined.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -140,7 +140,7 @@ public final class VideoStream extends Stream {
|
|||
* Set the {@link DeliveryMethod} of the {@link VideoStream}.
|
||||
*
|
||||
* <p>
|
||||
* It must be not null.
|
||||
* It must not be null.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -148,7 +148,7 @@ public final class VideoStream extends Stream {
|
|||
* </p>
|
||||
*
|
||||
* @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must
|
||||
* be not null
|
||||
* not be null
|
||||
* @return this {@link Builder} instance
|
||||
*/
|
||||
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
|
||||
|
@ -160,8 +160,8 @@ public final class VideoStream extends Stream {
|
|||
* Set the base URL of the {@link VideoStream}.
|
||||
*
|
||||
* <p>
|
||||
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
|
||||
* they have been parsed.
|
||||
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
|
||||
* from which the URLs have been parsed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
@ -245,8 +245,8 @@ public final class VideoStream extends Stream {
|
|||
*
|
||||
* @return a new {@link VideoStream} using the builder's current values
|
||||
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}),
|
||||
* {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set or
|
||||
* set as {@code null}
|
||||
* {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set, or
|
||||
* have been set as {@code null}
|
||||
*/
|
||||
@Nonnull
|
||||
public VideoStream build() {
|
||||
|
@ -289,8 +289,8 @@ public final class VideoStream extends Stream {
|
|||
/**
|
||||
* Create a new video stream.
|
||||
*
|
||||
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
|
||||
* would be the itag
|
||||
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
|
||||
* this would be the itag
|
||||
* @param content the content or the URL of the stream, depending on whether isUrl is
|
||||
* true
|
||||
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
|
||||
|
@ -303,6 +303,7 @@ public final class VideoStream extends Stream {
|
|||
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
|
||||
* information)
|
||||
*/
|
||||
@SuppressWarnings("checkstyle:ParameterNumber")
|
||||
private VideoStream(@Nonnull final String id,
|
||||
@Nonnull final String content,
|
||||
final boolean isUrl,
|
||||
|
|
|
@ -53,7 +53,7 @@ class YoutubeDashManifestCreatorTest {
|
|||
// Setting a higher number may let Google video servers return a lot of 403s
|
||||
private static final int MAXIMUM_NUMBER_OF_STREAMS_TO_TEST = 3;
|
||||
|
||||
public static class testGenerationOfOtfAndProgressiveManifests {
|
||||
public static class TestGenerationOfOtfAndProgressiveManifests {
|
||||
private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM";
|
||||
private static YoutubeStreamExtractor extractor;
|
||||
|
||||
|
|
Loading…
Reference in a new issue