From aa4c10e7517f60359f6c3fc911c67d2cd125b79a Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 15 Mar 2022 11:19:13 +0100 Subject: [PATCH] Improve documentation and adress most of the requested changes Also fix some issues in several places, in the code and the documentation. --- .../extractors/BandcampStreamExtractor.java | 4 +- .../MediaCCCLiveStreamExtractor.java | 171 ++++++------- .../MediaCCCLiveStreamMapperDTO.java | 29 +++ .../extractors/MediaCCCStreamExtractor.java | 9 +- .../extractors/PeertubeStreamExtractor.java | 110 ++++---- .../extractors/SoundcloudStreamExtractor.java | 25 +- .../services/youtube/DashMpdParser.java | 56 ++-- .../extractor/services/youtube/ItagInfo.java | 37 --- .../extractor/services/youtube/ItagItem.java | 86 +++---- .../youtube/YoutubeDashManifestCreator.java | 218 ++++++++++------ .../youtube/YoutubeParsingHelper.java | 6 +- .../services/youtube/extractors/ItagInfo.java | 80 ++++++ .../extractors/YoutubeStreamExtractor.java | 242 ++++++++++++------ .../newpipe/extractor/stream/AudioStream.java | 27 +- .../extractor/stream/DeliveryMethod.java | 28 +- .../newpipe/extractor/stream/Stream.java | 60 +++-- .../newpipe/extractor/stream/StreamType.java | 23 +- .../extractor/stream/SubtitlesStream.java | 41 +-- .../newpipe/extractor/stream/VideoStream.java | 27 +- .../YoutubeDashManifestCreatorTest.java | 2 +- 20 files changed, 759 insertions(+), 522 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index 9bb6d5c78..4b5d9d12a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -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 diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 54c2d056c..09e01ce36 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -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. * *

- * 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. *

* *

- * You can find the other video DASH streams by using {@link #getVideoStreams()} + * You can find the other DASH video streams by using {@link #getVideoStreams()} *

*/ @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. * *

- * 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. *

* *

- * You can find the other video HLS streams by using {@link #getVideoStreams()} + * You can find the other HLS video streams by using {@link #getVideoStreams()} *

*/ @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 getAudioStreams() throws IOException, ExtractionException { - final List 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 getVideoStreams() throws IOException, ExtractionException { - final List 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 List getStreams( + @Nonnull final String streamType, + @Nonnull final Function 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 diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java new file mode 100644 index 000000000..c06ef736b --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java @@ -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; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 53cc53ad0..83dcc381e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -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) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index bec41f481..60666db92 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -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) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 160bf572c..bacfd077e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -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 { * *

* 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. *

* - * @param audioStreams the audio streams to which add the downloadable file + *

+ * If the value of the {@code has_download_left} boolean is {@code true}, the track can be + * downloaded, and not otherwise. + *

+ * + * @param audioStreams the audio streams to which the downloadable file is added */ public void extractDownloadableFileIfAvailable(final List 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. * *

- * 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. *

* * @param hlsManifestUrl the URL of the manifest to be parsed diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java index 77cee35bb..2b5774049 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java @@ -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. + * + *

+ * 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. + *

+ * + *

+ * This class is not used by the extractor itself, as all streams are supported by the extractor. + *

+ */ 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}. + * + *

+ * The result contains video, video-only and audio streams. + *

+ */ public static class Result { private final List videoStreams; private final List videoOnlyStreams; private final List audioStreams; - - public Result(final List videoStreams, - final List videoOnlyStreams, - final List audioStreams) { + Result(final List videoStreams, + final List videoOnlyStreams, + final List 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. - *

- * It has video, video only and audio streams. - *

- * 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 + *

+ * The parser supports video, video-only and audio streams. + *

+ * + * @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 - * www.brendanlog.com + * www.brendanlong.com's page about the structure of an MPEG-DASH MPD manifest */ @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); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java deleted file mode 100644 index cdb5dc2de..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java +++ /dev/null @@ -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; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java index 79f44078f..9608de8ec 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java @@ -237,11 +237,15 @@ public class ItagItem implements Serializable { } /** - * Get the frame rate per second. + * Get the frame rate. * *

- * 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. + *

+ * + *

+ * It defaults to the standard value associated with this itag. *

* *

@@ -249,28 +253,24 @@ public class ItagItem implements Serializable { * #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags. *

* - * @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. * *

* 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. *

* - * @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}. * *

* It is only known for video itags. *

* - * @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 { * *

* 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. *

* * @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN} @@ -374,8 +374,8 @@ public class ItagItem implements Serializable { * Set the sample rate. * *

- * 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. *

* * @param sampleRate the sample rate of an audio itag @@ -392,8 +392,8 @@ public class ItagItem implements Serializable { * Get the number of audio channels. * *

- * 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. *

* * @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. * *

- * 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. *

* * @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. * *

- * 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. *

* * @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. * *

- * 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. *

* *

- * 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. *

* * @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; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java index 62d833f51..2d7f7fe22 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java @@ -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. *

*/ -@SuppressWarnings({"ConstantConditions", "unused"}) public final class YoutubeDashManifestCreator { /** @@ -115,6 +114,7 @@ public final class YoutubeDashManifestCreator { *

*/ 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, ...) *

+ * *

* Only used for videos; mostly those with a small amount of views, or ended livestreams * which have just been re-encoded as normal videos. *

*/ 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. *

+ * *

* Only used for livestreams (ended or running). *

@@ -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 } element looks like the manifest returned into the player * response of videos with OTF streams: *

+ * *

* {@code + * *

* 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 { *

* This element, with its attributes and values, is: *

+ * *

* {@code } *

+ * *

* The {@code } 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 } element will be * appended * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the document + * appending the {@code } 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 { *

* This method is only used when generating DASH manifests of audio streams. *

+ * *

* It will produce the following element: *
@@ -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) *

+ * *

* The {@code } 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 { *

* This method is only used when generating DASH manifests from progressive streams. *

+ * *

* The {@code } 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 { *

* This method is only used when generating DASH manifests from progressive streams. *

+ * *

* It generates the following element: *
@@ -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) *

+ * *

* The {@code } 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 { *

* This method is only used when generating DASH manifests from progressive streams. *

+ * *

* It generates the following element: *
@@ -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) *

+ * *

* The {@code } 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 { *

* This method is only used when generating DASH manifests from OTF and post-live-DVR streams. *

+ * *

* It will produce a {@code } element with the following attributes: *

    @@ -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: *

    + * *

    * {@code } *

    + * *

    * If there is no repetition of the duration between two segments, the {@code r} attribute is * not added to the {@code S} element. *

    + * *

    * These elements will be appended as children of the {@code } element. *

    + * *

    * The {@code } 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); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 28b802b9d..0c2958675 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -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) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java new file mode 100644 index 000000000..c1ac4f5f6 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java @@ -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}. + * + *

    + * It stores, per stream: + *

      + *
    • its content (the URL/the base URL of streams);
    • + *
    • whether its content is the URL the content itself or the base URL;
    • + *
    • its associated {@link ItagItem}.
    • + *
    + *

    + */ +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; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index 5617fd12c..d10fce3bc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -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 pair : streamingDataAndCpnLoopList) { itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey, - itagTypeWanted, streamType, pair.getSecond())); + itagTypeWanted, pair.getSecond())); } final List 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)} + * + *

    + * The {@code StreamBuilderHelper} will set the following attributes in the + * {@link AudioStream}s built: + *

      + *
    • the {@link ItagItem}'s id of the stream as its id;
    • + *
    • {@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and + * and as the value of {@code isUrl};
    • + *
    • the media format returned by the {@link ItagItem} as its media format;
    • + *
    • its average bitrate with the value returned by {@link + * ItagItem#getAverageBitrate()};
    • + *
    • the {@link ItagItem};
    • + *
    • the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams + * and ended streams.
    • + *
    + *

    + * + *

    + * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. + *

    + * + * @return a {@link StreamBuilderHelper} to build {@link AudioStream}s + */ @Nonnull private StreamBuilderHelper getAudioStreamBuilderHelper() { return new StreamBuilderHelper() { @@ -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)} + * + *

    + * The {@code StreamBuilderHelper} will set the following attributes in the + * {@link VideoStream}s built: + *

      + *
    • the {@link ItagItem}'s id of the stream as its id;
    • + *
    • {@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and + * and as the value of {@code isUrl};
    • + *
    • the media format returned by the {@link ItagItem} as its media format;
    • + *
    • whether it is video-only with the {@code areStreamsVideoOnly} parameter
    • + *
    • the {@link ItagItem};
    • + *
    • the resolution, by trying to use, in this order: + *
        + *
      1. the height returned by the {@link ItagItem} + {@code p} + the frame rate if + * it is more than 30;
      2. + *
      3. the default resolution string from the {@link ItagItem};
      4. + *
      5. an {@link Utils#EMPTY_STRING empty string}.
      6. + *
      + *
    • + *
    • the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams + * and ended streams.
    • + *
    + * + *

    + * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. + *

    + * + * @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 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 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 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 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 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 diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java index b5d673596..65062a649 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java @@ -68,10 +68,10 @@ public final class AudioStream extends Stream { } /** - * Set the identifier of the {@link SubtitlesStream}. + * Set the identifier of the {@link AudioStream}. * *

    - * It must be not null and should be non empty. + * It must not be null and should be non empty. *

    * *

    @@ -79,7 +79,7 @@ public final class AudioStream extends Stream { * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class. *

    * - * @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}. * *

    - * It must be non null and should be non empty. + * It must not be null, and should be non empty. *

    * * @param content the content of the {@link AudioStream} @@ -111,8 +111,8 @@ public final class AudioStream extends Stream { *

    * 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. *

    * *

    @@ -131,7 +131,7 @@ public final class AudioStream extends Stream { * Set the {@link DeliveryMethod} of the {@link AudioStream}. * *

    - * It must be not null. + * It must not be null. *

    * *

    @@ -139,7 +139,7 @@ public final class AudioStream extends Stream { *

    * * @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}. * *

    - * 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. *

    * *

    @@ -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, diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java index db74e91ab..444023e58 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java @@ -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 the + * Dynamic Adaptive Streaming over HTTP Wikipedia page and + * DASH Industry Forum's website 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 the HTTP Live Streaming + * page and Apple's developers website page + * about HTTP Live Streaming 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 Wikipedia's page about adaptive bitrate streaming, + * section Microsoft Smooth Streaming (MSS) 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 Wikipedia's BitTorrent's page, + * Wikipedia's page about torrent files + * and for more information about the + * BitTorrent protocol */ TORRENT } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java index b76594a6f..df47afdb5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java @@ -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). * *

    - * 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. *

    */ 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}). * *

    * 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. *

    * *

    - * 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. *

    * * @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. * *

    - * 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. *

    * - * @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 { * *

    * 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. *

    * * @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. * *

    - * 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. *

    * * @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); } -} \ No newline at end of file +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java index 082c7a228..5e6f438ea 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java @@ -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. * *

    - * Note that contents can contain audio streams even if they also contain + * Note that contents may contain audio streams even if they also contain * video streams (video-only or video with audio, depending of the stream/the content/the * service). *

    @@ -46,23 +46,22 @@ public enum StreamType { * *

    * Note that contents can contain audio live streams 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). *

    */ 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. * *

    * Note that contents returned as live audio streams should not return live video streams. *

    * *

    - * 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. *

    */ AUDIO_LIVE_STREAM, @@ -72,10 +71,10 @@ public enum StreamType { * ended live video stream. * *

    - * 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. *

    * *

    diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java index 732d822d7..ddd372343 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java @@ -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}. * *

    - * It must be non null and should be non empty. + * It must not be null, and should be non empty. *

    * - * @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. *

    * @@ -99,7 +99,7 @@ public final class SubtitlesStream extends Stream { * Set the {@link DeliveryMethod} of the {@link SubtitlesStream}. * *

    - * It must be not null. + * It must not be null. *

    * *

    @@ -107,7 +107,7 @@ public final class SubtitlesStream extends Stream { *

    * * @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}. * *

    - * 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. *

    * *

    @@ -139,7 +139,7 @@ public final class SubtitlesStream extends Stream { * Set the language code of the {@link SubtitlesStream}. * *

    - * It must be not null and should be not an empty string. + * It must not be null and should not be an empty string. *

    * * @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 { * *

    * 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. *

    * * @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; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java index c8ab5cfc9..e8b2595b9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java @@ -81,7 +81,7 @@ public final class VideoStream extends Stream { * Set the identifier of the {@link VideoStream}. * *

    - * It must be not null and should be non empty. + * It must not be null, and should be non empty. *

    * *

    @@ -89,7 +89,7 @@ public final class VideoStream extends Stream { * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class. *

    * - * @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}. * *

    - * It must be non null and should be non empty. + * It must not be null, and should be non empty. *

    * * @param content the content of the {@link VideoStream} @@ -120,8 +120,8 @@ public final class VideoStream extends Stream { * *

    * 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. *

    * *

    @@ -140,7 +140,7 @@ public final class VideoStream extends Stream { * Set the {@link DeliveryMethod} of the {@link VideoStream}. * *

    - * It must be not null. + * It must not be null. *

    * *

    @@ -148,7 +148,7 @@ public final class VideoStream extends Stream { *

    * * @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}. * *

    - * 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. *

    * *

    @@ -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, diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java index a644272d1..0cd23fcaf 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java @@ -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;