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 - * AudioStream.java is part of NewPipe. + * AudioStream.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * NewPipe Extractor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * NewPipe is distributed in the hope that it will be useful, + * NewPipe Extractor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . + * along with NewPipe Extractor. If not, see . */ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.services.youtube.ItagItem; -public class AudioStream extends Stream { +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public final class AudioStream extends Stream { + public static final int UNKNOWN_BITRATE = -1; + private final int averageBitrate; - // Fields for Dash - private int itag; + // Fields for DASH + private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; private int bitrate; private int initStart; private int initEnd; @@ -35,37 +41,249 @@ public class AudioStream extends Stream { private int indexEnd; private String quality; private String codec; + @Nullable + private ItagItem itagItem; /** - * Create a new audio stream - * @param url the url - * @param format the format - * @param averageBitrate the average bitrate + * Class to build {@link AudioStream} objects. */ - public AudioStream(final String url, - final MediaFormat format, - final int averageBitrate) { - super(url, format); + @SuppressWarnings("checkstyle:hiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + private int averageBitrate = UNKNOWN_BITRATE; + @Nullable + private ItagItem itagItem; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link SubtitlesStream}. + * + *

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

+ * + * @param id the identifier of the {@link SubtitlesStream}, which must be not null + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link AudioStream}. + * + *

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

+ * + * @param content the content of the {@link AudioStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link AudioStream}. + * + *

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

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link AudioStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must + * be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link AudioStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set the average bitrate of the {@link AudioStream}. + * + *

+ * The default value is {@link #UNKNOWN_BITRATE}. + *

+ * + * @param averageBitrate the average bitrate of the {@link AudioStream}, which should + * positive + * @return this {@link Builder} instance + */ + public Builder setAverageBitrate(final int averageBitrate) { + this.averageBitrate = averageBitrate; + return this; + } + + /** + * Set the {@link ItagItem} corresponding to the {@link AudioStream}. + * + *

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

+ * + * @param itagItem the {@link ItagItem} of the {@link AudioStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setItagItem(@Nullable final ItagItem itagItem) { + this.itagItem = itagItem; + return this; + } + + /** + * Build an {@link AudioStream} using the builder's current values. + * + *

+ * The identifier and the content (and so the {@code isUrl} boolean) properties must have + * been set. + *

+ * + * @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} + */ + @Nonnull + public AudioStream build() { + if (id == null) { + throw new IllegalStateException( + "The identifier of the audio stream has been not set or is null. If you " + + "are not able to get an identifier, use the static constant " + + "ID_UNKNOWN of the Stream class."); + } + + if (content == null) { + throw new IllegalStateException("The content of the audio stream has been not set " + + "or is null. Please specify a non-null one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "The delivery method of the audio stream has been set as null, which is " + + "not allowed. Pass a valid one instead with setDeliveryMethod."); + } + + return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate, + baseUrl, itagItem); + } + } + + + /** + * Create a new audio stream. + * + * @param id the ID which uniquely identifies the stream, e.g. for YouTube this + * would be the itag + * @param content the content or the URL of the stream, depending on whether isUrl is + * true + * @param isUrl whether content is the URL or the actual content of e.g. a DASH + * manifest + * @param format the {@link MediaFormat} used by the stream, which can be null + * @param deliveryMethod the {@link DeliveryMethod} of the 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) + */ + private AudioStream(@Nonnull final String id, + @Nonnull final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + @Nonnull final DeliveryMethod deliveryMethod, + final int averageBitrate, + @Nullable final String baseUrl, + @Nullable final ItagItem itagItem) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); + if (itagItem != null) { + this.itagItem = itagItem; + this.itag = itagItem.id; + this.quality = itagItem.getQuality(); + this.bitrate = itagItem.getBitrate(); + this.initStart = itagItem.getInitStart(); + this.initEnd = itagItem.getInitEnd(); + this.indexStart = itagItem.getIndexStart(); + this.indexEnd = itagItem.getIndexEnd(); + this.codec = itagItem.getCodec(); + } this.averageBitrate = averageBitrate; } /** - * Create a new audio stream - * @param url the url - * @param itag the ItagItem of the Stream + * {@inheritDoc} */ - public AudioStream(final String url, final ItagItem itag) { - this(url, itag.getMediaFormat(), itag.avgBitrate); - this.itag = itag.id; - this.quality = itag.getQuality(); - this.bitrate = itag.getBitrate(); - this.initStart = itag.getInitStart(); - this.initEnd = itag.getInitEnd(); - this.indexStart = itag.getIndexStart(); - this.indexEnd = itag.getIndexEnd(); - this.codec = itag.getCodec(); - } - @Override public boolean equalStats(final Stream cmp) { return super.equalStats(cmp) && cmp instanceof AudioStream @@ -73,42 +291,125 @@ public class AudioStream extends Stream { } /** - * Get the average bitrate - * @return the average bitrate or -1 + * Get the average bitrate of the stream. + * + * @return the average bitrate or {@link #UNKNOWN_BITRATE} if it is unknown */ public int getAverageBitrate() { return averageBitrate; } + /** + * Get the itag identifier of the stream. + * + *

+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the + * ones of the YouTube service. + *

+ * + * @return the number of the {@link ItagItem} passed in the constructor of the audio stream. + */ public int getItag() { return itag; } + /** + * Get the bitrate of the stream. + * + * @return the bitrate set from the {@link ItagItem} passed in the constructor of the stream. + */ public int getBitrate() { return bitrate; } + /** + * Get the initialization start of the stream. + * + * @return the initialization start value set from the {@link ItagItem} passed in the + * constructor of the stream. + */ public int getInitStart() { return initStart; } + /** + * Get the initialization end of the stream. + * + * @return the initialization end value set from the {@link ItagItem} passed in the constructor + * of the stream. + */ public int getInitEnd() { return initEnd; } + /** + * Get the index start of the stream. + * + * @return the index start value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexStart() { return indexStart; } + /** + * Get the index end of the stream. + * + * @return the index end value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexEnd() { return indexEnd; } + /** + * Get the quality of the stream. + * + * @return the quality label set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public String getQuality() { return quality; } + /** + * Get the codec of the stream. + * + * @return the codec set from the {@link ItagItem} passed in the constructor of the stream. + */ public String getCodec() { return codec; } + + /** + * {@inheritDoc} + */ + @Override + @Nullable + public ItagItem getItagItem() { + return itagItem; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final AudioStream audioStream = (AudioStream) obj; + return averageBitrate == audioStream.averageBitrate; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), averageBitrate); + } } 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 new file mode 100644 index 000000000..db74e91ab --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java @@ -0,0 +1,37 @@ +package org.schabi.newpipe.extractor.stream; + +/** + * An enum to represent the different delivery methods of {@link Stream streams} which are returned + * by the extractor. + */ +public enum DeliveryMethod { + + /** + * Enum constant which represents the use of the progressive HTTP streaming method to fetch a + * {@link Stream stream}. + */ + PROGRESSIVE_HTTP, + + /** + * Enum constant which represents the use of the DASH adaptive streaming method to fetch a + * {@link Stream stream}. + */ + DASH, + + /** + * Enum constant which represents the use of the HLS adaptive streaming method to fetch a + * {@link Stream stream}. + */ + HLS, + + /** + * Enum constant which represents the use of the SmoothStreaming adaptive streaming method to + * fetch a {@link Stream stream}. + */ + SS, + + /** + * Enum constant which represents the use of a torrent to fetch a {@link Stream stream}. + */ + 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 5b827c159..b76594a6f 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 @@ -1,68 +1,73 @@ package org.schabi.newpipe.extractor.stream; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; + import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import org.schabi.newpipe.extractor.MediaFormat; - -import java.io.Serializable; -import java.util.List; - /** - * Creates a stream object from url, format and optional torrent url + * Abstract class which represents streams in the extractor. */ public abstract class Stream implements Serializable { - private final MediaFormat mediaFormat; - private final String url; - private final String torrentUrl; + public static final int FORMAT_ID_UNKNOWN = -1; + public static final String ID_UNKNOWN = " "; /** - * @deprecated Use {@link #getFormat()} or {@link #getFormatId()} - */ - @Deprecated - public final int format; - - /** - * Instantiates a new stream object. + * 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). * - * @param url the url - * @param format the format + *

+ * An itag should not have a negative value so {@code -1} is used for this constant. + *

*/ - public Stream(final String url, final MediaFormat format) { - this(url, null, format); - } + public static final int ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE = -1; + + private final String id; + @Nullable private final MediaFormat mediaFormat; + private final String content; + private final boolean isUrl; + private final DeliveryMethod deliveryMethod; + @Nullable private final String baseUrl; /** - * Instantiates a new stream object. + * Instantiates a new {@code Stream} object. * - * @param url the url - * @param torrentUrl the url to torrent file, example - * https://webtorrent.io/torrents/big-buck-bunny.torrent - * @param format the format + * @param id the ID which uniquely identifies the file, e.g. for YouTube this would + * be the itag + * @param content the content or URL, depending on whether isUrl is true + * @param isUrl whether content is the URL or the actual content of e.g. a DASH + * manifest + * @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 */ - public Stream(final String url, final String torrentUrl, final MediaFormat format) { - this.url = url; - this.torrentUrl = torrentUrl; - //noinspection deprecation - this.format = format.id; + public Stream(final String id, + final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + final DeliveryMethod deliveryMethod, + @Nullable final String baseUrl) { + this.id = id; + this.content = content; + this.isUrl = isUrl; this.mediaFormat = format; + this.deliveryMethod = deliveryMethod; + this.baseUrl = baseUrl; } /** - * Reveals whether two streams have the same stats (format and bitrate, for example) - */ - public boolean equalStats(final Stream cmp) { - return cmp != null && getFormatId() == cmp.getFormatId(); - } - - /** - * Reveals whether two Streams are equal - */ - public boolean equals(final Stream cmp) { - return equalStats(cmp) && url.equals(cmp.url); - } - - /** - * Check if the list already contains one stream with equals stats + * Checks if the list already contains one stream with equals stats. + * + * @param stream the stream which will be compared to the streams in the stream list + * @param streamList the list of {@link Stream Streams} which will be compared + * @return whether the list already contains one stream with equals stats */ public static boolean containSimilarStream(final Stream stream, final List streamList) { @@ -78,38 +83,170 @@ public abstract class Stream implements Serializable { } /** - * Gets the url. + * Reveals whether two streams have the same stats ({@link MediaFormat media format} and + * {@link DeliveryMethod delivery method}). * - * @return the url + *

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

+ * + * @param cmp the stream object to be compared to this stream object + * @return whether the stream have the same stats or not, based on the criteria above */ + public boolean equalStats(@Nullable final Stream cmp) { + if (cmp == null) { + return false; + } + + Boolean haveSameMediaFormatId = null; + if (mediaFormat != null && cmp.mediaFormat != null) { + haveSameMediaFormatId = mediaFormat.id == cmp.mediaFormat.id; + } + final boolean areUsingSameDeliveryMethodAndAreUrlStreams = + deliveryMethod == cmp.deliveryMethod && isUrl == cmp.isUrl; + + return haveSameMediaFormatId != null + ? haveSameMediaFormatId && areUsingSameDeliveryMethodAndAreUrlStreams + : areUsingSameDeliveryMethodAndAreUrlStreams; + } + + /** + * Reveals whether two streams are equal. + * + * @param cmp the stream object to be compared to this stream object + * @return whether streams are equal + */ + public boolean equals(final Stream cmp) { + return equalStats(cmp) && content.equals(cmp.content); + } + + /** + * Gets the identifier of this stream, e.g. the itag for YouTube. + * + *

+ * It should be normally unique but {@link #ID_UNKNOWN} may be returned as the identifier if + * one used by the stream extractor cannot be extracted, if the extractor uses a value from a + * streaming service. + *

+ * + * @return the id (which may be {@link #ID_UNKNOWN}) + */ + public String getId() { + return id; + } + + /** + * Gets the URL of this stream if the content is a URL, or {@code null} if that's the not case. + * + * @return the URL if the content is a URL, {@code null} otherwise + * @deprecated Use {@link #getContent()} instead. + */ + @Deprecated + @Nullable public String getUrl() { - return url; + return isUrl ? content : null; } /** - * Gets the torrent url. + * Gets the content or URL. * - * @return the torrent url, example https://webtorrent.io/torrents/big-buck-bunny.torrent + * @return the content or URL */ - public String getTorrentUrl() { - return torrentUrl; + public String getContent() { + return content; } /** - * Gets the format. + * Returns if the content is a URL or not. + * + * @return {@code true} if the content of this stream content is a URL, {@code false} + * if it is the actual content + */ + public boolean isUrl() { + return isUrl; + } + + /** + * Gets the {@link MediaFormat}, which can be null. * * @return the format */ + @Nullable public MediaFormat getFormat() { return mediaFormat; } /** - * Gets the format id. + * Gets the format id, which can be unknown. * - * @return the format id + * @return the format id or {@link #FORMAT_ID_UNKNOWN} */ public int getFormatId() { - return mediaFormat.id; + if (mediaFormat != null) { + return mediaFormat.id; + } + return FORMAT_ID_UNKNOWN; } -} + + /** + * Gets the delivery method. + * + * @return the delivery method + */ + @Nonnull + public DeliveryMethod getDeliveryMethod() { + return deliveryMethod; + } + + /** + * Gets the base URL of a stream. + * + *

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

+ * + * @return the base URL of the stream or {@code null} + */ + @Nullable + public String getBaseUrl() { + return baseUrl; + } + + /** + * Gets the {@link ItagItem} of a stream. + * + *

+ * If the stream is not a YouTube stream, this value will always be null. + *

+ * + * @return the {@link ItagItem} of the stream or {@code null} + */ + @Nullable + public abstract ItagItem getItagItem(); + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final Stream stream = (Stream) obj; + return id.equals(stream.id) && mediaFormat == stream.mediaFormat + && deliveryMethod == stream.deliveryMethod; + } + + @Override + public int hashCode() { + return Objects.hash(id, mediaFormat, deliveryMethod); + } +} \ No newline at end of file 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 0ac01a89c..732d822d7 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 @@ -1,53 +1,295 @@ package org.schabi.newpipe.extractor.stream; import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; -import java.io.Serializable; import java.util.Locale; +import java.util.Objects; -public class SubtitlesStream extends Stream implements Serializable { +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; + +public final class SubtitlesStream extends Stream { private final MediaFormat format; private final Locale locale; private final boolean autoGenerated; private final String code; - public SubtitlesStream(final MediaFormat format, - final String languageCode, - final String url, - final boolean autoGenerated) { - super(url, format); + /** + * Class to build {@link SubtitlesStream} objects. + */ + @SuppressWarnings("checkstyle:HiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + private String languageCode; + // Use of the Boolean class instead of the primitive type needed for setter call check + private Boolean autoGenerated; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link SubtitlesStream}. + * + * @param id the identifier of the {@link SubtitlesStream}, which should be not null + * (otherwise the fallback to create the identifier will be used when building + * the builder) + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link SubtitlesStream}. + * + *

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

+ * + * @param content the content of the {@link SubtitlesStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link SubtitlesStream}. + * + *

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

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link SubtitlesStream}, which can be + * null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link SubtitlesStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which + * must be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link SubtitlesStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link SubtitlesStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set the language code of the {@link SubtitlesStream}. + * + *

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

+ * + * @param languageCode the language code of the {@link SubtitlesStream} + * @return this {@link Builder} instance + */ + public Builder setLanguageCode(@Nonnull final String languageCode) { + this.languageCode = languageCode; + return this; + } + + /** + * Set whether the subtitles have been generated by the streaming service. + * + * @param autoGenerated whether the subtitles have been generated by the streaming + * service + * @return this {@link Builder} instance + */ + public Builder setAutoGenerated(final boolean autoGenerated) { + this.autoGenerated = autoGenerated; + return this; + } + + /** + * Build a {@link SubtitlesStream} using the builder's current values. + * + *

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

+ * + * @return a new {@link SubtitlesStream} using the builder's current values + * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}), + * {@code deliveryMethod}, {@code languageCode} or the {@code isAutogenerated} have been + * not set or set as {@code null} + */ + @Nonnull + public SubtitlesStream build() { + if (content == null) { + throw new IllegalStateException("No valid content was specified. Please specify a " + + "valid one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "The delivery method of the subtitles stream has been set as null, which " + + "is not allowed. Pass a valid one instead with" + + "setDeliveryMethod."); + } + + if (languageCode == null) { + throw new IllegalStateException("The language code of the subtitles stream has " + + "been not set or is null. Make sure you specified an non null language " + + "code with setLanguageCode."); + } + + if (autoGenerated == null) { + throw new IllegalStateException("The subtitles stream has been not set as an " + + "autogenerated subtitles stream or not. Please specify this information " + + "with setIsAutoGenerated."); + } + + if (id == null) { + id = languageCode + (mediaFormat != null ? "." + mediaFormat.suffix + : EMPTY_STRING); + } + + return new SubtitlesStream(id, content, isUrl, mediaFormat, deliveryMethod, + languageCode, autoGenerated, baseUrl); + } + } + + /** + * Create a new subtitles stream. + * + * @param id the ID which uniquely identifies the stream, e.g. for YouTube this + * would be the itag + * @param content the content or the URL of the stream, depending on whether isUrl is + * true + * @param isUrl whether content is the URL or the actual content of e.g. a DASH + * manifest + * @param format the {@link MediaFormat} used by the stream + * @param 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) + */ + private SubtitlesStream(@Nonnull final String id, + @Nonnull final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + @Nonnull final DeliveryMethod deliveryMethod, + @Nonnull final String languageCode, + final boolean autoGenerated, + @Nullable final String baseUrl) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); /* - * Locale.forLanguageTag only for API >= 21 - * Locale.Builder only for API >= 21 - * Country codes doesn't work well without - */ + * Locale.forLanguageTag only for Android API >= 21 + * Locale.Builder only for Android API >= 21 + * Country codes doesn't work well without + */ final String[] splits = languageCode.split("-"); switch (splits.length) { - default: - this.locale = new Locale(splits[0]); - break; - case 3: - // complex variants doesn't work! - this.locale = new Locale(splits[0], splits[1], splits[2]); - break; case 2: this.locale = new Locale(splits[0], splits[1]); break; + case 3: + // Complex variants don't work! + this.locale = new Locale(splits[0], splits[1], splits[2]); + break; + default: + this.locale = new Locale(splits[0]); + break; } + this.code = languageCode; this.format = format; this.autoGenerated = autoGenerated; } + /** + * Get the extension of the subtitles. + * + * @return the extension of the subtitles + */ public String getExtension() { return format.suffix; } + /** + * Return whether if the subtitles are auto-generated. + *

+ * Some streaming services can generate subtitles for their contents, like YouTube. + *

+ * + * @return {@code true} if the subtitles are auto-generated, {@code false} otherwise + */ public boolean isAutoGenerated() { return autoGenerated; } + /** + * {@inheritDoc} + */ @Override public boolean equalStats(final Stream cmp) { return super.equalStats(cmp) @@ -56,16 +298,67 @@ public class SubtitlesStream extends Stream implements Serializable { && autoGenerated == ((SubtitlesStream) cmp).autoGenerated; } + /** + * Get the display language name of the subtitles. + * + * @return the display language name of the subtitles + */ public String getDisplayLanguageName() { return locale.getDisplayName(locale); } + /** + * Get the language tag of the subtitles. + * + * @return the language tag of the subtitles + */ public String getLanguageTag() { return code; } + /** + * Get the {@link Locale locale} of the subtitles. + * + * @return the {@link Locale locale} of the subtitles + */ public Locale getLocale() { return locale; } + /** + * No subtitles which are currently extracted use an {@link ItagItem}, so {@code null} is + * returned by this method. + * + * @return {@code null} + */ + @Nullable + @Override + public ItagItem getItagItem() { + return null; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final SubtitlesStream subtitlesStream = (SubtitlesStream) obj; + return autoGenerated == subtitlesStream.autoGenerated + && locale.equals(subtitlesStream.locale) + && code.equals(subtitlesStream.code); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), locale, autoGenerated, code); + } } 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 9e6b4eb2b..c8ab5cfc9 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 @@ -4,31 +4,42 @@ package org.schabi.newpipe.extractor.stream; * Created by Christian Schabesberger on 04.03.16. * * Copyright (C) Christian Schabesberger 2016 - * VideoStream.java is part of NewPipe. + * VideoStream.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * NewPipe Extractor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * NewPipe is distributed in the hope that it will be useful, + * NewPipe Extractor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . + * along with NewPipe Extractor. If not, see . */ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.services.youtube.ItagItem; -public class VideoStream extends Stream { +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +public final class VideoStream extends Stream { + public static final String RESOLUTION_UNKNOWN = ""; + + /** @deprecated Use {@link #getResolution()} instead. */ + @Deprecated public final String resolution; + + /** @deprecated Use {@link #isVideoOnly()} instead. */ + @Deprecated public final boolean isVideoOnly; - // Fields for Dash - private int itag; + // Fields for DASH + private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE; private int bitrate; private int initStart; private int initEnd; @@ -39,118 +50,468 @@ public class VideoStream extends Stream { private int fps; private String quality; private String codec; + @Nullable private ItagItem itagItem; - public VideoStream(final String url, final MediaFormat format, final String resolution) { - this(url, format, resolution, false); + /** + * Class to build {@link VideoStream} objects. + */ + @SuppressWarnings("checkstyle:hiddenField") + public static final class Builder { + private String id; + private String content; + private boolean isUrl; + private DeliveryMethod deliveryMethod = DeliveryMethod.PROGRESSIVE_HTTP; + @Nullable + private MediaFormat mediaFormat; + @Nullable + private String baseUrl; + // Use of the Boolean class instead of the primitive type needed for setter call check + private Boolean isVideoOnly; + private String resolution; + @Nullable + private ItagItem itagItem; + + /** + * Create a new {@link Builder} instance with its default values. + */ + public Builder() { + } + + /** + * Set the identifier of the {@link VideoStream}. + * + *

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

+ * + * @param id the identifier of the {@link VideoStream}, which must be not null + * @return this {@link Builder} instance + */ + public Builder setId(@Nonnull final String id) { + this.id = id; + return this; + } + + /** + * Set the content of the {@link VideoStream}. + * + *

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

+ * + * @param content the content of the {@link VideoStream} + * @param isUrl whether the content is a URL + * @return this {@link Builder} instance + */ + public Builder setContent(@Nonnull final String content, + final boolean isUrl) { + this.content = content; + this.isUrl = isUrl; + return this; + } + + /** + * Set the {@link MediaFormat} used by the {@link VideoStream}. + * + *

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

+ * + * @param mediaFormat the {@link MediaFormat} of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setMediaFormat(@Nullable final MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + return this; + } + + /** + * Set the {@link DeliveryMethod} of the {@link VideoStream}. + * + *

+ * It must be not null. + *

+ * + *

+ * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}. + *

+ * + * @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must + * be not null + * @return this {@link Builder} instance + */ + public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { + this.deliveryMethod = deliveryMethod; + return this; + } + + /** + * Set the base URL of the {@link VideoStream}. + * + *

+ * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which + * they have been parsed. + *

+ * + *

+ * The default value is {@code null}. + *

+ * + * @param baseUrl the base URL of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setBaseUrl(@Nullable final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * Set whether the {@link VideoStream} is video-only. + * + *

+ * This property must be set before building the {@link VideoStream}. + *

+ * + * @param isVideoOnly whether the {@link VideoStream} is video-only + * @return this {@link Builder} instance + */ + public Builder setIsVideoOnly(final boolean isVideoOnly) { + this.isVideoOnly = isVideoOnly; + return this; + } + + /** + * Set the resolution of 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. + *

+ * + * @param resolution the resolution of the {@link VideoStream} + * @return this {@link Builder} instance + */ + public Builder setResolution(@Nonnull final String resolution) { + this.resolution = resolution; + return this; + } + + /** + * Set the {@link ItagItem} corresponding to the {@link VideoStream}. + * + *

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

+ * + * @param itagItem the {@link ItagItem} of the {@link VideoStream}, which can be null + * @return this {@link Builder} instance + */ + public Builder setItagItem(@Nullable final ItagItem itagItem) { + this.itagItem = itagItem; + return this; + } + + /** + * Build a {@link VideoStream} using the builder's current values. + * + *

+ * The identifier, the content (and so the {@code isUrl} boolean), the {@code isVideoOnly} + * and the {@code resolution} properties must have been set. + *

+ * + * @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} + */ + @Nonnull + public VideoStream build() { + if (id == null) { + throw new IllegalStateException( + "The identifier of the video stream has been not set or is null. If you " + + "are not able to get an identifier, use the static constant " + + "ID_UNKNOWN of the Stream class."); + } + + if (content == null) { + throw new IllegalStateException("The content of the video stream has been not set " + + "or is null. Please specify a non-null one with setContent."); + } + + if (deliveryMethod == null) { + throw new IllegalStateException( + "The delivery method of the video stream has been set as null, which is " + + "not allowed. Pass a valid one instead with setDeliveryMethod."); + } + + if (isVideoOnly == null) { + throw new IllegalStateException("The video stream has been not set as a " + + "video-only stream or as a video stream with embedded audio. Please " + + "specify this information with setIsVideoOnly."); + } + + if (resolution == null) { + throw new IllegalStateException( + "The resolution of the video stream has been not set. Please specify it " + + "with setResolution (use an empty string if you are not able to " + + "get it)."); + } + + return new VideoStream(id, content, isUrl, mediaFormat, deliveryMethod, resolution, + isVideoOnly, baseUrl, itagItem); + } } - public VideoStream(final String url, - final MediaFormat format, - final String resolution, - final boolean isVideoOnly) { - this(url, null, format, resolution, isVideoOnly); - } - - public VideoStream(final String url, final boolean isVideoOnly, final ItagItem itag) { - this(url, itag.getMediaFormat(), itag.resolutionString, isVideoOnly); - this.itag = itag.id; - this.bitrate = itag.getBitrate(); - this.initStart = itag.getInitStart(); - this.initEnd = itag.getInitEnd(); - this.indexStart = itag.getIndexStart(); - this.indexEnd = itag.getIndexEnd(); - this.codec = itag.getCodec(); - this.height = itag.getHeight(); - this.width = itag.getWidth(); - this.quality = itag.getQuality(); - this.fps = itag.fps; - } - - public VideoStream(final String url, - final String torrentUrl, - final MediaFormat format, - final String resolution) { - this(url, torrentUrl, format, resolution, false); - } - - public VideoStream(final String url, - final String torrentUrl, - final MediaFormat format, - final String resolution, - final boolean isVideoOnly) { - super(url, torrentUrl, format); + /** + * Create a new video stream. + * + * @param id the ID which uniquely identifies the stream, e.g. for YouTube this + * would be the itag + * @param content the content or the URL of the stream, depending on whether isUrl is + * true + * @param isUrl whether content is the URL or the actual content of e.g. a DASH + * manifest + * @param format the {@link MediaFormat} used by the stream, which can be null + * @param deliveryMethod the {@link DeliveryMethod} of the 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) + */ + private VideoStream(@Nonnull final String id, + @Nonnull final String content, + final boolean isUrl, + @Nullable final MediaFormat format, + @Nonnull final DeliveryMethod deliveryMethod, + @Nonnull final String resolution, + final boolean isVideoOnly, + @Nullable final String baseUrl, + @Nullable final ItagItem itagItem) { + super(id, content, isUrl, format, deliveryMethod, baseUrl); + if (itagItem != null) { + this.itagItem = itagItem; + this.itag = itagItem.id; + this.bitrate = itagItem.getBitrate(); + this.initStart = itagItem.getInitStart(); + this.initEnd = itagItem.getInitEnd(); + this.indexStart = itagItem.getIndexStart(); + this.indexEnd = itagItem.getIndexEnd(); + this.codec = itagItem.getCodec(); + this.height = itagItem.getHeight(); + this.width = itagItem.getWidth(); + this.quality = itagItem.getQuality(); + this.fps = itagItem.getFps(); + } this.resolution = resolution; this.isVideoOnly = isVideoOnly; } + /** + * {@inheritDoc} + */ @Override public boolean equalStats(final Stream cmp) { - return super.equalStats(cmp) && cmp instanceof VideoStream + return super.equalStats(cmp) + && cmp instanceof VideoStream && resolution.equals(((VideoStream) cmp).resolution) && isVideoOnly == ((VideoStream) cmp).isVideoOnly; } /** - * Get the video resolution + * Get the video resolution. * - * @return the video resolution + *

+ * It can be unknown for some streams, like for HLS master playlists. In this case, + * {@link #RESOLUTION_UNKNOWN} is returned by this method. + *

+ * + * @return the video resolution or {@link #RESOLUTION_UNKNOWN} */ + @Nonnull public String getResolution() { return resolution; } /** - * Check if the video is video only. - *

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

+ * + * @return {@code true} if this stream is video-only, {@code false} otherwise */ public boolean isVideoOnly() { return isVideoOnly; } + /** + * Get the itag identifier of the stream. + * + *

+ * Always equals to {@link #ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE} for other streams than the + * ones of the YouTube service. + *

+ * + * @return the number of the {@link ItagItem} passed in the constructor of the video stream. + */ public int getItag() { return itag; } + /** + * Get the bitrate of the stream. + * + * @return the bitrate set from the {@link ItagItem} passed in the constructor of the stream. + */ public int getBitrate() { return bitrate; } + /** + * Get the initialization start of the stream. + * + * @return the initialization start value set from the {@link ItagItem} passed in the + * constructor of the + * stream. + */ public int getInitStart() { return initStart; } + /** + * Get the initialization end of the stream. + * + * @return the initialization end value set from the {@link ItagItem} passed in the constructor + * of the stream. + */ public int getInitEnd() { return initEnd; } + /** + * Get the index start of the stream. + * + * @return the index start value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexStart() { return indexStart; } + /** + * Get the index end of the stream. + * + * @return the index end value set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getIndexEnd() { return indexEnd; } + /** + * Get the width of the video stream. + * + * @return the width set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getWidth() { return width; } + /** + * Get the height of the video stream. + * + * @return the height set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getHeight() { return height; } + /** + * Get the frames per second of the video stream. + * + * @return the frames per second set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public int getFps() { return fps; } + /** + * Get the quality of the stream. + * + * @return the quality label set from the {@link ItagItem} passed in the constructor of the + * stream. + */ public String getQuality() { return quality; } + /** + * Get the codec of the stream. + * + * @return the codec set from the {@link ItagItem} passed in the constructor of the stream. + */ public String getCodec() { return codec; } + + /** + * {@inheritDoc} + */ + @Override + @Nullable + public ItagItem getItagItem() { + return itagItem; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + if (!super.equals(obj)) { + return false; + } + + final VideoStream videoStream = (VideoStream) obj; + return isVideoOnly == videoStream.isVideoOnly && resolution.equals(videoStream.resolution); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), resolution, isVideoOnly); + } } From ad993b920fe68efb956ff2c2cc93e9499572894a Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 3 Mar 2022 09:56:03 +0100 Subject: [PATCH 02/38] Remove fetching of the DASH manifest extracted when getting information of a content with StreamInfo DashMpdParser is only working with YouTube streams, as it uses the ItagItem class. Also improve code and comments of StreamInfo (especially final use where possible). --- .../newpipe/extractor/stream/StreamInfo.java | 139 ++++++------------ 1 file changed, 43 insertions(+), 96 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index fd5a55d75..c067221ca 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -9,7 +9,6 @@ import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.localization.DateWrapper; -import org.schabi.newpipe.extractor.utils.DashMpdParser; import org.schabi.newpipe.extractor.utils.ExtractorHelper; import java.io.IOException; @@ -26,24 +25,24 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; * Created by Christian Schabesberger on 26.08.15. * * Copyright (C) Christian Schabesberger 2016 - * StreamInfo.java is part of NewPipe. + * StreamInfo.java is part of NewPipe Extractor. * - * NewPipe is free software: you can redistribute it and/or modify + * NewPipe Extractor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * NewPipe is distributed in the hope that it will be useful, + * NewPipe Extractor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . + * along with NewPipe Extractor. If not, see . */ /** - * Info object for opened videos, ie the video ready to play. + * Info object for opened contents, i.e. the content ready to play. */ public class StreamInfo extends Info { @@ -69,27 +68,26 @@ public class StreamInfo extends Info { return getInfo(NewPipe.getServiceByUrl(url), url); } - public static StreamInfo getInfo(final StreamingService service, + public static StreamInfo getInfo(@Nonnull final StreamingService service, final String url) throws IOException, ExtractionException { return getInfo(service.getStreamExtractor(url)); } - public static StreamInfo getInfo(final StreamExtractor extractor) + public static StreamInfo getInfo(@Nonnull final StreamExtractor extractor) throws ExtractionException, IOException { extractor.fetchPage(); + final StreamInfo streamInfo; try { - final StreamInfo streamInfo = extractImportantData(extractor); + streamInfo = extractImportantData(extractor); extractStreams(streamInfo, extractor); extractOptionalData(streamInfo, extractor); return streamInfo; } catch (final ExtractionException e) { - // Currently YouTube does not distinguish between age restricted videos and - // videos blocked - // by country. This means that during the initialisation of the extractor, the - // extractor - // will assume that a video is age restricted while in reality it it blocked by - // country. + // Currently, YouTube does not distinguish between age restricted videos and videos + // blocked by country. This means that during the initialisation of the extractor, the + // extractor will assume that a video is age restricted while in reality it is blocked + // by country. // // We will now detect whether the video is blocked by country or not. @@ -102,22 +100,27 @@ public class StreamInfo extends Info { } } - private static StreamInfo extractImportantData(final StreamExtractor extractor) + @Nonnull + private static StreamInfo extractImportantData(@Nonnull final StreamExtractor extractor) throws ExtractionException { - /* ---- important data, without the video can't be displayed goes here: ---- */ - // if one of these is not available an exception is meant to be thrown directly - // into the frontend. + // Important data, without it the content can't be displayed. + // If one of these is not available, the frontend will receive an exception directly. + final int serviceId = extractor.getServiceId(); final String url = extractor.getUrl(); + final String originalUrl = extractor.getOriginalUrl(); final StreamType streamType = extractor.getStreamType(); final String id = extractor.getId(); final String name = extractor.getName(); final int ageLimit = extractor.getAgeLimit(); - // suppress always-non-null warning as here we double-check it really is not null + // Suppress always-non-null warning as here we double-check it is really not null //noinspection ConstantConditions - if (streamType == StreamType.NONE || isNullOrEmpty(url) || isNullOrEmpty(id) - || name == null /* but it can be empty of course */ || ageLimit == -1) { + if (streamType == StreamType.NONE + || isNullOrEmpty(url) + || isNullOrEmpty(id) + || name == null /* but it can be empty of course */ + || ageLimit == -1) { throw new ExtractionException("Some important stream information was not given."); } @@ -125,16 +128,18 @@ public class StreamInfo extends Info { streamType, id, name, ageLimit); } - private static void extractStreams(final StreamInfo streamInfo, final StreamExtractor extractor) + + private static void extractStreams(final StreamInfo streamInfo, + final StreamExtractor extractor) throws ExtractionException { - /* ---- stream extraction goes here ---- */ - // At least one type of stream has to be available, - // otherwise an exception will be thrown directly into the frontend. + /* ---- Stream extraction goes here ---- */ + // At least one type of stream has to be available, otherwise an exception will be thrown + // directly into the frontend. try { streamInfo.setDashMpdUrl(extractor.getDashMpdUrl()); } catch (final Exception e) { - streamInfo.addError(new ExtractionException("Couldn't get Dash manifest", e)); + streamInfo.addError(new ExtractionException("Couldn't get DASH manifest", e)); } try { @@ -151,12 +156,14 @@ public class StreamInfo extends Info { } catch (final Exception e) { streamInfo.addError(new ExtractionException("Couldn't get audio streams", e)); } + /* Extract video stream url */ try { streamInfo.setVideoStreams(extractor.getVideoStreams()); } catch (final Exception e) { streamInfo.addError(new ExtractionException("Couldn't get video streams", e)); } + /* Extract video only stream url */ try { streamInfo.setVideoOnlyStreams(extractor.getVideoOnlyStreams()); @@ -164,7 +171,7 @@ public class StreamInfo extends Info { streamInfo.addError(new ExtractionException("Couldn't get video only streams", e)); } - // Lists can be null if a exception was thrown during extraction + // Lists can be null if an exception was thrown during extraction if (streamInfo.getVideoStreams() == null) { streamInfo.setVideoStreams(Collections.emptyList()); } @@ -175,37 +182,9 @@ public class StreamInfo extends Info { streamInfo.setAudioStreams(Collections.emptyList()); } - Exception dashMpdError = null; - if (!isNullOrEmpty(streamInfo.getDashMpdUrl())) { - try { - final DashMpdParser.ParserResult result = DashMpdParser.getStreams(streamInfo); - streamInfo.getVideoOnlyStreams().addAll(result.getVideoOnlyStreams()); - streamInfo.getAudioStreams().addAll(result.getAudioStreams()); - streamInfo.getVideoStreams().addAll(result.getVideoStreams()); - streamInfo.segmentedVideoOnlyStreams = result.getSegmentedVideoOnlyStreams(); - streamInfo.segmentedAudioStreams = result.getSegmentedAudioStreams(); - streamInfo.segmentedVideoStreams = result.getSegmentedVideoStreams(); - } catch (final Exception e) { - // Sometimes we receive 403 (forbidden) error when trying to download the - // manifest (similar to what happens with youtube-dl), - // just skip the exception (but store it somewhere), as we later check if we - // have streams anyway. - dashMpdError = e; - } - } - - // Either audio or video has to be available, otherwise we didn't get a stream - // (since videoOnly are optional, they don't count). + // Either audio or video has to be available, otherwise we didn't get a stream (since + // videoOnly are optional, they don't count). if ((streamInfo.videoStreams.isEmpty()) && (streamInfo.audioStreams.isEmpty())) { - - if (dashMpdError != null) { - // If we don't have any video or audio and the dashMpd 'errored', add it to the - // error list - // (it's optional and it don't get added automatically, but it's good to have - // some additional error context) - streamInfo.addError(dashMpdError); - } - throw new StreamExtractException( "Could not get any stream. See error variable to get further details."); } @@ -214,11 +193,9 @@ public class StreamInfo extends Info { @SuppressWarnings("MethodLength") private static void extractOptionalData(final StreamInfo streamInfo, final StreamExtractor extractor) { - /* ---- optional data goes here: ---- */ - // If one of these fails, the frontend needs to handle that they are not - // available. - // Exceptions are therefore not thrown into the frontend, but stored into the - // error List, + /* ---- Optional data goes here: ---- */ + // If one of these fails, the frontend needs to handle that they are not available. + // Exceptions are therefore not thrown into the frontend, but stored into the error list, // so the frontend can afterwards check where errors happened. try { @@ -314,7 +291,7 @@ public class StreamInfo extends Info { streamInfo.addError(e); } - //additional info + // Additional info try { streamInfo.setHost(extractor.getHost()); } catch (final Exception e) { @@ -360,15 +337,14 @@ public class StreamInfo extends Info { } catch (final Exception e) { streamInfo.addError(e); } - try { streamInfo.setPreviewFrames(extractor.getFrames()); } catch (final Exception e) { streamInfo.addError(e); } - streamInfo - .setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo, extractor)); + streamInfo.setRelatedItems(ExtractorHelper.getRelatedItemsOrLogError(streamInfo, + extractor)); } private StreamType streamType; @@ -398,11 +374,6 @@ public class StreamInfo extends Info { private List videoOnlyStreams = new ArrayList<>(); private String dashMpdUrl = ""; - private List segmentedVideoStreams = new ArrayList<>(); - private List segmentedAudioStreams = new ArrayList<>(); - private List segmentedVideoOnlyStreams = new ArrayList<>(); - - private String hlsUrl = ""; private List relatedItems = new ArrayList<>(); @@ -625,30 +596,6 @@ public class StreamInfo extends Info { this.dashMpdUrl = dashMpdUrl; } - public List getSegmentedVideoStreams() { - return segmentedVideoStreams; - } - - public void setSegmentedVideoStreams(final List segmentedVideoStreams) { - this.segmentedVideoStreams = segmentedVideoStreams; - } - - public List getSegmentedAudioStreams() { - return segmentedAudioStreams; - } - - public void setSegmentedAudioStreams(final List segmentedAudioStreams) { - this.segmentedAudioStreams = segmentedAudioStreams; - } - - public List getSegmentedVideoOnlyStreams() { - return segmentedVideoOnlyStreams; - } - - public void setSegmentedVideoOnlyStreams(final List segmentedVideoOnlyStreams) { - this.segmentedVideoOnlyStreams = segmentedVideoOnlyStreams; - } - public String getHlsUrl() { return hlsUrl; } From 7c67d46e09a9e7c0125567449b3e3ede83a355c7 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 3 Mar 2022 09:57:28 +0100 Subject: [PATCH 03/38] Move DashMpdParser to the YouTube package and fix extraction of streams DashMpdParser is only working with YouTube streams, as it uses the ItagItem class. Also update creation of AudioStreams and VideoStreams objects. --- .../services/youtube/DashMpdParser.java | 220 +++++++++++++++++ .../extractor/utils/DashMpdParser.java | 225 ------------------ 2 files changed, 220 insertions(+), 225 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/utils/DashMpdParser.java 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 new file mode 100644 index 000000000..77cee35bb --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java @@ -0,0 +1,220 @@ +/* + * Created by Christian Schabesberger on 02.02.16. + * + * Copyright (C) Christian Schabesberger 2016 + * DashMpdParser.java is part of NewPipe Extractor. + * + * NewPipe Extractor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe Extractor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe Extractor. If not, see . + */ + +package org.schabi.newpipe.extractor.services.youtube; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.downloader.Downloader; +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; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +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; + +public final class DashMpdParser { + private DashMpdParser() { + } + + public static class DashMpdParsingException extends ParsingException { + + DashMpdParsingException(final String message, final Exception e) { + super(message, e); + } + } + + public static class Result { + private final List videoStreams; + private final List videoOnlyStreams; + private final List audioStreams; + + + public Result(final List videoStreams, + final List videoOnlyStreams, + final List audioStreams) { + this.videoStreams = videoStreams; + this.videoOnlyStreams = videoOnlyStreams; + this.audioStreams = audioStreams; + } + + public List getVideoStreams() { + return videoStreams; + } + + public List getVideoOnlyStreams() { + return videoOnlyStreams; + } + + public List getAudioStreams() { + return audioStreams; + } + } + + // TODO: Make this class generic and decouple from YouTube's ItagItem class. + + /** + * Will try to download and parse the DASH manifest (using {@link StreamInfo#getDashMpdUrl()}), + * adding items that are listed in the {@link ItagItem} class. + *

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

+ * Info about DASH MPD can be found here + * + * @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 videoStreams = new ArrayList<>(); + final List audioStreams = new ArrayList<>(); + final List videoOnlyStreams = new ArrayList<>(); + + for (int i = 0; i < representationList.getLength(); i++) { + final Element representation = (Element) representationList.item(i); + try { + final String mimeType = ((Element) representation.getParentNode()) + .getAttribute("mimeType"); + final String id = representation.getAttribute("id"); + final ItagItem itag = ItagItem.getItag(Integer.parseInt(id)); + final Element segmentationList = (Element) representation + .getElementsByTagName("SegmentList").item(0); + + if (segmentationList == null) { + continue; + } + + final MediaFormat mediaFormat = MediaFormat.getFromMimeType(mimeType); + + if (itag.itagType.equals(ItagItem.ItagType.AUDIO)) { + audioStreams.add(new AudioStream.Builder() + .setId(String.valueOf(itag.id)) + .setContent(manualDashFromRepresentation(doc, representation), + false) + .setMediaFormat(mediaFormat) + .setDeliveryMethod(DeliveryMethod.DASH) + .setAverageBitrate(itag.getAverageBitrate()) + .setBaseUrl(dashMpdUrl) + .setItagItem(itag) + .build()); + } else { + final boolean isVideoOnly = itag.itagType == ItagItem.ItagType.VIDEO_ONLY; + final VideoStream videoStream = new VideoStream.Builder() + .setId(String.valueOf(itag.id)) + .setContent(manualDashFromRepresentation(doc, representation), + false) + .setMediaFormat(mediaFormat) + .setDeliveryMethod(DeliveryMethod.DASH) + .setResolution(Objects.requireNonNull(itag.getResolutionString())) + .setIsVideoOnly(isVideoOnly) + .setBaseUrl(dashMpdUrl) + .setItagItem(itag) + .build(); + if (isVideoOnly) { + videoOnlyStreams.add(videoStream); + } else { + videoStreams.add(videoStream); + } + } + } catch (final Exception ignored) { + } + } + return new Result(videoStreams, videoOnlyStreams, audioStreams); + } catch (final Exception e) { + throw new DashMpdParsingException("Could not parse DASH MPD", e); + } + } + + @Nonnull + private static String manualDashFromRepresentation(@Nonnull final Document document, + @Nonnull final Element representation) + throws TransformerException { + final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); + + // Clone element so we can freely modify it + final Element adaptationSet = (Element) representation.getParentNode(); + final Element adaptationSetClone = (Element) adaptationSet.cloneNode(true); + + // Remove other representations from the adaptation set + final NodeList representations = adaptationSetClone.getElementsByTagName("Representation"); + for (int i = representations.getLength() - 1; i >= 0; i--) { + final Node item = representations.item(i); + if (!item.isEqualNode(representation)) { + adaptationSetClone.removeChild(item); + } + } + + final Element newMpdRootElement = (Element) mpdElement.cloneNode(false); + final Element periodElement = newMpdRootElement.getOwnerDocument().createElement("Period"); + periodElement.appendChild(adaptationSetClone); + newMpdRootElement.appendChild(periodElement); + + return nodeToString(newMpdRootElement); + } + + private static String nodeToString(final Node node) throws TransformerException { + final StringWriter result = new StringWriter(); + final Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.transform(new DOMSource(node), new StreamResult(result)); + return result.toString(); + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/DashMpdParser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/utils/DashMpdParser.java deleted file mode 100644 index b1acabc75..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/utils/DashMpdParser.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.schabi.newpipe.extractor.utils; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.downloader.Downloader; -import org.schabi.newpipe.extractor.exceptions.ParsingException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.services.youtube.ItagItem; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.Stream; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -/* - * Created by Christian Schabesberger on 02.02.16. - * - * Copyright (C) Christian Schabesberger 2016 - * DashMpdParser.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - -public final class DashMpdParser { - - private DashMpdParser() { - } - - public static class DashMpdParsingException extends ParsingException { - DashMpdParsingException(final String message, final Exception e) { - super(message, e); - } - } - - public static class ParserResult { - private final List videoStreams; - private final List audioStreams; - private final List videoOnlyStreams; - - private final List segmentedVideoStreams; - private final List segmentedAudioStreams; - private final List segmentedVideoOnlyStreams; - - - public ParserResult(final List videoStreams, - final List audioStreams, - final List videoOnlyStreams, - final List segmentedVideoStreams, - final List segmentedAudioStreams, - final List segmentedVideoOnlyStreams) { - this.videoStreams = videoStreams; - this.audioStreams = audioStreams; - this.videoOnlyStreams = videoOnlyStreams; - this.segmentedVideoStreams = segmentedVideoStreams; - this.segmentedAudioStreams = segmentedAudioStreams; - this.segmentedVideoOnlyStreams = segmentedVideoOnlyStreams; - } - - public List getVideoStreams() { - return videoStreams; - } - - public List getAudioStreams() { - return audioStreams; - } - - public List getVideoOnlyStreams() { - return videoOnlyStreams; - } - - public List getSegmentedVideoStreams() { - return segmentedVideoStreams; - } - - public List getSegmentedAudioStreams() { - return segmentedAudioStreams; - } - - public List getSegmentedVideoOnlyStreams() { - return segmentedVideoOnlyStreams; - } - } - - /** - * Will try to download (using {@link StreamInfo#getDashMpdUrl()}) and parse the dash manifest, - * then it will search for any stream that the ItagItem has (by the id). - *

- * 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 videoStreams = new ArrayList<>(); - final List audioStreams = new ArrayList<>(); - final List videoOnlyStreams = new ArrayList<>(); - - final List segmentedVideoStreams = new ArrayList<>(); - final List segmentedAudioStreams = new ArrayList<>(); - final List segmentedVideoOnlyStreams = new ArrayList<>(); - - for (int i = 0; i < representationList.getLength(); i++) { - final Element representation = (Element) representationList.item(i); - try { - final String mimeType - = ((Element) representation.getParentNode()).getAttribute("mimeType"); - final String id = representation.getAttribute("id"); - final String url = representation - .getElementsByTagName("BaseURL").item(0).getTextContent(); - final ItagItem itag = ItagItem.getItag(Integer.parseInt(id)); - final Node segmentationList - = representation.getElementsByTagName("SegmentList").item(0); - - // If SegmentList is not null this means that BaseUrl is not representing the - // url to the stream. Instead we need to add the "media=" value from the - // tags inside the tag in order to get a full - // working url. However each of these is just pointing to a part of the video, - // so we can not return a URL with a working stream here. Instead of putting - // those streams into the list of regular stream urls we put them in a for - // example "segmentedVideoStreams" list. - - final MediaFormat mediaFormat = MediaFormat.getFromMimeType(mimeType); - - if (itag.itagType.equals(ItagItem.ItagType.AUDIO)) { - if (segmentationList == null) { - final AudioStream audioStream - = new AudioStream(url, mediaFormat, itag.avgBitrate); - if (!Stream.containSimilarStream(audioStream, - streamInfo.getAudioStreams())) { - audioStreams.add(audioStream); - } - } else { - segmentedAudioStreams.add( - new AudioStream(id, mediaFormat, itag.avgBitrate)); - } - } else { - final boolean isVideoOnly - = itag.itagType.equals(ItagItem.ItagType.VIDEO_ONLY); - - if (segmentationList == null) { - final VideoStream videoStream = new VideoStream(url, - mediaFormat, - itag.resolutionString, - isVideoOnly); - - if (isVideoOnly) { - if (!Stream.containSimilarStream(videoStream, - streamInfo.getVideoOnlyStreams())) { - videoOnlyStreams.add(videoStream); - } - } else if (!Stream.containSimilarStream(videoStream, - streamInfo.getVideoStreams())) { - videoStreams.add(videoStream); - } - } else { - final VideoStream videoStream = new VideoStream(id, - mediaFormat, - itag.resolutionString, - isVideoOnly); - - if (isVideoOnly) { - segmentedVideoOnlyStreams.add(videoStream); - } else { - segmentedVideoStreams.add(videoStream); - } - } - } - } catch (final Exception ignored) { - } - } - return new ParserResult( - videoStreams, - audioStreams, - videoOnlyStreams, - segmentedVideoStreams, - segmentedAudioStreams, - segmentedVideoOnlyStreams); - } catch (final Exception e) { - throw new DashMpdParsingException("Could not parse Dash mpd", e); - } - } -} From d5f3637fc378c2c9978c13e4c5bfc2ff73f26af6 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 3 Mar 2022 11:05:52 +0100 Subject: [PATCH 04/38] [YouTube] Return more values returned inside the ItagItems of the player response and deprecate use of public audio and video fields These fields can be now replaced by a getter and a setter. New fields have been added and will allow the creation of DASH manifests for OTF and ended livestreams. There are: - contentLength; - approxDurationMs; - targetDurationSec; - sampleRate; - audioChannels. --- .../extractor/services/youtube/ItagItem.java | 303 ++++++++++++++++-- 1 file changed, 285 insertions(+), 18 deletions(-) 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 55d3082a2..79f44078f 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 @@ -14,16 +14,20 @@ import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.exceptions.ParsingException; import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.io.Serializable; + +public class ItagItem implements Serializable { -public class ItagItem { /** - * List can be found here - * https://github.com/ytdl-org/youtube-dl/blob/9fc5eaf/youtube_dl/extractor/youtube.py#L1071 + * List can be found here: + * https://github.com/ytdl-org/youtube-dl/blob/e988fa4/youtube_dl/extractor/youtube.py#L1195 */ private static final ItagItem[] ITAG_LIST = { ///////////////////////////////////////////////////// - // VIDEO ID Type Format Resolution FPS /// - /////////////////////////////////////////////////// + // VIDEO ID Type Format Resolution FPS //// + ///////////////////////////////////////////////////// new ItagItem(17, VIDEO, v3GPP, "144p"), new ItagItem(36, VIDEO, v3GPP, "240p"), @@ -41,8 +45,8 @@ public class ItagItem { new ItagItem(45, VIDEO, WEBM, "720p"), new ItagItem(46, VIDEO, WEBM, "1080p"), - //////////////////////////////////////////////////////////////////// - // AUDIO ID ItagType Format Bitrate /// + ////////////////////////////////////////////////////////////////// + // AUDIO ID ItagType Format Bitrate // ////////////////////////////////////////////////////////////////// new ItagItem(171, AUDIO, WEBMA, 128), new ItagItem(172, AUDIO, WEBMA, 256), @@ -54,8 +58,8 @@ public class ItagItem { new ItagItem(251, AUDIO, WEBMA_OPUS, 160), /// VIDEO ONLY //////////////////////////////////////////// - // ID Type Format Resolution FPS /// - ///////////////////////////////////////////////////////// + // ID Type Format Resolution FPS //// + /////////////////////////////////////////////////////////// new ItagItem(160, VIDEO_ONLY, MPEG_4, "144p"), new ItagItem(133, VIDEO_ONLY, MPEG_4, "240p"), new ItagItem(134, VIDEO_ONLY, MPEG_4, "360p"), @@ -105,11 +109,23 @@ public class ItagItem { return item; } } - throw new ParsingException("itag=" + itagId + " not supported"); + throw new ParsingException("itag " + itagId + " is not supported"); } /*////////////////////////////////////////////////////////////////////////// - // Contructors and misc + // Static constants + //////////////////////////////////////////////////////////////////////////*/ + + public static final int AVERAGE_BITRATE_UNKNOWN = -1; + public static final int SAMPLE_RATE_UNKNOWN = -1; + public static final int FPS_NOT_APPLICABLE_OR_UNKNOWN = -1; + public static final int TARGET_DURATION_SEC_UNKNOWN = -1; + public static final int AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN = -1; + public static final long CONTENT_LENGTH_UNKNOWN = -1; + public static final long APPROX_DURATION_MS_UNKNOWN = -1; + + /*////////////////////////////////////////////////////////////////////////// + // Constructors and misc //////////////////////////////////////////////////////////////////////////*/ public enum ItagType { @@ -134,8 +150,6 @@ public class ItagItem { /** * Constructor for videos. - * - * @param resolution string that will be used in the frontend */ public ItagItem(final int id, final ItagType type, @@ -159,22 +173,30 @@ public class ItagItem { this.avgBitrate = avgBitrate; } - private final MediaFormat mediaFormat; - - public MediaFormat getMediaFormat() { return mediaFormat; } + private final MediaFormat mediaFormat; + public final int id; public final ItagType itagType; // Audio fields - public int avgBitrate = -1; + /** @deprecated Use {@link #getAverageBitrate()} instead. */ + @Deprecated + public int avgBitrate = AVERAGE_BITRATE_UNKNOWN; + private int sampleRate = SAMPLE_RATE_UNKNOWN; + private int audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN; // Video fields + /** @deprecated Use {@link #getResolutionString()} instead. */ + @Deprecated public String resolutionString; - public int fps = -1; + + /** @deprecated Use {@link #getFps()} and {@link #setFps(int)} instead. */ + @Deprecated + public int fps = FPS_NOT_APPLICABLE_OR_UNKNOWN; // Fields for Dash private int bitrate; @@ -186,6 +208,9 @@ public class ItagItem { private int indexEnd; private String quality; private String codec; + private int targetDurationSec = TARGET_DURATION_SEC_UNKNOWN; + private long approxDurationMs = APPROX_DURATION_MS_UNKNOWN; + private long contentLength = CONTENT_LENGTH_UNKNOWN; public int getBitrate() { return bitrate; @@ -211,6 +236,43 @@ public class ItagItem { this.height = height; } + /** + * Get the frame rate per second. + * + *

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

+ * + * @return the frame rate per second or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} + */ + public int getFps() { + return fps; + } + + /** + * Set the frame rate per second. + * + *

+ * 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 + */ + public void setFps(final int fps) { + if (fps > 0) { + this.fps = fps; + } else { + this.fps = FPS_NOT_APPLICABLE_OR_UNKNOWN; + } + } + public int getInitStart() { return initStart; } @@ -251,6 +313,21 @@ public class ItagItem { this.quality = quality; } + /** + * Get the resolution string associated to this {@code ItagItem}. + * + *

+ * It is only known for video itags. + *

+ * + * @return the resolution string associated to this {@code ItagItem} or + * {@code null}. + */ + @Nullable + public String getResolutionString() { + return resolutionString; + } + public String getCodec() { return codec; } @@ -258,4 +335,194 @@ public class ItagItem { public void setCodec(final String codec) { this.codec = codec; } + + /** + * Get the average bitrate. + * + *

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

+ * + * @return the average bitrate or {@link #AVERAGE_BITRATE_UNKNOWN} + * @see #getBitrate() + */ + public int getAverageBitrate() { + return avgBitrate; + } + + /** + * Get the sample rate. + * + *

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

+ * + * @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN} + */ + public int getSampleRate() { + return sampleRate; + } + + /** + * 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. + *

+ * + * @param sampleRate the sample rate of an audio itag + */ + public void setSampleRate(final int sampleRate) { + if (sampleRate > 0) { + this.sampleRate = sampleRate; + } else { + this.sampleRate = SAMPLE_RATE_UNKNOWN; + } + } + + /** + * 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. + *

+ * + * @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} + */ + public int getAudioChannels() { + return audioChannels; + } + + /** + * 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 + * 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; + } + } + + /** + * 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. + *

+ * + * @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN} + */ + public int getTargetDurationSec() { + return targetDurationSec; + } + + /** + * Set 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} 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; + } + } + + /** + * Get the {@code approxDurationMs} value. + * + *

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

+ * + * @return the {@code approxDurationMs} value or {@link #APPROX_DURATION_MS_UNKNOWN} + */ + public long getApproxDurationMs() { + return approxDurationMs; + } + + /** + * Set the {@code approxDurationMs} value. + * + *

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

+ * + * @param approxDurationMs the approximate duration of a DASH progressive stream, in + * milliseconds + */ + public void setApproxDurationMs(final long approxDurationMs) { + if (approxDurationMs > 0) { + this.approxDurationMs = approxDurationMs; + } else { + this.approxDurationMs = APPROX_DURATION_MS_UNKNOWN; + } + } + + /** + * Get the {@code contentLength} value. + * + *

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

+ * + * @return the {@code contentLength} value or {@link #CONTENT_LENGTH_UNKNOWN} + */ + public long getContentLength() { + return contentLength; + } + + /** + * Set the content length of stream. + * + *

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

+ * + * @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; + } + } } From 881969f1da6da0e123e3aa51c80351bad34ceb6a Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Thu, 3 Mar 2022 20:46:53 +0100 Subject: [PATCH 05/38] Apply changes in all StreamExtractors except YouTube's one and fix extraction of PeerTube audio streams as video streams Some code in these classes has been also refactored/improved/optimized. Also fix the extraction of PeerTube audio streams as video streams, which are now returned as audio streams. --- .../BandcampRadioStreamExtractor.java | 44 +- .../extractors/BandcampStreamExtractor.java | 63 ++- .../MediaCCCLiveStreamExtractor.java | 175 ++++++-- .../extractors/MediaCCCStreamExtractor.java | 37 +- .../extractors/PeertubeStreamExtractor.java | 409 ++++++++++++++---- .../extractors/SoundcloudStreamExtractor.java | 136 ++++-- 6 files changed, 629 insertions(+), 235 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java index 639d2abb8..7389d4238 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java @@ -5,7 +5,6 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; import org.jsoup.Jsoup; -import org.jsoup.nodes.Element; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -30,9 +29,12 @@ import java.util.List; import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL; import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL; import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { + private static final String OPUS_LO = "opus_lo"; + private static final String MP3_128 = "mp3-128"; private JsonObject showInfo; public BandcampRadioStreamExtractor(final StreamingService service, @@ -78,11 +80,9 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { @Nonnull @Override - public String getUploaderName() throws ParsingException { - return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream() - .map(Element::text) - .findFirst() - .orElseThrow(() -> new ParsingException("Could not get uploader name")); + public String getUploaderName() { + return Jsoup.parse(showInfo.getString("image_caption")) + .getElementsByTag("a").first().text(); } @Nullable @@ -116,23 +116,25 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { @Override public List getAudioStreams() { - final ArrayList list = new ArrayList<>(); + final List audioStreams = new ArrayList<>(); final JsonObject streams = showInfo.getObject("audio_stream"); - if (streams.has("opus-lo")) { - list.add(new AudioStream( - streams.getString("opus-lo"), - MediaFormat.OPUS, 100 - )); - } - if (streams.has("mp3-128")) { - list.add(new AudioStream( - streams.getString("mp3-128"), - MediaFormat.MP3, 128 - )); + if (streams.has(MP3_128)) { + audioStreams.add(new AudioStream.Builder() + .setId(MP3_128) + .setContent(streams.getString(MP3_128), true) + .setMediaFormat(MediaFormat.MP3) + .setAverageBitrate(128) + .build()); + } else if (streams.has(OPUS_LO)) { + audioStreams.add(new AudioStream.Builder() + .setId(OPUS_LO) + .setContent(streams.getString(OPUS_LO), true) + .setMediaFormat(MediaFormat.OPUS) + .setAverageBitrate(100).build()); } - return list; + return audioStreams; } @Nonnull @@ -156,14 +158,14 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { @Override public String getLicence() { // Contrary to other Bandcamp streams, radio streams don't have a license - return ""; + return EMPTY_STRING; } @Nonnull @Override public String getCategory() { // Contrary to other Bandcamp streams, radio streams don't have categories - return ""; + return EMPTY_STRING; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index 896644e96..9bb6d5c78 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -3,6 +3,8 @@ package org.schabi.newpipe.extractor.services.bandcamp.extractors; import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; +import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParserException; @@ -10,7 +12,6 @@ import com.grack.nanojson.JsonParserException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.downloader.Downloader; @@ -27,16 +28,15 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.Utils; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import java.util.stream.Collectors; public class BandcampStreamExtractor extends StreamExtractor { - private JsonObject albumJson; private JsonObject current; private Document document; @@ -88,7 +88,7 @@ public class BandcampStreamExtractor extends StreamExtractor { public String getUploaderUrl() throws ParsingException { final String[] parts = getUrl().split("/"); // https: (/) (/) * .bandcamp.com (/) and leave out the rest - return "https://" + parts[2] + "/"; + return HTTPS + parts[2] + "/"; } @Nonnull @@ -119,7 +119,7 @@ public class BandcampStreamExtractor extends StreamExtractor { @Override public String getThumbnailUrl() throws ParsingException { if (albumJson.isNull("art_id")) { - return Utils.EMPTY_STRING; + return EMPTY_STRING; } else { return getImageUrl(albumJson.getLong("art_id"), true); } @@ -139,24 +139,26 @@ public class BandcampStreamExtractor extends StreamExtractor { public Description getDescription() { final String s = Utils.nonEmptyAndNullJoin( "\n\n", - new String[]{ + new String[] { current.getString("about"), current.getString("lyrics"), current.getString("credits") - } - ); + }); return new Description(s, Description.PLAIN_TEXT); } @Override public List getAudioStreams() { final List audioStreams = new ArrayList<>(); - - audioStreams.add(new AudioStream( - albumJson.getArray("trackinfo").getObject(0) - .getObject("file").getString("mp3-128"), - MediaFormat.MP3, 128 - )); + audioStreams.add(new AudioStream.Builder() + .setId("mp3-128") + .setContent(albumJson.getArray("trackinfo") + .getObject(0) + .getObject("file") + .getString("mp3-128"), true) + .setMediaFormat(MediaFormat.MP3) + .setAverageBitrate(128) + .build()); return audioStreams; } @@ -184,11 +186,11 @@ public class BandcampStreamExtractor extends StreamExtractor { @Override public PlaylistInfoItemsCollector getRelatedItems() { final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId()); - final Elements recommendedAlbums = document.getElementsByClass("recommended-album"); + document.getElementsByClass("recommended-album") + .stream() + .map(BandcampRelatedPlaylistInfoItemExtractor::new) + .forEach(collector::commit); - for (final Element album : recommendedAlbums) { - collector.commit(new BandcampRelatedPlaylistInfoItemExtractor(album)); - } return collector; } @@ -200,15 +202,17 @@ public class BandcampStreamExtractor extends StreamExtractor { .flatMap(element -> element.getElementsByClass("tag").stream()) .map(Element::text) .findFirst() - .orElse(""); + .orElse(EMPTY_STRING); } @Nonnull @Override public String getLicence() { - /* Tests resulted in this mapping of ints to licence: + /* + Tests resulted in this mapping of ints to licence: https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's - account) */ + account) + */ switch (current.getInt("license_type")) { case 1: @@ -233,14 +237,9 @@ public class BandcampStreamExtractor extends StreamExtractor { @Nonnull @Override public List getTags() { - final Elements tagElements = document.getElementsByAttributeValue("itemprop", "keywords"); - - final List tags = new ArrayList<>(); - - for (final Element e : tagElements) { - tags.add(e.text()); - } - - return tags; + return document.getElementsByAttributeValue("itemprop", "keywords") + .stream() + .map(Element::text) + .collect(Collectors.toList()); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 2a4eb45ed..54c2d056c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -10,6 +10,7 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; @@ -17,11 +18,21 @@ import org.schabi.newpipe.extractor.stream.VideoStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.IntStream; import javax.annotation.Nonnull; +import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; +import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; + public class MediaCCCLiveStreamExtractor extends StreamExtractor { + private static final String STREAMS = "streams"; + private static final String URLS = "urls"; + private static final String URL = "url"; + private JsonObject conference = null; private String group = ""; private JsonObject room = null; @@ -34,19 +45,22 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { @Override public void onFetchPage(@Nonnull final Downloader downloader) throws IOException, ExtractionException { - final JsonArray doc = - MediaCCCParsingHelper.getLiveStreams(downloader, getExtractorLocalization()); - // find correct room + final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader, + getExtractorLocalization()); + // Find the correct room for (int c = 0; c < doc.size(); c++) { - conference = doc.getObject(c); - final JsonArray groups = conference.getArray("groups"); + final JsonObject conferenceObject = doc.getObject(c); + final JsonArray groups = conferenceObject.getArray("groups"); for (int g = 0; g < groups.size(); g++) { - group = groups.getObject(g).getString("group"); + final String groupObject = groups.getObject(g).getString("group"); final JsonArray rooms = groups.getObject(g).getArray("rooms"); for (int r = 0; r < rooms.size(); r++) { - room = rooms.getObject(r); - if (getId().equals( - conference.getString("slug") + "/" + room.getString("slug"))) { + final JsonObject roomObject = rooms.getObject(r); + if (getId().equals(conferenceObject.getString("slug") + "/" + + roomObject.getString("slug"))) { + this.conference = conferenceObject; + this.group = groupObject; + this.room = roomObject; return; } } @@ -91,69 +105,136 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { return conference.getString("conference"); } + /** + * Get the URL of the first DASH stream found. + * + *

+ * There can be several DASH streams, so the URL of the first found is returned by this method. + *

+ * + *

+ * You can find the other video DASH 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; + } + + /** + * 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. + *

+ * + *

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

+ */ @Nonnull @Override public String getHlsUrl() { - // TODO: There are multiple HLS streams. - // Make getHlsUrl() and getDashMpdUrl() return lists of VideoStreams, - // so the user can choose a resolution. - for (int s = 0; s < room.getArray("streams").size(); s++) { - final JsonObject stream = room.getArray("streams").getObject(s); - if (stream.getString("type").equals("video")) { - if (stream.has("hls")) { - return stream.getObject("urls").getObject("hls").getString("url"); - } + 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 ""; + return EMPTY_STRING; } @Override public List getAudioStreams() throws IOException, ExtractionException { final List audioStreams = new ArrayList<>(); - for (int s = 0; s < room.getArray("streams").size(); s++) { - final JsonObject stream = room.getArray("streams").getObject(s); - if (stream.getString("type").equals("audio")) { - for (final String type : stream.getObject("urls").keySet()) { - final JsonObject url = stream.getObject("urls").getObject(type); - audioStreams.add(new AudioStream(url.getString("url"), - MediaFormat.getFromSuffix(type), -1)); - } - } - } + IntStream.range(0, room.getArray(STREAMS).size()) + .mapToObj(s -> room.getArray(STREAMS).getObject(s)) + .filter(streamJsonObject -> streamJsonObject.getString("type").equals("audio")) + .forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet() + .forEach(type -> { + final JsonObject urlObject = streamJsonObject.getObject(URLS) + .getObject(type); + // The DASH manifest will be extracted with getDashMpdUrl + if (!type.equals("dash")) { + final AudioStream.Builder builder = new AudioStream.Builder() + .setId(urlObject.getString("tech", ID_UNKNOWN)) + .setContent(urlObject.getString(URL), true) + .setAverageBitrate(UNKNOWN_BITRATE); + if (type.equals("hls")) { + // We don't know with the type string what media format will + // have HLS streams. + // However, the tech string may contain some information + // about the media format used. + builder.setDeliveryMethod(DeliveryMethod.HLS); + } else { + builder.setMediaFormat(MediaFormat.getFromSuffix(type)); + } + + audioStreams.add(builder.build()); + } + })); + return audioStreams; } @Override public List getVideoStreams() throws IOException, ExtractionException { final List videoStreams = new ArrayList<>(); - for (int s = 0; s < room.getArray("streams").size(); s++) { - final JsonObject stream = room.getArray("streams").getObject(s); - if (stream.getString("type").equals("video")) { - final String resolution = stream.getArray("videoSize").getInt(0) + "x" - + stream.getArray("videoSize").getInt(1); - for (final String type : stream.getObject("urls").keySet()) { - if (!type.equals("hls")) { - final JsonObject url = stream.getObject("urls").getObject(type); - videoStreams.add(new VideoStream( - url.getString("url"), - MediaFormat.getFromSuffix(type), - resolution)); - } - } - } - } + IntStream.range(0, room.getArray(STREAMS).size()) + .mapToObj(s -> room.getArray(STREAMS).getObject(s)) + .filter(stream -> stream.getString("type").equals("video")) + .forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet() + .forEach(type -> { + final String resolution = + streamJsonObject.getArray("videoSize").getInt(0) + + "x" + + streamJsonObject.getArray("videoSize").getInt(1); + final JsonObject urlObject = streamJsonObject.getObject(URLS) + .getObject(type); + // The DASH manifest will be extracted with getDashMpdUrl + if (!type.equals("dash")) { + final VideoStream.Builder builder = new VideoStream.Builder() + .setId(urlObject.getString("tech", ID_UNKNOWN)) + .setContent(urlObject.getString(URL), true) + .setIsVideoOnly(false) + .setResolution(resolution); + + if (type.equals("hls")) { + // We don't know with the type string what media format will + // have HLS streams. + // However, the tech string may contain some information + // about the media format used. + builder.setDeliveryMethod(DeliveryMethod.HLS); + } else { + builder.setMediaFormat(MediaFormat.getFromSuffix(type)); + } + + videoStreams.add(builder.build()); + } + })); + return videoStreams; } @Override public List getVideoOnlyStreams() { - return null; + return Collections.emptyList(); } @Override public StreamType getStreamType() throws ParsingException { - return StreamType.LIVE_STREAM; // TODO: video and audio only streams are both available + return StreamType.LIVE_STREAM; } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 64a268971..53cc53ad0 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -1,5 +1,8 @@ package org.schabi.newpipe.extractor.services.media_ccc.extractors; +import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; +import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; + import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; @@ -99,7 +102,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor { final JsonObject recording = recordings.getObject(i); final String mimeType = recording.getString("mime_type"); if (mimeType.startsWith("audio")) { - //first we need to resolve the actual video data from CDN + // First we need to resolve the actual video data from CDN final MediaFormat mediaFormat; if (mimeType.endsWith("opus")) { mediaFormat = MediaFormat.OPUS; @@ -108,11 +111,18 @@ public class MediaCCCStreamExtractor extends StreamExtractor { } else if (mimeType.endsWith("ogg")) { mediaFormat = MediaFormat.OGG; } else { - throw new ExtractionException("Unknown media format: " + mimeType); + mediaFormat = null; } - audioStreams.add(new AudioStream(recording.getString("recording_url"), - mediaFormat, -1)); + // Don't use the containsSimilarStream method because it will always return + // false so if there are multiples audio streams available, only the first will + // be extracted in this case. + audioStreams.add(new AudioStream.Builder() + .setId(recording.getString("filename", ID_UNKNOWN)) + .setContent(recording.getString("recording_url"), true) + .setMediaFormat(mediaFormat) + .setAverageBitrate(UNKNOWN_BITRATE) + .build()); } } return audioStreams; @@ -126,7 +136,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor { final JsonObject recording = recordings.getObject(i); final String mimeType = recording.getString("mime_type"); if (mimeType.startsWith("video")) { - //first we need to resolve the actual video data from CDN + // First we need to resolve the actual video data from CDN final MediaFormat mediaFormat; if (mimeType.endsWith("webm")) { @@ -134,13 +144,21 @@ public class MediaCCCStreamExtractor extends StreamExtractor { } else if (mimeType.endsWith("mp4")) { mediaFormat = MediaFormat.MPEG_4; } else { - throw new ExtractionException("Unknown media format: " + mimeType); + mediaFormat = null; } - videoStreams.add(new VideoStream(recording.getString("recording_url"), - mediaFormat, recording.getInt("height") + "p")); + // Don't use the containsSimilarStream method because it will remove the + // extraction of some video versions (mostly languages) + videoStreams.add(new VideoStream.Builder() + .setId(recording.getString("filename", ID_UNKNOWN)) + .setContent(recording.getString("recording_url"), true) + .setIsVideoOnly(false) + .setMediaFormat(mediaFormat) + .setResolution(recording.getInt("height") + "p") + .build()); } } + return videoStreams; } @@ -163,7 +181,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor { conferenceData = JsonParser.object() .from(downloader.get(data.getString("conference_url")).responseBody()); } catch (final JsonParserException jpe) { - throw new ExtractionException("Could not parse json returned by url: " + videoUrl, jpe); + throw new ExtractionException("Could not parse json returned by URL: " + videoUrl, + jpe); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index f80815d10..bec41f481 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -22,6 +22,7 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeStreamLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; @@ -39,14 +40,30 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; +import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; + public class PeertubeStreamExtractor extends StreamExtractor { + private static final String ACCOUNT_HOST = "account.host"; + private static final String ACCOUNT_NAME = "account.name"; + private static final String FILES = "files"; + private static final String FILE_DOWNLOAD_URL = "fileDownloadUrl"; + private static final String FILE_URL = "fileUrl"; + private static final String PLAYLIST_URL = "playlistUrl"; + private static final String RESOLUTION_ID = "resolution.id"; + private static final String STREAMING_PLAYLISTS = "streamingPlaylists"; + private final String baseUrl; private JsonObject json; + private final List subtitles = new ArrayList<>(); + private final List audioStreams = new ArrayList<>(); + private final List videoStreams = new ArrayList<>(); public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler) throws ParsingException { @@ -85,9 +102,8 @@ public class PeertubeStreamExtractor extends StreamExtractor { } catch (final ParsingException e) { return Description.EMPTY_DESCRIPTION; } - if (text.length() == 250 && text.substring(247).equals("...")) { - //if description is shortened, get full description + // If description is shortened, get full description final Downloader dl = NewPipe.getDownloader(); try { final Response response = dl.get(baseUrl @@ -95,8 +111,8 @@ public class PeertubeStreamExtractor extends StreamExtractor { + getId() + "/description"); final JsonObject jsonObject = JsonParser.object().from(response.responseBody()); text = JsonUtils.getString(jsonObject, "description"); - } catch (ReCaptchaException | IOException | JsonParserException e) { - e.printStackTrace(); + } catch (final IOException | ReCaptchaException | JsonParserException ignored) { + // Something went wrong when getting the full description, use the shortened one } } return new Description(text, Description.MARKDOWN); @@ -119,8 +135,8 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Override public long getTimeStamp() throws ParsingException { - final long timestamp = - getTimestampSeconds("((#|&|\\?)start=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)"); + final long timestamp = getTimestampSeconds( + "((#|&|\\?)start=\\d{0,3}h?\\d{0,3}m?\\d{1,3}s?)"); if (timestamp == -2) { // regex for timestamp was not found @@ -148,10 +164,10 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Nonnull @Override public String getUploaderUrl() throws ParsingException { - final String name = JsonUtils.getString(json, "account.name"); - final String host = JsonUtils.getString(json, "account.host"); - return getService().getChannelLHFactory() - .fromId("accounts/" + name + "@" + host, baseUrl).getUrl(); + final String name = JsonUtils.getString(json, ACCOUNT_NAME); + final String host = JsonUtils.getString(json, ACCOUNT_HOST); + return getService().getChannelLHFactory().fromId("accounts/" + name + "@" + host, baseUrl) + .getUrl(); } @Nonnull @@ -199,77 +215,51 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Nonnull @Override public String getHlsUrl() { - return json.getArray("streamingPlaylists").getObject(0).getString("playlistUrl"); + assertPageFetched(); + + if (getStreamType() == StreamType.VIDEO_STREAM + && !isNullOrEmpty(json.getObject(FILES))) { + return json.getObject(FILES).getString(PLAYLIST_URL, EMPTY_STRING); + } else { + return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL, + EMPTY_STRING); + } } @Override - public List getAudioStreams() { - return Collections.emptyList(); + public List getAudioStreams() throws ParsingException { + assertPageFetched(); + + /* + Some videos have audio streams, some videos don't have audio streams. + So an audio stream may be available if a video stream is available. + Audio streams are also not returned as separated streams for livestreams. + That's why the extraction of audio streams is only run when there are video streams + extracted and when the content is not a livestream. + */ + if (audioStreams.isEmpty() && videoStreams.isEmpty() + && getStreamType() == StreamType.VIDEO_STREAM) { + getStreams(); + } + + return audioStreams; } @Override public List getVideoStreams() throws ExtractionException { assertPageFetched(); - final List videoStreams = new ArrayList<>(); - // mp4 - try { - videoStreams.addAll(getVideoStreamsFromArray(json.getArray("files"))); - } catch (final Exception ignored) { } - - // HLS - try { - final JsonArray streamingPlaylists = json.getArray("streamingPlaylists"); - for (final Object p : streamingPlaylists) { - if (!(p instanceof JsonObject)) { - continue; - } - final JsonObject playlist = (JsonObject) p; - videoStreams.addAll(getVideoStreamsFromArray(playlist.getArray("files"))); + if (videoStreams.isEmpty()) { + if (getStreamType() == StreamType.VIDEO_STREAM) { + getStreams(); + } else { + extractLiveVideoStreams(); } - } catch (final Exception e) { - throw new ParsingException("Could not get video streams", e); - } - - if (getStreamType() == StreamType.LIVE_STREAM) { - videoStreams.add(new VideoStream(getHlsUrl(), MediaFormat.MPEG_4, "720p")); } return videoStreams; } - private List getVideoStreamsFromArray(final JsonArray streams) - throws ParsingException { - try { - final List videoStreams = new ArrayList<>(); - for (final Object s : streams) { - if (!(s instanceof JsonObject)) { - continue; - } - final JsonObject stream = (JsonObject) s; - final String url; - if (stream.has("fileDownloadUrl")) { - url = JsonUtils.getString(stream, "fileDownloadUrl"); - } else { - url = JsonUtils.getString(stream, "fileUrl"); - } - final String torrentUrl = JsonUtils.getString(stream, "torrentUrl"); - final String resolution = JsonUtils.getString(stream, "resolution.label"); - final String extension = url.substring(url.lastIndexOf(".") + 1); - final MediaFormat format = MediaFormat.getFromSuffix(extension); - final VideoStream videoStream - = new VideoStream(url, torrentUrl, format, resolution); - if (!Stream.containSimilarStream(videoStream, videoStreams)) { - videoStreams.add(videoStream); - } - } - return videoStreams; - } catch (final Exception e) { - throw new ParsingException("Could not get video streams from array"); - } - - } - @Override public List getVideoOnlyStreams() { return Collections.emptyList(); @@ -284,13 +274,9 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Nonnull @Override public List getSubtitles(final MediaFormat format) { - final List filteredSubs = new ArrayList<>(); - for (final SubtitlesStream sub : subtitles) { - if (sub.getFormat() == format) { - filteredSubs.add(sub); - } - } - return filteredSubs; + return subtitles.stream() + .filter(sub -> sub.getFormat() == format) + .collect(Collectors.toList()); } @Override @@ -304,8 +290,8 @@ public class PeertubeStreamExtractor extends StreamExtractor { final List tags = getTags(); final String apiUrl; if (tags.isEmpty()) { - apiUrl = baseUrl + "/api/v1/accounts/" + JsonUtils.getString(json, "account.name") - + "@" + JsonUtils.getString(json, "account.host") + apiUrl = baseUrl + "/api/v1/accounts/" + JsonUtils.getString(json, ACCOUNT_NAME) + + "@" + JsonUtils.getString(json, ACCOUNT_HOST) + "/videos?start=0&count=8"; } else { apiUrl = getRelatedItemsUrl(tags); @@ -314,7 +300,8 @@ public class PeertubeStreamExtractor extends StreamExtractor { if (Utils.isBlank(apiUrl)) { return null; } else { - final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); + final StreamInfoItemsCollector collector = new StreamInfoItemsCollector( + getServiceId()); getStreamsFromApi(collector, apiUrl); return collector; } @@ -332,11 +319,13 @@ public class PeertubeStreamExtractor extends StreamExtractor { try { return JsonUtils.getString(json, "support"); } catch (final ParsingException e) { - return ""; + return EMPTY_STRING; } } - private String getRelatedItemsUrl(final List tags) throws UnsupportedEncodingException { + @Nonnull + private String getRelatedItemsUrl(@Nonnull final List tags) + throws UnsupportedEncodingException { final String url = baseUrl + PeertubeSearchQueryHandlerFactory.SEARCH_ENDPOINT; final StringBuilder params = new StringBuilder(); params.append("start=0&count=8&sort=-createdAt"); @@ -348,7 +337,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { } private void getStreamsFromApi(final StreamInfoItemsCollector collector, final String apiUrl) - throws ReCaptchaException, IOException, ParsingException { + throws IOException, ReCaptchaException, ParsingException { final Response response = getDownloader().get(apiUrl); JsonObject relatedVideosJson = null; if (response != null && !Utils.isBlank(response.responseBody())) { @@ -365,21 +354,20 @@ public class PeertubeStreamExtractor extends StreamExtractor { } private void collectStreamsFrom(final StreamInfoItemsCollector collector, - final JsonObject jsonObject) - throws ParsingException { + final JsonObject jsonObject) throws ParsingException { final JsonArray contents; try { contents = (JsonArray) JsonUtils.getValue(jsonObject, "data"); } catch (final Exception e) { - throw new ParsingException("unable to extract related videos", e); + throw new ParsingException("Could not extract related videos", e); } for (final Object c : contents) { if (c instanceof JsonObject) { final JsonObject item = (JsonObject) c; - final PeertubeStreamInfoItemExtractor extractor - = new PeertubeStreamInfoItemExtractor(item, baseUrl); - //do not add the same stream in related streams + final PeertubeStreamInfoItemExtractor extractor = + new PeertubeStreamInfoItemExtractor(item, baseUrl); + // Do not add the same stream in related streams if (!extractor.getUrl().equals(getUrl())) { collector.commit(extractor); } @@ -395,7 +383,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { if (response != null) { setInitialData(response.responseBody()); } else { - throw new ExtractionException("Unable to extract PeerTube channel data"); + throw new ExtractionException("Could not extract PeerTube channel data"); } loadSubtitles(); @@ -405,10 +393,10 @@ public class PeertubeStreamExtractor extends StreamExtractor { try { json = JsonParser.object().from(responseBody); } catch (final JsonParserException e) { - throw new ExtractionException("Unable to extract PeerTube stream data", e); + throw new ExtractionException("Could not extract PeerTube stream data", e); } if (json == null) { - throw new ExtractionException("Unable to extract PeerTube stream data"); + throw new ExtractionException("Could not extract PeerTube stream data"); } PeertubeParsingHelper.validate(json); } @@ -429,16 +417,253 @@ public class PeertubeStreamExtractor extends StreamExtractor { final String ext = url.substring(url.lastIndexOf(".") + 1); final MediaFormat fmt = MediaFormat.getFromSuffix(ext); if (fmt != null && !isNullOrEmpty(languageCode)) { - subtitles.add(new SubtitlesStream(fmt, languageCode, url, false)); + subtitles.add(new SubtitlesStream.Builder() + .setContent(url, true) + .setMediaFormat(fmt) + .setLanguageCode(languageCode) + .setAutoGenerated(false) + .build()); } } } - } catch (final Exception e) { - // ignore all exceptions + } catch (final Exception ignored) { + // Ignore all exceptions } } } + private void extractLiveVideoStreams() throws ParsingException { + try { + final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS); + for (final Object s : streamingPlaylists) { + if (!(s instanceof JsonObject)) { + continue; + } + final JsonObject stream = (JsonObject) s; + // Don't use the containsSimilarStream method because it will always return false + // so if there are multiples HLS URLs returned, only the first will be extracted in + // this case. + videoStreams.add(new VideoStream.Builder() + .setId(String.valueOf(stream.getInt("id", -1))) + .setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true) + .setIsVideoOnly(false) + .setResolution(EMPTY_STRING) + .setMediaFormat(MediaFormat.MPEG_4) + .setDeliveryMethod(DeliveryMethod.HLS) + .build()); + } + } catch (final Exception e) { + throw new ParsingException("Could not get video streams", e); + } + } + + private void getStreams() throws ParsingException { + // Progressive streams + getStreamsFromArray(json.getArray(FILES), EMPTY_STRING); + + // HLS streams + try { + final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS); + for (final Object p : streamingPlaylists) { + if (!(p instanceof JsonObject)) { + continue; + } + final JsonObject playlist = (JsonObject) p; + final String playlistUrl = playlist.getString(PLAYLIST_URL); + getStreamsFromArray(playlist.getArray(FILES), playlistUrl); + } + } catch (final Exception e) { + throw new ParsingException("Could not get streams", e); + } + } + + private void getStreamsFromArray(@Nonnull final JsonArray streams, + final String playlistUrl) throws ParsingException { + try { + /* + Starting with version 3.4.0 of PeerTube, HLS playlist of stream resolutions contain the + UUID of the stream, so we can't use the same system to get HLS playlist URL of streams + without fetching the master playlist. + These UUIDs are the same that the ones returned into the fileUrl and fileDownloadUrl + strings. + */ + final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl) + && playlistUrl.endsWith("-master.m3u8"); + + for (final Object s : streams) { + if (!(s instanceof JsonObject)) { + continue; + } + + final JsonObject stream = (JsonObject) s; + final String resolution = JsonUtils.getString(stream, "resolution.label"); + final String url; + final String idSuffix; + + // Extract stream version of streams first + if (stream.has(FILE_URL)) { + url = JsonUtils.getString(stream, FILE_URL); + idSuffix = FILE_URL; + } else { + url = JsonUtils.getString(stream, FILE_DOWNLOAD_URL); + idSuffix = FILE_DOWNLOAD_URL; + } + + if (isNullOrEmpty(url)) { + // Not a valid stream URL + return; + } + + if (resolution.toLowerCase().contains("audio")) { + // An audio stream + addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution, + idSuffix, url, playlistUrl); + } else { + // A video stream + addNewVideoStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution, + idSuffix, url, playlistUrl); + } + } + } catch (final Exception e) { + throw new ParsingException("Could not get streams from array", e); + } + } + + @Nonnull + private String getHlsPlaylistUrlFromFragmentedFileUrl( + @Nonnull final JsonObject streamJsonObject, + @Nonnull final String idSuffix, + @Nonnull final String format, + @Nonnull final String url) throws ParsingException { + final String streamUrl; + if (FILE_DOWNLOAD_URL.equals(idSuffix)) { + streamUrl = JsonUtils.getString(streamJsonObject, FILE_URL); + } else { + streamUrl = url; + } + return streamUrl.replace("-fragmented." + format, ".m3u8"); + } + + @Nonnull + private String getHlsPlaylistUrlFromMasterPlaylist(@Nonnull final JsonObject streamJsonObject, + @Nonnull final String playlistUrl) + throws ParsingException { + return playlistUrl.replace("master", JsonUtils.getNumber(streamJsonObject, + RESOLUTION_ID).toString()); + } + + private void addNewAudioStream(@Nonnull final JsonObject streamJsonObject, + final boolean isInstanceUsingRandomUuidsForHlsStreams, + @Nonnull final String resolution, + @Nonnull final String idSuffix, + @Nonnull final String url, + @Nullable final String playlistUrl) throws ParsingException { + final String extension = url.substring(url.lastIndexOf(".") + 1); + final MediaFormat format = MediaFormat.getFromSuffix(extension); + final String id = resolution + "-" + extension; + + // Add progressive HTTP streams first + audioStreams.add(new AudioStream.Builder() + .setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP) + .setContent(url, true) + .setMediaFormat(format) + .setAverageBitrate(UNKNOWN_BITRATE) + .build()); + + // Then add HLS streams + if (!isNullOrEmpty(playlistUrl)) { + final String hlsStreamUrl; + if (isInstanceUsingRandomUuidsForHlsStreams) { + hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, + extension, url); + + } else { + hlsStreamUrl = getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl); + } + final AudioStream audioStream = new AudioStream.Builder() + .setId(id + "-" + DeliveryMethod.HLS) + .setContent(hlsStreamUrl, true) + .setDeliveryMethod(DeliveryMethod.HLS) + .setMediaFormat(format) + .setAverageBitrate(UNKNOWN_BITRATE) + .setBaseUrl(playlistUrl) + .build(); + if (!Stream.containSimilarStream(audioStream, audioStreams)) { + audioStreams.add(audioStream); + } + } + + // Add finally torrent URLs + final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl"); + if (!isNullOrEmpty(torrentUrl)) { + audioStreams.add(new AudioStream.Builder() + .setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT) + .setContent(torrentUrl, true) + .setDeliveryMethod(DeliveryMethod.TORRENT) + .setMediaFormat(format) + .setAverageBitrate(UNKNOWN_BITRATE) + .build()); + } + } + + private void addNewVideoStream(@Nonnull final JsonObject streamJsonObject, + final boolean isInstanceUsingRandomUuidsForHlsStreams, + @Nonnull final String resolution, + @Nonnull final String idSuffix, + @Nonnull final String url, + @Nullable final String playlistUrl) throws ParsingException { + final String extension = url.substring(url.lastIndexOf(".") + 1); + final MediaFormat format = MediaFormat.getFromSuffix(extension); + final String id = resolution + "-" + extension; + + // Add progressive HTTP streams first + videoStreams.add(new VideoStream.Builder() + .setId(id + "-" + idSuffix + "-" + DeliveryMethod.PROGRESSIVE_HTTP) + .setContent(url, true) + .setIsVideoOnly(false) + .setResolution(resolution) + .setMediaFormat(format) + .build()); + + // Then add HLS streams + if (!isNullOrEmpty(playlistUrl)) { + final String hlsStreamUrl; + if (isInstanceUsingRandomUuidsForHlsStreams) { + hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, + extension, url); + } else { + hlsStreamUrl = playlistUrl.replace("master", JsonUtils.getNumber( + streamJsonObject, RESOLUTION_ID).toString()); + } + + final VideoStream videoStream = new VideoStream.Builder() + .setId(id + "-" + DeliveryMethod.HLS) + .setContent(hlsStreamUrl, true) + .setIsVideoOnly(false) + .setDeliveryMethod(DeliveryMethod.HLS) + .setResolution(resolution) + .setMediaFormat(format) + .setBaseUrl(playlistUrl) + .build(); + if (!Stream.containSimilarStream(videoStream, videoStreams)) { + videoStreams.add(videoStream); + } + } + + // Add finally torrent URLs + final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl"); + if (!isNullOrEmpty(torrentUrl)) { + videoStreams.add(new VideoStream.Builder() + .setId(id + "-" + idSuffix + "-" + DeliveryMethod.TORRENT) + .setContent(torrentUrl, true) + .setIsVideoOnly(false) + .setDeliveryMethod(DeliveryMethod.TORRENT) + .setResolution(resolution) + .setMediaFormat(format) + .build()); + } + } + @Nonnull @Override public String getName() throws ParsingException { @@ -448,7 +673,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Nonnull @Override public String getHost() throws ParsingException { - return JsonUtils.getString(json, "account.host"); + return JsonUtils.getString(json, ACCOUNT_HOST); } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 24bb6ec5b..160bf572c 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -1,6 +1,9 @@ package org.schabi.newpipe.extractor.services.soundcloud.extractors; import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.SOUNDCLOUD_API_V2_URL; +import static org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper.clientId; +import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; +import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN; import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.HTTPS; import static org.schabi.newpipe.extractor.utils.Utils.UTF_8; @@ -26,6 +29,7 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector; import org.schabi.newpipe.extractor.stream.StreamType; @@ -58,13 +62,16 @@ public class SoundcloudStreamExtractor extends StreamExtractor { final String policy = track.getString("policy", EMPTY_STRING); if (!policy.equals("ALLOW") && !policy.equals("MONETIZE")) { isAvailable = false; + if (policy.equals("SNIP")) { throw new SoundCloudGoPlusContentException(); } + if (policy.equals("BLOCK")) { throw new GeographicRestrictionException( "This track is not available in user's country"); } + throw new ContentNotAvailableException("Content not available: policy " + policy); } } @@ -72,7 +79,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { @Nonnull @Override public String getId() { - return track.getInt("id") + EMPTY_STRING; + return String.valueOf(track.getInt("id")); } @Nonnull @@ -162,17 +169,19 @@ public class SoundcloudStreamExtractor extends StreamExtractor { // Streams can be streamable and downloadable - or explicitly not. // For playing the track, it is only necessary to have a streamable track. // If this is not the case, this track might not be published yet. + // If audio streams were calculated, return the calculated result if (!track.getBoolean("streamable") || !isAvailable) { return audioStreams; } try { final JsonArray transcodings = track.getObject("media").getArray("transcodings"); - if (transcodings != null) { + if (!isNullOrEmpty(transcodings)) { // Get information about what stream formats are available extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings), audioStreams); } + extractDownloadableFileIfAvailable(audioStreams); } catch (final NullPointerException e) { throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e); } @@ -180,7 +189,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { return audioStreams; } - private static boolean checkMp3ProgressivePresence(final JsonArray transcodings) { + private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) { boolean presence = false; for (final Object transcoding : transcodings) { final JsonObject transcodingJsonObject = (JsonObject) transcoding; @@ -195,48 +204,68 @@ public class SoundcloudStreamExtractor extends StreamExtractor { } @Nonnull - private static String getTranscodingUrl(final String endpointUrl, - final String protocol) + private String getTranscodingUrl(final String endpointUrl, + final String protocol) throws IOException, ExtractionException { final Downloader downloader = NewPipe.getDownloader(); final String apiStreamUrl = endpointUrl + "?client_id=" - + SoundcloudParsingHelper.clientId(); + + clientId(); final String response = downloader.get(apiStreamUrl).responseBody(); final JsonObject urlObject; try { urlObject = JsonParser.object().from(response); } catch (final JsonParserException e) { - throw new ParsingException("Could not parse streamable url", e); + throw new ParsingException("Could not parse streamable URL", e); } + final String urlString = urlObject.getString("url"); if (protocol.equals("progressive")) { return urlString; } else if (protocol.equals("hls")) { - try { - return getSingleUrlFromHlsManifest(urlString); - } catch (final ParsingException ignored) { - } + return getSingleUrlFromHlsManifest(urlString); } + // else, unknown protocol - return ""; + return EMPTY_STRING; } - private static void extractAudioStreams(final JsonArray transcodings, - final boolean mp3ProgressiveInStreams, - final List audioStreams) { + @Nullable + private String getDownloadUrl(@Nonnull final String trackId) + throws IOException, ExtractionException { + final Downloader dl = NewPipe.getDownloader(); + final JsonObject downloadJsonObject; + + final String response = dl.get(SOUNDCLOUD_API_V2_URL + "tracks/" + trackId + + "/download" + "?client_id=" + clientId()).responseBody(); + try { + downloadJsonObject = JsonParser.object().from(response); + } catch (final JsonParserException e) { + throw new ParsingException("Could not parse download URL", e); + } + final String redirectUri = downloadJsonObject.getString("redirectUri"); + if (!isNullOrEmpty(redirectUri)) { + return redirectUri; + } + return null; + } + + private void extractAudioStreams(@Nonnull final JsonArray transcodings, + final boolean mp3ProgressiveInStreams, + final List audioStreams) { for (final Object transcoding : transcodings) { final JsonObject transcodingJsonObject = (JsonObject) transcoding; final String url = transcodingJsonObject.getString("url"); if (isNullOrEmpty(url)) { continue; } + final String mediaUrl; - final String preset = transcodingJsonObject.getString("preset"); + final String preset = transcodingJsonObject.getString("preset", ID_UNKNOWN); final String protocol = transcodingJsonObject.getObject("format") .getString("protocol"); MediaFormat mediaFormat = null; - int bitrate = 0; + int averageBitrate = UNKNOWN_BITRATE; if (preset.contains("mp3")) { // Don't add the MP3 HLS stream if there is a progressive stream present // because the two have the same bitrate @@ -244,36 +273,75 @@ public class SoundcloudStreamExtractor extends StreamExtractor { continue; } mediaFormat = MediaFormat.MP3; - bitrate = 128; + averageBitrate = 128; } else if (preset.contains("opus")) { mediaFormat = MediaFormat.OPUS; - bitrate = 64; + averageBitrate = 64; } - if (mediaFormat != null) { - try { - mediaUrl = getTranscodingUrl(url, protocol); - if (!mediaUrl.isEmpty()) { - audioStreams.add(new AudioStream(mediaUrl, mediaFormat, bitrate)); + try { + mediaUrl = getTranscodingUrl(url, protocol); + if (!mediaUrl.isEmpty()) { + final AudioStream audioStream = new AudioStream.Builder() + .setId(preset) + .setContent(mediaUrl, true) + .setMediaFormat(mediaFormat) + .setAverageBitrate(averageBitrate) + .build(); + if (!Stream.containSimilarStream(audioStream, audioStreams)) { + audioStreams.add(audioStream); } - } catch (final Exception ignored) { - // something went wrong when parsing this transcoding, don't add it to - // audioStreams } + } catch (final Exception ignored) { + // Something went wrong when parsing this transcoding, don't add it to the + // audioStreams + } + } + } + + /** + * Add the downloadable format if it is available. + * + *

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

+ * + * @param audioStreams the audio streams to which add the downloadable file + */ + public void extractDownloadableFileIfAvailable(final List audioStreams) { + if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) { + try { + final String downloadUrl = getDownloadUrl(getId()); + if (!isNullOrEmpty(downloadUrl)) { + audioStreams.add(new AudioStream.Builder() + .setId("original-format") + .setContent(downloadUrl, true) + .setAverageBitrate(UNKNOWN_BITRATE) + .build()); + } + } catch (final Exception ignored) { + // If something went wrong when trying to get the download URL, ignore the + // exception throw because this "stream" is not necessary to play the track } } } /** * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. + * *

* This method downloads the provided manifest URL, find all web occurrences in the manifest, * get the last segment URL, changes its segment range to {@code 0/track-length} and return * this string. + *

+ * * @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 */ - private static String getSingleUrlFromHlsManifest(final String hlsManifestUrl) + @Nonnull + private static String getSingleUrlFromHlsManifest(@Nonnull final String hlsManifestUrl) throws ParsingException { final Downloader dl = NewPipe.getDownloader(); final String hlsManifestResponse; @@ -326,7 +394,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId()); final String apiUrl = SOUNDCLOUD_API_V2_URL + "tracks/" + urlEncode(getId()) - + "/related?client_id=" + urlEncode(SoundcloudParsingHelper.clientId()); + + "/related?client_id=" + urlEncode(clientId()); SoundcloudParsingHelper.getStreamsFromApi(collector, apiUrl); return collector; @@ -355,19 +423,19 @@ public class SoundcloudStreamExtractor extends StreamExtractor { // Tags are separated by spaces, but they can be multiple words escaped by quotes " final String[] tagList = track.getString("tag_list").split(" "); final List tags = new ArrayList<>(); - String escapedTag = ""; + final StringBuilder escapedTag = new StringBuilder(); boolean isEscaped = false; for (final String tag : tagList) { if (tag.startsWith("\"")) { - escapedTag += tag.replace("\"", ""); + escapedTag.append(tag.replace("\"", "")); isEscaped = true; } else if (isEscaped) { if (tag.endsWith("\"")) { - escapedTag += " " + tag.replace("\"", ""); + escapedTag.append(" ").append(tag.replace("\"", "")); isEscaped = false; - tags.add(escapedTag); + tags.add(escapedTag.toString()); } else { - escapedTag += " " + tag; + escapedTag.append(" ").append(tag); } } else if (!tag.isEmpty()) { tags.add(tag); From 4330b5f7befee8f77cae2be49fdff5c154ae4869 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 6 Mar 2022 19:41:01 +0100 Subject: [PATCH 06/38] Add POST_LIVE_STREAM and POST_LIVE_AUDIO_STREAM stream types This allows the extractor to determine if a content is an ended audio or video livestream. --- .../newpipe/extractor/stream/StreamType.java | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java index 2d6b9a571..082c7a228 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java @@ -1,10 +1,115 @@ package org.schabi.newpipe.extractor.stream; +/** + * An enum representing the stream types of stream contents returned by the extractor. + */ public enum StreamType { - NONE, // placeholder to check if stream type was checked or not + + /** + * Placeholder to check if the stream type of stream content 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 extractor} and is only used + * internally. + *

+ */ + NONE, + + /** + * Enum constant to indicate that the stream type of stream content is a video. + * + *

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

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

+ */ 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 (video-only or video with audio, depending of the stream/the content/the + * service). + *

+ */ LIVE_STREAM, + + /** + * Enum constant to indicate that the stream type of stream content is a live audio content. + * + *

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

+ */ AUDIO_LIVE_STREAM, + + /** + * Enum constant to indicate that the stream type of stream content is a video content of an + * ended live video stream. + * + *

+ * Note that most of ended live video (or audio) contents may be extracted as + * {@link #VIDEO_STREAM regular video contents} (or + * {@link #AUDIO_STREAM regular audio contents}) later, because the service may encode them + * again later as normal video/audio streams. That's the case for example on YouTube. + *

+ * + *

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

+ * 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 } From a85768444250a63d7b289b7ceb08d5e1b2b46d79 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 6 Mar 2022 20:10:11 +0100 Subject: [PATCH 07/38] Apply changes in YoutubeStreamExtractor Extract post live DVR streams as post live streams instead of live streams. A new class has been in order to improve code: ItagInfo, which stores an itag, the content (URL) extracted and if its an URL or not. A functional interface has been added in order to abstract the stream building: StreamBuilderHelper. Also add the cver parameter added by the desktop web client on the corresponding streams (a new method has been added in YoutubeParsingHelper to check this and another for Android streams). Some code in these classes has been also refactored/improved/optimized. --- .../extractor/services/youtube/ItagInfo.java | 37 ++ .../youtube/YoutubeParsingHelper.java | 50 ++- .../extractors/YoutubeStreamExtractor.java | 372 +++++++++++------- 3 files changed, 311 insertions(+), 148 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java 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 new file mode 100644 index 000000000..cdb5dc2de --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java @@ -0,0 +1,37 @@ +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/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index eddb69cde..28b802b9d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -71,6 +71,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -246,6 +247,11 @@ public final class YoutubeParsingHelper { private static final String FEED_BASE_CHANNEL_ID = "https://www.youtube.com/feeds/videos.xml?channel_id="; private static final String FEED_BASE_USER = "https://www.youtube.com/feeds/videos.xml?user="; + private static final Pattern C_WEB_PATTERN = Pattern.compile("&c=WEB"); + private static final Pattern C_TVHTML5_SIMPLY_EMBEDDED_PLAYER_PATTERN = + Pattern.compile("&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER"); + private static final Pattern C_ANDROID_PATTERN = Pattern.compile("&c=ANDROID"); + private static final Pattern C_IOS_PATTERN = Pattern.compile("&c=IOS"); private static boolean isGoogleURL(final String url) { final String cachedUrl = extractCachedUrlIfNeeded(url); @@ -1190,7 +1196,7 @@ public final class YoutubeParsingHelper { @Nonnull final Localization localization, @Nonnull final ContentCountry contentCountry, @Nonnull final String videoId) { - // @formatter:off + // @formatter:off return JsonObject.builder() .object("context") .object("client") @@ -1588,4 +1594,46 @@ public final class YoutubeParsingHelper { return RandomStringFromAlphabetGenerator.generate( CONTENT_PLAYBACK_NONCE_ALPHABET, 12, numberGenerator); } + + /** + * Check if the streaming URL is a URL from the YouTube {@code WEB} client. + * + * @param url the streaming URL on which check if it's a {@code WEB} streaming URL. + * @return true if it's a {@code WEB} streaming URL, false otherwise + */ + public static boolean isWebStreamingUrl(@Nonnull final String url) { + return Parser.isMatch(C_WEB_PATTERN, url); + } + + /** + * Check if the streaming URL is a URL from the YouTube {@code TVHTML5_SIMPLY_EMBEDDED_PLAYER} + * client. + * + * @param url the streaming URL on which check if it's a {@code TVHTML5_SIMPLY_EMBEDDED_PLAYER} + * streaming URL. + * @return true if it's a {@code TVHTML5_SIMPLY_EMBEDDED_PLAYER} streaming URL, false otherwise + */ + public static boolean isTvHtml5SimplyEmbeddedPlayerStreamingUrl(@Nonnull final String url) { + return Parser.isMatch(C_TVHTML5_SIMPLY_EMBEDDED_PLAYER_PATTERN, url); + } + + /** + * Check if the streaming URL is a URL from the YouTube {@code ANDROID} client. + * + * @param url the streaming URL on which check if it's a {@code ANDROID} streaming URL. + * @return true if it's a {@code ANDROID} streaming URL, false otherwise + */ + public static boolean isAndroidStreamingUrl(@Nonnull final String url) { + return Parser.isMatch(C_ANDROID_PATTERN, url); + } + + /** + * Check if the streaming URL is a URL from the YouTube {@code IOS} client. + * + * @param url the streaming URL on which check if it's a {@code IOS} streaming URL. + * @return true if it's a {@code IOS} streaming URL, false otherwise + */ + public static boolean isIosStreamingUrl(@Nonnull final String url) { + return Parser.isMatch(C_IOS_PATTERN, url); + } } 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 46676ff3c..5617fd12c 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 @@ -1,3 +1,23 @@ +/* + * Created by Christian Schabesberger on 06.08.15. + * + * Copyright (C) Christian Schabesberger 2019 + * YoutubeStreamExtractor.java is part of NewPipe Extractor. + * + * NewPipe Extractor is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPipe Extractor is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPipe Extractor. If not, see . + */ + package org.schabi.newpipe.extractor.services.youtube.extractors; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CONTENT_CHECK_OK; @@ -8,10 +28,12 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientVersion; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonAndroidPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonIosPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareAndroidMobileJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareIosMobileJsonBuilder; @@ -44,12 +66,14 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.TimeAgoParser; import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager; +import org.schabi.newpipe.extractor.services.youtube.ItagInfo; import org.schabi.newpipe.extractor.services.youtube.ItagItem; import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.services.youtube.YoutubeThrottlingDecrypter; import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeChannelLinkHandlerFactory; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Stream; @@ -64,7 +88,6 @@ import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Utils; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.OffsetDateTime; @@ -72,7 +95,6 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -82,26 +104,6 @@ import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; -/* - * Created by Christian Schabesberger on 06.08.15. - * - * Copyright (C) Christian Schabesberger 2019 - * YoutubeStreamExtractor.java is part of NewPipe. - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - */ - public class YoutubeStreamExtractor extends StreamExtractor { /*////////////////////////////////////////////////////////////////////////// // Exceptions @@ -113,7 +115,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { } } - /*//////////////////////////////////////////////////////////////////////////*/ + /*////////////////////////////////////////////////////////////////////////*/ @Nullable private static String cachedDeobfuscationCode = null; @@ -140,8 +142,6 @@ public class YoutubeStreamExtractor extends StreamExtractor { private JsonObject playerMicroFormatRenderer; private int ageLimit = -1; private StreamType streamType; - @Nullable - private List subtitles = null; // We need to store the contentPlaybackNonces because we need to append them to videoplayback // URLs (with the cpn parameter). @@ -580,73 +580,25 @@ public class YoutubeStreamExtractor extends StreamExtractor { .orElse(EMPTY_STRING); } - @FunctionalInterface - interface StreamTypeStreamBuilderHelper { - T buildStream(String url, ItagItem itagItem); - } - - /** - * Abstract method for - * {@link #getAudioStreams()}, {@link #getVideoOnlyStreams()} and {@link #getVideoStreams()}. - * - * @param itags A map of Urls + ItagItems - * @param streamBuilder Builds the stream from the provided data - * @param exMsgStreamType Stream type inside the exception message e.g. "video streams" - * @param Type of the stream - * @return - * @throws ExtractionException - */ - private List getStreamsByType( - final Map itags, - final StreamTypeStreamBuilderHelper streamBuilder, - final String exMsgStreamType - ) throws ExtractionException { - final List streams = new ArrayList<>(); - - try { - for (final Map.Entry entry : itags.entrySet()) { - final String url = tryDecryptUrl(entry.getKey(), getId()); - - final T stream = streamBuilder.buildStream(url, entry.getValue()); - if (!Stream.containSimilarStream(stream, streams)) { - streams.add(stream); - } - } - } catch (final Exception e) { - throw new ParsingException("Could not get " + exMsgStreamType, e); - } - - return streams; - } - @Override public List getAudioStreams() throws ExtractionException { assertPageFetched(); - return getStreamsByType( - getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO), - AudioStream::new, - "audio streams" - ); + return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO, + getAudioStreamBuilderHelper(), "audio"); } @Override public List getVideoStreams() throws ExtractionException { assertPageFetched(); - return getStreamsByType( - getItags(FORMATS, ItagItem.ItagType.VIDEO), - (url, itag) -> new VideoStream(url, false, itag), - "video streams" - ); + return getItags(FORMATS, ItagItem.ItagType.VIDEO, + getVideoStreamBuilderHelper(false), "video"); } @Override public List getVideoOnlyStreams() throws ExtractionException { assertPageFetched(); - return getStreamsByType( - getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY), - (url, itag) -> new VideoStream(url, true, itag), - "video only streams" - ); + return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY, + getVideoStreamBuilderHelper(true), "video-only"); } /** @@ -672,18 +624,15 @@ public class YoutubeStreamExtractor extends StreamExtractor { @Nonnull public List getSubtitles(final MediaFormat format) throws ParsingException { assertPageFetched(); - if (subtitles != null) { - // Already calculated - return subtitles; - } + // We cannot store the subtitles list because the media format may change + final List subtitlesToReturn = new ArrayList<>(); final JsonObject renderer = playerResponse.getObject("captions") .getObject("playerCaptionsTracklistRenderer"); final JsonArray captionsArray = renderer.getArray("captionTracks"); // TODO: use this to apply auto translation to different language from a source language // final JsonArray autoCaptionsArray = renderer.getArray("translationLanguages"); - subtitles = new ArrayList<>(); for (int i = 0; i < captionsArray.size(); i++) { final String languageCode = captionsArray.getObject(i).getString("languageCode"); final String baseUrl = captionsArray.getObject(i).getString("baseUrl"); @@ -692,15 +641,21 @@ public class YoutubeStreamExtractor extends StreamExtractor { if (languageCode != null && baseUrl != null && vssId != null) { final boolean isAutoGenerated = vssId.startsWith("a."); final String cleanUrl = baseUrl - .replaceAll("&fmt=[^&]*", "") // Remove preexisting format if exists - .replaceAll("&tlang=[^&]*", ""); // Remove translation language + // Remove preexisting format if exists + .replaceAll("&fmt=[^&]*", "") + // Remove translation language + .replaceAll("&tlang=[^&]*", ""); - subtitles.add(new SubtitlesStream(format, languageCode, - cleanUrl + "&fmt=" + format.getSuffix(), isAutoGenerated)); + subtitlesToReturn.add(new SubtitlesStream.Builder() + .setContent(cleanUrl + "&fmt=" + format.getSuffix(), true) + .setMediaFormat(format) + .setLanguageCode(languageCode) + .setAutoGenerated(isAutoGenerated) + .build()); } } - return subtitles; + return subtitlesToReturn; } @Override @@ -788,6 +743,8 @@ public class YoutubeStreamExtractor extends StreamExtractor { private static final String STREAMING_DATA = "streamingData"; private static final String PLAYER = "player"; private static final String NEXT = "next"; + private static final String SIGNATURE_CIPHER = "signatureCipher"; + private static final String CIPHER = "cipher"; private static final String[] REGEXES = { "(?:\\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2,})\\s*=\\s*function\\(\\s*a\\s*\\)" @@ -827,7 +784,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { final JsonObject playabilityStatus = playerResponse.getObject("playabilityStatus"); - final boolean ageRestricted = playabilityStatus.getString("reason", EMPTY_STRING) + final boolean isAgeRestricted = playabilityStatus.getString("reason", EMPTY_STRING) .contains("age"); setStreamType(); @@ -837,12 +794,12 @@ public class YoutubeStreamExtractor extends StreamExtractor { fetchTvHtml5EmbedJsonPlayer(contentCountry, localization, videoId); } catch (final Exception ignored) { } - - // Refresh the stream type because the stream type may be not properly known for - // age-restricted videos - setStreamType(); } + // Refresh the stream type because the stream type may be not properly known for + // age-restricted videos + setStreamType(); + if (html5StreamingData == null && playerResponse.has(STREAMING_DATA)) { html5StreamingData = playerResponse.getObject(STREAMING_DATA); } @@ -866,7 +823,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { .getBytes(StandardCharsets.UTF_8); nextResponse = getJsonPostResponse(NEXT, body, localization); - if ((!ageRestricted && streamType == StreamType.VIDEO_STREAM) + if ((!isAgeRestricted && streamType == StreamType.VIDEO_STREAM) || isAndroidClientFetchForced) { try { fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId); @@ -874,7 +831,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { } } - if ((!ageRestricted && streamType == StreamType.LIVE_STREAM) + if ((!isAgeRestricted && streamType == StreamType.LIVE_STREAM) || isIosClientFetchForced) { try { fetchIosMobileJsonPlayer(contentCountry, localization, videoId); @@ -1183,44 +1140,133 @@ public class YoutubeStreamExtractor extends StreamExtractor { return videoSecondaryInfoRenderer; } - @Nonnull - private Map getItags(@Nonnull final String streamingDataKey, - @Nonnull final ItagItem.ItagType itagTypeWanted) { - final Map urlAndItags = new LinkedHashMap<>(); - if (html5StreamingData == null && androidStreamingData == null - && iosStreamingData == null) { - return urlAndItags; - } - - final List> streamingDataAndCpnLoopList = new ArrayList<>(); - // Use the androidStreamingData object first because there is no n param and no - // signatureCiphers in streaming URLs of the Android client - streamingDataAndCpnLoopList.add(new Pair<>(androidStreamingData, androidCpn)); - streamingDataAndCpnLoopList.add(new Pair<>(html5StreamingData, html5Cpn)); - // Use the iosStreamingData object in the last position because most of the available - // streams can be extracted with the Android and web clients and also because the iOS - // client is only enabled by default on livestreams - streamingDataAndCpnLoopList.add(new Pair<>(iosStreamingData, iosCpn)); - - for (final Pair pair : streamingDataAndCpnLoopList) { - urlAndItags.putAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey, - itagTypeWanted, pair.getSecond())); - } - - return urlAndItags; + @FunctionalInterface + private interface StreamBuilderHelper { + @Nonnull + T buildStream(ItagInfo itagInfo); } @Nonnull - private Map getStreamsFromStreamingDataKey( + private List getItags( + final String streamingDataKey, + final ItagItem.ItagType itagTypeWanted, + final StreamBuilderHelper streamBuilderHelper, + final String streamTypeExceptionMessage) throws ParsingException { + try { + final List itagInfos = new ArrayList<>(); + if (html5StreamingData == null && androidStreamingData == null + && iosStreamingData == null) { + return Collections.emptyList(); + } + + final List> streamingDataAndCpnLoopList = new ArrayList<>(); + // Use the androidStreamingData object first because there is no n param and no + // signatureCiphers in streaming URLs of the Android client + streamingDataAndCpnLoopList.add(new Pair<>(androidStreamingData, androidCpn)); + streamingDataAndCpnLoopList.add(new Pair<>(html5StreamingData, html5Cpn)); + // Use the iosStreamingData object in the last position because most of the available + // streams can be extracted with the Android and web clients and also because the iOS + // client is only enabled by default on livestreams + streamingDataAndCpnLoopList.add(new Pair<>(iosStreamingData, iosCpn)); + + for (final Pair pair : streamingDataAndCpnLoopList) { + itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey, + itagTypeWanted, streamType, pair.getSecond())); + } + + final List streamList = new ArrayList<>(); + for (final ItagInfo itagInfo : itagInfos) { + final T stream = streamBuilderHelper.buildStream(itagInfo); + if (!Stream.containSimilarStream(stream, streamList)) { + streamList.add(stream); + } + } + + return streamList; + } catch (final Exception e) { + throw new ParsingException( + "Could not get " + streamTypeExceptionMessage + " streams", e); + } + } + + @Nonnull + private StreamBuilderHelper getAudioStreamBuilderHelper() { + return new StreamBuilderHelper() { + @Nonnull + @Override + public AudioStream buildStream(@Nonnull final ItagInfo itagInfo) { + final ItagItem itagItem = itagInfo.getItagItem(); + final AudioStream.Builder builder = new AudioStream.Builder() + .setId(String.valueOf(itagItem.id)) + .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) + .setMediaFormat(itagItem.getMediaFormat()) + .setAverageBitrate(itagItem.getAverageBitrate()) + .setItagItem(itagItem); + + if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { + // YouTube uses the DASH delivery method for videos on OTF streams and + // for all streams of post-live streams and live streams + builder.setDeliveryMethod(DeliveryMethod.DASH); + } + + return builder.build(); + } + }; + } + + @Nonnull + private StreamBuilderHelper getVideoStreamBuilderHelper( + final boolean areStreamsVideoOnly) { + return new StreamBuilderHelper() { + @Nonnull + @Override + public VideoStream buildStream(@Nonnull final ItagInfo itagInfo) { + final ItagItem itagItem = itagInfo.getItagItem(); + final VideoStream.Builder builder = new VideoStream.Builder() + .setId(String.valueOf(itagItem.id)) + .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) + .setMediaFormat(itagItem.getMediaFormat()) + .setIsVideoOnly(areStreamsVideoOnly) + .setItagItem(itagItem); + + final int height = itagItem.getHeight(); + if (height > 0) { + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(height); + stringBuilder.append("p"); + final int fps = itagItem.getFps(); + if (fps > 30) { + stringBuilder.append(fps); + } + builder.setResolution(stringBuilder.toString()); + } else { + final String resolutionString = itagItem.getResolutionString(); + builder.setResolution(resolutionString != null ? resolutionString : ""); + } + + if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { + // YouTube uses the DASH delivery method for videos on OTF streams and + // for all streams of post-live streams and live streams + builder.setDeliveryMethod(DeliveryMethod.DASH); + } + + return builder.build(); + } + }; + } + + @Nonnull + private List getStreamsFromStreamingDataKey( final JsonObject streamingData, - @Nonnull final String streamingDataKey, + final String streamingDataKey, @Nonnull final ItagItem.ItagType itagTypeWanted, + @Nonnull final StreamType contentStreamType, @Nonnull final String contentPlaybackNonce) { if (streamingData == null || !streamingData.has(streamingDataKey)) { - return Collections.emptyMap(); + return Collections.emptyList(); } - final Map urlAndItagsFromStreamingDataObject = new LinkedHashMap<>(); + final List itagInfos = new ArrayList<>(); final JsonArray formats = streamingData.getArray(streamingDataKey); for (int i = 0; i < formats.size(); i++) { final JsonObject formatData = formats.getObject(i); @@ -1232,53 +1278,85 @@ public class YoutubeStreamExtractor extends StreamExtractor { try { final ItagItem itagItem = ItagItem.getItag(itag); + final ItagItem.ItagType itagType = itagItem.itagType; if (itagItem.itagType != itagTypeWanted) { continue; } - - // Ignore streams that are delivered using YouTube's OTF format, - // as those only work with DASH and not with progressive HTTP. - if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(formatData.getString("type"))) { - continue; - } - - final String streamUrl; + String streamUrl; if (formatData.has("url")) { - streamUrl = formatData.getString("url"); + streamUrl = formatData.getString("url") + "&cpn=" + + contentPlaybackNonce; } else { // This url has an obfuscated signature - final String cipherString = formatData.has("cipher") - ? formatData.getString("cipher") - : formatData.getString("signatureCipher"); + final String cipherString = formatData.has(CIPHER) + ? formatData.getString(CIPHER) + : formatData.getString(SIGNATURE_CIPHER); final Map cipher = Parser.compatParseMap( cipherString); streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" + deobfuscateSignature(cipher.get("s")); } + if (isWebStreamingUrl(streamUrl)) { + streamUrl = tryDecryptUrl(streamUrl, getId()) + "&cver=" + + getClientVersion(); + } + final JsonObject initRange = formatData.getObject("initRange"); final JsonObject indexRange = formatData.getObject("indexRange"); final String mimeType = formatData.getString("mimeType", EMPTY_STRING); final String codec = mimeType.contains("codecs") - ? mimeType.split("\"")[1] - : EMPTY_STRING; + ? mimeType.split("\"")[1] : EMPTY_STRING; itagItem.setBitrate(formatData.getInt("bitrate")); itagItem.setWidth(formatData.getInt("width")); itagItem.setHeight(formatData.getInt("height")); - itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1"))); - itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1"))); - itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1"))); - itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1"))); - itagItem.fps = formatData.getInt("fps"); + itagItem.setInitStart(Integer.parseInt(initRange.getString("start", + "-1"))); + itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", + "-1"))); + itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", + "-1"))); + itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", + "-1"))); itagItem.setQuality(formatData.getString("quality")); itagItem.setCodec(codec); + if (contentStreamType != StreamType.VIDEO_STREAM) { + itagItem.setTargetDurationSec(formatData.getInt( + "targetDurationSec")); + } + if (itagType == ItagItem.ItagType.VIDEO + || itagType == ItagItem.ItagType.VIDEO_ONLY) { + itagItem.setFps(formatData.getInt("fps")); + } + if (itagType == ItagItem.ItagType.AUDIO) { + itagItem.setSampleRate(Integer.parseInt(formatData.getString( + "audioSampleRate"))); + itagItem.setAudioChannels(formatData.getInt("audioChannels")); + } + itagItem.setContentLength(Long.parseLong(formatData.getString( + "contentLength", "-1"))); - urlAndItagsFromStreamingDataObject.put(streamUrl, itagItem); - } catch (final UnsupportedEncodingException | ParsingException ignored) { + final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); + + if (contentStreamType == StreamType.VIDEO_STREAM) { + itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING) + .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); + } else { + // We are currently not able to generate DASH manifests for running + // livestreams, so because of the requirements of StreamInfo + // objects, return these streams as DASH URL streams (even if they + // are not playable). + // Ended livestreams are returned as non URL streams + itagInfo.setIsUrl(contentStreamType != StreamType.POST_LIVE_STREAM); + } + + itagInfos.add(itagInfo); + } catch (final IOException | ExtractionException ignored) { } } - return urlAndItagsFromStreamingDataObject; + + return itagInfos; } From 7477ed0f3d65fed34633d6fe229c5adf2cb6c99b Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 6 Mar 2022 20:33:19 +0100 Subject: [PATCH 08/38] [YouTube] Add ability to generate manifests of progressive, OTF and post live streams A new class has been added to do so: YoutubeDashManifestCreator. It relies on a new class: ManifestCreatorCache, to cache the content, which relies on a new pair class named Pair. Results are cached and there is a cache per delivery type, on which cache limit, clear factor, clearing and resetting can be applied to each cache and to all caches. Look at code changes for more details. --- .../youtube/YoutubeDashManifestCreator.java | 1887 +++++++++++++++++ .../extractor/utils/ManifestCreatorCache.java | 301 +++ 2 files changed, 2188 insertions(+) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCache.java 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 new file mode 100644 index 000000000..62d833f51 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java @@ -0,0 +1,1887 @@ +package org.schabi.newpipe.extractor.services.youtube; + +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 javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.*; +import static org.schabi.newpipe.extractor.utils.Utils.*; + +/** + * 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. + *

+ */ +@SuppressWarnings({"ConstantConditions", "unused"}) +public final class YoutubeDashManifestCreator { + + /** + * URL parameter of the first sequence for live, post-live-DVR and OTF streams. + */ + private static final String SQ_0 = "&sq=0"; + + /** + * URL parameter of the first stream request made by official clients. + */ + private 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. + */ + private static final String ALR_YES = "&alr=yes"; + + /** + * The redirect count limit that this class uses, which is the same limit as OkHttp. + */ + private static final int MAXIMUM_REDIRECT_COUNT = 20; + + /** + * A list of durations of segments of an OTF stream. + * + *

+ * This list is automatically cleared in the execution of + * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * manifest is converted to a string. + *

+ */ + private static final List SEGMENTS_DURATION = new ArrayList<>(); + + /** + * A list of contiguous repetitions of durations of an OTF stream. + * + *

+ * This list is automatically cleared in the execution of + * {@link #createDashManifestFromOtfStreamingUrl(String, ItagItem, long)}, before the DASH + * manifest is converted to a string. + *

+ */ + private static final List DURATION_REPETITIONS = new ArrayList<>(); + + /** + * Cache of DASH manifests generated for OTF streams. + */ + private static final ManifestCreatorCache GENERATED_OTF_MANIFESTS = + new ManifestCreatorCache<>(); + + /** + * Cache of DASH manifests generated for post-live-DVR streams. + */ + private static final ManifestCreatorCache + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS = new ManifestCreatorCache<>(); + + /** + * Cache of DASH manifests generated for progressive streams. + */ + private static final ManifestCreatorCache + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS = new ManifestCreatorCache<>(); + + /** + * Enum of streaming format types used by YouTube in their streams. + */ + private 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 {@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 YoutubeDashManifestCreationException extends Exception { + + YoutubeDashManifestCreationException(final String message) { + super(message); + } + + YoutubeDashManifestCreationException(final String message, final Exception e) { + super(message, e); + } + } + + /** + * 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: + *

    + *
  • the base URL of the stream (which, if you try to access to it, returns HTTP + * status code 404 after redirects, and if the URL is valid);
  • + *
  • an {@link ItagItem}, which needs to contain the following information: + *
      + *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
    • + *
    • its bitrate;
    • + *
    • its mime type;
    • + *
    • its codec(s);
    • + *
    • for an audio stream: its audio channels;
    • + *
    • for a video stream: its width and height.
    • + *
    + *
  • + *
  • the duration of the video, which will be used if the duration could not be + * parsed from the first sequence of the stream.
  • + *
+ *

+ * + *

In order to generate the DASH manifest, this method will: + *

    + *
  • request the first sequence of the stream (the base URL on which the first + * sequence parameters are appended (see {@link #RN_0} and {@link #SQ_0})) with a POST + * or GET request (depending of the client on which the streaming URL comes from); + *
  • + *
  • follow its redirection(s), if any;
  • + *
  • save the last URL, remove the first sequence parameters;
  • + *
  • use the information provided in the {@link ItagItem} to generate all + * elements of the DASH manifest.
  • + *
+ *

+ * + *

+ * 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 + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate + * the DASH manifest + */ + @Nonnull + public static String createDashManifestFromOtfStreamingUrl( + @Nonnull String otfBaseStreamingUrl, + @Nonnull final ItagItem itagItem, + final long durationSecondsFallback) + throws YoutubeDashManifestCreationException { + if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) { + return GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl).getSecond(); + } + + final String originalOtfBaseStreamingUrl = otfBaseStreamingUrl; + // Try to avoid redirects when streaming the content by saving the last URL we get + // from video servers. + final Response response = getInitializationResponse(otfBaseStreamingUrl, + itagItem, DeliveryType.OTF); + otfBaseStreamingUrl = 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); + } + + 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 YoutubeDashManifestCreationException( + "Unable to generate the DASH manifest: could not get the duration of segments", 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, otfBaseStreamingUrl, DeliveryType.OTF); + generateSegmentTimelineElement(document); + collectSegmentsData(segmentDuration); + generateSegmentElementsForOtfStreams(document); + + SEGMENTS_DURATION.clear(); + DURATION_REPETITIONS.clear(); + + return buildResult(originalOtfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); + } + + /** + * 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: + *

    + *
  • the base URL of the stream (which, if you try to access to it, returns HTTP + * status code 404 after redirects, and if the URL is valid);
  • + *
  • an {@link ItagItem}, which needs to contain the following information: + *
      + *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
    • + *
    • its bitrate;
    • + *
    • its mime type;
    • + *
    • its codec(s);
    • + *
    • for an audio stream: its audio channels;
    • + *
    • for a video stream: its width and height.
    • + *
    + *
  • + *
  • the duration of the video, which will be used if the duration could not be + * parsed from the first sequence of the stream.
  • + *
+ *

+ * + *

In order to generate the DASH manifest, this method will: + *

    + *
  • request the first sequence of the stream (the base URL on which the first + * sequence parameters are appended (see {@link #RN_0} and {@link #SQ_0})) with a POST + * or GET request (depending of the client on which the streaming URL comes from); + *
  • + *
  • follow its redirection(s), if any;
  • + *
  • save the last URL, remove the first sequence parameters;
  • + *
  • use the information provided in the {@link ItagItem} to generate all + * elements of the DASH manifest.
  • + *
+ *

+ * + *

+ * 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> responseHeaders = response.responseHeaders(); + streamDuration = responseHeaders.get("X-Head-Time-Millis").get(0); + 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); + } + + 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"); + } + + final Document document = generateDocumentAndMpdElement(new String[]{streamDuration}, + DeliveryType.LIVE, itagItem, durationSecondsFallback); + generatePeriodElement(document); + generateAdaptationSetElement(document, itagItem); + generateRoleElement(document); + generateRepresentationElement(document, itagItem); + if (itagItem.itagType == ItagItem.ItagType.AUDIO) { + generateAudioChannelConfigurationElement(document, itagItem); + } + generateSegmentTemplateElement(document, postLiveStreamDvrStreamingUrl, DeliveryType.LIVE); + generateSegmentTimelineElement(document); + generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); + + return buildResult(originalPostLiveStreamDvrStreamingUrl, document, + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS); + } + + /** + * Create DASH manifests from a YouTube progressive stream. + * + *

+ * 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 base URL of the stream (which, if you try to access to it, returns the whole + * stream, after redirects, and if the URL is valid);
  • + *
  • an {@link ItagItem}, which needs to contain the following information: + *
      + *
    • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
    • + *
    • its bitrate;
    • + *
    • its mime type;
    • + *
    • its codec(s);
    • + *
    • for an audio stream: its audio channels;
    • + *
    • for a video stream: its width and height.
    • + *
    + *
  • + *
  • the duration of the video, which will be used if the duration could not be + * parsed from the {@link ItagItem}.
  • + *
+ *

+ * + *

In order to generate the DASH manifest, this method will: + *

    + *
  • request the base URL of the stream with a HEAD request;
  • + *
  • follow its redirection(s), if any;
  • + *
  • save the last URL;
  • + *
  • use the information provided in the {@link ItagItem} to generate all + * elements of the DASH manifest.
  • + *
+ *

+ * + *

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

    + *
  • for progressive streams, the base URL of the stream with a HEAD request;
  • + *
  • for OTF streams and for post-live-DVR streams, the base URL of the stream, to which + * are appended {@link #SQ_0} and {@link #RN_0} params, with a GET request for streaming + * URLs from the WEB client and a POST request for the ones from the Android client;
  • + *
  • for streaming URLs from the WEB client, the {@link #ALR_YES} param is also added. + *
  • + *
+ *

+ * + * @param baseStreamingUrl the base URL of the stream, which cannot be null + * @param itagItem the {@link ItagItem} of stream, which cannot be null + * @param deliveryType the {@link DeliveryType} of the stream + * @return the "initialization" response, without redirections on the network on which the + * request(s) is/are made + * @throws YoutubeDashManifestCreationException if something goes wrong when fetching the + * "initialization" response and/or its redirects + */ + @Nonnull + private static Response getInitializationResponse(@Nonnull String baseStreamingUrl, + @Nonnull final ItagItem itagItem, + final DeliveryType deliveryType) + throws YoutubeDashManifestCreationException { + final boolean isAWebStreamingUrl = isWebStreamingUrl(baseStreamingUrl); + final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); + final boolean isAnAndroidStreamingUrlAndAPostLiveDvrStream = isAnAndroidStreamingUrl + && deliveryType == DeliveryType.LIVE; + if (isAWebStreamingUrl) { + baseStreamingUrl += ALR_YES; + } + baseStreamingUrl = appendRnParamAndSqParamIfNeeded(baseStreamingUrl, deliveryType); + + final Downloader downloader = NewPipe.getDownloader(); + if (isAWebStreamingUrl) { + final String mimeTypeExpected = itagItem.getMediaFormat().getMimeType(); + if (!isNullOrEmpty(mimeTypeExpected)) { + return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl, + mimeTypeExpected, deliveryType); + } + } else if (isAnAndroidStreamingUrlAndAPostLiveDvrStream) { + try { + final Map> headers = new HashMap<>(); + headers.put("User-Agent", Collections.singletonList( + getYoutubeAndroidAppUserAgent(null))); + final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); + 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); + } + } + + try { + final Map> headers = new HashMap<>(); + if (isAnAndroidStreamingUrl) { + headers.put("User-Agent", Collections.singletonList( + getYoutubeAndroidAppUserAgent(null))); + } + + return downloader.get(baseStreamingUrl, headers); + } 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); + } else { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: error when trying to get the streaming URL response", e); + } + } + } + + /** + * 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 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 + */ + @Nonnull + private static String appendRnParamAndSqParamIfNeeded( + @Nonnull String baseStreamingUrl, + @Nonnull final DeliveryType deliveryType) { + if (deliveryType != DeliveryType.PROGRESSIVE) { + baseStreamingUrl += SQ_0; + } + return baseStreamingUrl + RN_0; + } + + /** + * Get a URL on which no redirection between playback hosts should be present on the network + * and/or IP used to fetch the streaming URL. + * + *

+ * This method will follow redirects for web clients, which works in the following way: + *

    + *
  1. the {@link #ALR_YES} param is appended to all streaming URLs
  2. + *
  3. if no redirection occurs, the video server will return the streaming data;
  4. + *
  5. if a redirection occurs, the server will respond with HTTP status code 200 and a + * text/plain mime type. The redirection URL is the response body;
  6. + *
  7. the redirection URL is requested and the steps above from step 2 are repeated (until + * too many redirects are reached, of course).
  8. + *
+ *

+ * + * @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 + * @param deliveryType the {@link DeliveryType} of the stream + * @return the response of the stream which should have no redirections + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to get the + * response without any redirection + */ + @Nonnull + private static Response getStreamingWebUrlWithoutRedirects( + @Nonnull final Downloader downloader, + @Nonnull String streamingUrl, + @Nonnull final String responseMimeTypeExpected, + @Nonnull final DeliveryType deliveryType) throws YoutubeDashManifestCreationException { + try { + final Map> headers = new HashMap<>(); + addClientInfoHeaders(headers); + + String responseMimeType = ""; + + int redirectsCount = 0; + while (!responseMimeType.equals(responseMimeTypeExpected) + && redirectsCount < MAXIMUM_REDIRECT_COUNT) { + final Response response; + // We can use head requests to reduce the request size, but only for progressive + // streams + if (deliveryType == DeliveryType.PROGRESSIVE) { + response = downloader.head(streamingUrl, headers); + } else { + response = downloader.get(streamingUrl, headers); + } + + final int responseCode = response.responseCode(); + 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); + } 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 " + + responseCode); + } else { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: could not fetch the URL of the progressive stream: response code " + + responseCode); + } + } + + // A valid response must include a Content-Type header, so we can require that + // the response from video servers has this header. + try { + responseMimeType = Objects.requireNonNull(response.getHeader( + "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); + } + + // The response body is the redirection URL + if (responseMimeType.equals("text/plain")) { + streamingUrl = response.responseBody(); + redirectsCount++; + } else { + return response; + } + } + + 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"); + } + + // 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"); + } 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); + } + } + + /** + * Collect all segments from an OTF stream, by parsing the string array which contains all the + * sequences. + * + * @param segmentDuration the string array which contains all the sequences extracted with the + * regular expression + * @throws YoutubeDashManifestCreationException if something goes wrong when trying to collect + * the segments of the OTF stream + */ + private static void collectSegmentsData(@Nonnull final String[] segmentDuration) + throws YoutubeDashManifestCreationException { + try { + 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]); + SEGMENTS_DURATION.add(segmentLength); + DURATION_REPETITIONS.add(segmentRepeatCount); + } + } catch (final NumberFormatException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: unable to get the segments of the stream", e); + } + } + + /** + * Get the duration of an OTF stream. + * + *

+ * 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 } element of the manifest. + * + *

+ * The generated {@code } element looks like the manifest returned into the player + * response of videos with OTF streams: + *

+ *

+ * {@code } + * (where {@code $duration$} represents the duration in seconds (a number with 3 digits after + * the decimal point) + *

+ *

+ * 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 } element + * @throws YoutubeDashManifestCreationException if something goes wrong when generating/ + * appending the {@link Document object} or the + * {@code } element + */ + private static Document generateDocumentAndMpdElement(@Nonnull final String[] segmentDuration, + final DeliveryType deliveryType, + @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 Element mpdElement = document.createElement("MPD"); + document.appendChild(mpdElement); + + final Attr xmlnsXsiAttribute = document.createAttribute("xmlns:xsi"); + xmlnsXsiAttribute.setValue("http://www.w3.org/2001/XMLSchema-instance"); + mpdElement.setAttributeNode(xmlnsXsiAttribute); + + final Attr xmlns = document.createAttribute("xmlns"); + xmlns.setValue("urn:mpeg:DASH:schema:MPD:2011"); + mpdElement.setAttributeNode(xmlns); + + final Attr xsiSchemaLocationAttribute = document.createAttribute("xsi:schemaLocation"); + xsiSchemaLocationAttribute.setValue("urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); + mpdElement.setAttributeNode(xsiSchemaLocationAttribute); + + final Attr minBufferTimeAttribute = document.createAttribute("minBufferTime"); + minBufferTimeAttribute.setValue("PT1.500S"); + mpdElement.setAttributeNode(minBufferTimeAttribute); + + final Attr profilesAttribute = document.createAttribute("profiles"); + profilesAttribute.setValue("urn:mpeg:dash:profile:full:2011"); + mpdElement.setAttributeNode(profilesAttribute); + + final Attr typeAttribute = document.createAttribute("type"); + typeAttribute.setValue("static"); + mpdElement.setAttributeNode(typeAttribute); + + final Attr mediaPresentationDurationAttribute = document.createAttribute( + "mediaPresentationDuration"); + final long streamDuration; + if (deliveryType == DeliveryType.LIVE) { + streamDuration = Integer.parseInt(segmentDuration[0]); + } else if (deliveryType == DeliveryType.OTF) { + streamDuration = getStreamDuration(segmentDuration); + } else { + final long itagItemDuration = itagItem.getApproxDurationMs(); + if (itagItemDuration != -1) { + streamDuration = itagItemDuration; + } else { + if (durationSecondsFallback > 0) { + 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"); + } + } + } + final double duration = streamDuration / 1000.0; + final String durationSeconds = String.format(Locale.ENGLISH, "%.3f", duration); + mediaPresentationDurationAttribute.setValue("PT" + durationSeconds + "S"); + 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); + } + + return document; + } + + /** + * Generate the {@code } element, appended as a child of the {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateDocumentAndMpdElement(String[], DeliveryType, ItagItem, long)}. + *

+ * + * @param document the {@link Document} on which the the {@code } element will be + * appended + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element to the + * document + */ + private static void generatePeriodElement(@Nonnull final Document document) + throws YoutubeDashManifestCreationException { + try { + final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); + final Element periodElement = document.createElement("Period"); + 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); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateDocumentAndMpdElement(String[], DeliveryType, ItagItem, long)}. + *

+ * + * @param document the {@link Document} on which the the {@code } element will be + * appended + * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element to the + * document + */ + private static void generateAdaptationSetElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws YoutubeDashManifestCreationException { + try { + final Element periodElement = (Element) document.getElementsByTagName("Period").item(0); + final Element adaptationSetElement = document.createElement("AdaptationSet"); + + final Attr idAttribute = document.createAttribute("id"); + idAttribute.setValue("0"); + adaptationSetElement.setAttributeNode(idAttribute); + + 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"); + } + + final Attr mimeTypeAttribute = document.createAttribute("mimeType"); + mimeTypeAttribute.setValue(mediaFormat.mimeType); + adaptationSetElement.setAttributeNode(mimeTypeAttribute); + + 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); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } + * element. + * + *

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

+ *

+ * {@code } + *

+ *

+ * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the the {@code } element will be + * appended + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element to the document + */ + private static void generateRoleElement(@Nonnull final Document document) + throws YoutubeDashManifestCreationException { + try { + final Element adaptationSetElement = (Element) document.getElementsByTagName( + "AdaptationSet").item(0); + final Element roleElement = document.createElement("Role"); + + final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); + schemeIdUriAttribute.setValue("urn:mpeg:DASH:role:2011"); + roleElement.setAttributeNode(schemeIdUriAttribute); + + final Attr valueAttribute = document.createAttribute("value"); + valueAttribute.setValue("main"); + roleElement.setAttributeNode(valueAttribute); + + adaptationSetElement.appendChild(roleElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the Role element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the the {@code } element will + * be appended + * @param itagItem the {@link ItagItem} to use, which cannot be null + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateRepresentationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws YoutubeDashManifestCreationException { + try { + final Element adaptationSetElement = (Element) document.getElementsByTagName( + "AdaptationSet").item(0); + final Element representationElement = document.createElement("Representation"); + + final int id = itagItem.id; + if (id <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the Representation element of the DASH manifest to the document: the id of the ItagItem is less than or equal to 0"); + } + final Attr idAttribute = document.createAttribute("id"); + idAttribute.setValue(String.valueOf(id)); + representationElement.setAttributeNode(idAttribute); + + final String codec = itagItem.getCodec(); + if (isNullOrEmpty(codec)) { + throw new YoutubeDashManifestCreationException( + "Could not generate the AdaptationSet element of the DASH manifest to the document: the codecs value is null or empty"); + } + final Attr codecsAttribute = document.createAttribute("codecs"); + codecsAttribute.setValue(codec); + representationElement.setAttributeNode(codecsAttribute); + + final Attr startWithSAPAttribute = document.createAttribute("startWithSAP"); + startWithSAPAttribute.setValue("1"); + representationElement.setAttributeNode(startWithSAPAttribute); + + final Attr maxPlayoutRateAttribute = document.createAttribute("maxPlayoutRate"); + maxPlayoutRateAttribute.setValue("1"); + representationElement.setAttributeNode(maxPlayoutRateAttribute); + + final int bitrate = itagItem.getBitrate(); + if (bitrate <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the Representation element of the DASH manifest to the document: the bitrate of the ItagItem is less than or equal to 0"); + } + final Attr bandwidthAttribute = document.createAttribute("bandwidth"); + bandwidthAttribute.setValue(String.valueOf(bitrate)); + representationElement.setAttributeNode(bandwidthAttribute); + + final ItagItem.ItagType itagType = itagItem.itagType; + + if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { + final int height = itagItem.getHeight(); + final int width = itagItem.getWidth(); + if (height <= 0 && width <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the Representation element of the DASH manifest to the document: the width and the height of the ItagItem are less than or equal to 0"); + } + + if (width > 0) { + final Attr widthAttribute = document.createAttribute("width"); + widthAttribute.setValue(String.valueOf(width)); + representationElement.setAttributeNode(widthAttribute); + } + + final Attr heightAttribute = document.createAttribute("height"); + heightAttribute.setValue(String.valueOf(itagItem.getHeight())); + representationElement.setAttributeNode(heightAttribute); + + final int fps = itagItem.getFps(); + if (fps > 0) { + final Attr frameRateAttribute = document.createAttribute("frameRate"); + frameRateAttribute.setValue(String.valueOf(fps)); + representationElement.setAttributeNode(frameRateAttribute); + } + } + + if (itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { + final Attr audioSamplingRateAttribute = document.createAttribute( + "audioSamplingRate"); + audioSamplingRateAttribute.setValue(String.valueOf(itagItem.getSampleRate())); + } + + adaptationSetElement.appendChild(representationElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the Representation element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

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

+ *

+ * It will produce the following element: + *
+ * {@code + * (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second + * parameter of this method) + *

+ *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the {@code } + * element will be appended + * @param itagItem the {@link ItagItem} to use, which cannot be null + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the + * {@code } element + * to the document + */ + private static void generateAudioChannelConfigurationElement( + @Nonnull final Document document, + @Nonnull final ItagItem itagItem) throws YoutubeDashManifestCreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + "Representation").item(0); + final Element audioChannelConfigurationElement = document.createElement( + "AudioChannelConfiguration"); + + final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); + schemeIdUriAttribute.setValue( + "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); + audioChannelConfigurationElement.setAttributeNode(schemeIdUriAttribute); + + final Attr valueAttribute = document.createAttribute("value"); + final int audioChannels = itagItem.getAudioChannels(); + if (audioChannels <= 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the audioChannels value is less than or equal to 0 (" + audioChannels + ")"); + } + valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); + audioChannelConfigurationElement.setAttributeNode(valueAttribute); + + representationElement.appendChild(audioChannelConfigurationElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the AudioChannelConfiguration element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

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

+ *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param baseUrl the base URL of the stream, which cannot be null and will be set as the + * content of the {@code } element + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateBaseUrlElement(@Nonnull final Document document, + @Nonnull final String baseUrl) + throws YoutubeDashManifestCreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + "Representation").item(0); + final Element baseURLElement = document.createElement("BaseURL"); + baseURLElement.setTextContent(baseUrl); + representationElement.appendChild(baseURLElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the BaseURL element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * 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 } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param itagItem the {@link ItagItem} to use, which cannot be null + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateSegmentBaseElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws YoutubeDashManifestCreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + "Representation").item(0); + + final Element segmentBaseElement = document.createElement("SegmentBase"); + + final Attr indexRangeAttribute = document.createAttribute("indexRange"); + + final int indexStart = itagItem.getIndexStart(); + if (indexStart < 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the indexStart value of the ItagItem is less than to 0 (" + indexStart + ")"); + } + final int indexEnd = itagItem.getIndexEnd(); + if (indexEnd < 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the indexEnd value of the ItagItem is less than to 0 (" + indexStart + ")"); + } + + indexRangeAttribute.setValue(indexStart + "-" + indexEnd); + segmentBaseElement.setAttributeNode(indexRangeAttribute); + + representationElement.appendChild(segmentBaseElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the SegmentBase element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * 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 } element needs to be generated before this element with + * {@link #generateSegmentBaseElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param itagItem the {@link ItagItem} to use, which cannot be null + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateInitializationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws YoutubeDashManifestCreationException { + try { + final Element segmentBaseElement = (Element) document.getElementsByTagName( + "SegmentBase").item(0); + + final Element initializationElement = document.createElement("Initialization"); + + final Attr rangeAttribute = document.createAttribute("range"); + + final int initStart = itagItem.getInitStart(); + if (initStart < 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the initStart value of the ItagItem is less than to 0 (" + initStart + ")"); + } + final int initEnd = itagItem.getInitEnd(); + if (initEnd < 0) { + throw new YoutubeDashManifestCreationException( + "Could not generate the DASH manifest: the initEnd value of the ItagItem is less than to 0 (" + initEnd + ")"); + } + + rangeAttribute.setValue(initStart + "-" + initEnd); + initializationElement.setAttributeNode(rangeAttribute); + + segmentBaseElement.appendChild(initializationElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the Initialization element of the DASH manifest to the document", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

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

+ *

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

    + *
  • {@code startNumber}, which takes the value {@code 0} for post-live-DVR streams and + * {@code 1} for OTF streams;
  • + *
  • {@code timescale}, which is always {@code 1000};
  • + *
  • {@code media}, which is the base URL of the stream on which is appended + * {@code &sq=$Number$&rn=$Number$};
  • + *
  • {@code initialization} (only for OTF streams), which is the base URL of the stream + * on which is appended {@link #SQ_0} and {@link #RN_0}.
  • + *
+ *

+ * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

+ * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param baseUrl the base URL of the OTF/post-live-DVR stream + * @param deliveryType the stream {@link DeliveryType delivery type} + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateSegmentTemplateElement(@Nonnull final Document document, + @Nonnull final String baseUrl, + final DeliveryType deliveryType) + throws YoutubeDashManifestCreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + "Representation").item(0); + final Element segmentTemplateElement = document.createElement("SegmentTemplate"); + + final Attr startNumberAttribute = document.createAttribute("startNumber"); + final boolean isDeliveryTypeLive = deliveryType == DeliveryType.LIVE; + // The first sequence of post DVR streams is the beginning of the video stream and not + // an initialization segment + final String startNumberValue = isDeliveryTypeLive ? "0" : "1"; + startNumberAttribute.setValue(startNumberValue); + segmentTemplateElement.setAttributeNode(startNumberAttribute); + + final Attr timescaleAttribute = document.createAttribute("timescale"); + timescaleAttribute.setValue("1000"); + segmentTemplateElement.setAttributeNode(timescaleAttribute); + + // Post-live-DVR/ended livestreams streams don't require an initialization sequence + if (!isDeliveryTypeLive) { + final Attr initializationAttribute = document.createAttribute("initialization"); + initializationAttribute.setValue(baseUrl + SQ_0 + RN_0); + segmentTemplateElement.setAttributeNode(initializationAttribute); + } + + final Attr mediaAttribute = document.createAttribute("media"); + mediaAttribute.setValue(baseUrl + "&sq=$Number$&rn=$Number$"); + segmentTemplateElement.setAttributeNode(mediaAttribute); + + 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); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

+ * The {@code } element needs to be generated before this element with + * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. + *

+ * + * @param document the {@link Document} on which the the {@code } element will + * be appended + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element + * to the document + */ + private static void generateSegmentTimelineElement(@Nonnull final Document document) + throws YoutubeDashManifestCreationException { + try { + final Element segmentTemplateElement = (Element) document.getElementsByTagName( + "SegmentTemplate").item(0); + final Element segmentTimelineElement = document.createElement("SegmentTimeline"); + + 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); + } + } + + /** + * Generate segment elements for OTF streams. + * + *

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

+ *

+ * The {@code } element needs to be generated before this element with + * {@link #generateSegmentTimelineElement(Document)}. + *

+ * + * @param document the {@link Document} on which the the {@code } elements will be appended + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } elements to the + * document + */ + private static void generateSegmentElementsForOtfStreams(@Nonnull final Document document) + throws YoutubeDashManifestCreationException { + try { + if (SEGMENTS_DURATION.isEmpty() || DURATION_REPETITIONS.isEmpty()) { + throw new IllegalStateException( + "Duration of segments and/or repetition(s) of segments are unknown"); + } + final Element segmentTimelineElement = (Element) document.getElementsByTagName( + "SegmentTimeline").item(0); + + for (int i = 0; i < SEGMENTS_DURATION.size(); i++) { + final Element sElement = document.createElement("S"); + + final int durationRepetition = DURATION_REPETITIONS.get(i); + if (durationRepetition != 0) { + final Attr rAttribute = document.createAttribute("r"); + rAttribute.setValue(String.valueOf(durationRepetition)); + sElement.setAttributeNode(rAttribute); + } + + final Attr dAttribute = document.createAttribute("d"); + dAttribute.setValue(String.valueOf(SEGMENTS_DURATION.get(i))); + sElement.setAttributeNode(dAttribute); + + segmentTimelineElement.appendChild(sElement); + } + + } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the segment (S) elements of the DASH manifest to the document", e); + } + } + + /** + * Generate the segment element for post-live-DVR streams. + * + *

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

+ * + * @param document the {@link Document} on which the the {@code } element will + * be appended + * @param targetDurationSeconds the {@code targetDurationSec} value from player response's + * stream + * @param segmentCount the number of segments, extracted by the main method which + * generates manifests of post DVR livestreams + * @throws YoutubeDashManifestCreationException if something goes wrong when generating or + * appending the {@code } element to the + * document + */ + private static void generateSegmentElementForPostLiveDvrStreams( + @Nonnull final Document document, + final int targetDurationSeconds, + @Nonnull final String segmentCount) throws YoutubeDashManifestCreationException { + try { + final Element segmentTimelineElement = (Element) document.getElementsByTagName( + "SegmentTimeline").item(0); + final Element sElement = document.createElement("S"); + + final Attr dAttribute = document.createAttribute("d"); + dAttribute.setValue(String.valueOf(targetDurationSeconds * 1000)); + sElement.setAttributeNode(dAttribute); + + final Attr rAttribute = document.createAttribute("r"); + rAttribute.setValue(segmentCount); + sElement.setAttributeNode(rAttribute); + + segmentTimelineElement.appendChild(sElement); + } catch (final DOMException e) { + throw new YoutubeDashManifestCreationException( + "Could not generate or append the segment (S) elements of the DASH manifest to the document", e); + } + } + + /** + * Convert a DASH manifest {@link Document document} to a string. + * + * @param originalBaseStreamingUrl the original base URL of the stream + * @param document the document to be converted + * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the + * string generated (which is either + * {@link #GENERATED_OTF_MANIFESTS}, + * {@link #GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS} or + * {@link #GENERATED_PROGRESSIVE_STREAMS_MANIFESTS}) + * @return the DASH manifest {@link Document document} converted to a string + * @throws YoutubeDashManifestCreationException if something goes wrong when converting the + * {@link Document document} + */ + private static String buildResult( + @Nonnull final String originalBaseStreamingUrl, + @Nonnull final Document document, + @Nonnull final ManifestCreatorCache manifestCreatorCache) + throws YoutubeDashManifestCreationException { + try { + final StringWriter result = new StringWriter(); + final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + + final Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + transformer.transform(new DOMSource(document), new StreamResult(result)); + final String stringResult = result.toString(); + manifestCreatorCache.put(originalBaseStreamingUrl, stringResult); + return stringResult; + } catch (final Exception e) { + throw new YoutubeDashManifestCreationException( + "Could not convert the DASH manifest generated to a string", e); + } + } + + /** + * Get the number of cached OTF streams manifests. + * + * @return the number of cached OTF streams manifests + */ + public static int getOtfCachedManifestsSize() { + return GENERATED_OTF_MANIFESTS.size(); + } + + /** + * Get the number of cached post-live-DVR streams manifests. + * + * @return the number of cached post-live-DVR streams manifests + */ + public static int getPostLiveDvrStreamsCachedManifestsSize() { + return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.size(); + } + + /** + * Get the number of cached progressive manifests. + * + * @return the number of cached progressive manifests + */ + public static int getProgressiveStreamsCachedManifestsSize() { + return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.size(); + } + + /** + * Get the number of cached OTF, post-live-DVR streams and progressive manifests. + * + * @return the number of cached OTF, post-live-DVR streams and progressive manifests. + */ + public static int getSizeOfManifestsCaches() { + return GENERATED_OTF_MANIFESTS.size() + + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.size() + + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.size(); + } + + /** + * Get the clear factor of OTF streams manifests cache. + * + * @return the clear factor of OTF streams manifests cache. + */ + public static double getOtfStreamsClearFactor() { + return GENERATED_OTF_MANIFESTS.getClearFactor(); + } + + /** + * Get the clear factor of post-live-DVR streams manifests cache. + * + * @return the clear factor of post-live-DVR streams manifests cache. + */ + public static double getPostLiveDvrStreamsClearFactor() { + return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.getClearFactor(); + } + + /** + * Get the clear factor of progressive streams manifests cache. + * + * @return the clear factor of progressive streams manifests cache. + */ + public static double getProgressiveStreamsClearFactor() { + return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.getClearFactor(); + } + + /** + * Set the clear factor of cached OTF streams + * + * @param otfStreamsClearFactor the clear factor of OTF streams manifests cache. + */ + public static void setOtfStreamsClearFactor(final double otfStreamsClearFactor) { + GENERATED_OTF_MANIFESTS.setClearFactor(otfStreamsClearFactor); + } + + /** + * Set the clear factor of cached post-live-DVR streams + * + * @param postLiveDvrStreamsClearFactor the clear factor of post-live-DVR streams manifests + * cache. + */ + public static void setPostLiveDvrStreamsClearFactor( + final double postLiveDvrStreamsClearFactor) { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setClearFactor(postLiveDvrStreamsClearFactor); + } + + /** + * Set the clear factor of cached progressive streams + * + * @param progressiveStreamsClearFactor the clear factor of progressive streams manifests + * cache. + */ + public static void setProgressiveStreamsClearFactor( + final double progressiveStreamsClearFactor) { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setClearFactor(progressiveStreamsClearFactor); + } + + /** + * Set the clear factor of cached OTF, post-live-DVR and progressive streams. + * + * @param cachesClearFactor the clear factor of OTF, post-live-DVR and progressive streams + * manifests caches. + */ + public static void setCachesClearFactor(final double cachesClearFactor) { + GENERATED_OTF_MANIFESTS.setClearFactor(cachesClearFactor); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setClearFactor(cachesClearFactor); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setClearFactor(cachesClearFactor); + } + + /** + * Reset the clear factor of OTF streams cache to its + * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. + */ + public static void resetOtfStreamsClearFactor() { + GENERATED_OTF_MANIFESTS.resetClearFactor(); + } + + /** + * Reset the clear factor of post-live-DVR streams cache to its + * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. + */ + public static void resetPostLiveDvrStreamsClearFactor() { + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.resetClearFactor(); + } + + /** + * Reset the clear factor of progressive streams cache to its + * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. + */ + public static void resetProgressiveStreamsClearFactor() { + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.resetClearFactor(); + } + + /** + * Reset the clear factor of OTF, post-live-DVR and progressive streams caches to their + * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. + */ + public static void resetCachesClearFactor() { + GENERATED_OTF_MANIFESTS.resetClearFactor(); + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.resetClearFactor(); + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.resetClearFactor(); + } + + /** + * Set the limit of cached OTF 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 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 type of cache keys, which must be {@link Serializable serializable} + * @param the type of the second element of {@link Pair pairs} used as values of the cache, + * which must be {@link Serializable serializable} + */ +public final class ManifestCreatorCache + implements Serializable { + + /** + * The default maximum size of a manifest cache. + */ + public static final int DEFAULT_MAXIMUM_SIZE = Integer.MAX_VALUE; + + /** + * The default clear factor of a manifest cache. + */ + public static final double DEFAULT_CLEAR_FACTOR = 0.75; + + /** + * The {@link ConcurrentHashMap} used internally as the cache of manifests. + */ + private final ConcurrentHashMap> concurrentHashMap; + + /** + * The maximum size of the cache. + * + *

+ * 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 get(final K key) { + return concurrentHashMap.get(key); + } + + /** + * Adds a new element to the cache. + * + *

+ * 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 returnValue = concurrentHashMap.put(key, + new Pair<>(concurrentHashMap.size(), value)); + return returnValue == null ? null : returnValue.getSecond(); + } + + /** + * Clears the cached manifests. + * + *

+ * 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>> entriesToRemove = new ArrayList<>(); + + for (final Map.Entry> entry : concurrentHashMap.entrySet()) { + final Pair value = entry.getValue(); + if (value.getFirst() < difference) { + entriesToRemove.add(entry); + } else { + value.setFirst(value.getFirst() - difference); + } + } + + for (final Map.Entry> entry : entriesToRemove) { + concurrentHashMap.remove(entry.getKey(), entry.getValue()); + } + } +} From f6ec7f9a6120064fdf7348ff13e1adfbcab8492f Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 8 Mar 2022 19:12:09 +0100 Subject: [PATCH 09/38] Update DefaultStreamExtractorTest and SoundcloudStreamExtractorTest to support changes made in Stream classes --- .../services/DefaultStreamExtractorTest.java | 45 ++++++++++++------- .../SoundcloudStreamExtractorTest.java | 18 +++++--- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java index f8fb6e935..d9b4e6cde 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/DefaultStreamExtractorTest.java @@ -271,13 +271,20 @@ public abstract class DefaultStreamExtractorTest extends DefaultExtractorTest audioStreams = extractor.getAudioStreams(); assertEquals(2, audioStreams.size()); for (final AudioStream audioStream : audioStreams) { - final String mediaUrl = audioStream.getUrl(); + final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod(); + assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod, + "Wrong delivery method for stream " + audioStream.getId() + ": " + + deliveryMethod); + final String mediaUrl = audioStream.getContent(); if (audioStream.getFormat() == MediaFormat.OPUS) { - // assert that it's an OPUS 64 kbps media URL with a single range which comes from an HLS SoundCloud CDN + // Assert that it's an OPUS 64 kbps media URL with a single range which comes + // from an HLS SoundCloud CDN ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl); ExtractorAsserts.assertContains(".64.opus", mediaUrl); } if (audioStream.getFormat() == MediaFormat.MP3) { - // assert that it's a MP3 128 kbps media URL which comes from a progressive SoundCloud CDN - ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3", mediaUrl); + // Assert that it's a MP3 128 kbps media URL which comes from a progressive + // SoundCloud CDN + ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3", + mediaUrl); } } } From 6985167e63947a497ef27e40b29744b42160d296 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 8 Mar 2022 19:16:34 +0100 Subject: [PATCH 10/38] Add tests for YoutubeDashManifestCreator and ManifestCreatorCache The test added in YoutubeDashManifestCreator uses a video of the Creative Commons channel, licenced under the Creative Commons Attribution licence (reuse allowed). Also remove public keywords of tests in UtilsTest, as suggested by SonarLint, because they are not needed with Junit 5. --- .../YoutubeDashManifestCreatorTest.java | 620 ++++++++++++++++++ .../newpipe/extractor/utils/UtilsTest.java | 59 +- 2 files changed, 674 insertions(+), 5 deletions(-) create mode 100644 extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java 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 new file mode 100644 index 000000000..a644272d1 --- /dev/null +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java @@ -0,0 +1,620 @@ +package org.schabi.newpipe.extractor.services.youtube; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.schabi.newpipe.downloader.DownloaderTestImpl; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; +import org.schabi.newpipe.extractor.stream.Stream; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.*; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + +/** + * Test for {@link YoutubeDashManifestCreator}. + * + *

+ * 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 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 cache = new ManifestCreatorCache<>(); + cache.setMaximumSize(30); + setCacheContent(cache); + // 30 elements set -> cache resized to 23 -> 5 new elements set to the cache -> 28 + assertEquals(28, cache.size(), + "Wrong cache size with default clear factor and 30 as the maximum size"); + + cache.reset(); + cache.setMaximumSize(20); + cache.setClearFactor(0.5); + + setCacheContent(cache); + // 30 elements set -> cache resized to 10 -> 5 new elements set to the cache -> 15 + assertEquals(15, cache.size(), + "Wrong cache size with 0.5 as the clear factor and 20 as the maximum size"); + + // Clear factor and maximum size getters tests + assertEquals(0.5, cache.getClearFactor(), + "Wrong clear factor gotten from clear factor getter"); + assertEquals(20, cache.getMaximumSize(), + "Wrong maximum cache size gotten from maximum size getter"); + + // Resetters tests + cache.resetMaximumSize(); + assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), + "Wrong maximum cache size gotten from maximum size getter after maximum size reset"); + + cache.resetClearFactor(); + assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), + "Wrong clear factor gotten from clear factor getter after clear factor reset"); + } + + private void setCacheContent(@Nonnull final ManifestCreatorCache cache) { + int i = 0; + while (i < 26) { + cache.put(Character.toString((char) (97 + i)), "V"); + ++i; + } + + i = 0; + while (i < 9) { + cache.put("a" + (char) (97 + i), "V"); + ++i; + } + } } From aa4c10e7517f60359f6c3fc911c67d2cd125b79a Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 15 Mar 2022 11:19:13 +0100 Subject: [PATCH 11/38] Improve documentation and adress most of the requested changes Also fix some issues in several places, in the code and the documentation. --- .../extractors/BandcampStreamExtractor.java | 4 +- .../MediaCCCLiveStreamExtractor.java | 171 ++++++------- .../MediaCCCLiveStreamMapperDTO.java | 29 +++ .../extractors/MediaCCCStreamExtractor.java | 9 +- .../extractors/PeertubeStreamExtractor.java | 110 ++++---- .../extractors/SoundcloudStreamExtractor.java | 25 +- .../services/youtube/DashMpdParser.java | 56 ++-- .../extractor/services/youtube/ItagInfo.java | 37 --- .../extractor/services/youtube/ItagItem.java | 86 +++---- .../youtube/YoutubeDashManifestCreator.java | 218 ++++++++++------ .../youtube/YoutubeParsingHelper.java | 6 +- .../services/youtube/extractors/ItagInfo.java | 80 ++++++ .../extractors/YoutubeStreamExtractor.java | 242 ++++++++++++------ .../newpipe/extractor/stream/AudioStream.java | 27 +- .../extractor/stream/DeliveryMethod.java | 28 +- .../newpipe/extractor/stream/Stream.java | 60 +++-- .../newpipe/extractor/stream/StreamType.java | 23 +- .../extractor/stream/SubtitlesStream.java | 41 +-- .../newpipe/extractor/stream/VideoStream.java | 27 +- .../YoutubeDashManifestCreatorTest.java | 2 +- 20 files changed, 759 insertions(+), 522 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java index 9bb6d5c78..4b5d9d12a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampStreamExtractor.java @@ -120,9 +120,9 @@ public class BandcampStreamExtractor extends StreamExtractor { public String getThumbnailUrl() throws ParsingException { if (albumJson.isNull("art_id")) { return EMPTY_STRING; - } else { - return getImageUrl(albumJson.getLong("art_id"), true); } + + return getImageUrl(albumJson.getLong("art_id"), true); } @Nonnull diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 54c2d056c..09e01ce36 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -12,15 +12,16 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.stream.IntStream; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -58,9 +59,9 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { final JsonObject roomObject = rooms.getObject(r); if (getId().equals(conferenceObject.getString("slug") + "/" + roomObject.getString("slug"))) { - this.conference = conferenceObject; - this.group = groupObject; - this.room = roomObject; + conference = conferenceObject; + group = groupObject; + room = roomObject; return; } } @@ -109,122 +110,120 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { * Get the URL of the first DASH stream found. * *

- * There can be several DASH streams, so the URL of the first found is returned by this method. + * There can be several DASH streams, so the URL of the first one found is returned by this + * method. *

* *

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

*/ @Nonnull @Override public String getDashMpdUrl() throws ParsingException { - - for (int s = 0; s < room.getArray(STREAMS).size(); s++) { - final JsonObject stream = room.getArray(STREAMS).getObject(s); - final JsonObject urls = stream.getObject(URLS); - if (urls.has("dash")) { - return urls.getObject("dash").getString(URL, EMPTY_STRING); - } - } - - return EMPTY_STRING; + return getManifestOfDeliveryMethodWanted("dash"); } /** * Get the URL of the first HLS stream found. * *

- * There can be several HLS streams, so the URL of the first found is returned by this method. + * There can be several HLS streams, so the URL of the first one found is returned by this + * method. *

* *

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

*/ @Nonnull @Override public String getHlsUrl() { - for (int s = 0; s < room.getArray(STREAMS).size(); s++) { - final JsonObject stream = room.getArray(STREAMS).getObject(s); - final JsonObject urls = stream.getObject(URLS); - if (urls.has("hls")) { - return urls.getObject("hls").getString(URL, EMPTY_STRING); - } - } - return EMPTY_STRING; + return getManifestOfDeliveryMethodWanted("hls"); + } + + @Nonnull + private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryMethod) { + return room.getArray(STREAMS).stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .map(streamObject -> streamObject.getObject(URLS)) + .filter(urls -> urls.has(deliveryMethod)) + .map(urls -> urls.getObject(deliveryMethod).getString(URL, EMPTY_STRING)) + .findFirst() + .orElse(EMPTY_STRING); } @Override public List getAudioStreams() throws IOException, ExtractionException { - final List audioStreams = new ArrayList<>(); - IntStream.range(0, room.getArray(STREAMS).size()) - .mapToObj(s -> room.getArray(STREAMS).getObject(s)) - .filter(streamJsonObject -> streamJsonObject.getString("type").equals("audio")) - .forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet() - .forEach(type -> { - final JsonObject urlObject = streamJsonObject.getObject(URLS) - .getObject(type); - // The DASH manifest will be extracted with getDashMpdUrl - if (!type.equals("dash")) { - final AudioStream.Builder builder = new AudioStream.Builder() - .setId(urlObject.getString("tech", ID_UNKNOWN)) - .setContent(urlObject.getString(URL), true) - .setAverageBitrate(UNKNOWN_BITRATE); - if (type.equals("hls")) { - // We don't know with the type string what media format will - // have HLS streams. - // However, the tech string may contain some information - // about the media format used. - builder.setDeliveryMethod(DeliveryMethod.HLS); - } else { - builder.setMediaFormat(MediaFormat.getFromSuffix(type)); - } + return getStreams("audio", + dto -> { + final AudioStream.Builder builder = new AudioStream.Builder() + .setId(dto.getUrlValue().getString("tech", ID_UNKNOWN)) + .setContent(dto.getUrlValue().getString(URL), true) + .setAverageBitrate(UNKNOWN_BITRATE); - audioStreams.add(builder.build()); - } - })); + if ("hls".equals(dto.getUrlKey())) { + // We don't know with the type string what media format will + // have HLS streams. + // However, the tech string may contain some information + // about the media format used. + return builder.setDeliveryMethod(DeliveryMethod.HLS) + .build(); + } - return audioStreams; + return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey())) + .build(); + }); } @Override public List getVideoStreams() throws IOException, ExtractionException { - final List videoStreams = new ArrayList<>(); - IntStream.range(0, room.getArray(STREAMS).size()) - .mapToObj(s -> room.getArray(STREAMS).getObject(s)) - .filter(stream -> stream.getString("type").equals("video")) - .forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet() - .forEach(type -> { - final String resolution = - streamJsonObject.getArray("videoSize").getInt(0) - + "x" - + streamJsonObject.getArray("videoSize").getInt(1); - final JsonObject urlObject = streamJsonObject.getObject(URLS) - .getObject(type); - // The DASH manifest will be extracted with getDashMpdUrl - if (!type.equals("dash")) { - final VideoStream.Builder builder = new VideoStream.Builder() - .setId(urlObject.getString("tech", ID_UNKNOWN)) - .setContent(urlObject.getString(URL), true) - .setIsVideoOnly(false) - .setResolution(resolution); + return getStreams("video", + dto -> { + final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize"); - if (type.equals("hls")) { - // We don't know with the type string what media format will - // have HLS streams. - // However, the tech string may contain some information - // about the media format used. - builder.setDeliveryMethod(DeliveryMethod.HLS); - } else { - builder.setMediaFormat(MediaFormat.getFromSuffix(type)); - } + final VideoStream.Builder builder = new VideoStream.Builder() + .setId(dto.getUrlValue().getString("tech", ID_UNKNOWN)) + .setContent(dto.getUrlValue().getString(URL), true) + .setIsVideoOnly(false) + .setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1)); - videoStreams.add(builder.build()); - } - })); + if ("hls".equals(dto.getUrlKey())) { + // We don't know with the type string what media format will + // have HLS streams. + // However, the tech string may contain some information + // about the media format used. + return builder.setDeliveryMethod(DeliveryMethod.HLS) + .build(); + } - return videoStreams; + return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey())) + .build(); + }); + } + + private List getStreams( + @Nonnull final String streamType, + @Nonnull final Function converter) { + return room.getArray(STREAMS).stream() + // Ensure that we use only process JsonObjects + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + // Only process audio streams + .filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type"))) + // Flatmap Urls and ensure that we use only process JsonObjects + .flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream() + .filter(e -> e.getValue() instanceof JsonObject) + .map(e -> new MediaCCCLiveStreamMapperDTO( + streamJsonObj, + e.getKey(), + (JsonObject) e.getValue()))) + // The DASH manifest will be extracted with getDashMpdUrl + .filter(dto -> !"dash".equals(dto.getUrlKey())) + // Convert + .map(converter) + .collect(Collectors.toList()); } @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java new file mode 100644 index 000000000..c06ef736b --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java @@ -0,0 +1,29 @@ +package org.schabi.newpipe.extractor.services.media_ccc.extractors; + +import com.grack.nanojson.JsonObject; + +final class MediaCCCLiveStreamMapperDTO { + private final JsonObject streamJsonObj; + private final String urlKey; + private final JsonObject urlValue; + + MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, + final String urlKey, + final JsonObject urlValue) { + this.streamJsonObj = streamJsonObj; + this.urlKey = urlKey; + this.urlValue = urlValue; + } + + JsonObject getStreamJsonObj() { + return streamJsonObj; + } + + String getUrlKey() { + return urlKey; + } + + JsonObject getUrlValue() { + return urlValue; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 53cc53ad0..83dcc381e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -102,7 +102,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor { final JsonObject recording = recordings.getObject(i); final String mimeType = recording.getString("mime_type"); if (mimeType.startsWith("audio")) { - // First we need to resolve the actual video data from CDN + // First we need to resolve the actual video data from the CDN final MediaFormat mediaFormat; if (mimeType.endsWith("opus")) { mediaFormat = MediaFormat.OPUS; @@ -115,7 +115,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor { } // Don't use the containsSimilarStream method because it will always return - // false so if there are multiples audio streams available, only the first will + // false. So if there are multiple audio streams available, only the first one will // be extracted in this case. audioStreams.add(new AudioStream.Builder() .setId(recording.getString("filename", ID_UNKNOWN)) @@ -136,7 +136,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor { final JsonObject recording = recordings.getObject(i); final String mimeType = recording.getString("mime_type"); if (mimeType.startsWith("video")) { - // First we need to resolve the actual video data from CDN + // First we need to resolve the actual video data from the CDN final MediaFormat mediaFormat; if (mimeType.endsWith("webm")) { @@ -148,7 +148,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor { } // Don't use the containsSimilarStream method because it will remove the - // extraction of some video versions (mostly languages) + // extraction of some video versions (mostly languages). So if there are multiple + // video streams available, only the first one will be extracted in this case. videoStreams.add(new VideoStream.Builder() .setId(recording.getString("filename", ID_UNKNOWN)) .setContent(recording.getString("recording_url"), true) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index bec41f481..60666db92 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -220,10 +220,10 @@ public class PeertubeStreamExtractor extends StreamExtractor { if (getStreamType() == StreamType.VIDEO_STREAM && !isNullOrEmpty(json.getObject(FILES))) { return json.getObject(FILES).getString(PLAYLIST_URL, EMPTY_STRING); - } else { - return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL, - EMPTY_STRING); } + + return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL, + EMPTY_STRING); } @Override @@ -231,7 +231,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { assertPageFetched(); /* - Some videos have audio streams, some videos don't have audio streams. + Some videos have audio streams; others don't. So an audio stream may be available if a video stream is available. Audio streams are also not returned as separated streams for livestreams. That's why the extraction of audio streams is only run when there are video streams @@ -435,23 +435,21 @@ public class PeertubeStreamExtractor extends StreamExtractor { private void extractLiveVideoStreams() throws ParsingException { try { final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS); - for (final Object s : streamingPlaylists) { - if (!(s instanceof JsonObject)) { - continue; - } - final JsonObject stream = (JsonObject) s; - // Don't use the containsSimilarStream method because it will always return false - // so if there are multiples HLS URLs returned, only the first will be extracted in - // this case. - videoStreams.add(new VideoStream.Builder() - .setId(String.valueOf(stream.getInt("id", -1))) - .setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true) - .setIsVideoOnly(false) - .setResolution(EMPTY_STRING) - .setMediaFormat(MediaFormat.MPEG_4) - .setDeliveryMethod(DeliveryMethod.HLS) - .build()); - } + streamingPlaylists.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .map(stream -> new VideoStream.Builder() + .setId(String.valueOf(stream.getInt("id", -1))) + .setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true) + .setIsVideoOnly(false) + .setResolution(EMPTY_STRING) + .setMediaFormat(MediaFormat.MPEG_4) + .setDeliveryMethod(DeliveryMethod.HLS) + .build()) + // Don't use the containsSimilarStream method because it will always return + // false so if there are multiples HLS URLs returned, only the first will be + // extracted in this case. + .forEachOrdered(videoStreams::add); } catch (final Exception e) { throw new ParsingException("Could not get video streams", e); } @@ -463,14 +461,11 @@ public class PeertubeStreamExtractor extends StreamExtractor { // HLS streams try { - final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS); - for (final Object p : streamingPlaylists) { - if (!(p instanceof JsonObject)) { - continue; - } - final JsonObject playlist = (JsonObject) p; - final String playlistUrl = playlist.getString(PLAYLIST_URL); - getStreamsFromArray(playlist.getArray(FILES), playlistUrl); + for (final JsonObject playlist : json.getArray(STREAMING_PLAYLISTS).stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .collect(Collectors.toList())) { + getStreamsFromArray(playlist.getArray(FILES), playlist.getString(PLAYLIST_URL)); } } catch (final Exception e) { throw new ParsingException("Could not get streams", e); @@ -481,39 +476,31 @@ public class PeertubeStreamExtractor extends StreamExtractor { final String playlistUrl) throws ParsingException { try { /* - Starting with version 3.4.0 of PeerTube, HLS playlist of stream resolutions contain the - UUID of the stream, so we can't use the same system to get HLS playlist URL of streams - without fetching the master playlist. - These UUIDs are the same that the ones returned into the fileUrl and fileDownloadUrl + Starting with version 3.4.0 of PeerTube, the HLS playlist of stream resolutions + contains the UUID of the streams, so we can't use the same method to get the URL of + the HLS playlist without fetching the master playlist. + These UUIDs are the same as the ones returned into the fileUrl and fileDownloadUrl strings. */ final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl) && playlistUrl.endsWith("-master.m3u8"); - for (final Object s : streams) { - if (!(s instanceof JsonObject)) { - continue; - } - - final JsonObject stream = (JsonObject) s; - final String resolution = JsonUtils.getString(stream, "resolution.label"); - final String url; - final String idSuffix; + for (final JsonObject stream : streams.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .collect(Collectors.toList())) { // Extract stream version of streams first - if (stream.has(FILE_URL)) { - url = JsonUtils.getString(stream, FILE_URL); - idSuffix = FILE_URL; - } else { - url = JsonUtils.getString(stream, FILE_DOWNLOAD_URL); - idSuffix = FILE_DOWNLOAD_URL; - } - + final String url = JsonUtils.getString(stream, + stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL); if (isNullOrEmpty(url)) { // Not a valid stream URL return; } + final String resolution = JsonUtils.getString(stream, "resolution.label"); + final String idSuffix = stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL; + if (resolution.toLowerCase().contains("audio")) { // An audio stream addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution, @@ -535,12 +522,9 @@ public class PeertubeStreamExtractor extends StreamExtractor { @Nonnull final String idSuffix, @Nonnull final String format, @Nonnull final String url) throws ParsingException { - final String streamUrl; - if (FILE_DOWNLOAD_URL.equals(idSuffix)) { - streamUrl = JsonUtils.getString(streamJsonObject, FILE_URL); - } else { - streamUrl = url; - } + final String streamUrl = FILE_DOWNLOAD_URL.equals(idSuffix) + ? JsonUtils.getString(streamJsonObject, FILE_URL) + : url; return streamUrl.replace("-fragmented." + format, ".m3u8"); } @@ -593,7 +577,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { } } - // Add finally torrent URLs + // Finally, add torrent URLs final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl"); if (!isNullOrEmpty(torrentUrl)) { audioStreams.add(new AudioStream.Builder() @@ -627,14 +611,10 @@ public class PeertubeStreamExtractor extends StreamExtractor { // Then add HLS streams if (!isNullOrEmpty(playlistUrl)) { - final String hlsStreamUrl; - if (isInstanceUsingRandomUuidsForHlsStreams) { - hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, - extension, url); - } else { - hlsStreamUrl = playlistUrl.replace("master", JsonUtils.getNumber( - streamJsonObject, RESOLUTION_ID).toString()); - } + final String hlsStreamUrl = isInstanceUsingRandomUuidsForHlsStreams + ? getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, extension, + url) + : getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl); final VideoStream videoStream = new VideoStream.Builder() .setId(id + "-" + DeliveryMethod.HLS) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index 160bf572c..bacfd077e 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -233,11 +233,10 @@ public class SoundcloudStreamExtractor extends StreamExtractor { @Nullable private String getDownloadUrl(@Nonnull final String trackId) throws IOException, ExtractionException { - final Downloader dl = NewPipe.getDownloader(); - final JsonObject downloadJsonObject; + final String response = NewPipe.getDownloader().get(SOUNDCLOUD_API_V2_URL + "tracks/" + + trackId + "/download" + "?client_id=" + clientId()).responseBody(); - final String response = dl.get(SOUNDCLOUD_API_V2_URL + "tracks/" + trackId - + "/download" + "?client_id=" + clientId()).responseBody(); + final JsonObject downloadJsonObject; try { downloadJsonObject = JsonParser.object().from(response); } catch (final JsonParserException e) { @@ -293,7 +292,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { } } } catch (final Exception ignored) { - // Something went wrong when parsing this transcoding, don't add it to the + // Something went wrong when parsing this transcoding URL, so don't add it to the // audioStreams } } @@ -304,11 +303,15 @@ public class SoundcloudStreamExtractor extends StreamExtractor { * *

* A track can have the {@code downloadable} boolean set to {@code true}, but it doesn't mean - * we can download it: if the value of the {@code has_download_left} boolean is true, the track - * can be downloaded; otherwise not. + * we can download it. *

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

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

+ * + * @param audioStreams the audio streams to which the downloadable file is added */ public void extractDownloadableFileIfAvailable(final List audioStreams) { if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) { @@ -332,9 +335,9 @@ public class SoundcloudStreamExtractor extends StreamExtractor { * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. * *

- * This method downloads the provided manifest URL, find all web occurrences in the manifest, - * get the last segment URL, changes its segment range to {@code 0/track-length} and return - * this string. + * This method downloads the provided manifest URL, finds all web occurrences in the manifest, + * gets the last segment URL, changes its segment range to {@code 0/track-length}, and return + * this as a string. *

* * @param hlsManifestUrl the URL of the manifest to be parsed diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java index 77cee35bb..2b5774049 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java @@ -27,7 +27,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.DeliveryMethod; -import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.VideoStream; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -52,10 +51,26 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; +/** + * Class to extract streams from a DASH manifest. + * + *

+ * Note that this class relies on the YouTube's {@link ItagItem} class and should be made generic + * in order to be used on other services. + *

+ * + *

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

+ */ public final class DashMpdParser { private DashMpdParser() { } + /** + * Exception class which is thrown when something went wrong when using + * {@link DashMpdParser#getStreams(String)}. + */ public static class DashMpdParsingException extends ParsingException { DashMpdParsingException(final String message, final Exception e) { @@ -63,15 +78,21 @@ public final class DashMpdParser { } } + /** + * Class which represents the result of a DASH MPD file parsing by {@link DashMpdParser}. + * + *

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

+ */ public static class Result { private final List videoStreams; private final List videoOnlyStreams; private final List audioStreams; - - public Result(final List videoStreams, - final List videoOnlyStreams, - final List audioStreams) { + Result(final List videoStreams, + final List videoOnlyStreams, + final List audioStreams) { this.videoStreams = videoStreams; this.videoOnlyStreams = videoOnlyStreams; this.audioStreams = audioStreams; @@ -90,19 +111,22 @@ public final class DashMpdParser { } } - // TODO: Make this class generic and decouple from YouTube's ItagItem class. - /** - * Will try to download and parse the DASH manifest (using {@link StreamInfo#getDashMpdUrl()}), - * adding items that are listed in the {@link ItagItem} class. - *

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

- * Info about DASH MPD can be found here + * This method will try to download and parse the YouTube DASH MPD manifest URL provided to get + * supported {@link AudioStream}s and {@link VideoStream}s. * - * @param dashMpdUrl URL to the DASH MPD + *

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

+ * + * @param dashMpdUrl the URL of the DASH MPD manifest + * @return a {@link Result} which contains all video, video-only and audio streams extracted + * and supported by the extractor (so the ones for which {@link ItagItem#isSupported(int)} + * returns {@code true}). + * @throws DashMpdParsingException if something went wrong when downloading or parsing the + * manifest * @see - * www.brendanlog.com + * www.brendanlong.com's page about the structure of an MPEG-DASH MPD manifest */ @Nonnull public static Result getStreams(final String dashMpdUrl) @@ -188,7 +212,7 @@ public final class DashMpdParser { throws TransformerException { final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); - // Clone element so we can freely modify it + // Clone the element so we can freely modify it final Element adaptationSet = (Element) representation.getParentNode(); final Element adaptationSetClone = (Element) adaptationSet.cloneNode(true); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java deleted file mode 100644 index cdb5dc2de..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagInfo.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.schabi.newpipe.extractor.services.youtube; - -import javax.annotation.Nonnull; -import java.io.Serializable; - -public final class ItagInfo implements Serializable { - - @Nonnull - private final String content; - @Nonnull - private final ItagItem itagItem; - private boolean isUrl; - - public ItagInfo(@Nonnull final String content, - @Nonnull final ItagItem itagItem) { - this.content = content; - this.itagItem = itagItem; - } - - public void setIsUrl(final boolean isUrl) { - this.isUrl = isUrl; - } - - @Nonnull - public String getContent() { - return content; - } - - @Nonnull - public ItagItem getItagItem() { - return itagItem; - } - - public boolean getIsUrl() { - return isUrl; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java index 79f44078f..9608de8ec 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java @@ -237,11 +237,15 @@ public class ItagItem implements Serializable { } /** - * Get the frame rate per second. + * Get the frame rate. * *

- * It defaults to the standard value associated with this itag and is set to the {@code fps} - * value returned in the corresponding itag in the YouTube player response. + * It is set to the {@code fps} value returned in the corresponding itag in the YouTube player + * response. + *

+ * + *

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

* *

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

* - * @return the frame rate per second or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} + * @return the frame rate or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} */ public int getFps() { return fps; } /** - * Set the frame rate per second. + * Set the frame rate. * *

* It is only known for video itags, so {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} is set/used for * non video itags or if the sample rate value is less than or equal to 0. *

* - * @param fps the frame rate per second + * @param fps the frame rate */ public void setFps(final int fps) { - if (fps > 0) { - this.fps = fps; - } else { - this.fps = FPS_NOT_APPLICABLE_OR_UNKNOWN; - } + this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN; } public int getInitStart() { @@ -314,13 +314,13 @@ public class ItagItem implements Serializable { } /** - * Get the resolution string associated to this {@code ItagItem}. + * Get the resolution string associated with this {@code ItagItem}. * *

* It is only known for video itags. *

* - * @return the resolution string associated to this {@code ItagItem} or + * @return the resolution string associated with this {@code ItagItem} or * {@code null}. */ @Nullable @@ -361,7 +361,7 @@ public class ItagItem implements Serializable { * *

* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is returned for non audio - * itags or if the sample rate is unknown. + * itags, or if the sample rate is unknown. *

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

- * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non video - * itags or if the sample rate value is less than or equal to 0. + * It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non audio + * itags, or if the sample rate value is less than or equal to 0. *

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

- * It is only known for audio streams, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is - * returned for video streams or if it is unknown. + * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is + * returned for non audio itags, or if it is unknown. *

* * @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} @@ -406,28 +406,26 @@ public class ItagItem implements Serializable { * Set the number of audio channels. * *

- * It is only known for audio itag, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is - * set/used for non audio itags or if the {@code audioChannels} value is less than or equal to + * It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is + * set/used for non audio itags, or if the {@code audioChannels} value is less than or equal to * 0. *

* * @param audioChannels the number of audio channels of an audio itag */ public void setAudioChannels(final int audioChannels) { - if (audioChannels > 0) { - this.audioChannels = audioChannels; - } else { - this.audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN; - } + this.audioChannels = audioChannels > 0 + ? audioChannels + : AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN; } /** * Get the {@code targetDurationSec} value. * *

- * This value is an average time in seconds of sequences duration of livestreams and ended - * livestreams. It is only returned for these stream types by YouTube and makes no sense for - * videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for video streams. + * This value is the average time in seconds of the duration of sequences of livestreams and + * ended livestreams. It is only returned by YouTube for these stream types, and makes no sense + * for videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for those. *

* * @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN} @@ -440,25 +438,23 @@ public class ItagItem implements Serializable { * Set the {@code targetDurationSec} value. * *

- * This value is an average time in seconds of sequences duration of livestreams and ended - * livestreams. + * This value is the average time in seconds of the duration of sequences of livestreams and + * ended livestreams. *

* *

- * It is only returned for these stream types by YouTube and makes no sense for - * videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if - * this value is less than or equal to 0. + * It is only returned for these stream types by YouTube and makes no sense for videos, so + * {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if this value is + * less than or equal to 0. *

* * @param targetDurationSec the target duration of a segment of streams which are using the * live delivery method type */ public void setTargetDurationSec(final int targetDurationSec) { - if (targetDurationSec > 0) { - this.targetDurationSec = targetDurationSec; - } else { - this.targetDurationSec = TARGET_DURATION_SEC_UNKNOWN; - } + this.targetDurationSec = targetDurationSec > 0 + ? targetDurationSec + : TARGET_DURATION_SEC_UNKNOWN; } /** @@ -487,11 +483,9 @@ public class ItagItem implements Serializable { * milliseconds */ public void setApproxDurationMs(final long approxDurationMs) { - if (approxDurationMs > 0) { - this.approxDurationMs = approxDurationMs; - } else { - this.approxDurationMs = APPROX_DURATION_MS_UNKNOWN; - } + this.approxDurationMs = approxDurationMs > 0 + ? approxDurationMs + : APPROX_DURATION_MS_UNKNOWN; } /** @@ -519,10 +513,6 @@ public class ItagItem implements Serializable { * @param contentLength the content length of a DASH progressive stream */ public void setContentLength(final long contentLength) { - if (contentLength > 0) { - this.contentLength = contentLength; - } else { - this.contentLength = CONTENT_LENGTH_UNKNOWN; - } + this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_UNKNOWN; } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java index 62d833f51..2d7f7fe22 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java @@ -35,7 +35,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.*; * It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages. *

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

*/ PROGRESSIVE, + /** * YouTube's OTF delivery method which uses a sequence parameter to get segments of * streams. @@ -124,12 +124,14 @@ public final class YoutubeDashManifestCreator { * metadata needed to build the stream source (sidx boxes, segment length, segment count, * duration, ...) *

+ * *

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

*/ OTF, + /** * YouTube's delivery method for livestreams which uses a sequence parameter to get * segments of streams. @@ -139,6 +141,7 @@ public final class YoutubeDashManifestCreator { * metadata (sidx boxes, segment length, ...), which make no need of an initialization * segment. *

+ * *

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

@@ -225,27 +228,27 @@ public final class YoutubeDashManifestCreator { */ @Nonnull public static String createDashManifestFromOtfStreamingUrl( - @Nonnull String otfBaseStreamingUrl, + @Nonnull final String otfBaseStreamingUrl, @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) - throws YoutubeDashManifestCreationException { + final long durationSecondsFallback) throws YoutubeDashManifestCreationException { if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) { - return GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl).getSecond(); + return Objects.requireNonNull(GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl)) + .getSecond(); } - final String originalOtfBaseStreamingUrl = otfBaseStreamingUrl; + String realOtfBaseStreamingUrl = otfBaseStreamingUrl; // Try to avoid redirects when streaming the content by saving the last URL we get // from video servers. - final Response response = getInitializationResponse(otfBaseStreamingUrl, + final Response response = getInitializationResponse(realOtfBaseStreamingUrl, itagItem, DeliveryType.OTF); - otfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) + realOtfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) .replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING); final int responseCode = response.responseCode(); if (responseCode != 200) { throw new YoutubeDashManifestCreationException( - "Unable to create the DASH manifest: could not get the initialization URL of the OTF stream: response code " - + responseCode); + "Unable to create the DASH manifest: could not get the initialization URL of " + + "the OTF stream: response code " + responseCode); } final String[] segmentDuration; @@ -266,7 +269,8 @@ public final class YoutubeDashManifestCreator { } } catch (final Exception e) { throw new YoutubeDashManifestCreationException( - "Unable to generate the DASH manifest: could not get the duration of segments", e); + "Unable to generate the DASH manifest: could not get the duration of segments", + e); } final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF, @@ -278,7 +282,7 @@ public final class YoutubeDashManifestCreator { if (itagItem.itagType == ItagItem.ItagType.AUDIO) { generateAudioChannelConfigurationElement(document, itagItem); } - generateSegmentTemplateElement(document, otfBaseStreamingUrl, DeliveryType.OTF); + generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF); generateSegmentTimelineElement(document); collectSegmentsData(segmentDuration); generateSegmentElementsForOtfStreams(document); @@ -286,7 +290,7 @@ public final class YoutubeDashManifestCreator { SEGMENTS_DURATION.clear(); DURATION_REPETITIONS.clear(); - return buildResult(originalOtfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); + return buildResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); } /** @@ -358,36 +362,37 @@ public final class YoutubeDashManifestCreator { */ @Nonnull public static String createDashManifestFromPostLiveStreamDvrStreamingUrl( - @Nonnull String postLiveStreamDvrStreamingUrl, + @Nonnull final String postLiveStreamDvrStreamingUrl, @Nonnull final ItagItem itagItem, final int targetDurationSec, - final long durationSecondsFallback) - throws YoutubeDashManifestCreationException { + final long durationSecondsFallback) throws YoutubeDashManifestCreationException { if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) { - return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(postLiveStreamDvrStreamingUrl) - .getSecond(); + return Objects.requireNonNull(GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get( + postLiveStreamDvrStreamingUrl)).getSecond(); } - final String originalPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; + String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; final String streamDuration; final String segmentCount; if (targetDurationSec <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the targetDurationSec value is less than or equal to 0 (" + targetDurationSec + ")"); + "Could not generate the DASH manifest: the targetDurationSec value is less " + + "than or equal to 0 (" + targetDurationSec + ")"); } try { // Try to avoid redirects when streaming the content by saving the latest URL we get // from video servers. - final Response response = getInitializationResponse(postLiveStreamDvrStreamingUrl, + final Response response = getInitializationResponse(realPostLiveStreamDvrStreamingUrl, itagItem, DeliveryType.LIVE); - postLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) + realPostLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING) .replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING); final int responseCode = response.responseCode(); if (responseCode != 200) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code " + "Could not generate the DASH manifest: could not get the initialization " + + "segment of the post-live-DVR stream: response code " + responseCode); } @@ -396,15 +401,18 @@ public final class YoutubeDashManifestCreator { segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); } catch (final IndexOutOfBoundsException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR streaming URL", e); + "Could not generate the DASH manifest: could not get the value of the " + + "X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR" + + "streaming URL", e); } if (isNullOrEmpty(segmentCount)) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the number of segments of the post-live-DVR stream"); + "Could not generate the DASH manifest: could not get the number of segments of" + + "the post-live-DVR stream"); } - final Document document = generateDocumentAndMpdElement(new String[]{streamDuration}, + final Document document = generateDocumentAndMpdElement(new String[] {streamDuration}, DeliveryType.LIVE, itagItem, durationSecondsFallback); generatePeriodElement(document); generateAdaptationSetElement(document, itagItem); @@ -413,11 +421,12 @@ public final class YoutubeDashManifestCreator { if (itagItem.itagType == ItagItem.ItagType.AUDIO) { generateAudioChannelConfigurationElement(document, itagItem); } - generateSegmentTemplateElement(document, postLiveStreamDvrStreamingUrl, DeliveryType.LIVE); + generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl, + DeliveryType.LIVE); generateSegmentTimelineElement(document); generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); - return buildResult(originalPostLiveStreamDvrStreamingUrl, document, + return buildResult(postLiveStreamDvrStreamingUrl, document, GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS); } @@ -486,13 +495,14 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) { - return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(progressiveStreamingBaseUrl) - .getSecond(); + return Objects.requireNonNull(GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get( + progressiveStreamingBaseUrl)).getSecond(); } if (durationSecondsFallback <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the durationSecondsFallback value is less than or equal to 0 (" + durationSecondsFallback + ")"); + "Could not generate the DASH manifest: the durationSecondsFallback value is" + + "less than or equal to 0 (" + durationSecondsFallback + ")"); } final Document document = generateDocumentAndMpdElement(new String[]{}, @@ -508,7 +518,8 @@ public final class YoutubeDashManifestCreator { generateSegmentBaseElement(document, itagItem); generateInitializationElement(document, itagItem); - return buildResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); + return buildResult(progressiveStreamingBaseUrl, document, + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); } /** @@ -564,7 +575,8 @@ public final class YoutubeDashManifestCreator { return downloader.post(baseStreamingUrl, headers, emptyBody); } catch (final IOException | ExtractionException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the ANDROID streaming post-live-DVR URL response", e); + "Could not generate the DASH manifest: error when trying to get the " + + "ANDROID streaming post-live-DVR URL response", e); } } @@ -579,10 +591,12 @@ public final class YoutubeDashManifestCreator { } catch (final IOException | ExtractionException e) { if (isAnAndroidStreamingUrl) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the ANDROID streaming URL response", e); + "Could not generate the DASH manifest: error when trying to get the " + + "ANDROID streaming URL response", e); } else { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the streaming URL response", e); + "Could not generate the DASH manifest: error when trying to get the " + + "streaming URL response", e); } } } @@ -658,16 +672,18 @@ public final class YoutubeDashManifestCreator { if (responseCode != 200) { if (deliveryType == DeliveryType.LIVE) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code " - + responseCode); + "Could not generate the DASH manifest: could not get the " + + "initialization URL of the post-live-DVR stream: " + + "response code " + responseCode); } else if (deliveryType == DeliveryType.OTF) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the initialization URL of the OTF stream: response code " + "Could not generate the DASH manifest: could not get the " + + "initialization URL of the OTF stream: response code " + responseCode); } else { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not fetch the URL of the progressive stream: response code " - + responseCode); + "Could not generate the DASH manifest: could not fetch the URL of " + + "the progressive stream: response code " + responseCode); } } @@ -678,7 +694,8 @@ public final class YoutubeDashManifestCreator { "Content-Type")); } catch (final NullPointerException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the Content-Type header from the streaming URL", e); + "Could not generate the DASH manifest: could not get the Content-Type " + + "header from the streaming URL", e); } // The response body is the redirection URL @@ -692,16 +709,19 @@ public final class YoutubeDashManifestCreator { if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: too many redirects when trying to get the WEB streaming URL response"); + "Could not generate the DASH manifest: too many redirects when trying to " + + "get the WEB streaming URL response"); } // This should never be reached, but is required because we don't want to return null // here throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the WEB streaming URL response"); + "Could not generate the DASH manifest: error when trying to get the WEB " + + "streaming URL response"); } catch (final IOException | ExtractionException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the WEB streaming URL response", e); + "Could not generate the DASH manifest: error when trying to get the WEB " + + "streaming URL response", e); } } @@ -731,7 +751,8 @@ public final class YoutubeDashManifestCreator { } } catch (final NumberFormatException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: unable to get the segments of the stream", e); + "Could not generate the DASH manifest: unable to get the segments of the " + + "stream", e); } } @@ -767,7 +788,8 @@ public final class YoutubeDashManifestCreator { return streamLengthMs; } catch (final NumberFormatException e) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: unable to get the length of the stream", e); + "Could not generate the DASH manifest: unable to get the length of the stream", + e); } } @@ -778,6 +800,7 @@ public final class YoutubeDashManifestCreator { * The generated {@code } element looks like the manifest returned into the player * response of videos with OTF streams: *

+ * *

* {@code + * *

* If the duration is an integer or a double with less than 3 digits after the decimal point, * it will be converted into a double with 3 digits after the decimal point. @@ -859,8 +883,10 @@ public final class YoutubeDashManifestCreator { streamDuration = durationSecondsFallback * 1000; } else { throw new YoutubeDashManifestCreationException( - "Could not generate or append the MPD element of the DASH manifest to the document: " - + "the duration of the stream could not be determined and the durationSecondsFallback is less than or equal to 0"); + "Could not generate or append the MPD element of the DASH " + + "manifest to the document: the duration of the stream " + + "could not be determined and the " + + "durationSecondsFallback is less than or equal to 0"); } } } @@ -870,7 +896,8 @@ public final class YoutubeDashManifestCreator { mpdElement.setAttributeNode(mediaPresentationDurationAttribute); } catch (final Exception e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the MPD element of the DASH manifest to the document", e); + "Could not generate or append the MPD element of the DASH manifest to the " + + "document", e); } return document; @@ -898,7 +925,8 @@ public final class YoutubeDashManifestCreator { mpdElement.appendChild(periodElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the Period element of the DASH manifest to the document", e); + "Could not generate or append the Period element of the DASH manifest to the " + + "document", e); } } @@ -921,7 +949,8 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem) throws YoutubeDashManifestCreationException { try { - final Element periodElement = (Element) document.getElementsByTagName("Period").item(0); + final Element periodElement = (Element) document.getElementsByTagName("Period") + .item(0); final Element adaptationSetElement = document.createElement("AdaptationSet"); final Attr idAttribute = document.createAttribute("id"); @@ -931,21 +960,25 @@ public final class YoutubeDashManifestCreator { final MediaFormat mediaFormat = itagItem.getMediaFormat(); if (mediaFormat == null || isNullOrEmpty(mediaFormat.mimeType)) { throw new YoutubeDashManifestCreationException( - "Could not generate the AdaptationSet element of the DASH manifest to the document: the MediaFormat or the mime type of the MediaFormat of the ItagItem is null or empty"); + "Could not generate the AdaptationSet element of the DASH manifest to the " + + "document: the MediaFormat or the mime type of the MediaFormat " + + "of the ItagItem is null or empty"); } final Attr mimeTypeAttribute = document.createAttribute("mimeType"); mimeTypeAttribute.setValue(mediaFormat.mimeType); adaptationSetElement.setAttributeNode(mimeTypeAttribute); - final Attr subsegmentAlignmentAttribute = document.createAttribute("subsegmentAlignment"); + final Attr subsegmentAlignmentAttribute = document.createAttribute( + "subsegmentAlignment"); subsegmentAlignmentAttribute.setValue("true"); adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute); periodElement.appendChild(adaptationSetElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the AdaptationSet element of the DASH manifest to the document", e); + "Could not generate or append the AdaptationSet element of the DASH manifest " + + "to the document", e); } } @@ -956,9 +989,11 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

* {@code } *

+ * *

* The {@code } element needs to be generated before this element with * {@link #generateAdaptationSetElement(Document, ItagItem)}). @@ -967,7 +1002,8 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the the {@code } element will be * appended * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the document + * appending the {@code } element to the + * document */ private static void generateRoleElement(@Nonnull final Document document) throws YoutubeDashManifestCreationException { @@ -987,7 +1023,8 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(roleElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the Role element of the DASH manifest to the document", e); + "Could not generate or append the Role element of the DASH manifest to the " + + "document", e); } } @@ -1018,7 +1055,9 @@ public final class YoutubeDashManifestCreator { final int id = itagItem.id; if (id <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest to the document: the id of the ItagItem is less than or equal to 0"); + "Could not generate the Representation element of the DASH manifest to " + + "the document: the id of the ItagItem is less than or equal to " + + "0"); } final Attr idAttribute = document.createAttribute("id"); idAttribute.setValue(String.valueOf(id)); @@ -1027,7 +1066,8 @@ public final class YoutubeDashManifestCreator { final String codec = itagItem.getCodec(); if (isNullOrEmpty(codec)) { throw new YoutubeDashManifestCreationException( - "Could not generate the AdaptationSet element of the DASH manifest to the document: the codecs value is null or empty"); + "Could not generate the AdaptationSet element of the DASH manifest to the " + + "document: the codec value is null or empty"); } final Attr codecsAttribute = document.createAttribute("codecs"); codecsAttribute.setValue(codec); @@ -1044,7 +1084,9 @@ public final class YoutubeDashManifestCreator { final int bitrate = itagItem.getBitrate(); if (bitrate <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest to the document: the bitrate of the ItagItem is less than or equal to 0"); + "Could not generate the Representation element of the DASH manifest to " + + "the document: the bitrate of the ItagItem is less than or " + + "equal to 0"); } final Attr bandwidthAttribute = document.createAttribute("bandwidth"); bandwidthAttribute.setValue(String.valueOf(bitrate)); @@ -1057,7 +1099,9 @@ public final class YoutubeDashManifestCreator { final int width = itagItem.getWidth(); if (height <= 0 && width <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest to the document: the width and the height of the ItagItem are less than or equal to 0"); + "Could not generate the Representation element of the DASH manifest " + + "to the document: the width and the height of the ItagItem " + + "are less than or equal to 0"); } if (width > 0) { @@ -1087,7 +1131,8 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(representationElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the Representation element of the DASH manifest to the document", e); + "Could not generate or append the Representation element of the DASH manifest " + + "to the document", e); } } @@ -1098,6 +1143,7 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

* It will produce the following element: *
@@ -1108,6 +1154,7 @@ public final class YoutubeDashManifestCreator { * (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second * parameter of this method) *

+ * *

* The {@code } element needs to be generated before this element with * {@link #generateRepresentationElement(Document, ItagItem)}). @@ -1139,7 +1186,8 @@ public final class YoutubeDashManifestCreator { final int audioChannels = itagItem.getAudioChannels(); if (audioChannels <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the audioChannels value is less than or equal to 0 (" + audioChannels + ")"); + "Could not generate the DASH manifest: the audioChannels value is less " + + "than or equal to 0 (" + audioChannels + ")"); } valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); audioChannelConfigurationElement.setAttributeNode(valueAttribute); @@ -1147,7 +1195,8 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(audioChannelConfigurationElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the AudioChannelConfiguration element of the DASH manifest to the document", e); + "Could not generate or append the AudioChannelConfiguration element of the " + + "DASH manifest to the document", e); } } @@ -1158,6 +1207,7 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

* The {@code } element needs to be generated before this element with * {@link #generateRepresentationElement(Document, ItagItem)}). @@ -1182,7 +1232,8 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(baseURLElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the BaseURL element of the DASH manifest to the document", e); + "Could not generate or append the BaseURL element of the DASH manifest to the " + + "document", e); } } @@ -1193,6 +1244,7 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

* It generates the following element: *
@@ -1201,6 +1253,7 @@ public final class YoutubeDashManifestCreator { * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed * as the second parameter) *

+ * *

* The {@code } element needs to be generated before this element with * {@link #generateRepresentationElement(Document, ItagItem)}). @@ -1227,12 +1280,14 @@ public final class YoutubeDashManifestCreator { final int indexStart = itagItem.getIndexStart(); if (indexStart < 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the indexStart value of the ItagItem is less than to 0 (" + indexStart + ")"); + "Could not generate the DASH manifest: the indexStart value of the " + + "ItagItem is less than to 0 (" + indexStart + ")"); } final int indexEnd = itagItem.getIndexEnd(); if (indexEnd < 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the indexEnd value of the ItagItem is less than to 0 (" + indexStart + ")"); + "Could not generate the DASH manifest: the indexEnd value of the ItagItem " + + "is less than to 0 (" + indexStart + ")"); } indexRangeAttribute.setValue(indexStart + "-" + indexEnd); @@ -1241,7 +1296,8 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(segmentBaseElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the SegmentBase element of the DASH manifest to the document", e); + "Could not generate or append the SegmentBase element of the DASH manifest to " + + "the document", e); } } @@ -1252,6 +1308,7 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

* It generates the following element: *
@@ -1260,6 +1317,7 @@ public final class YoutubeDashManifestCreator { * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed * as the second parameter) *

+ * *

* The {@code } element needs to be generated before this element with * {@link #generateSegmentBaseElement(Document, ItagItem)}). @@ -1286,12 +1344,14 @@ public final class YoutubeDashManifestCreator { final int initStart = itagItem.getInitStart(); if (initStart < 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the initStart value of the ItagItem is less than to 0 (" + initStart + ")"); + "Could not generate the DASH manifest: the initStart value of the " + + "ItagItem is less than to 0 (" + initStart + ")"); } final int initEnd = itagItem.getInitEnd(); if (initEnd < 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the initEnd value of the ItagItem is less than to 0 (" + initEnd + ")"); + "Could not generate the DASH manifest: the initEnd value of the ItagItem " + + "is less than to 0 (" + initEnd + ")"); } rangeAttribute.setValue(initStart + "-" + initEnd); @@ -1300,7 +1360,8 @@ public final class YoutubeDashManifestCreator { segmentBaseElement.appendChild(initializationElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the Initialization element of the DASH manifest to the document", e); + "Could not generate or append the Initialization element of the DASH manifest " + + "to the document", e); } } @@ -1311,6 +1372,7 @@ public final class YoutubeDashManifestCreator { *

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

+ * *

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

    @@ -1372,7 +1434,8 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(segmentTemplateElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the SegmentTemplate element of the DASH manifest to the document", e); + "Could not generate or append the SegmentTemplate element of the DASH " + + "manifest to the document", e); } } @@ -1401,7 +1464,8 @@ public final class YoutubeDashManifestCreator { segmentTemplateElement.appendChild(segmentTimelineElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the SegmentTimeline element of the DASH manifest to the document", e); + "Could not generate or append the SegmentTimeline element of the DASH " + + "manifest to the document", e); } } @@ -1413,16 +1477,20 @@ public final class YoutubeDashManifestCreator { * so we just have to loop into {@link #SEGMENTS_DURATION} and {@link #DURATION_REPETITIONS} * to generate the following element for each duration: *

    + * *

    * {@code } *

    + * *

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

    + * *

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

    + * *

    * The {@code } element needs to be generated before this element with * {@link #generateSegmentTimelineElement(Document)}. @@ -1462,7 +1530,8 @@ public final class YoutubeDashManifestCreator { } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the segment (S) elements of the DASH manifest to the document", e); + "Could not generate or append the segment (S) elements of the DASH manifest " + + "to the document", e); } } @@ -1507,7 +1576,8 @@ public final class YoutubeDashManifestCreator { segmentTimelineElement.appendChild(sElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the segment (S) elements of the DASH manifest to the document", e); + "Could not generate or append the segment (S) elements of the DASH manifest " + + "to the document", e); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 28b802b9d..0c2958675 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -1596,9 +1596,9 @@ public final class YoutubeParsingHelper { } /** - * Check if the streaming URL is a URL from the YouTube {@code WEB} client. + * Check if the streaming URL is from the YouTube {@code WEB} client. * - * @param url the streaming URL on which check if it's a {@code WEB} streaming URL. + * @param url the streaming URL to be checked. * @return true if it's a {@code WEB} streaming URL, false otherwise */ public static boolean isWebStreamingUrl(@Nonnull final String url) { @@ -1620,7 +1620,7 @@ public final class YoutubeParsingHelper { /** * Check if the streaming URL is a URL from the YouTube {@code ANDROID} client. * - * @param url the streaming URL on which check if it's a {@code ANDROID} streaming URL. + * @param url the streaming URL to be checked. * @return true if it's a {@code ANDROID} streaming URL, false otherwise */ public static boolean isAndroidStreamingUrl(@Nonnull final String url) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java new file mode 100644 index 000000000..c1ac4f5f6 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java @@ -0,0 +1,80 @@ +package org.schabi.newpipe.extractor.services.youtube.extractors; + +import org.schabi.newpipe.extractor.services.youtube.ItagItem; + +import javax.annotation.Nonnull; +import java.io.Serializable; + +/** + * Class to build easier {@link org.schabi.newpipe.extractor.stream.Stream}s for + * {@link YoutubeStreamExtractor}. + * + *

    + * It stores, per stream: + *

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

    + */ +final class ItagInfo implements Serializable { + @Nonnull + private final String content; + @Nonnull + private final ItagItem itagItem; + private boolean isUrl; + + /** + * Creates a new {@code ItagInfo} instance. + * + * @param content the content of the stream, which must be not null + * @param itagItem the {@link ItagItem} associated with the stream, which must be not null + */ + ItagInfo(@Nonnull final String content, + @Nonnull final ItagItem itagItem) { + this.content = content; + this.itagItem = itagItem; + } + + /** + * Sets whether the stream is a URL. + * + * @param isUrl whether the content is a URL + */ + void setIsUrl(final boolean isUrl) { + this.isUrl = isUrl; + } + + /** + * Gets the content stored in this {@code ItagInfo} instance, which is either the URL to the + * content itself or the base URL. + * + * @return the content stored in this {@code ItagInfo} instance + */ + @Nonnull + String getContent() { + return content; + } + + /** + * Gets the {@link ItagItem} associated with this {@code ItagInfo} instance. + * + * @return the {@link ItagItem} associated with this {@code ItagInfo} instance, which is not + * null + */ + @Nonnull + ItagItem getItagItem() { + return itagItem; + } + + /** + * Gets whether the content stored is the URL to the content itself or the base URL of it. + * + * @return whether the content stored is the URL to the content itself or the base URL of it + * @see #getContent() for more details + */ + boolean getIsUrl() { + return isUrl; + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java index 5617fd12c..d10fce3bc 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java @@ -25,6 +25,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.VIDEO_ID; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.createDesktopPlayerBody; +import static org.schabi.newpipe.extractor.services.youtube.ItagItem.CONTENT_LENGTH_UNKNOWN; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter; @@ -66,7 +67,6 @@ import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.TimeAgoParser; import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager; -import org.schabi.newpipe.extractor.services.youtube.ItagInfo; import org.schabi.newpipe.extractor.services.youtube.ItagItem; import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor; import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; @@ -666,9 +666,10 @@ public class YoutubeStreamExtractor extends StreamExtractor { } private void setStreamType() { - if (playerResponse.getObject("playabilityStatus").has("liveStreamability") - || playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) { + if (playerResponse.getObject("playabilityStatus").has("liveStreamability")) { streamType = StreamType.LIVE_STREAM; + } else if (playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) { + streamType = StreamType.POST_LIVE_STREAM; } else { streamType = StreamType.VIDEO_STREAM; } @@ -1171,7 +1172,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { for (final Pair pair : streamingDataAndCpnLoopList) { itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey, - itagTypeWanted, streamType, pair.getSecond())); + itagTypeWanted, pair.getSecond())); } final List streamList = new ArrayList<>(); @@ -1189,6 +1190,32 @@ public class YoutubeStreamExtractor extends StreamExtractor { } } + /** + * Get the {@link StreamBuilderHelper} which will be used to build {@link AudioStream}s in + * {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)} + * + *

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

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

    + * + *

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

    + * + * @return a {@link StreamBuilderHelper} to build {@link AudioStream}s + */ @Nonnull private StreamBuilderHelper getAudioStreamBuilderHelper() { return new StreamBuilderHelper() { @@ -1203,9 +1230,11 @@ public class YoutubeStreamExtractor extends StreamExtractor { .setAverageBitrate(itagItem.getAverageBitrate()) .setItagItem(itagItem); - if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { - // YouTube uses the DASH delivery method for videos on OTF streams and - // for all streams of post-live streams and live streams + if (streamType == StreamType.LIVE_STREAM + || streamType == StreamType.POST_LIVE_STREAM + || !itagInfo.getIsUrl()) { + // For YouTube videos on OTF streams and for all streams of post-live streams + // and live streams, only the DASH delivery method can be used. builder.setDeliveryMethod(DeliveryMethod.DASH); } @@ -1214,6 +1243,40 @@ public class YoutubeStreamExtractor extends StreamExtractor { }; } + /** + * Get the {@link StreamBuilderHelper} which will be used to build {@link VideoStream}s in + * {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)} + * + *

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

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

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

    + * + * @param areStreamsVideoOnly whether the {@link StreamBuilderHelper} will set the video + * streams as video-only streams + * @return a {@link StreamBuilderHelper} to build {@link VideoStream}s + */ @Nonnull private StreamBuilderHelper getVideoStreamBuilderHelper( final boolean areStreamsVideoOnly) { @@ -1241,12 +1304,13 @@ public class YoutubeStreamExtractor extends StreamExtractor { builder.setResolution(stringBuilder.toString()); } else { final String resolutionString = itagItem.getResolutionString(); - builder.setResolution(resolutionString != null ? resolutionString : ""); + builder.setResolution(resolutionString != null ? resolutionString + : EMPTY_STRING); } if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { - // YouTube uses the DASH delivery method for videos on OTF streams and - // for all streams of post-live streams and live streams + // For YouTube videos on OTF streams and for all streams of post-live streams + // and live streams, only the DASH delivery method can be used. builder.setDeliveryMethod(DeliveryMethod.DASH); } @@ -1260,15 +1324,15 @@ public class YoutubeStreamExtractor extends StreamExtractor { final JsonObject streamingData, final String streamingDataKey, @Nonnull final ItagItem.ItagType itagTypeWanted, - @Nonnull final StreamType contentStreamType, - @Nonnull final String contentPlaybackNonce) { + @Nonnull final String contentPlaybackNonce) throws ParsingException { if (streamingData == null || !streamingData.has(streamingDataKey)) { return Collections.emptyList(); } + final String videoId = getId(); final List itagInfos = new ArrayList<>(); final JsonArray formats = streamingData.getArray(streamingDataKey); - for (int i = 0; i < formats.size(); i++) { + for (int i = 0; i != formats.size(); ++i) { final JsonObject formatData = formats.getObject(i); final int itag = formatData.getInt("itag"); @@ -1279,79 +1343,10 @@ public class YoutubeStreamExtractor extends StreamExtractor { try { final ItagItem itagItem = ItagItem.getItag(itag); final ItagItem.ItagType itagType = itagItem.itagType; - if (itagItem.itagType != itagTypeWanted) { - continue; + if (itagType == itagTypeWanted) { + buildAndAddItagInfoToList(videoId, itagInfos, formatData, itagItem, + itagType, contentPlaybackNonce); } - String streamUrl; - if (formatData.has("url")) { - streamUrl = formatData.getString("url") + "&cpn=" - + contentPlaybackNonce; - } else { - // This url has an obfuscated signature - final String cipherString = formatData.has(CIPHER) - ? formatData.getString(CIPHER) - : formatData.getString(SIGNATURE_CIPHER); - final Map cipher = Parser.compatParseMap( - cipherString); - streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" - + deobfuscateSignature(cipher.get("s")); - } - - if (isWebStreamingUrl(streamUrl)) { - streamUrl = tryDecryptUrl(streamUrl, getId()) + "&cver=" - + getClientVersion(); - } - - final JsonObject initRange = formatData.getObject("initRange"); - final JsonObject indexRange = formatData.getObject("indexRange"); - final String mimeType = formatData.getString("mimeType", EMPTY_STRING); - final String codec = mimeType.contains("codecs") - ? mimeType.split("\"")[1] : EMPTY_STRING; - - itagItem.setBitrate(formatData.getInt("bitrate")); - itagItem.setWidth(formatData.getInt("width")); - itagItem.setHeight(formatData.getInt("height")); - itagItem.setInitStart(Integer.parseInt(initRange.getString("start", - "-1"))); - itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", - "-1"))); - itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", - "-1"))); - itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", - "-1"))); - itagItem.setQuality(formatData.getString("quality")); - itagItem.setCodec(codec); - if (contentStreamType != StreamType.VIDEO_STREAM) { - itagItem.setTargetDurationSec(formatData.getInt( - "targetDurationSec")); - } - if (itagType == ItagItem.ItagType.VIDEO - || itagType == ItagItem.ItagType.VIDEO_ONLY) { - itagItem.setFps(formatData.getInt("fps")); - } - if (itagType == ItagItem.ItagType.AUDIO) { - itagItem.setSampleRate(Integer.parseInt(formatData.getString( - "audioSampleRate"))); - itagItem.setAudioChannels(formatData.getInt("audioChannels")); - } - itagItem.setContentLength(Long.parseLong(formatData.getString( - "contentLength", "-1"))); - - final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); - - if (contentStreamType == StreamType.VIDEO_STREAM) { - itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING) - .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); - } else { - // We are currently not able to generate DASH manifests for running - // livestreams, so because of the requirements of StreamInfo - // objects, return these streams as DASH URL streams (even if they - // are not playable). - // Ended livestreams are returned as non URL streams - itagInfo.setIsUrl(contentStreamType != StreamType.POST_LIVE_STREAM); - } - - itagInfos.add(itagInfo); } catch (final IOException | ExtractionException ignored) { } } @@ -1359,6 +1354,83 @@ public class YoutubeStreamExtractor extends StreamExtractor { return itagInfos; } + private void buildAndAddItagInfoToList( + @Nonnull final String videoId, + @Nonnull final List itagInfos, + @Nonnull final JsonObject formatData, + @Nonnull final ItagItem itagItem, + @Nonnull final ItagItem.ItagType itagType, + @Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException { + String streamUrl; + if (formatData.has("url")) { + streamUrl = formatData.getString("url"); + } else { + // This url has an obfuscated signature + final String cipherString = formatData.has(CIPHER) + ? formatData.getString(CIPHER) + : formatData.getString(SIGNATURE_CIPHER); + final Map cipher = Parser.compatParseMap( + cipherString); + streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "=" + + deobfuscateSignature(cipher.get("s")); + } + + // Add the content playback nonce to the stream URL + streamUrl += "&" + CPN + "=" + contentPlaybackNonce; + + if (isWebStreamingUrl(streamUrl)) { + streamUrl = tryDecryptUrl(streamUrl, videoId) + "&cver=" + getClientVersion(); + } + + final JsonObject initRange = formatData.getObject("initRange"); + final JsonObject indexRange = formatData.getObject("indexRange"); + final String mimeType = formatData.getString("mimeType", EMPTY_STRING); + final String codec = mimeType.contains("codecs") + ? mimeType.split("\"")[1] : EMPTY_STRING; + + itagItem.setBitrate(formatData.getInt("bitrate")); + itagItem.setWidth(formatData.getInt("width")); + itagItem.setHeight(formatData.getInt("height")); + itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1"))); + itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1"))); + itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1"))); + itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1"))); + itagItem.setQuality(formatData.getString("quality")); + itagItem.setCodec(codec); + + if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) { + itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); + } + + if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { + itagItem.setFps(formatData.getInt("fps")); + } + if (itagType == ItagItem.ItagType.AUDIO) { + // YouTube return the audio sample rate as a string + itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate"))); + itagItem.setAudioChannels(formatData.getInt("audioChannels")); + } + + // YouTube return the content length as a string + itagItem.setContentLength(Long.parseLong(formatData.getString("contentLength", + String.valueOf(CONTENT_LENGTH_UNKNOWN)))); + + final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); + + if (streamType == StreamType.VIDEO_STREAM) { + itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING) + .equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF")); + } else { + // We are currently not able to generate DASH manifests for running + // livestreams, so because of the requirements of StreamInfo + // objects, return these streams as DASH URL streams (even if they + // are not playable). + // Ended livestreams are returned as non URL streams + itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM); + } + + itagInfos.add(itagInfo); + } @Nonnull @Override diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java index b5d673596..65062a649 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java @@ -68,10 +68,10 @@ public final class AudioStream extends Stream { } /** - * Set the identifier of the {@link SubtitlesStream}. + * Set the identifier of the {@link AudioStream}. * *

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

    * *

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

    * - * @param id the identifier of the {@link SubtitlesStream}, which must be not null + * @param id the identifier of the {@link AudioStream}, which must not be null * @return this {@link Builder} instance */ public Builder setId(@Nonnull final String id) { @@ -91,7 +91,7 @@ public final class AudioStream extends Stream { * Set the content of the {@link AudioStream}. * *

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

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

    * It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A}, * {@link MediaFormat#WEBMA WEBMA}, {@link MediaFormat#MP3 MP3}, {@link MediaFormat#OPUS - * OPUS}, {@link MediaFormat#OGG OGG}, {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but can - * be {@code null} if the media format could not be determined. + * OPUS}, {@link MediaFormat#OGG OGG}, or {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but + * can be {@code null} if the media format could not be determined. *

    * *

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

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

    * *

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

    * * @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must - * be not null + * not be null * @return this {@link Builder} instance */ public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { @@ -151,8 +151,8 @@ public final class AudioStream extends Stream { * Set the base URL of the {@link AudioStream}. * *

    - * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which - * they have been parsed. + * For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest + * from which the URLs have been parsed. *

    * *

    @@ -213,7 +213,7 @@ public final class AudioStream extends Stream { * * @return a new {@link AudioStream} using the builder's current values * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or - * {@code deliveryMethod} have been not set or set as {@code null} + * {@code deliveryMethod} have been not set, or have been set as {@code null} */ @Nonnull public AudioStream build() { @@ -244,8 +244,8 @@ public final class AudioStream extends Stream { /** * Create a new audio stream. * - * @param id the ID which uniquely identifies the stream, e.g. for YouTube this - * would be the itag + * @param id the identifier which uniquely identifies the stream, e.g. for YouTube + * this would be the itag * @param content the content or the URL of the stream, depending on whether isUrl is * true * @param isUrl whether content is the URL or the actual content of e.g. a DASH @@ -258,6 +258,7 @@ public final class AudioStream extends Stream { * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more * information) */ + @SuppressWarnings("checkstyle:ParameterNumber") private AudioStream(@Nonnull final String id, @Nonnull final String content, final boolean isUrl, diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java index db74e91ab..444023e58 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java @@ -13,25 +13,43 @@ public enum DeliveryMethod { PROGRESSIVE_HTTP, /** - * Enum constant which represents the use of the DASH adaptive streaming method to fetch a - * {@link Stream stream}. + * Enum constant which represents the use of the DASH (Dynamic Adaptive Streaming over HTTP) + * adaptive streaming method to fetch a {@link Stream stream}. + * + * @see the + * Dynamic Adaptive Streaming over HTTP Wikipedia page and + * DASH Industry Forum's website for more information about the DASH delivery method */ DASH, /** - * Enum constant which represents the use of the HLS adaptive streaming method to fetch a - * {@link Stream stream}. + * Enum constant which represents the use of the HLS (HTTP Live Streaming) adaptive streaming + * method to fetch a {@link Stream stream}. + * + * @see the HTTP Live Streaming + * page and Apple's developers website page + * about HTTP Live Streaming for more information about the HLS delivery method */ HLS, /** * Enum constant which represents the use of the SmoothStreaming adaptive streaming method to * fetch a {@link Stream stream}. + * + * @see Wikipedia's page about adaptive bitrate streaming, + * section Microsoft Smooth Streaming (MSS) for more information about the + * SmoothStreaming delivery method */ SS, /** - * Enum constant which represents the use of a torrent to fetch a {@link Stream stream}. + * Enum constant which represents the use of a torrent file to fetch a {@link Stream stream}. + * + * @see Wikipedia's BitTorrent's page, + * Wikipedia's page about torrent files + * and for more information about the + * BitTorrent protocol */ TORRENT } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java index b76594a6f..df47afdb5 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java @@ -19,11 +19,11 @@ public abstract class Stream implements Serializable { public static final String ID_UNKNOWN = " "; /** - * An integer to represent that the itag id returned is not available (only for YouTube, this + * An integer to represent that the itag ID returned is not available (only for YouTube; this * should never happen) or not applicable (for other services than YouTube). * *

    - * An itag should not have a negative value so {@code -1} is used for this constant. + * An itag should not have a negative value, so {@code -1} is used for this constant. *

    */ public static final int ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE = -1; @@ -38,8 +38,8 @@ public abstract class Stream implements Serializable { /** * Instantiates a new {@code Stream} object. * - * @param id the ID which uniquely identifies the file, e.g. for YouTube this would - * be the itag + * @param id the identifier which uniquely identifies the file, e.g. for YouTube + * this would be the itag * @param content the content or URL, depending on whether isUrl is true * @param isUrl whether content is the URL or the actual content of e.g. a DASH * manifest @@ -63,10 +63,10 @@ public abstract class Stream implements Serializable { } /** - * Checks if the list already contains one stream with equals stats. + * Checks if the list already contains a stream with the same statistics. * - * @param stream the stream which will be compared to the streams in the stream list - * @param streamList the list of {@link Stream Streams} which will be compared + * @param stream the stream to be compared against the streams in the stream list + * @param streamList the list of {@link Stream}s which will be compared * @return whether the list already contains one stream with equals stats */ public static boolean containSimilarStream(final Stream stream, @@ -83,16 +83,16 @@ public abstract class Stream implements Serializable { } /** - * Reveals whether two streams have the same stats ({@link MediaFormat media format} and + * Reveals whether two streams have the same statistics ({@link MediaFormat media format} and * {@link DeliveryMethod delivery method}). * *

    * If the {@link MediaFormat media format} of the stream is unknown, the streams are compared - * by only using the {@link DeliveryMethod delivery method} and their id. + * by using only the {@link DeliveryMethod delivery method} and their ID. *

    * *

    - * Note: This method always returns always false if the stream passed is null. + * Note: This method always returns false if the stream passed is null. *

    * * @param cmp the stream object to be compared to this stream object @@ -118,9 +118,12 @@ public abstract class Stream implements Serializable { /** * Reveals whether two streams are equal. * - * @param cmp the stream object to be compared to this stream object - * @return whether streams are equal + * @param cmp a {@link Stream} object to be compared to this {@link Stream} instance. + * @return whether the compared streams are equal + * @deprecated Use {@link #equalStats(Stream)} to compare statistics of two streams and + * {@link #equals(Object)} to compare the equality of two streams instead. */ + @Deprecated public boolean equals(final Stream cmp) { return equalStats(cmp) && content.equals(cmp.content); } @@ -129,19 +132,19 @@ public abstract class Stream implements Serializable { * Gets the identifier of this stream, e.g. the itag for YouTube. * *

    - * It should be normally unique but {@link #ID_UNKNOWN} may be returned as the identifier if - * one used by the stream extractor cannot be extracted, if the extractor uses a value from a - * streaming service. + * It should normally be unique, but {@link #ID_UNKNOWN} may be returned as the identifier if + * the one used by the stream extractor cannot be extracted, which could happen if the + * extractor uses a value from a streaming service. *

    * - * @return the id (which may be {@link #ID_UNKNOWN}) + * @return the identifier (which may be {@link #ID_UNKNOWN}) */ public String getId() { return id; } /** - * Gets the URL of this stream if the content is a URL, or {@code null} if that's the not case. + * Gets the URL of this stream if the content is a URL, or {@code null} otherwise. * * @return the URL if the content is a URL, {@code null} otherwise * @deprecated Use {@link #getContent()} instead. @@ -162,10 +165,10 @@ public abstract class Stream implements Serializable { } /** - * Returns if the content is a URL or not. + * Returns whether the content is a URL or not. * - * @return {@code true} if the content of this stream content is a URL, {@code false} - * if it is the actual content + * @return {@code true} if the content of this stream is a URL, {@code false} if it's the + * actual content */ public boolean isUrl() { return isUrl; @@ -182,9 +185,9 @@ public abstract class Stream implements Serializable { } /** - * Gets the format id, which can be unknown. + * Gets the format ID, which can be unknown. * - * @return the format id or {@link #FORMAT_ID_UNKNOWN} + * @return the format ID or {@link #FORMAT_ID_UNKNOWN} */ public int getFormatId() { if (mediaFormat != null) { @@ -208,7 +211,7 @@ public abstract class Stream implements Serializable { * *

    * If the stream is not a DASH stream or an HLS stream, this value will always be null. - * It may be also null for these streams too. + * It may also be null for these streams too. *

    * * @return the base URL of the stream or {@code null} @@ -222,7 +225,7 @@ public abstract class Stream implements Serializable { * Gets the {@link ItagItem} of a stream. * *

    - * If the stream is not a YouTube stream, this value will always be null. + * If the stream is not from YouTube, this value will always be null. *

    * * @return the {@link ItagItem} of the stream or {@code null} @@ -242,11 +245,14 @@ public abstract class Stream implements Serializable { final Stream stream = (Stream) obj; return id.equals(stream.id) && mediaFormat == stream.mediaFormat - && deliveryMethod == stream.deliveryMethod; + && deliveryMethod == stream.deliveryMethod + && content.equals(stream.content) + && isUrl == stream.isUrl + && Objects.equals(baseUrl, stream.baseUrl); } @Override public int hashCode() { - return Objects.hash(id, mediaFormat, deliveryMethod); + return Objects.hash(id, mediaFormat, deliveryMethod, content, isUrl, baseUrl); } -} \ No newline at end of file +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java index 082c7a228..5e6f438ea 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java @@ -17,10 +17,10 @@ public enum StreamType { NONE, /** - * Enum constant to indicate that the stream type of stream content is a video. + * Enum constant to indicate that the stream type of stream content is a live video. * *

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

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

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

    */ LIVE_STREAM, /** - * Enum constant to indicate that the stream type of stream content is a live audio content. + * Enum constant to indicate that the stream type of stream content is a live audio. * *

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

    * *

    - * So, in order to prevent unexpected behaviors, stream extractors which are returning this - * stream type for a content should ensure that no live video stream is returned for this - * content. + * To prevent unexpected behavior, stream extractors which are returning this stream type for a + * content should ensure that no live video stream is returned along with it. *

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

    - * Note that most of ended live video (or audio) contents may be extracted as - * {@link #VIDEO_STREAM regular video contents} (or - * {@link #AUDIO_STREAM regular audio contents}) later, because the service may encode them - * again later as normal video/audio streams. That's the case for example on YouTube. + * Note that most of the content of an ended live video (or audio) may be extracted as {@link + * #VIDEO_STREAM regular video contents} (or {@link #AUDIO_STREAM regular audio contents}) + * later, because the service may encode them again later as normal video/audio streams. That's + * the case on YouTube, for example. *

    * *

    diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java index 732d822d7..ddd372343 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/SubtitlesStream.java @@ -35,7 +35,7 @@ public final class SubtitlesStream extends Stream { private Boolean autoGenerated; /** - * Create a new {@link Builder} instance with its default values. + * Create a new {@link Builder} instance with default values. */ public Builder() { } @@ -43,7 +43,7 @@ public final class SubtitlesStream extends Stream { /** * Set the identifier of the {@link SubtitlesStream}. * - * @param id the identifier of the {@link SubtitlesStream}, which should be not null + * @param id the identifier of the {@link SubtitlesStream}, which should not be null * (otherwise the fallback to create the identifier will be used when building * the builder) * @return this {@link Builder} instance @@ -57,10 +57,10 @@ public final class SubtitlesStream extends Stream { * Set the content of the {@link SubtitlesStream}. * *

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

    * - * @param content the content of the {@link SubtitlesStream} + * @param content the content of the {@link SubtitlesStream}, which must not be null * @param isUrl whether the content is a URL * @return this {@link Builder} instance */ @@ -78,7 +78,7 @@ public final class SubtitlesStream extends Stream { * It should be one of the subtitles {@link MediaFormat}s ({@link MediaFormat#SRT SRT}, * {@link MediaFormat#TRANSCRIPT1 TRANSCRIPT1}, {@link MediaFormat#TRANSCRIPT2 * TRANSCRIPT2}, {@link MediaFormat#TRANSCRIPT3 TRANSCRIPT3}, {@link MediaFormat#TTML - * TTML}, {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could + * TTML}, or {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could * not be determined. *

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

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

    * *

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

    * * @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which - * must be not null + * must not be null * @return this {@link Builder} instance */ public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { @@ -119,8 +119,8 @@ public final class SubtitlesStream extends Stream { * Set the base URL of the {@link SubtitlesStream}. * *

    - * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which - * they have been parsed. + * For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest + * from which the URLs have been parsed. *

    * *

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

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

    * * @param languageCode the language code of the {@link SubtitlesStream} @@ -151,10 +151,10 @@ public final class SubtitlesStream extends Stream { } /** - * Set whether the subtitles have been generated by the streaming service. + * Set whether the subtitles have been auto-generated by the streaming service. * * @param autoGenerated whether the subtitles have been generated by the streaming - * service + * service * @return this {@link Builder} instance */ public Builder setAutoGenerated(final boolean autoGenerated) { @@ -172,13 +172,13 @@ public final class SubtitlesStream extends Stream { * *

    * If no identifier has been set, an identifier will be generated using the language code - * and the media format suffix if the media format is known + * and the media format suffix, if the media format is known. *

    * * @return a new {@link SubtitlesStream} using the builder's current values * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}), * {@code deliveryMethod}, {@code languageCode} or the {@code isAutogenerated} have been - * not set or set as {@code null} + * not set, or have been set as {@code null} */ @Nonnull public SubtitlesStream build() { @@ -219,28 +219,29 @@ public final class SubtitlesStream extends Stream { /** * Create a new subtitles stream. * - * @param id the ID which uniquely identifies the stream, e.g. for YouTube this - * would be the itag + * @param id the identifier which uniquely identifies the stream, e.g. for YouTube + * this would be the itag * @param content the content or the URL of the stream, depending on whether isUrl is * true * @param isUrl whether content is the URL or the actual content of e.g. a DASH * manifest - * @param format the {@link MediaFormat} used by the stream + * @param mediaFormat the {@link MediaFormat} used by the stream * @param deliveryMethod the {@link DeliveryMethod} of the stream * @param languageCode the language code of the stream * @param autoGenerated whether the subtitles are auto-generated by the streaming service * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more * information) */ + @SuppressWarnings("checkstyle:ParameterNumber") private SubtitlesStream(@Nonnull final String id, @Nonnull final String content, final boolean isUrl, - @Nullable final MediaFormat format, + @Nullable final MediaFormat mediaFormat, @Nonnull final DeliveryMethod deliveryMethod, @Nonnull final String languageCode, final boolean autoGenerated, @Nullable final String baseUrl) { - super(id, content, isUrl, format, deliveryMethod, baseUrl); + super(id, content, isUrl, mediaFormat, deliveryMethod, baseUrl); /* * Locale.forLanguageTag only for Android API >= 21 @@ -262,7 +263,7 @@ public final class SubtitlesStream extends Stream { } this.code = languageCode; - this.format = format; + this.format = mediaFormat; this.autoGenerated = autoGenerated; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java index c8ab5cfc9..e8b2595b9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/VideoStream.java @@ -81,7 +81,7 @@ public final class VideoStream extends Stream { * Set the identifier of the {@link VideoStream}. * *

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

    * *

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

    * - * @param id the identifier of the {@link VideoStream}, which must be not null + * @param id the identifier of the {@link VideoStream}, which must not be null * @return this {@link Builder} instance */ public Builder setId(@Nonnull final String id) { @@ -101,7 +101,7 @@ public final class VideoStream extends Stream { * Set the content of the {@link VideoStream}. * *

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

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

    * It should be one of the video {@link MediaFormat}s ({@link MediaFormat#MPEG_4 MPEG_4}, - * {@link MediaFormat#v3GPP v3GPP}, {@link MediaFormat#WEBM WEBM}) but can be {@code null} - * if the media format could not be determined. + * {@link MediaFormat#v3GPP v3GPP}, or {@link MediaFormat#WEBM WEBM}) but can be {@code + * null} if the media format could not be determined. *

    * *

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

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

    * *

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

    * * @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must - * be not null + * not be null * @return this {@link Builder} instance */ public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) { @@ -160,8 +160,8 @@ public final class VideoStream extends Stream { * Set the base URL of the {@link VideoStream}. * *

    - * Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which - * they have been parsed. + * For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest + * from which the URLs have been parsed. *

    * *

    @@ -245,8 +245,8 @@ public final class VideoStream extends Stream { * * @return a new {@link VideoStream} using the builder's current values * @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}), - * {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set or - * set as {@code null} + * {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set, or + * have been set as {@code null} */ @Nonnull public VideoStream build() { @@ -289,8 +289,8 @@ public final class VideoStream extends Stream { /** * Create a new video stream. * - * @param id the ID which uniquely identifies the stream, e.g. for YouTube this - * would be the itag + * @param id the identifier which uniquely identifies the stream, e.g. for YouTube + * this would be the itag * @param content the content or the URL of the stream, depending on whether isUrl is * true * @param isUrl whether content is the URL or the actual content of e.g. a DASH @@ -303,6 +303,7 @@ public final class VideoStream extends Stream { * @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more * information) */ + @SuppressWarnings("checkstyle:ParameterNumber") private VideoStream(@Nonnull final String id, @Nonnull final String content, final boolean isUrl, diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java index a644272d1..0cd23fcaf 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java @@ -53,7 +53,7 @@ class YoutubeDashManifestCreatorTest { // Setting a higher number may let Google video servers return a lot of 403s private static final int MAXIMUM_NUMBER_OF_STREAMS_TO_TEST = 3; - public static class testGenerationOfOtfAndProgressiveManifests { + public static class TestGenerationOfOtfAndProgressiveManifests { private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; private static YoutubeStreamExtractor extractor; 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: *

      *
    • request the first sequence of the stream (the base URL on which the first - * sequence parameters are appended (see {@link #RN_0} and {@link #SQ_0})) with a POST - * or GET request (depending of the client on which the streaming URL comes from); + * sequence parameter is appended (see {@link #SQ_0})) with a POST or GET request + * (depending of the client on which the streaming URL comes from); *
    • *
    • follow its redirection(s), if any;
    • - *
    • save the last URL, remove the first sequence parameters;
    • + *
    • save the last URL, remove the first sequence parameter;
    • *
    • use the information provided in the {@link ItagItem} to generate all * elements of the DASH manifest.
    • *
    @@ -331,8 +343,8 @@ public final class YoutubeDashManifestCreator { *

    In order to generate the DASH manifest, this method will: *

      *
    • request the first sequence of the stream (the base URL on which the first - * sequence parameters are appended (see {@link #RN_0} and {@link #SQ_0})) with a POST - * or GET request (depending of the client on which the streaming URL comes from); + * sequence parameter is appended (see {@link #SQ_0})) with a POST or GET request + * (depending of the client on which the streaming URL comes from); *
    • *
    • follow its redirection(s), if any;
    • *
    • save the last URL, remove the first sequence parameters;
    • @@ -491,7 +503,7 @@ public final class YoutubeDashManifestCreator { */ @Nonnull public static String createDashManifestFromProgressiveStreamingUrl( - @Nonnull String progressiveStreamingBaseUrl, + @Nonnull final String progressiveStreamingBaseUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) { @@ -526,12 +538,11 @@ public final class YoutubeDashManifestCreator { * Get the "initialization" {@link Response response} of a stream. * *

      - * This method fetches: + * This method fetches, for OTF streams and for post-live-DVR streams: *

        - *
      • for progressive streams, the base URL of the stream with a HEAD request;
      • - *
      • for OTF streams and for post-live-DVR streams, the base URL of the stream, to which - * are appended {@link #SQ_0} and {@link #RN_0} params, with a GET request for streaming - * URLs from the WEB client and a POST request for the ones from the Android client;
      • + *
      • the base URL of the stream, to which are appended {@link #SQ_0} and {@link #RN_0} + * parameters, with a GET request for streaming URLs from the WEB client and a POST request + * for the ones from the Android client;
      • *
      • for streaming URLs from the WEB client, the {@link #ALR_YES} param is also added. *
      • *
      @@ -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 { *

      * It generates the following element: *
      - * {@code } + * {@code } *
      * (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed * as the second parameter) @@ -1380,9 +1394,9 @@ public final class YoutubeDashManifestCreator { * {@code 1} for OTF streams; *

    • {@code timescale}, which is always {@code 1000};
    • *
    • {@code media}, which is the base URL of the stream on which is appended - * {@code &sq=$Number$&rn=$Number$};
    • + * {@code &sq=$Number$}; *
    • {@code initialization} (only for OTF streams), which is the base URL of the stream - * on which is appended {@link #SQ_0} and {@link #RN_0}.
    • + * on which is appended {@link #SQ_0}. *
    *

    * @@ -1423,12 +1437,12 @@ public final class YoutubeDashManifestCreator { // Post-live-DVR/ended livestreams streams don't require an initialization sequence if (!isDeliveryTypeLive) { final Attr initializationAttribute = document.createAttribute("initialization"); - initializationAttribute.setValue(baseUrl + SQ_0 + RN_0); + initializationAttribute.setValue(baseUrl + SQ_0); segmentTemplateElement.setAttributeNode(initializationAttribute); } final Attr mediaAttribute = document.createAttribute("media"); - mediaAttribute.setValue(baseUrl + "&sq=$Number$&rn=$Number$"); + mediaAttribute.setValue(baseUrl + "&sq=$Number$"); segmentTemplateElement.setAttributeNode(mediaAttribute); representationElement.appendChild(segmentTemplateElement); 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 d10fce3bc..14b9a9db1 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 @@ -1292,21 +1292,9 @@ public class YoutubeStreamExtractor extends StreamExtractor { .setIsVideoOnly(areStreamsVideoOnly) .setItagItem(itagItem); - final int height = itagItem.getHeight(); - if (height > 0) { - final StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append(height); - stringBuilder.append("p"); - final int fps = itagItem.getFps(); - if (fps > 30) { - stringBuilder.append(fps); - } - builder.setResolution(stringBuilder.toString()); - } else { - final String resolutionString = itagItem.getResolutionString(); - builder.setResolution(resolutionString != null ? resolutionString - : EMPTY_STRING); - } + final String resolutionString = itagItem.getResolutionString(); + builder.setResolution(resolutionString != null ? resolutionString + : EMPTY_STRING); if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { // For YouTube videos on OTF streams and for all streams of post-live streams 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 444023e58..5d8437b9e 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 @@ -48,8 +48,8 @@ public enum DeliveryMethod { * * @see Wikipedia's BitTorrent's page, * Wikipedia's page about torrent files - * and for more information about the - * BitTorrent protocol + * and Bitorrent's website for more information + * about the BitTorrent protocol */ TORRENT } 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 0cd23fcaf..fcfffda4e 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 @@ -21,7 +21,12 @@ import java.net.URL; import java.util.List; import java.util.Random; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +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 static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; @@ -428,8 +433,8 @@ class YoutubeDashManifestCreatorTest { 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"); + assertTrue(initializationValue.endsWith("&sq=0"), + "The value of the initialization attribute doesn't end with &sq=0"); final String mediaValue = segmentTemplateElement.getAttribute("media"); assertFalse(isBlank(mediaValue), @@ -440,8 +445,8 @@ class YoutubeDashManifestCreatorTest { 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$"); + assertTrue(mediaValue.endsWith("&sq=$Number$"), + "The value of the media attribute doesn't end with &sq=$Number$"); final String startNumberValue = segmentTemplateElement.getAttribute("startNumber"); assertFalse(isBlank(startNumberValue), From d64d7bbd015d0e27d8e136cd7fd90f94c7ea19b0 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 3 Apr 2022 20:54:35 +0200 Subject: [PATCH 14/38] Move ManifestCreatorCache tests to a separate class and remove override of equals and hashCode methods in ManifestCreatorCache These methods don't need to be overriden, as they are not excepted to be used in collections. Also improve the toString method of this class, which contains also now clearFactor and maximumSize attributes and for each operations. --- .../extractor/utils/ManifestCreatorCache.java | 56 +++------------ .../utils/ManifestCreatorCacheTest.java | 69 +++++++++++++++++++ .../newpipe/extractor/utils/UtilsTest.java | 48 ------------- 3 files changed, 77 insertions(+), 96 deletions(-) create mode 100644 extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java 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 8e885f7cf..e2084f1d4 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 @@ -1,10 +1,10 @@ package org.schabi.newpipe.extractor.utils; +import javax.annotation.Nonnull; 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; /** @@ -225,50 +225,11 @@ public final class 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 - */ + @Nonnull @Override public String toString() { - return concurrentHashMap.toString(); + return "ManifestCreatorCache[clearFactor=" + clearFactor + ", maximumSize=" + maximumSize + + ", concurrentHashMap=" + concurrentHashMap + "]"; } /** @@ -285,17 +246,16 @@ public final class ManifestCreatorCache>> entriesToRemove = new ArrayList<>(); - for (final Map.Entry> entry : concurrentHashMap.entrySet()) { + concurrentHashMap.entrySet().forEach(entry -> { final Pair value = entry.getValue(); if (value.getFirst() < difference) { entriesToRemove.add(entry); } else { value.setFirst(value.getFirst() - difference); } - } + }); - for (final Map.Entry> entry : entriesToRemove) { - concurrentHashMap.remove(entry.getKey(), entry.getValue()); - } + entriesToRemove.forEach(entry -> concurrentHashMap.remove(entry.getKey(), + entry.getValue())); } } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java new file mode 100644 index 000000000..7d3fa5b65 --- /dev/null +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java @@ -0,0 +1,69 @@ +package org.schabi.newpipe.extractor.utils; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ManifestCreatorCacheTest { + @Test + void basicMaximumSizeAndResetTest() { + final ManifestCreatorCache cache = new ManifestCreatorCache<>(); + + // 30 elements set -> cache resized to 23 -> 5 new elements set to the cache -> 28 + cache.setMaximumSize(30); + setCacheContent(cache); + assertEquals(28, cache.size(), + "Wrong cache size with default clear factor and 30 as the maximum size"); + cache.reset(); + + assertEquals(0, cache.size(), + "The cache has been not cleared after a reset call (wrong cache size)"); + assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), + "Wrong maximum size after cache reset"); + assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), + "Wrong clear factor after cache reset"); + } + + @Test + void maximumSizeAndClearFactorSettersAndResettersTest() { + final ManifestCreatorCache cache = new ManifestCreatorCache<>(); + cache.setMaximumSize(20); + cache.setClearFactor(0.5); + + setCacheContent(cache); + // 30 elements set -> cache resized to 10 -> 5 new elements set to the cache -> 15 + assertEquals(15, cache.size(), + "Wrong cache size with 0.5 as the clear factor and 20 as the maximum size"); + + // Clear factor and maximum size getters tests + assertEquals(0.5, cache.getClearFactor(), + "Wrong clear factor gotten from clear factor getter"); + assertEquals(20, cache.getMaximumSize(), + "Wrong maximum cache size gotten from maximum size getter"); + + // Resetters tests + cache.resetMaximumSize(); + assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), + "Wrong maximum cache size gotten from maximum size getter after maximum size " + + "resetter call"); + + cache.resetClearFactor(); + assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), + "Wrong clear factor gotten from clear factor getter after clear factor resetter " + + "call"); + } + + private static void setCacheContent(final ManifestCreatorCache cache) { + int i = 0; + while (i < 26) { + cache.put(String.valueOf((char) ('a' + i)), "V"); + ++i; + } + + i = 0; + while (i < 9) { + cache.put("a" + (char) ('a' + i), "V"); + ++i; + } + } +} 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 3d835dce2..2dd787b88 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 @@ -47,52 +47,4 @@ 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 cache = new ManifestCreatorCache<>(); - cache.setMaximumSize(30); - setCacheContent(cache); - // 30 elements set -> cache resized to 23 -> 5 new elements set to the cache -> 28 - assertEquals(28, cache.size(), - "Wrong cache size with default clear factor and 30 as the maximum size"); - - cache.reset(); - cache.setMaximumSize(20); - cache.setClearFactor(0.5); - - setCacheContent(cache); - // 30 elements set -> cache resized to 10 -> 5 new elements set to the cache -> 15 - assertEquals(15, cache.size(), - "Wrong cache size with 0.5 as the clear factor and 20 as the maximum size"); - - // Clear factor and maximum size getters tests - assertEquals(0.5, cache.getClearFactor(), - "Wrong clear factor gotten from clear factor getter"); - assertEquals(20, cache.getMaximumSize(), - "Wrong maximum cache size gotten from maximum size getter"); - - // Resetters tests - cache.resetMaximumSize(); - assertEquals(ManifestCreatorCache.DEFAULT_MAXIMUM_SIZE, cache.getMaximumSize(), - "Wrong maximum cache size gotten from maximum size getter after maximum size reset"); - - cache.resetClearFactor(); - assertEquals(ManifestCreatorCache.DEFAULT_CLEAR_FACTOR, cache.getClearFactor(), - "Wrong clear factor gotten from clear factor getter after clear factor reset"); - } - - private void setCacheContent(@Nonnull final ManifestCreatorCache cache) { - int i = 0; - while (i < 26) { - cache.put(Character.toString((char) (97 + i)), "V"); - ++i; - } - - i = 0; - while (i < 9) { - cache.put("a" + (char) (97 + i), "V"); - ++i; - } - } } From 436ddde29f31a7fc1a0789d94b42268e779995cb Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 3 Apr 2022 20:55:54 +0200 Subject: [PATCH 15/38] Use assertThrows in YoutubeDashManifestCreatorTest --- .../YoutubeDashManifestCreatorTest.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 fcfffda4e..db06307d1 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 @@ -7,6 +7,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.VideoStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -26,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; @@ -96,14 +98,14 @@ class YoutubeDashManifestCreatorTest { 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"); + // This exception should be always thrown, as we are not able to generate DASH + // manifests of video formats with audio + final List videoStreams = extractor.getVideoStreams(); + if (!videoStreams.isEmpty()) { + assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + () -> testStreams(DeliveryMethod.PROGRESSIVE_HTTP, videoStreams), + "The exception thrown for the generation of DASH manifests for YouTube " + + "progressive video streams with audio was not the one excepted"); } } @@ -145,7 +147,7 @@ class YoutubeDashManifestCreatorTest { assertFalse(isBlank(dashManifest), "The DASH manifest is null or empty: " + dashManifest); } - i++; + ++i; } } From 07b045f20d06683b19ba8c8249340b4610c99a9b Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Mon, 11 Apr 2022 19:35:57 +0200 Subject: [PATCH 16/38] [YouTube] Support the iOS client in YoutubeDashManifestCreator and decrypt again the n parameter, if present, for all clients This commits reverts a new behavior introduced in this branch, which only applied the decryption if needed on streams from the WEB client. Also fix rebase issues and documentations style in YoutubeDashManifestCreator. --- .../youtube/YoutubeDashManifestCreator.java | 51 +++++++++---------- .../extractors/YoutubeStreamExtractor.java | 7 +-- 2 files changed, 25 insertions(+), 33 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 a61b86951..12818d085 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 @@ -33,8 +33,11 @@ import java.util.Map; import java.util.Objects; 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.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; @@ -562,54 +565,44 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem, final DeliveryType deliveryType) throws YoutubeDashManifestCreationException { - final boolean isAWebStreamingUrl = isWebStreamingUrl(baseStreamingUrl); + final boolean isAHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) + || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); - final boolean isAnAndroidStreamingUrlAndAPostLiveDvrStream = isAnAndroidStreamingUrl - && deliveryType == DeliveryType.LIVE; - if (isAWebStreamingUrl) { + final boolean isAnIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); + if (isAHtml5StreamingUrl) { baseStreamingUrl += ALR_YES; } baseStreamingUrl = appendRnParamAndSqParamIfNeeded(baseStreamingUrl, deliveryType); final Downloader downloader = NewPipe.getDownloader(); - if (isAWebStreamingUrl) { + if (isAHtml5StreamingUrl) { final String mimeTypeExpected = itagItem.getMediaFormat().getMimeType(); if (!isNullOrEmpty(mimeTypeExpected)) { return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl, mimeTypeExpected, deliveryType); } - } else if (isAnAndroidStreamingUrlAndAPostLiveDvrStream) { + } else if (isAnAndroidStreamingUrl || isAnIosStreamingUrl) { try { final Map> headers = new HashMap<>(); headers.put("User-Agent", Collections.singletonList( - getYoutubeAndroidAppUserAgent(null))); + isAnAndroidStreamingUrl ? getAndroidUserAgent(null) + : getIosUserAgent(null))); final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); 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); + + (isAnIosStreamingUrl ? "ANDROID" : "IOS") + + " streaming URL response", e); } } try { - final Map> headers = new HashMap<>(); - if (isAnAndroidStreamingUrl) { - headers.put("User-Agent", Collections.singletonList( - getYoutubeAndroidAppUserAgent(null))); - } - - return downloader.get(baseStreamingUrl, headers); + return downloader.get(baseStreamingUrl); } 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); - } 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); } } @@ -834,8 +827,10 @@ public final class YoutubeDashManifestCreator { * 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 + * as a fallback if the duration could not be determined * @return a {@link Document} object which contains a {@code } element * @throws YoutubeDashManifestCreationException if something goes wrong when generating/ * appending the {@link Document object} or the @@ -1698,7 +1693,7 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached OTF streams + * Set the clear factor of cached OTF streams. * * @param otfStreamsClearFactor the clear factor of OTF streams manifests cache. */ @@ -1707,7 +1702,7 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached post-live-DVR streams + * Set the clear factor of cached post-live-DVR streams. * * @param postLiveDvrStreamsClearFactor the clear factor of post-live-DVR streams manifests * cache. @@ -1718,7 +1713,7 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached progressive streams + * Set the clear factor of cached progressive streams. * * @param progressiveStreamsClearFactor the clear factor of progressive streams manifests * cache. 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 14b9a9db1..507c21f28 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 @@ -29,12 +29,10 @@ import static org.schabi.newpipe.extractor.services.youtube.ItagItem.CONTENT_LEN import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getClientVersion; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonAndroidPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonIosPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getJsonPostResponse; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareAndroidMobileJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareIosMobileJsonBuilder; @@ -1366,9 +1364,8 @@ public class YoutubeStreamExtractor extends StreamExtractor { // Add the content playback nonce to the stream URL streamUrl += "&" + CPN + "=" + contentPlaybackNonce; - if (isWebStreamingUrl(streamUrl)) { - streamUrl = tryDecryptUrl(streamUrl, videoId) + "&cver=" + getClientVersion(); - } + // Decrypt the n parameter if it is present + streamUrl = tryDecryptUrl(streamUrl, videoId); final JsonObject initRange = formatData.getObject("initRange"); final JsonObject indexRange = formatData.getObject("indexRange"); From 50272db9464f742e937a222db1c69e07adacdd9d Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 30 Apr 2022 19:45:37 +0200 Subject: [PATCH 17/38] Apply reviews: improve comments, remove FILE, remove Stream#equals(Stream) --- .../MediaCCCLiveStreamExtractor.java | 2 +- .../extractors/MediaCCCStreamExtractor.java | 4 +- .../youtube/YoutubeParsingHelper.java | 4 +- .../extractor/stream/DeliveryMethod.java | 16 ++-- .../newpipe/extractor/stream/Stream.java | 13 --- .../newpipe/extractor/stream/StreamType.java | 96 ++++++------------- .../extractor/utils/ManifestCreatorCache.java | 10 +- 7 files changed, 42 insertions(+), 103 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index 09e01ce36..da03e20e6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -210,7 +210,7 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { // Ensure that we use only process JsonObjects .filter(JsonObject.class::isInstance) .map(JsonObject.class::cast) - // Only process audio streams + // Only process streams of requested type .filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type"))) // Flatmap Urls and ensure that we use only process JsonObjects .flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream() diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 83dcc381e..7aa7569f8 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -147,8 +147,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor { mediaFormat = null; } - // Don't use the containsSimilarStream method because it will remove the - // extraction of some video versions (mostly languages). So if there are multiple + // Don't use the containsSimilarStream method because it will prevent the + // extraction of some video variations (mostly languages). So if there are multiple // video streams available, only the first one will be extracted in this case. videoStreams.add(new VideoStream.Builder() .setId(recording.getString("filename", ID_UNKNOWN)) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 0c2958675..0b94d3d71 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -1264,8 +1264,8 @@ public final class YoutubeParsingHelper { // Spoofing an Android 12 device with the hardcoded version of the Android app return "com.google.android.youtube/" + MOBILE_YOUTUBE_CLIENT_VERSION + " (Linux; U; Android 12; " - + (localization != null ? localization.getCountryCode() - : Localization.DEFAULT.getCountryCode()) + + (localization == null ? Localization.DEFAULT.getCountryCode() + : localization.getCountryCode()) + ") gzip"; } 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 5d8437b9e..ed9893572 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 @@ -7,14 +7,13 @@ package org.schabi.newpipe.extractor.stream; public enum DeliveryMethod { /** - * Enum constant which represents the use of the progressive HTTP streaming method to fetch a - * {@link Stream stream}. + * Used for {@link Stream}s served using the progressive HTTP streaming method. */ PROGRESSIVE_HTTP, /** - * Enum constant which represents the use of the DASH (Dynamic Adaptive Streaming over HTTP) - * adaptive streaming method to fetch a {@link Stream stream}. + * Used for {@link Stream}s served using the DASH (Dynamic Adaptive Streaming over HTTP) + * adaptive streaming method. * * @see the * Dynamic Adaptive Streaming over HTTP Wikipedia page and @@ -23,8 +22,8 @@ public enum DeliveryMethod { DASH, /** - * Enum constant which represents the use of the HLS (HTTP Live Streaming) adaptive streaming - * method to fetch a {@link Stream stream}. + * Used for {@link Stream}s served using the HLS (HTTP Live Streaming) adaptive streaming + * method. * * @see the HTTP Live Streaming * page and Apple's developers website page @@ -33,8 +32,7 @@ public enum DeliveryMethod { HLS, /** - * Enum constant which represents the use of the SmoothStreaming adaptive streaming method to - * fetch a {@link Stream stream}. + * Used for {@link Stream}s served using the SmoothStreaming adaptive streaming method. * * @see Wikipedia's page about adaptive bitrate streaming, @@ -44,7 +42,7 @@ public enum DeliveryMethod { SS, /** - * Enum constant which represents the use of a torrent file to fetch a {@link Stream stream}. + * Used for {@link Stream}s served via a torrent file. * * @see Wikipedia's BitTorrent's page, * Wikipedia's page about torrent files 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 df47afdb5..02ca3cf16 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 @@ -115,19 +115,6 @@ public abstract class Stream implements Serializable { : areUsingSameDeliveryMethodAndAreUrlStreams; } - /** - * Reveals whether two streams are equal. - * - * @param cmp a {@link Stream} object to be compared to this {@link Stream} instance. - * @return whether the compared streams are equal - * @deprecated Use {@link #equalStats(Stream)} to compare statistics of two streams and - * {@link #equals(Object)} to compare the equality of two streams instead. - */ - @Deprecated - public boolean equals(final Stream cmp) { - return equalStats(cmp) && content.equals(cmp.content); - } - /** * Gets the identifier of this stream, e.g. the itag for YouTube. * diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java index 5e6f438ea..7e668cbd4 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamType.java @@ -1,74 +1,52 @@ package org.schabi.newpipe.extractor.stream; /** - * An enum representing the stream types of stream contents returned by the extractor. + * An enum representing the stream type of a {@link StreamInfo} extracted by a {@link + * StreamExtractor}. */ public enum StreamType { /** - * Placeholder to check if the stream type of stream content 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 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 Date: Sat, 30 Apr 2022 19:46:23 +0200 Subject: [PATCH 18/38] Remove unused DashMpdParser (but kept in git history) --- .../services/youtube/DashMpdParser.java | 244 ------------------ 1 file changed, 244 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java 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 deleted file mode 100644 index 2b5774049..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DashMpdParser.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Created by Christian Schabesberger on 02.02.16. - * - * Copyright (C) Christian Schabesberger 2016 - * DashMpdParser.java is part of NewPipe Extractor. - * - * NewPipe Extractor is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * NewPipe Extractor is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with NewPipe Extractor. If not, see . - */ - -package org.schabi.newpipe.extractor.services.youtube; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.downloader.Downloader; -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.VideoStream; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import javax.annotation.Nonnull; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -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 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) { - 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 videoStreams; - private final List videoOnlyStreams; - private final List audioStreams; - - Result(final List videoStreams, - final List videoOnlyStreams, - final List audioStreams) { - this.videoStreams = videoStreams; - this.videoOnlyStreams = videoOnlyStreams; - this.audioStreams = audioStreams; - } - - public List getVideoStreams() { - return videoStreams; - } - - public List getVideoOnlyStreams() { - return videoOnlyStreams; - } - - public List getAudioStreams() { - return audioStreams; - } - } - - /** - * 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. - * - *

    - * 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 videoStreams = new ArrayList<>(); - final List audioStreams = new ArrayList<>(); - final List videoOnlyStreams = new ArrayList<>(); - - for (int i = 0; i < representationList.getLength(); i++) { - final Element representation = (Element) representationList.item(i); - try { - final String mimeType = ((Element) representation.getParentNode()) - .getAttribute("mimeType"); - final String id = representation.getAttribute("id"); - final ItagItem itag = ItagItem.getItag(Integer.parseInt(id)); - final Element segmentationList = (Element) representation - .getElementsByTagName("SegmentList").item(0); - - if (segmentationList == null) { - continue; - } - - final MediaFormat mediaFormat = MediaFormat.getFromMimeType(mimeType); - - if (itag.itagType.equals(ItagItem.ItagType.AUDIO)) { - audioStreams.add(new AudioStream.Builder() - .setId(String.valueOf(itag.id)) - .setContent(manualDashFromRepresentation(doc, representation), - false) - .setMediaFormat(mediaFormat) - .setDeliveryMethod(DeliveryMethod.DASH) - .setAverageBitrate(itag.getAverageBitrate()) - .setBaseUrl(dashMpdUrl) - .setItagItem(itag) - .build()); - } else { - final boolean isVideoOnly = itag.itagType == ItagItem.ItagType.VIDEO_ONLY; - final VideoStream videoStream = new VideoStream.Builder() - .setId(String.valueOf(itag.id)) - .setContent(manualDashFromRepresentation(doc, representation), - false) - .setMediaFormat(mediaFormat) - .setDeliveryMethod(DeliveryMethod.DASH) - .setResolution(Objects.requireNonNull(itag.getResolutionString())) - .setIsVideoOnly(isVideoOnly) - .setBaseUrl(dashMpdUrl) - .setItagItem(itag) - .build(); - if (isVideoOnly) { - videoOnlyStreams.add(videoStream); - } else { - videoStreams.add(videoStream); - } - } - } catch (final Exception ignored) { - } - } - return new Result(videoStreams, videoOnlyStreams, audioStreams); - } catch (final Exception e) { - throw new DashMpdParsingException("Could not parse DASH MPD", e); - } - } - - @Nonnull - private static String manualDashFromRepresentation(@Nonnull final Document document, - @Nonnull final Element representation) - throws TransformerException { - final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); - - // Clone the element so we can freely modify it - final Element adaptationSet = (Element) representation.getParentNode(); - final Element adaptationSetClone = (Element) adaptationSet.cloneNode(true); - - // Remove other representations from the adaptation set - final NodeList representations = adaptationSetClone.getElementsByTagName("Representation"); - for (int i = representations.getLength() - 1; i >= 0; i--) { - final Node item = representations.item(i); - if (!item.isEqualNode(representation)) { - adaptationSetClone.removeChild(item); - } - } - - final Element newMpdRootElement = (Element) mpdElement.cloneNode(false); - final Element periodElement = newMpdRootElement.getOwnerDocument().createElement("Period"); - periodElement.appendChild(adaptationSetClone); - newMpdRootElement.appendChild(periodElement); - - return nodeToString(newMpdRootElement); - } - - private static String nodeToString(final Node node) throws TransformerException { - final StringWriter result = new StringWriter(); - final Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.transform(new DOMSource(node), new StreamResult(result)); - return result.toString(); - } -} From ba68b8c014377b8a0b0558b584052281eba81822 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 30 Apr 2022 21:24:58 +0200 Subject: [PATCH 19/38] [YouTube] Secure DashManifestCreator against XEE attack Also remove duplicate lines from javadoc comments, otherwise checkstyle complaints. XEE prevention is inspired from https://github.com/ChuckerTeam/chucker/pull/201/files For an explanation of XEE see https://rules.sonarsource.com/java/RSPEC-2755, though the solution reported there causes crashes on Android --- .../youtube/YoutubeDashManifestCreator.java | 122 ++++++++++-------- 1 file changed, 68 insertions(+), 54 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 12818d085..09e5bb439 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 @@ -13,10 +13,13 @@ import org.w3c.dom.Document; import org.w3c.dom.Element; 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; @@ -45,10 +48,7 @@ import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; /** * 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 { @@ -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 manifestCreatorCache) throws YoutubeDashManifestCreationException { - try { - final StringWriter result = new StringWriter(); - final TransformerFactory transformerFactory = TransformerFactory.newInstance(); - final Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); - transformer.transform(new DOMSource(document), new StreamResult(result)); - final String stringResult = result.toString(); - manifestCreatorCache.put(originalBaseStreamingUrl, stringResult); - return stringResult; + try { + final String documentXml = documentToXml(document); + manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); + return documentXml; } catch (final Exception e) { throw new YoutubeDashManifestCreationException( "Could not convert the DASH manifest generated to a string", e); @@ -1628,8 +1616,54 @@ public final class YoutubeDashManifestCreator { } /** - * Get the number of cached OTF streams manifests. + * Securing against XEE is done by passing {@code false} to {@link + * DocumentBuilderFactory#setExpandEntityReferences(boolean)}, also see + * ChuckerTeam/chucker#201. * + * @return an instance of document secured against XEE attacks, that should then be convertible + * to an XML string without security problems + * @see #documentToXml(Document) Use documentToXml to convert the created document to XML, which + * is also secured against XEE! + */ + private static Document newDocument() throws ParserConfigurationException { + final DocumentBuilderFactory documentBuilderFactory + = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setExpandEntityReferences(false); + + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + return documentBuilder.newDocument(); + } + + /** + * Securing against XEE is done by setting {@link XMLConstants#FEATURE_SECURE_PROCESSING} to + * {@code true} in the {@link TransformerFactory}, also see + * ChuckerTeam/chucker#201. + * The best way to do this would be setting the attributes {@link + * XMLConstants#ACCESS_EXTERNAL_DTD} and {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET}, but + * unfortunately the engine on Android does not support them. + * + * @param document the document to convert; must have been created using {@link #newDocument()} + * to properly prevent XEE attacks! + * @return the document converted to an XML string, making sure there can't be XEE attacks + */ + private static String documentToXml(@Nonnull final Document document) + throws TransformerException { + + final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + + final Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + + final StringWriter result = new StringWriter(); + transformer.transform(new DOMSource(document), new StreamResult(result)); + + return result.toString(); + } + + /** * @return the number of cached OTF streams manifests */ public static int getOtfCachedManifestsSize() { @@ -1637,8 +1671,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the number of cached post-live-DVR streams manifests. - * * @return the number of cached post-live-DVR streams manifests */ public static int getPostLiveDvrStreamsCachedManifestsSize() { @@ -1646,8 +1678,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the number of cached progressive manifests. - * * @return the number of cached progressive manifests */ public static int getProgressiveStreamsCachedManifestsSize() { @@ -1655,8 +1685,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the number of cached OTF, post-live-DVR streams and progressive manifests. - * * @return the number of cached OTF, post-live-DVR streams and progressive manifests. */ public static int getSizeOfManifestsCaches() { @@ -1666,8 +1694,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the clear factor of OTF streams manifests cache. - * * @return the clear factor of OTF streams manifests cache. */ public static double getOtfStreamsClearFactor() { @@ -1675,8 +1701,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the clear factor of post-live-DVR streams manifests cache. - * * @return the clear factor of post-live-DVR streams manifests cache. */ public static double getPostLiveDvrStreamsClearFactor() { @@ -1684,8 +1708,6 @@ public final class YoutubeDashManifestCreator { } /** - * Get the clear factor of progressive streams manifests cache. - * * @return the clear factor of progressive streams manifests cache. */ public static double getProgressiveStreamsClearFactor() { @@ -1693,8 +1715,6 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached OTF streams. - * * @param otfStreamsClearFactor the clear factor of OTF streams manifests cache. */ public static void setOtfStreamsClearFactor(final double otfStreamsClearFactor) { @@ -1702,10 +1722,8 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached post-live-DVR streams. - * - * @param postLiveDvrStreamsClearFactor the clear factor of post-live-DVR streams manifests - * cache. + * @param postLiveDvrStreamsClearFactor the clear factor to set for post-live-DVR streams + * manifests cache */ public static void setPostLiveDvrStreamsClearFactor( final double postLiveDvrStreamsClearFactor) { @@ -1713,10 +1731,8 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached progressive streams. - * - * @param progressiveStreamsClearFactor the clear factor of progressive streams manifests - * cache. + * @param progressiveStreamsClearFactor the clear factor to set for progressive streams + * manifests cache */ public static void setProgressiveStreamsClearFactor( final double progressiveStreamsClearFactor) { @@ -1724,10 +1740,8 @@ public final class YoutubeDashManifestCreator { } /** - * Set the clear factor of cached OTF, post-live-DVR and progressive streams. - * - * @param cachesClearFactor the clear factor of OTF, post-live-DVR and progressive streams - * manifests caches. + * @param cachesClearFactor the clear factor to set for OTF, post-live-DVR and progressive + * streams manifests caches */ public static void setCachesClearFactor(final double cachesClearFactor) { GENERATED_OTF_MANIFESTS.setClearFactor(cachesClearFactor); From 8226fd044f5403ecf544ae9b81172628b4869cf5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 30 Apr 2022 21:56:23 +0200 Subject: [PATCH 20/38] [YouTube] Suppress Sonar security warning for XEE --- .../extractor/services/youtube/YoutubeDashManifestCreator.java | 1 + 1 file changed, 1 insertion(+) 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 09e5bb439..8383d0e2b 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 @@ -1649,6 +1649,7 @@ public final class YoutubeDashManifestCreator { private static String documentToXml(@Nonnull final Document document) throws TransformerException { + @SuppressWarnings("java:S2755") // see javadoc: this is actually taken care of final TransformerFactory transformerFactory = TransformerFactory.newInstance(); transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); From 5c8340903954cbfed8b23078c80ac5bcb97d59de Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 14:44:30 +0200 Subject: [PATCH 21/38] [YouTube] Rewrite manifest test and rename long methods --- .../youtube/YoutubeDashManifestCreator.java | 10 +- .../newpipe/extractor/ExtractorAsserts.java | 10 + .../YoutubeDashManifestCreatorTest.java | 877 ++++++------------ 3 files changed, 311 insertions(+), 586 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 8383d0e2b..8929185c1 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 @@ -79,7 +79,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. *

    */ @@ -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 errors) { if (!errors.isEmpty()) { @@ -64,6 +66,14 @@ public class ExtractorAsserts { } } + public static void assertNotBlank(String stringToCheck) { + assertNotBlank(stringToCheck, null); + } + + public static void assertNotBlank(String stringToCheck, @Nullable String message) { + assertFalse(Utils.isBlank(stringToCheck), message); + } + public static void assertGreater(final long expected, final long actual) { assertGreater(expected, actual, actual + " is not > " + expected); } 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 db06307d1..c5fdf71af 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 @@ -1,5 +1,18 @@ package org.schabi.newpipe.extractor.services.youtube; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreater; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.schabi.newpipe.downloader.DownloaderTestImpl; @@ -7,33 +20,25 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Stream; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; +import java.io.StringReader; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import javax.annotation.Nonnull; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import java.io.StringReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Random; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; /** - * Test for {@link YoutubeDashManifestCreator}. + * Test for {@link YoutubeDashManifestCreator}. Tests the generation of OTF and Progressive + * manifests. * *

    * 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 videoStreams = extractor.getVideoStreams(); - if (!videoStreams.isEmpty()) { - assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, - () -> testStreams(DeliveryMethod.PROGRESSIVE_HTTP, videoStreams), - "The exception thrown for the generation of DASH manifests for YouTube " - + "progressive video streams with audio was not the one excepted"); - } - } - - private void testStreams(@Nonnull final DeliveryMethod deliveryMethodToTest, - @Nonnull final List 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"), - "The value of the initialization attribute doesn't end with &sq=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$"), - "The value of the media attribute doesn't end with &sq=$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 + ")"); + 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; + private static long videoLength; + + @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(); + videoLength = extractor.getLength(); + } + + @Test + void testOtfStreams() throws Exception { + assertDashStreams(extractor.getVideoOnlyStreams()); + assertDashStreams(extractor.getAudioStreams()); + + // no video stream with audio uses the DASH delivery method (YouTube OTF stream type) + assertEquals(0, assertFilterStreams(extractor.getVideoStreams(), + DeliveryMethod.DASH).size()); + } + + @Test + void testProgressiveStreams() throws Exception { + assertProgressiveStreams(extractor.getVideoOnlyStreams()); + assertProgressiveStreams(extractor.getAudioStreams()); + + // we are not able to generate DASH manifests of video formats with audio + assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + () -> assertProgressiveStreams(extractor.getVideoStreams())); + } + + private void assertDashStreams(final List streams) throws Exception { + + for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.DASH)) { + //noinspection ConstantConditions + final String manifest = YoutubeDashManifestCreator.fromOtfStreamingUrl( + stream.getContent(), stream.getItagItem(), videoLength); + assertNotBlank(manifest); + + assertManifestGenerated( + manifest, + stream.getItagItem(), + document -> assertAll( + () -> assertSegmentTemplateElement(document), + () -> assertSegmentTimelineAndSElements(document) + ) + ); } } + + private void assertProgressiveStreams(final List streams) throws Exception { + + for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.PROGRESSIVE_HTTP)) { + //noinspection ConstantConditions + final String manifest = YoutubeDashManifestCreator.fromProgressiveStreamingUrl( + stream.getContent(), stream.getItagItem(), videoLength); + assertNotBlank(manifest); + + assertManifestGenerated( + manifest, + stream.getItagItem(), + document -> assertAll( + () -> assertBaseUrlElement(document), + () -> assertSegmentBaseElement(document, stream.getItagItem()), + () -> assertInitializationElement(document, stream.getItagItem()) + ) + ); + } + } + + private List assertFilterStreams(final List streams, + final DeliveryMethod deliveryMethod) { + + final List filteredStreams = streams.stream() + .filter(stream -> stream.getDeliveryMethod() == deliveryMethod) + .limit(MAX_STREAMS_TO_TEST_PER_METHOD) + .collect(Collectors.toList()); + + assertAll(filteredStreams.stream() + .flatMap(stream -> java.util.stream.Stream.of( + () -> assertNotBlank(stream.getContent()), + () -> assertNotNull(stream.getItagItem()) + )) + ); + + return filteredStreams; + } + + private void assertManifestGenerated(final String dashManifest, + final ItagItem itagItem, + final Consumer additionalAsserts) + throws Exception { + + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + final Document document = documentBuilder.parse(new InputSource( + new StringReader(dashManifest))); + + assertAll( + () -> assertMpdElement(document), + () -> assertPeriodElement(document), + () -> assertAdaptationSetElement(document, itagItem), + () -> assertRoleElement(document), + () -> assertRepresentationElement(document, itagItem), + () -> { + if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { + assertAudioChannelConfigurationElement(document, itagItem); + } + }, + () -> additionalAsserts.accept(document) + ); + } + + private void assertMpdElement(@Nonnull final Document document) { + final Element element = (Element) document.getElementsByTagName("MPD").item(0); + assertNotNull(element); + assertNull(element.getParentNode().getNodeValue()); + + final String mediaPresentationDuration = element.getAttribute("mediaPresentationDuration"); + assertNotNull(mediaPresentationDuration); + assertTrue(mediaPresentationDuration.startsWith("PT")); + } + + private void assertPeriodElement(@Nonnull final Document document) { + assertGetElement(document, "Period", "MPD"); + } + + private void assertAdaptationSetElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "AdaptationSet", "Period"); + assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType"); + } + + private void assertRoleElement(@Nonnull final Document document) { + assertGetElement(document, "Role", "AdaptationSet"); + } + + private void assertRepresentationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "Representation", "AdaptationSet"); + + assertAttrEquals(itagItem.getBitrate(), element, "bandwidth"); + assertAttrEquals(itagItem.getCodec(), element, "codecs"); + + if (itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY + || itagItem.itagType == ItagItem.ItagType.VIDEO) { + assertAttrEquals(itagItem.getFps(), element, "frameRate"); + assertAttrEquals(itagItem.getHeight(), element, "height"); + assertAttrEquals(itagItem.getWidth(), element, "width"); + } + + assertAttrEquals(itagItem.id, element, "id"); + } + + private void assertAudioChannelConfigurationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, + "AudioChannelConfiguration", "Representation"); + assertAttrEquals(itagItem.getAudioChannels(), element, "value"); + } + + private void assertSegmentTemplateElement(@Nonnull final Document document) { + final Element element = assertGetElement(document, "SegmentTemplate", "Representation"); + + final String initializationValue = element.getAttribute("initialization"); + assertIsValidUrl(initializationValue); + assertTrue(initializationValue.endsWith("&sq=0")); + + final String mediaValue = element.getAttribute("media"); + assertIsValidUrl(mediaValue); + assertTrue(mediaValue.endsWith("&sq=$Number$")); + + assertEquals("1", element.getAttribute("startNumber")); + } + + private void assertSegmentTimelineAndSElements(@Nonnull final Document document) { + final Element element = assertGetElement(document, "SegmentTimeline", "SegmentTemplate"); + final NodeList childNodes = element.getChildNodes(); + assertGreater(0, childNodes.getLength()); + + assertAll(IntStream.range(0, childNodes.getLength()) + .mapToObj(childNodes::item) + .map(Element.class::cast) + .flatMap(sElement -> java.util.stream.Stream.of( + () -> assertEquals("S", sElement.getTagName()), + () -> assertGreater(0, Integer.parseInt(sElement.getAttribute("d"))), + () -> { + 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)) { + assertGreater(0, Integer.parseInt(rValue)); + } + } + ) + ) + ); + } + + private void assertBaseUrlElement(@Nonnull final Document document) { + final Element element = assertGetElement(document, "BaseURL", "Representation"); + assertIsValidUrl(element.getTextContent()); + } + + private void assertSegmentBaseElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "SegmentBase", "Representation"); + assertRangeEquals(itagItem.getIndexStart(), itagItem.getIndexEnd(), element, "indexRange"); + } + + private void assertInitializationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "Initialization", "SegmentBase"); + assertRangeEquals(itagItem.getInitStart(), itagItem.getInitEnd(), element, "range"); + } + + + private void assertAttrEquals(final int expected, + final Element element, + final String attribute) { + + final int actual = Integer.parseInt(element.getAttribute(attribute)); + assertAll( + () -> assertGreater(0, actual), + () -> assertEquals(expected, actual) + ); + } + + private void assertAttrEquals(final String expected, + final Element element, + final String attribute) { + final String actual = element.getAttribute(attribute); + assertAll( + () -> assertNotBlank(actual), + () -> assertEquals(expected, actual) + ); + } + + private void assertRangeEquals(final int expectedStart, + final int expectedEnd, + final Element element, + final String attribute) { + final String range = element.getAttribute(attribute); + assertNotBlank(range); + final String[] rangeParts = range.split("-"); + assertEquals(2, rangeParts.length); + + final int actualStart = Integer.parseInt(rangeParts[0]); + final int actualEnd = Integer.parseInt(rangeParts[1]); + + assertAll( + () -> assertGreaterOrEqual(0, actualStart), + () -> assertEquals(expectedStart, actualStart), + () -> assertGreater(0, actualEnd), + () -> assertEquals(expectedEnd, actualEnd) + ); + } + + private Element assertGetElement(final Document document, + final String tagName, + final String expectedParentTagName) { + + final Element element = (Element) document.getElementsByTagName(tagName).item(0); + assertNotNull(element); + assertTrue(element.getParentNode().isEqualNode( + document.getElementsByTagName(expectedParentTagName).item(0)), + "Element with tag name \"" + tagName + "\" does not have a parent node" + + " with tag name \"" + expectedParentTagName + "\""); + return element; + } } From 3708ab9ed5f6f1be13fc8081723990201c79eac5 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 18:00:02 +0200 Subject: [PATCH 22/38] [YouTube] Refactor YoutubeDashManifestCreator - Remove all of the methods used to access caches and replace them with three caches getters - Rename caches to shorter and more meaningful names - Remove redundant @throws tags that just say "if this method fails to do what it should do", which is obvious --- .../youtube/YoutubeDashManifestCreator.java | 411 ++---------------- 1 file changed, 29 insertions(+), 382 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 8929185c1..22e8f57ba 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 @@ -99,20 +99,20 @@ public final class YoutubeDashManifestCreator { /** * Cache of DASH manifests generated for OTF streams. */ - private static final ManifestCreatorCache GENERATED_OTF_MANIFESTS = - new ManifestCreatorCache<>(); + private static final ManifestCreatorCache OTF_CACHE + = new ManifestCreatorCache<>(); /** * Cache of DASH manifests generated for post-live-DVR streams. */ - private static final ManifestCreatorCache - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS = new ManifestCreatorCache<>(); + private static final ManifestCreatorCache POST_LIVE_DVR_CACHE + = new ManifestCreatorCache<>(); /** * Cache of DASH manifests generated for progressive streams. */ - private static final ManifestCreatorCache - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS = new ManifestCreatorCache<>(); + private static final ManifestCreatorCache PROGRESSIVE_CACHE + = new ManifestCreatorCache<>(); /** * Enum of streaming format types used by YouTube in their streams. @@ -238,17 +238,14 @@ public final class YoutubeDashManifestCreator { * @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 - * @throws YoutubeDashManifestCreationException if something goes wrong when trying to generate - * the DASH manifest */ @Nonnull public static String fromOtfStreamingUrl( @Nonnull final String otfBaseStreamingUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { - if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) { - return Objects.requireNonNull(GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl)) - .getSecond(); + if (OTF_CACHE.containsKey(otfBaseStreamingUrl)) { + return Objects.requireNonNull(OTF_CACHE.get(otfBaseStreamingUrl)).getSecond(); } String realOtfBaseStreamingUrl = otfBaseStreamingUrl; @@ -305,7 +302,7 @@ public final class YoutubeDashManifestCreator { SEGMENTS_DURATION.clear(); DURATION_REPETITIONS.clear(); - return buildAndCacheResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS); + return buildAndCacheResult(otfBaseStreamingUrl, document, OTF_CACHE); } /** @@ -372,8 +369,6 @@ public final class YoutubeDashManifestCreator { * 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 fromPostLiveStreamDvrStreamingUrl( @@ -381,9 +376,9 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem, final int targetDurationSec, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { - if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) { - return Objects.requireNonNull(GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get( - postLiveStreamDvrStreamingUrl)).getSecond(); + if (POST_LIVE_DVR_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) { + return Objects.requireNonNull(POST_LIVE_DVR_CACHE.get(postLiveStreamDvrStreamingUrl)) + .getSecond(); } String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl; final String streamDuration; @@ -442,7 +437,7 @@ public final class YoutubeDashManifestCreator { generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); return buildAndCacheResult(postLiveStreamDvrStreamingUrl, document, - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS); + POST_LIVE_DVR_CACHE); } /** @@ -501,17 +496,15 @@ public final class YoutubeDashManifestCreator { * 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 fromProgressiveStreamingUrl( @Nonnull final String progressiveStreamingBaseUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { - if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) { - return Objects.requireNonNull(GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get( - progressiveStreamingBaseUrl)).getSecond(); + if (PROGRESSIVE_CACHE.containsKey(progressiveStreamingBaseUrl)) { + return Objects.requireNonNull(PROGRESSIVE_CACHE.get(progressiveStreamingBaseUrl)) + .getSecond(); } if (durationSecondsFallback <= 0) { @@ -534,7 +527,7 @@ public final class YoutubeDashManifestCreator { generateInitializationElement(document, itagItem); return buildAndCacheResult(progressiveStreamingBaseUrl, document, - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS); + PROGRESSIVE_CACHE); } /** @@ -555,9 +548,7 @@ public final class YoutubeDashManifestCreator { * @param itagItem the {@link ItagItem} of stream, which cannot be null * @param deliveryType the {@link DeliveryType} of the stream * @return the "initialization" response, without redirections on the network on which the - * request(s) is/are made - * @throws YoutubeDashManifestCreationException if something goes wrong when fetching the - * "initialization" response and/or its redirects + * request(s) is/are made */ @SuppressWarnings("checkstyle:FinalParameters") @Nonnull @@ -647,8 +638,6 @@ public final class YoutubeDashManifestCreator { * @param responseMimeTypeExpected the response mime type expected from Google video servers * @param deliveryType the {@link DeliveryType} of the stream * @return the response of the stream which should have no redirections - * @throws YoutubeDashManifestCreationException if something goes wrong when trying to get the - * response without any redirection */ @SuppressWarnings("checkstyle:FinalParameters") @Nonnull @@ -738,8 +727,6 @@ public final class YoutubeDashManifestCreator { * * @param segmentDuration the string array which contains all the sequences extracted with the * regular expression - * @throws YoutubeDashManifestCreationException if something goes wrong when trying to collect - * the segments of the OTF stream */ private static void collectSegmentsData(@Nonnull final String[] segmentDuration) throws YoutubeDashManifestCreationException { @@ -774,8 +761,6 @@ public final class YoutubeDashManifestCreator { * @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 { @@ -832,9 +817,6 @@ public final class YoutubeDashManifestCreator { * @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 } element - * @throws YoutubeDashManifestCreationException if something goes wrong when generating/ - * appending the {@link Document object} or the - * {@code } element */ private static Document generateDocumentAndMpdElement(@Nonnull final String[] segmentDuration, final DeliveryType deliveryType, @@ -917,9 +899,6 @@ public final class YoutubeDashManifestCreator { * * @param document the {@link Document} on which the the {@code } element will be * appended - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the - * document */ private static void generatePeriodElement(@Nonnull final Document document) throws YoutubeDashManifestCreationException { @@ -945,9 +924,6 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the the {@code } element will be * appended * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the - * document */ private static void generateAdaptationSetElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) @@ -1005,9 +981,6 @@ public final class YoutubeDashManifestCreator { * * @param document the {@link Document} on which the the {@code } element will be * appended - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the - * document */ private static void generateRoleElement(@Nonnull final Document document) throws YoutubeDashManifestCreationException { @@ -1044,9 +1017,6 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the the {@code } element will * be appended * @param itagItem the {@link ItagItem} to use, which cannot be null - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateRepresentationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) @@ -1167,10 +1137,6 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the {@code } * element will be appended * @param itagItem the {@link ItagItem} to use, which cannot be null - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the - * {@code } element - * to the document */ private static void generateAudioChannelConfigurationElement( @Nonnull final Document document, @@ -1221,9 +1187,6 @@ public final class YoutubeDashManifestCreator { * be appended * @param baseUrl the base URL of the stream, which cannot be null and will be set as the * content of the {@code } element - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateBaseUrlElement(@Nonnull final Document document, @Nonnull final String baseUrl) @@ -1266,9 +1229,6 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the {@code } element will * be appended * @param itagItem the {@link ItagItem} to use, which cannot be null - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateSegmentBaseElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) @@ -1330,9 +1290,6 @@ public final class YoutubeDashManifestCreator { * @param document the {@link Document} on which the {@code } element will * be appended * @param itagItem the {@link ItagItem} to use, which cannot be null - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateInitializationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) @@ -1399,9 +1356,6 @@ public final class YoutubeDashManifestCreator { * be appended * @param baseUrl the base URL of the OTF/post-live-DVR stream * @param deliveryType the stream {@link DeliveryType delivery type} - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateSegmentTemplateElement(@Nonnull final Document document, @Nonnull final String baseUrl, @@ -1454,9 +1408,6 @@ public final class YoutubeDashManifestCreator { * * @param document the {@link Document} on which the the {@code } element will * be appended - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element - * to the document */ private static void generateSegmentTimelineElement(@Nonnull final Document document) throws YoutubeDashManifestCreationException { @@ -1501,9 +1452,6 @@ public final class YoutubeDashManifestCreator { *

    * * @param document the {@link Document} on which the the {@code } elements will be appended - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } elements to the - * document */ private static void generateSegmentElementsForOtfStreams(@Nonnull final Document document) throws YoutubeDashManifestCreationException { @@ -1556,9 +1504,6 @@ public final class YoutubeDashManifestCreator { * stream * @param segmentCount the number of segments, extracted by the main method which * generates manifests of post DVR livestreams - * @throws YoutubeDashManifestCreationException if something goes wrong when generating or - * appending the {@code } element to the - * document */ private static void generateSegmentElementForPostLiveDvrStreams( @Nonnull final Document document, @@ -1591,13 +1536,9 @@ public final class YoutubeDashManifestCreator { * @param originalBaseStreamingUrl the original base URL of the stream * @param document the document to be converted * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the - * string generated (which is either - * {@link #GENERATED_OTF_MANIFESTS}, - * {@link #GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS} or - * {@link #GENERATED_PROGRESSIVE_STREAMS_MANIFESTS}) + * string generated (use either {@link #OTF_CACHE}, + * {@link #POST_LIVE_DVR_CACHE} or {@link #PROGRESSIVE_CACHE}) * @return the DASH manifest {@link Document document} converted to a string - * @throws YoutubeDashManifestCreationException if something goes wrong when converting the - * {@link Document document} */ private static String buildAndCacheResult( @Nonnull final String originalBaseStreamingUrl, @@ -1665,317 +1606,23 @@ public final class YoutubeDashManifestCreator { } /** - * @return the number of cached OTF streams manifests + * @return the cache of DASH manifests generated for OTF streams */ - public static int getOtfCachedManifestsSize() { - return GENERATED_OTF_MANIFESTS.size(); + public static ManifestCreatorCache getOtfManifestsCache() { + return OTF_CACHE; } /** - * @return the number of cached post-live-DVR streams manifests + * @return the cache of DASH manifests generated for post-live-DVR streams */ - public static int getPostLiveDvrStreamsCachedManifestsSize() { - return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.size(); + public static ManifestCreatorCache getPostLiveDvrManifestsCache() { + return POST_LIVE_DVR_CACHE; } /** - * @return the number of cached progressive manifests + * @return the cache of DASH manifests generated for progressive streams */ - public static int getProgressiveStreamsCachedManifestsSize() { - return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.size(); - } - - /** - * @return the number of cached OTF, post-live-DVR streams and progressive manifests. - */ - public static int getSizeOfManifestsCaches() { - return GENERATED_OTF_MANIFESTS.size() - + GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.size() - + GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.size(); - } - - /** - * @return the clear factor of OTF streams manifests cache. - */ - public static double getOtfStreamsClearFactor() { - return GENERATED_OTF_MANIFESTS.getClearFactor(); - } - - /** - * @return the clear factor of post-live-DVR streams manifests cache. - */ - public static double getPostLiveDvrStreamsClearFactor() { - return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.getClearFactor(); - } - - /** - * @return the clear factor of progressive streams manifests cache. - */ - public static double getProgressiveStreamsClearFactor() { - return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.getClearFactor(); - } - - /** - * @param otfStreamsClearFactor the clear factor of OTF streams manifests cache. - */ - public static void setOtfStreamsClearFactor(final double otfStreamsClearFactor) { - GENERATED_OTF_MANIFESTS.setClearFactor(otfStreamsClearFactor); - } - - /** - * @param postLiveDvrStreamsClearFactor the clear factor to set for post-live-DVR streams - * manifests cache - */ - public static void setPostLiveDvrStreamsClearFactor( - final double postLiveDvrStreamsClearFactor) { - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setClearFactor(postLiveDvrStreamsClearFactor); - } - - /** - * @param progressiveStreamsClearFactor the clear factor to set for progressive streams - * manifests cache - */ - public static void setProgressiveStreamsClearFactor( - final double progressiveStreamsClearFactor) { - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setClearFactor(progressiveStreamsClearFactor); - } - - /** - * @param cachesClearFactor the clear factor to set for OTF, post-live-DVR and progressive - * streams manifests caches - */ - public static void setCachesClearFactor(final double cachesClearFactor) { - GENERATED_OTF_MANIFESTS.setClearFactor(cachesClearFactor); - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.setClearFactor(cachesClearFactor); - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.setClearFactor(cachesClearFactor); - } - - /** - * Reset the clear factor of OTF streams cache to its - * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. - */ - public static void resetOtfStreamsClearFactor() { - GENERATED_OTF_MANIFESTS.resetClearFactor(); - } - - /** - * Reset the clear factor of post-live-DVR streams cache to its - * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. - */ - public static void resetPostLiveDvrStreamsClearFactor() { - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.resetClearFactor(); - } - - /** - * Reset the clear factor of progressive streams cache to its - * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. - */ - public static void resetProgressiveStreamsClearFactor() { - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.resetClearFactor(); - } - - /** - * Reset the clear factor of OTF, post-live-DVR and progressive streams caches to their - * {@link ManifestCreatorCache#DEFAULT_CLEAR_FACTOR default value}. - */ - public static void resetCachesClearFactor() { - GENERATED_OTF_MANIFESTS.resetClearFactor(); - GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.resetClearFactor(); - GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.resetClearFactor(); - } - - /** - * Set the limit of cached OTF 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 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 getProgressiveManifestsCache() { + return PROGRESSIVE_CACHE; } } From 4da05afe116c4bbe1d3f870f2ae9ba7d722dfaa0 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 18:23:00 +0200 Subject: [PATCH 23/38] [YouTube] Inline collectSegmentsData in YoutubeDashManifestCreator --- .../youtube/YoutubeDashManifestCreator.java | 133 ++++++------------ 1 file changed, 42 insertions(+), 91 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 22e8f57ba..d32028eac 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 @@ -1,5 +1,16 @@ 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; @@ -12,6 +23,17 @@ 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; @@ -23,28 +45,6 @@ import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import java.io.IOException; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -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.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; /** * Class to generate DASH manifests from YouTube OTF, progressive and ended/post-live-DVR streams. @@ -74,28 +74,6 @@ public final class YoutubeDashManifestCreator { */ private static final int MAXIMUM_REDIRECT_COUNT = 20; - /** - * A list of durations of segments of an OTF stream. - * - *

    - * 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 SEGMENTS_DURATION = new ArrayList<>(); - - /** - * A list of contiguous repetitions of durations of an OTF stream. - * - *

    - * 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 DURATION_REPETITIONS = new ArrayList<>(); - /** * Cache of DASH manifests generated for OTF streams. */ @@ -296,11 +274,7 @@ public final class YoutubeDashManifestCreator { } generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF); generateSegmentTimelineElement(document); - collectSegmentsData(segmentDuration); - generateSegmentElementsForOtfStreams(document); - - SEGMENTS_DURATION.clear(); - DURATION_REPETITIONS.clear(); + generateSegmentElementsForOtfStreams(segmentDuration, document); return buildAndCacheResult(otfBaseStreamingUrl, document, OTF_CACHE); } @@ -721,35 +695,6 @@ public final class YoutubeDashManifestCreator { } } - /** - * Collect all segments from an OTF stream, by parsing the string array which contains all the - * sequences. - * - * @param segmentDuration the string array which contains all the sequences extracted with the - * regular expression - */ - private static void collectSegmentsData(@Nonnull final String[] segmentDuration) - throws YoutubeDashManifestCreationException { - try { - 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]); - SEGMENTS_DURATION.add(segmentLength); - DURATION_REPETITIONS.add(segmentRepeatCount); - } - } catch (final NumberFormatException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: unable to get the segments of the " - + "stream", e); - } - } - /** * Get the duration of an OTF stream. * @@ -1429,8 +1374,7 @@ public final class YoutubeDashManifestCreator { * *

    * 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 } elements will be appended */ - private static void generateSegmentElementsForOtfStreams(@Nonnull final Document document) + private static void generateSegmentElementsForOtfStreams(final String[] segmentDurations, + final Document document) throws YoutubeDashManifestCreationException { + try { - if (SEGMENTS_DURATION.isEmpty() || DURATION_REPETITIONS.isEmpty()) { - throw new IllegalStateException( - "Duration of segments and/or repetition(s) of segments are unknown"); - } final Element segmentTimelineElement = (Element) document.getElementsByTagName( "SegmentTimeline").item(0); - for (int i = 0; i < SEGMENTS_DURATION.size(); i++) { + for (final String segmentDuration : segmentDurations) { final Element sElement = document.createElement("S"); - final int durationRepetition = DURATION_REPETITIONS.get(i); - if (durationRepetition != 0) { + final String[] segmentLengthRepeat = segmentDuration.split("\\(r="); + // make sure segmentLengthRepeat[0], which is the length, is convertible to int + Integer.parseInt(segmentLengthRepeat[0]); + + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + final int segmentRepeatCount = Integer.parseInt( + Utils.removeNonDigitCharacters(segmentLengthRepeat[1])); final Attr rAttribute = document.createAttribute("r"); - rAttribute.setValue(String.valueOf(durationRepetition)); + rAttribute.setValue(String.valueOf(segmentRepeatCount)); sElement.setAttributeNode(rAttribute); } final Attr dAttribute = document.createAttribute("d"); - dAttribute.setValue(String.valueOf(SEGMENTS_DURATION.get(i))); + dAttribute.setValue(segmentLengthRepeat[0]); sElement.setAttributeNode(dAttribute); segmentTimelineElement.appendChild(sElement); } - } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException e) { + } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException + | NumberFormatException e) { throw new YoutubeDashManifestCreationException( "Could not generate or append the segment (S) elements of the DASH manifest " + "to the document", e); From 00bbe5eb4dcd517ed08c3844c181ca602499148c Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 19:30:36 +0200 Subject: [PATCH 24/38] [YouTube] Make exception messages smaller --- .../youtube/YoutubeDashManifestCreator.java | 269 ++++++------------ 1 file changed, 94 insertions(+), 175 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 d32028eac..14354420a 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 @@ -52,23 +52,6 @@ import javax.xml.transform.stream.StreamResult; */ public final class YoutubeDashManifestCreator { - /** - * URL parameter of the first sequence for live, post-live-DVR and OTF streams. - */ - private static final String SQ_0 = "&sq=0"; - - /** - * URL parameter of the first stream request made by official clients. - */ - private 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. - */ - private static final String ALR_YES = "&alr=yes"; - /** * The redirect count limit that this class uses, which is the same limit as OkHttp. */ @@ -92,6 +75,28 @@ public final class YoutubeDashManifestCreator { private static final ManifestCreatorCache PROGRESSIVE_CACHE = new ManifestCreatorCache<>(); + + /** + * URL parameter of the first sequence for live, post-live-DVR and OTF streams. + */ + private static final String SQ_0 = "&sq=0"; + + /** + * URL parameter of the first stream request made by official clients. + */ + private 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. + */ + private static final String ALR_YES = "&alr=yes"; + + public static final String SEGMENT_TIMELINE = "SegmentTimeline"; + public static final String ADAPTATION_SET = "AdaptationSet"; + public static final String REPRESENTATION = "Representation"; + /** * Enum of streaming format types used by YouTube in their streams. */ @@ -360,8 +365,7 @@ public final class YoutubeDashManifestCreator { if (targetDurationSec <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the targetDurationSec value is less " - + "than or equal to 0 (" + targetDurationSec + ")"); + "targetDurationSec value is <= 0: " + targetDurationSec); } try { @@ -374,26 +378,22 @@ public final class YoutubeDashManifestCreator { final int responseCode = response.responseCode(); if (responseCode != 200) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not get the initialization " - + "segment of the post-live-DVR stream: response code " - + responseCode); + throw new YoutubeDashManifestCreationException("Could not get the initialization " + + "segment of the post-live-DVR stream: response code " + responseCode); } final Map> responseHeaders = response.responseHeaders(); streamDuration = responseHeaders.get("X-Head-Time-Millis").get(0); 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); + throw new YoutubeDashManifestCreationException("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"); + throw new YoutubeDashManifestCreationException("Could not get the number of segments " + + "of the post-live-DVR stream"); } final Document document = generateDocumentAndMpdElement(new String[] {streamDuration}, @@ -481,12 +481,6 @@ public final class YoutubeDashManifestCreator { .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); @@ -555,19 +549,16 @@ public final class YoutubeDashManifestCreator { final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); 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 " - + (isAnIosStreamingUrl ? "ANDROID" : "IOS") - + " streaming URL response", e); + throw new YoutubeDashManifestCreationException("Could not get the " + + (isAnIosStreamingUrl ? "ANDROID" : "IOS") + " streaming URL response", e); } } try { return downloader.get(baseStreamingUrl); } catch (final IOException | ExtractionException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: error when trying to get the streaming " - + "URL response", e); + throw new YoutubeDashManifestCreationException("Could not get the streaming URL " + + "response", e); } } @@ -640,33 +631,15 @@ public final class YoutubeDashManifestCreator { final int responseCode = response.responseCode(); 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); - } 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 " - + responseCode); - } else { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: could not fetch the URL of " - + "the progressive stream: response code " + responseCode); - } + throw new YoutubeDashManifestCreationException("Could not get the " + + "initialization URL of the " + deliveryType + + " stream: response code " + responseCode); } // A valid response must include a Content-Type header, so we can require that // the response from video servers has this header. - try { - responseMimeType = Objects.requireNonNull(response.getHeader( - "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); - } + responseMimeType = Objects.requireNonNull(response.getHeader("Content-Type"), + "Could not get the Content-Type header from the streaming URL"); // The response body is the redirection URL if (responseMimeType.equals("text/plain")) { @@ -679,19 +652,16 @@ 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"); + "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 get the WEB streaming URL response: unreachable code reached!"); } 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 get the WEB streaming URL response", e); } } @@ -724,8 +694,7 @@ 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", + throw new YoutubeDashManifestCreationException("Unable to get the length of the stream", e); } } @@ -813,11 +782,9 @@ public final class YoutubeDashManifestCreator { if (durationSecondsFallback > 0) { 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"); + throw new YoutubeDashManifestCreationException("Could not add MPD element: " + + "the duration of the stream could not be determined and the " + + "durationSecondsFallback is <= 0"); } } } @@ -828,9 +795,7 @@ public final class YoutubeDashManifestCreator { 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); + throw new YoutubeDashManifestCreationException("Could not add MPD element", e); } } @@ -852,9 +817,7 @@ public final class YoutubeDashManifestCreator { final Element periodElement = document.createElement("Period"); 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); + throw new YoutubeDashManifestCreationException("Could not add Period element", e); } } @@ -876,7 +839,7 @@ public final class YoutubeDashManifestCreator { try { final Element periodElement = (Element) document.getElementsByTagName("Period") .item(0); - final Element adaptationSetElement = document.createElement("AdaptationSet"); + final Element adaptationSetElement = document.createElement(ADAPTATION_SET); final Attr idAttribute = document.createAttribute("id"); idAttribute.setValue("0"); @@ -884,10 +847,8 @@ 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"); + throw new YoutubeDashManifestCreationException("Could not add AdaptationSet " + + "element: the MediaFormat or its mime type are null or empty"); } final Attr mimeTypeAttribute = document.createAttribute("mimeType"); @@ -901,9 +862,8 @@ public final class YoutubeDashManifestCreator { 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); + throw new YoutubeDashManifestCreationException("Could not add AdaptationSet element", + e); } } @@ -931,7 +891,7 @@ public final class YoutubeDashManifestCreator { throws YoutubeDashManifestCreationException { try { final Element adaptationSetElement = (Element) document.getElementsByTagName( - "AdaptationSet").item(0); + ADAPTATION_SET).item(0); final Element roleElement = document.createElement("Role"); final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); @@ -944,9 +904,7 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(roleElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the Role element of the DASH manifest to the " - + "document", e); + throw new YoutubeDashManifestCreationException("Could not add Role element", e); } } @@ -968,15 +926,13 @@ public final class YoutubeDashManifestCreator { throws YoutubeDashManifestCreationException { try { final Element adaptationSetElement = (Element) document.getElementsByTagName( - "AdaptationSet").item(0); - final Element representationElement = document.createElement("Representation"); + ADAPTATION_SET).item(0); + final Element representationElement = document.createElement(REPRESENTATION); final int id = itagItem.id; if (id <= 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest to " - + "the document: the id of the ItagItem is less than or equal to " - + "0"); + throw new YoutubeDashManifestCreationException("Could not add Representation " + + "element: the id of the ItagItem is <= 0"); } final Attr idAttribute = document.createAttribute("id"); idAttribute.setValue(String.valueOf(id)); @@ -984,9 +940,8 @@ public final class YoutubeDashManifestCreator { final String codec = itagItem.getCodec(); if (isNullOrEmpty(codec)) { - throw new YoutubeDashManifestCreationException( - "Could not generate the AdaptationSet element of the DASH manifest to the " - + "document: the codec value is null or empty"); + throw new YoutubeDashManifestCreationException("Could not add AdaptationSet " + + "element: the codec value is null or empty"); } final Attr codecsAttribute = document.createAttribute("codecs"); codecsAttribute.setValue(codec); @@ -1002,10 +957,8 @@ public final class YoutubeDashManifestCreator { final int bitrate = itagItem.getBitrate(); if (bitrate <= 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest to " - + "the document: the bitrate of the ItagItem is less than or " - + "equal to 0"); + throw new YoutubeDashManifestCreationException("Could not add Representation " + + "element: the bitrate of the ItagItem is <= 0"); } final Attr bandwidthAttribute = document.createAttribute("bandwidth"); bandwidthAttribute.setValue(String.valueOf(bitrate)); @@ -1017,10 +970,8 @@ public final class YoutubeDashManifestCreator { final int height = itagItem.getHeight(); final int width = itagItem.getWidth(); if (height <= 0 && width <= 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the Representation element of the DASH manifest " - + "to the document: the width and the height of the ItagItem " - + "are less than or equal to 0"); + throw new YoutubeDashManifestCreationException("Could not add Representation " + + "element: both width and height of the ItagItem are <= 0"); } if (width > 0) { @@ -1049,9 +1000,8 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(representationElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the Representation element of the DASH manifest " - + "to the document", e); + throw new YoutubeDashManifestCreationException("Could not add Representation element", + e); } } @@ -1088,7 +1038,7 @@ public final class YoutubeDashManifestCreator { @Nonnull final ItagItem itagItem) throws YoutubeDashManifestCreationException { try { final Element representationElement = (Element) document.getElementsByTagName( - "Representation").item(0); + REPRESENTATION).item(0); final Element audioChannelConfigurationElement = document.createElement( "AudioChannelConfiguration"); @@ -1101,8 +1051,7 @@ public final class YoutubeDashManifestCreator { final int audioChannels = itagItem.getAudioChannels(); if (audioChannels <= 0) { throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the audioChannels value is less " - + "than or equal to 0 (" + audioChannels + ")"); + "audioChannels is <= 0: " + audioChannels); } valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); audioChannelConfigurationElement.setAttributeNode(valueAttribute); @@ -1110,8 +1059,7 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(audioChannelConfigurationElement); } catch (final DOMException e) { throw new YoutubeDashManifestCreationException( - "Could not generate or append the AudioChannelConfiguration element of the " - + "DASH manifest to the document", e); + "Could not add AudioChannelConfiguration element", e); } } @@ -1138,14 +1086,12 @@ public final class YoutubeDashManifestCreator { throws YoutubeDashManifestCreationException { try { final Element representationElement = (Element) document.getElementsByTagName( - "Representation").item(0); + REPRESENTATION).item(0); final Element baseURLElement = document.createElement("BaseURL"); baseURLElement.setTextContent(baseUrl); representationElement.appendChild(baseURLElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the BaseURL element of the DASH manifest to the " - + "document", e); + throw new YoutubeDashManifestCreationException("Could not add BaseURL element", e); } } @@ -1180,33 +1126,22 @@ public final class YoutubeDashManifestCreator { throws YoutubeDashManifestCreationException { try { final Element representationElement = (Element) document.getElementsByTagName( - "Representation").item(0); + REPRESENTATION).item(0); final Element segmentBaseElement = document.createElement("SegmentBase"); - final Attr indexRangeAttribute = document.createAttribute("indexRange"); - final int indexStart = itagItem.getIndexStart(); - if (indexStart < 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the indexStart value of the " - + "ItagItem is less than to 0 (" + indexStart + ")"); - } - final int indexEnd = itagItem.getIndexEnd(); - if (indexEnd < 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the indexEnd value of the ItagItem " - + "is less than to 0 (" + indexStart + ")"); + if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { + throw new YoutubeDashManifestCreationException("ItagItem's indexStart or indexEnd " + + "are < 0: " + itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); } - indexRangeAttribute.setValue(indexStart + "-" + indexEnd); + indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); segmentBaseElement.setAttributeNode(indexRangeAttribute); representationElement.appendChild(segmentBaseElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the SegmentBase element of the DASH manifest to " - + "the document", e); + throw new YoutubeDashManifestCreationException("Could not add SegmentBase element", e); } } @@ -1244,30 +1179,20 @@ public final class YoutubeDashManifestCreator { "SegmentBase").item(0); final Element initializationElement = document.createElement("Initialization"); - final Attr rangeAttribute = document.createAttribute("range"); - final int initStart = itagItem.getInitStart(); - if (initStart < 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the initStart value of the " - + "ItagItem is less than to 0 (" + initStart + ")"); - } - final int initEnd = itagItem.getInitEnd(); - if (initEnd < 0) { - throw new YoutubeDashManifestCreationException( - "Could not generate the DASH manifest: the initEnd value of the ItagItem " - + "is less than to 0 (" + initEnd + ")"); + if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { + throw new YoutubeDashManifestCreationException("ItagItem's initStart or initEnd " + + "are < 0: " + itagItem.getInitStart() + "-" + itagItem.getInitEnd()); } - rangeAttribute.setValue(initStart + "-" + initEnd); + rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd()); initializationElement.setAttributeNode(rangeAttribute); segmentBaseElement.appendChild(initializationElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the Initialization element of the DASH manifest " - + "to the document", e); + throw new YoutubeDashManifestCreationException("Could not add Initialization element", + e); } } @@ -1308,7 +1233,7 @@ public final class YoutubeDashManifestCreator { throws YoutubeDashManifestCreationException { try { final Element representationElement = (Element) document.getElementsByTagName( - "Representation").item(0); + REPRESENTATION).item(0); final Element segmentTemplateElement = document.createElement("SegmentTemplate"); final Attr startNumberAttribute = document.createAttribute("startNumber"); @@ -1336,9 +1261,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); + throw new YoutubeDashManifestCreationException("Could not add SegmentTemplate element", + e); } } @@ -1359,13 +1283,12 @@ public final class YoutubeDashManifestCreator { try { final Element segmentTemplateElement = (Element) document.getElementsByTagName( "SegmentTemplate").item(0); - final Element segmentTimelineElement = document.createElement("SegmentTimeline"); + final Element segmentTimelineElement = document.createElement(SEGMENT_TIMELINE); 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); + throw new YoutubeDashManifestCreationException("Could not add SegmentTimeline element", + e); } } @@ -1405,7 +1328,7 @@ public final class YoutubeDashManifestCreator { try { final Element segmentTimelineElement = (Element) document.getElementsByTagName( - "SegmentTimeline").item(0); + SEGMENT_TIMELINE).item(0); for (final String segmentDuration : segmentDurations) { final Element sElement = document.createElement("S"); @@ -1432,9 +1355,7 @@ public final class YoutubeDashManifestCreator { } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException | NumberFormatException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the segment (S) elements of the DASH manifest " - + "to the document", e); + throw new YoutubeDashManifestCreationException("Could not add segment (S) elements", e); } } @@ -1462,7 +1383,7 @@ public final class YoutubeDashManifestCreator { @Nonnull final String segmentCount) throws YoutubeDashManifestCreationException { try { final Element segmentTimelineElement = (Element) document.getElementsByTagName( - "SegmentTimeline").item(0); + SEGMENT_TIMELINE).item(0); final Element sElement = document.createElement("S"); final Attr dAttribute = document.createAttribute("d"); @@ -1475,9 +1396,7 @@ public final class YoutubeDashManifestCreator { segmentTimelineElement.appendChild(sElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not generate or append the segment (S) elements of the DASH manifest " - + "to the document", e); + throw new YoutubeDashManifestCreationException("Could not add segment (S) elements", e); } } From cfc13f4a6f6d3d3266de1dfb112335aef873c00a Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 20:06:04 +0200 Subject: [PATCH 25/38] [YouTube] Reduce exception generation code and move several attributes of MPD documents into constants Rename YoutubeDashManifestCreationException to CreationException. Also use these constants in YoutubeDashManifestCreatorTest. --- .../youtube/YoutubeDashManifestCreator.java | 186 +++++++++--------- .../YoutubeDashManifestCreatorTest.java | 29 +-- 2 files changed, 110 insertions(+), 105 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 14354420a..5e71f7587 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 @@ -96,6 +96,10 @@ public final class YoutubeDashManifestCreator { public static final String SEGMENT_TIMELINE = "SegmentTimeline"; public static final String ADAPTATION_SET = "AdaptationSet"; public static final String REPRESENTATION = "Representation"; + public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; + public static final String INITIALIZATION = "Initialization"; + public static final String PERIOD = "Period"; + public static final String SEGMENT_BASE = "SegmentBase"; /** * Enum of streaming format types used by YouTube in their streams. @@ -154,15 +158,23 @@ public final class YoutubeDashManifestCreator { * Exception that is thrown when the {@link YoutubeDashManifestCreator} encounters a problem * while creating a manifest. */ - public static final class YoutubeDashManifestCreationException extends Exception { + public static final class CreationException extends Exception { - YoutubeDashManifestCreationException(final String message) { + CreationException(final String message) { super(message); } - YoutubeDashManifestCreationException(final String message, final Exception e) { + 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); + } } /** @@ -226,7 +238,7 @@ public final class YoutubeDashManifestCreator { public static String fromOtfStreamingUrl( @Nonnull final String otfBaseStreamingUrl, @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws YoutubeDashManifestCreationException { + final long durationSecondsFallback) throws CreationException { if (OTF_CACHE.containsKey(otfBaseStreamingUrl)) { return Objects.requireNonNull(OTF_CACHE.get(otfBaseStreamingUrl)).getSecond(); } @@ -241,9 +253,8 @@ public final class YoutubeDashManifestCreator { 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); + throw new CreationException("Could not get the initialization URL of " + + "the OTF stream: response code " + responseCode); } final String[] segmentDuration; @@ -263,9 +274,7 @@ public final class YoutubeDashManifestCreator { segmentDuration = segmentsAndDurationsResponseSplit; } } catch (final Exception e) { - throw new YoutubeDashManifestCreationException( - "Unable to generate the DASH manifest: could not get the duration of segments", - e); + throw new CreationException("Could not get segment durations", e); } final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF, @@ -354,7 +363,7 @@ public final class YoutubeDashManifestCreator { @Nonnull final String postLiveStreamDvrStreamingUrl, @Nonnull final ItagItem itagItem, final int targetDurationSec, - final long durationSecondsFallback) throws YoutubeDashManifestCreationException { + final long durationSecondsFallback) throws CreationException { if (POST_LIVE_DVR_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) { return Objects.requireNonNull(POST_LIVE_DVR_CACHE.get(postLiveStreamDvrStreamingUrl)) .getSecond(); @@ -364,8 +373,7 @@ public final class YoutubeDashManifestCreator { final String segmentCount; if (targetDurationSec <= 0) { - throw new YoutubeDashManifestCreationException( - "targetDurationSec value is <= 0: " + targetDurationSec); + throw new CreationException("targetDurationSec value is <= 0: " + targetDurationSec); } try { @@ -378,7 +386,7 @@ public final class YoutubeDashManifestCreator { final int responseCode = response.responseCode(); if (responseCode != 200) { - throw new YoutubeDashManifestCreationException("Could not get the initialization " + throw new CreationException("Could not get the initialization " + "segment of the post-live-DVR stream: response code " + responseCode); } @@ -386,14 +394,13 @@ public final class YoutubeDashManifestCreator { streamDuration = responseHeaders.get("X-Head-Time-Millis").get(0); segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); } catch (final IndexOutOfBoundsException e) { - throw new YoutubeDashManifestCreationException("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); + throw new CreationException("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 get the number of segments " - + "of the post-live-DVR stream"); + throw new CreationException( + "Could not get the number of segments of the post-live-DVR stream"); } final Document document = generateDocumentAndMpdElement(new String[] {streamDuration}, @@ -475,7 +482,7 @@ public final class YoutubeDashManifestCreator { public static String fromProgressiveStreamingUrl( @Nonnull final String progressiveStreamingBaseUrl, @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) throws YoutubeDashManifestCreationException { + final long durationSecondsFallback) throws CreationException { if (PROGRESSIVE_CACHE.containsKey(progressiveStreamingBaseUrl)) { return Objects.requireNonNull(PROGRESSIVE_CACHE.get(progressiveStreamingBaseUrl)) .getSecond(); @@ -523,7 +530,7 @@ public final class YoutubeDashManifestCreator { private static Response getInitializationResponse(@Nonnull String baseStreamingUrl, @Nonnull final ItagItem itagItem, final DeliveryType deliveryType) - throws YoutubeDashManifestCreationException { + throws CreationException { final boolean isAHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); @@ -549,7 +556,7 @@ public final class YoutubeDashManifestCreator { final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); return downloader.post(baseStreamingUrl, headers, emptyBody); } catch (final IOException | ExtractionException e) { - throw new YoutubeDashManifestCreationException("Could not get the " + throw new CreationException("Could not get the " + (isAnIosStreamingUrl ? "ANDROID" : "IOS") + " streaming URL response", e); } } @@ -557,8 +564,7 @@ public final class YoutubeDashManifestCreator { try { return downloader.get(baseStreamingUrl); } catch (final IOException | ExtractionException e) { - throw new YoutubeDashManifestCreationException("Could not get the streaming URL " - + "response", e); + throw new CreationException("Could not get the streaming URL response", e); } } @@ -610,7 +616,7 @@ public final class YoutubeDashManifestCreator { @Nonnull final Downloader downloader, @Nonnull String streamingUrl, @Nonnull final String responseMimeTypeExpected, - @Nonnull final DeliveryType deliveryType) throws YoutubeDashManifestCreationException { + @Nonnull final DeliveryType deliveryType) throws CreationException { try { final Map> headers = new HashMap<>(); addClientInfoHeaders(headers); @@ -631,9 +637,8 @@ public final class YoutubeDashManifestCreator { final int responseCode = response.responseCode(); if (responseCode != 200) { - throw new YoutubeDashManifestCreationException("Could not get the " - + "initialization URL of the " + deliveryType - + " stream: response code " + responseCode); + throw new CreationException("Could not get the initialization URL of the " + + deliveryType + " stream: response code " + responseCode); } // A valid response must include a Content-Type header, so we can require that @@ -651,17 +656,16 @@ public final class YoutubeDashManifestCreator { } if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) { - throw new YoutubeDashManifestCreationException( + throw new CreationException( "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( + throw new CreationException( "Could not get the WEB streaming URL response: unreachable code reached!"); } catch (final IOException | ExtractionException e) { - throw new YoutubeDashManifestCreationException( - "Could not get the WEB streaming URL response", e); + throw new CreationException("Could not get the WEB streaming URL response", e); } } @@ -678,7 +682,7 @@ public final class YoutubeDashManifestCreator { * @return the duration of the OTF stream */ private static int getStreamDuration(@Nonnull final String[] segmentDuration) - throws YoutubeDashManifestCreationException { + throws CreationException { try { int streamLengthMs = 0; for (final String segDuration : segmentDuration) { @@ -694,8 +698,7 @@ public final class YoutubeDashManifestCreator { } return streamLengthMs; } catch (final NumberFormatException e) { - throw new YoutubeDashManifestCreationException("Unable to get the length of the stream", - e); + throw new CreationException("Could not get stream length", e); } } @@ -736,7 +739,7 @@ public final class YoutubeDashManifestCreator { final DeliveryType deliveryType, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Document document = newDocument(); @@ -782,8 +785,8 @@ public final class YoutubeDashManifestCreator { if (durationSecondsFallback > 0) { streamDuration = durationSecondsFallback * 1000; } else { - throw new YoutubeDashManifestCreationException("Could not add MPD element: " - + "the duration of the stream could not be determined and the " + throw CreationException.couldNotAdd("MPD", + "the duration of the stream could not be determined and the " + "durationSecondsFallback is <= 0"); } } @@ -795,7 +798,7 @@ public final class YoutubeDashManifestCreator { return document; } catch (final Exception e) { - throw new YoutubeDashManifestCreationException("Could not add MPD element", e); + throw CreationException.couldNotAdd("MPD", e); } } @@ -811,13 +814,13 @@ public final class YoutubeDashManifestCreator { * appended */ private static void generatePeriodElement(@Nonnull final Document document) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); - final Element periodElement = document.createElement("Period"); + final Element periodElement = document.createElement(PERIOD); mpdElement.appendChild(periodElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add Period element", e); + throw CreationException.couldNotAdd(PERIOD, e); } } @@ -835,9 +838,9 @@ public final class YoutubeDashManifestCreator { */ private static void generateAdaptationSetElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) - throws YoutubeDashManifestCreationException { + throws CreationException { try { - final Element periodElement = (Element) document.getElementsByTagName("Period") + final Element periodElement = (Element) document.getElementsByTagName(PERIOD) .item(0); final Element adaptationSetElement = document.createElement(ADAPTATION_SET); @@ -847,8 +850,8 @@ public final class YoutubeDashManifestCreator { final MediaFormat mediaFormat = itagItem.getMediaFormat(); if (mediaFormat == null || isNullOrEmpty(mediaFormat.mimeType)) { - throw new YoutubeDashManifestCreationException("Could not add AdaptationSet " - + "element: the MediaFormat or its mime type are null or empty"); + throw CreationException.couldNotAdd(ADAPTATION_SET, + "the MediaFormat or its mime type are null or empty"); } final Attr mimeTypeAttribute = document.createAttribute("mimeType"); @@ -862,8 +865,7 @@ public final class YoutubeDashManifestCreator { periodElement.appendChild(adaptationSetElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add AdaptationSet element", - e); + throw CreationException.couldNotAdd(ADAPTATION_SET, e); } } @@ -888,7 +890,7 @@ public final class YoutubeDashManifestCreator { * appended */ private static void generateRoleElement(@Nonnull final Document document) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element adaptationSetElement = (Element) document.getElementsByTagName( ADAPTATION_SET).item(0); @@ -904,7 +906,7 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(roleElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add Role element", e); + throw CreationException.couldNotAdd("Role", e); } } @@ -923,7 +925,7 @@ public final class YoutubeDashManifestCreator { */ private static void generateRepresentationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element adaptationSetElement = (Element) document.getElementsByTagName( ADAPTATION_SET).item(0); @@ -931,8 +933,8 @@ public final class YoutubeDashManifestCreator { final int id = itagItem.id; if (id <= 0) { - throw new YoutubeDashManifestCreationException("Could not add Representation " - + "element: the id of the ItagItem is <= 0"); + throw CreationException.couldNotAdd(REPRESENTATION, + "the id of the ItagItem is <= 0"); } final Attr idAttribute = document.createAttribute("id"); idAttribute.setValue(String.valueOf(id)); @@ -940,8 +942,8 @@ public final class YoutubeDashManifestCreator { final String codec = itagItem.getCodec(); if (isNullOrEmpty(codec)) { - throw new YoutubeDashManifestCreationException("Could not add AdaptationSet " - + "element: the codec value is null or empty"); + throw CreationException.couldNotAdd(ADAPTATION_SET, + "the codec value is null or empty"); } final Attr codecsAttribute = document.createAttribute("codecs"); codecsAttribute.setValue(codec); @@ -957,8 +959,8 @@ public final class YoutubeDashManifestCreator { final int bitrate = itagItem.getBitrate(); if (bitrate <= 0) { - throw new YoutubeDashManifestCreationException("Could not add Representation " - + "element: the bitrate of the ItagItem is <= 0"); + throw CreationException.couldNotAdd(REPRESENTATION, + "the bitrate of the ItagItem is <= 0"); } final Attr bandwidthAttribute = document.createAttribute("bandwidth"); bandwidthAttribute.setValue(String.valueOf(bitrate)); @@ -970,8 +972,8 @@ public final class YoutubeDashManifestCreator { final int height = itagItem.getHeight(); final int width = itagItem.getWidth(); if (height <= 0 && width <= 0) { - throw new YoutubeDashManifestCreationException("Could not add Representation " - + "element: both width and height of the ItagItem are <= 0"); + throw CreationException.couldNotAdd(REPRESENTATION, + "both width and height of the ItagItem are <= 0"); } if (width > 0) { @@ -1000,8 +1002,7 @@ public final class YoutubeDashManifestCreator { adaptationSetElement.appendChild(representationElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add Representation element", - e); + throw CreationException.couldNotAdd(REPRESENTATION, e); } } @@ -1035,7 +1036,7 @@ public final class YoutubeDashManifestCreator { */ private static void generateAudioChannelConfigurationElement( @Nonnull final Document document, - @Nonnull final ItagItem itagItem) throws YoutubeDashManifestCreationException { + @Nonnull final ItagItem itagItem) throws CreationException { try { final Element representationElement = (Element) document.getElementsByTagName( REPRESENTATION).item(0); @@ -1050,16 +1051,14 @@ public final class YoutubeDashManifestCreator { final Attr valueAttribute = document.createAttribute("value"); final int audioChannels = itagItem.getAudioChannels(); if (audioChannels <= 0) { - throw new YoutubeDashManifestCreationException( - "audioChannels is <= 0: " + audioChannels); + throw new CreationException("audioChannels is <= 0: " + audioChannels); } valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); audioChannelConfigurationElement.setAttributeNode(valueAttribute); representationElement.appendChild(audioChannelConfigurationElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException( - "Could not add AudioChannelConfiguration element", e); + throw CreationException.couldNotAdd("AudioChannelConfiguration", e); } } @@ -1083,7 +1082,7 @@ public final class YoutubeDashManifestCreator { */ private static void generateBaseUrlElement(@Nonnull final Document document, @Nonnull final String baseUrl) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element representationElement = (Element) document.getElementsByTagName( REPRESENTATION).item(0); @@ -1091,7 +1090,7 @@ public final class YoutubeDashManifestCreator { baseURLElement.setTextContent(baseUrl); representationElement.appendChild(baseURLElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add BaseURL element", e); + throw CreationException.couldNotAdd("BaseURL", e); } } @@ -1123,17 +1122,18 @@ public final class YoutubeDashManifestCreator { */ private static void generateSegmentBaseElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element representationElement = (Element) document.getElementsByTagName( REPRESENTATION).item(0); - final Element segmentBaseElement = document.createElement("SegmentBase"); + final Element segmentBaseElement = document.createElement(SEGMENT_BASE); final Attr indexRangeAttribute = document.createAttribute("indexRange"); if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { - throw new YoutubeDashManifestCreationException("ItagItem's indexStart or indexEnd " - + "are < 0: " + itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); + throw CreationException.couldNotAdd(SEGMENT_BASE, "ItagItem's indexStart or " + + "indexEnd are < 0: " + itagItem.getIndexStart() + "-" + + itagItem.getIndexEnd()); } indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); @@ -1141,7 +1141,7 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(segmentBaseElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add SegmentBase element", e); + throw CreationException.couldNotAdd(SEGMENT_BASE, e); } } @@ -1173,17 +1173,18 @@ public final class YoutubeDashManifestCreator { */ private static void generateInitializationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element segmentBaseElement = (Element) document.getElementsByTagName( - "SegmentBase").item(0); + SEGMENT_BASE).item(0); - final Element initializationElement = document.createElement("Initialization"); + final Element initializationElement = document.createElement(INITIALIZATION); final Attr rangeAttribute = document.createAttribute("range"); if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { - throw new YoutubeDashManifestCreationException("ItagItem's initStart or initEnd " - + "are < 0: " + itagItem.getInitStart() + "-" + itagItem.getInitEnd()); + throw CreationException.couldNotAdd(INITIALIZATION, "ItagItem's initStart or " + + "initEnd are < 0: " + itagItem.getInitStart() + "-" + + itagItem.getInitEnd()); } rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd()); @@ -1191,8 +1192,7 @@ public final class YoutubeDashManifestCreator { segmentBaseElement.appendChild(initializationElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add Initialization element", - e); + throw CreationException.couldNotAdd(INITIALIZATION, e); } } @@ -1230,11 +1230,11 @@ public final class YoutubeDashManifestCreator { private static void generateSegmentTemplateElement(@Nonnull final Document document, @Nonnull final String baseUrl, final DeliveryType deliveryType) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element representationElement = (Element) document.getElementsByTagName( REPRESENTATION).item(0); - final Element segmentTemplateElement = document.createElement("SegmentTemplate"); + final Element segmentTemplateElement = document.createElement(SEGMENT_TEMPLATE); final Attr startNumberAttribute = document.createAttribute("startNumber"); final boolean isDeliveryTypeLive = deliveryType == DeliveryType.LIVE; @@ -1261,8 +1261,7 @@ public final class YoutubeDashManifestCreator { representationElement.appendChild(segmentTemplateElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add SegmentTemplate element", - e); + throw CreationException.couldNotAdd(SEGMENT_TEMPLATE, e); } } @@ -1279,16 +1278,15 @@ public final class YoutubeDashManifestCreator { * be appended */ private static void generateSegmentTimelineElement(@Nonnull final Document document) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element segmentTemplateElement = (Element) document.getElementsByTagName( - "SegmentTemplate").item(0); + SEGMENT_TEMPLATE).item(0); final Element segmentTimelineElement = document.createElement(SEGMENT_TIMELINE); segmentTemplateElement.appendChild(segmentTimelineElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add SegmentTimeline element", - e); + throw CreationException.couldNotAdd(SEGMENT_TIMELINE, e); } } @@ -1324,7 +1322,7 @@ public final class YoutubeDashManifestCreator { */ private static void generateSegmentElementsForOtfStreams(final String[] segmentDurations, final Document document) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final Element segmentTimelineElement = (Element) document.getElementsByTagName( @@ -1355,7 +1353,7 @@ public final class YoutubeDashManifestCreator { } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException | NumberFormatException e) { - throw new YoutubeDashManifestCreationException("Could not add segment (S) elements", e); + throw CreationException.couldNotAdd("segment (S)", e); } } @@ -1380,7 +1378,7 @@ public final class YoutubeDashManifestCreator { private static void generateSegmentElementForPostLiveDvrStreams( @Nonnull final Document document, final int targetDurationSeconds, - @Nonnull final String segmentCount) throws YoutubeDashManifestCreationException { + @Nonnull final String segmentCount) throws CreationException { try { final Element segmentTimelineElement = (Element) document.getElementsByTagName( SEGMENT_TIMELINE).item(0); @@ -1396,7 +1394,7 @@ public final class YoutubeDashManifestCreator { segmentTimelineElement.appendChild(sElement); } catch (final DOMException e) { - throw new YoutubeDashManifestCreationException("Could not add segment (S) elements", e); + throw CreationException.couldNotAdd("segment (S)", e); } } @@ -1414,14 +1412,14 @@ public final class YoutubeDashManifestCreator { @Nonnull final String originalBaseStreamingUrl, @Nonnull final Document document, @Nonnull final ManifestCreatorCache manifestCreatorCache) - throws YoutubeDashManifestCreationException { + throws CreationException { try { final String documentXml = documentToXml(document); manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); return documentXml; } catch (final Exception e) { - throw new YoutubeDashManifestCreationException( + throw new CreationException( "Could not convert the DASH manifest generated to a string", e); } } 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 c5fdf71af..ca8185b9f 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 @@ -11,6 +11,13 @@ import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.ADAPTATION_SET; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.INITIALIZATION; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.PERIOD; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.REPRESENTATION; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_BASE; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_TEMPLATE; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_TIMELINE; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import org.junit.jupiter.api.BeforeAll; @@ -95,7 +102,7 @@ class YoutubeDashManifestCreatorTest { assertProgressiveStreams(extractor.getAudioStreams()); // we are not able to generate DASH manifests of video formats with audio - assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + assertThrows(YoutubeDashManifestCreator.CreationException.class, () -> assertProgressiveStreams(extractor.getVideoStreams())); } @@ -193,22 +200,22 @@ class YoutubeDashManifestCreatorTest { } private void assertPeriodElement(@Nonnull final Document document) { - assertGetElement(document, "Period", "MPD"); + assertGetElement(document, PERIOD, "MPD"); } private void assertAdaptationSetElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { - final Element element = assertGetElement(document, "AdaptationSet", "Period"); + final Element element = assertGetElement(document, ADAPTATION_SET, PERIOD); assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType"); } private void assertRoleElement(@Nonnull final Document document) { - assertGetElement(document, "Role", "AdaptationSet"); + assertGetElement(document, "Role", ADAPTATION_SET); } private void assertRepresentationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { - final Element element = assertGetElement(document, "Representation", "AdaptationSet"); + final Element element = assertGetElement(document, REPRESENTATION, ADAPTATION_SET); assertAttrEquals(itagItem.getBitrate(), element, "bandwidth"); assertAttrEquals(itagItem.getCodec(), element, "codecs"); @@ -226,12 +233,12 @@ class YoutubeDashManifestCreatorTest { private void assertAudioChannelConfigurationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { final Element element = assertGetElement(document, - "AudioChannelConfiguration", "Representation"); + "AudioChannelConfiguration", REPRESENTATION); assertAttrEquals(itagItem.getAudioChannels(), element, "value"); } private void assertSegmentTemplateElement(@Nonnull final Document document) { - final Element element = assertGetElement(document, "SegmentTemplate", "Representation"); + final Element element = assertGetElement(document, SEGMENT_TEMPLATE, REPRESENTATION); final String initializationValue = element.getAttribute("initialization"); assertIsValidUrl(initializationValue); @@ -245,7 +252,7 @@ class YoutubeDashManifestCreatorTest { } private void assertSegmentTimelineAndSElements(@Nonnull final Document document) { - final Element element = assertGetElement(document, "SegmentTimeline", "SegmentTemplate"); + final Element element = assertGetElement(document, SEGMENT_TIMELINE, SEGMENT_TEMPLATE); final NodeList childNodes = element.getChildNodes(); assertGreater(0, childNodes.getLength()); @@ -269,19 +276,19 @@ class YoutubeDashManifestCreatorTest { } private void assertBaseUrlElement(@Nonnull final Document document) { - final Element element = assertGetElement(document, "BaseURL", "Representation"); + final Element element = assertGetElement(document, "BaseURL", REPRESENTATION); assertIsValidUrl(element.getTextContent()); } private void assertSegmentBaseElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { - final Element element = assertGetElement(document, "SegmentBase", "Representation"); + final Element element = assertGetElement(document, SEGMENT_BASE, REPRESENTATION); assertRangeEquals(itagItem.getIndexStart(), itagItem.getIndexEnd(), element, "indexRange"); } private void assertInitializationElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) { - final Element element = assertGetElement(document, "Initialization", "SegmentBase"); + final Element element = assertGetElement(document, INITIALIZATION, SEGMENT_BASE); assertRangeEquals(itagItem.getInitStart(), itagItem.getInitEnd(), element, "range"); } From 232182284414f600119fdf816159055df8688e1a Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 1 May 2022 20:57:51 +0200 Subject: [PATCH 26/38] Rename Stream's baseUrl to manifestUrl --- .../extractors/PeertubeStreamExtractor.java | 4 +- .../newpipe/extractor/stream/AudioStream.java | 29 ++++------- .../newpipe/extractor/stream/Stream.java | 29 +++++------ .../extractor/stream/SubtitlesStream.java | 49 ++++++++----------- .../newpipe/extractor/stream/VideoStream.java | 29 ++++------- 5 files changed, 54 insertions(+), 86 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java index 60666db92..d42e23ede 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamExtractor.java @@ -570,7 +570,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { .setDeliveryMethod(DeliveryMethod.HLS) .setMediaFormat(format) .setAverageBitrate(UNKNOWN_BITRATE) - .setBaseUrl(playlistUrl) + .setManifestUrl(playlistUrl) .build(); if (!Stream.containSimilarStream(audioStream, audioStreams)) { audioStreams.add(audioStream); @@ -623,7 +623,7 @@ public class PeertubeStreamExtractor extends StreamExtractor { .setDeliveryMethod(DeliveryMethod.HLS) .setResolution(resolution) .setMediaFormat(format) - .setBaseUrl(playlistUrl) + .setManifestUrl(playlistUrl) .build(); if (!Stream.containSimilarStream(videoStream, videoStreams)) { videoStreams.add(videoStream); 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 65062a649..44d6e15ff 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 @@ -56,7 +56,7 @@ public final class AudioStream extends Stream { @Nullable private MediaFormat mediaFormat; @Nullable - private String baseUrl; + private String manifestUrl; private int averageBitrate = UNKNOWN_BITRATE; @Nullable private ItagItem itagItem; @@ -148,22 +148,13 @@ public final class AudioStream extends Stream { } /** - * Set the base URL of the {@link AudioStream}. + * 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 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 } element, appended as a child of the * {@code } element. @@ -1120,6 +1121,7 @@ public final class YoutubeDashManifestCreator { * be appended * @param itagItem the {@link ItagItem} to use, which cannot be null */ + // CHECKSTYLE:ON private static void generateSegmentBaseElement(@Nonnull final Document document, @Nonnull final ItagItem itagItem) throws CreationException { From 4158fc46a0b941f13f08013a6abe17a00b553727 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 7 May 2022 19:23:21 +0200 Subject: [PATCH 28/38] [Bandcamp] Fix regression of Opus radio streams extraction When moving opus-lo into a constant, opus-lo was renamed to opus_lo and was only used if no MP3 stream was available (which was not the case before the changes in BandcampRadioStreamExtractor related to the addition of the support of all delivery methods), so these changes removed the ability to get Opus streams of Bandcamp radios. This commit reverts this unwanted change. --- .../bandcamp/extractors/BandcampRadioStreamExtractor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java index 7389d4238..9668b8035 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java @@ -33,7 +33,7 @@ import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { - private static final String OPUS_LO = "opus_lo"; + private static final String OPUS_LO = "opus-lo"; private static final String MP3_128 = "mp3-128"; private JsonObject showInfo; @@ -126,7 +126,9 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { .setMediaFormat(MediaFormat.MP3) .setAverageBitrate(128) .build()); - } else if (streams.has(OPUS_LO)) { + } + + if (streams.has(OPUS_LO)) { audioStreams.add(new AudioStream.Builder() .setId(OPUS_LO) .setContent(streams.getString(OPUS_LO), true) From 2f3920c6482bab4196111246c5728354653bd14e Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 7 May 2022 19:32:12 +0200 Subject: [PATCH 29/38] [YouTube] Return approxDurationMs value from YouTube's player response in ItagItems generated This change allows to build DASH manifests using YoutubeDashManifestCreator with the real duration of streams and prevent potential cuts of the end of progressive streams, because the duration in YouTube's player response is in seconds and not milliseconds. --- .../services/youtube/extractors/YoutubeStreamExtractor.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 507c21f28..012700129 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 @@ -20,6 +20,7 @@ package org.schabi.newpipe.extractor.services.youtube.extractors; +import static org.schabi.newpipe.extractor.services.youtube.ItagItem.APPROX_DURATION_MS_UNKNOWN; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CONTENT_CHECK_OK; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.CPN; import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK; @@ -1396,9 +1397,11 @@ public class YoutubeStreamExtractor extends StreamExtractor { itagItem.setAudioChannels(formatData.getInt("audioChannels")); } - // YouTube return the content length as a string + // YouTube return the content length and the approximate duration as strings itagItem.setContentLength(Long.parseLong(formatData.getString("contentLength", String.valueOf(CONTENT_LENGTH_UNKNOWN)))); + itagItem.setApproxDurationMs(Long.parseLong(formatData.getString("approxDurationMs", + String.valueOf(APPROX_DURATION_MS_UNKNOWN)))); final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem); From 301b9fa0244c78e4d8b1ef130ad58b7cd02bb02e Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sat, 7 May 2022 20:19:08 +0200 Subject: [PATCH 30/38] Remove hashCode and equals methods overrides of Stream classes --- .../newpipe/extractor/stream/AudioStream.java | 24 ----------------- .../newpipe/extractor/stream/Stream.java | 24 ----------------- .../extractor/stream/SubtitlesStream.java | 26 ------------------- .../newpipe/extractor/stream/VideoStream.java | 24 ----------------- 4 files changed, 98 deletions(-) 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 44d6e15ff..59cf9a323 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 @@ -25,7 +25,6 @@ import org.schabi.newpipe.extractor.services.youtube.ItagItem; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Objects; public final class AudioStream extends Stream { public static final int UNKNOWN_BITRATE = -1; @@ -381,27 +380,4 @@ public final class AudioStream extends Stream { public ItagItem getItagItem() { return itagItem; } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - if (!super.equals(obj)) { - return false; - } - - final AudioStream audioStream = (AudioStream) obj; - return averageBitrate == audioStream.averageBitrate; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), averageBitrate); - } } 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 44ce2772e..e9232c8cf 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 @@ -7,7 +7,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.Serializable; import java.util.List; -import java.util.Objects; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -214,27 +213,4 @@ public abstract class Stream implements Serializable { */ @Nullable public abstract ItagItem getItagItem(); - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - final Stream stream = (Stream) obj; - return id.equals(stream.id) && mediaFormat == stream.mediaFormat - && deliveryMethod == stream.deliveryMethod - && content.equals(stream.content) - && isUrl == stream.isUrl - && Objects.equals(manifestUrl, stream.manifestUrl); - } - - @Override - public int hashCode() { - 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 b40d0e928..796264d41 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 @@ -7,7 +7,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Locale; -import java.util.Objects; import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; @@ -328,29 +327,4 @@ public final class SubtitlesStream extends Stream { public ItagItem getItagItem() { return null; } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - if (!super.equals(obj)) { - return false; - } - - final SubtitlesStream subtitlesStream = (SubtitlesStream) obj; - return autoGenerated == subtitlesStream.autoGenerated - && locale.equals(subtitlesStream.locale) - && code.equals(subtitlesStream.code); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), locale, autoGenerated, code); - } } 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 0709af90e..14952ebd1 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 @@ -25,7 +25,6 @@ import org.schabi.newpipe.extractor.services.youtube.ItagItem; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Objects; public final class VideoStream extends Stream { public static final String RESOLUTION_UNKNOWN = ""; @@ -483,27 +482,4 @@ public final class VideoStream extends Stream { public ItagItem getItagItem() { return itagItem; } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - if (!super.equals(obj)) { - return false; - } - - final VideoStream videoStream = (VideoStream) obj; - return isVideoOnly == videoStream.isVideoOnly && resolution.equals(videoStream.resolution); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), resolution, isVideoOnly); - } } From f17f7b9842973757d57b25014ec6c39138ca48ec Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Tue, 10 May 2022 21:38:15 +0200 Subject: [PATCH 31/38] Apply requested changes in YoutubeParsingHelper --- .../extractor/services/youtube/YoutubeParsingHelper.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java index 0b94d3d71..566da5217 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java @@ -1264,8 +1264,7 @@ public final class YoutubeParsingHelper { // Spoofing an Android 12 device with the hardcoded version of the Android app return "com.google.android.youtube/" + MOBILE_YOUTUBE_CLIENT_VERSION + " (Linux; U; Android 12; " - + (localization == null ? Localization.DEFAULT.getCountryCode() - : localization.getCountryCode()) + + (localization != null ? localization : Localization.DEFAULT).getCountryCode() + ") gzip"; } @@ -1284,10 +1283,8 @@ public final class YoutubeParsingHelper { public static String getIosUserAgent(@Nullable final Localization localization) { // Spoofing an iPhone running iOS 15.4 with the hardcoded mobile client version return "com.google.ios.youtube/" + MOBILE_YOUTUBE_CLIENT_VERSION - + "(" + IOS_DEVICE_MODEL - + "; U; CPU iOS 15_4 like Mac OS X; " - + (localization != null ? localization.getCountryCode() - : Localization.DEFAULT.getCountryCode()) + + "(" + IOS_DEVICE_MODEL + "; U; CPU iOS 15_4 like Mac OS X; " + + (localization != null ? localization : Localization.DEFAULT).getCountryCode() + ")"; } From f7b151529008bb9a46731d4a8ed86b2bed351993 Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Wed, 11 May 2022 20:58:50 +0200 Subject: [PATCH 32/38] [YouTube] Refactor DASH manifests creation Move DASH manifests creation into a new subpackage of the YouTube package, dashmanifestcreators. This subpackage contains: - CreationException, exception extending Java's RuntimeException, thrown by manifest creators when something goes wrong; - YoutubeDashManifestCreatorsUtils, class which contains all common methods and constants of all or a part of the manifest creators; - a manifest creator has been added per delivery type of YouTube streams: - YoutubeProgressiveDashManifestCreator, for progressive streams; - YoutubeOtfDashManifestCreator, for OTF streams; - YoutubePostLiveStreamDvrDashManifestCreator, for post-live DVR streams (which use the live delivery method). Every DASH manifest creator has a getCache() static method, which returns the ManifestCreatorCache instance used to cache results. DeliveryType has been also extracted from the YouTube DASH manifest creators part of the extractor and moved to the YouTube package. YoutubeDashManifestCreatorTest has been updated and renamed to YoutubeDashManifestCreatorsTest, and YoutubeDashManifestCreator has been removed. Finally, several documentation and exception messages fixes and improvements have been made. --- .../services/youtube/DeliveryType.java | 55 + .../youtube/YoutubeDashManifestCreator.java | 1498 ----------------- .../CreationException.java | 63 + .../YoutubeDashManifestCreatorsUtils.java | 856 ++++++++++ .../YoutubeOtfDashManifestCreator.java | 268 +++ ...ePostLiveStreamDvrDashManifestCreator.java | 221 +++ ...YoutubeProgressiveDashManifestCreator.java | 244 +++ ...a => YoutubeDashManifestCreatorsTest.java} | 118 +- 8 files changed, 1773 insertions(+), 1550 deletions(-) create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java create mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java rename extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/{YoutubeDashManifestCreatorTest.java => YoutubeDashManifestCreatorsTest.java} (80%) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java new file mode 100644 index 000000000..0e5b34504 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java @@ -0,0 +1,55 @@ +package org.schabi.newpipe.extractor.services.youtube; + +/** + * Streaming format types used by YouTube in their streams. + * + *

    + * 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 OTF_CACHE - = new ManifestCreatorCache<>(); - - /** - * Cache of DASH manifests generated for post-live-DVR streams. - */ - private static final ManifestCreatorCache POST_LIVE_DVR_CACHE - = new ManifestCreatorCache<>(); - - /** - * Cache of DASH manifests generated for progressive streams. - */ - private static final ManifestCreatorCache PROGRESSIVE_CACHE - = new ManifestCreatorCache<>(); - - - /** - * URL parameter of the first sequence for live, post-live-DVR and OTF streams. - */ - private static final String SQ_0 = "&sq=0"; - - /** - * URL parameter of the first stream request made by official clients. - */ - private 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. - */ - private static final String ALR_YES = "&alr=yes"; - - public static final String SEGMENT_TIMELINE = "SegmentTimeline"; - public static final String ADAPTATION_SET = "AdaptationSet"; - public static final String REPRESENTATION = "Representation"; - public static final String SEGMENT_TEMPLATE = "SegmentTemplate"; - public static final String INITIALIZATION = "Initialization"; - public static final String PERIOD = "Period"; - public static final String SEGMENT_BASE = "SegmentBase"; - - /** - * Enum of streaming format types used by YouTube in their streams. - */ - private 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 {@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: - *

      - *
    • the base URL of the stream (which, if you try to access to it, returns HTTP - * status code 404 after redirects, and if the URL is valid);
    • - *
    • an {@link ItagItem}, which needs to contain the following information: - *
        - *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
      • - *
      • its bitrate;
      • - *
      • its mime type;
      • - *
      • its codec(s);
      • - *
      • for an audio stream: its audio channels;
      • - *
      • for a video stream: its width and height.
      • - *
      - *
    • - *
    • the duration of the video, which will be used if the duration could not be - * parsed from the first sequence of the stream.
    • - *
    - *

    - * - *

    In order to generate the DASH manifest, this method will: - *

      - *
    • request the first sequence of the stream (the base URL on which the first - * sequence parameter is appended (see {@link #SQ_0})) with a POST or GET request - * (depending of the client on which the streaming URL comes from); - *
    • - *
    • follow its redirection(s), if any;
    • - *
    • save the last URL, remove the first sequence parameter;
    • - *
    • use the information provided in the {@link ItagItem} to generate all - * elements of the DASH manifest.
    • - *
    - *

    - * - *

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

      - *
    • the base URL of the stream (which, if you try to access to it, returns HTTP - * status code 404 after redirects, and if the URL is valid);
    • - *
    • an {@link ItagItem}, which needs to contain the following information: - *
        - *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
      • - *
      • its bitrate;
      • - *
      • its mime type;
      • - *
      • its codec(s);
      • - *
      • for an audio stream: its audio channels;
      • - *
      • for a video stream: its width and height.
      • - *
      - *
    • - *
    • the duration of the video, which will be used if the duration could not be - * parsed from the first sequence of the stream.
    • - *
    - *

    - * - *

    In order to generate the DASH manifest, this method will: - *

      - *
    • request the first sequence of the stream (the base URL on which the first - * sequence parameter is appended (see {@link #SQ_0})) with a POST or GET request - * (depending of the client on which the streaming URL comes from); - *
    • - *
    • follow its redirection(s), if any;
    • - *
    • save the last URL, remove the first sequence parameters;
    • - *
    • use the information provided in the {@link ItagItem} to generate all - * elements of the DASH manifest.
    • - *
    - *

    - * - *

    - * 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> responseHeaders = response.responseHeaders(); - streamDuration = responseHeaders.get("X-Head-Time-Millis").get(0); - segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); - } catch (final IndexOutOfBoundsException e) { - throw new CreationException("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 CreationException( - "Could not get the number of segments of the post-live-DVR stream"); - } - - final Document document = generateDocumentAndMpdElement(new String[] {streamDuration}, - DeliveryType.LIVE, itagItem, durationSecondsFallback); - generatePeriodElement(document); - generateAdaptationSetElement(document, itagItem); - generateRoleElement(document); - generateRepresentationElement(document, itagItem); - if (itagItem.itagType == ItagItem.ItagType.AUDIO) { - generateAudioChannelConfigurationElement(document, itagItem); - } - generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl, - DeliveryType.LIVE); - generateSegmentTimelineElement(document); - generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); - - return buildAndCacheResult(postLiveStreamDvrStreamingUrl, document, - POST_LIVE_DVR_CACHE); - } - - /** - * Create DASH manifests from a YouTube progressive stream. - * - *

    - * 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 base URL of the stream (which, if you try to access to it, returns the whole - * stream, after redirects, and if the URL is valid);
    • - *
    • an {@link ItagItem}, which needs to contain the following information: - *
        - *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is - * an audio or a video stream;
      • - *
      • its bitrate;
      • - *
      • its mime type;
      • - *
      • its codec(s);
      • - *
      • for an audio stream: its audio channels;
      • - *
      • for a video stream: its width and height.
      • - *
      - *
    • - *
    • the duration of the video, which will be used if the duration could not be - * parsed from the {@link ItagItem}.
    • - *
    - *

    - * - *

    In order to generate the DASH manifest, this method will: - *

      - *
    • request the base URL of the stream with a HEAD request;
    • - *
    • follow its redirection(s), if any;
    • - *
    • save the last URL;
    • - *
    • use the information provided in the {@link ItagItem} to generate all - * elements of the DASH manifest.
    • - *
    - *

    - * - *

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

      - *
    • the base URL of the stream, to which are appended {@link #SQ_0} and {@link #RN_0} - * parameters, with a GET request for streaming URLs from the WEB client and a POST request - * for the ones from the Android client;
    • - *
    • for streaming URLs from the WEB client, the {@link #ALR_YES} param is also added. - *
    • - *
    - *

    - * - * @param baseStreamingUrl the base URL of the stream, which cannot be null - * @param itagItem the {@link ItagItem} of stream, which cannot be null - * @param deliveryType the {@link DeliveryType} of the stream - * @return the "initialization" response, without redirections on the network on which the - * request(s) is/are made - */ - @SuppressWarnings("checkstyle:FinalParameters") - @Nonnull - private static Response getInitializationResponse(@Nonnull String baseStreamingUrl, - @Nonnull final ItagItem itagItem, - final DeliveryType deliveryType) - throws CreationException { - final boolean isAHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) - || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); - final boolean isAnAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); - final boolean isAnIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); - if (isAHtml5StreamingUrl) { - baseStreamingUrl += ALR_YES; - } - baseStreamingUrl = appendRnParamAndSqParamIfNeeded(baseStreamingUrl, deliveryType); - - final Downloader downloader = NewPipe.getDownloader(); - if (isAHtml5StreamingUrl) { - final String mimeTypeExpected = itagItem.getMediaFormat().getMimeType(); - if (!isNullOrEmpty(mimeTypeExpected)) { - return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl, - mimeTypeExpected, deliveryType); - } - } else if (isAnAndroidStreamingUrl || isAnIosStreamingUrl) { - try { - final Map> headers = new HashMap<>(); - headers.put("User-Agent", Collections.singletonList( - isAnAndroidStreamingUrl ? getAndroidUserAgent(null) - : getIosUserAgent(null))); - final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); - return downloader.post(baseStreamingUrl, headers, emptyBody); - } catch (final IOException | ExtractionException e) { - throw new CreationException("Could not get the " - + (isAnIosStreamingUrl ? "ANDROID" : "IOS") + " streaming URL response", e); - } - } - - try { - return downloader.get(baseStreamingUrl); - } catch (final IOException | ExtractionException e) { - throw new CreationException("Could not get the streaming URL response", e); - } - } - - /** - * 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 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, - @Nonnull final DeliveryType deliveryType) { - if (deliveryType != DeliveryType.PROGRESSIVE) { - baseStreamingUrl += SQ_0; - } - return baseStreamingUrl + RN_0; - } - - /** - * Get a URL on which no redirection between playback hosts should be present on the network - * and/or IP used to fetch the streaming URL. - * - *

    - * This method will follow redirects for web clients, which works in the following way: - *

      - *
    1. the {@link #ALR_YES} param is appended to all streaming URLs
    2. - *
    3. if no redirection occurs, the video server will return the streaming data;
    4. - *
    5. if a redirection occurs, the server will respond with HTTP status code 200 and a - * text/plain mime type. The redirection URL is the response body;
    6. - *
    7. the redirection URL is requested and the steps above from step 2 are repeated (until - * too many redirects are reached, of course).
    8. - *
    - *

    - * - * @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 - * @param deliveryType the {@link DeliveryType} of the stream - * @return the 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, - @Nonnull final DeliveryType deliveryType) throws CreationException { - try { - final Map> headers = new HashMap<>(); - addClientInfoHeaders(headers); - - String responseMimeType = ""; - - int redirectsCount = 0; - while (!responseMimeType.equals(responseMimeTypeExpected) - && redirectsCount < MAXIMUM_REDIRECT_COUNT) { - final Response response; - // We can use head requests to reduce the request size, but only for progressive - // streams - if (deliveryType == DeliveryType.PROGRESSIVE) { - response = downloader.head(streamingUrl, headers); - } else { - response = downloader.get(streamingUrl, headers); - } - - final int responseCode = response.responseCode(); - if (responseCode != 200) { - throw new CreationException("Could not get the initialization URL of the " - + deliveryType + " stream: response code " + responseCode); - } - - // A valid response must include a Content-Type header, so we can require that - // the response from video servers has this header. - responseMimeType = Objects.requireNonNull(response.getHeader("Content-Type"), - "Could not get the Content-Type header from the streaming URL"); - - // The response body is the redirection URL - if (responseMimeType.equals("text/plain")) { - streamingUrl = response.responseBody(); - redirectsCount++; - } else { - return response; - } - } - - if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) { - throw new CreationException( - "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 CreationException( - "Could not get the WEB streaming URL response: unreachable code reached!"); - } catch (final IOException | ExtractionException e) { - throw new CreationException("Could not get the WEB streaming URL response", e); - } - } - - /** - * Get the duration of an OTF stream. - * - *

    - * 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 } element of the manifest. - * - *

    - * The generated {@code } element looks like the manifest returned into the player - * response of videos with OTF streams: - *

    - * - *

    - * {@code } - * (where {@code $duration$} represents the duration in seconds (a number with 3 digits after - * the decimal point) - *

    - * - *

    - * 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 } element - */ - private static Document generateDocumentAndMpdElement(@Nonnull final String[] segmentDuration, - final DeliveryType deliveryType, - @Nonnull final ItagItem itagItem, - final long durationSecondsFallback) - throws CreationException { - try { - final Document document = newDocument(); - - final Element mpdElement = document.createElement("MPD"); - document.appendChild(mpdElement); - - final Attr xmlnsXsiAttribute = document.createAttribute("xmlns:xsi"); - xmlnsXsiAttribute.setValue("http://www.w3.org/2001/XMLSchema-instance"); - mpdElement.setAttributeNode(xmlnsXsiAttribute); - - final Attr xmlns = document.createAttribute("xmlns"); - xmlns.setValue("urn:mpeg:DASH:schema:MPD:2011"); - mpdElement.setAttributeNode(xmlns); - - final Attr xsiSchemaLocationAttribute = document.createAttribute("xsi:schemaLocation"); - xsiSchemaLocationAttribute.setValue("urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); - mpdElement.setAttributeNode(xsiSchemaLocationAttribute); - - final Attr minBufferTimeAttribute = document.createAttribute("minBufferTime"); - minBufferTimeAttribute.setValue("PT1.500S"); - mpdElement.setAttributeNode(minBufferTimeAttribute); - - final Attr profilesAttribute = document.createAttribute("profiles"); - profilesAttribute.setValue("urn:mpeg:dash:profile:full:2011"); - mpdElement.setAttributeNode(profilesAttribute); - - final Attr typeAttribute = document.createAttribute("type"); - typeAttribute.setValue("static"); - mpdElement.setAttributeNode(typeAttribute); - - final Attr mediaPresentationDurationAttribute = document.createAttribute( - "mediaPresentationDuration"); - final long streamDuration; - if (deliveryType == DeliveryType.LIVE) { - streamDuration = Integer.parseInt(segmentDuration[0]); - } else if (deliveryType == DeliveryType.OTF) { - streamDuration = getStreamDuration(segmentDuration); - } else { - final long itagItemDuration = itagItem.getApproxDurationMs(); - if (itagItemDuration != -1) { - streamDuration = itagItemDuration; - } else { - if (durationSecondsFallback > 0) { - streamDuration = durationSecondsFallback * 1000; - } else { - throw CreationException.couldNotAdd("MPD", - "the duration of the stream could not be determined and the " - + "durationSecondsFallback is <= 0"); - } - } - } - final double duration = streamDuration / 1000.0; - final String durationSeconds = String.format(Locale.ENGLISH, "%.3f", duration); - mediaPresentationDurationAttribute.setValue("PT" + durationSeconds + "S"); - mpdElement.setAttributeNode(mediaPresentationDurationAttribute); - - return document; - } catch (final Exception e) { - throw CreationException.couldNotAdd("MPD", e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } element. - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateDocumentAndMpdElement(String[], DeliveryType, ItagItem, long)}. - *

    - * - * @param document the {@link Document} on which the the {@code } element will be - * appended - */ - private static void generatePeriodElement(@Nonnull final Document document) - throws CreationException { - try { - final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0); - final Element periodElement = document.createElement(PERIOD); - mpdElement.appendChild(periodElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(PERIOD, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } element. - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateDocumentAndMpdElement(String[], DeliveryType, ItagItem, long)}. - *

    - * - * @param document the {@link Document} on which the the {@code } element will be - * appended - * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null - */ - private static void generateAdaptationSetElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element periodElement = (Element) document.getElementsByTagName(PERIOD) - .item(0); - final Element adaptationSetElement = document.createElement(ADAPTATION_SET); - - final Attr idAttribute = document.createAttribute("id"); - idAttribute.setValue("0"); - adaptationSetElement.setAttributeNode(idAttribute); - - final MediaFormat mediaFormat = itagItem.getMediaFormat(); - if (mediaFormat == null || isNullOrEmpty(mediaFormat.mimeType)) { - throw CreationException.couldNotAdd(ADAPTATION_SET, - "the MediaFormat or its mime type are null or empty"); - } - - final Attr mimeTypeAttribute = document.createAttribute("mimeType"); - mimeTypeAttribute.setValue(mediaFormat.mimeType); - adaptationSetElement.setAttributeNode(mimeTypeAttribute); - - final Attr subsegmentAlignmentAttribute = document.createAttribute( - "subsegmentAlignment"); - subsegmentAlignmentAttribute.setValue("true"); - adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute); - - periodElement.appendChild(adaptationSetElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(ADAPTATION_SET, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the {@code } - * element. - * - *

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

    - * - *

    - * {@code } - *

    - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateAdaptationSetElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the the {@code } element will be - * appended - */ - private static void generateRoleElement(@Nonnull final Document document) - throws CreationException { - try { - final Element adaptationSetElement = (Element) document.getElementsByTagName( - ADAPTATION_SET).item(0); - final Element roleElement = document.createElement("Role"); - - final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); - schemeIdUriAttribute.setValue("urn:mpeg:DASH:role:2011"); - roleElement.setAttributeNode(schemeIdUriAttribute); - - final Attr valueAttribute = document.createAttribute("value"); - valueAttribute.setValue("main"); - roleElement.setAttributeNode(valueAttribute); - - adaptationSetElement.appendChild(roleElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd("Role", e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateAdaptationSetElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the the {@code } element will - * be appended - * @param itagItem the {@link ItagItem} to use, which cannot be null - */ - private static void generateRepresentationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element adaptationSetElement = (Element) document.getElementsByTagName( - ADAPTATION_SET).item(0); - final Element representationElement = document.createElement(REPRESENTATION); - - final int id = itagItem.id; - if (id <= 0) { - throw CreationException.couldNotAdd(REPRESENTATION, - "the id of the ItagItem is <= 0"); - } - final Attr idAttribute = document.createAttribute("id"); - idAttribute.setValue(String.valueOf(id)); - representationElement.setAttributeNode(idAttribute); - - final String codec = itagItem.getCodec(); - if (isNullOrEmpty(codec)) { - throw CreationException.couldNotAdd(ADAPTATION_SET, - "the codec value is null or empty"); - } - final Attr codecsAttribute = document.createAttribute("codecs"); - codecsAttribute.setValue(codec); - representationElement.setAttributeNode(codecsAttribute); - - final Attr startWithSAPAttribute = document.createAttribute("startWithSAP"); - startWithSAPAttribute.setValue("1"); - representationElement.setAttributeNode(startWithSAPAttribute); - - final Attr maxPlayoutRateAttribute = document.createAttribute("maxPlayoutRate"); - maxPlayoutRateAttribute.setValue("1"); - representationElement.setAttributeNode(maxPlayoutRateAttribute); - - final int bitrate = itagItem.getBitrate(); - if (bitrate <= 0) { - throw CreationException.couldNotAdd(REPRESENTATION, - "the bitrate of the ItagItem is <= 0"); - } - final Attr bandwidthAttribute = document.createAttribute("bandwidth"); - bandwidthAttribute.setValue(String.valueOf(bitrate)); - representationElement.setAttributeNode(bandwidthAttribute); - - final ItagItem.ItagType itagType = itagItem.itagType; - - if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { - final int height = itagItem.getHeight(); - final int width = itagItem.getWidth(); - if (height <= 0 && width <= 0) { - throw CreationException.couldNotAdd(REPRESENTATION, - "both width and height of the ItagItem are <= 0"); - } - - if (width > 0) { - final Attr widthAttribute = document.createAttribute("width"); - widthAttribute.setValue(String.valueOf(width)); - representationElement.setAttributeNode(widthAttribute); - } - - final Attr heightAttribute = document.createAttribute("height"); - heightAttribute.setValue(String.valueOf(itagItem.getHeight())); - representationElement.setAttributeNode(heightAttribute); - - final int fps = itagItem.getFps(); - if (fps > 0) { - final Attr frameRateAttribute = document.createAttribute("frameRate"); - frameRateAttribute.setValue(String.valueOf(fps)); - representationElement.setAttributeNode(frameRateAttribute); - } - } - - if (itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { - final Attr audioSamplingRateAttribute = document.createAttribute( - "audioSamplingRate"); - audioSamplingRateAttribute.setValue(String.valueOf(itagItem.getSampleRate())); - } - - adaptationSetElement.appendChild(representationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(REPRESENTATION, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

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

    - * - *

    - * It will produce the following element: - *
    - * {@code - * (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second - * parameter of this method) - *

    - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the {@code } - * element will be appended - * @param itagItem the {@link ItagItem} to use, which cannot be null - */ - private static void generateAudioChannelConfigurationElement( - @Nonnull final Document document, - @Nonnull final ItagItem itagItem) throws CreationException { - try { - final Element representationElement = (Element) document.getElementsByTagName( - REPRESENTATION).item(0); - final Element audioChannelConfigurationElement = document.createElement( - "AudioChannelConfiguration"); - - final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); - schemeIdUriAttribute.setValue( - "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); - audioChannelConfigurationElement.setAttributeNode(schemeIdUriAttribute); - - final Attr valueAttribute = document.createAttribute("value"); - final int audioChannels = itagItem.getAudioChannels(); - if (audioChannels <= 0) { - throw new CreationException("audioChannels is <= 0: " + audioChannels); - } - valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); - audioChannelConfigurationElement.setAttributeNode(valueAttribute); - - representationElement.appendChild(audioChannelConfigurationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd("AudioChannelConfiguration", e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

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

    - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the {@code } element will - * be appended - * @param baseUrl the base URL of the stream, which cannot be null and will be set as the - * content of the {@code } element - */ - private static void generateBaseUrlElement(@Nonnull final Document document, - @Nonnull final String baseUrl) - throws CreationException { - try { - final Element representationElement = (Element) document.getElementsByTagName( - REPRESENTATION).item(0); - final Element baseURLElement = document.createElement("BaseURL"); - baseURLElement.setTextContent(baseUrl); - representationElement.appendChild(baseURLElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd("BaseURL", e); - } - } - - // CHECKSTYLE:OFF - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

    - * 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 } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the {@code } element will - * be appended - * @param itagItem the {@link ItagItem} to use, which cannot be null - */ - // CHECKSTYLE:ON - private static void generateSegmentBaseElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element representationElement = (Element) document.getElementsByTagName( - REPRESENTATION).item(0); - - final Element segmentBaseElement = document.createElement(SEGMENT_BASE); - final Attr indexRangeAttribute = document.createAttribute("indexRange"); - - if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { - throw CreationException.couldNotAdd(SEGMENT_BASE, "ItagItem's indexStart or " - + "indexEnd are < 0: " + itagItem.getIndexStart() + "-" - + itagItem.getIndexEnd()); - } - - indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); - segmentBaseElement.setAttributeNode(indexRangeAttribute); - - representationElement.appendChild(segmentBaseElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(SEGMENT_BASE, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

    - * 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 } element needs to be generated before this element with - * {@link #generateSegmentBaseElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the {@code } element will - * be appended - * @param itagItem the {@link ItagItem} to use, which cannot be null - */ - private static void generateInitializationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) - throws CreationException { - try { - final Element segmentBaseElement = (Element) document.getElementsByTagName( - SEGMENT_BASE).item(0); - - final Element initializationElement = document.createElement(INITIALIZATION); - final Attr rangeAttribute = document.createAttribute("range"); - - if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { - throw CreationException.couldNotAdd(INITIALIZATION, "ItagItem's initStart or " - + "initEnd are < 0: " + itagItem.getInitStart() + "-" - + itagItem.getInitEnd()); - } - - rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd()); - initializationElement.setAttributeNode(rangeAttribute); - - segmentBaseElement.appendChild(initializationElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(INITIALIZATION, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

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

    - * - *

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

      - *
    • {@code startNumber}, which takes the value {@code 0} for post-live-DVR streams and - * {@code 1} for OTF streams;
    • - *
    • {@code timescale}, which is always {@code 1000};
    • - *
    • {@code media}, which is the base URL of the stream on which is appended - * {@code &sq=$Number$};
    • - *
    • {@code initialization} (only for OTF streams), which is the base URL of the stream - * on which is appended {@link #SQ_0}.
    • - *
    - *

    - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateRepresentationElement(Document, ItagItem)}). - *

    - * - * @param document the {@link Document} on which the {@code } element will - * be appended - * @param baseUrl the base URL of the OTF/post-live-DVR stream - * @param deliveryType the stream {@link DeliveryType delivery type} - */ - private static void generateSegmentTemplateElement(@Nonnull final Document document, - @Nonnull final String baseUrl, - final DeliveryType deliveryType) - throws CreationException { - try { - final Element representationElement = (Element) document.getElementsByTagName( - REPRESENTATION).item(0); - final Element segmentTemplateElement = document.createElement(SEGMENT_TEMPLATE); - - final Attr startNumberAttribute = document.createAttribute("startNumber"); - final boolean isDeliveryTypeLive = deliveryType == DeliveryType.LIVE; - // The first sequence of post DVR streams is the beginning of the video stream and not - // an initialization segment - final String startNumberValue = isDeliveryTypeLive ? "0" : "1"; - startNumberAttribute.setValue(startNumberValue); - segmentTemplateElement.setAttributeNode(startNumberAttribute); - - final Attr timescaleAttribute = document.createAttribute("timescale"); - timescaleAttribute.setValue("1000"); - segmentTemplateElement.setAttributeNode(timescaleAttribute); - - // Post-live-DVR/ended livestreams streams don't require an initialization sequence - if (!isDeliveryTypeLive) { - final Attr initializationAttribute = document.createAttribute("initialization"); - initializationAttribute.setValue(baseUrl + SQ_0); - segmentTemplateElement.setAttributeNode(initializationAttribute); - } - - final Attr mediaAttribute = document.createAttribute("media"); - mediaAttribute.setValue(baseUrl + "&sq=$Number$"); - segmentTemplateElement.setAttributeNode(mediaAttribute); - - representationElement.appendChild(segmentTemplateElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(SEGMENT_TEMPLATE, e); - } - } - - /** - * Generate the {@code } element, appended as a child of the - * {@code } element. - * - *

    - * The {@code } element needs to be generated before this element with - * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. - *

    - * - * @param document the {@link Document} on which the the {@code } element will - * be appended - */ - private static void generateSegmentTimelineElement(@Nonnull final Document document) - throws CreationException { - try { - final Element segmentTemplateElement = (Element) document.getElementsByTagName( - SEGMENT_TEMPLATE).item(0); - final Element segmentTimelineElement = document.createElement(SEGMENT_TIMELINE); - - segmentTemplateElement.appendChild(segmentTimelineElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd(SEGMENT_TIMELINE, e); - } - } - - /** - * Generate segment elements for OTF streams. - * - *

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

    - * - *

    - * The {@code } element needs to be generated before this element with - * {@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 } elements will be appended - */ - private static void generateSegmentElementsForOtfStreams(final String[] segmentDurations, - final Document document) - throws CreationException { - - try { - final Element segmentTimelineElement = (Element) document.getElementsByTagName( - SEGMENT_TIMELINE).item(0); - - for (final String segmentDuration : segmentDurations) { - final Element sElement = document.createElement("S"); - - final String[] segmentLengthRepeat = segmentDuration.split("\\(r="); - // make sure segmentLengthRepeat[0], which is the length, is convertible to int - Integer.parseInt(segmentLengthRepeat[0]); - - // There are repetitions of a segment duration in other segments - if (segmentLengthRepeat.length > 1) { - final int segmentRepeatCount = Integer.parseInt( - Utils.removeNonDigitCharacters(segmentLengthRepeat[1])); - final Attr rAttribute = document.createAttribute("r"); - rAttribute.setValue(String.valueOf(segmentRepeatCount)); - sElement.setAttributeNode(rAttribute); - } - - final Attr dAttribute = document.createAttribute("d"); - dAttribute.setValue(segmentLengthRepeat[0]); - sElement.setAttributeNode(dAttribute); - - segmentTimelineElement.appendChild(sElement); - } - - } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException - | NumberFormatException e) { - throw CreationException.couldNotAdd("segment (S)", e); - } - } - - /** - * Generate the segment element for post-live-DVR streams. - * - *

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

    - * - * @param document the {@link Document} on which the the {@code } element will - * be appended - * @param targetDurationSeconds the {@code targetDurationSec} value from player response's - * stream - * @param segmentCount the number of segments, extracted by the main method which - * generates manifests of post DVR livestreams - */ - private static void generateSegmentElementForPostLiveDvrStreams( - @Nonnull final Document document, - final int targetDurationSeconds, - @Nonnull final String segmentCount) throws CreationException { - try { - final Element segmentTimelineElement = (Element) document.getElementsByTagName( - SEGMENT_TIMELINE).item(0); - final Element sElement = document.createElement("S"); - - final Attr dAttribute = document.createAttribute("d"); - dAttribute.setValue(String.valueOf(targetDurationSeconds * 1000)); - sElement.setAttributeNode(dAttribute); - - final Attr rAttribute = document.createAttribute("r"); - rAttribute.setValue(segmentCount); - sElement.setAttributeNode(rAttribute); - - segmentTimelineElement.appendChild(sElement); - } catch (final DOMException e) { - throw CreationException.couldNotAdd("segment (S)", e); - } - } - - /** - * 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 - * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the - * string generated (use either {@link #OTF_CACHE}, - * {@link #POST_LIVE_DVR_CACHE} or {@link #PROGRESSIVE_CACHE}) - * @return the DASH manifest {@link Document document} converted to a string - */ - private static String buildAndCacheResult( - @Nonnull final String originalBaseStreamingUrl, - @Nonnull final Document document, - @Nonnull final ManifestCreatorCache manifestCreatorCache) - throws CreationException { - - try { - final String documentXml = documentToXml(document); - manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); - return documentXml; - } catch (final Exception e) { - throw new CreationException( - "Could not convert the DASH manifest generated to a string", e); - } - } - - /** - * Securing against XEE is done by passing {@code false} to {@link - * DocumentBuilderFactory#setExpandEntityReferences(boolean)}, also see - * ChuckerTeam/chucker#201. - * - * @return an instance of document secured against XEE attacks, that should then be convertible - * to an XML string without security problems - * @see #documentToXml(Document) Use documentToXml to convert the created document to XML, which - * is also secured against XEE! - */ - private static Document newDocument() throws ParserConfigurationException { - final DocumentBuilderFactory documentBuilderFactory - = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setExpandEntityReferences(false); - - final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - return documentBuilder.newDocument(); - } - - /** - * Securing against XEE is done by setting {@link XMLConstants#FEATURE_SECURE_PROCESSING} to - * {@code true} in the {@link TransformerFactory}, also see - * ChuckerTeam/chucker#201. - * The best way to do this would be setting the attributes {@link - * XMLConstants#ACCESS_EXTERNAL_DTD} and {@link XMLConstants#ACCESS_EXTERNAL_STYLESHEET}, but - * unfortunately the engine on Android does not support them. - * - * @param document the document to convert; must have been created using {@link #newDocument()} - * to properly prevent XEE attacks! - * @return the document converted to an XML string, making sure there can't be XEE attacks - */ - private static String documentToXml(@Nonnull final Document document) - throws TransformerException { - - @SuppressWarnings("java:S2755") // see javadoc: this is actually taken care of - final TransformerFactory transformerFactory = TransformerFactory.newInstance(); - transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - - final Transformer transformer = transformerFactory.newTransformer(); - transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); - transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); - transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); - - final StringWriter result = new StringWriter(); - transformer.transform(new DOMSource(document), new StreamResult(result)); - - return result.toString(); - } - - /** - * @return the cache of DASH manifests generated for OTF streams - */ - public static ManifestCreatorCache getOtfManifestsCache() { - return OTF_CACHE; - } - - /** - * @return the cache of DASH manifests generated for post-live-DVR streams - */ - public static ManifestCreatorCache getPostLiveDvrManifestsCache() { - return POST_LIVE_DVR_CACHE; - } - - /** - * @return the cache of DASH manifests generated for progressive streams - */ - public static ManifestCreatorCache getProgressiveManifestsCache() { - return PROGRESSIVE_CACHE; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java new file mode 100644 index 000000000..46f32664b --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java @@ -0,0 +1,63 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; + +import javax.annotation.Nonnull; + +/** + * Exception that is thrown when a YouTube DASH manifest creator encounters a problem + * while creating a manifest. + */ +public final class CreationException extends RuntimeException { + + /** + * Create a new {@link CreationException} with a detail message. + * + * @param message the detail message to add in the exception + */ + public CreationException(final String message) { + super(message); + } + + /** + * Create a new {@link CreationException} with a detail message and a cause. + * @param message the detail message to add in the exception + * @param cause the exception cause of this {@link CreationException} + */ + public CreationException(final String message, final Exception cause) { + super(message, cause); + } + + // Methods to create exceptions easily without having to use big exception messages and to + // reduce duplication + + /** + * Create a new {@link CreationException} with a cause and the following detail message format: + *
    + * {@code "Could not add " + element + " element", cause}, where {@code element} is an element + * of a DASH manifest. + * + * @param element the element which was not added to the DASH document + * @param cause the exception which prevented addition of the element to the DASH document + * @return a new {@link CreationException} + */ + @Nonnull + public static CreationException couldNotAddElement(final String element, + final Exception cause) { + return new CreationException("Could not add " + element + " element", cause); + } + + /** + * Create a new {@link CreationException} with a cause and the following detail message format: + *
    + * {@code "Could not add " + element + " element: " + reason}, where {@code element} is an + * element of a DASH manifest and {@code reason} the reason why this element cannot be added to + * the DASH document. + * + * @param element the element which was not added to the DASH document + * @param reason the reason message of why the element has been not added to the DASH document + * @return a new {@link CreationException} + */ + @Nonnull + public static CreationException couldNotAddElement(final String element, final String reason) { + return new CreationException("Could not add " + element + " element: " + reason); + } +} 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 new file mode 100644 index 000000000..48b0bf41a --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java @@ -0,0 +1,856 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; + +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.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 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; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +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.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.isNullOrEmpty; + +/** + * Utilities and constants for YouTube DASH manifest creators. + * + *

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

      + *
    • {@code MPD} (using {@link #generateDocumentAndMpdElement(long)});
    • + *
    • {@code Period} (using {@link #generatePeriodElement(Document)});
    • + *
    • {@code AdaptationSet} (using {@link #generateAdaptationSetElement(Document, + * ItagItem)});
    • + *
    • {@code Role} (using {@link #generateRoleElement(Document)});
    • + *
    • {@code Representation} (using {@link #generateRepresentationElement(Document, + * ItagItem)});
    • + *
    • and, for audio streams, {@code AudioChannelConfiguration} (using + * {@link #generateAudioChannelConfigurationElement(Document, ItagItem)}).
    • + *
    + *

    + * + * @param itagItem the {@link ItagItem} associated to the stream, which must not be null + * @param streamDuration the duration of the stream, in milliseconds + * @return a {@link Document} with the common elements added in it + */ + @Nonnull + public static Document generateDocumentAndDoCommonElementsGeneration( + @Nonnull final ItagItem itagItem, + final long streamDuration) throws CreationException { + final Document document = generateDocumentAndMpdElement(streamDuration); + + generatePeriodElement(document); + generateAdaptationSetElement(document, itagItem); + generateRoleElement(document); + generateRepresentationElement(document, itagItem); + if (itagItem.itagType == ItagItem.ItagType.AUDIO) { + generateAudioChannelConfigurationElement(document, itagItem); + } + + return document; + } + + /** + * Create a {@link Document} instance and generate the {@code } element of the manifest. + * + *

    + * The generated {@code } element looks like the manifest returned into the player + * response of videos: + *

    + * + *

    + * {@code } + * (where {@code $duration$} represents the duration in seconds (a number with 3 digits after + * the decimal point)). + *

    + * + * @param duration the duration of the stream, in milliseconds + * @return a {@link Document} instance which contains a {@code } element + */ + @Nonnull + public static Document generateDocumentAndMpdElement(final long duration) + throws CreationException { + try { + final Document document = newDocument(); + + final Element mpdElement = document.createElement(MPD); + document.appendChild(mpdElement); + + final Attr xmlnsXsiAttribute = document.createAttribute("xmlns:xsi"); + xmlnsXsiAttribute.setValue("http://www.w3.org/2001/XMLSchema-instance"); + mpdElement.setAttributeNode(xmlnsXsiAttribute); + + final Attr xmlns = document.createAttribute("xmlns"); + xmlns.setValue("urn:mpeg:DASH:schema:MPD:2011"); + mpdElement.setAttributeNode(xmlns); + + final Attr xsiSchemaLocationAttribute = document.createAttribute("xsi:schemaLocation"); + xsiSchemaLocationAttribute.setValue("urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); + mpdElement.setAttributeNode(xsiSchemaLocationAttribute); + + final Attr minBufferTimeAttribute = document.createAttribute("minBufferTime"); + minBufferTimeAttribute.setValue("PT1.500S"); + mpdElement.setAttributeNode(minBufferTimeAttribute); + + final Attr profilesAttribute = document.createAttribute("profiles"); + profilesAttribute.setValue("urn:mpeg:dash:profile:full:2011"); + mpdElement.setAttributeNode(profilesAttribute); + + final Attr typeAttribute = document.createAttribute("type"); + typeAttribute.setValue("static"); + mpdElement.setAttributeNode(typeAttribute); + + final Attr mediaPresentationDurationAttribute = document.createAttribute( + "mediaPresentationDuration"); + final String durationSeconds = String.format(Locale.ENGLISH, "%.3f", + duration / 1000.0); + mediaPresentationDurationAttribute.setValue("PT" + durationSeconds + "S"); + mpdElement.setAttributeNode(mediaPresentationDurationAttribute); + + return document; + } catch (final Exception e) { + throw new CreationException( + "Could not generate the DASH manifest or append the MPD document to it", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } element. + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateDocumentAndMpdElement(long)}. + *

    + * + * @param document the {@link Document} on which the the {@code } element will be + * appended + */ + public static void generatePeriodElement(@Nonnull final Document document) + throws CreationException { + try { + final Element mpdElement = (Element) document.getElementsByTagName(MPD).item(0); + final Element periodElement = document.createElement(PERIOD); + mpdElement.appendChild(periodElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(PERIOD, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } + * element. + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generatePeriodElement(Document)}. + *

    + * + * @param document the {@link Document} on which the {@code } element will be + * appended + * @param itagItem the {@link ItagItem} corresponding to the stream, which must not be null + */ + public static void generateAdaptationSetElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws CreationException { + try { + final Element periodElement = (Element) document.getElementsByTagName(PERIOD) + .item(0); + final Element adaptationSetElement = document.createElement(ADAPTATION_SET); + + final Attr idAttribute = document.createAttribute("id"); + idAttribute.setValue("0"); + adaptationSetElement.setAttributeNode(idAttribute); + + final MediaFormat mediaFormat = itagItem.getMediaFormat(); + if (mediaFormat == null || isNullOrEmpty(mediaFormat.getMimeType())) { + throw CreationException.couldNotAddElement(ADAPTATION_SET, + "the MediaFormat or its mime type is null or empty"); + } + + final Attr mimeTypeAttribute = document.createAttribute("mimeType"); + mimeTypeAttribute.setValue(mediaFormat.getMimeType()); + adaptationSetElement.setAttributeNode(mimeTypeAttribute); + + final Attr subsegmentAlignmentAttribute = document.createAttribute( + "subsegmentAlignment"); + subsegmentAlignmentAttribute.setValue("true"); + adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute); + + periodElement.appendChild(adaptationSetElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(ADAPTATION_SET, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the {@code } + * element. + * + *

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

    + * + *

    + * {@code } + *

    + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the the {@code } element will be + * appended + */ + public static void generateRoleElement(@Nonnull final Document document) + throws CreationException { + try { + final Element adaptationSetElement = (Element) document.getElementsByTagName( + ADAPTATION_SET).item(0); + final Element roleElement = document.createElement(ROLE); + + final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); + schemeIdUriAttribute.setValue("urn:mpeg:DASH:role:2011"); + roleElement.setAttributeNode(schemeIdUriAttribute); + + final Attr valueAttribute = document.createAttribute("value"); + valueAttribute.setValue("main"); + roleElement.setAttributeNode(valueAttribute); + + adaptationSetElement.appendChild(roleElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(ROLE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateAdaptationSetElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the the {@code } element will + * be appended + * @param itagItem the {@link ItagItem} to use, which must not be null + */ + public static void generateRepresentationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws CreationException { + try { + final Element adaptationSetElement = (Element) document.getElementsByTagName( + ADAPTATION_SET).item(0); + final Element representationElement = document.createElement(REPRESENTATION); + + final int id = itagItem.id; + if (id <= 0) { + throw CreationException.couldNotAddElement(REPRESENTATION, + "the id of the ItagItem is <= 0"); + } + final Attr idAttribute = document.createAttribute("id"); + idAttribute.setValue(String.valueOf(id)); + representationElement.setAttributeNode(idAttribute); + + final String codec = itagItem.getCodec(); + if (isNullOrEmpty(codec)) { + throw CreationException.couldNotAddElement(ADAPTATION_SET, + "the codec value of the ItagItem is null or empty"); + } + final Attr codecsAttribute = document.createAttribute("codecs"); + codecsAttribute.setValue(codec); + representationElement.setAttributeNode(codecsAttribute); + + final Attr startWithSAPAttribute = document.createAttribute("startWithSAP"); + startWithSAPAttribute.setValue("1"); + representationElement.setAttributeNode(startWithSAPAttribute); + + final Attr maxPlayoutRateAttribute = document.createAttribute("maxPlayoutRate"); + maxPlayoutRateAttribute.setValue("1"); + representationElement.setAttributeNode(maxPlayoutRateAttribute); + + final int bitrate = itagItem.getBitrate(); + if (bitrate <= 0) { + throw CreationException.couldNotAddElement(REPRESENTATION, + "the bitrate of the ItagItem is <= 0"); + } + final Attr bandwidthAttribute = document.createAttribute("bandwidth"); + bandwidthAttribute.setValue(String.valueOf(bitrate)); + representationElement.setAttributeNode(bandwidthAttribute); + + final ItagItem.ItagType itagType = itagItem.itagType; + + if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { + final int height = itagItem.getHeight(); + final int width = itagItem.getWidth(); + if (height <= 0 && width <= 0) { + throw CreationException.couldNotAddElement(REPRESENTATION, + "both width and height of the ItagItem are <= 0"); + } + + if (width > 0) { + final Attr widthAttribute = document.createAttribute("width"); + widthAttribute.setValue(String.valueOf(width)); + representationElement.setAttributeNode(widthAttribute); + } + + final Attr heightAttribute = document.createAttribute("height"); + heightAttribute.setValue(String.valueOf(itagItem.getHeight())); + representationElement.setAttributeNode(heightAttribute); + + final int fps = itagItem.getFps(); + if (fps > 0) { + final Attr frameRateAttribute = document.createAttribute("frameRate"); + frameRateAttribute.setValue(String.valueOf(fps)); + representationElement.setAttributeNode(frameRateAttribute); + } + } + + if (itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { + final Attr audioSamplingRateAttribute = document.createAttribute( + "audioSamplingRate"); + audioSamplingRateAttribute.setValue(String.valueOf(itagItem.getSampleRate())); + } + + adaptationSetElement.appendChild(representationElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(REPRESENTATION, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

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

    + * + *

    + * It will produce the following element: + *
    + * {@code + * (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second + * parameter of this method) + *

    + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the {@code } + * element will be appended + * @param itagItem the {@link ItagItem} to use, which must not be null + */ + public static void generateAudioChannelConfigurationElement( + @Nonnull final Document document, + @Nonnull final ItagItem itagItem) throws CreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + REPRESENTATION).item(0); + final Element audioChannelConfigurationElement = document.createElement( + AUDIO_CHANNEL_CONFIGURATION); + + final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); + schemeIdUriAttribute.setValue( + "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); + audioChannelConfigurationElement.setAttributeNode(schemeIdUriAttribute); + + final Attr valueAttribute = document.createAttribute("value"); + final int audioChannels = itagItem.getAudioChannels(); + if (audioChannels <= 0) { + throw new CreationException("the number of audioChannels in the ItagItem is <= 0: " + + audioChannels); + } + valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); + audioChannelConfigurationElement.setAttributeNode(valueAttribute); + + representationElement.appendChild(audioChannelConfigurationElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(AUDIO_CHANNEL_CONFIGURATION, e); + } + } + + /** + * 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 + * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the string + * generated + * @return the DASH manifest {@link Document document} converted to a string + */ + public static String buildAndCacheResult( + @Nonnull final String originalBaseStreamingUrl, + @Nonnull final Document document, + @Nonnull final ManifestCreatorCache manifestCreatorCache) + throws CreationException { + + try { + final String documentXml = documentToXml(document); + manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); + return documentXml; + } catch (final Exception e) { + throw new CreationException( + "Could not convert the DASH manifest generated to a string", e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

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

    + * + *

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

      + *
    • {@code startNumber}, which takes the value {@code 0} for post-live-DVR streams and + * {@code 1} for OTF streams;
    • + *
    • {@code timescale}, which is always {@code 1000};
    • + *
    • {@code media}, which is the base URL of the stream on which is appended + * {@code &sq=$Number$};
    • + *
    • {@code initialization} (only for OTF streams), which is the base URL of the stream + * on which is appended {@link #SQ_0}.
    • + *
    + *

    + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateRepresentationElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param baseUrl the base URL of the OTF/post-live-DVR stream + * @param deliveryType the stream {@link DeliveryType delivery type}, which must be either + * {@link DeliveryType#OTF OTF} or {@link DeliveryType#LIVE LIVE} + */ + public static void generateSegmentTemplateElement(@Nonnull final Document document, + @Nonnull final String baseUrl, + final DeliveryType deliveryType) + throws CreationException { + if (deliveryType != DeliveryType.OTF && deliveryType != DeliveryType.LIVE) { + throw CreationException.couldNotAddElement(SEGMENT_TEMPLATE, "invalid delivery type: " + + deliveryType); + } + + try { + final Element representationElement = (Element) document.getElementsByTagName( + REPRESENTATION).item(0); + final Element segmentTemplateElement = document.createElement(SEGMENT_TEMPLATE); + + final Attr startNumberAttribute = document.createAttribute("startNumber"); + final boolean isDeliveryTypeLive = deliveryType == DeliveryType.LIVE; + // The first sequence of post DVR streams is the beginning of the video stream and not + // an initialization segment + final String startNumberValue = isDeliveryTypeLive ? "0" : "1"; + startNumberAttribute.setValue(startNumberValue); + segmentTemplateElement.setAttributeNode(startNumberAttribute); + + final Attr timescaleAttribute = document.createAttribute("timescale"); + timescaleAttribute.setValue("1000"); + segmentTemplateElement.setAttributeNode(timescaleAttribute); + + // Post-live-DVR/ended livestreams streams don't require an initialization sequence + if (!isDeliveryTypeLive) { + final Attr initializationAttribute = document.createAttribute("initialization"); + initializationAttribute.setValue(baseUrl + SQ_0); + segmentTemplateElement.setAttributeNode(initializationAttribute); + } + + final Attr mediaAttribute = document.createAttribute("media"); + mediaAttribute.setValue(baseUrl + "&sq=$Number$"); + segmentTemplateElement.setAttributeNode(mediaAttribute); + + representationElement.appendChild(segmentTemplateElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(SEGMENT_TEMPLATE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

    + * The {@code } element needs to be generated before this element with + * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. + *

    + * + * @param document the {@link Document} on which the the {@code } element will + * be appended + */ + public static void generateSegmentTimelineElement(@Nonnull final Document document) + throws CreationException { + try { + final Element segmentTemplateElement = (Element) document.getElementsByTagName( + SEGMENT_TEMPLATE).item(0); + final Element segmentTimelineElement = document.createElement(SEGMENT_TIMELINE); + + segmentTemplateElement.appendChild(segmentTimelineElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(SEGMENT_TIMELINE, e); + } + } + + /** + * Get the "initialization" {@link Response response} of a stream. + * + *

    This method fetches, for OTF streams and for post-live-DVR streams: + *

      + *
    • the base URL of the stream, to which are appended {@link #SQ_0} and + * {@link #RN_0} parameters, with a {@code GET} request for streaming URLs from HTML5 + * clients and a {@code POST} request for the ones from the {@code ANDROID} and the + * {@code IOS} clients;
    • + *
    • for streaming URLs from HTML5 clients, the {@link #ALR_YES} param is also added. + *
    • + *
    + *

    + * + * @param baseStreamingUrl the base URL of the stream, which must not be null + * @param itagItem the {@link ItagItem} of stream, which must not be null + * @param deliveryType the {@link DeliveryType} of the stream + * @return the "initialization" response, without redirections on the network on which the + * request(s) is/are made + */ + @SuppressWarnings("checkstyle:FinalParameters") + @Nonnull + public static Response getInitializationResponse(@Nonnull String baseStreamingUrl, + @Nonnull final ItagItem itagItem, + final DeliveryType deliveryType) + throws CreationException { + final boolean isHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl) + || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl); + final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl); + final boolean isIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl); + if (isHtml5StreamingUrl) { + baseStreamingUrl += ALR_YES; + } + baseStreamingUrl = appendRnParamAndSqParamIfNeeded(baseStreamingUrl, deliveryType); + + final Downloader downloader = NewPipe.getDownloader(); + if (isHtml5StreamingUrl) { + final String mimeTypeExpected = itagItem.getMediaFormat().getMimeType(); + if (!isNullOrEmpty(mimeTypeExpected)) { + return getStreamingWebUrlWithoutRedirects(downloader, baseStreamingUrl, + mimeTypeExpected); + } + } else if (isAndroidStreamingUrl || isIosStreamingUrl) { + try { + final Map> headers = new HashMap<>(); + headers.put("User-Agent", Collections.singletonList( + isAndroidStreamingUrl ? getAndroidUserAgent(null) + : getIosUserAgent(null))); + final byte[] emptyBody = "".getBytes(StandardCharsets.UTF_8); + return downloader.post(baseStreamingUrl, headers, emptyBody); + } catch (final IOException | ExtractionException e) { + throw new CreationException("Could not get the " + + (isIosStreamingUrl ? "ANDROID" : "IOS") + " streaming URL response", e); + } + } + + try { + return downloader.get(baseStreamingUrl); + } catch (final IOException | ExtractionException e) { + throw new CreationException("Could not get the streaming URL response", e); + } + } + + /** + * Generate a new {@link DocumentBuilder} secured from XEE attacks, on platforms which + * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and + * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link DocumentBuilderFactory} instances. + * + * @return an instance of {@link Document} secured against XEE attacks on supported platforms, + * that should then be convertible to an XML string without security problems + */ + private static Document newDocument() throws ParserConfigurationException { + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + try { + 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) + } + + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + return documentBuilder.newDocument(); + } + + /** + * Generate a new {@link TransformerFactory} secured from XEE attacks, on platforms which + * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and + * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link TransformerFactory} instances. + * + * @param document the document to convert, which must have been created using + * {@link #newDocument()} to properly prevent XEE attacks + * @return the document converted to an XML string, making sure there can't be XEE attacks + */ + // Sonar warning is suppressed because it is still shown even if we apply its solution + @SuppressWarnings("squid:S2755") + private static String documentToXml(@Nonnull final Document document) + throws TransformerException { + + final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + try { + 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) + } + + final Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.VERSION, "1.0"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + + final StringWriter result = new StringWriter(); + transformer.transform(new DOMSource(document), new StreamResult(result)); + + return result.toString(); + } + + /** + * 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 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, + @Nonnull final DeliveryType deliveryType) { + if (deliveryType != DeliveryType.PROGRESSIVE) { + baseStreamingUrl += SQ_0; + } + + return baseStreamingUrl + RN_0; + } + + /** + * Get a URL on which no redirection between playback hosts should be present on the network + * and/or IP used to fetch the streaming URL, for HTML5 clients. + * + *

    This method will follow redirects which works in the following way: + *

      + *
    1. the {@link #ALR_YES} param is appended to all streaming URLs
    2. + *
    3. if no redirection occurs, the video server will return the streaming data;
    4. + *
    5. if a redirection occurs, the server will respond with HTTP status code 200 and a + * {@code text/plain} mime type. The redirection URL is the response body;
    6. + *
    7. the redirection URL is requested and the steps above from step 2 are repeated, + * until too many redirects are reached of course (the maximum number of redirects is + * {@link #MAXIMUM_REDIRECT_COUNT the same as OkHttp}).
    8. + *
    + *

    + * + *

    + * 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> headers = new HashMap<>(); + addClientInfoHeaders(headers); + + String responseMimeType = ""; + + int redirectsCount = 0; + while (!responseMimeType.equals(responseMimeTypeExpected) + && redirectsCount < MAXIMUM_REDIRECT_COUNT) { + final Response response = downloader.get(streamingUrl, headers); + + final int responseCode = response.responseCode(); + if (responseCode != 200) { + throw new CreationException( + "Could not get the initialization URL: HTTP response code " + + responseCode); + } + + // A valid HTTP 1.0+ response should include a Content-Type header, so we can + // require that the response from video servers has this header. + responseMimeType = Objects.requireNonNull(response.getHeader("Content-Type"), + "Could not get the Content-Type header from the response headers"); + + // The response body is the redirection URL + if (responseMimeType.equals("text/plain")) { + streamingUrl = response.responseBody(); + redirectsCount++; + } else { + return response; + } + } + + if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) { + throw new CreationException( + "Too many redirects when trying to get the the streaming URL response of a " + + "HTML5 client"); + } + + // This should never be reached, but is required because we don't want to return null + // here + throw new CreationException( + "Could not get the streaming URL response of a HTML5 client: unreachable code " + + "reached!"); + } catch (final IOException | ExtractionException e) { + throw new CreationException( + "Could not get the streaming URL response of a HTML5 client", e); + } + } +} 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 new file mode 100644 index 000000000..375fd7421 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java @@ -0,0 +1,268 @@ +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.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 javax.annotation.Nonnull; +import java.util.Arrays; +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.isBlank; + +/** + * Class which generates DASH manifests of YouTube {@link DeliveryType#OTF OTF streams}. + */ +public final class YoutubeOtfDashManifestCreator { + + /** + * Cache of DASH manifests generated for OTF streams. + */ + private static final ManifestCreatorCache OTF_STREAMS_CACHE + = new ManifestCreatorCache<>(); + + private YoutubeOtfDashManifestCreator() { + } + + /** + * 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: + *

      + *
    • the base URL of the stream (which, if you try to access to it, returns HTTP + * status code 404 after redirects, and if the URL is valid);
    • + *
    • an {@link ItagItem}, which needs to contain the following information: + *
        + *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
      • + *
      • its bitrate;
      • + *
      • its mime type;
      • + *
      • its codec(s);
      • + *
      • for an audio stream: its audio channels;
      • + *
      • for a video stream: its width and height.
      • + *
      + *
    • + *
    • the duration of the video, which will be used if the duration could not be + * parsed from the first sequence of the stream.
    • + *
    + *

    + * + *

    In order to generate the DASH manifest, this method will: + *

      + *
    • request the first sequence of the stream (the base URL on which the first + * sequence parameter is appended (see {@link YoutubeDashManifestCreatorsUtils#SQ_0})) + * with a {@code POST} or {@code GET} request (depending of the client on which the + * streaming URL comes from is a mobile one ({@code POST}) or not ({@code GET}));
    • + *
    • follow its redirection(s), if any;
    • + *
    • save the last URL, remove the first sequence parameter;
    • + *
    • use the information provided in the {@link ItagItem} to generate all + * elements of the DASH manifest.
    • + *
    + *

    + * + *

    + * 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 getCache() { + return OTF_STREAMS_CACHE; + } + + /** + * Generate segment elements for OTF streams. + * + *

    + * 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 } element, which + * needs to be generated before these elements with + * {@link YoutubeDashManifestCreatorsUtils#generateSegmentTimelineElement(Document)}. + *

    + * + * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the + * regular expressions + * @param document the {@link Document} on which the {@code } elements will be appended + */ + private static void generateSegmentElementsForOtfStreams( + @Nonnull final String[] segmentDurations, + @Nonnull final Document document) throws CreationException { + try { + final Element segmentTimelineElement = (Element) document.getElementsByTagName( + SEGMENT_TIMELINE).item(0); + + for (final String segmentDuration : segmentDurations) { + final Element sElement = document.createElement("S"); + + final String[] segmentLengthRepeat = segmentDuration.split("\\(r="); + // make sure segmentLengthRepeat[0], which is the length, is convertible to int + Integer.parseInt(segmentLengthRepeat[0]); + + // There are repetitions of a segment duration in other segments + if (segmentLengthRepeat.length > 1) { + final int segmentRepeatCount = Integer.parseInt( + Utils.removeNonDigitCharacters(segmentLengthRepeat[1])); + final Attr rAttribute = document.createAttribute("r"); + rAttribute.setValue(String.valueOf(segmentRepeatCount)); + sElement.setAttributeNode(rAttribute); + } + + final Attr dAttribute = document.createAttribute("d"); + dAttribute.setValue(segmentLengthRepeat[0]); + sElement.setAttributeNode(dAttribute); + + segmentTimelineElement.appendChild(sElement); + } + + } catch (final DOMException | IllegalStateException | IndexOutOfBoundsException + | NumberFormatException e) { + throw CreationException.couldNotAddElement("segment (S)", e); + } + } + + /** + * Get the duration of an OTF stream. + * + *

    + * 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_CACHE + = new ManifestCreatorCache<>(); + + private YoutubePostLiveStreamDvrDashManifestCreator() { + } + + /** + * 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 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: + *

      + *
    • the base URL of the stream (which, if you try to access to it, returns HTTP + * status code 404 after redirects, and if the URL is valid);
    • + *
    • an {@link ItagItem}, which needs to contain the following information: + *
        + *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
      • + *
      • its bitrate;
      • + *
      • its mime type;
      • + *
      • its codec(s);
      • + *
      • for an audio stream: its audio channels;
      • + *
      • for a video stream: its width and height.
      • + *
      + *
    • + *
    • the duration of the video, which will be used if the duration could not be + * parsed from the first sequence of the stream.
    • + *
    + *

    + * + *

    In order to generate the DASH manifest, this method will: + *

      + *
    • request the first sequence of the stream (the base URL on which the first + * sequence parameter is appended (see {@link YoutubeDashManifestCreatorsUtils#SQ_0})) + * with a {@code POST} or {@code GET} request (depending of the client on which the + * streaming URL comes from is a mobile one ({@code POST}) or not ({@code GET}));
    • + *
    • follow its redirection(s), if any;
    • + *
    • save the last URL, remove the first sequence parameters;
    • + *
    • use the information provided in the {@link ItagItem} to generate all elements + * of the DASH manifest.
    • + *
    + *

    + * + *

    + * 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> responseHeaders = response.responseHeaders(); + streamDurationString = responseHeaders.get("X-Head-Time-Millis").get(0); + segmentCount = responseHeaders.get("X-Head-Seqnum").get(0); + } catch (final IndexOutOfBoundsException e) { + throw new CreationException( + "Could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header", + e); + } + + if (isNullOrEmpty(segmentCount)) { + throw new CreationException("Could not get the number of segments"); + } + + long streamDuration; + try { + streamDuration = Long.parseLong(streamDurationString); + } catch (final NumberFormatException e) { + streamDuration = durationSecondsFallback; + } + + final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + streamDuration); + + generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl, + DeliveryType.LIVE); + generateSegmentTimelineElement(document); + generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); + + return buildAndCacheResult(postLiveStreamDvrStreamingUrl, document, + POST_LIVE_DVR_STREAMS_CACHE); + } + + /** + * @return the cache of DASH manifests generated for post-live-DVR streams + */ + public static ManifestCreatorCache getCache() { + return POST_LIVE_DVR_STREAMS_CACHE; + } + + /** + * Generate the segment ({@code }) element. + * + *

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

    + * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param targetDurationSeconds the {@code targetDurationSec} value from YouTube player + * response's stream + * @param segmentCount the number of segments, extracted by {@link + * #fromPostLiveStreamDvrStreamingUrl(String, ItagItem, int, long)} + */ + private static void generateSegmentElementForPostLiveDvrStreams( + @Nonnull final Document document, + final int targetDurationSeconds, + @Nonnull final String segmentCount) throws CreationException { + try { + final Element segmentTimelineElement = (Element) document.getElementsByTagName( + SEGMENT_TIMELINE).item(0); + final Element sElement = document.createElement("S"); + + final Attr dAttribute = document.createAttribute("d"); + dAttribute.setValue(String.valueOf(targetDurationSeconds * 1000)); + sElement.setAttributeNode(dAttribute); + + final Attr rAttribute = document.createAttribute("r"); + rAttribute.setValue(segmentCount); + sElement.setAttributeNode(rAttribute); + + segmentTimelineElement.appendChild(sElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement("segment (S)", e); + } + } +} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java new file mode 100644 index 000000000..0a8ed4533 --- /dev/null +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java @@ -0,0 +1,244 @@ +package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; + +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.Objects; + +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.BASE_URL; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.INITIALIZATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.MPD; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.REPRESENTATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration; + +/** + * Class which generates DASH manifests of {@link DeliveryType#PROGRESSIVE YouTube progressive} + * streams. + */ +public final class YoutubeProgressiveDashManifestCreator { + + /** + * Cache of DASH manifests generated for progressive streams. + */ + private static final ManifestCreatorCache PROGRESSIVE_STREAMS_CACHE + = new ManifestCreatorCache<>(); + + private YoutubeProgressiveDashManifestCreator() { + } + + /** + * Create DASH manifests from a YouTube progressive stream. + * + *

    + * 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 base URL of the stream (which, if you try to access to it, returns the whole + * stream, after redirects, and if the URL is valid);
    • + *
    • an {@link ItagItem}, which needs to contain the following information: + *
        + *
      • its type (see {@link ItagItem.ItagType}), to identify if the content is + * an audio or a video stream;
      • + *
      • its bitrate;
      • + *
      • its mime type;
      • + *
      • its codec(s);
      • + *
      • for an audio stream: its audio channels;
      • + *
      • for a video stream: its width and height.
      • + *
      + *
    • + *
    • the duration of the video (parameter {@code durationSecondsFallback}), which + * will be used as the stream duration if the duration could not be parsed from the + * {@link ItagItem}.
    • + *
    + *

    + * + * @param progressiveStreamingBaseUrl the base URL of the progressive 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 progressive stream which will be used + * if the duration could not be extracted from the + * {@link ItagItem} + * @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_STREAMS_CACHE.containsKey(progressiveStreamingBaseUrl)) { + return Objects.requireNonNull( + PROGRESSIVE_STREAMS_CACHE.get(progressiveStreamingBaseUrl)).getSecond(); + } + + final long itagItemDuration = itagItem.getApproxDurationMs(); + final long streamDuration; + if (itagItemDuration != -1) { + streamDuration = itagItemDuration; + } else { + if (durationSecondsFallback > 0) { + streamDuration = durationSecondsFallback * 1000; + } else { + throw CreationException.couldNotAddElement(MPD, "the duration of the stream " + + "could not be determined and durationSecondsFallback is <= 0"); + } + } + + final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + streamDuration); + + generateBaseUrlElement(document, progressiveStreamingBaseUrl); + generateSegmentBaseElement(document, itagItem); + generateInitializationElement(document, itagItem); + + return buildAndCacheResult(progressiveStreamingBaseUrl, document, + PROGRESSIVE_STREAMS_CACHE); + } + + /** + * @return the cache of DASH manifests generated for progressive streams + */ + public static ManifestCreatorCache getCache() { + return PROGRESSIVE_STREAMS_CACHE; + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

    + * The {@code } element needs to be generated before this element with + * {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param baseUrl the base URL of the stream, which must not be null and will be set as the + * content of the {@code } element + */ + private static void generateBaseUrlElement(@Nonnull final Document document, + @Nonnull final String baseUrl) + throws CreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + REPRESENTATION).item(0); + final Element baseURLElement = document.createElement(BASE_URL); + baseURLElement.setTextContent(baseUrl); + representationElement.appendChild(baseURLElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(BASE_URL, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

    + * 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 } element needs to be generated before this element with + * {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}), + * and the {@code BaseURL} element with {@link #generateBaseUrlElement(Document, String)} + * should be generated too. + *

    + * + * @param document the {@link Document} on which the {@code } element will be + * appended + * @param itagItem the {@link ItagItem} to use, which must not be null + */ + private static void generateSegmentBaseElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws CreationException { + try { + final Element representationElement = (Element) document.getElementsByTagName( + REPRESENTATION).item(0); + + final Element segmentBaseElement = document.createElement(SEGMENT_BASE); + final Attr indexRangeAttribute = document.createAttribute("indexRange"); + + if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { + throw CreationException.couldNotAddElement(SEGMENT_BASE, + "ItagItem's indexStart or " + "indexEnd are < 0: " + + itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); + } + + indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); + segmentBaseElement.setAttributeNode(indexRangeAttribute); + + representationElement.appendChild(segmentBaseElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(SEGMENT_BASE, e); + } + } + + /** + * Generate the {@code } element, appended as a child of the + * {@code } element. + * + *

    + * 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 } element needs to be generated before this element with + * {@link #generateSegmentBaseElement(Document, ItagItem)}). + *

    + * + * @param document the {@link Document} on which the {@code } element will + * be appended + * @param itagItem the {@link ItagItem} to use, which must not be null + */ + private static void generateInitializationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) + throws CreationException { + try { + final Element segmentBaseElement = (Element) document.getElementsByTagName( + SEGMENT_BASE).item(0); + + final Element initializationElement = document.createElement(INITIALIZATION); + final Attr rangeAttribute = document.createAttribute("range"); + + if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { + throw CreationException.couldNotAddElement(INITIALIZATION, + "ItagItem's initStart and/or " + "initEnd are/is < 0: " + + itagItem.getInitStart() + "-" + itagItem.getInitEnd()); + } + + rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd()); + initializationElement.setAttributeNode(rangeAttribute); + + segmentBaseElement.appendChild(initializationElement); + } catch (final DOMException e) { + throw CreationException.couldNotAddElement(INITIALIZATION, e); + } + } +} 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/YoutubeDashManifestCreatorsTest.java similarity index 80% rename from extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java rename to extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorsTest.java index ca8185b9f..0d276f901 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/YoutubeDashManifestCreatorsTest.java @@ -1,5 +1,30 @@ package org.schabi.newpipe.extractor.services.youtube; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.schabi.newpipe.downloader.DownloaderTestImpl; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; +import org.schabi.newpipe.extractor.stream.Stream; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import javax.annotation.Nonnull; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.StringReader; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,41 +36,25 @@ import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.ADAPTATION_SET; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.INITIALIZATION; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.PERIOD; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.REPRESENTATION; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_BASE; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_TEMPLATE; -import static org.schabi.newpipe.extractor.services.youtube.YoutubeDashManifestCreator.SEGMENT_TIMELINE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ADAPTATION_SET; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.AUDIO_CHANNEL_CONFIGURATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.BASE_URL; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.INITIALIZATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.MPD; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.PERIOD; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.REPRESENTATION; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ROLE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TEMPLATE; +import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.schabi.newpipe.downloader.DownloaderTestImpl; -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; -import org.schabi.newpipe.extractor.stream.DeliveryMethod; -import org.schabi.newpipe.extractor.stream.Stream; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import java.io.StringReader; -import java.util.List; -import java.util.Random; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import javax.annotation.Nonnull; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - /** - * Test for {@link YoutubeDashManifestCreator}. Tests the generation of OTF and Progressive - * manifests. + * Test for YouTube DASH manifest creators. + * + *

    + * 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 assertFilterStreams(final List streams, - final DeliveryMethod deliveryMethod) { + @Nonnull + private List assertFilterStreams( + @Nonnull final List streams, + final DeliveryMethod deliveryMethod) { final List 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 getCache() { return OTF_STREAMS_CACHE; } 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 index 21a21d2c6..07ee3b887 100644 --- 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 @@ -174,6 +174,7 @@ public final class YoutubePostLiveStreamDvrDashManifestCreator { /** * @return the cache of DASH manifests generated for post-live-DVR streams */ + @Nonnull public static ManifestCreatorCache getCache() { return POST_LIVE_DVR_STREAMS_CACHE; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java index 0a8ed4533..2b4ff86ae 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java @@ -114,6 +114,7 @@ public final class YoutubeProgressiveDashManifestCreator { /** * @return the cache of DASH manifests generated for progressive streams */ + @Nonnull public static ManifestCreatorCache getCache() { return PROGRESSIVE_STREAMS_CACHE; } From c33d392958d65d349720651f03ed6d189f5992b8 Mon Sep 17 00:00:00 2001 From: litetex <40789489+litetex@users.noreply.github.com> Date: Mon, 16 May 2022 21:09:50 +0200 Subject: [PATCH 34/38] =?UTF-8?q?Fixed=20typo=20XEE=20=E2=86=92=20XXE=20(X?= =?UTF-8?q?ml=20eXternal=20Entity=20attack)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See also: https://en.wikipedia.org/wiki/XML_external_entity_attack https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing --- .../YoutubeDashManifestCreatorsUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 48b0bf41a..7f3c52323 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 @@ -696,11 +696,11 @@ public final class YoutubeDashManifestCreatorsUtils { } /** - * Generate a new {@link DocumentBuilder} secured from XEE attacks, on platforms which + * Generate a new {@link DocumentBuilder} secured from XXE attacks, on platforms which * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link DocumentBuilderFactory} instances. * - * @return an instance of {@link Document} secured against XEE attacks on supported platforms, + * @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 */ private static Document newDocument() throws ParserConfigurationException { @@ -718,13 +718,13 @@ public final class YoutubeDashManifestCreatorsUtils { } /** - * Generate a new {@link TransformerFactory} secured from XEE attacks, on platforms which + * Generate a new {@link TransformerFactory} secured from XXE attacks, on platforms which * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link TransformerFactory} instances. * * @param document the document to convert, which must have been created using - * {@link #newDocument()} to properly prevent XEE attacks - * @return the document converted to an XML string, making sure there can't be XEE attacks + * {@link #newDocument()} to properly prevent XXE attacks + * @return the document converted to an XML string, making sure there can't be XXE attacks */ // Sonar warning is suppressed because it is still shown even if we apply its solution @SuppressWarnings("squid:S2755") From 044639c32bb715882685570be08ee3d6662bb075 Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 18 May 2022 12:39:41 +0200 Subject: [PATCH 35/38] Solve some review comments --- .../BandcampRadioStreamExtractor.java | 9 +- .../services/youtube/DeliveryType.java | 2 +- .../YoutubeDashManifestCreatorsUtils.java | 69 ++---------- .../extractors/YoutubeStreamExtractor.java | 102 ++++++++---------- .../newpipe/extractor/stream/StreamInfo.java | 2 +- 5 files changed, 63 insertions(+), 121 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java index 9668b8035..5724d371d 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioStreamExtractor.java @@ -5,6 +5,7 @@ import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonParserException; import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; @@ -80,9 +81,11 @@ public class BandcampRadioStreamExtractor extends BandcampStreamExtractor { @Nonnull @Override - public String getUploaderName() { - return Jsoup.parse(showInfo.getString("image_caption")) - .getElementsByTag("a").first().text(); + public String getUploaderName() throws ParsingException { + return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream() + .map(Element::text) + .findFirst() + .orElseThrow(() -> new ParsingException("Could not get uploader name")); } @Nullable diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java index 0e5b34504..17833dc5f 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java @@ -4,7 +4,7 @@ package org.schabi.newpipe.extractor.services.youtube; * Streaming format types used by YouTube in their streams. * *

    - * 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 { - @Nonnull - T buildStream(ItagInfo itagInfo); - } - @Nonnull private List getItags( final String streamingDataKey, final ItagItem.ItagType itagTypeWanted, - final StreamBuilderHelper streamBuilderHelper, + final java.util.function.Function streamBuilderHelper, final String streamTypeExceptionMessage) throws ParsingException { try { final List itagInfos = new ArrayList<>(); @@ -1176,7 +1170,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { final List streamList = new ArrayList<>(); for (final ItagInfo itagInfo : itagInfos) { - final T stream = streamBuilderHelper.buildStream(itagInfo); + final T stream = streamBuilderHelper.apply(itagInfo); if (!Stream.containSimilarStream(stream, streamList)) { streamList.add(stream); } @@ -1190,8 +1184,8 @@ public class YoutubeStreamExtractor extends StreamExtractor { } /** - * Get the {@link StreamBuilderHelper} which will be used to build {@link AudioStream}s in - * {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)} + * Get the stream builder helper which will be used to build {@link AudioStream}s in + * {@link #getItags(String, ItagItem.ItagType, java.util.function.Function, String)} * *

    * 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 getAudioStreamBuilderHelper() { - return new StreamBuilderHelper() { - @Nonnull - @Override - public AudioStream buildStream(@Nonnull final ItagInfo itagInfo) { - final ItagItem itagItem = itagInfo.getItagItem(); - final AudioStream.Builder builder = new AudioStream.Builder() - .setId(String.valueOf(itagItem.id)) - .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) - .setMediaFormat(itagItem.getMediaFormat()) - .setAverageBitrate(itagItem.getAverageBitrate()) - .setItagItem(itagItem); + private java.util.function.Function getAudioStreamBuilderHelper() { + return (itagInfo) -> { + final ItagItem itagItem = itagInfo.getItagItem(); + final AudioStream.Builder builder = new AudioStream.Builder() + .setId(String.valueOf(itagItem.id)) + .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) + .setMediaFormat(itagItem.getMediaFormat()) + .setAverageBitrate(itagItem.getAverageBitrate()) + .setItagItem(itagItem); - if (streamType == StreamType.LIVE_STREAM - || streamType == StreamType.POST_LIVE_STREAM - || !itagInfo.getIsUrl()) { - // For YouTube videos on OTF streams and for all streams of post-live streams - // and live streams, only the DASH delivery method can be used. - builder.setDeliveryMethod(DeliveryMethod.DASH); - } - - return builder.build(); + if (streamType == StreamType.LIVE_STREAM + || streamType == StreamType.POST_LIVE_STREAM + || !itagInfo.getIsUrl()) { + // For YouTube videos on OTF streams and for all streams of post-live streams + // and live streams, only the DASH delivery method can be used. + builder.setDeliveryMethod(DeliveryMethod.DASH); } + + return builder.build(); }; } /** - * Get the {@link StreamBuilderHelper} which will be used to build {@link VideoStream}s in - * {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)} + * Get the stream builder helper which will be used to build {@link VideoStream}s in + * {@link #getItags(String, ItagItem.ItagType, java.util.function.Function, String)} * *

    * 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 getVideoStreamBuilderHelper( + private java.util.function.Function getVideoStreamBuilderHelper( final boolean areStreamsVideoOnly) { - return new StreamBuilderHelper() { - @Nonnull - @Override - public VideoStream buildStream(@Nonnull final ItagInfo itagInfo) { - final ItagItem itagItem = itagInfo.getItagItem(); - final VideoStream.Builder builder = new VideoStream.Builder() - .setId(String.valueOf(itagItem.id)) - .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) - .setMediaFormat(itagItem.getMediaFormat()) - .setIsVideoOnly(areStreamsVideoOnly) - .setItagItem(itagItem); + return (itagInfo) -> { + final ItagItem itagItem = itagInfo.getItagItem(); + final VideoStream.Builder builder = new VideoStream.Builder() + .setId(String.valueOf(itagItem.id)) + .setContent(itagInfo.getContent(), itagInfo.getIsUrl()) + .setMediaFormat(itagItem.getMediaFormat()) + .setIsVideoOnly(areStreamsVideoOnly) + .setItagItem(itagItem); - final String resolutionString = itagItem.getResolutionString(); - builder.setResolution(resolutionString != null ? resolutionString - : EMPTY_STRING); + final String resolutionString = itagItem.getResolutionString(); + builder.setResolution(resolutionString != null ? resolutionString + : EMPTY_STRING); - if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { - // For YouTube videos on OTF streams and for all streams of post-live streams - // and live streams, only the DASH delivery method can be used. - builder.setDeliveryMethod(DeliveryMethod.DASH); - } - - return builder.build(); + if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) { + // For YouTube videos on OTF streams and for all streams of post-live streams + // and live streams, only the DASH delivery method can be used. + builder.setDeliveryMethod(DeliveryMethod.DASH); } + + return builder.build(); }; } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java index c067221ca..8d3f2c522 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamInfo.java @@ -114,7 +114,7 @@ public class StreamInfo extends Info { final String name = extractor.getName(); final int ageLimit = extractor.getAgeLimit(); - // Suppress always-non-null warning as here we double-check it is really not null + // Suppress always-non-null warning as here we double-check it really is not null //noinspection ConstantConditions if (streamType == StreamType.NONE || isNullOrEmpty(url) From d652e05874bcdae73e717b7157041d44daa3911b Mon Sep 17 00:00:00 2001 From: Stypox Date: Fri, 20 May 2022 11:06:58 +0200 Subject: [PATCH 36/38] [MediaCCC] Fix comments about containsSimilarStream --- .../extractors/MediaCCCStreamExtractor.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java index 7aa7569f8..0a086fcc6 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCStreamExtractor.java @@ -114,9 +114,9 @@ public class MediaCCCStreamExtractor extends StreamExtractor { mediaFormat = null; } - // Don't use the containsSimilarStream method because it will always return - // false. So if there are multiple audio streams available, only the first one will - // be extracted in this case. + // Not checking containsSimilarStream here, since MediaCCC does not provide enough + // information to decide whether two streams are similar. Hence that method would + // always return false, e.g. even for different language variations. audioStreams.add(new AudioStream.Builder() .setId(recording.getString("filename", ID_UNKNOWN)) .setContent(recording.getString("recording_url"), true) @@ -137,7 +137,6 @@ public class MediaCCCStreamExtractor extends StreamExtractor { final String mimeType = recording.getString("mime_type"); if (mimeType.startsWith("video")) { // First we need to resolve the actual video data from the CDN - final MediaFormat mediaFormat; if (mimeType.endsWith("webm")) { mediaFormat = MediaFormat.WEBM; @@ -147,9 +146,9 @@ public class MediaCCCStreamExtractor extends StreamExtractor { mediaFormat = null; } - // Don't use the containsSimilarStream method because it will prevent the - // extraction of some video variations (mostly languages). So if there are multiple - // video streams available, only the first one will be extracted in this case. + // Not checking containsSimilarStream here, since MediaCCC does not provide enough + // information to decide whether two streams are similar. Hence that method would + // always return false, e.g. even for different language variations. videoStreams.add(new VideoStream.Builder() .setId(recording.getString("filename", ID_UNKNOWN)) .setContent(recording.getString("recording_url"), true) From b3c620f0d8b19ddc532fe90dec7b813aea2caa29 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 28 May 2022 00:26:53 +0200 Subject: [PATCH 37/38] Apply code review and Streams rework --- .../MediaCCCLiveStreamExtractor.java | 41 ++- .../MediaCCCLiveStreamMapperDTO.java | 29 -- .../extractor/services/youtube/ItagItem.java | 6 +- .../YoutubeDashManifestCreatorsUtils.java | 274 +++++++----------- .../YoutubeOtfDashManifestCreator.java | 30 +- ...ePostLiveStreamDvrDashManifestCreator.java | 29 +- ...YoutubeProgressiveDashManifestCreator.java | 60 ++-- .../extractors/YoutubeStreamExtractor.java | 106 +++---- .../newpipe/extractor/stream/Stream.java | 19 +- .../utils/ManifestCreatorCacheTest.java | 5 + 10 files changed, 248 insertions(+), 351 deletions(-) delete mode 100644 extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java index da03e20e6..c761b33a1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamExtractor.java @@ -159,11 +159,11 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { return getStreams("audio", dto -> { final AudioStream.Builder builder = new AudioStream.Builder() - .setId(dto.getUrlValue().getString("tech", ID_UNKNOWN)) - .setContent(dto.getUrlValue().getString(URL), true) + .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) + .setContent(dto.urlValue.getString(URL), true) .setAverageBitrate(UNKNOWN_BITRATE); - if ("hls".equals(dto.getUrlKey())) { + if ("hls".equals(dto.urlKey)) { // We don't know with the type string what media format will // have HLS streams. // However, the tech string may contain some information @@ -172,7 +172,7 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { .build(); } - return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey())) + return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) .build(); }); } @@ -181,15 +181,15 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { public List getVideoStreams() throws IOException, ExtractionException { return getStreams("video", dto -> { - final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize"); + final JsonArray videoSize = dto.streamJsonObj.getArray("videoSize"); final VideoStream.Builder builder = new VideoStream.Builder() - .setId(dto.getUrlValue().getString("tech", ID_UNKNOWN)) - .setContent(dto.getUrlValue().getString(URL), true) + .setId(dto.urlValue.getString("tech", ID_UNKNOWN)) + .setContent(dto.urlValue.getString(URL), true) .setIsVideoOnly(false) .setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1)); - if ("hls".equals(dto.getUrlKey())) { + if ("hls".equals(dto.urlKey)) { // We don't know with the type string what media format will // have HLS streams. // However, the tech string may contain some information @@ -198,11 +198,32 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { .build(); } - return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey())) + return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey)) .build(); }); } + + /** + * This is just an internal class used in {@link #getStreams(String, Function)} to tie together + * the stream json object, its URL key and its URL value. An object of this class would be + * temporary and the three values it holds would be converted to a proper {@link Stream} + * object based on the wanted stream type. + */ + private static final class MediaCCCLiveStreamMapperDTO { + final JsonObject streamJsonObj; + final String urlKey; + final JsonObject urlValue; + + MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, + final String urlKey, + final JsonObject urlValue) { + this.streamJsonObj = streamJsonObj; + this.urlKey = urlKey; + this.urlValue = urlValue; + } + } + private List getStreams( @Nonnull final String streamType, @Nonnull final Function converter) { @@ -220,7 +241,7 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor { e.getKey(), (JsonObject) e.getValue()))) // The DASH manifest will be extracted with getDashMpdUrl - .filter(dto -> !"dash".equals(dto.getUrlKey())) + .filter(dto -> !"dash".equals(dto.urlKey)) // Convert .map(converter) .collect(Collectors.toList()); diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java deleted file mode 100644 index c06ef736b..000000000 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamMapperDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.schabi.newpipe.extractor.services.media_ccc.extractors; - -import com.grack.nanojson.JsonObject; - -final class MediaCCCLiveStreamMapperDTO { - private final JsonObject streamJsonObj; - private final String urlKey; - private final JsonObject urlValue; - - MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj, - final String urlKey, - final JsonObject urlValue) { - this.streamJsonObj = streamJsonObj; - this.urlKey = urlKey; - this.urlValue = urlValue; - } - - JsonObject getStreamJsonObj() { - return streamJsonObj; - } - - String getUrlKey() { - return urlKey; - } - - JsonObject getUrlValue() { - return urlValue; - } -} diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java index fa6e97326..e0ff09a6f 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 @@ -409,11 +409,7 @@ public class ItagItem implements Serializable { * @param sampleRate the sample rate of an audio itag */ public void setSampleRate(final int sampleRate) { - if (sampleRate > 0) { - this.sampleRate = sampleRate; - } else { - this.sampleRate = SAMPLE_RATE_UNKNOWN; - } + this.sampleRate = sampleRate > 0 ? sampleRate : SAMPLE_RATE_UNKNOWN; } /** 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 ec876c671..5c45f65df 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 @@ -97,6 +97,25 @@ public final class YoutubeDashManifestCreatorsUtils { public static final String SEGMENT_BASE = "SegmentBase"; public static final String INITIALIZATION = "Initialization"; + /** + * Create an attribute with {@link Document#createAttribute(String)}, assign to it the provided + * name and value, then add it to the provided element using {@link + * Element#setAttributeNode(Attr)}. + * + * @param element element to which to add the created node + * @param doc document to use to create the attribute + * @param name name of the attribute + * @param value value of the attribute, will be set using {@link Attr#setValue(String)} + */ + public static void setAttribute(final Element element, + final Document doc, + final String name, + final String value) { + final Attr attr = doc.createAttribute(name); + attr.setValue(value); + element.setAttributeNode(attr); + } + /** * Generate a {@link Document} with common manifest creator elements added to it. * @@ -123,17 +142,17 @@ public final class YoutubeDashManifestCreatorsUtils { public static Document generateDocumentAndDoCommonElementsGeneration( @Nonnull final ItagItem itagItem, final long streamDuration) throws CreationException { - final Document document = generateDocumentAndMpdElement(streamDuration); + final Document doc = generateDocumentAndMpdElement(streamDuration); - generatePeriodElement(document); - generateAdaptationSetElement(document, itagItem); - generateRoleElement(document); - generateRepresentationElement(document, itagItem); + generatePeriodElement(doc); + generateAdaptationSetElement(doc, itagItem); + generateRoleElement(doc); + generateRepresentationElement(doc, itagItem); if (itagItem.itagType == ItagItem.ItagType.AUDIO) { - generateAudioChannelConfigurationElement(document, itagItem); + generateAudioChannelConfigurationElement(doc, itagItem); } - return document; + return doc; } /** @@ -161,46 +180,25 @@ public final class YoutubeDashManifestCreatorsUtils { public static Document generateDocumentAndMpdElement(final long duration) throws CreationException { try { - final Document document = newDocument(); + final Document doc = newDocument(); - final Element mpdElement = document.createElement(MPD); - document.appendChild(mpdElement); + final Element mpdElement = doc.createElement(MPD); + doc.appendChild(mpdElement); - final Attr xmlnsXsiAttribute = document.createAttribute("xmlns:xsi"); - xmlnsXsiAttribute.setValue("http://www.w3.org/2001/XMLSchema-instance"); - mpdElement.setAttributeNode(xmlnsXsiAttribute); + setAttribute(mpdElement, doc, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + setAttribute(mpdElement, doc, "xmlns", "urn:mpeg:DASH:schema:MPD:2011"); + setAttribute(mpdElement, doc, "xsi:schemaLocation", + "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); + setAttribute(mpdElement, doc, "minBufferTime", "PT1.500S"); + setAttribute(mpdElement, doc, "profiles", "urn:mpeg:dash:profile:full:2011"); + setAttribute(mpdElement, doc, "type", "static"); + setAttribute(mpdElement, doc, "mediaPresentationDuration", + String.format(Locale.ENGLISH, "PT%.3fS", duration / 1000.0)); - final Attr xmlns = document.createAttribute("xmlns"); - xmlns.setValue("urn:mpeg:DASH:schema:MPD:2011"); - mpdElement.setAttributeNode(xmlns); - - final Attr xsiSchemaLocationAttribute = document.createAttribute("xsi:schemaLocation"); - xsiSchemaLocationAttribute.setValue("urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"); - mpdElement.setAttributeNode(xsiSchemaLocationAttribute); - - final Attr minBufferTimeAttribute = document.createAttribute("minBufferTime"); - minBufferTimeAttribute.setValue("PT1.500S"); - mpdElement.setAttributeNode(minBufferTimeAttribute); - - final Attr profilesAttribute = document.createAttribute("profiles"); - profilesAttribute.setValue("urn:mpeg:dash:profile:full:2011"); - mpdElement.setAttributeNode(profilesAttribute); - - final Attr typeAttribute = document.createAttribute("type"); - typeAttribute.setValue("static"); - mpdElement.setAttributeNode(typeAttribute); - - final Attr mediaPresentationDurationAttribute = document.createAttribute( - "mediaPresentationDuration"); - final String durationSeconds = String.format(Locale.ENGLISH, "%.3f", - duration / 1000.0); - mediaPresentationDurationAttribute.setValue("PT" + durationSeconds + "S"); - mpdElement.setAttributeNode(mediaPresentationDurationAttribute); - - return document; + return doc; } catch (final Exception e) { throw new CreationException( - "Could not generate the DASH manifest or append the MPD document to it", e); + "Could not generate the DASH manifest or append the MPD doc to it", e); } } @@ -212,14 +210,13 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateDocumentAndMpdElement(long)}. *

    * - * @param document the {@link Document} on which the the {@code } element will be - * appended + * @param doc the {@link Document} on which the the {@code } element will be appended */ - public static void generatePeriodElement(@Nonnull final Document document) + public static void generatePeriodElement(@Nonnull final Document doc) throws CreationException { try { - final Element mpdElement = (Element) document.getElementsByTagName(MPD).item(0); - final Element periodElement = document.createElement(PERIOD); + final Element mpdElement = (Element) doc.getElementsByTagName(MPD).item(0); + final Element periodElement = doc.createElement(PERIOD); mpdElement.appendChild(periodElement); } catch (final DOMException e) { throw CreationException.couldNotAddElement(PERIOD, e); @@ -235,21 +232,18 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generatePeriodElement(Document)}. *

    * - * @param document the {@link Document} on which the {@code } element will be - * appended + * @param doc the {@link Document} on which the {@code } element will be appended * @param itagItem the {@link ItagItem} corresponding to the stream, which must not be null */ - public static void generateAdaptationSetElement(@Nonnull final Document document, + public static void generateAdaptationSetElement(@Nonnull final Document doc, @Nonnull final ItagItem itagItem) throws CreationException { try { - final Element periodElement = (Element) document.getElementsByTagName(PERIOD) + final Element periodElement = (Element) doc.getElementsByTagName(PERIOD) .item(0); - final Element adaptationSetElement = document.createElement(ADAPTATION_SET); + final Element adaptationSetElement = doc.createElement(ADAPTATION_SET); - final Attr idAttribute = document.createAttribute("id"); - idAttribute.setValue("0"); - adaptationSetElement.setAttributeNode(idAttribute); + setAttribute(adaptationSetElement, doc, "id", "0"); final MediaFormat mediaFormat = itagItem.getMediaFormat(); if (mediaFormat == null || isNullOrEmpty(mediaFormat.getMimeType())) { @@ -257,14 +251,8 @@ public final class YoutubeDashManifestCreatorsUtils { "the MediaFormat or its mime type is null or empty"); } - final Attr mimeTypeAttribute = document.createAttribute("mimeType"); - mimeTypeAttribute.setValue(mediaFormat.getMimeType()); - adaptationSetElement.setAttributeNode(mimeTypeAttribute); - - final Attr subsegmentAlignmentAttribute = document.createAttribute( - "subsegmentAlignment"); - subsegmentAlignmentAttribute.setValue("true"); - adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute); + setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType()); + setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true"); periodElement.appendChild(adaptationSetElement); } catch (final DOMException e) { @@ -289,23 +277,17 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateAdaptationSetElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the the {@code } element will be - * appended + * @param doc the {@link Document} on which the the {@code } element will be appended */ - public static void generateRoleElement(@Nonnull final Document document) + public static void generateRoleElement(@Nonnull final Document doc) throws CreationException { try { - final Element adaptationSetElement = (Element) document.getElementsByTagName( + final Element adaptationSetElement = (Element) doc.getElementsByTagName( ADAPTATION_SET).item(0); - final Element roleElement = document.createElement(ROLE); + final Element roleElement = doc.createElement(ROLE); - final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); - schemeIdUriAttribute.setValue("urn:mpeg:DASH:role:2011"); - roleElement.setAttributeNode(schemeIdUriAttribute); - - final Attr valueAttribute = document.createAttribute("value"); - valueAttribute.setValue("main"); - roleElement.setAttributeNode(valueAttribute); + setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011"); + setAttribute(roleElement, doc, "value", "main"); adaptationSetElement.appendChild(roleElement); } catch (final DOMException e) { @@ -322,56 +304,43 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateAdaptationSetElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the the {@code } element will - * be appended + * @param doc the {@link Document} on which the the {@code } element will be + * appended * @param itagItem the {@link ItagItem} to use, which must not be null */ - public static void generateRepresentationElement(@Nonnull final Document document, + public static void generateRepresentationElement(@Nonnull final Document doc, @Nonnull final ItagItem itagItem) throws CreationException { try { - final Element adaptationSetElement = (Element) document.getElementsByTagName( + final Element adaptationSetElement = (Element) doc.getElementsByTagName( ADAPTATION_SET).item(0); - final Element representationElement = document.createElement(REPRESENTATION); + final Element representationElement = doc.createElement(REPRESENTATION); final int id = itagItem.id; if (id <= 0) { throw CreationException.couldNotAddElement(REPRESENTATION, "the id of the ItagItem is <= 0"); } - final Attr idAttribute = document.createAttribute("id"); - idAttribute.setValue(String.valueOf(id)); - representationElement.setAttributeNode(idAttribute); + setAttribute(representationElement, doc, "id", String.valueOf(id)); final String codec = itagItem.getCodec(); if (isNullOrEmpty(codec)) { throw CreationException.couldNotAddElement(ADAPTATION_SET, "the codec value of the ItagItem is null or empty"); } - final Attr codecsAttribute = document.createAttribute("codecs"); - codecsAttribute.setValue(codec); - representationElement.setAttributeNode(codecsAttribute); - - final Attr startWithSAPAttribute = document.createAttribute("startWithSAP"); - startWithSAPAttribute.setValue("1"); - representationElement.setAttributeNode(startWithSAPAttribute); - - final Attr maxPlayoutRateAttribute = document.createAttribute("maxPlayoutRate"); - maxPlayoutRateAttribute.setValue("1"); - representationElement.setAttributeNode(maxPlayoutRateAttribute); + setAttribute(representationElement, doc, "codecs", codec); + setAttribute(representationElement, doc, "startWithSAP", "1"); + setAttribute(representationElement, doc, "maxPlayoutRate", "1"); final int bitrate = itagItem.getBitrate(); if (bitrate <= 0) { throw CreationException.couldNotAddElement(REPRESENTATION, "the bitrate of the ItagItem is <= 0"); } - final Attr bandwidthAttribute = document.createAttribute("bandwidth"); - bandwidthAttribute.setValue(String.valueOf(bitrate)); - representationElement.setAttributeNode(bandwidthAttribute); + setAttribute(representationElement, doc, "bandwidth", String.valueOf(bitrate)); - final ItagItem.ItagType itagType = itagItem.itagType; - - if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { + if (itagItem.itagType == ItagItem.ItagType.VIDEO + || itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY) { final int height = itagItem.getHeight(); final int width = itagItem.getWidth(); if (height <= 0 && width <= 0) { @@ -380,25 +349,19 @@ public final class YoutubeDashManifestCreatorsUtils { } if (width > 0) { - final Attr widthAttribute = document.createAttribute("width"); - widthAttribute.setValue(String.valueOf(width)); - representationElement.setAttributeNode(widthAttribute); + setAttribute(representationElement, doc, "width", String.valueOf(width)); } - - final Attr heightAttribute = document.createAttribute("height"); - heightAttribute.setValue(String.valueOf(itagItem.getHeight())); - representationElement.setAttributeNode(heightAttribute); + setAttribute(representationElement, doc, "height", + String.valueOf(itagItem.getHeight())); final int fps = itagItem.getFps(); if (fps > 0) { - final Attr frameRateAttribute = document.createAttribute("frameRate"); - frameRateAttribute.setValue(String.valueOf(fps)); - representationElement.setAttributeNode(frameRateAttribute); + setAttribute(representationElement, doc, "frameRate", String.valueOf(fps)); } } - if (itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { - final Attr audioSamplingRateAttribute = document.createAttribute( + if (itagItem.itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) { + final Attr audioSamplingRateAttribute = doc.createAttribute( "audioSamplingRate"); audioSamplingRateAttribute.setValue(String.valueOf(itagItem.getSampleRate())); } @@ -433,32 +396,28 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateRepresentationElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the {@code } - * element will be appended + * @param doc the {@link Document} on which the {@code } element will + * be appended * @param itagItem the {@link ItagItem} to use, which must not be null */ public static void generateAudioChannelConfigurationElement( - @Nonnull final Document document, + @Nonnull final Document doc, @Nonnull final ItagItem itagItem) throws CreationException { try { - final Element representationElement = (Element) document.getElementsByTagName( + final Element representationElement = (Element) doc.getElementsByTagName( REPRESENTATION).item(0); - final Element audioChannelConfigurationElement = document.createElement( + final Element audioChannelConfigurationElement = doc.createElement( AUDIO_CHANNEL_CONFIGURATION); - final Attr schemeIdUriAttribute = document.createAttribute("schemeIdUri"); - schemeIdUriAttribute.setValue( + setAttribute(audioChannelConfigurationElement, doc, "schemeIdUri", "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"); - audioChannelConfigurationElement.setAttributeNode(schemeIdUriAttribute); - final Attr valueAttribute = document.createAttribute("value"); - final int audioChannels = itagItem.getAudioChannels(); - if (audioChannels <= 0) { + if (itagItem.getAudioChannels() <= 0) { throw new CreationException("the number of audioChannels in the ItagItem is <= 0: " - + audioChannels); + + itagItem.getAudioChannels()); } - valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels())); - audioChannelConfigurationElement.setAttributeNode(valueAttribute); + setAttribute(audioChannelConfigurationElement, doc, "value", + String.valueOf(itagItem.getAudioChannels())); representationElement.appendChild(audioChannelConfigurationElement); } catch (final DOMException e) { @@ -467,22 +426,22 @@ public final class YoutubeDashManifestCreatorsUtils { } /** - * Convert a DASH manifest {@link Document document} to a string and cache it. + * Convert a DASH manifest {@link Document doc} to a string and cache it. * * @param originalBaseStreamingUrl the original base URL of the stream - * @param document the document to be converted + * @param doc the doc to be converted * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the string * generated - * @return the DASH manifest {@link Document document} converted to a string + * @return the DASH manifest {@link Document doc} converted to a string */ public static String buildAndCacheResult( @Nonnull final String originalBaseStreamingUrl, - @Nonnull final Document document, + @Nonnull final Document doc, @Nonnull final ManifestCreatorCache manifestCreatorCache) throws CreationException { try { - final String documentXml = documentToXml(document); + final String documentXml = documentToXml(doc); manifestCreatorCache.put(originalBaseStreamingUrl, documentXml); return documentXml; } catch (final Exception e) { @@ -517,13 +476,13 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateRepresentationElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the {@code } element will + * @param doc the {@link Document} on which the {@code } element will * be appended * @param baseUrl the base URL of the OTF/post-live-DVR stream * @param deliveryType the stream {@link DeliveryType delivery type}, which must be either * {@link DeliveryType#OTF OTF} or {@link DeliveryType#LIVE LIVE} */ - public static void generateSegmentTemplateElement(@Nonnull final Document document, + public static void generateSegmentTemplateElement(@Nonnull final Document doc, @Nonnull final String baseUrl, final DeliveryType deliveryType) throws CreationException { @@ -533,32 +492,22 @@ public final class YoutubeDashManifestCreatorsUtils { } try { - final Element representationElement = (Element) document.getElementsByTagName( + final Element representationElement = (Element) doc.getElementsByTagName( REPRESENTATION).item(0); - final Element segmentTemplateElement = document.createElement(SEGMENT_TEMPLATE); + final Element segmentTemplateElement = doc.createElement(SEGMENT_TEMPLATE); - final Attr startNumberAttribute = document.createAttribute("startNumber"); - final boolean isDeliveryTypeLive = deliveryType == DeliveryType.LIVE; // The first sequence of post DVR streams is the beginning of the video stream and not // an initialization segment - final String startNumberValue = isDeliveryTypeLive ? "0" : "1"; - startNumberAttribute.setValue(startNumberValue); - segmentTemplateElement.setAttributeNode(startNumberAttribute); - - final Attr timescaleAttribute = document.createAttribute("timescale"); - timescaleAttribute.setValue("1000"); - segmentTemplateElement.setAttributeNode(timescaleAttribute); + setAttribute(segmentTemplateElement, doc, "startNumber", + deliveryType == DeliveryType.LIVE ? "0" : "1"); + setAttribute(segmentTemplateElement, doc, "timescale", "1000"); // Post-live-DVR/ended livestreams streams don't require an initialization sequence - if (!isDeliveryTypeLive) { - final Attr initializationAttribute = document.createAttribute("initialization"); - initializationAttribute.setValue(baseUrl + SQ_0); - segmentTemplateElement.setAttributeNode(initializationAttribute); + if (deliveryType != DeliveryType.LIVE) { + setAttribute(segmentTemplateElement, doc, "initialization", baseUrl + SQ_0); } - final Attr mediaAttribute = document.createAttribute("media"); - mediaAttribute.setValue(baseUrl + "&sq=$Number$"); - segmentTemplateElement.setAttributeNode(mediaAttribute); + setAttribute(segmentTemplateElement, doc, "media", baseUrl + "&sq=$Number$"); representationElement.appendChild(segmentTemplateElement); } catch (final DOMException e) { @@ -575,15 +524,15 @@ public final class YoutubeDashManifestCreatorsUtils { * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. *

    * - * @param document the {@link Document} on which the the {@code } element will - * be appended + * @param doc the {@link Document} on which the the {@code } element will be + * appended */ - public static void generateSegmentTimelineElement(@Nonnull final Document document) + public static void generateSegmentTimelineElement(@Nonnull final Document doc) throws CreationException { try { - final Element segmentTemplateElement = (Element) document.getElementsByTagName( + final Element segmentTemplateElement = (Element) doc.getElementsByTagName( SEGMENT_TEMPLATE).item(0); - final Element segmentTimelineElement = document.createElement(SEGMENT_TIMELINE); + final Element segmentTimelineElement = doc.createElement(SEGMENT_TIMELINE); segmentTemplateElement.appendChild(segmentTimelineElement); } catch (final DOMException e) { @@ -672,8 +621,7 @@ public final class YoutubeDashManifestCreatorsUtils { // supported by all platforms (like the Android implementation) } - final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - return documentBuilder.newDocument(); + return documentBuilderFactory.newDocumentBuilder().newDocument(); } /** @@ -681,13 +629,13 @@ public final class YoutubeDashManifestCreatorsUtils { * support setting {@link XMLConstants#ACCESS_EXTERNAL_DTD} and * {@link XMLConstants#ACCESS_EXTERNAL_SCHEMA} in {@link TransformerFactory} instances. * - * @param document the document to convert, which must have been created using - * {@link #newDocument()} to properly prevent XXE attacks - * @return the document converted to an XML string, making sure there can't be XXE attacks + * @param doc the doc to convert, which must have been created using {@link #newDocument()} to + * properly prevent XXE attacks + * @return the doc converted to an XML string, making sure there can't be XXE attacks */ // Sonar warning is suppressed because it is still shown even if we apply its solution @SuppressWarnings("squid:S2755") - private static String documentToXml(@Nonnull final Document document) + private static String documentToXml(@Nonnull final Document doc) throws TransformerException { final TransformerFactory transformerFactory = TransformerFactory.newInstance(); @@ -705,7 +653,7 @@ public final class YoutubeDashManifestCreatorsUtils { transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); final StringWriter result = new StringWriter(); - transformer.transform(new DOMSource(document), new StreamResult(result)); + transformer.transform(new DOMSource(doc), new StreamResult(result)); return result.toString(); } 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 f76d22356..8161b5263 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 @@ -5,7 +5,6 @@ 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.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; @@ -23,6 +22,7 @@ import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators 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.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute; import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.isBlank; @@ -148,14 +148,14 @@ public final class YoutubeOtfDashManifestCreator { streamDuration = durationSecondsFallback * 1000; } - final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, streamDuration); - generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF); - generateSegmentTimelineElement(document); - generateSegmentElementsForOtfStreams(segmentDuration, document); + generateSegmentTemplateElement(doc, realOtfBaseStreamingUrl, DeliveryType.OTF); + generateSegmentTimelineElement(doc); + generateSegmentElementsForOtfStreams(segmentDuration, doc); - return buildAndCacheResult(otfBaseStreamingUrl, document, OTF_STREAMS_CACHE); + return buildAndCacheResult(otfBaseStreamingUrl, doc, OTF_STREAMS_CACHE); } /** @@ -192,17 +192,18 @@ public final class YoutubeOtfDashManifestCreator { * * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the * regular expressions - * @param document the {@link Document} on which the {@code } elements will be appended + * @param doc the {@link Document} on which the {@code } elements will be + * appended */ private static void generateSegmentElementsForOtfStreams( @Nonnull final String[] segmentDurations, - @Nonnull final Document document) throws CreationException { + @Nonnull final Document doc) throws CreationException { try { - final Element segmentTimelineElement = (Element) document.getElementsByTagName( + final Element segmentTimelineElement = (Element) doc.getElementsByTagName( SEGMENT_TIMELINE).item(0); for (final String segmentDuration : segmentDurations) { - final Element sElement = document.createElement("S"); + final Element sElement = doc.createElement("S"); final String[] segmentLengthRepeat = segmentDuration.split("\\(r="); // make sure segmentLengthRepeat[0], which is the length, is convertible to int @@ -212,14 +213,9 @@ public final class YoutubeOtfDashManifestCreator { if (segmentLengthRepeat.length > 1) { final int segmentRepeatCount = Integer.parseInt( Utils.removeNonDigitCharacters(segmentLengthRepeat[1])); - final Attr rAttribute = document.createAttribute("r"); - rAttribute.setValue(String.valueOf(segmentRepeatCount)); - sElement.setAttributeNode(rAttribute); + setAttribute(sElement, doc, "r", String.valueOf(segmentRepeatCount)); } - - final Attr dAttribute = document.createAttribute("d"); - dAttribute.setValue(segmentLengthRepeat[0]); - sElement.setAttributeNode(dAttribute); + setAttribute(sElement, doc, "d", segmentLengthRepeat[0]); segmentTimelineElement.appendChild(sElement); } 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 index 07ee3b887..43d7e41e5 100644 --- 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 @@ -4,7 +4,6 @@ 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; @@ -23,6 +22,7 @@ import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators 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.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute; import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; @@ -159,15 +159,15 @@ public final class YoutubePostLiveStreamDvrDashManifestCreator { streamDuration = durationSecondsFallback; } - final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, streamDuration); - generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl, + generateSegmentTemplateElement(doc, realPostLiveStreamDvrStreamingUrl, DeliveryType.LIVE); - generateSegmentTimelineElement(document); - generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount); + generateSegmentTimelineElement(doc); + generateSegmentElementForPostLiveDvrStreams(doc, targetDurationSec, segmentCount); - return buildAndCacheResult(postLiveStreamDvrStreamingUrl, document, + return buildAndCacheResult(postLiveStreamDvrStreamingUrl, doc, POST_LIVE_DVR_STREAMS_CACHE); } @@ -190,7 +190,7 @@ public final class YoutubePostLiveStreamDvrDashManifestCreator { * {@code } *

    * - * @param document the {@link Document} on which the {@code } element will + * @param doc the {@link Document} on which the {@code } element will * be appended * @param targetDurationSeconds the {@code targetDurationSec} value from YouTube player * response's stream @@ -198,21 +198,16 @@ public final class YoutubePostLiveStreamDvrDashManifestCreator { * #fromPostLiveStreamDvrStreamingUrl(String, ItagItem, int, long)} */ private static void generateSegmentElementForPostLiveDvrStreams( - @Nonnull final Document document, + @Nonnull final Document doc, final int targetDurationSeconds, @Nonnull final String segmentCount) throws CreationException { try { - final Element segmentTimelineElement = (Element) document.getElementsByTagName( + final Element segmentTimelineElement = (Element) doc.getElementsByTagName( SEGMENT_TIMELINE).item(0); - final Element sElement = document.createElement("S"); + final Element sElement = doc.createElement("S"); - final Attr dAttribute = document.createAttribute("d"); - dAttribute.setValue(String.valueOf(targetDurationSeconds * 1000)); - sElement.setAttributeNode(dAttribute); - - final Attr rAttribute = document.createAttribute("r"); - rAttribute.setValue(segmentCount); - sElement.setAttributeNode(rAttribute); + setAttribute(sElement, doc, "d", String.valueOf(targetDurationSeconds * 1000)); + setAttribute(sElement, doc, "r", segmentCount); segmentTimelineElement.appendChild(sElement); } catch (final DOMException e) { diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java index 2b4ff86ae..0f69895bb 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java @@ -3,7 +3,6 @@ package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators; 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; @@ -18,6 +17,7 @@ import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE; 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.setAttribute; /** * Class which generates DASH manifests of {@link DeliveryType#PROGRESSIVE YouTube progressive} @@ -100,14 +100,14 @@ public final class YoutubeProgressiveDashManifestCreator { } } - final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem, + final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem, streamDuration); - generateBaseUrlElement(document, progressiveStreamingBaseUrl); - generateSegmentBaseElement(document, itagItem); - generateInitializationElement(document, itagItem); + generateBaseUrlElement(doc, progressiveStreamingBaseUrl); + generateSegmentBaseElement(doc, itagItem); + generateInitializationElement(doc, itagItem); - return buildAndCacheResult(progressiveStreamingBaseUrl, document, + return buildAndCacheResult(progressiveStreamingBaseUrl, doc, PROGRESSIVE_STREAMS_CACHE); } @@ -128,18 +128,17 @@ public final class YoutubeProgressiveDashManifestCreator { * {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the {@code } element will - * be appended + * @param doc the {@link Document} on which the {@code } element will be appended * @param baseUrl the base URL of the stream, which must not be null and will be set as the * content of the {@code } element */ - private static void generateBaseUrlElement(@Nonnull final Document document, + private static void generateBaseUrlElement(@Nonnull final Document doc, @Nonnull final String baseUrl) throws CreationException { try { - final Element representationElement = (Element) document.getElementsByTagName( + final Element representationElement = (Element) doc.getElementsByTagName( REPRESENTATION).item(0); - final Element baseURLElement = document.createElement(BASE_URL); + final Element baseURLElement = doc.createElement(BASE_URL); baseURLElement.setTextContent(baseUrl); representationElement.appendChild(baseURLElement); } catch (final DOMException e) { @@ -167,28 +166,23 @@ public final class YoutubeProgressiveDashManifestCreator { * should be generated too. *

    * - * @param document the {@link Document} on which the {@code } element will be - * appended + * @param doc the {@link Document} on which the {@code } element will be appended * @param itagItem the {@link ItagItem} to use, which must not be null */ - private static void generateSegmentBaseElement(@Nonnull final Document document, + private static void generateSegmentBaseElement(@Nonnull final Document doc, @Nonnull final ItagItem itagItem) throws CreationException { try { - final Element representationElement = (Element) document.getElementsByTagName( + final Element representationElement = (Element) doc.getElementsByTagName( REPRESENTATION).item(0); + final Element segmentBaseElement = doc.createElement(SEGMENT_BASE); - final Element segmentBaseElement = document.createElement(SEGMENT_BASE); - final Attr indexRangeAttribute = document.createAttribute("indexRange"); - + final String range = itagItem.getIndexStart() + "-" + itagItem.getIndexEnd(); if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) { throw CreationException.couldNotAddElement(SEGMENT_BASE, - "ItagItem's indexStart or " + "indexEnd are < 0: " - + itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); + "ItagItem's indexStart or " + "indexEnd are < 0: " + range); } - - indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd()); - segmentBaseElement.setAttributeNode(indexRangeAttribute); + setAttribute(segmentBaseElement, doc, "indexRange", range); representationElement.appendChild(segmentBaseElement); } catch (final DOMException e) { @@ -214,28 +208,24 @@ public final class YoutubeProgressiveDashManifestCreator { * {@link #generateSegmentBaseElement(Document, ItagItem)}). *

    * - * @param document the {@link Document} on which the {@code } element will - * be appended + * @param doc the {@link Document} on which the {@code } element will be + * appended * @param itagItem the {@link ItagItem} to use, which must not be null */ - private static void generateInitializationElement(@Nonnull final Document document, + private static void generateInitializationElement(@Nonnull final Document doc, @Nonnull final ItagItem itagItem) throws CreationException { try { - final Element segmentBaseElement = (Element) document.getElementsByTagName( + final Element segmentBaseElement = (Element) doc.getElementsByTagName( SEGMENT_BASE).item(0); + final Element initializationElement = doc.createElement(INITIALIZATION); - final Element initializationElement = document.createElement(INITIALIZATION); - final Attr rangeAttribute = document.createAttribute("range"); - + final String range = itagItem.getInitStart() + "-" + itagItem.getInitEnd(); if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) { throw CreationException.couldNotAddElement(INITIALIZATION, - "ItagItem's initStart and/or " + "initEnd are/is < 0: " - + itagItem.getInitStart() + "-" + itagItem.getInitEnd()); + "ItagItem's initStart and/or " + "initEnd are/is < 0: " + range); } - - rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd()); - initializationElement.setAttributeNode(rangeAttribute); + setAttribute(initializationElement, doc, "range", range); segmentBaseElement.appendChild(initializationElement); } catch (final DOMException e) { 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 5e57b2a13..a41325f3d 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 @@ -1147,34 +1147,27 @@ public class YoutubeStreamExtractor extends StreamExtractor { final java.util.function.Function streamBuilderHelper, final String streamTypeExceptionMessage) throws ParsingException { try { - final List itagInfos = new ArrayList<>(); - if (html5StreamingData == null && androidStreamingData == null - && iosStreamingData == null) { - return Collections.emptyList(); - } - - final List> streamingDataAndCpnLoopList = new ArrayList<>(); - // Use the androidStreamingData object first because there is no n param and no - // signatureCiphers in streaming URLs of the Android client - streamingDataAndCpnLoopList.add(new Pair<>(androidStreamingData, androidCpn)); - streamingDataAndCpnLoopList.add(new Pair<>(html5StreamingData, html5Cpn)); - // Use the iosStreamingData object in the last position because most of the available - // streams can be extracted with the Android and web clients and also because the iOS - // client is only enabled by default on livestreams - streamingDataAndCpnLoopList.add(new Pair<>(iosStreamingData, iosCpn)); - - for (final Pair pair : streamingDataAndCpnLoopList) { - itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey, - itagTypeWanted, pair.getSecond())); - } - + final String videoId = getId(); final List streamList = new ArrayList<>(); - for (final ItagInfo itagInfo : itagInfos) { - final T stream = streamBuilderHelper.apply(itagInfo); - if (!Stream.containSimilarStream(stream, streamList)) { - streamList.add(stream); - } - } + + java.util.stream.Stream.of( + // Use the androidStreamingData object first because there is no n param and no + // signatureCiphers in streaming URLs of the Android client + new Pair<>(androidStreamingData, androidCpn), + new Pair<>(html5StreamingData, html5Cpn), + // Use the iosStreamingData object in the last position because most of the + // available streams can be extracted with the Android and web clients and also + // because the iOS client is only enabled by default on livestreams + new Pair<>(iosStreamingData, iosCpn) + ) + .flatMap(pair -> getStreamsFromStreamingDataKey(videoId, pair.getFirst(), + streamingDataKey, itagTypeWanted, pair.getSecond())) + .map(streamBuilderHelper) + .forEachOrdered(stream -> { + if (!Stream.containSimilarStream(stream, streamList)) { + streamList.add(stream); + } + }); return streamList; } catch (final Exception e) { @@ -1293,43 +1286,36 @@ public class YoutubeStreamExtractor extends StreamExtractor { } @Nonnull - private List getStreamsFromStreamingDataKey( + private java.util.stream.Stream getStreamsFromStreamingDataKey( + final String videoId, final JsonObject streamingData, final String streamingDataKey, @Nonnull final ItagItem.ItagType itagTypeWanted, - @Nonnull final String contentPlaybackNonce) throws ParsingException { + @Nonnull final String contentPlaybackNonce) { if (streamingData == null || !streamingData.has(streamingDataKey)) { - return Collections.emptyList(); + return java.util.stream.Stream.empty(); } - final String videoId = getId(); - final List itagInfos = new ArrayList<>(); - final JsonArray formats = streamingData.getArray(streamingDataKey); - for (int i = 0; i != formats.size(); ++i) { - final JsonObject formatData = formats.getObject(i); - final int itag = formatData.getInt("itag"); - - if (!ItagItem.isSupported(itag)) { - continue; - } - - try { - final ItagItem itagItem = ItagItem.getItag(itag); - final ItagItem.ItagType itagType = itagItem.itagType; - if (itagType == itagTypeWanted) { - buildAndAddItagInfoToList(videoId, itagInfos, formatData, itagItem, - itagType, contentPlaybackNonce); - } - } catch (final IOException | ExtractionException ignored) { - } - } - - return itagInfos; + return streamingData.getArray(streamingDataKey).stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .map(formatData -> { + try { + final ItagItem itagItem = ItagItem.getItag(formatData.getInt("itag")); + if (itagItem.itagType == itagTypeWanted) { + return buildAndAddItagInfoToList(videoId, formatData, itagItem, + itagItem.itagType, contentPlaybackNonce); + } + } catch (final IOException | ExtractionException ignored) { + // if the itag is not supported and getItag fails, we end up here + } + return null; + }) + .filter(Objects::nonNull); } - private void buildAndAddItagInfoToList( + private ItagInfo buildAndAddItagInfoToList( @Nonnull final String videoId, - @Nonnull final List itagInfos, @Nonnull final JsonObject formatData, @Nonnull final ItagItem itagItem, @Nonnull final ItagItem.ItagType itagType, @@ -1372,12 +1358,10 @@ public class YoutubeStreamExtractor extends StreamExtractor { if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) { itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); - } - - if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) { + } else if (itagType == ItagItem.ItagType.VIDEO + || itagType == ItagItem.ItagType.VIDEO_ONLY) { itagItem.setFps(formatData.getInt("fps")); - } - if (itagType == ItagItem.ItagType.AUDIO) { + } else if (itagType == ItagItem.ItagType.AUDIO) { // YouTube return the audio sample rate as a string itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate"))); itagItem.setAudioChannels(formatData.getInt("audioChannels")); @@ -1403,7 +1387,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM); } - itagInfos.add(itagInfo); + return itagInfo; } @Nonnull 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 e9232c8cf..04d2b3fac 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 @@ -94,24 +94,15 @@ public abstract class Stream implements Serializable { * Note: This method always returns false if the stream passed is null. *

    * - * @param cmp the stream object to be compared to this stream object + * @param other the stream object to be compared to this stream object * @return whether the stream have the same stats or not, based on the criteria above */ - public boolean equalStats(@Nullable final Stream cmp) { - if (cmp == null) { + public boolean equalStats(@Nullable final Stream other) { + if (other == null || mediaFormat == null || other.mediaFormat == null) { return false; } - - Boolean haveSameMediaFormatId = null; - if (mediaFormat != null && cmp.mediaFormat != null) { - haveSameMediaFormatId = mediaFormat.id == cmp.mediaFormat.id; - } - final boolean areUsingSameDeliveryMethodAndAreUrlStreams = - deliveryMethod == cmp.deliveryMethod && isUrl == cmp.isUrl; - - return haveSameMediaFormatId != null - ? haveSameMediaFormatId && areUsingSameDeliveryMethodAndAreUrlStreams - : areUsingSameDeliveryMethodAndAreUrlStreams; + return mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod + && isUrl == other.isUrl; } /** diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java index 7d3fa5b65..83c5c1dfb 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/utils/ManifestCreatorCacheTest.java @@ -53,6 +53,11 @@ class ManifestCreatorCacheTest { + "call"); } + /** + * Adds sample strings to the provided manifest creator cache, in order to test clear factor and + * maximum size. + * @param cache the cache to fill with some data + */ private static void setCacheContent(final ManifestCreatorCache cache) { int i = 0; while (i < 26) { From 287d1dfd63d8bacec02f53724309374798c45dfc Mon Sep 17 00:00:00 2001 From: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com> Date: Sun, 29 May 2022 19:08:18 +0200 Subject: [PATCH 38/38] [SoundCloud] Use the HLS delivery method for all streams and extract only a single stream URL from HLS manifest for MP3 streams SoundCloud broke the workaround used to get a single file from HLS manifests for Opus manifests, but it still works for MP3 ones. The code has been adapted to prevent an unneeded request (the one to the Opus HLS manifest) and the HLS delivery method is now used for SoundCloud MP3 and Opus streams, plus the progressive one (for tracks which have a progressive stream (MP3) and for the ones which doesn't have one, it is still used by trying to get a progressive stream, using the workaround). Streams extraction has been also moved to Java 8 Stream's API and the relevant test has been also updated. --- .../extractors/SoundcloudStreamExtractor.java | 177 ++++++++++-------- .../SoundcloudStreamExtractorTest.java | 16 +- 2 files changed, 110 insertions(+), 83 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java index bacfd077e..42a832cde 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamExtractor.java @@ -28,6 +28,7 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler; import org.schabi.newpipe.extractor.localization.DateWrapper; import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Description; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamExtractor; @@ -169,7 +170,6 @@ public class SoundcloudStreamExtractor extends StreamExtractor { // Streams can be streamable and downloadable - or explicitly not. // For playing the track, it is only necessary to have a streamable track. // If this is not the case, this track might not be published yet. - // If audio streams were calculated, return the calculated result if (!track.getBoolean("streamable") || !isAvailable) { return audioStreams; } @@ -181,36 +181,29 @@ public class SoundcloudStreamExtractor extends StreamExtractor { extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings), audioStreams); } + extractDownloadableFileIfAvailable(audioStreams); } catch (final NullPointerException e) { - throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e); + throw new ExtractionException("Could not get audio streams", e); } return audioStreams; } private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) { - boolean presence = false; - for (final Object transcoding : transcodings) { - final JsonObject transcodingJsonObject = (JsonObject) transcoding; - if (transcodingJsonObject.getString("preset").contains("mp3") - && transcodingJsonObject.getObject("format").getString("protocol") - .equals("progressive")) { - presence = true; - break; - } - } - return presence; + return transcodings.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .anyMatch(transcodingJsonObject -> transcodingJsonObject.getString("preset") + .contains("mp3") && transcodingJsonObject.getObject("format") + .getString("protocol").equals("progressive")); } @Nonnull - private String getTranscodingUrl(final String endpointUrl, - final String protocol) + private String getTranscodingUrl(final String endpointUrl) throws IOException, ExtractionException { - final Downloader downloader = NewPipe.getDownloader(); - final String apiStreamUrl = endpointUrl + "?client_id=" - + clientId(); - final String response = downloader.get(apiStreamUrl).responseBody(); + final String apiStreamUrl = endpointUrl + "?client_id=" + clientId(); + final String response = NewPipe.getDownloader().get(apiStreamUrl).responseBody(); final JsonObject urlObject; try { urlObject = JsonParser.object().from(response); @@ -218,16 +211,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { throw new ParsingException("Could not parse streamable URL", e); } - final String urlString = urlObject.getString("url"); - - if (protocol.equals("progressive")) { - return urlString; - } else if (protocol.equals("hls")) { - return getSingleUrlFromHlsManifest(urlString); - } - - // else, unknown protocol - return EMPTY_STRING; + return urlObject.getString("url"); } @Nullable @@ -252,50 +236,87 @@ public class SoundcloudStreamExtractor extends StreamExtractor { private void extractAudioStreams(@Nonnull final JsonArray transcodings, final boolean mp3ProgressiveInStreams, final List audioStreams) { - for (final Object transcoding : transcodings) { - final JsonObject transcodingJsonObject = (JsonObject) transcoding; - final String url = transcodingJsonObject.getString("url"); - if (isNullOrEmpty(url)) { - continue; - } - - final String mediaUrl; - final String preset = transcodingJsonObject.getString("preset", ID_UNKNOWN); - final String protocol = transcodingJsonObject.getObject("format") - .getString("protocol"); - MediaFormat mediaFormat = null; - int averageBitrate = UNKNOWN_BITRATE; - if (preset.contains("mp3")) { - // Don't add the MP3 HLS stream if there is a progressive stream present - // because the two have the same bitrate - if (mp3ProgressiveInStreams && protocol.equals("hls")) { - continue; - } - mediaFormat = MediaFormat.MP3; - averageBitrate = 128; - } else if (preset.contains("opus")) { - mediaFormat = MediaFormat.OPUS; - averageBitrate = 64; - } - - try { - mediaUrl = getTranscodingUrl(url, protocol); - if (!mediaUrl.isEmpty()) { - final AudioStream audioStream = new AudioStream.Builder() - .setId(preset) - .setContent(mediaUrl, true) - .setMediaFormat(mediaFormat) - .setAverageBitrate(averageBitrate) - .build(); - if (!Stream.containSimilarStream(audioStream, audioStreams)) { - audioStreams.add(audioStream); + transcodings.stream() + .filter(JsonObject.class::isInstance) + .map(JsonObject.class::cast) + .forEachOrdered(transcoding -> { + final String url = transcoding.getString("url"); + if (isNullOrEmpty(url)) { + return; } - } - } catch (final Exception ignored) { - // Something went wrong when parsing this transcoding URL, so don't add it to the - // audioStreams - } - } + + final String preset = transcoding.getString("preset", ID_UNKNOWN); + final String protocol = transcoding.getObject("format").getString("protocol"); + final AudioStream.Builder builder = new AudioStream.Builder() + .setId(preset); + + try { + // streamUrl can be either the MP3 progressive stream URL or the + // manifest URL of the HLS MP3 stream (if there is no MP3 progressive + // stream, see above) + final String streamUrl = getTranscodingUrl(url); + + if (preset.contains("mp3")) { + // Don't add the MP3 HLS stream if there is a progressive stream + // present because the two have the same bitrate + final boolean isHls = protocol.equals("hls"); + if (mp3ProgressiveInStreams && isHls) { + return; + } + + builder.setMediaFormat(MediaFormat.MP3); + builder.setAverageBitrate(128); + + if (isHls) { + builder.setDeliveryMethod(DeliveryMethod.HLS); + builder.setContent(streamUrl, true); + + final AudioStream hlsStream = builder.build(); + if (!Stream.containSimilarStream(hlsStream, audioStreams)) { + audioStreams.add(hlsStream); + } + + final String progressiveHlsUrl = + getSingleUrlFromHlsManifest(streamUrl); + builder.setDeliveryMethod(DeliveryMethod.PROGRESSIVE_HTTP); + builder.setContent(progressiveHlsUrl, true); + + final AudioStream progressiveHlsStream = builder.build(); + if (!Stream.containSimilarStream( + progressiveHlsStream, audioStreams)) { + audioStreams.add(progressiveHlsStream); + } + + // The MP3 HLS stream has been added in both versions (HLS and + // progressive with the manifest parsing trick), so we need to + // continue (otherwise the code would try to add again the stream, + // which would be not added because the containsSimilarStream + // method would return false and an audio stream object would be + // created for nothing) + return; + } else { + builder.setContent(streamUrl, true); + } + } else if (preset.contains("opus")) { + // The HLS manifest trick doesn't work for opus streams + builder.setContent(streamUrl, true); + builder.setMediaFormat(MediaFormat.OPUS); + builder.setAverageBitrate(64); + builder.setDeliveryMethod(DeliveryMethod.HLS); + } else { + // Unknown format, skip to the next audio stream + return; + } + + final AudioStream audioStream = builder.build(); + if (!Stream.containSimilarStream(audioStream, audioStreams)) { + audioStreams.add(audioStream); + } + } catch (final ExtractionException | IOException ignored) { + // Something went wrong when trying to get and add this audio stream, + // skip to the next one + } + }); } /** @@ -332,7 +353,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor { } /** - * Parses a SoundCloud HLS manifest to get a single URL of HLS streams. + * Parses a SoundCloud HLS MP3 manifest to get a single URL of HLS streams. * *

    * 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 audioStreams = extractor.getAudioStreams(); assertEquals(2, audioStreams.size()); - for (final AudioStream audioStream : audioStreams) { + audioStreams.forEach(audioStream -> { final DeliveryMethod deliveryMethod = audioStream.getDeliveryMethod(); - assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod, - "Wrong delivery method for stream " + audioStream.getId() + ": " - + deliveryMethod); final String mediaUrl = audioStream.getContent(); if (audioStream.getFormat() == MediaFormat.OPUS) { // Assert that it's an OPUS 64 kbps media URL with a single range which comes // from an HLS SoundCloud CDN ExtractorAsserts.assertContains("-hls-opus-media.sndcdn.com", mediaUrl); ExtractorAsserts.assertContains(".64.opus", mediaUrl); - } - if (audioStream.getFormat() == MediaFormat.MP3) { + assertSame(DeliveryMethod.HLS, deliveryMethod, + "Wrong delivery method for stream " + audioStream.getId() + ": " + + deliveryMethod); + } else if (audioStream.getFormat() == MediaFormat.MP3) { // Assert that it's a MP3 128 kbps media URL which comes from a progressive // SoundCloud CDN ExtractorAsserts.assertContains("-media.sndcdn.com/bKOA7Pwbut93.128.mp3", mediaUrl); + assertSame(DeliveryMethod.PROGRESSIVE_HTTP, deliveryMethod, + "Wrong delivery method for stream " + audioStream.getId() + ": " + + deliveryMethod); } - } + }); } } }