From 2f061b8dbdc5f18d80d0b907a105b9ac48c2f3ca Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Thu, 3 Mar 2022 09:42:20 +0100
Subject: [PATCH 01/38] Add support of other delivery methods than progressive
HTTP in Stream classes
Stream constructors are now private and streams can be constructed with new Builder classes per stream class. This change has been made to prevent creating and using several constructors in stream classes.
Some default cases have been also added in these Builder classes, so not everything has to be set, depending of the service and the content.
---
.../newpipe/extractor/stream/AudioStream.java | 367 ++++++++++++--
.../extractor/stream/DeliveryMethod.java | 37 ++
.../newpipe/extractor/stream/Stream.java | 253 +++++++---
.../extractor/stream/SubtitlesStream.java | 329 ++++++++++++-
.../newpipe/extractor/stream/VideoStream.java | 465 ++++++++++++++++--
5 files changed, 1290 insertions(+), 161 deletions(-)
create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java
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 c1cf2e0e1..b5d673596 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
@@ -4,30 +4,36 @@ package org.schabi.newpipe.extractor.stream;
* Created by Christian Schabesberger on 04.03.16.
*
* Copyright (C) Christian Schabesberger 2016
+ * It must be not null and should be non empty.
+ *
+ * If you are not able to get an identifier, use the static constant {@link
+ * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
+ *
+ * It must be non null and should be non empty.
+ *
+ * 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.
+ *
+ * The default value is {@code null}.
+ *
+ * It must be not null.
+ *
+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}.
+ *
+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
+ * they have been parsed.
+ *
+ * The default value is {@code null}.
+ *
+ * The default value is {@link #UNKNOWN_BITRATE}.
+ *
+ * {@link ItagItem}s are YouTube specific objects, so they are only known for this service
+ * and can be null.
+ *
+ * The default value is {@code null}.
+ *
+ * The identifier and the content (and so the {@code isUrl} boolean) properties must have
+ * been set.
+ *
+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the
+ * ones of the YouTube service.
+ *
+ * An itag should not have a negative value so {@code -1} is used for this constant.
+ *
+ * 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.
+ *
+ * Note: This method always returns always false if the stream passed is null.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * If the stream is not a YouTube stream, this value will always be null.
+ *
+ * It must be non null and should be non empty.
+ *
+ * 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
+ * not be determined.
+ *
+ * The default value is {@code null}.
+ *
+ * It must be not null.
+ *
+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}.
+ *
+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
+ * they have been parsed.
+ *
+ * The default value is {@code null}.
+ *
+ * It must be not null and should be not an empty string.
+ *
+ * The content (and so the {@code isUrl} boolean), the language code and the {@code
+ * isAutoGenerated} properties must have been set.
+ *
+ * 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
+ *
+ * Some streaming services can generate subtitles for their contents, like YouTube.
+ *
+ * It must be not null and should be non empty.
+ *
+ * If you are not able to get an identifier, use the static constant {@link
+ * Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
+ *
+ * It must be non null and should be non empty.
+ *
+ * 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.
+ *
+ * The default value is {@code null}.
+ *
+ * It must be not null.
+ *
+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}.
+ *
+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
+ * they have been parsed.
+ *
+ * The default value is {@code null}.
+ *
+ * This property must be set before building the {@link VideoStream}.
+ *
+ * This resolution can be used by clients to know the quality of the video stream.
+ *
+ * If you are not able to know the resolution, you should use {@link #RESOLUTION_UNKNOWN}
+ * as the resolution of the video stream.
+ *
+ * It must be set before building the builder and not null.
+ *
+ * {@link ItagItem}s are YouTube specific objects, so they are only known for this service
+ * and can be null.
+ *
+ * The default value is {@code null}.
+ *
+ * The identifier, the content (and so the {@code isUrl} boolean), the {@code isVideoOnly}
+ * and the {@code resolution} properties must have been set.
+ *
+ * It can be unknown for some streams, like for HLS master playlists. In this case,
+ * {@link #RESOLUTION_UNKNOWN} is returned by this method.
+ *
- * Video only streams have no audio
+ * Return whether the stream is video-only.
*
- * @return {@code true} if this stream is vid
+ *
+ * Video-only streams have no audio.
+ *
+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the
+ * ones of the YouTube service.
+ *
+ * It has video, video only and audio streams.
+ *
+ * Info about DASH MPD can be found here
+ *
+ * @param dashMpdUrl URL to the DASH MPD
+ * @see
+ * www.brendanlog.com
+ */
+ @Nonnull
+ public static Result getStreams(final String dashMpdUrl)
+ throws DashMpdParsingException, ReCaptchaException {
+ final String dashDoc;
+ final Downloader downloader = NewPipe.getDownloader();
+ try {
+ dashDoc = downloader.get(dashMpdUrl).responseBody();
+ } catch (final IOException e) {
+ throw new DashMpdParsingException("Could not fetch DASH manifest: " + dashMpdUrl, e);
+ }
+
+ try {
+ final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ final DocumentBuilder builder = factory.newDocumentBuilder();
+ final InputStream stream = new ByteArrayInputStream(dashDoc.getBytes());
+
+ final Document doc = builder.parse(stream);
+ final NodeList representationList = doc.getElementsByTagName("Representation");
+
+ final List
- * It has video, video only and audio streams and will only add to the list if it don't
- * find a similar stream in the respective lists (calling {@link Stream#equalStats}).
- *
- * Info about dash MPD can be found
- * here.
- *
- * @param streamInfo where the parsed streams will be added
- */
- public static ParserResult getStreams(final StreamInfo streamInfo)
- throws DashMpdParsingException, ReCaptchaException {
- final String dashDoc;
- final Downloader downloader = NewPipe.getDownloader();
- try {
- dashDoc = downloader.get(streamInfo.getDashMpdUrl()).responseBody();
- } catch (final IOException ioe) {
- throw new DashMpdParsingException(
- "Could not get dash mpd: " + streamInfo.getDashMpdUrl(), ioe);
- }
-
- try {
- final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- final DocumentBuilder builder = factory.newDocumentBuilder();
- final InputStream stream = new ByteArrayInputStream(dashDoc.getBytes());
-
- final Document doc = builder.parse(stream);
- final NodeList representationList = doc.getElementsByTagName("Representation");
-
- final List
+ * 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.
+ *
+ * Note that this value is only known for video itags, so {@link
+ * #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags.
+ *
+ * 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.
+ *
+ * It is only known for video itags.
+ *
+ * It is only known for audio itags, so {@link #AVERAGE_BITRATE_UNKNOWN} is always returned for
+ * other itag types.
+ *
+ * Bitrate of video itags and precise bitrate of audio itags can be known using
+ * {@link #getBitrate()}.
+ *
+ * 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.
+ *
+ * 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 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 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
+ * 0.
+ *
+ * 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 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} will be set/used for video streams or if
+ * this value is less than or equal to 0.
+ *
+ * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is
+ * returned for other stream types or if this value is less than or equal to 0.
+ *
+ * It is only known for DASH progressive streams, so {@link #APPROX_DURATION_MS_UNKNOWN} is
+ * set/used for other stream types or if this value is less than or equal to 0.
+ *
+ * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is
+ * returned for other stream types or if this value is less than or equal to 0.
+ *
+ * It is only known for DASH progressive streams, so {@link #CONTENT_LENGTH_UNKNOWN} is
+ * set/used for other stream types or if this value is less than or equal to 0.
+ *
+ * There can be several DASH streams, so the URL of the first found is returned by this method.
+ *
+ * You can find the other video DASH streams by using {@link #getVideoStreams()}
+ *
+ * There can be several HLS streams, so the URL of the first found is returned by this method.
+ *
+ * You can find the other video HLS streams by using {@link #getVideoStreams()}
+ *
+ * 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.
+ *
* 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.
+ *
+ * It doesn't make sense to use this enum constant outside of the extractor as it will never be
+ * returned by an {@link org.schabi.newpipe.extractor.Extractor extractor} and is only used
+ * internally.
+ *
+ * Note that contents can contain audio streams even if they also contain
+ * video streams (video-only or video with audio, depending of the stream/the content/the
+ * service).
+ *
+ * Note that contents returned as audio streams should not return video streams.
+ *
+ * So, in order to prevent unexpected behaviors, stream extractors which are returning this
+ * stream type for a content should ensure that no video stream is returned for this content.
+ *
+ * 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).
+ *
+ * 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.
+ *
+ * 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 contents can contain post-live audio streams even if they also
+ * contain post-live video streams (video-only or video with audio, depending of the stream/the
+ * content/the service).
+ *
+ * Note that most of ended live audio streams extracted with this value are processed as
+ * {@link #AUDIO_STREAM regular audio streams} later, because the service may encode them
+ * again later.
+ *
+ * Contents returned as post-live audio streams should not return post-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 post-live video stream is returned for this
+ * content.
+ *
+ * It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages.
+ *
+ * This list is automatically cleared in the execution of
+ * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH
+ * manifest is converted to a string.
+ *
+ * This list is automatically cleared in the execution of
+ * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH
+ * manifest is converted to a string.
+ *
+ * Initialization and index ranges are available to get metadata (the corresponding values
+ * are returned in the player response).
+ *
+ * The first sequence (which can be fetched with the {@link #SQ_0} param) contains all the
+ * 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.
+ *
+ * Each sequence (which can be fetched with the {@link #SQ_0} param) contains its own
+ * metadata (sidx boxes, segment length, ...), which make no need of an initialization
+ * segment.
+ *
+ * Only used for livestreams (ended or running).
+ *
+ * OTF streams are YouTube-DASH specific streams which work with sequences and without the need
+ * to get a manifest (even if one is provided, it is not used by official clients).
+ *
+ * They can be found only on videos; mostly those with a small amount of views, or ended
+ * livestreams which have just been re-encoded as normal videos.
+ * This method needs:
+ * In order to generate the DASH manifest, this method will:
+ *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used
+ * as the stream duration.
+ *
+ * Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which
+ * works with sequences and without the need to get a manifest (even if one is provided but not
+ * used by main clients (and is complete for big ended livestreams because it doesn't return
+ * the full stream)).
+ *
+ * They can be found only on livestreams which have ended very recently (a few hours, most of
+ * the time)
+ * This method needs:
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended + * livestream, which cannot be null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * cannot be null + * @param targetDurationSec the target duration of each sequence, in seconds (this + * value is returned with the targetDurationSec field for + * each stream in YouTube player response) + * @param durationSecondsFallback the duration of the ended livestream which will be used + * if the duration could not be extracted from the first + * sequence + * @return the manifest generated into a string + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromPostLiveStreamDvrStreamingUrl( + @Nonnull String postLiveStreamDvrStreamingUrl, + @Nonnull final ItagItem itagItem, + final int targetDurationSec, + final long durationSecondsFallback) + throws YoutubeDashManifestCreationException { + if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) { + return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(postLiveStreamDvrStreamingUrl) + .getSecond(); + } + final String originalPostLiveStreamDvrStreamingUrl = 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 + ")"); + } + + try { + // Try to avoid redirects when streaming the content by saving the latest URL we get + // from video servers. + final Response response = getInitializationResponse(postLiveStreamDvrStreamingUrl, + itagItem, DeliveryType.LIVE); + postLiveStreamDvrStreamingUrl = 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 " + + responseCode); + } + + final Map+ * Progressive streams are YouTube DASH streams which work with range requests and without the + * need to get a manifest. + *
+ * + *+ * They can be found on all videos, and for all streams for most of videos which come from a + * YouTube partner, and on videos with a large number of views. + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param progressiveStreamingBaseUrl the base URL of the progressive stream, which cannot be + * null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * cannot be null + * @param durationSecondsFallback the duration of the progressive stream which will be used + * if the duration could not be extracted from the first + * sequence + * @return the manifest generated into a string + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromProgressiveStreamingUrl( + @Nonnull String progressiveStreamingBaseUrl, + @Nonnull final ItagItem itagItem, + final long durationSecondsFallback) throws YoutubeDashManifestCreationException { + if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) { + return 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 + ")"); + } + + final Document document = generateDocumentAndMpdElement(new String[]{}, + DeliveryType.PROGRESSIVE, itagItem, durationSecondsFallback); + generatePeriodElement(document); + generateAdaptationSetElement(document, itagItem); + generateRoleElement(document); + generateRepresentationElement(document, itagItem); + if (itagItem.itagType == ItagItem.ItagType.AUDIO) { + generateAudioChannelConfigurationElement(document, itagItem); + } + generateBaseUrlElement(document, progressiveStreamingBaseUrl); + generateSegmentBaseElement(document, itagItem); + generateInitializationElement(document, itagItem); + + return buildResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); + } + + /** + * Get the "initialization" {@link Response response} of a stream. + * + *+ * This method fetches: + *
+ * This method will follow redirects for web clients, which works in the following way: + *
+ * The duration of OTF streams is not returned into the player response and needs to be + * calculated by adding the duration of each segment. + *
+ * + * @param segmentDuration the segment duration object extracted from the initialization + * sequence of the stream + * @return the duration of the OTF stream + * @throws YoutubeDashManifestCreationException if something goes wrong when parsing the + * {@code segmentDuration} object + */ + private static int getStreamDuration(@Nonnull final String[] segmentDuration) + throws YoutubeDashManifestCreationException { + try { + int streamLengthMs = 0; + for (final String segDuration : segmentDuration) { + final String[] segmentLengthRepeat = segDuration.split("\\(r="); + int segmentRepeatCount = 0; + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + segmentRepeatCount = Integer.parseInt(Utils.removeNonDigitCharacters( + segmentLengthRepeat[1])); + } + final int segmentLength = Integer.parseInt(segmentLengthRepeat[0]); + streamLengthMs += segmentLength + segmentRepeatCount * segmentLength; + } + return streamLengthMs; + } catch (final NumberFormatException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: unable to get the length of the stream", e); + } + } + + /** + * Create a {@link Document} object and generate the {@code
+ * The generated {@code
+ * {@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. + *
+ * + * @param segmentDuration the segment duration object extracted from the initialization + * sequence of the stream + * @param deliveryType the {@link DeliveryType} of the stream, see the enum for + * possible values + * @param durationSecondsFallback the duration in seconds, extracted from player response, used + * as a fallback + * @return a {@link Document} object which contains a {@code
+ * The {@code
+ * The {@code
+ * This element, with its attributes and values, is: + *
+ *
+ * {@code
+ * The {@code
+ * The {@code
+ * This method is only used when generating DASH manifests of audio streams. + *
+ *
+ * It will produce the following element:
+ *
+ * {@code
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * It generates the following element:
+ *
+ * {@code
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
+ * as the second parameter)
+ *
+ * The {@code
+ * This method is only used when generating DASH manifests from progressive streams. + *
+ *
+ * It generates the following element:
+ *
+ * {@code
+ * The {@code
+ * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. + *
+ *
+ * It will produce a {@code
+ *
+ *
+ * The {@code
+ * The {@code
+ * By parsing by the first media sequence, we know how many durations and repetitions there are + * 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
+ * The {@code
+ * We don't know the exact duration of segments for post-live-DVR streams but an
+ * average instead (which is the {@code targetDurationSec} value), so we can use the following
+ * structure to generate the segment timeline for DASH manifests of ended livestreams:
+ *
+ * {@code }
+ *
+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param otfStreamsCacheLimit the maximum number of OTF streams in the corresponding cache. + */ + public static void setOtfStreamsMaximumSize(final int otfStreamsCacheLimit) { + GENERATED_OTF_MANIFESTS.setMaximumSize(otfStreamsCacheLimit); + } + + /** + * Set the limit of cached post-live-DVR streams. + * + *+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param postLiveDvrStreamsCacheLimit the maximum number of post-live-DVR streams in the + * corresponding cache. + */ + public static void setPostLiveDvrStreamsMaximumSize(final int postLiveDvrStreamsCacheLimit) { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(postLiveDvrStreamsCacheLimit); + } + + /** + * Set the limit of cached progressive streams, if needed. + * + *+ * When the cache limit size is reached, oldest manifests will be removed. + *
+ * + *+ * If the new cache size set is less than the number of current cached manifests, oldest + * manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param progressiveCacheLimit the maximum number of progressive streams in the corresponding + * cache. + */ + public static void setProgressiveStreamsMaximumSize(final int progressiveCacheLimit) { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(progressiveCacheLimit); + } + + /** + * Set the limit of cached OTF manifests, cached post-live-DVR manifests and cached progressive + * manifests. + * + *+ * When the caches limit size are reached, oldest manifests will be removed from their + * respective cache. + *
+ * + *+ * For each cache, if its new size set is less than the number of current cached manifests, + * oldest manifests will be also removed. + *
+ * + *+ * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be + * thrown. + *
+ * + * @param cachesLimit the maximum size of OTF, post-live-DVR and progressive caches + */ + public static void setManifestsCachesMaximumSize(final int cachesLimit) { + GENERATED_OTF_MANIFESTS.setMaximumSize(cachesLimit); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); + } + + /** + * Clear cached OTF manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearOtfCachedManifests() { + GENERATED_OTF_MANIFESTS.clear(); + } + + /** + * Clear cached post-live-DVR streams manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearPostLiveDvrStreamsCachedManifests() { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); + } + + /** + * Clear cached progressive streams manifests. + * + *+ * The limit of this cache size set, if there is one, will be not unset. + *
+ */ + public static void clearProgressiveCachedManifests() { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); + } + + /** + * Clear cached OTF manifests, cached post-live-DVR streams manifests and cached progressive + * manifests in their respective caches. + * + *+ * The limit of the caches size set, if any, will be not unset. + *
+ */ + public static void clearManifestsInCaches() { + GENERATED_OTF_MANIFESTS.clear(); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); + } + + /** + * Reset OTF manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetOtfManifestsCache() { + GENERATED_OTF_MANIFESTS.reset(); + } + + /** + * Reset post-live-DVR manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetPostLiveDvrManifestsCache() { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); + } + + /** + * Reset progressive manifests cache. + * + *+ * All cached manifests will be removed and the clear factor and the maximum size will be set + * to their default values. + *
+ */ + public static void resetProgressiveManifestsCache() { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); + } + + /** + * Reset OTF, post-live-DVR and progressive manifests caches. + * + *+ * For each cache, all cached manifests will be removed and the clear factor and the maximum + * size will be set to their default values. + *
+ */ + public static void resetCaches() { + GENERATED_OTF_MANIFESTS.reset(); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java new file mode 100644 index 000000000..8e885f7cf --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java @@ -0,0 +1,301 @@ +package org.schabi.newpipe.extractor.utils; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A {@link Serializable serializable} cache class used by the extractor to cache manifests + * generated with extractor's manifests generators. + * + *+ * It relies internally on a {@link ConcurrentHashMap} to allow concurrent access to the cache. + *
+ * + * @param+ * The default value is {@link #DEFAULT_MAXIMUM_SIZE}. + *
+ */ + private int maximumSize = DEFAULT_MAXIMUM_SIZE; + + /** + * The clear factor of the cache, which is a double between {@code 0} and {@code 1} excluded. + * + *+ * The default value is {@link #DEFAULT_CLEAR_FACTOR}. + *
+ */ + private double clearFactor = DEFAULT_CLEAR_FACTOR; + + /** + * Creates a new {@link ManifestCreatorCache}. + */ + public ManifestCreatorCache() { + concurrentHashMap = new ConcurrentHashMap<>(); + } + + /** + * Tests if the specified key is in the cache. + * + * @param key the key to test its presence in the cache + * @return {@code true} if the key is in the cache, {@code false} otherwise. + */ + public boolean containsKey(final K key) { + return concurrentHashMap.containsKey(key); + } + + /** + * Returns the value to which the specified key is mapped, or {@code null} if the cache + * contains no mapping for the key. + * + * @param key the key to which getting its value + * @return the value to which the specified key is mapped, or {@code null} + */ + @Nullable + public Pair+ * If the cache limit is reached, oldest elements will be cleared first using the load factor + * and the maximum size. + *
+ * + * @param key the key to put + * @param value the value to associate to the key + * + * @return the previous value associated with the key, or {@code null} if there was no mapping + * for the key (note that a null return can also indicate that the cache previously associated + * {@code null} with the key). + */ + @Nullable + public V put(final K key, final V value) { + if (!concurrentHashMap.containsKey(key) && concurrentHashMap.size() == maximumSize) { + final int newCacheSize = (int) Math.round(maximumSize * clearFactor); + keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); + } + + final Pair+ * The cache will be empty after this method is called. + *
+ */ + public void clear() { + concurrentHashMap.clear(); + } + + /** + * Resets the cache. + * + *+ * The cache will be empty and the clear factor and the maximum size will be reset to their + * default values. + *
+ * + * @see #clear() + * @see #resetClearFactor() + * @see #resetMaximumSize() + */ + public void reset() { + clear(); + resetClearFactor(); + resetMaximumSize(); + } + + /** + * Returns the number of cached manifests in the cache. + * + * @return the number of cached manifests + */ + public int size() { + return concurrentHashMap.size(); + } + + /** + * Gets the maximum size of the cache. + * + * @return the maximum size of the cache + */ + public long getMaximumSize() { + return maximumSize; + } + + /** + * Sets the maximum size of the cache. + * + * If the current cache size is more than the new maximum size, the percentage of one less the + * clear factor of the maximum new size of manifests in the cache will be removed. + * + * @param maximumSize the new maximum size of the cache + * @throws IllegalArgumentException if {@code maximumSize} is less than or equal to 0 + */ + public void setMaximumSize(final int maximumSize) { + if (maximumSize <= 0) { + throw new IllegalArgumentException("Invalid maximum size"); + } + + if (maximumSize < this.maximumSize && !concurrentHashMap.isEmpty()) { + final int newCacheSize = (int) Math.round(maximumSize * clearFactor); + keepNewestEntries(newCacheSize != 0 ? newCacheSize : 1); + } + + this.maximumSize = maximumSize; + } + + /** + * Resets the maximum size of the cache to its {@link #DEFAULT_MAXIMUM_SIZE default value}. + */ + public void resetMaximumSize() { + this.maximumSize = DEFAULT_MAXIMUM_SIZE; + } + + /** + * Gets the current clear factor of the cache, used when the cache limit size is reached. + * + * @return the current clear factor of the cache + */ + public double getClearFactor() { + return clearFactor; + } + + /** + * Sets the clear factor of the cache, used when the cache limit size is reached. + * + *+ * The clear factor must be a double between {@code 0} excluded and {@code 1} excluded. + *
+ * + *+ * Note that it will be only used the next time the cache size limit is reached. + *
+ * + * @param clearFactor the new clear factor of the cache + * @throws IllegalArgumentException if the clear factor passed a parameter is invalid + */ + public void setClearFactor(final double clearFactor) { + if (clearFactor <= 0 || clearFactor >= 1) { + throw new IllegalArgumentException("Invalid clear factor"); + } + + this.clearFactor = clearFactor; + } + + /** + * Resets the clear factor to its {@link #DEFAULT_CLEAR_FACTOR default value}. + */ + public void resetClearFactor() { + this.clearFactor = DEFAULT_CLEAR_FACTOR; + } + + /** + * Reveals whether an object is equal to a {@code ManifestCreator} cache existing object. + * + * @param obj the object to compare with the current {@code ManifestCreatorCache} object + * @return whether the object compared is equal to the current {@code ManifestCreatorCache} + * object + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final ManifestCreatorCache, ?> manifestCreatorCache = + (ManifestCreatorCache, ?>) obj; + return maximumSize == manifestCreatorCache.maximumSize + && Double.compare(manifestCreatorCache.clearFactor, clearFactor) == 0 + && concurrentHashMap.equals(manifestCreatorCache.concurrentHashMap); + } + + /** + * Returns a hash code of the current {@code ManifestCreatorCache}, using its + * {@link #maximumSize maximum size}, {@link #clearFactor clear factor} and + * {@link #concurrentHashMap internal concurrent hash map} used as a cache. + * + * @return a hash code of the current {@code ManifestCreatorCache} + */ + @Override + public int hashCode() { + return Objects.hash(maximumSize, clearFactor, concurrentHashMap); + } + + /** + * Returns a string version of the {@link ConcurrentHashMap} used internally as the cache. + * + * @return the string version of the {@link ConcurrentHashMap} used internally as the cache + */ + @Override + public String toString() { + return concurrentHashMap.toString(); + } + + /** + * Keeps only the newest entries in a cache. + * + *+ * This method will first collect the entries to remove by looping through the concurrent hash + * map + *
+ * + * @param newLimit the new limit of the cache + */ + private void keepNewestEntries(final int newLimit) { + final int difference = concurrentHashMap.size() - newLimit; + final ArrayList+ * We cannot test the generation of DASH manifests for ended livestreams because these videos will + * be re-encoded as normal videos later, so we can't use a specific video. + *
+ * + *+ * The generation of DASH manifests for OTF streams, which can be tested, uses a video licenced + * under the Creative Commons Attribution licence (reuse allowed): + * {@code https://www.youtube.com/watch?v=DJ8GQUNUXGM} + *
+ * + *+ * We couldn't use mocks for these tests because the streaming URLs needs to fetched and the IP + * address used to get these URLs is required (used as a param in the URLs; without it, video + * servers return 403/Forbidden HTTP response code). + *
+ * + *+ * So the real downloader will be used everytime on this test class. + *
+ */ +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 { + private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; + private static YoutubeStreamExtractor extractor; + + @BeforeAll + public static void setUp() throws Exception { + YoutubeParsingHelper.resetClientVersionAndKey(); + YoutubeParsingHelper.setNumberGenerator(new Random(1)); + NewPipe.init(DownloaderTestImpl.getInstance()); + extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url); + extractor.fetchPage(); + } + + @Test + void testOtfStreamsANewEraOfOpen() throws Exception { + testStreams(DeliveryMethod.DASH, + extractor.getVideoOnlyStreams()); + testStreams(DeliveryMethod.DASH, + extractor.getAudioStreams()); + // This should not happen because there are no video stream with audio which use the + // DASH delivery method (YouTube OTF stream type) + try { + testStreams(DeliveryMethod.DASH, + extractor.getVideoStreams()); + } catch (final Exception e) { + assertEquals(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + e.getClass(), "The exception thrown was not the one excepted: " + + e.getClass().getName() + + "was thrown instead of YoutubeDashManifestCreationException"); + } + } + + @Test + void testProgressiveStreamsANewEraOfOpen() throws Exception { + testStreams(DeliveryMethod.PROGRESSIVE_HTTP, + extractor.getVideoOnlyStreams()); + testStreams(DeliveryMethod.PROGRESSIVE_HTTP, + extractor.getAudioStreams()); + try { + testStreams(DeliveryMethod.PROGRESSIVE_HTTP, + extractor.getVideoStreams()); + } catch (final Exception e) { + assertEquals(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + e.getClass(), "The exception thrown was not the one excepted: " + + e.getClass().getName() + + "was thrown instead of YoutubeDashManifestCreationException"); + } + } + + private void testStreams(@Nonnull final DeliveryMethod deliveryMethodToTest, + @Nonnull final List extends Stream> streamList) + throws Exception { + int i = 0; + final int streamListSize = streamList.size(); + final boolean isDeliveryMethodToTestProgressiveHttpDeliveryMethod = + deliveryMethodToTest == DeliveryMethod.PROGRESSIVE_HTTP; + final long videoLength = extractor.getLength(); + + // Test at most the first five streams we found + while (i <= YoutubeDashManifestCreatorTest.MAXIMUM_NUMBER_OF_STREAMS_TO_TEST + && i < streamListSize) { + final Stream stream = streamList.get(i); + if (stream.getDeliveryMethod() == deliveryMethodToTest) { + final String baseUrl = stream.getContent(); + assertFalse(isBlank(baseUrl), "The base URL of the stream is empty"); + + final ItagItem itagItem = stream.getItagItem(); + assertNotNull(itagItem, "The itagItem is null"); + + final String dashManifest; + if (isDeliveryMethodToTestProgressiveHttpDeliveryMethod) { + dashManifest = YoutubeDashManifestCreator + .createDashManifestFromProgressiveStreamingUrl(baseUrl, itagItem, + videoLength); + } else if (deliveryMethodToTest == DeliveryMethod.DASH) { + dashManifest = YoutubeDashManifestCreator + .createDashManifestFromOtfStreamingUrl(baseUrl, itagItem, + videoLength); + } else { + throw new IllegalArgumentException( + "The delivery method provided is not the progressive HTTP or the DASH delivery method"); + } + testManifestGenerated(dashManifest, itagItem, + isDeliveryMethodToTestProgressiveHttpDeliveryMethod); + assertFalse(isBlank(dashManifest), "The DASH manifest is null or empty: " + + dashManifest); + } + i++; + } + } + + private void testManifestGenerated(final String dashManifest, + @Nonnull final ItagItem itagItem, + final boolean isAProgressiveStreamingUrl) + throws Exception { + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + final Document document = documentBuilder.parse(new InputSource( + new StringReader(dashManifest))); + + testMpdElement(document); + testPeriodElement(document); + testAdaptationSetElement(document, itagItem); + testRoleElement(document); + testRepresentationElement(document, itagItem); + if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { + testAudioChannelConfigurationElement(document, itagItem); + } + if (isAProgressiveStreamingUrl) { + testBaseUrlElement(document); + testSegmentBaseElement(document, itagItem); + testInitializationElement(document, itagItem); + } else { + testSegmentTemplateElement(document); + testSegmentTimelineAndSElements(document); + } + } + + private void testMpdElement(@Nonnull final Document document) { + final Element mpdElement = (Element) document.getElementsByTagName("MPD") + .item(0); + assertNotNull(mpdElement, "The MPD element doesn't exist"); + assertNull(mpdElement.getParentNode().getNodeValue(), "The MPD element has a parent element"); + + final String mediaPresentationDurationValue = mpdElement + .getAttribute("mediaPresentationDuration"); + assertNotNull(mediaPresentationDurationValue, + "The value of the mediaPresentationDuration attribute is empty or the corresponding attribute doesn't exist"); + assertTrue(mediaPresentationDurationValue.startsWith("PT"), + "The mediaPresentationDuration attribute of the DASH manifest is not valid"); + } + + private void testPeriodElement(@Nonnull final Document document) { + final Element periodElement = (Element) document.getElementsByTagName("Period") + .item(0); + assertNotNull(periodElement, "The Period element doesn't exist"); + assertTrue(periodElement.getParentNode().isEqualNode( + document.getElementsByTagName("MPD").item(0)), + "The MPD element doesn't contain a Period element"); + } + + private void testAdaptationSetElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element adaptationSetElement = (Element) document + .getElementsByTagName("AdaptationSet").item(0); + assertNotNull(adaptationSetElement, "The AdaptationSet element doesn't exist"); + assertTrue(adaptationSetElement.getParentNode().isEqualNode( + document.getElementsByTagName("Period").item(0)), + "The Period element doesn't contain an AdaptationSet element"); + + final String mimeTypeDashManifestValue = adaptationSetElement + .getAttribute("mimeType"); + assertFalse(isBlank(mimeTypeDashManifestValue), + "The value of the mimeType attribute is empty or the corresponding attribute doesn't exist"); + + final String mimeTypeItagItemValue = itagItem.getMediaFormat().getMimeType(); + assertFalse(isBlank(mimeTypeItagItemValue), "The mimeType of the ItagItem is empty"); + + assertEquals(mimeTypeDashManifestValue, mimeTypeItagItemValue, + "The mimeType attribute of the DASH manifest (" + mimeTypeItagItemValue + + ") is not equal to the mimeType set in the ItagItem object (" + + mimeTypeItagItemValue + ")"); + } + + private void testRoleElement(@Nonnull final Document document) { + final Element roleElement = (Element) document.getElementsByTagName("Role") + .item(0); + assertNotNull(roleElement, "The Role element doesn't exist"); + assertTrue(roleElement.getParentNode().isEqualNode( + document.getElementsByTagName("AdaptationSet").item(0)), + "The AdaptationSet element doesn't contain a Role element"); + } + + private void testRepresentationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element representationElement = (Element) document + .getElementsByTagName("Representation").item(0); + assertNotNull(representationElement, "The Representation element doesn't exist"); + assertTrue(representationElement.getParentNode().isEqualNode( + document.getElementsByTagName("AdaptationSet").item(0)), + "The AdaptationSet element doesn't contain a Representation element"); + + final String bandwidthDashManifestValue = representationElement + .getAttribute("bandwidth"); + assertFalse(isBlank(bandwidthDashManifestValue), + "The value of the bandwidth attribute is empty or the corresponding attribute doesn't exist"); + + final int bandwidthDashManifest; + try { + bandwidthDashManifest = Integer.parseInt(bandwidthDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the bandwidth attribute is not an integer", + e); + } + assertTrue(bandwidthDashManifest > 0, + "The value of the bandwidth attribute is less than or equal to 0"); + + final int bitrateItagItem = itagItem.getBitrate(); + assertTrue(bitrateItagItem > 0, + "The bitrate of the ItagItem is less than or equal to 0"); + + assertEquals(bandwidthDashManifest, bitrateItagItem, + "The value of the bandwidth attribute of the DASH manifest (" + + bandwidthDashManifest + + ") is not equal to the bitrate value set in the ItagItem object (" + + bitrateItagItem + ")"); + + final String codecsDashManifestValue = representationElement.getAttribute("codecs"); + assertFalse(isBlank(codecsDashManifestValue), + "The value of the codecs attribute is empty or the corresponding attribute doesn't exist"); + + final String codecsItagItemValue = itagItem.getCodec(); + assertFalse(isBlank(codecsItagItemValue), "The codec of the ItagItem is empty"); + + assertEquals(codecsDashManifestValue, codecsItagItemValue, + "The value of the codecs attribute of the DASH manifest (" + + codecsDashManifestValue + + ") is not equal to the codecs value set in the ItagItem object (" + + codecsItagItemValue + ")"); + + if (itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY + || itagItem.itagType == ItagItem.ItagType.VIDEO) { + testVideoItagItemAttributes(representationElement, itagItem); + } + + final String idDashManifestValue = representationElement.getAttribute("id"); + assertFalse(isBlank(idDashManifestValue), + "The value of the id attribute is empty or the corresponding attribute doesn't exist"); + + final int idDashManifest; + try { + idDashManifest = Integer.parseInt(idDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the id attribute is not an integer", + e); + } + assertTrue(idDashManifest > 0, "The value of the id attribute is less than or equal to 0"); + + final int idItagItem = itagItem.id; + assertTrue(idItagItem > 0, "The id of the ItagItem is less than or equal to 0"); + assertEquals(idDashManifest, idItagItem, + "The value of the id attribute of the DASH manifest (" + idDashManifestValue + + ") is not equal to the id of the ItagItem object (" + idItagItem + + ")"); + } + + private void testVideoItagItemAttributes(@Nonnull final Element representationElement, + @Nonnull final ItagItem itagItem) { + final String frameRateDashManifestValue = representationElement + .getAttribute("frameRate"); + assertFalse(isBlank(frameRateDashManifestValue), + "The value of the frameRate attribute is empty or the corresponding attribute doesn't exist"); + + final int frameRateDashManifest; + try { + frameRateDashManifest = Integer.parseInt(frameRateDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the frameRate attribute is not an integer", + e); + } + assertTrue(frameRateDashManifest > 0, + "The value of the frameRate attribute is less than or equal to 0"); + + final int fpsItagItem = itagItem.getFps(); + assertTrue(fpsItagItem > 0, "The fps of the ItagItem is unknown"); + + assertEquals(frameRateDashManifest, fpsItagItem, + "The value of the frameRate attribute of the DASH manifest (" + + frameRateDashManifest + + ") is not equal to the frame rate value set in the ItagItem object (" + + fpsItagItem + ")"); + + final String heightDashManifestValue = representationElement.getAttribute("height"); + assertFalse(isBlank(heightDashManifestValue), + "The value of the height attribute is empty or the corresponding attribute doesn't exist"); + + final int heightDashManifest; + try { + heightDashManifest = Integer.parseInt(heightDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the height attribute is not an integer", + e); + } + assertTrue(heightDashManifest > 0, + "The value of the height attribute is less than or equal to 0"); + + final int heightItagItem = itagItem.getHeight(); + assertTrue(heightItagItem > 0, + "The height of the ItagItem is less than or equal to 0"); + + assertEquals(heightDashManifest, heightItagItem, + "The value of the height attribute of the DASH manifest (" + + heightDashManifest + + ") is not equal to the height value set in the ItagItem object (" + + heightItagItem + ")"); + + final String widthDashManifestValue = representationElement.getAttribute("width"); + assertFalse(isBlank(widthDashManifestValue), + "The value of the width attribute is empty or the corresponding attribute doesn't exist"); + + final int widthDashManifest; + try { + widthDashManifest = Integer.parseInt(widthDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the width attribute is not an integer", + e); + } + assertTrue(widthDashManifest > 0, + "The value of the width attribute is less than or equal to 0"); + + final int widthItagItem = itagItem.getWidth(); + assertTrue(widthItagItem > 0, "The width of the ItagItem is less than or equal to 0"); + + assertEquals(widthDashManifest, widthItagItem, + "The value of the width attribute of the DASH manifest (" + widthDashManifest + + ") is not equal to the width value set in the ItagItem object (" + + widthItagItem + ")"); + } + + private void testAudioChannelConfigurationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element audioChannelConfigurationElement = (Element) document + .getElementsByTagName("AudioChannelConfiguration").item(0); + assertNotNull(audioChannelConfigurationElement, + "The AudioChannelConfiguration element doesn't exist"); + assertTrue(audioChannelConfigurationElement.getParentNode().isEqualNode( + document.getElementsByTagName("Representation").item(0)), + "The Representation element doesn't contain an AudioChannelConfiguration element"); + + final String audioChannelsDashManifestValue = audioChannelConfigurationElement + .getAttribute("value"); + assertFalse(isBlank(audioChannelsDashManifestValue), + "The value of the value attribute is empty or the corresponding attribute doesn't exist"); + + final int audioChannelsDashManifest; + try { + audioChannelsDashManifest = Integer.parseInt(audioChannelsDashManifestValue); + } catch (final NumberFormatException e) { + throw new AssertionError( + "The number of audio channels (the value attribute) is not an integer", + e); + } + assertTrue(audioChannelsDashManifest > 0, + "The number of audio channels (the value attribute) is less than or equal to 0"); + + final int audioChannelsItagItem = itagItem.getAudioChannels(); + assertTrue(audioChannelsItagItem > 0, + "The number of audio channels of the ItagItem is less than or equal to 0"); + + assertEquals(audioChannelsDashManifest, audioChannelsItagItem, + "The value of the value attribute of the DASH manifest (" + + audioChannelsDashManifest + + ") is not equal to the number of audio channels set in the ItagItem object (" + + audioChannelsItagItem + ")"); + } + + private void testSegmentTemplateElement(@Nonnull final Document document) { + final Element segmentTemplateElement = (Element) document + .getElementsByTagName("SegmentTemplate").item(0); + assertNotNull(segmentTemplateElement, "The SegmentTemplate element doesn't exist"); + assertTrue(segmentTemplateElement.getParentNode().isEqualNode( + document.getElementsByTagName("Representation").item(0)), + "The Representation element doesn't contain a SegmentTemplate element"); + + final String initializationValue = segmentTemplateElement + .getAttribute("initialization"); + assertFalse(isBlank(initializationValue), + "The value of the initialization attribute is empty or the corresponding attribute doesn't exist"); + try { + new URL(initializationValue); + } catch (final MalformedURLException e) { + throw new AssertionError("The value of the initialization attribute is not an URL", + e); + } + assertTrue(initializationValue.endsWith("&sq=0&rn=0"), + "The value of the initialization attribute doesn't end with &sq=0&rn=0"); + + final String mediaValue = segmentTemplateElement.getAttribute("media"); + assertFalse(isBlank(mediaValue), + "The value of the media attribute is empty or the corresponding attribute doesn't exist"); + try { + new URL(mediaValue); + } catch (final MalformedURLException e) { + throw new AssertionError("The value of the media attribute is not an URL", + e); + } + assertTrue(mediaValue.endsWith("&sq=$Number$&rn=$Number$"), + "The value of the media attribute doesn't end with &sq=$Number$&rn=$Number$"); + + final String startNumberValue = segmentTemplateElement.getAttribute("startNumber"); + assertFalse(isBlank(startNumberValue), + "The value of the startNumber attribute is empty or the corresponding attribute doesn't exist"); + assertEquals("1", startNumberValue, + "The value of the startNumber attribute is not equal to 1"); + } + + private void testSegmentTimelineAndSElements(@Nonnull final Document document) { + final Element segmentTimelineElement = (Element) document + .getElementsByTagName("SegmentTimeline").item(0); + assertNotNull(segmentTimelineElement, "The SegmentTimeline element doesn't exist"); + assertTrue(segmentTimelineElement.getParentNode().isEqualNode( + document.getElementsByTagName("SegmentTemplate").item(0)), + "The SegmentTemplate element doesn't contain a SegmentTimeline element"); + testSElements(segmentTimelineElement); + } + + private void testSElements(@Nonnull final Element segmentTimelineElement) { + final NodeList segmentTimelineElementChildren = segmentTimelineElement.getChildNodes(); + final int segmentTimelineElementChildrenLength = segmentTimelineElementChildren + .getLength(); + assertNotEquals(0, segmentTimelineElementChildrenLength, + "The DASH manifest doesn't have a segment element (S) in the SegmentTimeLine element"); + + for (int i = 0; i < segmentTimelineElementChildrenLength; i++) { + final Element sElement = (Element) segmentTimelineElement.getElementsByTagName("S") + .item(i); + + final String dValue = sElement.getAttribute("d"); + assertFalse(isBlank(dValue), + "The value of the duration of this segment (the d attribute of this S element) is empty or the corresponding attribute doesn't exist"); + + final int d; + try { + d = Integer.parseInt(dValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the d attribute is not an integer", e); + } + assertTrue(d > 0, "The value of the d attribute is less than or equal to 0"); + + final String rValue = sElement.getAttribute("r"); + // A segment duration can or can't be repeated, so test the next segment if there + // is no r attribute + if (!isBlank(rValue)) { + final int r; + try { + r = Integer.parseInt(dValue); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the r attribute is not an integer", + e); + } + assertTrue(r > 0, "The value of the r attribute is less than or equal to 0"); + } + } + } + + private void testBaseUrlElement(@Nonnull final Document document) { + final Element baseURLElement = (Element) document + .getElementsByTagName("BaseURL").item(0); + assertNotNull(baseURLElement, "The BaseURL element doesn't exist"); + assertTrue(baseURLElement.getParentNode().isEqualNode( + document.getElementsByTagName("Representation").item(0)), + "The Representation element doesn't contain a BaseURL element"); + + final String baseURLElementContentValue = baseURLElement + .getTextContent(); + assertFalse(isBlank(baseURLElementContentValue), + "The content of the BaseURL element is empty or the corresponding element has no content"); + + try { + new URL(baseURLElementContentValue); + } catch (final MalformedURLException e) { + throw new AssertionError("The content of the BaseURL element is not an URL", e); + } + } + + private void testSegmentBaseElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element segmentBaseElement = (Element) document + .getElementsByTagName("SegmentBase").item(0); + assertNotNull(segmentBaseElement, "The SegmentBase element doesn't exist"); + assertTrue(segmentBaseElement.getParentNode().isEqualNode( + document.getElementsByTagName("Representation").item(0)), + "The Representation element doesn't contain a SegmentBase element"); + + final String indexRangeValue = segmentBaseElement + .getAttribute("indexRange"); + assertFalse(isBlank(indexRangeValue), + "The value of the indexRange attribute is empty or the corresponding attribute doesn't exist"); + final String[] indexRangeParts = indexRangeValue.split("-"); + assertEquals(2, indexRangeParts.length, + "The value of the indexRange attribute is not valid"); + + final int dashManifestIndexStart; + try { + dashManifestIndexStart = Integer.parseInt(indexRangeParts[0]); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the indexRange attribute is not valid", e); + } + + final int itagItemIndexStart = itagItem.getIndexStart(); + assertTrue(itagItemIndexStart > 0, + "The indexStart of the ItagItem is less than or equal to 0"); + assertEquals(dashManifestIndexStart, itagItemIndexStart, + "The indexStart value of the indexRange attribute of the DASH manifest (" + + dashManifestIndexStart + + ") is not equal to the indexStart of the ItagItem object (" + + itagItemIndexStart + ")"); + + final int dashManifestIndexEnd; + try { + dashManifestIndexEnd = Integer.parseInt(indexRangeParts[1]); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the indexRange attribute is not valid", e); + } + + final int itagItemIndexEnd = itagItem.getIndexEnd(); + assertTrue(itagItemIndexEnd > 0, + "The indexEnd of the ItagItem is less than or equal to 0"); + + assertEquals(dashManifestIndexEnd, itagItemIndexEnd, + "The indexEnd value of the indexRange attribute of the DASH manifest (" + + dashManifestIndexEnd + + ") is not equal to the indexEnd of the ItagItem object (" + + itagItemIndexEnd + ")"); + } + + private void testInitializationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element initializationElement = (Element) document + .getElementsByTagName("Initialization").item(0); + assertNotNull(initializationElement, "The Initialization element doesn't exist"); + assertTrue(initializationElement.getParentNode().isEqualNode( + document.getElementsByTagName("SegmentBase").item(0)), + "The SegmentBase element doesn't contain an Initialization element"); + + final String rangeValue = initializationElement + .getAttribute("range"); + assertFalse(isBlank(rangeValue), + "The value of the range attribute is empty or the corresponding attribute doesn't exist"); + final String[] rangeParts = rangeValue.split("-"); + assertEquals(2, rangeParts.length, "The value of the range attribute is not valid"); + + final int dashManifestInitStart; + try { + dashManifestInitStart = Integer.parseInt(rangeParts[0]); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the range attribute is not valid", e); + } + + final int itagItemInitStart = itagItem.getInitStart(); + assertTrue(itagItemInitStart >= 0, "The initStart of the ItagItem is less than 0"); + assertEquals(dashManifestInitStart, itagItemInitStart, + "The initStart value of the range attribute of the DASH manifest (" + + dashManifestInitStart + + ") is not equal to the initStart of the ItagItem object (" + + itagItemInitStart + ")"); + + final int dashManifestInitEnd; + try { + dashManifestInitEnd = Integer.parseInt(rangeParts[1]); + } catch (final NumberFormatException e) { + throw new AssertionError("The value of the indexRange attribute is not valid", e); + } + + final int itagItemInitEnd = itagItem.getInitEnd(); + assertTrue(itagItemInitEnd > 0, "The indexEnd of the ItagItem is less than or equal to 0"); + assertEquals(dashManifestInitEnd, itagItemInitEnd, + "The initEnd value of the range attribute of the DASH manifest (" + + dashManifestInitEnd + + ") is not equal to the initEnd of the ItagItem object (" + + itagItemInitEnd + ")"); + } + } +} diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/UtilsTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/UtilsTest.java index 6e6b2a8e0..3d835dce2 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/UtilsTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/UtilsTest.java @@ -3,13 +3,14 @@ package org.schabi.newpipe.extractor.utils; import org.junit.jupiter.api.Test; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import javax.annotation.Nonnull; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; -public class UtilsTest { +class UtilsTest { @Test - public void testMixedNumberWordToLong() throws ParsingException { + void testMixedNumberWordToLong() throws ParsingException { assertEquals(10, Utils.mixedNumberWordToLong("10")); assertEquals(10.5e3, Utils.mixedNumberWordToLong("10.5K"), 0.0); assertEquals(10.5e6, Utils.mixedNumberWordToLong("10.5M"), 0.0); @@ -18,13 +19,13 @@ public class UtilsTest { } @Test - public void testJoin() { + void testJoin() { assertEquals("some,random,stuff", Utils.join(",", Arrays.asList("some", "random", "stuff"))); assertEquals("some,random,not-null,stuff", Utils.nonEmptyAndNullJoin(",", new String[]{"some", "null", "random", "", "not-null", null, "stuff"})); } @Test - public void testGetBaseUrl() throws ParsingException { + void testGetBaseUrl() throws ParsingException { assertEquals("https://www.youtube.com", Utils.getBaseUrl("https://www.youtube.com/watch?v=Hu80uDzh8RY")); assertEquals("vnd.youtube", Utils.getBaseUrl("vnd.youtube://www.youtube.com/watch?v=jZViOEv90dI")); assertEquals("vnd.youtube", Utils.getBaseUrl("vnd.youtube:jZViOEv90dI")); @@ -33,7 +34,7 @@ public class UtilsTest { } @Test - public void testFollowGoogleRedirect() { + void testFollowGoogleRedirect() { assertEquals("https://www.youtube.com/watch?v=Hu80uDzh8RY", Utils.followGoogleRedirectIfNeeded("https://www.google.it/url?sa=t&rct=j&q=&esrc=s&cd=&cad=rja&uact=8&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHu80uDzh8RY&source=video")); assertEquals("https://www.youtube.com/watch?v=0b6cFWG45kA", @@ -46,4 +47,52 @@ public class UtilsTest { assertEquals("https://www.youtube.com/watch?v=Hu80uDzh8RY&url=hello", Utils.followGoogleRedirectIfNeeded("https://www.youtube.com/watch?v=Hu80uDzh8RY&url=hello")); } + + @Test + void dashManifestCreatorCacheTest() { + final ManifestCreatorCache- * 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* 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- * 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- * 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
* {@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
* This method is only used when generating DASH manifests of audio streams.
*
* It will produce the following element:
*
* The {@code
* This method is only used when generating DASH manifests from progressive streams.
*
* The {@code
* This method is only used when generating DASH manifests from progressive streams.
*
* It generates the following element:
*
* The {@code
* This method is only used when generating DASH manifests from progressive streams.
*
* It generates the following element:
*
* The {@code
* This method is only used when generating DASH manifests from OTF and post-live-DVR streams.
*
* It will produce a {@code
* {@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
* The {@code
+ * It stores, per stream:
+ *
+ * The {@code StreamBuilderHelper} will set the following attributes in the
+ * {@link AudioStream}s built:
+ *
+ * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
+ *
+ * The {@code StreamBuilderHelper} will set the following attributes in the
+ * {@link VideoStream}s built:
+ *
+ * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
+ *
- * 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.
*
- * It must be non null and should be non empty.
+ * It must not be null, and should be non empty.
*
* 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 {
*
- * 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.
*
* 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.
*
- * 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.
*
* 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.
*
- * 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.
*
- * 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).
*
* 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).
*
* 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.
*
- * 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.
*
- * It must be not null.
+ * It must not be null.
*
@@ -107,7 +107,7 @@ public final class SubtitlesStream extends Stream {
*
- * 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.
*
* 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.
*
- * 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.
*
- * It must be non null and should be non empty.
+ * It must not be null, and should be non empty.
*
* 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 {
*
- * 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;
From f61e2092a109135aec0e7120ee64381b2d2e5b4c Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Mon, 28 Mar 2022 19:50:03 +0200
Subject: [PATCH 12/38] [YouTube] Return a copy of the hardcoded ItagItem
instead of returning the reference to the hardcoded one in ItagItem.getItag
To do so, a copy constructor has been added in the class.
This fixes, for instance, an issue in NewPipe, in which the ItagItem values where not the ones corresponsing to a stream but to another, when generating DASH manifests.
---
.../extractor/services/youtube/ItagItem.java | 30 ++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
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 9608de8ec..fa6e97326 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
@@ -106,7 +106,7 @@ public class ItagItem implements Serializable {
public static ItagItem getItag(final int itagId) throws ParsingException {
for (final ItagItem item : ITAG_LIST) {
if (itagId == item.id) {
- return item;
+ return new ItagItem(item);
}
}
throw new ParsingException("itag " + itagId + " is not supported");
@@ -173,6 +173,34 @@ public class ItagItem implements Serializable {
this.avgBitrate = avgBitrate;
}
+ /**
+ * Copy constructor of the {@link ItagItem} class.
+ *
+ * @param itagItem the {@link ItagItem} to copy its properties into a new {@link ItagItem}
+ */
+ public ItagItem(@Nonnull final ItagItem itagItem) {
+ this.mediaFormat = itagItem.mediaFormat;
+ this.id = itagItem.id;
+ this.itagType = itagItem.itagType;
+ this.avgBitrate = itagItem.avgBitrate;
+ this.sampleRate = itagItem.sampleRate;
+ this.audioChannels = itagItem.audioChannels;
+ this.resolutionString = itagItem.resolutionString;
+ this.fps = itagItem.fps;
+ this.bitrate = itagItem.bitrate;
+ this.width = itagItem.width;
+ this.height = itagItem.height;
+ this.initStart = itagItem.initStart;
+ this.initEnd = itagItem.initEnd;
+ this.indexStart = itagItem.indexStart;
+ this.indexEnd = itagItem.indexEnd;
+ this.quality = itagItem.quality;
+ this.codec = itagItem.codec;
+ this.targetDurationSec = itagItem.targetDurationSec;
+ this.approxDurationMs = itagItem.approxDurationMs;
+ this.contentLength = itagItem.contentLength;
+ }
+
public MediaFormat getMediaFormat() {
return mediaFormat;
}
From 2fb1a412a6453db62b5824ff6562f237cf1193d0 Mon Sep 17 00:00:00 2001
From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
Date: Sun, 3 Apr 2022 18:42:01 +0200
Subject: [PATCH 13/38] Fix Checkstyle issues, revert resolution string changes
for YouTube video streams and don't return the rn parameter in DASH manifests
This parameter is still used to get the initialization sequence of OTF and POST-live streams, but is not returned anymore in the manifests.
It has been removed in order to avoid fingerprinting based on the number sent (e.g. when starting to play a stream close to the end and using 123 as the request number where it should be 1) and should be added dynamically by clients in their requests.
The relevant test has been also updated.
Checkstyle issues in YoutubeDashManifestCreator have been fixed, and the changes in the resolution string returned for video streams in YoutubeStreamExtractor have been reverted, as they create issues on NewPipe right now.
---
.../youtube/YoutubeDashManifestCreator.java | 54 ++++++++++++-------
.../extractors/YoutubeStreamExtractor.java | 18 ++-----
.../extractor/stream/DeliveryMethod.java | 4 +-
.../YoutubeDashManifestCreatorTest.java | 15 ++++--
4 files changed, 49 insertions(+), 42 deletions(-)
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 2d7f7fe22..a61b86951 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
@@ -23,10 +23,22 @@ import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
-import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*;
-import static org.schabi.newpipe.extractor.utils.Utils.*;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getYoutubeAndroidAppUserAgent;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
+import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
+import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
+import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/**
* Class to generate DASH manifests from YouTube OTF, progressive and ended/post-live-DVR streams.
@@ -202,11 +214,11 @@ public final class YoutubeDashManifestCreator {
* In order to generate the DASH manifest, this method will:
* In order to generate the DASH manifest, this method will:
*
- * This method fetches:
+ * This method fetches, for OTF streams and for post-live-DVR streams:
*
* It generates 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)
*
@@ -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)
*
@@ -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)
*
@@ -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:
*
}
*
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
*
@@ -331,8 +343,8 @@ public final class YoutubeDashManifestCreator {
*
*
- *
@@ -545,6 +556,7 @@ public final class YoutubeDashManifestCreator {
* @throws YoutubeDashManifestCreationException if something goes wrong when fetching the
* "initialization" response and/or its redirects
*/
+ @SuppressWarnings("checkstyle:FinalParameters")
@Nonnull
private static Response getInitializationResponse(@Nonnull String baseStreamingUrl,
@Nonnull final ItagItem itagItem,
@@ -604,11 +616,12 @@ public final class YoutubeDashManifestCreator {
/**
* Append {@link #SQ_0} for post-live-DVR and OTF streams and {@link #RN_0} to all streams.
*
- * @param baseStreamingUrl the base streaming URL to which the param(s) are being appended
+ * @param baseStreamingUrl the base streaming URL to which the parameter(s) are being appended
* @param deliveryType the {@link DeliveryType} of the stream
* @return the base streaming URL to which the param(s) are appended, depending on the
* {@link DeliveryType} of the stream
*/
+ @SuppressWarnings({"checkstyle:FinalParameters", "checkstyle:FinalLocalVariable"})
@Nonnull
private static String appendRnParamAndSqParamIfNeeded(
@Nonnull String baseStreamingUrl,
@@ -644,6 +657,7 @@ public final class YoutubeDashManifestCreator {
* @throws YoutubeDashManifestCreationException if something goes wrong when trying to get the
* response without any redirection
*/
+ @SuppressWarnings("checkstyle:FinalParameters")
@Nonnull
private static Response getStreamingWebUrlWithoutRedirects(
@Nonnull final Downloader downloader,
@@ -1312,7 +1326,7 @@ public final class YoutubeDashManifestCreator {
*
- * {@code
- * It doesn't make sense to use this enum constant outside of the extractor as it will never be - * returned by an {@link org.schabi.newpipe.extractor.Extractor extractor} and is only used - * internally. - *
+ * Placeholder to check if the stream type was checked or not. It doesn't make sense to use this + * enum constant outside of the extractor as it will never be returned by an {@link + * org.schabi.newpipe.extractor.Extractor} and is only used internally. */ NONE, /** - * Enum constant to indicate that the stream type of stream content is a live video. - * - *- * 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). - *
+ * A normal video stream, usually with audio. Note that the {@link StreamInfo} can also + * provide audio-only {@link AudioStream}s in addition to video or video-only {@link + * VideoStream}s. */ VIDEO_STREAM, /** - * Enum constant to indicate that the stream type of stream content is an audio. - * - *- * Note that contents returned as audio streams should not return video streams. - *
- * - *- * So, in order to prevent unexpected behaviors, stream extractors which are returning this - * stream type for a content should ensure that no video stream is returned for this content. - *
+ * An audio-only stream. There should be no {@link VideoStream}s available! In order to prevent + * unexpected behaviors, when {@link StreamExtractor}s return this stream type, they should + * ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} and + * {@link StreamExtractor#getVideoOnlyStreams()}. */ AUDIO_STREAM, /** - * Enum constant to indicate that the stream type of stream content is a video. - * - *- * Note that contents can contain audio live streams even if they also contain - * live video streams (so video-only or video with audio, depending on the stream/the content/ - * the service). - *
+ * A video live stream, usually with audio. Note that the {@link StreamInfo} can also + * provide audio-only {@link AudioStream}s in addition to video or video-only {@link + * VideoStream}s. */ LIVE_STREAM, /** - * 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. - *
- * - *- * 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. - *
+ * An audio-only live stream. There should be no {@link VideoStream}s available! In order to + * prevent unexpected behaviors, when {@link StreamExtractor}s return this stream type, they + * should ensure that no video stream is returned in {@link StreamExtractor#getVideoStreams()} + * and {@link StreamExtractor#getVideoOnlyStreams()}. */ AUDIO_LIVE_STREAM, /** - * Enum constant to indicate that the stream type of stream content is a video content of an - * ended live video stream. + * A video live stream that has just ended but has not yet been encoded into a normal video + * stream. Note that the {@link StreamInfo} can also provide audio-only {@link + * AudioStream}s in addition to video or video-only {@link VideoStream}s. * ** Note that most of the content of an ended live video (or audio) may be extracted as {@link @@ -76,39 +54,21 @@ public enum StreamType { * later, because the service may encode them again later as normal video/audio streams. That's * the case on YouTube, for example. *
- * - *- * Note that contents can contain post-live audio streams even if they also - * contain post-live video streams (video-only or video with audio, depending of the stream/the - * content/the service). - *
*/ POST_LIVE_STREAM, /** - * Enum constant to indicate that the stream type of stream content is an audio content of an - * ended live audio stream. + * An audio live stream that has just ended but has not yet been encoded into a normal audio + * stream. There should be no {@link VideoStream}s available! In order to prevent unexpected + * behaviors, when {@link StreamExtractor}s return this stream type, they should ensure that no + * video stream is returned in {@link StreamExtractor#getVideoStreams()} and + * {@link StreamExtractor#getVideoOnlyStreams()}. * ** Note that most of ended live audio streams extracted with this value are processed as * {@link #AUDIO_STREAM regular audio streams} later, because the service may encode them * again later. *
- * - *- * Contents returned as post-live audio streams should not return post-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 post-live video stream is returned for this - * content. - *
*/ - POST_LIVE_AUDIO_STREAM, - - /** - * Enum constant to indicate that the stream type of stream content is a file. - */ - FILE + POST_LIVE_AUDIO_STREAM } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java index e2084f1d4..ac12f83f9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java @@ -141,17 +141,13 @@ public final class ManifestCreatorCache- * 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) { - super(message, e); - } - } - - /** - * 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- * 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.brendanlong.com's page about the structure of an MPEG-DASH MPD manifest - */ - @Nonnull - public static Result getStreams(final String dashMpdUrl) - throws DashMpdParsingException, ReCaptchaException { - final String dashDoc; - final Downloader downloader = NewPipe.getDownloader(); - try { - dashDoc = downloader.get(dashMpdUrl).responseBody(); - } catch (final IOException e) { - throw new DashMpdParsingException("Could not fetch DASH manifest: " + dashMpdUrl, e); - } - - try { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - final DocumentBuilder builder = factory.newDocumentBuilder(); - final InputStream stream = new ByteArrayInputStream(dashDoc.getBytes()); - - final Document doc = builder.parse(stream); - final NodeList representationList = doc.getElementsByTagName("Representation"); - - final List* It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages. - *
*/ public final class YoutubeDashManifestCreator { @@ -305,7 +305,7 @@ public final class YoutubeDashManifestCreator { SEGMENTS_DURATION.clear(); DURATION_REPETITIONS.clear(); - return buildResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); + return buildAndCacheResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); } /** @@ -441,7 +441,7 @@ public final class YoutubeDashManifestCreator { generateSegmentTimelineElement(document); generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); - return buildResult(postLiveStreamDvrStreamingUrl, document, + return buildAndCacheResult(postLiveStreamDvrStreamingUrl, document, GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS); } @@ -533,7 +533,7 @@ public final class YoutubeDashManifestCreator { generateSegmentBaseElement(document, itagItem); generateInitializationElement(document, itagItem); - return buildResult(progressiveStreamingBaseUrl, document, + return buildAndCacheResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); } @@ -841,13 +841,8 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { - final DocumentBuilderFactory documentBuilderFactory; - final DocumentBuilder documentBuilder; - final Document document; try { - documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilder = documentBuilderFactory.newDocumentBuilder(); - document = documentBuilder.newDocument(); + final Document document = newDocument(); final Element mpdElement = document.createElement("MPD"); document.appendChild(mpdElement); @@ -903,13 +898,13 @@ public final class YoutubeDashManifestCreator { final String durationSeconds = String.format(Locale.ENGLISH, "%.3f", duration); mediaPresentationDurationAttribute.setValue("PT" + durationSeconds + "S"); mpdElement.setAttributeNode(mediaPresentationDurationAttribute); + + return document; } catch (final Exception e) { throw new YoutubeDashManifestCreationException( "Could not generate or append the MPD element of the DASH manifest to the " + "document", e); } - - return document; } /** @@ -1591,7 +1586,7 @@ public final class YoutubeDashManifestCreator { } /** - * Convert a DASH manifest {@link Document document} to a string. + * Convert a DASH manifest {@link Document document} to a string and cache it. * * @param originalBaseStreamingUrl the original base URL of the stream * @param document the document to be converted @@ -1604,23 +1599,16 @@ public final class YoutubeDashManifestCreator { * @throws YoutubeDashManifestCreationException if something goes wrong when converting the * {@link Document document} */ - private static String buildResult( + private static String buildAndCacheResult( @Nonnull final String originalBaseStreamingUrl, @Nonnull final Document document, @Nonnull final ManifestCreatorCache* This list is automatically cleared in the execution of - * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * {@link #fromOtfStreamingUrl(String, ItagItem, long)}, before the DASH * manifest is converted to a string. *
*/ @@ -90,7 +90,7 @@ public final class YoutubeDashManifestCreator { * ** This list is automatically cleared in the execution of - * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * {@link #fromOtfStreamingUrl(String, ItagItem, long)}, before the DASH * manifest is converted to a string. *
*/ @@ -242,7 +242,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromOtfStreamingUrl( + public static String fromOtfStreamingUrl( @Nonnull final String otfBaseStreamingUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { @@ -376,7 +376,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromPostLiveStreamDvrStreamingUrl( + public static String fromPostLiveStreamDvrStreamingUrl( @Nonnull final String postLiveStreamDvrStreamingUrl, @Nonnull final ItagItem itagItem, final int targetDurationSec, @@ -505,7 +505,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromProgressiveStreamingUrl( + public static String fromProgressiveStreamingUrl( @Nonnull final String progressiveStreamingBaseUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java b/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java index bdbd59530..124d998d0 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java @@ -15,6 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.schabi.newpipe.extractor.utils.Utils; + public class ExtractorAsserts { public static void assertEmptyErrors(String message, List
* We cannot test the generation of DASH manifests for ended livestreams because these videos will
@@ -58,570 +63,280 @@ import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
*/
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 {
- private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM";
- private static YoutubeStreamExtractor extractor;
-
- @BeforeAll
- public static void setUp() throws Exception {
- YoutubeParsingHelper.resetClientVersionAndKey();
- YoutubeParsingHelper.setNumberGenerator(new Random(1));
- NewPipe.init(DownloaderTestImpl.getInstance());
- extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url);
- extractor.fetchPage();
- }
-
- @Test
- void testOtfStreamsANewEraOfOpen() throws Exception {
- testStreams(DeliveryMethod.DASH,
- extractor.getVideoOnlyStreams());
- testStreams(DeliveryMethod.DASH,
- extractor.getAudioStreams());
- // This should not happen because there are no video stream with audio which use the
- // DASH delivery method (YouTube OTF stream type)
- try {
- testStreams(DeliveryMethod.DASH,
- extractor.getVideoStreams());
- } catch (final Exception e) {
- assertEquals(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class,
- e.getClass(), "The exception thrown was not the one excepted: "
- + e.getClass().getName()
- + "was thrown instead of YoutubeDashManifestCreationException");
- }
- }
-
- @Test
- void testProgressiveStreamsANewEraOfOpen() throws Exception {
- testStreams(DeliveryMethod.PROGRESSIVE_HTTP,
- extractor.getVideoOnlyStreams());
- testStreams(DeliveryMethod.PROGRESSIVE_HTTP,
- extractor.getAudioStreams());
- // This exception should be always thrown, as we are not able to generate DASH
- // manifests of video formats with audio
- final List
- * When the cache limit size is reached, oldest manifests will be removed. - *
- * - *- * If the new cache size set is less than the number of current cached manifests, oldest - * manifests will be also removed. - *
- * - *- * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be - * thrown. - *
- * - * @param otfStreamsCacheLimit the maximum number of OTF streams in the corresponding cache. - */ - public static void setOtfStreamsMaximumSize(final int otfStreamsCacheLimit) { - GENERATED_OTF_MANIFESTS.setMaximumSize(otfStreamsCacheLimit); - } - - /** - * Set the limit of cached post-live-DVR streams. - * - *- * When the cache limit size is reached, oldest manifests will be removed. - *
- * - *- * If the new cache size set is less than the number of current cached manifests, oldest - * manifests will be also removed. - *
- * - *- * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be - * thrown. - *
- * - * @param postLiveDvrStreamsCacheLimit the maximum number of post-live-DVR streams in the - * corresponding cache. - */ - public static void setPostLiveDvrStreamsMaximumSize(final int postLiveDvrStreamsCacheLimit) { - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(postLiveDvrStreamsCacheLimit); - } - - /** - * Set the limit of cached progressive streams, if needed. - * - *- * When the cache limit size is reached, oldest manifests will be removed. - *
- * - *- * If the new cache size set is less than the number of current cached manifests, oldest - * manifests will be also removed. - *
- * - *- * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be - * thrown. - *
- * - * @param progressiveCacheLimit the maximum number of progressive streams in the corresponding - * cache. - */ - public static void setProgressiveStreamsMaximumSize(final int progressiveCacheLimit) { - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(progressiveCacheLimit); - } - - /** - * Set the limit of cached OTF manifests, cached post-live-DVR manifests and cached progressive - * manifests. - * - *- * When the caches limit size are reached, oldest manifests will be removed from their - * respective cache. - *
- * - *- * For each cache, if its new size set is less than the number of current cached manifests, - * oldest manifests will be also removed. - *
- * - *- * Note that the limit must be more than 0 or an {@link IllegalArgumentException} will be - * thrown. - *
- * - * @param cachesLimit the maximum size of OTF, post-live-DVR and progressive caches - */ - public static void setManifestsCachesMaximumSize(final int cachesLimit) { - GENERATED_OTF_MANIFESTS.setMaximumSize(cachesLimit); - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setMaximumSize(cachesLimit); - } - - /** - * Clear cached OTF manifests. - * - *- * The limit of this cache size set, if there is one, will be not unset. - *
- */ - public static void clearOtfCachedManifests() { - GENERATED_OTF_MANIFESTS.clear(); - } - - /** - * Clear cached post-live-DVR streams manifests. - * - *- * The limit of this cache size set, if there is one, will be not unset. - *
- */ - public static void clearPostLiveDvrStreamsCachedManifests() { - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); - } - - /** - * Clear cached progressive streams manifests. - * - *- * The limit of this cache size set, if there is one, will be not unset. - *
- */ - public static void clearProgressiveCachedManifests() { - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); - } - - /** - * Clear cached OTF manifests, cached post-live-DVR streams manifests and cached progressive - * manifests in their respective caches. - * - *- * The limit of the caches size set, if any, will be not unset. - *
- */ - public static void clearManifestsInCaches() { - GENERATED_OTF_MANIFESTS.clear(); - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.clear(); - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.clear(); - } - - /** - * Reset OTF manifests cache. - * - *- * All cached manifests will be removed and the clear factor and the maximum size will be set - * to their default values. - *
- */ - public static void resetOtfManifestsCache() { - GENERATED_OTF_MANIFESTS.reset(); - } - - /** - * Reset post-live-DVR manifests cache. - * - *- * All cached manifests will be removed and the clear factor and the maximum size will be set - * to their default values. - *
- */ - public static void resetPostLiveDvrManifestsCache() { - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); - } - - /** - * Reset progressive manifests cache. - * - *- * All cached manifests will be removed and the clear factor and the maximum size will be set - * to their default values. - *
- */ - public static void resetProgressiveManifestsCache() { - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); - } - - /** - * Reset OTF, post-live-DVR and progressive manifests caches. - * - *- * For each cache, all cached manifests will be removed and the clear factor and the maximum - * size will be set to their default values. - *
- */ - public static void resetCaches() { - GENERATED_OTF_MANIFESTS.reset(); - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.reset(); - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.reset(); + public static ManifestCreatorCache- * This list is automatically cleared in the execution of - * {@link #fromOtfStreamingUrl(String, ItagItem, long)}, before the DASH - * manifest is converted to a string. - *
- */ - private static final List- * This list is automatically cleared in the execution of - * {@link #fromOtfStreamingUrl(String, ItagItem, long)}, before the DASH - * manifest is converted to a string. - *
- */ - private static final List* By parsing by the first media sequence, we know how many durations and repetitions there are - * so we just have to loop into {@link #SEGMENTS_DURATION} and {@link #DURATION_REPETITIONS} - * to generate the following element for each duration: + * so we just have to loop into segment durations to generate the following elements for each: *
* *@@ -1451,36 +1395,43 @@ public final class YoutubeDashManifestCreator { * {@link #generateSegmentTimelineElement(Document)}. *
* + * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the + * regexes * @param document the {@link Document} on which the the {@code- * 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. - *
- * - *- * The default value is {@code null}. - *
- * - * @param baseUrl the base URL of the {@link AudioStream}, which can be null + * @param manifestUrl the URL of the manifest this stream comes from or {@code null} * @return this {@link Builder} instance */ - public Builder setBaseUrl(@Nullable final String baseUrl) { - this.baseUrl = baseUrl; + public Builder setManifestUrl(@Nullable final String manifestUrl) { + this.manifestUrl = manifestUrl; return this; } @@ -236,7 +227,7 @@ public final class AudioStream extends Stream { } return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate, - baseUrl, itagItem); + manifestUrl, itagItem); } } @@ -255,8 +246,8 @@ public final class AudioStream extends Stream { * @param averageBitrate the average bitrate of the stream (which can be unknown, see * {@link #UNKNOWN_BITRATE}) * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null - * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more - * information) + * @param manifestUrl the URL of the manifest this stream comes from (if applicable, + * otherwise null) */ @SuppressWarnings("checkstyle:ParameterNumber") private AudioStream(@Nonnull final String id, @@ -265,9 +256,9 @@ public final class AudioStream extends Stream { @Nullable final MediaFormat format, @Nonnull final DeliveryMethod deliveryMethod, final int averageBitrate, - @Nullable final String baseUrl, + @Nullable final String manifestUrl, @Nullable final ItagItem itagItem) { - super(id, content, isUrl, format, deliveryMethod, baseUrl); + super(id, content, isUrl, format, deliveryMethod, manifestUrl); if (itagItem != null) { this.itagItem = itagItem; this.itag = itagItem.id; 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 02ca3cf16..44ce2772e 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 @@ -33,7 +33,7 @@ public abstract class Stream implements Serializable { private final String content; private final boolean isUrl; private final DeliveryMethod deliveryMethod; - @Nullable private final String baseUrl; + @Nullable private final String manifestUrl; /** * Instantiates a new {@code Stream} object. @@ -45,21 +45,21 @@ public abstract class Stream implements Serializable { * manifest * @param format the {@link MediaFormat}, which can be null * @param deliveryMethod the delivery method of the stream - * @param baseUrl the base URL of the content if the stream is a DASH or an HLS - * manifest, which can be null + * @param manifestUrl the URL of the manifest this stream comes from (if applicable, + * otherwise null) */ public Stream(final String id, final String content, final boolean isUrl, @Nullable final MediaFormat format, final DeliveryMethod deliveryMethod, - @Nullable final String baseUrl) { + @Nullable final String manifestUrl) { this.id = id; this.content = content; this.isUrl = isUrl; this.mediaFormat = format; this.deliveryMethod = deliveryMethod; - this.baseUrl = baseUrl; + this.manifestUrl = manifestUrl; } /** @@ -184,7 +184,7 @@ public abstract class Stream implements Serializable { } /** - * Gets the delivery method. + * Gets the {@link DeliveryMethod}. * * @return the delivery method */ @@ -194,18 +194,13 @@ public abstract class Stream implements Serializable { } /** - * Gets the base URL of a stream. + * Gets the URL of the manifest this stream comes from (if applicable, otherwise null). * - *- * If the stream is not a DASH stream or an HLS stream, this value will always be null. - * It may also be null for these streams too. - *
- * - * @return the base URL of the stream or {@code null} + * @return the URL of the manifest this stream comes from or {@code null} */ @Nullable - public String getBaseUrl() { - return baseUrl; + public String getManifestUrl() { + return manifestUrl; } /** @@ -235,11 +230,11 @@ public abstract class Stream implements Serializable { && deliveryMethod == stream.deliveryMethod && content.equals(stream.content) && isUrl == stream.isUrl - && Objects.equals(baseUrl, stream.baseUrl); + && Objects.equals(manifestUrl, stream.manifestUrl); } @Override public int hashCode() { - return Objects.hash(id, mediaFormat, deliveryMethod, content, isUrl, baseUrl); + return Objects.hash(id, mediaFormat, deliveryMethod, content, isUrl, manifestUrl); } } 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 ddd372343..b40d0e928 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 @@ -29,7 +29,7 @@ public final class SubtitlesStream extends Stream { @Nullable private MediaFormat mediaFormat; @Nullable - private String baseUrl; + private String manifestUrl; private String languageCode; // Use of the Boolean class instead of the primitive type needed for setter call check private Boolean autoGenerated; @@ -116,22 +116,13 @@ public final class SubtitlesStream extends Stream { } /** - * Set the base URL of the {@link SubtitlesStream}. + * Sets the URL of the manifest this stream comes from (if applicable, otherwise null). * - *- * 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. - *
- * - *- * The default value is {@code null}. - *
- * - * @param baseUrl the base URL of the {@link SubtitlesStream}, which can be null + * @param manifestUrl the URL of the manifest this stream comes from or {@code null} * @return this {@link Builder} instance */ - public Builder setBaseUrl(@Nullable final String baseUrl) { - this.baseUrl = baseUrl; + public Builder setManifestUrl(@Nullable final String manifestUrl) { + this.manifestUrl = manifestUrl; return this; } @@ -212,25 +203,25 @@ public final class SubtitlesStream extends Stream { } return new SubtitlesStream(id, content, isUrl, mediaFormat, deliveryMethod, - languageCode, autoGenerated, baseUrl); + languageCode, autoGenerated, manifestUrl); } } /** * Create a new subtitles stream. * - * @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 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) + * @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 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 manifestUrl the URL of the manifest this stream comes from (if applicable, + * otherwise null) */ @SuppressWarnings("checkstyle:ParameterNumber") private SubtitlesStream(@Nonnull final String id, @@ -240,8 +231,8 @@ public final class SubtitlesStream extends Stream { @Nonnull final DeliveryMethod deliveryMethod, @Nonnull final String languageCode, final boolean autoGenerated, - @Nullable final String baseUrl) { - super(id, content, isUrl, mediaFormat, deliveryMethod, baseUrl); + @Nullable final String manifestUrl) { + super(id, content, isUrl, mediaFormat, deliveryMethod, manifestUrl); /* * Locale.forLanguageTag only for Android API >= 21 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 e8b2595b9..0709af90e 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 @@ -64,7 +64,7 @@ public final class VideoStream extends Stream { @Nullable private MediaFormat mediaFormat; @Nullable - private String baseUrl; + private String manifestUrl; // Use of the Boolean class instead of the primitive type needed for setter call check private Boolean isVideoOnly; private String resolution; @@ -157,22 +157,13 @@ public final class VideoStream extends Stream { } /** - * Set the base URL of the {@link VideoStream}. + * Sets the URL of the manifest this stream comes from (if applicable, otherwise null). * - *- * 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. - *
- * - *- * The default value is {@code null}. - *
- * - * @param baseUrl the base URL of the {@link VideoStream}, which can be null + * @param manifestUrl the URL of the manifest this stream comes from or {@code null} * @return this {@link Builder} instance */ - public Builder setBaseUrl(@Nullable final String baseUrl) { - this.baseUrl = baseUrl; + public Builder setManifestUrl(@Nullable final String manifestUrl) { + this.manifestUrl = manifestUrl; return this; } @@ -282,7 +273,7 @@ public final class VideoStream extends Stream { } return new VideoStream(id, content, isUrl, mediaFormat, deliveryMethod, resolution, - isVideoOnly, baseUrl, itagItem); + isVideoOnly, manifestUrl, itagItem); } } @@ -300,8 +291,8 @@ public final class VideoStream extends Stream { * @param resolution the resolution of the stream * @param isVideoOnly whether the stream is video-only * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null - * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more - * information) + * @param manifestUrl the URL of the manifest this stream comes from (if applicable, + * otherwise null) */ @SuppressWarnings("checkstyle:ParameterNumber") private VideoStream(@Nonnull final String id, @@ -311,9 +302,9 @@ public final class VideoStream extends Stream { @Nonnull final DeliveryMethod deliveryMethod, @Nonnull final String resolution, final boolean isVideoOnly, - @Nullable final String baseUrl, + @Nullable final String manifestUrl, @Nullable final ItagItem itagItem) { - super(id, content, isUrl, format, deliveryMethod, baseUrl); + super(id, content, isUrl, format, deliveryMethod, manifestUrl); if (itagItem != null) { this.itagItem = itagItem; this.itag = itagItem.id; From 54d323c2ae7209e0a4f80e1ee90d9fdd076e3140 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Mon, 2 May 2022 22:18:46 +0200 Subject: [PATCH 27/38] Fix Checkstyle issue in YoutubeDashManifestCreator --- .../extractor/services/youtube/YoutubeDashManifestCreator.java | 2 ++ 1 file changed, 2 insertions(+) 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 5e71f7587..e52476a77 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 @@ -1094,6 +1094,7 @@ public final class YoutubeDashManifestCreator { } } + // CHECKSTYLE:OFF /** * Generate the {@code+ * It is different of {@link org.schabi.newpipe.extractor.stream.DeliveryMethod delivery methods}! + *
+ */ +public enum DeliveryType { + + /** + * YouTube's progressive delivery method, which works with HTTP range headers. + * (Note that official clients use the corresponding parameter instead.) + * + *+ * Initialization and index ranges are available to get metadata (the corresponding values + * are returned in the player response). + *
+ */ + PROGRESSIVE, + + /** + * YouTube's OTF delivery method which uses a sequence parameter to get segments of + * streams. + * + *+ * The first sequence (which can be fetched with the {@code &sq=0} parameter) contains all the + * 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. + * + *+ * Each sequence (which can be fetched with the {@code &sq=0} parameter) contains its own + * metadata (sidx boxes, segment length, ...), which make no need of an initialization + * segment. + *
+ * + *+ * Only used for livestreams (ended or running). + *
+ */ + LIVE +} 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 deleted file mode 100644 index e52476a77..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java +++ /dev/null @@ -1,1498 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube; - -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.addClientInfoHeaders; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; -import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.downloader.Downloader; -import org.schabi.newpipe.extractor.downloader.Response; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; -import org.schabi.newpipe.extractor.utils.Utils; -import org.w3c.dom.Attr; -import org.w3c.dom.DOMException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.io.IOException; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import javax.annotation.Nonnull; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -/** - * Class to generate DASH manifests from YouTube OTF, progressive and ended/post-live-DVR streams. - * It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages. - */ -public final class YoutubeDashManifestCreator { - - /** - * The redirect count limit that this class uses, which is the same limit as OkHttp. - */ - private static final int MAXIMUM_REDIRECT_COUNT = 20; - - /** - * Cache of DASH manifests generated for OTF streams. - */ - private static final ManifestCreatorCache- * Initialization and index ranges are available to get metadata (the corresponding values - * are returned in the player response). - *
- */ - PROGRESSIVE, - - /** - * YouTube's OTF delivery method which uses a sequence parameter to get segments of - * streams. - * - *- * The first sequence (which can be fetched with the {@link #SQ_0} param) contains all the - * 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. - * - *- * Each sequence (which can be fetched with the {@link #SQ_0} param) contains its own - * metadata (sidx boxes, segment length, ...), which make no need of an initialization - * segment. - *
- * - *- * Only used for livestreams (ended or running). - *
- */ - LIVE - } - - private YoutubeDashManifestCreator() { - } - - /** - * Exception that is thrown when the {@link YoutubeDashManifestCreator} encounters a problem - * while creating a manifest. - */ - public static final class CreationException extends Exception { - - CreationException(final String message) { - super(message); - } - - CreationException(final String message, final Exception e) { - super(message, e); - } - - public static CreationException couldNotAdd(final String element, final Exception e) { - return new CreationException("Could not add " + element + " element", e); - } - - public static CreationException couldNotAdd(final String element, final String reason) { - return new CreationException("Could not add " + element + " element: " + reason); - } - } - - /** - * Create DASH manifests from a YouTube OTF stream. - * - *- * OTF streams are YouTube-DASH specific streams which work with sequences and without the need - * to get a manifest (even if one is provided, it is not used by official clients). - *
- *- * They can be found only on videos; mostly those with a small amount of views, or ended - * livestreams which have just been re-encoded as normal videos. - *
- * - *This method needs: - *
In order to generate the DASH manifest, this method will: - *
- * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used - * as the stream duration. - *
- * - * @param otfBaseStreamingUrl the base URL of the OTF stream, which cannot be null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * cannot be null - * @param durationSecondsFallback the duration of the video, which will be used if the duration - * could not be extracted from the first sequence - * @return the manifest generated into a string - */ - @Nonnull - public static String fromOtfStreamingUrl( - @Nonnull final String otfBaseStreamingUrl, - @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws CreationException { - if (OTF_CACHE.containsKey(otfBaseStreamingUrl)) { - return Objects.requireNonNull(OTF_CACHE.get(otfBaseStreamingUrl)).getSecond(); - } - - 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(realOtfBaseStreamingUrl, - itagItem, DeliveryType.OTF); - 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 CreationException("Could not get the initialization URL of " - + "the OTF stream: response code " + responseCode); - } - - final String[] segmentDuration; - - try { - final String[] segmentsAndDurationsResponseSplit = response.responseBody() - // Get the lines with the durations and the following - .split("Segment-Durations-Ms: ")[1] - // Remove the other lines - .split("\n")[0] - // Get all durations and repetitions which are separated by a comma - .split(","); - final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; - if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { - segmentDuration = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); - } else { - segmentDuration = segmentsAndDurationsResponseSplit; - } - } catch (final Exception e) { - throw new CreationException("Could not get segment durations", e); - } - - final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF, - itagItem, durationSecondsFallback); - generatePeriodElement(document); - generateAdaptationSetElement(document, itagItem); - generateRoleElement(document); - generateRepresentationElement(document, itagItem); - if (itagItem.itagType == ItagItem.ItagType.AUDIO) { - generateAudioChannelConfigurationElement(document, itagItem); - } - generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF); - generateSegmentTimelineElement(document); - generateSegmentElementsForOtfStreams(segmentDuration, document); - - return buildAndCacheResult(otfBaseStreamingUrl, document, OTF_CACHE); - } - - /** - * Create DASH manifests from a YouTube post-live-DVR stream/ended livestream. - * - *- * Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which - * works with sequences and without the need to get a manifest (even if one is provided but not - * used by main clients (and is complete for big ended livestreams because it doesn't return - * the full stream)). - *
- * - *- * They can be found only on livestreams which have ended very recently (a few hours, most of - * the time) - *
- * - *This method needs: - *
In order to generate the DASH manifest, this method will: - *
- * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used - * as the stream duration. - *
- * - * @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended - * livestream, which cannot be null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * cannot be null - * @param targetDurationSec the target duration of each sequence, in seconds (this - * value is returned with the targetDurationSec field for - * each stream in YouTube player response) - * @param durationSecondsFallback the duration of the ended livestream which will be used - * if the duration could not be extracted from the first - * sequence - * @return the manifest generated into a string - */ - @Nonnull - public static String fromPostLiveStreamDvrStreamingUrl( - @Nonnull final String postLiveStreamDvrStreamingUrl, - @Nonnull final ItagItem itagItem, - final int targetDurationSec, - final long durationSecondsFallback) throws CreationException { - if (POST_LIVE_DVR_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) { - return Objects.requireNonNull(POST_LIVE_DVR_CACHE.get(postLiveStreamDvrStreamingUrl)) - .getSecond(); - } - String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; - final String streamDuration; - final String segmentCount; - - if (targetDurationSec <= 0) { - throw new CreationException("targetDurationSec value is <= 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(realPostLiveStreamDvrStreamingUrl, - itagItem, DeliveryType.LIVE); - 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 CreationException("Could not get the initialization " - + "segment of the post-live-DVR stream: response code " + responseCode); - } - - final Map- * Progressive streams are YouTube DASH streams which work with range requests and without the - * need to get a manifest. - *
- * - *- * They can be found on all videos, and for all streams for most of videos which come from a - * YouTube partner, and on videos with a large number of views. - *
- * - *This method needs: - *
In order to generate the DASH manifest, this method will: - *
- * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used - * as the stream duration. - *
- * - * @param progressiveStreamingBaseUrl the base URL of the progressive stream, which cannot be - * null - * @param itagItem the {@link ItagItem} corresponding to the stream, which - * cannot be null - * @param durationSecondsFallback the duration of the progressive stream which will be used - * if the duration could not be extracted from the first - * sequence - * @return the manifest generated into a string - */ - @Nonnull - public static String fromProgressiveStreamingUrl( - @Nonnull final String progressiveStreamingBaseUrl, - @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws CreationException { - if (PROGRESSIVE_CACHE.containsKey(progressiveStreamingBaseUrl)) { - return Objects.requireNonNull(PROGRESSIVE_CACHE.get(progressiveStreamingBaseUrl)) - .getSecond(); - } - - final Document document = generateDocumentAndMpdElement(new String[]{}, - DeliveryType.PROGRESSIVE, itagItem, durationSecondsFallback); - generatePeriodElement(document); - generateAdaptationSetElement(document, itagItem); - generateRoleElement(document); - generateRepresentationElement(document, itagItem); - if (itagItem.itagType == ItagItem.ItagType.AUDIO) { - generateAudioChannelConfigurationElement(document, itagItem); - } - generateBaseUrlElement(document, progressiveStreamingBaseUrl); - generateSegmentBaseElement(document, itagItem); - generateInitializationElement(document, itagItem); - - return buildAndCacheResult(progressiveStreamingBaseUrl, document, - PROGRESSIVE_CACHE); - } - - /** - * Get the "initialization" {@link Response response} of a stream. - * - *- * This method fetches, for OTF streams and for post-live-DVR streams: - *
- * This method will follow redirects for web clients, which works in the following way: - *
- * The duration of OTF streams is not returned into the player response and needs to be - * calculated by adding the duration of each segment. - *
- * - * @param segmentDuration the segment duration object extracted from the initialization - * sequence of the stream - * @return the duration of the OTF stream - */ - private static int getStreamDuration(@Nonnull final String[] segmentDuration) - throws CreationException { - try { - int streamLengthMs = 0; - for (final String segDuration : segmentDuration) { - final String[] segmentLengthRepeat = segDuration.split("\\(r="); - int segmentRepeatCount = 0; - // There are repetitions of a segment duration in other segments - if (segmentLengthRepeat.length > 1) { - segmentRepeatCount = Integer.parseInt(Utils.removeNonDigitCharacters( - segmentLengthRepeat[1])); - } - final int segmentLength = Integer.parseInt(segmentLengthRepeat[0]); - streamLengthMs += segmentLength + segmentRepeatCount * segmentLength; - } - return streamLengthMs; - } catch (final NumberFormatException e) { - throw new CreationException("Could not get stream length", e); - } - } - - /** - * Create a {@link Document} object and generate the {@code
- * The generated {@code
- * {@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. - *
- * - * @param segmentDuration the segment duration object extracted from the initialization - * sequence of the stream - * @param deliveryType the {@link DeliveryType} of the stream, see the enum for - * possible values - * @param itagItem the {@link ItagItem} which will be used to get the duration - * of progressive streams - * @param durationSecondsFallback the duration in seconds, extracted from player response, used - * as a fallback if the duration could not be determined - * @return a {@link Document} object which contains a {@code
- * The {@code
- * The {@code
- * This element, with its attributes and values, is: - *
- * - *
- * {@code
- * The {@code
- * The {@code
- * This method is only used when generating DASH manifests of audio streams. - *
- * - *
- * It will produce the following element:
- *
- * {@code
- * The {@code
- * This method is only used when generating DASH manifests from progressive streams. - *
- * - *
- * The {@code
- * This method is only used when generating DASH manifests from progressive streams. - *
- * - *
- * It generates the following element:
- *
- * {@code
- * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
- * as the second parameter)
- *
- * The {@code
- * This method is only used when generating DASH manifests from progressive streams. - *
- * - *
- * It generates the following element:
- *
- * {@code
- * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
- * as the second parameter)
- *
- * The {@code
- * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. - *
- * - *
- * It will produce a {@code
- *
- *
- * The {@code
- * The {@code
- * By parsing by the first media sequence, we know how many durations and repetitions there are - * so we just have to loop into segment durations to generate the following elements for each: - *
- * - *
- * {@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
- * The {@code
- * We don't know the exact duration of segments for post-live-DVR streams but an
- * average instead (which is the {@code targetDurationSec} value), so we can use the following
- * structure to generate the segment timeline for DASH manifests of ended livestreams:
- *
- * {@code }
- *
+ * This class includes common methods of manifest creators and useful constants. + *
+ * + *+ * Generation of DASH documents and their conversion as a string is done using external classes + * from {@link org.w3c.dom} and {@link javax.xml} packages. + *
+ */ +public final class YoutubeDashManifestCreatorsUtils { + + private YoutubeDashManifestCreatorsUtils() { + } + + /** + * The redirect count limit that this class uses, which is the same limit as OkHttp. + */ + public static final int MAXIMUM_REDIRECT_COUNT = 20; + + /** + * URL parameter of the first sequence for live, post-live-DVR and OTF streams. + */ + public static final String SQ_0 = "&sq=0"; + + /** + * URL parameter of the first stream request made by official clients. + */ + public static final String RN_0 = "&rn=0"; + + /** + * URL parameter specific to web clients. When this param is added, if a redirection occurs, + * the server will not redirect clients to the redirect URL. Instead, it will provide this URL + * as the response body. + */ + public static final String ALR_YES = "&alr=yes"; + + /** + * Constant which represents the {@code MPD} element of DASH manifests. + */ + public static final String MPD = "MPD"; + + /** + * Constant which represents the {@code Period} element of DASH manifests. + */ + public static final String PERIOD = "Period"; + + /** + * Constant which represents the {@code AdaptationSet} element of DASH manifests. + */ + public static final String ADAPTATION_SET = "AdaptationSet"; + + /** + * Constant which represents the {@code Role} element of DASH manifests. + */ + public static final String ROLE = "Role"; + + /** + * Constant which represents the {@code Representation} element of DASH manifests. + */ + public static final String REPRESENTATION = "Representation"; + + /** + * Constant which represents the {@code AudioChannelConfiguration} element of DASH manifests. + */ + public static final String AUDIO_CHANNEL_CONFIGURATION = "AudioChannelConfiguration"; + + /** + * Constant which represents the {@code SegmentTemplate} element of DASH manifests. + */ + public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; + + /** + * Constant which represents the {@code SegmentTimeline} element of DASH manifests. + */ + public static final String SEGMENT_TIMELINE = "SegmentTimeline"; + + /** + * Constant which represents the {@code SegmentBase} element of DASH manifests. + */ + public static final String BASE_URL = "BaseURL"; + + /** + * Constant which represents the {@code SegmentBase} element of DASH manifests. + */ + public static final String SEGMENT_BASE = "SegmentBase"; + + /** + * Constant which represents the {@code Initialization} element of DASH manifests. + */ + public static final String INITIALIZATION = "Initialization"; + + /** + * Generate a {@link Document} with common manifest creator elements added to it. + * + *+ * Those are: + *
+ * The generated {@code
+ * {@code
+ * The {@code
+ * The {@code
+ * This element, with its attributes and values, is: + *
+ * + *
+ * {@code
+ * The {@code
+ * The {@code
+ * This method is only used when generating DASH manifests of audio streams. + *
+ * + *
+ * It will produce the following element:
+ *
+ * {@code
+ * The {@code
+ * This method is only used when generating DASH manifests from OTF and post-live-DVR streams. + *
+ * + *
+ * It will produce a {@code
+ *
+ *
+ * The {@code
+ * The {@code
This method fetches, for OTF streams and for post-live-DVR streams: + *
This method will follow redirects which works in the following way: + *
+ * For non-HTML5 clients, redirections are managed in the standard way in + * {@link #getInitializationResponse(String, ItagItem, DeliveryType)}. + *
+ * + * @param downloader the {@link Downloader} instance to be used + * @param streamingUrl the streaming URL which we are trying to get a streaming URL + * without any redirection on the network and/or IP used + * @param responseMimeTypeExpected the response mime type expected from Google video servers + * @return the {@link Response} of the stream, which should have no redirections + */ + @SuppressWarnings("checkstyle:FinalParameters") + @Nonnull + private static Response getStreamingWebUrlWithoutRedirects( + @Nonnull final Downloader downloader, + @Nonnull String streamingUrl, + @Nonnull final String responseMimeTypeExpected) + throws CreationException { + try { + final Map+ * OTF streams are YouTube-DASH specific streams which work with sequences and without the need + * to get a manifest (even if one is provided, it is not used by official clients). + *
+ * + *+ * They can be found only on videos; mostly those with a small amount of views, or ended + * livestreams which have just been re-encoded as normal videos. + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param otfBaseStreamingUrl the base URL of the OTF stream, which must not be null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * must not be null + * @param durationSecondsFallback the duration of the video, which will be used if the duration + * could not be extracted from the first sequence + * @return the manifest generated into a string + */ + @Nonnull + public static String fromOtfStreamingUrl( + @Nonnull final String otfBaseStreamingUrl, + @Nonnull final ItagItem itagItem, + final long durationSecondsFallback) throws CreationException { + if (OTF_STREAMS_CACHE.containsKey(otfBaseStreamingUrl)) { + return Objects.requireNonNull(OTF_STREAMS_CACHE.get(otfBaseStreamingUrl)).getSecond(); + } + + 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(realOtfBaseStreamingUrl, + itagItem, DeliveryType.OTF); + 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 CreationException("Could not get the initialization URL: response code " + + responseCode); + } + + final String[] segmentDuration; + + try { + final String[] segmentsAndDurationsResponseSplit = response.responseBody() + // Get the lines with the durations and the following + .split("Segment-Durations-Ms: ")[1] + // Remove the other lines + .split("\n")[0] + // Get all durations and repetitions which are separated by a comma + .split(","); + final int lastIndex = segmentsAndDurationsResponseSplit.length - 1; + if (isBlank(segmentsAndDurationsResponseSplit[lastIndex])) { + segmentDuration = Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex); + } else { + segmentDuration = segmentsAndDurationsResponseSplit; + } + } catch (final Exception e) { + throw new CreationException("Could not get segment durations", e); + } + + long streamDuration; + try { + streamDuration = getStreamDuration(segmentDuration); + } catch (final CreationException e) { + streamDuration = durationSecondsFallback * 1000; + } + + final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + streamDuration); + + generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF); + generateSegmentTimelineElement(document); + generateSegmentElementsForOtfStreams(segmentDuration, document); + + return buildAndCacheResult(otfBaseStreamingUrl, document, OTF_STREAMS_CACHE); + } + + /** + * @return the cache of DASH manifests generated for OTF streams + */ + public static ManifestCreatorCache+ * By parsing by the first media sequence, we know how many durations and repetitions there are + * so we just have to loop into segment durations to generate the following elements for each + * duration repeated X times: + *
+ * + *
+ * {@code }
+ *
+ * If there is no repetition of the duration between two segments, the {@code r} attribute is + * not added to the {@code S} element, as it is not needed. + *
+ * + *
+ * These elements will be appended as children of the {@code
+ * The duration of OTF streams is not returned into the player response and needs to be + * calculated by adding the duration of each segment. + *
+ * + * @param segmentDuration the segment duration object extracted from the initialization + * sequence of the stream + * @return the duration of the OTF stream, in milliseconds + */ + private static long getStreamDuration(@Nonnull final String[] segmentDuration) + throws CreationException { + try { + long streamLengthMs = 0; + + for (final String segDuration : segmentDuration) { + final String[] segmentLengthRepeat = segDuration.split("\\(r="); + long segmentRepeatCount = 0; + + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + segmentRepeatCount = Long.parseLong(Utils.removeNonDigitCharacters( + segmentLengthRepeat[1])); + } + + final long segmentLength = Integer.parseInt(segmentLengthRepeat[0]); + streamLengthMs += segmentLength + segmentRepeatCount * segmentLength; + } + + return streamLengthMs; + } catch (final NumberFormatException e) { + throw new CreationException("Could not get stream length from sequences list", e); + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java new file mode 100644 index 000000000..21a21d2c6 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java @@ -0,0 +1,221 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; + +import org.schabi.newpipe.extractor.downloader.Response; +import org.schabi.newpipe.extractor.services.youtube.DeliveryType; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; +import org.schabi.newpipe.extractor.utils.ManifestCreatorCache; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ALR_YES; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.RN_0; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SQ_0; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTemplateElement; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTimelineElement; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.getInitializationResponse; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +/** + * Class which generates DASH manifests of YouTube post-live DVR streams (which use the + * {@link DeliveryType#LIVE LIVE delivery type}). + */ +public final class YoutubePostLiveStreamDvrDashManifestCreator { + + /** + * Cache of DASH manifests generated for post-live-DVR streams. + */ + private static final ManifestCreatorCache+ * Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which + * works with sequences and without the need to get a manifest (even if one is provided but not + * used by main clients (and is not complete for big ended livestreams because it doesn't + * return the full stream)). + *
+ * + *+ * They can be found only on livestreams which have ended very recently (a few hours, most of + * the time) + *
+ * + *This method needs: + *
In order to generate the DASH manifest, this method will: + *
+ * If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used + * as the stream duration. + *
+ * + * @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended + * livestream, which must not be null + * @param itagItem the {@link ItagItem} corresponding to the stream, which + * must not be null + * @param targetDurationSec the target duration of each sequence, in seconds (this + * value is returned with the {@code targetDurationSec} + * field for each stream in YouTube's player response) + * @param durationSecondsFallback the duration of the ended livestream, which will be + * used if the duration could not be extracted from the + * first sequence + * @return the manifest generated into a string + */ + @Nonnull + public static String fromPostLiveStreamDvrStreamingUrl( + @Nonnull final String postLiveStreamDvrStreamingUrl, + @Nonnull final ItagItem itagItem, + final int targetDurationSec, + final long durationSecondsFallback) throws CreationException { + if (POST_LIVE_DVR_STREAMS_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) { + return Objects.requireNonNull( + POST_LIVE_DVR_STREAMS_CACHE.get(postLiveStreamDvrStreamingUrl)).getSecond(); + } + + String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; + final String streamDurationString; + final String segmentCount; + + if (targetDurationSec <= 0) { + throw new CreationException("targetDurationSec value is <= 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(realPostLiveStreamDvrStreamingUrl, + itagItem, DeliveryType.LIVE); + 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 CreationException( + "Could not get the initialization sequence: response code " + responseCode); + } + + final Map
+ * We don't know the exact duration of segments for post-live-DVR streams but an
+ * average instead (which is the {@code targetDurationSec} value), so we can use the following
+ * structure to generate the segment timeline for DASH manifests of ended livestreams:
+ *
+ * {@code }
+ *
+ * Progressive streams are YouTube DASH streams which work with range requests and without the + * need to get a manifest. + *
+ * + *+ * They can be found on all videos, and for all streams for most of videos which come from a + * YouTube partner, and on videos with a large number of views. + *
+ * + *This method needs: + *
+ * The {@code
+ * It generates the following element:
+ *
+ * {@code
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
+ * as the second parameter)
+ *
+ * The {@code
+ * It generates the following element:
+ *
+ * {@code
+ * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
+ * as the second parameter)
+ *
+ * The {@code
+ * Tests the generation of OTF and progressive manifests. + *
* ** We cannot test the generation of DASH manifests for ended livestreams because these videos will @@ -54,8 +63,9 @@ import javax.xml.parsers.DocumentBuilderFactory; * *
* The generation of DASH manifests for OTF streams, which can be tested, uses a video licenced - * under the Creative Commons Attribution licence (reuse allowed): - * {@code https://www.youtube.com/watch?v=DJ8GQUNUXGM} + * under the Creative Commons Attribution licence (reuse allowed): {@code A New Era of Open? + * COVID-19 and the Pursuit for Equitable Solutions} (https://www.youtube.com/watch?v=DJ8GQUNUXGM) *
* *@@ -68,8 +78,8 @@ import javax.xml.parsers.DocumentBuilderFactory; * So the real downloader will be used everytime on this test class. *
*/ -class YoutubeDashManifestCreatorTest { - // Setting a higher number may let Google video servers return a lot of 403s +class YoutubeDashManifestCreatorsTest { + // Setting a higher number may let Google video servers return 403s private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 3; private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; private static YoutubeStreamExtractor extractor; @@ -102,7 +112,7 @@ class YoutubeDashManifestCreatorTest { assertProgressiveStreams(extractor.getAudioStreams()); // we are not able to generate DASH manifests of video formats with audio - assertThrows(YoutubeDashManifestCreator.CreationException.class, + assertThrows(CreationException.class, () -> assertProgressiveStreams(extractor.getVideoStreams())); } @@ -110,7 +120,7 @@ class YoutubeDashManifestCreatorTest { for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.DASH)) { //noinspection ConstantConditions - final String manifest = YoutubeDashManifestCreator.fromOtfStreamingUrl( + final String manifest = YoutubeOtfDashManifestCreator.fromOtfStreamingUrl( stream.getContent(), stream.getItagItem(), videoLength); assertNotBlank(manifest); @@ -129,8 +139,9 @@ class YoutubeDashManifestCreatorTest { for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.PROGRESSIVE_HTTP)) { //noinspection ConstantConditions - final String manifest = YoutubeDashManifestCreator.fromProgressiveStreamingUrl( - stream.getContent(), stream.getItagItem(), videoLength); + final String manifest = + YoutubeProgressiveDashManifestCreator.fromProgressiveStreamingUrl( + stream.getContent(), stream.getItagItem(), videoLength); assertNotBlank(manifest); assertManifestGenerated( @@ -145,8 +156,10 @@ class YoutubeDashManifestCreatorTest { } } - private List extends Stream> assertFilterStreams(final List extends Stream> streams, - final DeliveryMethod deliveryMethod) { + @Nonnull + private List extends Stream> assertFilterStreams( + @Nonnull final List extends Stream> streams, + final DeliveryMethod deliveryMethod) { final List extends Stream> filteredStreams = streams.stream() .filter(stream -> stream.getDeliveryMethod() == deliveryMethod) @@ -190,7 +203,7 @@ class YoutubeDashManifestCreatorTest { } private void assertMpdElement(@Nonnull final Document document) { - final Element element = (Element) document.getElementsByTagName("MPD").item(0); + final Element element = (Element) document.getElementsByTagName(MPD).item(0); assertNotNull(element); assertNull(element.getParentNode().getNodeValue()); @@ -200,7 +213,7 @@ class YoutubeDashManifestCreatorTest { } private void assertPeriodElement(@Nonnull final Document document) { - assertGetElement(document, PERIOD, "MPD"); + assertGetElement(document, PERIOD, MPD); } private void assertAdaptationSetElement(@Nonnull final Document document, @@ -210,7 +223,7 @@ class YoutubeDashManifestCreatorTest { } private void assertRoleElement(@Nonnull final Document document) { - assertGetElement(document, "Role", ADAPTATION_SET); + assertGetElement(document, ROLE, ADAPTATION_SET); } private void assertRepresentationElement(@Nonnull final Document document, @@ -232,8 +245,8 @@ class YoutubeDashManifestCreatorTest { private void assertAudioChannelConfigurationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { - final Element element = assertGetElement(document, - "AudioChannelConfiguration", REPRESENTATION); + final Element element = assertGetElement(document, AUDIO_CHANNEL_CONFIGURATION, + REPRESENTATION); assertAttrEquals(itagItem.getAudioChannels(), element, "value"); } @@ -276,7 +289,7 @@ class YoutubeDashManifestCreatorTest { } private void assertBaseUrlElement(@Nonnull final Document document) { - final Element element = assertGetElement(document, "BaseURL", REPRESENTATION); + final Element element = assertGetElement(document, BASE_URL, REPRESENTATION); assertIsValidUrl(element.getTextContent()); } @@ -294,7 +307,7 @@ class YoutubeDashManifestCreatorTest { private void assertAttrEquals(final int expected, - final Element element, + @Nonnull final Element element, final String attribute) { final int actual = Integer.parseInt(element.getAttribute(attribute)); @@ -305,7 +318,7 @@ class YoutubeDashManifestCreatorTest { } private void assertAttrEquals(final String expected, - final Element element, + @Nonnull final Element element, final String attribute) { final String actual = element.getAttribute(attribute); assertAll( @@ -316,7 +329,7 @@ class YoutubeDashManifestCreatorTest { private void assertRangeEquals(final int expectedStart, final int expectedEnd, - final Element element, + @Nonnull final Element element, final String attribute) { final String range = element.getAttribute(attribute); assertNotBlank(range); @@ -334,7 +347,8 @@ class YoutubeDashManifestCreatorTest { ); } - private Element assertGetElement(final Document document, + @Nonnull + private Element assertGetElement(@Nonnull final Document document, final String tagName, final String expectedParentTagName) { From fffbbee7f37127f14ffc011dad5903555f510881 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 14 May 2022 12:30:53 +0200 Subject: [PATCH 33/38] [YouTube] Add missing Nonnull annotations in getCache method of YouTube DASH manifest creators --- .../dashmanifestcreators/YoutubeOtfDashManifestCreator.java | 1 + .../YoutubePostLiveStreamDvrDashManifestCreator.java | 1 + .../YoutubeProgressiveDashManifestCreator.java | 1 + 3 files changed, 3 insertions(+) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java index 375fd7421..f76d22356 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java @@ -161,6 +161,7 @@ public final class YoutubeOtfDashManifestCreator { /** * @return the cache of DASH manifests generated for OTF streams */ + @Nonnull public static ManifestCreatorCache- * It is different of {@link org.schabi.newpipe.extractor.stream.DeliveryMethod delivery methods}! + * It is different from {@link org.schabi.newpipe.extractor.stream.DeliveryMethod delivery methods}! *
*/ public enum DeliveryType { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java index 7f3c52323..ec876c671 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java @@ -83,59 +83,18 @@ public final class YoutubeDashManifestCreatorsUtils { */ public static final String ALR_YES = "&alr=yes"; - /** - * Constant which represents the {@code MPD} element of DASH manifests. - */ + // XML elements of DASH MPD manifests + // see https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html public static final String MPD = "MPD"; - - /** - * Constant which represents the {@code Period} element of DASH manifests. - */ public static final String PERIOD = "Period"; - - /** - * Constant which represents the {@code AdaptationSet} element of DASH manifests. - */ public static final String ADAPTATION_SET = "AdaptationSet"; - - /** - * Constant which represents the {@code Role} element of DASH manifests. - */ public static final String ROLE = "Role"; - - /** - * Constant which represents the {@code Representation} element of DASH manifests. - */ public static final String REPRESENTATION = "Representation"; - - /** - * Constant which represents the {@code AudioChannelConfiguration} element of DASH manifests. - */ public static final String AUDIO_CHANNEL_CONFIGURATION = "AudioChannelConfiguration"; - - /** - * Constant which represents the {@code SegmentTemplate} element of DASH manifests. - */ public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; - - /** - * Constant which represents the {@code SegmentTimeline} element of DASH manifests. - */ public static final String SEGMENT_TIMELINE = "SegmentTimeline"; - - /** - * Constant which represents the {@code SegmentBase} element of DASH manifests. - */ public static final String BASE_URL = "BaseURL"; - - /** - * Constant which represents the {@code SegmentBase} element of DASH manifests. - */ public static final String SEGMENT_BASE = "SegmentBase"; - - /** - * Constant which represents the {@code Initialization} element of DASH manifests. - */ public static final String INITIALIZATION = "Initialization"; /** @@ -665,7 +624,7 @@ public final class YoutubeDashManifestCreatorsUtils { if (isHtml5StreamingUrl) { baseStreamingUrl += ALR_YES; } - baseStreamingUrl = appendRnParamAndSqParamIfNeeded(baseStreamingUrl, deliveryType); + baseStreamingUrl = appendRnSqParamsIfNeeded(baseStreamingUrl, deliveryType); final Downloader downloader = NewPipe.getDownloader(); if (isHtml5StreamingUrl) { @@ -701,7 +660,7 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link DocumentBuilderFactory} instances. * * @return an instance of {@link Document} secured against XXE attacks on supported platforms, - * that should then be convertible to an XML string without security problems + * that should then be convertible to an XML string without security problems */ private static Document newDocument() throws ParserConfigurationException { final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); @@ -709,8 +668,8 @@ public final class YoutubeDashManifestCreatorsUtils { documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); } catch (final Exception ignored) { - // Ignore exceptions as setting these attributes to secure XML generation is supported - // by all platforms (like the Android implementation) + // Ignore exceptions as setting these attributes to secure XML generation is not + // supported by all platforms (like the Android implementation) } final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); @@ -736,8 +695,8 @@ public final class YoutubeDashManifestCreatorsUtils { transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); } catch (final Exception ignored) { - // Ignore exceptions as setting these attributes to secure XML generation is supported - // by all platforms (like the Android implementation) + // Ignore exceptions as setting these attributes to secure XML generation is not + // supported by all platforms (like the Android implementation) } final Transformer transformer = transformerFactory.newTransformer(); @@ -759,16 +718,10 @@ public final class YoutubeDashManifestCreatorsUtils { * @return the base streaming URL to which the param(s) are appended, depending on the * {@link DeliveryType} of the stream */ - @SuppressWarnings({"checkstyle:FinalParameters", "checkstyle:FinalLocalVariable"}) @Nonnull - private static String appendRnParamAndSqParamIfNeeded( - @Nonnull String baseStreamingUrl, - @Nonnull final DeliveryType deliveryType) { - if (deliveryType != DeliveryType.PROGRESSIVE) { - baseStreamingUrl += SQ_0; - } - - return baseStreamingUrl + RN_0; + private static String appendRnSqParamsIfNeeded(@Nonnull final String baseStreamingUrl, + @Nonnull final DeliveryType deliveryType) { + return baseStreamingUrl + (deliveryType == DeliveryType.PROGRESSIVE ? "" : SQ_0) + RN_0; } /** 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 012700129..5e57b2a13 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 @@ -1140,17 +1140,11 @@ public class YoutubeStreamExtractor extends StreamExtractor { return videoSecondaryInfoRenderer; } - @FunctionalInterface - private interface StreamBuilderHelper* The {@code StreamBuilderHelper} will set the following attributes in the @@ -1213,38 +1207,34 @@ public class YoutubeStreamExtractor extends StreamExtractor { * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. *
* - * @return a {@link StreamBuilderHelper} to build {@link AudioStream}s + * @return a stream builder helper to build {@link AudioStream}s */ @Nonnull - private StreamBuilderHelper* The {@code StreamBuilderHelper} will set the following attributes in the @@ -1272,37 +1262,33 @@ public class YoutubeStreamExtractor extends StreamExtractor { * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance. *
* - * @param areStreamsVideoOnly whether the {@link StreamBuilderHelper} will set the video + * @param areStreamsVideoOnly whether the stream builder helper will set the video * streams as video-only streams - * @return a {@link StreamBuilderHelper} to build {@link VideoStream}s + * @return a stream builder helper to build {@link VideoStream}s */ @Nonnull - private StreamBuilderHelper* This method downloads the provided manifest URL, finds all web occurrences in the manifest, @@ -340,17 +361,20 @@ public class SoundcloudStreamExtractor extends StreamExtractor { * this as a string. *
* + *+ * This was working before for Opus streams, but has been broken by SoundCloud. + *
+ * * @param hlsManifestUrl the URL of the manifest to be parsed * @return a single URL that contains a range equal to the length of the track */ @Nonnull private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl) throws ParsingException { - final Downloader dl = NewPipe.getDownloader(); final String hlsManifestResponse; try { - hlsManifestResponse = dl.get(hlsManifestUrl).responseBody(); + hlsManifestResponse = NewPipe.getDownloader().get(hlsManifestUrl).responseBody(); } catch (final IOException | ReCaptchaException e) { throw new ParsingException("Could not get SoundCloud HLS manifest"); } @@ -359,12 +383,13 @@ public class SoundcloudStreamExtractor extends StreamExtractor { for (int l = lines.length - 1; l >= 0; l--) { final String line = lines[l]; // Get the last URL from manifest, because it contains the range of the stream - if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith("https")) { + if (line.trim().length() != 0 && !line.startsWith("#") && line.startsWith(HTTPS)) { final String[] hlsLastRangeUrlArray = line.split("/"); return HTTPS + hlsLastRangeUrlArray[2] + "/media/0/" + hlsLastRangeUrlArray[5] + "/" + hlsLastRangeUrlArray[6]; } } + throw new ParsingException("Could not get any URL from HLS manifest"); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java index b72f054db..da2c0511d 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/soundcloud/SoundcloudStreamExtractorTest.java @@ -188,25 +188,27 @@ public class SoundcloudStreamExtractorTest { super.testAudioStreams(); final List