diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java b/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
index 78a15553b1..83a5707243 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
@@ -42,11 +42,11 @@ public void addAllErrors(final Collection throwables) {
this.errors.addAll(throwables);
}
- public Info(final int serviceId,
- final String id,
- final String url,
- final String originalUrl,
- final String name) {
+ protected Info(final int serviceId,
+ final String id,
+ final String url,
+ final String originalUrl,
+ final String name) {
this.serviceId = serviceId;
this.id = id;
this.url = url;
@@ -54,7 +54,7 @@ public Info(final int serviceId,
this.name = name;
}
- public Info(final int serviceId, final LinkHandler linkHandler, final String name) {
+ protected Info(final int serviceId, final LinkHandler linkHandler, final String name) {
this(serviceId,
linkHandler.getId(),
linkHandler.getUrl(),
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java
deleted file mode 100644
index 4423068488..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/MediaFormat.java
+++ /dev/null
@@ -1,168 +0,0 @@
-package org.schabi.newpipe.extractor;
-
-/*
- * Created by Adam Howard on 08/11/15.
- *
- * Copyright (c) Christian Schabesberger
- * and Adam Howard 2015
- *
- * MediaFormat.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 .
- */
-
-import java.util.Arrays;
-import java.util.function.Function;
-
-/**
- * Static data about various media formats support by NewPipe, eg mime type, extension
- */
-
-@SuppressWarnings("MethodParamPad") // we want the media format table below to be aligned
-public enum MediaFormat {
- // @formatter:off
- //video and audio combined formats
- // id name suffix mimeType
- MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
- v3GPP (0x10, "3GPP", "3gp", "video/3gpp"),
- WEBM (0x20, "WebM", "webm", "video/webm"),
- // audio formats
- M4A (0x100, "m4a", "m4a", "audio/mp4"),
- WEBMA (0x200, "WebM", "webm", "audio/webm"),
- MP3 (0x300, "MP3", "mp3", "audio/mpeg"),
- OPUS (0x400, "opus", "opus", "audio/opus"),
- OGG (0x500, "ogg", "ogg", "audio/ogg"),
- WEBMA_OPUS(0x200, "WebM Opus", "webm", "audio/webm"),
- // subtitles formats
- VTT (0x1000, "WebVTT", "vtt", "text/vtt"),
- TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"),
- TRANSCRIPT1(0x3000, "TranScript v1", "srv1", "text/xml"),
- TRANSCRIPT2(0x4000, "TranScript v2", "srv2", "text/xml"),
- TRANSCRIPT3(0x5000, "TranScript v3", "srv3", "text/xml"),
- SRT (0x6000, "SubRip file format", "srt", "text/srt");
- // @formatter:on
-
- public final int id;
- public final String name;
- public final String suffix;
- public final String mimeType;
-
- MediaFormat(final int id, final String name, final String suffix, final String mimeType) {
- this.id = id;
- this.name = name;
- this.suffix = suffix;
- this.mimeType = mimeType;
- }
-
- private static T getById(final int id,
- final Function field,
- final T orElse) {
- return Arrays.stream(MediaFormat.values())
- .filter(mediaFormat -> mediaFormat.id == id)
- .map(field)
- .findFirst()
- .orElse(orElse);
- }
-
- /**
- * Return the friendly name of the media format with the supplied id
- *
- * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
- * @return the friendly name of the MediaFormat associated with this ids,
- * or an empty String if none match it.
- */
- public static String getNameById(final int id) {
- return getById(id, MediaFormat::getName, "");
- }
-
- /**
- * Return the file extension of the media format with the supplied id
- *
- * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
- * @return the file extension of the MediaFormat associated with this ids,
- * or an empty String if none match it.
- */
- public static String getSuffixById(final int id) {
- return getById(id, MediaFormat::getSuffix, "");
- }
-
- /**
- * Return the MIME type of the media format with the supplied id
- *
- * @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
- * @return the MIME type of the MediaFormat associated with this ids,
- * or an empty String if none match it.
- */
- public static String getMimeById(final int id) {
- return getById(id, MediaFormat::getMimeType, null);
- }
-
- /**
- * Return the MediaFormat with the supplied mime type
- *
- * @return MediaFormat associated with this mime type,
- * or null if none match it.
- */
- public static MediaFormat getFromMimeType(final String mimeType) {
- return Arrays.stream(MediaFormat.values())
- .filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType))
- .findFirst()
- .orElse(null);
- }
-
- /**
- * Get the media format by its id.
- *
- * @param id the id
- * @return the id of the media format or null.
- */
- public static MediaFormat getFormatById(final int id) {
- return getById(id, mediaFormat -> mediaFormat, null);
- }
-
- public static MediaFormat getFromSuffix(final String suffix) {
- return Arrays.stream(MediaFormat.values())
- .filter(mediaFormat -> mediaFormat.suffix.equals(suffix))
- .findFirst()
- .orElse(null);
- }
-
- /**
- * Get the name of the format
- *
- * @return the name of the format
- */
- public String getName() {
- return name;
- }
-
- /**
- * Get the filename extension
- *
- * @return the filename extension
- */
- public String getSuffix() {
- return suffix;
- }
-
- /**
- * Get the mime type
- *
- * @return the mime type
- */
- public String getMimeType() {
- return mimeType;
- }
-
-}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java b/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java
index 75d0bbf805..18017ded2a 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/downloader/Downloader.java
@@ -4,12 +4,13 @@
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.Localization;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
/**
* A base for downloader implementations that NewPipe will use
* to download needed resources during extraction.
@@ -148,8 +149,27 @@ public Response post(final String url,
/**
* Do a request using the specified {@link Request} object.
*
+ * @param request The request to process
* @return the result of the request
*/
public abstract Response execute(@Nonnull Request request)
throws IOException, ReCaptchaException;
+
+ /**
+ * Get the size of the content that the url is pointing by firing a HEAD request.
+ *
+ * @param url an url pointing to the content
+ * @return the size of the content, in bytes or -1 if unknown
+ */
+ public long getContentLength(final String url) {
+ try {
+ final String contentLengthHeader = head(url).getHeader("Content-Length");
+ if (contentLengthHeader == null) {
+ return -1;
+ }
+ return Long.parseLong(contentLengthHeader);
+ } catch (final Exception e) {
+ return -1;
+ }
+ }
}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java
index be6ba0b7ad..9992d985c0 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/BandcampRadioInfoItemExtractor.java
@@ -2,17 +2,17 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
+import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
+import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
+
import com.grack.nanojson.JsonObject;
+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
-import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
-import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
-
public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
private final JsonObject show;
@@ -58,8 +58,8 @@ public String getThumbnailUrl() {
}
@Override
- public StreamType getStreamType() {
- return StreamType.AUDIO_STREAM;
+ public boolean isAudioOnly() {
+ return true;
}
@Override
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 03a9c2222b..dcc2815274 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
@@ -11,7 +11,6 @@
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;
import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -21,9 +20,12 @@
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
-import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamSegment;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
import java.io.IOException;
import java.util.ArrayList;
@@ -120,24 +122,23 @@ public long getLength() {
@Override
public List getAudioStreams() {
- final List audioStreams = new ArrayList<>();
final JsonObject streams = showInfo.getObject("audio_stream");
+ final List audioStreams = new ArrayList<>();
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());
+ audioStreams.add(new SimpleAudioStreamImpl(
+ AudioFormatRegistry.MP3,
+ new SimpleProgressiveHTTPDeliveryDataImpl(streams.getString(MP3_128)),
+ 128
+ ));
}
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());
+ audioStreams.add(new SimpleAudioStreamImpl(
+ AudioFormatRegistry.OPUS,
+ new SimpleProgressiveHTTPDeliveryDataImpl(streams.getString(OPUS_LO)),
+ 100
+ ));
}
return audioStreams;
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 e4120bed89..714dca714b 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
@@ -11,7 +11,6 @@
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -19,11 +18,12 @@
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
-import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
@@ -143,36 +143,28 @@ public Description getDescription() {
@Override
public List getAudioStreams() {
- return Collections.singletonList(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 Collections.singletonList(
+ new SimpleAudioStreamImpl(
+ AudioFormatRegistry.MP3,
+ new SimpleProgressiveHTTPDeliveryDataImpl(albumJson
+ .getArray("trackinfo")
+ .getObject(0)
+ .getObject("file")
+ .getString("mp3-128")),
+ 128
+ )
+ );
}
@Override
- public long getLength() throws ParsingException {
- return (long) albumJson.getArray("trackinfo").getObject(0)
- .getDouble("duration");
- }
-
- @Override
- public List getVideoStreams() {
- return Collections.emptyList();
+ public boolean isAudioOnly() {
+ return true;
}
@Override
- public List getVideoOnlyStreams() {
- return Collections.emptyList();
- }
-
- @Override
- public StreamType getStreamType() {
- return StreamType.AUDIO_STREAM;
+ public long getLength() throws ParsingException {
+ return (long) albumJson.getArray("trackinfo").getObject(0)
+ .getDouble("duration");
}
@Override
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java
index 5e585e7e00..d1a55b33a1 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampSearchStreamInfoItemExtractor.java
@@ -22,11 +22,7 @@ public BandcampSearchStreamInfoItemExtractor(final Element searchResult,
public String getUploaderName() {
final String subhead = resultInfo.getElementsByClass("subhead").text();
final String[] splitBy = subhead.split("by ");
- if (splitBy.length > 1) {
- return splitBy[1];
- } else {
- return splitBy[0];
- }
+ return splitBy[splitBy.length > 1 ? 1 : 0];
}
@Nullable
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java
index fee96fcd97..07ca6d7ed9 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/bandcamp/extractors/streaminfoitem/BandcampStreamInfoItemExtractor.java
@@ -3,7 +3,6 @@
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
@@ -13,13 +12,13 @@
public abstract class BandcampStreamInfoItemExtractor implements StreamInfoItemExtractor {
private final String uploaderUrl;
- public BandcampStreamInfoItemExtractor(final String uploaderUrl) {
+ protected BandcampStreamInfoItemExtractor(final String uploaderUrl) {
this.uploaderUrl = uploaderUrl;
}
@Override
- public StreamType getStreamType() {
- return StreamType.AUDIO_STREAM;
+ public boolean isAudioOnly() {
+ return true;
}
@Override
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 9271bbc322..7322f75405 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
@@ -1,30 +1,32 @@
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 org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
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.Stream;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleDASHUrlDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl;
import java.io.IOException;
-import java.util.Collections;
import java.util.List;
-import java.util.function.Function;
+import java.util.Objects;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
@@ -45,7 +47,8 @@ public MediaCCCLiveStreamExtractor(final StreamingService service,
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
- final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader,
+ final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(
+ downloader,
getExtractorLocalization());
// Find the correct room
for (int c = 0; c < doc.size(); c++) {
@@ -137,7 +140,7 @@ public String getDashMpdUrl() throws ParsingException {
*/
@Nonnull
@Override
- public String getHlsUrl() {
+ public String getHlsMasterPlaylistUrl() {
return getManifestOfDeliveryMethodWanted("hls");
}
@@ -155,77 +158,56 @@ private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryM
@Override
public List getAudioStreams() throws IOException, ExtractionException {
- return getStreams("audio",
- dto -> {
- final AudioStream.Builder builder = new AudioStream.Builder()
- .setId(dto.urlValue.getString("tech", ID_UNKNOWN))
- .setContent(dto.urlValue.getString(URL), true)
- .setAverageBitrate(UNKNOWN_BITRATE);
-
- 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
- // about the media format used.
- return builder.setDeliveryMethod(DeliveryMethod.HLS)
- .build();
+ return getStreamDTOs("audio")
+ .map(dto -> {
+ try {
+ return new SimpleAudioStreamImpl(
+ new AudioFormatRegistry().getFromSuffixOrThrow(dto.getUrlKey()),
+ buildDeliveryData(dto)
+ );
+ } catch (final Exception ignored) {
+ return null;
}
-
- return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey))
- .build();
- });
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
@Override
- public List getVideoStreams() throws IOException, ExtractionException {
- return getStreams("video",
- dto -> {
- final JsonArray videoSize = dto.streamJsonObj.getArray("videoSize");
-
- final VideoStream.Builder builder = new VideoStream.Builder()
- .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.urlKey)) {
- // 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();
+ public List getVideoStreams() throws IOException, ExtractionException {
+ return getStreamDTOs("video")
+ .map(dto -> {
+ try {
+ final JsonArray videoSize =
+ dto.getStreamJsonObj().getArray("videoSize");
+
+ return new SimpleVideoAudioStreamImpl(
+ new VideoAudioFormatRegistry()
+ .getFromSuffixOrThrow(dto.getUrlKey()),
+ buildDeliveryData(dto),
+ VideoQualityData.fromHeightWidth(
+ videoSize.getInt(1, VideoQualityData.UNKNOWN),
+ videoSize.getInt(0, VideoQualityData.UNKNOWN))
+ );
+ } catch (final Exception ignored) {
+ return null;
}
-
- return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey))
- .build();
- });
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
-
- /**
- * 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 DeliveryData buildDeliveryData(final MediaCCCLiveStreamMapperDTO dto) {
+ final String url = dto.getUrlValue().getString(URL);
+ if ("hls".equals(dto.getUrlKey())) {
+ return new SimpleHLSDeliveryDataImpl(url);
+ } else if ("dash".equals(dto.getUrlKey())) {
+ return new SimpleDASHUrlDeliveryDataImpl(url);
}
+ return new SimpleProgressiveHTTPDeliveryDataImpl(url);
}
- private List getStreams(
- @Nonnull final String streamType,
- @Nonnull final Function converter) {
+ private Stream getStreamDTOs(@Nonnull final String streamType) {
return room.getArray(STREAMS).stream()
// Ensure that we use only process JsonObjects
.filter(JsonObject.class::isInstance)
@@ -238,22 +220,12 @@ private List getStreams(
.map(e -> new MediaCCCLiveStreamMapperDTO(
streamJsonObj,
e.getKey(),
- (JsonObject) e.getValue())))
- // The DASH manifest will be extracted with getDashMpdUrl
- .filter(dto -> !"dash".equals(dto.urlKey))
- // Convert
- .map(converter)
- .collect(Collectors.toList());
+ (JsonObject) e.getValue())));
}
@Override
- public List getVideoOnlyStreams() {
- return Collections.emptyList();
- }
-
- @Override
- public StreamType getStreamType() throws ParsingException {
- return StreamType.LIVE_STREAM;
+ public boolean isLive() {
+ return true;
}
@Nonnull
@@ -261,4 +233,31 @@ public StreamType getStreamType() throws ParsingException {
public String getCategory() {
return group;
}
+
+ static 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/MediaCCCLiveStreamKioskExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java
index 2b311fb352..69b78dc791 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCLiveStreamKioskExtractor.java
@@ -1,10 +1,10 @@
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
import com.grack.nanojson.JsonObject;
+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
@@ -38,15 +38,16 @@ public String getThumbnailUrl() throws ParsingException {
}
@Override
- public StreamType getStreamType() throws ParsingException {
- boolean isVideo = false;
- for (final Object stream : roomInfo.getArray("streams")) {
- if ("video".equals(((JsonObject) stream).getString("type"))) {
- isVideo = true;
- break;
- }
- }
- return isVideo ? StreamType.LIVE_STREAM : StreamType.AUDIO_LIVE_STREAM;
+ public boolean isLive() {
+ return true;
+ }
+
+ @Override
+ public boolean isAudioOnly() {
+ return roomInfo.getArray("streams").stream()
+ .filter(JsonObject.class::isInstance)
+ .map(JsonObject.class::cast)
+ .noneMatch(s -> "video".equalsIgnoreCase(s.getString("type")));
}
@Override
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java
index ad07c4720d..84b62e225e 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCParsingHelper.java
@@ -3,6 +3,7 @@
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
+
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
@@ -43,13 +44,21 @@ public static boolean isLiveStreamId(final String id) {
/**
* Get currently available live streams from
*
- * https://streaming.media.ccc.de/streams/v2.json.
+ * https://streaming.media.ccc.de/streams/v2.json.
+ *
* Use this method to cache requests, because they can get quite big.
+ *
+ *
+ *
+ * For more information see also:
+ * https://github.com/voc/streaming-website#json-api.
+ *
* TODO: implement better caching policy (max-age: 3 min)
- * @param downloader The downloader to use for making the request
+ *
+ * @param downloader The downloader to use for making the request
* @param localization The localization to be used. Will most likely be ignored.
* @return {@link JsonArray} containing current conferences and info about their rooms and
- * streams.
+ * streams.
* @throws ExtractionException if the data could not be fetched or the retrieved data could not
* be parsed to a {@link JsonArray}
*/
@@ -58,13 +67,14 @@ public static JsonArray getLiveStreams(final Downloader downloader,
throws ExtractionException {
if (liveStreams == null) {
try {
- final String site = downloader.get("https://streaming.media.ccc.de/streams/v2.json",
- localization).responseBody();
+ final String site = downloader
+ .get("https://streaming.media.ccc.de/streams/v2.json", localization)
+ .responseBody();
liveStreams = JsonParser.array().from(site);
} catch (final IOException | ReCaptchaException e) {
- throw new ExtractionException("Could not get live stream JSON.", e);
+ throw new ExtractionException("Could not get live stream JSON", e);
} catch (final JsonParserException e) {
- throw new ExtractionException("Could not parse JSON.", e);
+ throw new ExtractionException("Could not parse JSON", e);
}
}
return liveStreams;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java
index 1e18f8da13..386d6c2ce2 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/MediaCCCRecentKioskExtractor.java
@@ -6,7 +6,6 @@
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
@@ -36,11 +35,6 @@ public String getThumbnailUrl() throws ParsingException {
return event.getString("thumb_url");
}
- @Override
- public StreamType getStreamType() throws ParsingException {
- return StreamType.VIDEO_STREAM;
- }
-
@Override
public boolean isAd() {
return false;
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 0a086fcc6b..9c99ac1191 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,14 +1,9 @@
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;
import com.grack.nanojson.JsonParserException;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@@ -18,18 +13,23 @@
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCStreamLinkHandlerFactory;
-import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
@@ -96,80 +96,34 @@ public String getUploaderAvatarUrl() {
@Override
public List getAudioStreams() throws ExtractionException {
- final JsonArray recordings = data.getArray("recordings");
- final List audioStreams = new ArrayList<>();
- for (int i = 0; i < recordings.size(); i++) {
- 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 the CDN
- final MediaFormat mediaFormat;
- if (mimeType.endsWith("opus")) {
- mediaFormat = MediaFormat.OPUS;
- } else if (mimeType.endsWith("mpeg")) {
- mediaFormat = MediaFormat.MP3;
- } else if (mimeType.endsWith("ogg")) {
- mediaFormat = MediaFormat.OGG;
- } else {
- mediaFormat = null;
- }
-
- // 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)
- .setMediaFormat(mediaFormat)
- .setAverageBitrate(UNKNOWN_BITRATE)
- .build());
- }
- }
- return audioStreams;
- }
-
- @Override
- public List getVideoStreams() throws ExtractionException {
- final JsonArray recordings = data.getArray("recordings");
- final List videoStreams = new ArrayList<>();
- for (int i = 0; i < recordings.size(); i++) {
- 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 the CDN
- final MediaFormat mediaFormat;
- if (mimeType.endsWith("webm")) {
- mediaFormat = MediaFormat.WEBM;
- } else if (mimeType.endsWith("mp4")) {
- mediaFormat = MediaFormat.MPEG_4;
- } else {
- mediaFormat = null;
- }
-
- // 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)
- .setIsVideoOnly(false)
- .setMediaFormat(mediaFormat)
- .setResolution(recording.getInt("height") + "p")
- .build());
- }
- }
-
- return videoStreams;
+ return getRecordingsByMimeType("audio")
+ .map(o -> new SimpleAudioStreamImpl(
+ new AudioFormatRegistry().getFromMimeTypeOrThrow(o.getString("mime_type")),
+ new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url"))
+ ))
+ .collect(Collectors.toList());
}
@Override
- public List getVideoOnlyStreams() {
- return Collections.emptyList();
+ public List getVideoStreams() throws ExtractionException {
+ return getRecordingsByMimeType("video")
+ .map(o -> new SimpleVideoAudioStreamImpl(
+ new VideoAudioFormatRegistry()
+ .getFromMimeTypeOrThrow(o.getString("mime_type")),
+ new SimpleProgressiveHTTPDeliveryDataImpl(o.getString("recording_url")),
+ VideoQualityData.fromHeightWidth(
+ o.getInt("height", VideoQualityData.UNKNOWN),
+ o.getInt("width", VideoQualityData.UNKNOWN))
+ ))
+ .collect(Collectors.toList());
}
- @Override
- public StreamType getStreamType() {
- return StreamType.VIDEO_STREAM;
+ private Stream getRecordingsByMimeType(final String startsWithMimeType) {
+ return data.getArray("recordings").stream()
+ .filter(JsonObject.class::isInstance)
+ .map(JsonObject.class::cast)
+ .filter(rec -> rec.getString("mime_type", "")
+ .startsWith(startsWithMimeType));
}
@Override
@@ -181,8 +135,7 @@ public void onFetchPage(@Nonnull final Downloader downloader)
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/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java
index 92f0894b9f..293d825f33 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/media_ccc/extractors/infoItems/MediaCCCStreamInfoItemExtractor.java
@@ -1,11 +1,11 @@
package org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems;
import com.grack.nanojson.JsonObject;
+
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
@@ -16,11 +16,6 @@ public MediaCCCStreamInfoItemExtractor(final JsonObject event) {
this.event = event;
}
- @Override
- public StreamType getStreamType() {
- return StreamType.VIDEO_STREAM;
- }
-
@Override
public boolean isAd() {
return false;
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 d70abf11d1..991d30d53f 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
@@ -1,6 +1,5 @@
package org.schabi.newpipe.extractor.services.peertube.extractors;
-import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
import static org.schabi.newpipe.extractor.utils.Utils.UTF_8;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@@ -9,7 +8,6 @@
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
@@ -22,15 +20,25 @@
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.Privacy;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.SubtitlesStream;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleTorrentDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.format.registry.SubtitleFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.Stream;
+import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleSubtitleStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
@@ -41,6 +49,9 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@@ -53,15 +64,14 @@ public class PeertubeStreamExtractor extends StreamExtractor {
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<>();
+ private List subtitles = null;
+ private List audioStreams = null;
+ private List videoStreams = null;
public PeertubeStreamExtractor(final StreamingService service, final LinkHandler linkHandler)
throws ParsingException {
@@ -118,12 +128,7 @@ public Description getDescription() throws ParsingException {
@Override
public int getAgeLimit() throws ParsingException {
- final boolean isNSFW = JsonUtils.getBoolean(json, "nsfw");
- if (isNSFW) {
- return 18;
- } else {
- return NO_AGE_LIMIT;
- }
+ return JsonUtils.getBoolean(json, "nsfw") ? 18 : NO_AGE_LIMIT;
}
@Override
@@ -136,12 +141,9 @@ public long getTimeStamp() throws ParsingException {
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
- return 0;
- } else {
- return timestamp;
- }
+ return (timestamp == -2)
+ ? 0 // regex for timestamp was not found
+ : timestamp;
}
@Override
@@ -212,15 +214,16 @@ public String getSubChannelAvatarUrl() {
@Nonnull
@Override
- public String getHlsUrl() {
+ public String getHlsMasterPlaylistUrl() throws ParsingException {
assertPageFetched();
- if (getStreamType() == StreamType.VIDEO_STREAM
- && !isNullOrEmpty(json.getObject(FILES))) {
+ if (!isLive() && !isNullOrEmpty(json.getObject(FILES))) {
return json.getObject(FILES).getString(PLAYLIST_URL, "");
}
- return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL, "");
+ return json.getArray(STREAMING_PLAYLISTS)
+ .getObject(0)
+ .getString(PLAYLIST_URL, "");
}
@Override
@@ -234,51 +237,30 @@ public List getAudioStreams() throws ParsingException {
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();
- }
+ tryExtractStreams();
return audioStreams;
}
@Override
- public List getVideoStreams() throws ExtractionException {
+ public List getVideoStreams() throws ExtractionException {
assertPageFetched();
- if (videoStreams.isEmpty()) {
- if (getStreamType() == StreamType.VIDEO_STREAM) {
- getStreams();
- } else {
- extractLiveVideoStreams();
- }
- }
+ tryExtractStreams();
return videoStreams;
}
- @Override
- public List getVideoOnlyStreams() {
- return Collections.emptyList();
- }
-
@Nonnull
@Override
- public List getSubtitlesDefault() {
+ public List getSubtitles() {
+ assertPageFetched();
return subtitles;
}
- @Nonnull
@Override
- public List getSubtitles(final MediaFormat format) {
- return subtitles.stream()
- .filter(sub -> sub.getFormat() == format)
- .collect(Collectors.toList());
- }
-
- @Override
- public StreamType getStreamType() {
- return json.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM;
+ public boolean isLive() {
+ return json.getBoolean("isLive");
}
@Nullable
@@ -383,7 +365,7 @@ public void onFetchPage(@Nonnull final Downloader downloader)
throw new ExtractionException("Could not extract PeerTube channel data");
}
- loadSubtitles();
+ tryExtractSubtitles();
}
private void setInitialData(final String responseBody) throws ExtractionException {
@@ -398,246 +380,166 @@ private void setInitialData(final String responseBody) throws ExtractionExceptio
PeertubeParsingHelper.validate(json);
}
- private void loadSubtitles() {
- if (subtitles.isEmpty()) {
- try {
- final Response response = getDownloader().get(baseUrl
- + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT
- + getId() + "/captions");
- final JsonObject captionsJson = JsonParser.object().from(response.responseBody());
- final JsonArray captions = JsonUtils.getArray(captionsJson, "data");
- for (final Object c : captions) {
- if (c instanceof JsonObject) {
- final JsonObject caption = (JsonObject) c;
- final String url = baseUrl + JsonUtils.getString(caption, "captionPath");
- final String languageCode = JsonUtils.getString(caption, "language.id");
- final String ext = url.substring(url.lastIndexOf(".") + 1);
- final MediaFormat fmt = MediaFormat.getFromSuffix(ext);
- if (fmt != null && !isNullOrEmpty(languageCode)) {
- subtitles.add(new SubtitlesStream.Builder()
- .setContent(url, true)
- .setMediaFormat(fmt)
- .setLanguageCode(languageCode)
- .setAutoGenerated(false)
- .build());
- }
- }
- }
- } catch (final Exception ignored) {
- // Ignore all exceptions
- }
+ private void tryExtractSubtitles() {
+ if (subtitles != null) {
+ return;
}
- }
-
- private void extractLiveVideoStreams() throws ParsingException {
try {
- final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
- streamingPlaylists.stream()
+ final Response response = getDownloader().get(baseUrl
+ + PeertubeStreamLinkHandlerFactory.VIDEO_API_ENDPOINT
+ + getId() + "/captions");
+ final JsonObject captionsJson = JsonParser.object().from(response.responseBody());
+ final JsonArray captions = JsonUtils.getArray(captionsJson, "data");
+
+ subtitles = captions.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, ""), true)
- .setIsVideoOnly(false)
- .setResolution("")
- .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);
+ .map(caption -> {
+ try {
+ final String url =
+ baseUrl + JsonUtils.getString(caption, "captionPath");
+
+ return new SimpleSubtitleStreamImpl(
+ new SubtitleFormatRegistry()
+ .getFromSuffixOrThrow(
+ url.substring(url.lastIndexOf(".") + 1)),
+ new SimpleProgressiveHTTPDeliveryDataImpl(url),
+ false,
+ JsonUtils.getString(caption, "language.id")
+ );
+ } catch (final Exception ignored) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ } catch (final Exception ignored) {
+ subtitles = Collections.emptyList();
}
}
- private void getStreams() throws ParsingException {
- // Progressive streams
- getStreamsFromArray(json.getArray(FILES), "");
+ private void tryExtractStreams() throws ParsingException {
+ if (audioStreams != null && videoStreams != null) {
+ return;
+ }
- // HLS streams
+ // Initialize
+ audioStreams = new ArrayList<>();
+ videoStreams = new ArrayList<>();
+
+ // Progressive streams
try {
- 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));
- }
+ addStreamsFromArray(
+ json.getArray(FILES),
+ null);
} catch (final Exception e) {
- throw new ParsingException("Could not get streams", e);
+ throw new ParsingException("Could not add HLS streams", e);
}
- }
- private void getStreamsFromArray(@Nonnull final JsonArray streams,
- final String playlistUrl) throws ParsingException {
+ // HLS streams
try {
- /*
- 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 JsonObject stream : streams.stream()
+ json.getArray(STREAMING_PLAYLISTS).stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
- .collect(Collectors.toList())) {
-
- // Extract stream version of streams first
- 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,
- idSuffix, url, playlistUrl);
- } else {
- // A video stream
- addNewVideoStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution,
- idSuffix, url, playlistUrl);
- }
- }
+ .forEach(playlist -> addStreamsFromArray(
+ playlist.getArray(FILES),
+ playlist.getString(PLAYLIST_URL)));
} catch (final Exception e) {
- throw new ParsingException("Could not get streams from array", e);
+ throw new ParsingException("Could not add HLS streams", 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 = FILE_DOWNLOAD_URL.equals(idSuffix)
- ? JsonUtils.getString(streamJsonObject, FILE_URL)
- : url;
- return streamUrl.replace("-fragmented." + format, ".m3u8");
+ private void addStreamsFromArray(
+ @Nonnull final JsonArray streams,
+ final String playlistUrl
+ ) {
+ streams.stream()
+ .filter(JsonObject.class::isInstance)
+ .map(JsonObject.class::cast)
+ .filter(stream -> !isNullOrEmpty(getUrlFromStream(stream)))
+ .forEach(stream -> {
+ final JsonObject resJson = stream.getObject("resolution");
+ if (resJson.getString("label", "")
+ .toLowerCase()
+ .contains("audio")) {
+ // An audio stream
+ addNewStreams(
+ this.audioStreams,
+ stream,
+ playlistUrl,
+ (s, dd) -> new SimpleAudioStreamImpl(
+ new AudioFormatRegistry()
+ .getFromSuffixOrThrow(getExtensionFromStream(s)),
+ dd
+ )
+ );
+
+ } else {
+ // A video stream
+ addNewStreams(
+ this.videoStreams,
+ stream,
+ playlistUrl,
+ (s, dd) -> new SimpleVideoAudioStreamImpl(
+ new VideoAudioFormatRegistry()
+ .getFromSuffixOrThrow(getExtensionFromStream(s)),
+ dd,
+ VideoQualityData.fromHeightFps(
+ resJson.getInt("id", VideoQualityData.UNKNOWN),
+ stream.getInt("fps", VideoQualityData.UNKNOWN))
+ )
+ );
+ }
+ });
}
- @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)
- .setManifestUrl(playlistUrl)
- .build();
- if (!Stream.containSimilarStream(audioStream, audioStreams)) {
- audioStreams.add(audioStream);
- }
- }
- // Finally, add 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 static String getStreamUrlKeyFromStream(@Nonnull final JsonObject stream) {
+ return stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL;
}
- 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 = isInstanceUsingRandomUuidsForHlsStreams
- ? getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, extension,
- url)
- : getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl);
-
- final VideoStream videoStream = new VideoStream.Builder()
- .setId(id + "-" + DeliveryMethod.HLS)
- .setContent(hlsStreamUrl, true)
- .setIsVideoOnly(false)
- .setDeliveryMethod(DeliveryMethod.HLS)
- .setResolution(resolution)
- .setMediaFormat(format)
- .setManifestUrl(playlistUrl)
- .build();
- if (!Stream.containSimilarStream(videoStream, videoStreams)) {
- videoStreams.add(videoStream);
- }
+ private static String getUrlFromStream(@Nonnull final JsonObject stream) {
+ return stream.getString(getStreamUrlKeyFromStream(stream));
+ }
+
+ private static String getExtensionFromStream(@Nonnull final JsonObject stream) {
+ final String url = stream.getString(getStreamUrlKeyFromStream(stream));
+ return url.substring(url.lastIndexOf(".") + 1);
+ }
+
+ private void addNewStreams(
+ final List streams,
+ @Nonnull final JsonObject stream,
+ final String playlistUrl,
+ @Nonnull final BiFunction buildStream
+ ) {
+ final Consumer addDeliveryDataToStream =
+ dd -> {
+ try {
+ streams.add(buildStream.apply(stream, dd));
+ } catch (final Exception ignored) {
+ // Ignore exception when a single stream couldn't be added
+ }
+ };
+
+ // Add Progressive HTTP (this is also done for HLS streams because the source file can
+ // also be streamed over progressive HTTP)
+ addDeliveryDataToStream.accept(
+ new SimpleProgressiveHTTPDeliveryDataImpl(getUrlFromStream(stream)));
+
+ // Add HLS (only for PeerTube 3.4+)
+ if (!isNullOrEmpty(playlistUrl)
+ && playlistUrl.endsWith("-master.m3u8")
+ && !isNullOrEmpty(stream.getString(FILE_URL))
+ ) {
+ addDeliveryDataToStream.accept(new SimpleHLSDeliveryDataImpl(stream.getString(FILE_URL)
+ .replace("-fragmented." + getExtensionFromStream(stream), ".m3u8")));
}
- // 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());
+ // Add torrent
+ if (!isNullOrEmpty(stream.getString("torrentUrl"))) {
+ addDeliveryDataToStream.accept(
+ new SimpleTorrentDeliveryDataImpl(stream.getString("torrentUrl")));
}
}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java
index b9c7f4b13e..9cc6afeb80 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/peertube/extractors/PeertubeStreamInfoItemExtractor.java
@@ -1,12 +1,12 @@
package org.schabi.newpipe.extractor.services.peertube.extractors;
import com.grack.nanojson.JsonObject;
+
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import javax.annotation.Nullable;
@@ -93,8 +93,8 @@ public DateWrapper getUploadDate() throws ParsingException {
}
@Override
- public StreamType getStreamType() {
- return item.getBoolean("isLive") ? StreamType.LIVE_STREAM : StreamType.VIDEO_STREAM;
+ public boolean isLive() {
+ return item.getBoolean("isLive");
}
@Override
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 0d28ed9544..049552b314 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
@@ -2,8 +2,6 @@
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.UTF_8;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@@ -12,26 +10,29 @@
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
+import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
+import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
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.Privacy;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -39,6 +40,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -162,38 +166,22 @@ public String getUploaderAvatarUrl() {
@Override
public List getAudioStreams() throws ExtractionException {
- final List audioStreams = new ArrayList<>();
// 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;
+ return Collections.emptyList();
}
try {
- final JsonArray transcodings = track.getObject("media").getArray("transcodings");
- if (!isNullOrEmpty(transcodings)) {
- // Get information about what stream formats are available
- extractAudioStreams(transcodings, checkMp3ProgressivePresence(transcodings),
- audioStreams);
- }
-
- extractDownloadableFileIfAvailable(audioStreams);
+ final List audioStreams = new ArrayList<>(extractAudioStreams());
+ extractDownloadableFileIfAvailable().ifPresent(audioStreams::add);
+ return audioStreams;
} catch (final NullPointerException e) {
- throw new ExtractionException("Could not get audio streams", e);
+ throw new ExtractionException("Could not get SoundCloud's tracks audio URL", e);
}
-
- return audioStreams;
- }
-
- private static boolean checkMp3ProgressivePresence(@Nonnull final JsonArray transcodings) {
- 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
@@ -230,59 +218,54 @@ private String getDownloadUrl(@Nonnull final String trackId)
return null;
}
- private void extractAudioStreams(@Nonnull final JsonArray transcodings,
- final boolean mp3ProgressiveInStreams,
- final List audioStreams) {
- transcodings.stream()
+ private List extractAudioStreams() {
+ final JsonArray transcodings = track.getObject("media").getArray("transcodings");
+ if (isNullOrEmpty(transcodings)) {
+ return Collections.emptyList();
+ }
+
+ return transcodings.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
- .forEachOrdered(transcoding -> {
- final String url = transcoding.getString("url");
- if (isNullOrEmpty(url)) {
- return;
+ .filter(transcoding -> !isNullOrEmpty(transcoding.getString("url")))
+ .map(transcoding -> {
+ final String protocol = transcoding
+ .getObject("format")
+ .getString("protocol");
+ final String mediaUrl;
+ try {
+ mediaUrl = getTranscodingUrl(transcoding.getString("url"));
+ } catch (final Exception e) {
+ return null; // Abort if something went wrong
+ }
+ if (isNullOrEmpty(mediaUrl)) {
+ return null; // Ignore invalid urls
}
- try {
- 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);
-
- final boolean isHls = protocol.equals("hls");
- if (isHls) {
- builder.setDeliveryMethod(DeliveryMethod.HLS);
- }
-
- builder.setContent(getTranscodingUrl(url), true);
-
- if (preset.contains("mp3")) {
- // Don't add the MP3 HLS stream if there is a progressive stream
- // present because both have the same bitrate
- if (mp3ProgressiveInStreams && isHls) {
- return;
- }
-
- builder.setMediaFormat(MediaFormat.MP3);
- builder.setAverageBitrate(128);
- } else if (preset.contains("opus")) {
- 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
+ final String preset = transcoding.getString("preset", "");
+
+ final AudioMediaFormat mediaFormat;
+ final int averageBitrate;
+ if (preset.contains("mp3")) {
+ mediaFormat = AudioFormatRegistry.MP3;
+ averageBitrate = 128;
+ } else if (preset.contains("opus")) {
+ mediaFormat = AudioFormatRegistry.OPUS;
+ averageBitrate = 64;
+ } else {
+ return null;
}
- });
+
+ return new SimpleAudioStreamImpl(
+ mediaFormat,
+ "hls".equals(protocol)
+ ? new SimpleHLSDeliveryDataImpl(mediaUrl)
+ : new SimpleProgressiveHTTPDeliveryDataImpl(mediaUrl),
+ averageBitrate
+ );
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
/**
@@ -298,47 +281,73 @@ private void extractAudioStreams(@Nonnull final JsonArray transcodings,
* downloaded, and not otherwise.
*
*
- * @param audioStreams the audio streams to which the downloadable file is added
+ * @return An {@link Optional} that may contain the extracted audio stream
*/
- 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
- }
+ @Nonnull
+ private Optional extractDownloadableFileIfAvailable() {
+ if (!track.getBoolean("downloadable") || !track.getBoolean("has_downloads_left")) {
+ return Optional.empty();
}
- }
- private static String urlEncode(final String value) {
try {
- return URLEncoder.encode(value, UTF_8);
- } catch (final UnsupportedEncodingException e) {
- throw new IllegalStateException(e);
+ final String downloadUrl = getDownloadUrl(getId());
+ if (isNullOrEmpty(downloadUrl)) {
+ return Optional.empty();
+ }
+
+ // Find out what type of file is served
+ final String fileType = determineFileTypeFromDownloadUrl(downloadUrl);
+
+ // No fileType found -> ignore it
+ if (isNullOrEmpty(fileType)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new SimpleAudioStreamImpl(
+ new AudioFormatRegistry().getFromSuffixOrThrow(fileType),
+ new SimpleProgressiveHTTPDeliveryDataImpl(downloadUrl)
+ ));
+ } 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
+ return Optional.empty();
}
}
- @Override
- public List getVideoStreams() {
- return Collections.emptyList();
- }
+ /**
+ * Determines the file type/extension of the download url.
+ *
+ * Note: Uses HTTP FETCH for inspection.
+ *
+ */
+ @Nullable
+ private String determineFileTypeFromDownloadUrl(final String downloadUrl)
+ throws IOException, ReCaptchaException {
- @Override
- public List getVideoOnlyStreams() {
- return Collections.emptyList();
+ final Response response = NewPipe.getDownloader().head(downloadUrl);
+
+ // As of 2022-06 Soundcloud uses AWS S3
+ // Use the AWS header to identify the filetype first because it's simpler
+ final String amzMetaFileType = response.getHeader("x-amz-meta-file-type");
+ if (!isNullOrEmpty(amzMetaFileType)) {
+ return amzMetaFileType;
+ }
+
+ // If the AWS header was not present try extract the filetype
+ // by inspecting the download file name
+ // Example-Value:
+ // attachment;filename="SoundCloud%20Download"; filename*=utf-8''song.mp3
+ final String contentDisp = response.getHeader("Content-Disposition");
+ if (!isNullOrEmpty(contentDisp) && contentDisp.contains(".")) {
+ return contentDisp.substring(contentDisp.lastIndexOf(".") + 1);
+ }
+
+ return null;
}
@Override
- public StreamType getStreamType() {
- return StreamType.AUDIO_STREAM;
+ public boolean isAudioOnly() {
+ return true;
}
@Nullable
@@ -396,4 +405,13 @@ public List getTags() {
}
return tags;
}
+
+
+ private static String urlEncode(final String value) {
+ try {
+ return URLEncoder.encode(value, UTF_8);
+ } catch (final UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java
index 3d265e4e41..8e9e5878bb 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/soundcloud/extractors/SoundcloudStreamInfoItemExtractor.java
@@ -8,7 +8,6 @@
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudParsingHelper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
@@ -20,6 +19,11 @@ public SoundcloudStreamInfoItemExtractor(final JsonObject itemObject) {
this.itemObject = itemObject;
}
+ @Override
+ public boolean isAudioOnly() {
+ return true;
+ }
+
@Override
public String getUrl() {
return replaceHttpWithHttps(itemObject.getString("permalink_url"));
@@ -80,11 +84,6 @@ public String getThumbnailUrl() {
return artworkUrl.replace("large.jpg", "crop.jpg");
}
- @Override
- public StreamType getStreamType() {
- return StreamType.AUDIO_STREAM;
- }
-
@Override
public boolean isAd() {
return false;
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
deleted file mode 100644
index 17833dc5fc..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/DeliveryType.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.schabi.newpipe.extractor.services.youtube;
-
-/**
- * Streaming format types used by YouTube in their streams.
- *
- *
- * It is different from {@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/ItagItem.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java
deleted file mode 100644
index e0ff09a6f7..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/ItagItem.java
+++ /dev/null
@@ -1,542 +0,0 @@
-package org.schabi.newpipe.extractor.services.youtube;
-
-import static org.schabi.newpipe.extractor.MediaFormat.M4A;
-import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4;
-import static org.schabi.newpipe.extractor.MediaFormat.WEBM;
-import static org.schabi.newpipe.extractor.MediaFormat.WEBMA;
-import static org.schabi.newpipe.extractor.MediaFormat.WEBMA_OPUS;
-import static org.schabi.newpipe.extractor.MediaFormat.v3GPP;
-import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.AUDIO;
-import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO;
-import static org.schabi.newpipe.extractor.services.youtube.ItagItem.ItagType.VIDEO_ONLY;
-
-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 {
-
- /**
- * 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 ////
- /////////////////////////////////////////////////////
- new ItagItem(17, VIDEO, v3GPP, "144p"),
- new ItagItem(36, VIDEO, v3GPP, "240p"),
-
- new ItagItem(18, VIDEO, MPEG_4, "360p"),
- new ItagItem(34, VIDEO, MPEG_4, "360p"),
- new ItagItem(35, VIDEO, MPEG_4, "480p"),
- new ItagItem(59, VIDEO, MPEG_4, "480p"),
- new ItagItem(78, VIDEO, MPEG_4, "480p"),
- new ItagItem(22, VIDEO, MPEG_4, "720p"),
- new ItagItem(37, VIDEO, MPEG_4, "1080p"),
- new ItagItem(38, VIDEO, MPEG_4, "1080p"),
-
- new ItagItem(43, VIDEO, WEBM, "360p"),
- new ItagItem(44, VIDEO, WEBM, "480p"),
- new ItagItem(45, VIDEO, WEBM, "720p"),
- new ItagItem(46, VIDEO, WEBM, "1080p"),
-
- //////////////////////////////////////////////////////////////////
- // AUDIO ID ItagType Format Bitrate //
- //////////////////////////////////////////////////////////////////
- new ItagItem(171, AUDIO, WEBMA, 128),
- new ItagItem(172, AUDIO, WEBMA, 256),
- new ItagItem(139, AUDIO, M4A, 48),
- new ItagItem(140, AUDIO, M4A, 128),
- new ItagItem(141, AUDIO, M4A, 256),
- new ItagItem(249, AUDIO, WEBMA_OPUS, 50),
- new ItagItem(250, AUDIO, WEBMA_OPUS, 70),
- new ItagItem(251, AUDIO, WEBMA_OPUS, 160),
-
- /// VIDEO ONLY ////////////////////////////////////////////
- // 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"),
- new ItagItem(135, VIDEO_ONLY, MPEG_4, "480p"),
- new ItagItem(212, VIDEO_ONLY, MPEG_4, "480p"),
- new ItagItem(136, VIDEO_ONLY, MPEG_4, "720p"),
- new ItagItem(298, VIDEO_ONLY, MPEG_4, "720p60", 60),
- new ItagItem(137, VIDEO_ONLY, MPEG_4, "1080p"),
- new ItagItem(299, VIDEO_ONLY, MPEG_4, "1080p60", 60),
- new ItagItem(266, VIDEO_ONLY, MPEG_4, "2160p"),
-
- new ItagItem(278, VIDEO_ONLY, WEBM, "144p"),
- new ItagItem(242, VIDEO_ONLY, WEBM, "240p"),
- new ItagItem(243, VIDEO_ONLY, WEBM, "360p"),
- new ItagItem(244, VIDEO_ONLY, WEBM, "480p"),
- new ItagItem(245, VIDEO_ONLY, WEBM, "480p"),
- new ItagItem(246, VIDEO_ONLY, WEBM, "480p"),
- new ItagItem(247, VIDEO_ONLY, WEBM, "720p"),
- new ItagItem(248, VIDEO_ONLY, WEBM, "1080p"),
- new ItagItem(271, VIDEO_ONLY, WEBM, "1440p"),
- // #272 is either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
- new ItagItem(272, VIDEO_ONLY, WEBM, "2160p"),
- new ItagItem(302, VIDEO_ONLY, WEBM, "720p60", 60),
- new ItagItem(303, VIDEO_ONLY, WEBM, "1080p60", 60),
- new ItagItem(308, VIDEO_ONLY, WEBM, "1440p60", 60),
- new ItagItem(313, VIDEO_ONLY, WEBM, "2160p"),
- new ItagItem(315, VIDEO_ONLY, WEBM, "2160p60", 60)
- };
-
- /*//////////////////////////////////////////////////////////////////////////
- // Utils
- //////////////////////////////////////////////////////////////////////////*/
-
- public static boolean isSupported(final int itag) {
- for (final ItagItem item : ITAG_LIST) {
- if (itag == item.id) {
- return true;
- }
- }
- return false;
- }
-
- @Nonnull
- public static ItagItem getItag(final int itagId) throws ParsingException {
- for (final ItagItem item : ITAG_LIST) {
- if (itagId == item.id) {
- return new ItagItem(item);
- }
- }
- throw new ParsingException("itag " + itagId + " is not supported");
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // 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 {
- AUDIO,
- VIDEO,
- VIDEO_ONLY
- }
-
- /**
- * Call {@link #ItagItem(int, ItagType, MediaFormat, String, int)} with the fps set to 30.
- */
- public ItagItem(final int id,
- final ItagType type,
- final MediaFormat format,
- final String resolution) {
- this.id = id;
- this.itagType = type;
- this.mediaFormat = format;
- this.resolutionString = resolution;
- this.fps = 30;
- }
-
- /**
- * Constructor for videos.
- */
- public ItagItem(final int id,
- final ItagType type,
- final MediaFormat format,
- final String resolution,
- final int fps) {
- this.id = id;
- this.itagType = type;
- this.mediaFormat = format;
- this.resolutionString = resolution;
- this.fps = fps;
- }
-
- public ItagItem(final int id,
- final ItagType type,
- final MediaFormat format,
- final int avgBitrate) {
- this.id = id;
- this.itagType = type;
- this.mediaFormat = format;
- 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;
- }
-
- private final MediaFormat mediaFormat;
-
- public final int id;
- public final ItagType itagType;
-
- // Audio fields
- /** @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;
-
- /** @deprecated Use {@link #getFps()} and {@link #setFps(int)} instead. */
- @Deprecated
- public int fps = FPS_NOT_APPLICABLE_OR_UNKNOWN;
-
- // Fields for Dash
- private int bitrate;
- private int width;
- private int height;
- private int initStart;
- private int initEnd;
- private int indexStart;
- 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;
- }
-
- public void setBitrate(final int bitrate) {
- this.bitrate = bitrate;
- }
-
- public int getWidth() {
- return width;
- }
-
- public void setWidth(final int width) {
- this.width = width;
- }
-
- public int getHeight() {
- return height;
- }
-
- public void setHeight(final int height) {
- this.height = height;
- }
-
- /**
- * Get the frame rate.
- *
- *
- * 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.
- *
- *
- *
- * 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 or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
- */
- public int getFps() {
- return fps;
- }
-
- /**
- * 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
- */
- public void setFps(final int fps) {
- this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN;
- }
-
- public int getInitStart() {
- return initStart;
- }
-
- public void setInitStart(final int initStart) {
- this.initStart = initStart;
- }
-
- public int getInitEnd() {
- return initEnd;
- }
-
- public void setInitEnd(final int initEnd) {
- this.initEnd = initEnd;
- }
-
- public int getIndexStart() {
- return indexStart;
- }
-
- public void setIndexStart(final int indexStart) {
- this.indexStart = indexStart;
- }
-
- public int getIndexEnd() {
- return indexEnd;
- }
-
- public void setIndexEnd(final int indexEnd) {
- this.indexEnd = indexEnd;
- }
-
- public String getQuality() {
- return quality;
- }
-
- public void setQuality(final String quality) {
- this.quality = quality;
- }
-
- /**
- * Get the resolution string associated with this {@code ItagItem}.
- *
- *
- * It is only known for video itags.
- *
- *
- * @return the resolution string associated with this {@code ItagItem} or
- * {@code null}.
- */
- @Nullable
- public String getResolutionString() {
- return resolutionString;
- }
-
- public String getCodec() {
- return codec;
- }
-
- 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 audio
- * 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) {
- this.sampleRate = sampleRate > 0 ? sampleRate : SAMPLE_RATE_UNKNOWN;
- }
-
- /**
- * Get the number of audio channels.
- *
- *
- * 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}
- */
- public int getAudioChannels() {
- return audioChannels;
- }
-
- /**
- * Set the number of audio channels.
- *
- *
- * 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) {
- this.audioChannels = audioChannels > 0
- ? audioChannels
- : AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
- }
-
- /**
- * Get the {@code targetDurationSec} value.
- *
- *
- * 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}
- */
- public int getTargetDurationSec() {
- return targetDurationSec;
- }
-
- /**
- * Set the {@code targetDurationSec} value.
- *
- *
- * 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.
- *
- *
- * @param targetDurationSec the target duration of a segment of streams which are using the
- * live delivery method type
- */
- public void setTargetDurationSec(final int targetDurationSec) {
- this.targetDurationSec = targetDurationSec > 0
- ? 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) {
- this.approxDurationMs = approxDurationMs > 0
- ? 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) {
- this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_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 273d17abd7..d9fd8cf283 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
@@ -680,18 +680,10 @@ private static void extractClientVersionAndKeyFromHtmlSearchResultsPage()
} catch (final Parser.RegexException ignored) {
}
- if (isNullOrEmpty(key)) {
- throw new ParsingException(
- // CHECKSTYLE:OFF
- "Could not extract YouTube WEB InnerTube API key from HTML search results page");
- // CHECKSTYLE:ON
- }
-
- if (clientVersion == null) {
- throw new ParsingException(
- // CHECKSTYLE:OFF
- "Could not extract YouTube WEB InnerTube client version from HTML search results page");
- // CHECKSTYLE:ON
+ if (isNullOrEmpty(key) || clientVersion == null) {
+ throw new ParsingException("Could not extract YouTube WEB InnerTube "
+ + (isNullOrEmpty(key) ? "API key" : "client version")
+ + " from HTML search results page");
}
keyAndVersionExtracted = true;
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java
new file mode 100644
index 0000000000..1333c5db40
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/AbstractYoutubeDashManifestCreator.java
@@ -0,0 +1,679 @@
+package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator;
+
+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.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ADAPTATION_SET;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.AUDIO_CHANNEL_CONFIGURATION;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.MPD;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.PERIOD;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.REPRESENTATION;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.ROLE;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TEMPLATE;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
+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.itag.format.AudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+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.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;
+
+/**
+ * Abstract class for YouTube DASH manifest creation.
+ *
+ *
+ * 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 abstract class AbstractYoutubeDashManifestCreator implements DashManifestCreator {
+
+ /**
+ * 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";
+
+ protected final ItagInfo> itagInfo;
+ protected final long durationSecondsFallback;
+
+ protected Document document;
+
+ protected AbstractYoutubeDashManifestCreator(
+ @Nonnull final ItagInfo> itagInfo,
+ final long durationSecondsFallback) {
+ this.itagInfo = Objects.requireNonNull(itagInfo);
+ this.durationSecondsFallback = durationSecondsFallback;
+ }
+
+ @Nonnull
+ @Override
+ public String downloadUrl() {
+ return itagInfo.getStreamUrl();
+ }
+
+ @Override
+ public long getExpectedContentLength(final Downloader downloader) {
+ return downloader.getContentLength(itagInfo.getStreamUrl());
+ }
+
+ protected boolean isLiveDelivery() {
+ return false;
+ }
+
+ // region generate manifest elements
+
+ /**
+ * 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()});
+ * - {@code AdaptationSet} (using {@link #generateAdaptationSetElement()});
+ * - {@code Role} (using {@link #generateRoleElement()});
+ * - {@code Representation} (using {@link #generateRepresentationElement()});
+ * - and, for audio streams, {@code AudioChannelConfiguration} (using
+ * {@link #generateAudioChannelConfigurationElement()}).
+ *
+ *
+ *
+ * @param streamDurationMs the duration of the stream, in milliseconds
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateDocumentAndCommonElements(final long streamDurationMs) {
+ generateDocumentAndMpdElement(streamDurationMs);
+
+ generatePeriodElement();
+ generateAdaptationSetElement();
+ generateRoleElement();
+ generateRepresentationElement();
+ if (itagInfo.getItagFormat() instanceof AudioItagFormat) {
+ generateAudioChannelConfigurationElement();
+ }
+ }
+
+ /**
+ * 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 durationMs the duration of the stream, in milliseconds
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateDocumentAndMpdElement(final long durationMs) {
+ try {
+ newDocument();
+
+ final Element mpdElement = createElement(MPD);
+ document.appendChild(mpdElement);
+
+ appendNewAttrWithValue(
+ mpdElement, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+
+ appendNewAttrWithValue(mpdElement, "xmlns", "urn:mpeg:DASH:schema:MPD:2011");
+
+ appendNewAttrWithValue(
+ mpdElement,
+ "xsi:schemaLocation",
+ "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd");
+
+ appendNewAttrWithValue(mpdElement, "minBufferTime", "PT1.500S");
+
+ appendNewAttrWithValue(
+ mpdElement, "profiles", "urn:mpeg:dash:profile:full:2011");
+
+ appendNewAttrWithValue(mpdElement, "type", "static");
+
+ final String durationSeconds =
+ String.format(Locale.ENGLISH, "%.3f", durationMs / 1000.0);
+ appendNewAttrWithValue(
+ mpdElement, "mediaPresentationDuration", "PT" + durationSeconds + "S");
+ } catch (final Exception e) {
+ throw new DashManifestCreationException(
+ "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)}.
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generatePeriodElement() {
+ try {
+ getFirstElementByName(MPD).appendChild(createElement(PERIOD));
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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()}.
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateAdaptationSetElement() {
+ try {
+ final Element adaptationSetElement = createElement(ADAPTATION_SET);
+
+ appendNewAttrWithValue(adaptationSetElement, "id", "0");
+
+ appendNewAttrWithValue(
+ adaptationSetElement,
+ "mimeType",
+ itagInfo.getItagFormat().mediaFormat().mimeType());
+
+ appendNewAttrWithValue(adaptationSetElement, "subsegmentAlignment", "true");
+
+ getFirstElementByName(PERIOD).appendChild(adaptationSetElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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)}).
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateRoleElement() {
+ try {
+ final Element roleElement = createElement(ROLE);
+
+ appendNewAttrWithValue(roleElement, "schemeIdUri", "urn:mpeg:DASH:role:2011");
+
+ appendNewAttrWithValue(roleElement, "value", "main");
+
+ getFirstElementByName(ADAPTATION_SET).appendChild(roleElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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()}).
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateRepresentationElement() {
+ try {
+ final Element representationElement = createElement(REPRESENTATION);
+
+ appendNewAttrWithValue(
+ representationElement, "id", itagInfo.getItagFormat().id());
+
+ final String codec = itagInfo.getCodec();
+ if (isNullOrEmpty(codec)) {
+ throw DashManifestCreationException.couldNotAddElement(ADAPTATION_SET,
+ "invalid codec=" + codec);
+ }
+
+ appendNewAttrWithValue(
+ representationElement, "codecs", codec);
+
+ appendNewAttrWithValue(
+ representationElement, "startWithSAP", "1");
+
+ appendNewAttrWithValue(
+ representationElement, "maxPlayoutRate", "1");
+
+ final Integer bitrate = itagInfo.getBitRate();
+ if (bitrate == null || bitrate <= 0) {
+ throw DashManifestCreationException.couldNotAddElement(
+ REPRESENTATION,
+ "invalid bitrate=" + bitrate);
+ }
+
+ appendNewAttrWithValue(
+ representationElement, "bandwidth", bitrate);
+
+ if (itagInfo.getItagFormat() instanceof VideoItagFormat) {
+
+ final VideoQualityData videoQualityData = itagInfo.getCombinedVideoQualityData();
+
+ if (videoQualityData.height() <= 0 && videoQualityData.width() <= 0) {
+ throw DashManifestCreationException.couldNotAddElement(
+ REPRESENTATION,
+ "both width and height are <= 0");
+ }
+
+ if (videoQualityData.width() > 0) {
+ appendNewAttrWithValue(
+ representationElement, "width", videoQualityData.width());
+ }
+
+ appendNewAttrWithValue(
+ representationElement, "height", videoQualityData.height());
+
+ if (videoQualityData.fps() > 0) {
+ appendNewAttrWithValue(
+ representationElement, "frameRate", videoQualityData.fps());
+ }
+ }
+
+ if (itagInfo.getItagFormat() instanceof AudioItagFormat
+ && itagInfo.getAudioSampleRate() != null
+ && itagInfo.getAudioSampleRate() > 0) {
+
+ appendNewAttrWithValue(
+ representationElement,
+ "audioSamplingRate",
+ itagInfo.getAudioSampleRate());
+ }
+
+ getFirstElementByName(ADAPTATION_SET).appendChild(representationElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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 ItagInfo} passed as the second
+ * parameter of this method)
+ *
+ *
+ *
+ * The {@code } element needs to be generated before this element with
+ * {@link #generateRepresentationElement()}).
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateAudioChannelConfigurationElement() {
+ try {
+ final Element audioChannelConfigElement = createElement(AUDIO_CHANNEL_CONFIGURATION);
+
+ appendNewAttrWithValue(
+ audioChannelConfigElement,
+ "schemeIdUri",
+ "urn:mpeg:dash:23003:3:audio_channel_configuration:2011");
+
+ Integer audioChannels = itagInfo.getAudioChannels();
+ if (audioChannels != null && audioChannels <= 0) {
+ // Encountered invalid number of audio channels
+ // Most audio streams have 2 audio channels, so use this value as fallback
+ // see also https://github.com/TeamNewPipe/NewPipeExtractor/pull/859
+ audioChannels = 2;
+ }
+
+ appendNewAttrWithValue(
+ audioChannelConfigElement, "value", audioChannels);
+
+ getFirstElementByName(REPRESENTATION).appendChild(audioChannelConfigElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.couldNotAddElement(AUDIO_CHANNEL_CONFIGURATION, 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()}).
+ *
+ *
+ * @param baseUrl the base URL of the OTF/post-live-DVR stream
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateSegmentTemplateElement(@Nonnull final String baseUrl) {
+ try {
+ final Element segmentTemplateElement = createElement(SEGMENT_TEMPLATE);
+
+ // The first sequence of post DVR streams is the beginning of the video stream and not
+ // an initialization segment
+ appendNewAttrWithValue(
+ segmentTemplateElement, "startNumber", isLiveDelivery() ? "0" : "1");
+
+ appendNewAttrWithValue(
+ segmentTemplateElement, "timescale", "1000");
+
+ // Post-live-DVR/ended livestreams streams don't require an initialization sequence
+ if (!isLiveDelivery()) {
+ appendNewAttrWithValue(
+ segmentTemplateElement, "initialization", baseUrl + SQ_0);
+ }
+
+ appendNewAttrWithValue(
+ segmentTemplateElement, "media", baseUrl + "&sq=$Number$");
+
+ getFirstElementByName(REPRESENTATION).appendChild(segmentTemplateElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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(String)}.
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateSegmentTimelineElement()
+ throws DashManifestCreationException {
+ try {
+ final Element segmentTemplateElement = getFirstElementByName(SEGMENT_TEMPLATE);
+ final Element segmentTimelineElement = createElement(SEGMENT_TIMELINE);
+
+ segmentTemplateElement.appendChild(segmentTimelineElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.couldNotAddElement(SEGMENT_TIMELINE, e);
+ }
+ }
+ // endregion
+
+ // region initResponse
+
+ @SuppressWarnings("checkstyle:FinalParameters")
+ @Nonnull
+ protected Response getInitializationResponse(@Nonnull final String baseStreamingUrl) {
+ final boolean isHtml5StreamingUrl = isWebStreamingUrl(baseStreamingUrl)
+ || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(baseStreamingUrl);
+ final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(baseStreamingUrl);
+ final boolean isIosStreamingUrl = isIosStreamingUrl(baseStreamingUrl);
+
+ String streamingUrl = baseStreamingUrl;
+ if (isHtml5StreamingUrl) {
+ streamingUrl += ALR_YES;
+ }
+ streamingUrl = appendBaseStreamingUrlParams(streamingUrl);
+
+ final Downloader downloader = NewPipe.getDownloader();
+ if (isHtml5StreamingUrl) {
+ return getStreamingWebUrlWithoutRedirects(downloader, streamingUrl);
+ } 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(streamingUrl, headers, emptyBody);
+ } catch (final IOException | ExtractionException e) {
+ throw new DashManifestCreationException("Could not get the "
+ + (isIosStreamingUrl ? "IOS" : "ANDROID") + " streaming URL response", e);
+ }
+ }
+
+ try {
+ return downloader.get(streamingUrl);
+ } catch (final IOException | ExtractionException e) {
+ throw new DashManifestCreationException("Could not get the streaming URL response", e);
+ }
+ }
+
+ @Nonnull
+ protected Response getStreamingWebUrlWithoutRedirects(
+ @Nonnull final Downloader downloader,
+ @Nonnull final String streamingUrl) {
+ try {
+ final Map> headers = new HashMap<>();
+ addClientInfoHeaders(headers);
+
+ String currentStreamingUrl = streamingUrl;
+
+ for (int r = 0; r < MAXIMUM_REDIRECT_COUNT; r++) {
+ final Response response = downloader.get(currentStreamingUrl, headers);
+
+ final int responseCode = response.responseCode();
+ if (responseCode != 200) {
+ throw new DashManifestCreationException(
+ "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.
+ final String responseMimeType =
+ Objects.requireNonNull(
+ response.getHeader("Content-Type"),
+ "Could not get the Content-Type header from the response headers");
+
+ // The response body is not the redirection URL
+ if (!responseMimeType.equals("text/plain")) {
+ return response;
+ }
+
+ currentStreamingUrl = response.responseBody();
+ }
+
+ throw new DashManifestCreationException("Too many redirects");
+
+ } catch (final IOException | ExtractionException e) {
+ throw new DashManifestCreationException(
+ "Could not get the streaming URL response of a HTML5 client", e);
+ }
+ }
+
+ @Nonnull
+ protected String appendBaseStreamingUrlParams(@Nonnull final String baseStreamingUrl) {
+ return baseStreamingUrl + SQ_0 + RN_0;
+ }
+
+ // endregion
+
+ // region document util
+
+ /**
+ * 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.
+ */
+ protected void 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 not
+ // supported by all platforms (like the Android implementation)
+ }
+
+ final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ document = documentBuilder.newDocument();
+ }
+
+ protected Attr appendNewAttrWithValue(
+ final Element baseElement,
+ final String name,
+ final Object value
+ ) {
+ return appendNewAttrWithValue(baseElement, name, String.valueOf(value));
+ }
+
+ protected Attr appendNewAttrWithValue(
+ final Element baseElement,
+ final String name,
+ final String value
+ ) {
+ final Attr attr = document.createAttribute(name);
+ attr.setValue(value);
+ baseElement.setAttributeNode(attr);
+
+ return attr;
+ }
+
+ protected Element getFirstElementByName(final String name) {
+ return (Element) document.getElementsByTagName(name).item(0);
+ }
+
+ protected Element createElement(final String name) {
+ return document.createElement(name);
+ }
+
+ @SuppressWarnings("squid:S2755") // warning is still shown despite applied solution
+ protected String documentToXml() throws TransformerException {
+ /*
+ * 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.
+ */
+ 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 not
+ // 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();
+ }
+
+ protected String documentToXmlSafe() {
+ try {
+ return documentToXml();
+ } catch (final TransformerException e) {
+ throw new DashManifestCreationException("Failed to convert XML-document to string", e);
+ }
+ }
+
+ // endregion
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java
new file mode 100644
index 0000000000..df24ece3b6
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeOtfDashManifestCreator.java
@@ -0,0 +1,158 @@
+package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator;
+
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE;
+import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
+import static org.schabi.newpipe.extractor.utils.Utils.removeNonDigitCharacters;
+
+import org.schabi.newpipe.extractor.downloader.Response;
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import javax.annotation.Nonnull;
+
+public class YoutubeOtfDashManifestCreator extends AbstractYoutubeDashManifestCreator {
+
+ public YoutubeOtfDashManifestCreator(@Nonnull final ItagInfo> itagInfo,
+ final long durationSecondsFallback) {
+ super(itagInfo, durationSecondsFallback);
+ }
+
+ @Nonnull
+ @Override
+ public String generateManifest() {
+ // Try to avoid redirects when streaming the content by saving the last URL we get
+ // from video servers.
+ final Response response = getInitializationResponse(itagInfo.getStreamUrl());
+ final String otfBaseStreamingUrl = response.latestUrl()
+ .replace(SQ_0, "")
+ .replace(RN_0, "")
+ .replace(ALR_YES, "");
+
+ final int responseCode = response.responseCode();
+ if (responseCode != 200) {
+ throw new DashManifestCreationException("Could not get the initialization URL: "
+ + "response code " + responseCode);
+ }
+
+ final String[] segmentDurations;
+
+ 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;
+ segmentDurations = isBlank(segmentsAndDurationsResponseSplit[lastIndex])
+ ? Arrays.copyOf(segmentsAndDurationsResponseSplit, lastIndex)
+ : segmentsAndDurationsResponseSplit;
+ } catch (final Exception e) {
+ throw new DashManifestCreationException("Could not get segment durations", e);
+ }
+
+ long streamDurationMs;
+ try {
+ streamDurationMs = getStreamDuration(segmentDurations);
+ } catch (final DashManifestCreationException e) {
+ streamDurationMs = durationSecondsFallback * 1000;
+ }
+
+ generateDocumentAndCommonElements(streamDurationMs);
+ generateSegmentTemplateElement(otfBaseStreamingUrl);
+ generateSegmentTimelineElement();
+ generateSegmentElementsForOtfStreams(segmentDurations);
+
+ return documentToXmlSafe();
+ }
+
+ /**
+ * 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 #generateSegmentTimelineElement()}.
+ *
+ *
+ * @param segmentDurations the sequences "length" or "length(r=repeat_count" extracted with the
+ * regular expressions
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateSegmentElementsForOtfStreams(@Nonnull final String[] segmentDurations) {
+ try {
+ final Element segmentTimelineElement = getFirstElementByName(SEGMENT_TIMELINE);
+ streamAndSplitSegmentDurations(segmentDurations)
+ .map(segmentLengthRepeat -> {
+ final Element sElement = createElement("S");
+ // There are repetitions of a segment duration in other segments
+ if (segmentLengthRepeat.length > 1) {
+ appendNewAttrWithValue(sElement, "r", Integer.parseInt(
+ removeNonDigitCharacters(segmentLengthRepeat[1])));
+ }
+
+ appendNewAttrWithValue(
+ sElement, "d", Integer.parseInt(segmentLengthRepeat[0]));
+ return sElement;
+ })
+ .forEach(segmentTimelineElement::appendChild);
+ } catch (final Exception e) {
+ throw DashManifestCreationException.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 segmentDurations the segment duration object extracted from the initialization
+ * sequence of the stream
+ * @return the duration of the OTF stream, in milliseconds
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected long getStreamDuration(@Nonnull final String[] segmentDurations) {
+ try {
+ return streamAndSplitSegmentDurations(segmentDurations)
+ .mapToLong(segmentLengthRepeat -> {
+ final long segmentLength = Integer.parseInt(segmentLengthRepeat[0]);
+ final long segmentRepeatCount = segmentLengthRepeat.length > 1
+ ? Long.parseLong(removeNonDigitCharacters(segmentLengthRepeat[1]))
+ : 0;
+ return segmentLength + segmentRepeatCount * segmentLength;
+ })
+ .sum();
+ } catch (final NumberFormatException e) {
+ throw new DashManifestCreationException(
+ "Could not get stream length from sequences list", e);
+ }
+ }
+
+ protected Stream streamAndSplitSegmentDurations(@Nonnull final String[] durations) {
+ return Stream.of(durations)
+ .map(segDuration -> segDuration.split("\\(r="));
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java
new file mode 100644
index 0000000000..1b7c769cf2
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubePostLiveStreamDvrDashManifestCreator.java
@@ -0,0 +1,122 @@
+package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator;
+
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_TIMELINE;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
+import org.schabi.newpipe.extractor.downloader.Response;
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException;
+import org.w3c.dom.Attr;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+public class YoutubePostLiveStreamDvrDashManifestCreator
+ extends AbstractYoutubeDashManifestCreator {
+
+ public YoutubePostLiveStreamDvrDashManifestCreator(@Nonnull final ItagInfo> itagInfo,
+ final long durationSecondsFallback) {
+ super(itagInfo, durationSecondsFallback);
+ }
+
+ @Override
+ protected boolean isLiveDelivery() {
+ return true;
+ }
+
+ @Nonnull
+ @Override
+ public String generateManifest() {
+ final Integer targetDurationSec = itagInfo.getTargetDurationSec();
+ if (targetDurationSec == null || targetDurationSec <= 0) {
+ throw new DashManifestCreationException(
+ "Invalid value for 'targetDurationSec'=" + targetDurationSec);
+ }
+
+ // Try to avoid redirects when streaming the content by saving the latest URL we get
+ // from video servers.
+ final Response response = getInitializationResponse(itagInfo.getStreamUrl());
+ final String realPostLiveStreamDvrStreamingUrl = response.latestUrl()
+ .replace(SQ_0, "")
+ .replace(RN_0, "")
+ .replace(ALR_YES, "");
+
+ final int responseCode = response.responseCode();
+ if (responseCode != 200) {
+ throw new DashManifestCreationException(
+ "Could not get the initialization sequence: response code " + responseCode);
+ }
+
+ final String streamDurationMsString;
+ final String segmentCount;
+ try {
+ final Map> responseHeaders = response.responseHeaders();
+ streamDurationMsString = responseHeaders.get("X-Head-Time-Millis").get(0);
+ segmentCount = responseHeaders.get("X-Head-Seqnum").get(0);
+ } catch (final IndexOutOfBoundsException e) {
+ throw new DashManifestCreationException(
+ "Could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header",
+ e);
+ }
+
+ if (isNullOrEmpty(segmentCount)) {
+ throw new DashManifestCreationException("Could not get the number of segments");
+ }
+
+ long streamDurationMs;
+ try {
+ streamDurationMs = Long.parseLong(streamDurationMsString);
+ } catch (final NumberFormatException e) {
+ streamDurationMs = durationSecondsFallback * 1000;
+ }
+
+ generateDocumentAndCommonElements(streamDurationMs);
+
+ generateSegmentTemplateElement(realPostLiveStreamDvrStreamingUrl);
+ generateSegmentTimelineElement();
+ generateSegmentElementForPostLiveDvrStreams(targetDurationSec, segmentCount);
+
+ return documentToXmlSafe();
+ }
+
+ /**
+ * 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 targetDurationSeconds the {@code targetDurationSec} value from YouTube player
+ * response's stream
+ * @param segmentCount the number of segments
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ private void generateSegmentElementForPostLiveDvrStreams(
+ final int targetDurationSeconds,
+ @Nonnull final String segmentCount
+ ) {
+ try {
+ final Element sElement = document.createElement("S");
+
+ appendNewAttrWithValue(sElement, "d", targetDurationSeconds * 1000);
+
+ final Attr rAttribute = document.createAttribute("r");
+ rAttribute.setValue(segmentCount);
+ sElement.setAttributeNode(rAttribute);
+
+ appendNewAttrWithValue(sElement, "r", segmentCount);
+
+ getFirstElementByName(SEGMENT_TIMELINE).appendChild(sElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.couldNotAddElement("segment (S)", e);
+ }
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java
new file mode 100644
index 0000000000..5e4a5d074f
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreator/YoutubeProgressiveDashManifestCreator.java
@@ -0,0 +1,162 @@
+package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator;
+
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.BASE_URL;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.INITIALIZATION;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.MPD;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.REPRESENTATION;
+import static org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreatorConstants.SEGMENT_BASE;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreationException;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Element;
+
+import javax.annotation.Nonnull;
+
+public class YoutubeProgressiveDashManifestCreator extends AbstractYoutubeDashManifestCreator {
+
+ public YoutubeProgressiveDashManifestCreator(@Nonnull final ItagInfo> itagInfo,
+ final long durationSecondsFallback) {
+ super(itagInfo, durationSecondsFallback);
+ }
+
+ @Nonnull
+ @Override
+ public String generateManifest() {
+ final long streamDurationMs;
+ if (itagInfo.getApproxDurationMs() != null) {
+ streamDurationMs = itagInfo.getApproxDurationMs();
+ } else if (durationSecondsFallback > 0) {
+ streamDurationMs = durationSecondsFallback * 1000;
+ } else {
+ throw DashManifestCreationException.couldNotAddElement(MPD,
+ "unable to determine duration and fallback is invalid");
+ }
+
+ generateDocumentAndCommonElements(streamDurationMs);
+ generateBaseUrlElement(itagInfo.getStreamUrl());
+ generateSegmentBaseElement();
+ generateInitializationElement();
+
+ return documentToXmlSafe();
+ }
+
+ /**
+ * Generate the {@code } element, appended as a child of the
+ * {@code } element.
+ *
+ *
+ * The {@code } element needs to be generated before this element with
+ * {@link #generateRepresentationElement()}).
+ *
+ *
+ * @param baseUrl the base URL of the stream, which must not be null and will be set as the
+ * content of the {@code } element
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateBaseUrlElement(@Nonnull final String baseUrl)
+ throws DashManifestCreationException {
+ try {
+ final Element baseURLElement = createElement(BASE_URL);
+
+ baseURLElement.setTextContent(baseUrl);
+
+ getFirstElementByName(REPRESENTATION).appendChild(baseURLElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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 ItagInfo} passed
+ * as the second parameter)
+ *
+ *
+ *
+ * The {@code } element needs to be generated before this element with
+ * {@link #generateRepresentationElement()}),
+ * and the {@code BaseURL} element with {@link #generateBaseUrlElement(String)}
+ * should be generated too.
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateSegmentBaseElement() {
+ try {
+ final Element segmentBaseElement = createElement(SEGMENT_BASE);
+
+ if (itagInfo.getIndexRange() == null
+ || itagInfo.getIndexRange().start() < 0
+ || itagInfo.getIndexRange().end() < 0) {
+ throw DashManifestCreationException.couldNotAddElement(SEGMENT_BASE,
+ "invalid index-range: " + itagInfo.getIndexRange());
+ }
+
+ appendNewAttrWithValue(
+ segmentBaseElement,
+ "indexRange",
+ itagInfo.getIndexRange().start() + "-" + itagInfo.getIndexRange().end());
+
+ getFirstElementByName(REPRESENTATION).appendChild(segmentBaseElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.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 ItagInfo} passed
+ * as the second parameter)
+ *
+ *
+ *
+ * The {@code } element needs to be generated before this element with
+ * {@link #generateSegmentBaseElement()}).
+ *
+ *
+ * @throws DashManifestCreationException May throw a CreationException
+ */
+ protected void generateInitializationElement() {
+ try {
+ final Element initializationElement = createElement(INITIALIZATION);
+
+ if (itagInfo.getInitRange() == null
+ || itagInfo.getInitRange().start() < 0
+ || itagInfo.getInitRange().end() < 0) {
+ throw DashManifestCreationException.couldNotAddElement(SEGMENT_BASE,
+ "invalid (init)-range: " + itagInfo.getInitRange());
+ }
+
+ appendNewAttrWithValue(
+ initializationElement,
+ "range",
+ itagInfo.getInitRange().start() + "-" + itagInfo.getInitRange().end());
+
+ getFirstElementByName(SEGMENT_BASE).appendChild(initializationElement);
+ } catch (final DOMException e) {
+ throw DashManifestCreationException.couldNotAddElement(INITIALIZATION, e);
+ }
+ }
+
+
+ @Nonnull
+ @Override
+ protected String appendBaseStreamingUrlParams(@Nonnull final String baseStreamingUrl) {
+ return baseStreamingUrl + RN_0;
+ }
+}
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
deleted file mode 100644
index 46f32664b1..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/CreationException.java
+++ /dev/null
@@ -1,63 +0,0 @@
-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
deleted file mode 100644
index 045e5dda49..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeDashManifestCreatorsUtils.java
+++ /dev/null
@@ -1,756 +0,0 @@
-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";
-
- // 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";
- public static final String PERIOD = "Period";
- public static final String ADAPTATION_SET = "AdaptationSet";
- public static final String ROLE = "Role";
- public static final String REPRESENTATION = "Representation";
- public static final String AUDIO_CHANNEL_CONFIGURATION = "AudioChannelConfiguration";
- public static final String SEGMENT_TEMPLATE = "SegmentTemplate";
- public static final String SEGMENT_TIMELINE = "SegmentTimeline";
- public static final String BASE_URL = "BaseURL";
- 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.
- *
- *
- * 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 doc = generateDocumentAndMpdElement(streamDuration);
-
- generatePeriodElement(doc);
- generateAdaptationSetElement(doc, itagItem);
- generateRoleElement(doc);
- generateRepresentationElement(doc, itagItem);
- if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
- generateAudioChannelConfigurationElement(doc, itagItem);
- }
-
- return doc;
- }
-
- /**
- * 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 doc = newDocument();
-
- final Element mpdElement = doc.createElement(MPD);
- doc.appendChild(mpdElement);
-
- 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));
-
- return doc;
- } catch (final Exception e) {
- throw new CreationException(
- "Could not generate the DASH manifest or append the MPD doc 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 doc the {@link Document} on which the the {@code } element will be appended
- */
- public static void generatePeriodElement(@Nonnull final Document doc)
- throws CreationException {
- try {
- 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);
- }
- }
-
- /**
- * 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 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 doc,
- @Nonnull final ItagItem itagItem)
- throws CreationException {
- try {
- final Element periodElement = (Element) doc.getElementsByTagName(PERIOD)
- .item(0);
- final Element adaptationSetElement = doc.createElement(ADAPTATION_SET);
-
- setAttribute(adaptationSetElement, doc, "id", "0");
-
- 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");
- }
-
- setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType());
- setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true");
-
- 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 doc the {@link Document} on which the the {@code } element will be appended
- */
- public static void generateRoleElement(@Nonnull final Document doc)
- throws CreationException {
- try {
- final Element adaptationSetElement = (Element) doc.getElementsByTagName(
- ADAPTATION_SET).item(0);
- final Element roleElement = doc.createElement(ROLE);
-
- setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011");
- setAttribute(roleElement, doc, "value", "main");
-
- 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 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 doc,
- @Nonnull final ItagItem itagItem)
- throws CreationException {
- try {
- final Element adaptationSetElement = (Element) doc.getElementsByTagName(
- ADAPTATION_SET).item(0);
- 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");
- }
- 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");
- }
- 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");
- }
- setAttribute(representationElement, doc, "bandwidth", String.valueOf(bitrate));
-
- 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) {
- throw CreationException.couldNotAddElement(REPRESENTATION,
- "both width and height of the ItagItem are <= 0");
- }
-
- if (width > 0) {
- setAttribute(representationElement, doc, "width", String.valueOf(width));
- }
- setAttribute(representationElement, doc, "height",
- String.valueOf(itagItem.getHeight()));
-
- final int fps = itagItem.getFps();
- if (fps > 0) {
- setAttribute(representationElement, doc, "frameRate", String.valueOf(fps));
- }
- }
-
- if (itagItem.itagType == ItagItem.ItagType.AUDIO && itagItem.getSampleRate() > 0) {
- final Attr audioSamplingRateAttribute = doc.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 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 doc,
- @Nonnull final ItagItem itagItem) throws CreationException {
- try {
- final Element representationElement = (Element) doc.getElementsByTagName(
- REPRESENTATION).item(0);
- final Element audioChannelConfigurationElement = doc.createElement(
- AUDIO_CHANNEL_CONFIGURATION);
-
- setAttribute(audioChannelConfigurationElement, doc, "schemeIdUri",
- "urn:mpeg:dash:23003:3:audio_channel_configuration:2011");
-
- if (itagItem.getAudioChannels() <= 0) {
- throw new CreationException("the number of audioChannels in the ItagItem is <= 0: "
- + itagItem.getAudioChannels());
- }
- setAttribute(audioChannelConfigurationElement, doc, "value",
- String.valueOf(itagItem.getAudioChannels()));
-
- representationElement.appendChild(audioChannelConfigurationElement);
- } catch (final DOMException e) {
- throw CreationException.couldNotAddElement(AUDIO_CHANNEL_CONFIGURATION, e);
- }
- }
-
- /**
- * Convert a DASH manifest {@link Document doc} to a string and cache it.
- *
- * @param originalBaseStreamingUrl the original base URL of the stream
- * @param doc the doc to be converted
- * @param manifestCreatorCache the {@link ManifestCreatorCache} on which store the string
- * generated
- * @return the DASH manifest {@link Document doc} converted to a string
- */
- public static String buildAndCacheResult(
- @Nonnull final String originalBaseStreamingUrl,
- @Nonnull final Document doc,
- @Nonnull final ManifestCreatorCache manifestCreatorCache)
- throws CreationException {
-
- try {
- final String documentXml = documentToXml(doc);
- 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 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 doc,
- @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) doc.getElementsByTagName(
- REPRESENTATION).item(0);
- final Element segmentTemplateElement = doc.createElement(SEGMENT_TEMPLATE);
-
- // The first sequence of post DVR streams is the beginning of the video stream and not
- // an initialization segment
- 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 (deliveryType != DeliveryType.LIVE) {
- setAttribute(segmentTemplateElement, doc, "initialization", baseUrl + SQ_0);
- }
-
- setAttribute(segmentTemplateElement, doc, "media", baseUrl + "&sq=$Number$");
-
- 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 doc the {@link Document} on which the the {@code } element will be
- * appended
- */
- public static void generateSegmentTimelineElement(@Nonnull final Document doc)
- throws CreationException {
- try {
- final Element segmentTemplateElement = (Element) doc.getElementsByTagName(
- SEGMENT_TEMPLATE).item(0);
- final Element segmentTimelineElement = doc.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 = appendRnSqParamsIfNeeded(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 = Collections.singletonMap("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 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 XXE 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 not
- // supported by all platforms (like the Android implementation)
- }
-
- return documentBuilderFactory.newDocumentBuilder().newDocument();
- }
-
- /**
- * 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 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 doc)
- 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 not
- // 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(doc), 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
- */
- @Nonnull
- private static String appendRnSqParamsIfNeeded(@Nonnull final String baseStreamingUrl,
- @Nonnull final DeliveryType deliveryType) {
- return baseStreamingUrl + (deliveryType == DeliveryType.PROGRESSIVE ? "" : SQ_0) + 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:
- *
- * - the {@link #ALR_YES} param is appended to all streaming URLs
- * - if no redirection occurs, the video server will return the streaming data;
- * - 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;
- * - 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}).
- *
- *
- *
- *
- * 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
deleted file mode 100644
index 46e84df1db..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeOtfDashManifestCreator.java
+++ /dev/null
@@ -1,265 +0,0 @@
-package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators;
-
-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.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute;
-import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
-
-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.DOMException;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-import java.util.Arrays;
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-/**
- * 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, "")
- .replace(RN_0, "").replace(ALR_YES, "");
-
- 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 doc = generateDocumentAndDoCommonElementsGeneration(itagItem,
- streamDuration);
-
- generateSegmentTemplateElement(doc, realOtfBaseStreamingUrl, DeliveryType.OTF);
- generateSegmentTimelineElement(doc);
- generateSegmentElementsForOtfStreams(segmentDuration, doc);
-
- return buildAndCacheResult(otfBaseStreamingUrl, doc, OTF_STREAMS_CACHE);
- }
-
- /**
- * @return the cache of DASH manifests generated for OTF streams
- */
- @Nonnull
- 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 doc the {@link Document} on which the {@code } elements will be
- * appended
- */
- private static void generateSegmentElementsForOtfStreams(
- @Nonnull final String[] segmentDurations,
- @Nonnull final Document doc) throws CreationException {
- try {
- final Element segmentTimelineElement = (Element) doc.getElementsByTagName(
- SEGMENT_TIMELINE).item(0);
-
- for (final String segmentDuration : segmentDurations) {
- final Element sElement = doc.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]));
- setAttribute(sElement, doc, "r", String.valueOf(segmentRepeatCount));
- }
- setAttribute(sElement, doc, "d", segmentLengthRepeat[0]);
-
- 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
deleted file mode 100644
index 3a5a7dd23d..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubePostLiveStreamDvrDashManifestCreator.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators;
-
-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.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute;
-import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-
-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.DOMException;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import javax.annotation.Nonnull;
-
-/**
- * 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, "")
- .replace(RN_0, "").replace(ALR_YES, "");
-
- 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 doc = generateDocumentAndDoCommonElementsGeneration(itagItem,
- streamDuration);
-
- generateSegmentTemplateElement(doc, realPostLiveStreamDvrStreamingUrl,
- DeliveryType.LIVE);
- generateSegmentTimelineElement(doc);
- generateSegmentElementForPostLiveDvrStreams(doc, targetDurationSec, segmentCount);
-
- return buildAndCacheResult(postLiveStreamDvrStreamingUrl, doc,
- POST_LIVE_DVR_STREAMS_CACHE);
- }
-
- /**
- * @return the cache of DASH manifests generated for post-live-DVR streams
- */
- @Nonnull
- 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 doc 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 doc,
- final int targetDurationSeconds,
- @Nonnull final String segmentCount) throws CreationException {
- try {
- final Element segmentTimelineElement = (Element) doc.getElementsByTagName(
- SEGMENT_TIMELINE).item(0);
- final Element sElement = doc.createElement("S");
-
- setAttribute(sElement, doc, "d", String.valueOf(targetDurationSeconds * 1000));
- setAttribute(sElement, doc, "r", segmentCount);
-
- 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
deleted file mode 100644
index 0f69895bba..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManifestCreator.java
+++ /dev/null
@@ -1,235 +0,0 @@
-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.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;
-import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute;
-
-/**
- * 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 doc = generateDocumentAndDoCommonElementsGeneration(itagItem,
- streamDuration);
-
- generateBaseUrlElement(doc, progressiveStreamingBaseUrl);
- generateSegmentBaseElement(doc, itagItem);
- generateInitializationElement(doc, itagItem);
-
- return buildAndCacheResult(progressiveStreamingBaseUrl, doc,
- PROGRESSIVE_STREAMS_CACHE);
- }
-
- /**
- * @return the cache of DASH manifests generated for progressive streams
- */
- @Nonnull
- 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 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 doc,
- @Nonnull final String baseUrl)
- throws CreationException {
- try {
- final Element representationElement = (Element) doc.getElementsByTagName(
- REPRESENTATION).item(0);
- final Element baseURLElement = doc.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 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 doc,
- @Nonnull final ItagItem itagItem)
- throws CreationException {
- try {
- final Element representationElement = (Element) doc.getElementsByTagName(
- REPRESENTATION).item(0);
- final Element segmentBaseElement = doc.createElement(SEGMENT_BASE);
-
- 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: " + range);
- }
- setAttribute(segmentBaseElement, doc, "indexRange", range);
-
- 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 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 doc,
- @Nonnull final ItagItem itagItem)
- throws CreationException {
- try {
- final Element segmentBaseElement = (Element) doc.getElementsByTagName(
- SEGMENT_BASE).item(0);
- final Element initializationElement = doc.createElement(INITIALIZATION);
-
- 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: " + range);
- }
- setAttribute(initializationElement, doc, "range", range);
-
- segmentBaseElement.appendChild(initializationElement);
- } catch (final DOMException e) {
- throw CreationException.couldNotAddElement(INITIALIZATION, e);
- }
- }
-}
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
deleted file mode 100644
index c1ac4f5f6d..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/ItagInfo.java
+++ /dev/null
@@ -1,80 +0,0 @@
-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/YoutubeFeedInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java
index a79586e8f4..ba8f4f8d07 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeFeedInfoItemExtractor.java
@@ -4,12 +4,12 @@
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import javax.annotation.Nullable;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
+import javax.annotation.Nullable;
+
public class YoutubeFeedInfoItemExtractor implements StreamInfoItemExtractor {
private final Element entryElement;
@@ -17,13 +17,6 @@ public YoutubeFeedInfoItemExtractor(final Element entryElement) {
this.entryElement = entryElement;
}
- @Override
- public StreamType getStreamType() {
- // It is not possible to determine the stream type using the feed endpoint.
- // All entries are considered a video stream.
- return StreamType.VIDEO_STREAM;
- }
-
@Override
public boolean isAd() {
return false;
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 a5cb04b456..878424dd63 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,8 +20,6 @@
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.ItagItem.CONTENT_LENGTH_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;
@@ -37,6 +35,7 @@
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;
+import static org.schabi.newpipe.extractor.utils.JsonUtils.getNullableInteger;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonArray;
@@ -46,7 +45,6 @@
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptableObject;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.StreamingService;
@@ -65,21 +63,41 @@
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.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.dashmanifestcreator.YoutubeOtfDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubePostLiveStreamDvrDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreator.YoutubeProgressiveDashManifestCreator;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.BaseAudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.registry.ItagFormatRegistry;
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfo;
+import org.schabi.newpipe.extractor.services.youtube.itag.info.builder.ItagInfoRangeHelper;
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;
+import org.schabi.newpipe.extractor.stream.Privacy;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamSegment;
-import org.schabi.newpipe.extractor.stream.StreamType;
-import org.schabi.newpipe.extractor.stream.SubtitlesStream;
-import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.delivery.DeliveryData;
+import org.schabi.newpipe.extractor.streamdata.delivery.dashmanifestcreator.DashManifestCreator;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleDASHManifestDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleHLSDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.delivery.simpleimpl.SimpleProgressiveHTTPDeliveryDataImpl;
+import org.schabi.newpipe.extractor.streamdata.format.registry.SubtitleFormatRegistry;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoStream;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleAudioStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleSubtitleStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoAudioStreamImpl;
+import org.schabi.newpipe.extractor.streamdata.stream.simpleimpl.SimpleVideoStreamImpl;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Pair;
import org.schabi.newpipe.extractor.utils.Parser;
@@ -97,7 +115,9 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.function.BiFunction;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -139,7 +159,9 @@ public static class DeobfuscateException extends ParsingException {
private JsonObject videoSecondaryInfoRenderer;
private JsonObject playerMicroFormatRenderer;
private int ageLimit = -1;
- private StreamType streamType;
+
+ private Boolean isLive;
+ private Boolean isPostLive;
// We need to store the contentPlaybackNonces because we need to append them to videoplayback
// URLs (with the cpn parameter).
@@ -198,7 +220,7 @@ public String getTextualUploadDate() throws ParsingException {
} else if (!liveDetails.getString("startTimestamp", "").isEmpty()) {
// a running live stream
return liveDetails.getString("startTimestamp");
- } else if (getStreamType() == StreamType.LIVE_STREAM) {
+ } else if (isLive()) {
// this should never be reached, but a live stream without upload date is valid
return null;
}
@@ -352,23 +374,22 @@ public long getLength() throws ParsingException {
}
}
- private int getDurationFromFirstAdaptiveFormat(@Nonnull final List streamingDatas)
+ private int getDurationFromFirstAdaptiveFormat(@Nonnull final List streamingData)
throws ParsingException {
- for (final JsonObject streamingData : streamingDatas) {
- final JsonArray adaptiveFormats = streamingData.getArray(ADAPTIVE_FORMATS);
- if (adaptiveFormats.isEmpty()) {
- continue;
- }
-
- final String durationMs = adaptiveFormats.getObject(0)
- .getString("approxDurationMs");
- try {
- return Math.round(Long.parseLong(durationMs) / 1000f);
- } catch (final NumberFormatException ignored) {
- }
- }
-
- throw new ParsingException("Could not get duration");
+ return streamingData.stream()
+ .map(s -> s.getArray(ADAPTIVE_FORMATS))
+ .filter(af -> !af.isEmpty())
+ .map(af -> af.getObject(0).getString("approxDurationMs"))
+ .map(durationMs -> {
+ try {
+ return Math.round(Long.parseLong(durationMs) / 1000f);
+ } catch (final NumberFormatException ignored) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseThrow(() -> new ParsingException("Could not get duration"));
}
/**
@@ -549,31 +570,31 @@ public String getDashMpdUrl() throws ParsingException {
// There is no DASH manifest available in the iOS clients and the DASH manifest of the
// Android client doesn't contain all available streams (mainly the WEBM ones)
return getManifestUrl(
- "dash",
+ "dashManifestUrl",
Arrays.asList(html5StreamingData, androidStreamingData));
}
@Nonnull
@Override
- public String getHlsUrl() throws ParsingException {
+ public String getHlsMasterPlaylistUrl() throws ParsingException {
assertPageFetched();
// Return HLS manifest of the iOS client first because on livestreams, the HLS manifest
// returned has separated audio and video streams
// Also, on videos, non-iOS clients don't have an HLS manifest URL in their player response
return getManifestUrl(
- "hls",
+ "hlsManifestUrl",
Arrays.asList(iosStreamingData, html5StreamingData, androidStreamingData));
}
@Nonnull
- private static String getManifestUrl(@Nonnull final String manifestType,
- @Nonnull final List streamingDataObjects) {
- final String manifestKey = manifestType + "ManifestUrl";
-
+ private static String getManifestUrl(
+ @Nonnull final String manifestKey,
+ @Nonnull final List streamingDataObjects
+ ) {
return streamingDataObjects.stream()
.filter(Objects::nonNull)
- .map(streamingDataObject -> streamingDataObject.getString(manifestKey))
+ .map(streamingData -> streamingData.getString(manifestKey))
.filter(Objects::nonNull)
.findFirst()
.orElse("");
@@ -581,23 +602,39 @@ private static String getManifestUrl(@Nonnull final String manifestType,
@Override
public List getAudioStreams() throws ExtractionException {
- assertPageFetched();
- return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.AUDIO,
- getAudioStreamBuilderHelper(), "audio");
+ return buildStrems(
+ ADAPTIVE_FORMATS,
+ ItagFormatRegistry.AUDIO_FORMATS,
+ (itagInfo, deliveryData) -> new SimpleAudioStreamImpl(
+ itagInfo.getItagFormat().mediaFormat(),
+ deliveryData,
+ itagInfo.getCombinedAverageBitrate()),
+ "audio");
}
@Override
- public List getVideoStreams() throws ExtractionException {
- assertPageFetched();
- return getItags(FORMATS, ItagItem.ItagType.VIDEO,
- getVideoStreamBuilderHelper(false), "video");
+ public List getVideoStreams() throws ExtractionException {
+ return buildStrems(
+ FORMATS,
+ ItagFormatRegistry.VIDEO_AUDIO_FORMATS,
+ (itagInfo, deliveryData) -> new SimpleVideoAudioStreamImpl(
+ itagInfo.getItagFormat().mediaFormat(),
+ deliveryData,
+ itagInfo.getCombinedVideoQualityData(),
+ itagInfo.getCombinedAverageBitrate()),
+ "video");
}
@Override
public List getVideoOnlyStreams() throws ExtractionException {
- assertPageFetched();
- return getItags(ADAPTIVE_FORMATS, ItagItem.ItagType.VIDEO_ONLY,
- getVideoStreamBuilderHelper(true), "video-only");
+ return buildStrems(
+ ADAPTIVE_FORMATS,
+ ItagFormatRegistry.VIDEO_FORMATS,
+ (itagInfo, deliveryData) -> new SimpleVideoStreamImpl(
+ itagInfo.getItagFormat().mediaFormat(),
+ deliveryData,
+ itagInfo.getCombinedVideoQualityData()),
+ "video-only");
}
/**
@@ -621,63 +658,43 @@ private String tryDecryptUrl(final String streamingUrl, final String videoId) {
@Override
@Nonnull
- public List getSubtitlesDefault() throws ParsingException {
- return getSubtitles(MediaFormat.TTML);
- }
-
- @Override
- @Nonnull
- public List getSubtitles(final MediaFormat format) throws ParsingException {
+ public List getSubtitles() throws ParsingException {
assertPageFetched();
- // 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");
-
- for (int i = 0; i < captionsArray.size(); i++) {
- final String languageCode = captionsArray.getObject(i).getString("languageCode");
- final String baseUrl = captionsArray.getObject(i).getString("baseUrl");
- final String vssId = captionsArray.getObject(i).getString("vssId");
-
- if (languageCode != null && baseUrl != null && vssId != null) {
- final boolean isAutoGenerated = vssId.startsWith("a.");
- final String cleanUrl = baseUrl
- // Remove preexisting format if exists
- .replaceAll("&fmt=[^&]*", "")
- // Remove translation language
- .replaceAll("&tlang=[^&]*", "");
-
- subtitlesToReturn.add(new SubtitlesStream.Builder()
- .setContent(cleanUrl + "&fmt=" + format.getSuffix(), true)
- .setMediaFormat(format)
- .setLanguageCode(languageCode)
- .setAutoGenerated(isAutoGenerated)
- .build());
- }
- }
-
- return subtitlesToReturn;
+ return playerResponse
+ .getObject("captions")
+ .getObject("playerCaptionsTracklistRenderer")
+ .getArray("captionTracks")
+ .stream()
+ .filter(JsonObject.class::isInstance)
+ .map(JsonObject.class::cast)
+ .filter(jsObj -> jsObj.getString("languageCode") != null
+ && jsObj.getString("baseUrl") != null
+ && jsObj.getString("vssId") != null)
+ .map(jsObj -> new SimpleSubtitleStreamImpl(
+ SubtitleFormatRegistry.TTML,
+ new SimpleProgressiveHTTPDeliveryDataImpl(jsObj.getString("baseUrl")
+ // Remove preexisting format if exists
+ .replaceAll("&fmt=[^&]*", "")
+ // Remove translation language
+ .replaceAll("&tlang=[^&]*", "")),
+ jsObj.getString("vssId").startsWith("a."),
+ jsObj.getString("languageCode")
+ ))
+ .collect(Collectors.toList());
}
@Override
- public StreamType getStreamType() {
+ public boolean isLive() {
assertPageFetched();
- return streamType;
+ return isLive;
}
- private void setStreamType() {
- 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;
- }
+ public boolean isPostLive() {
+ assertPageFetched();
+
+ return isPostLive;
}
@Nullable
@@ -793,8 +810,6 @@ public void onFetchPage(@Nonnull final Downloader downloader)
final boolean isAgeRestricted = playabilityStatus.getString("reason", "")
.contains("age");
- setStreamType();
-
if (!playerResponse.has(STREAMING_DATA)) {
try {
fetchTvHtml5EmbedJsonPlayer(contentCountry, localization, videoId);
@@ -802,9 +817,12 @@ public void onFetchPage(@Nonnull final Downloader downloader)
}
}
+ isLive = playerResponse.getObject("playabilityStatus").has("liveStreamability");
+ isPostLive = !isLive
+ && playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false);
+
// 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);
@@ -829,10 +847,8 @@ public void onFetchPage(@Nonnull final Downloader downloader)
.getBytes(StandardCharsets.UTF_8);
nextResponse = getJsonPostResponse(NEXT, body, localization);
- // streamType can only have LIVE_STREAM, POST_LIVE_STREAM and VIDEO_STREAM values (see
- // setStreamType()), so this block will be run only for POST_LIVE_STREAM and VIDEO_STREAM
- // values if fetching of the ANDROID client is not forced
- if ((!isAgeRestricted && streamType != StreamType.LIVE_STREAM)
+ // this will only be run for post-live and normal streams
+ if ((!isAgeRestricted && !isLive)
|| isAndroidClientFetchForced) {
try {
fetchAndroidMobileJsonPlayer(contentCountry, localization, videoId);
@@ -842,7 +858,7 @@ public void onFetchPage(@Nonnull final Downloader downloader)
}
}
- if ((!isAgeRestricted && streamType == StreamType.LIVE_STREAM)
+ if ((!isAgeRestricted && isLive)
|| isIosClientFetchForced) {
try {
fetchIosMobileJsonPlayer(contentCountry, localization, videoId);
@@ -1194,195 +1210,147 @@ private JsonObject getVideoSecondaryInfoRenderer() throws ParsingException {
}
@Nonnull
- private List getItags(
+ private ,
+ I extends ItagFormat>> List buildStrems(
final String streamingDataKey,
- final ItagItem.ItagType itagTypeWanted,
- final java.util.function.Function streamBuilderHelper,
- final String streamTypeExceptionMessage) throws ParsingException {
+ final I[] itagFormats,
+ final BiFunction, DeliveryData, T> streamBuilder,
+ final String streamTypeExceptionMessage
+ ) throws ParsingException {
+ assertPageFetched();
try {
- final String videoId = getId();
- final List streamList = new ArrayList<>();
+ if (html5StreamingData == null
+ && androidStreamingData == null
+ && iosStreamingData == null) {
+ return Collections.emptyList();
+ }
- java.util.stream.Stream.of(
+ final List> streamingDataAndCpnLoopList = Arrays.asList(
// 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
+ // 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 streamingDataAndCpnLoopList.stream()
+ .flatMap(p -> buildItagInfoForItags(
+ p.getFirst(),
+ streamingDataKey,
+ itagFormats,
+ p.getSecond()))
+ .map(i -> streamBuilder.apply(i, buildDeliveryData(i)))
+ .collect(Collectors.toList());
- return streamList;
} catch (final Exception e) {
throw new ParsingException(
"Could not get " + streamTypeExceptionMessage + " streams", e);
}
}
- /**
- * 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
- * {@link AudioStream}s built:
+ /*
+ * Note: We build the manifests for YT ourself because the provided ones (according to AudricV)
*
- * - 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.
+ * - aren't working https://github.com/google/ExoPlayer/issues/2422#issuecomment-283080031
+ *
+ * - causes memory problems; TransactionTooLargeException: data parcel size 3174340
+ * - are not always returned, only for videos with OTF streams, or on (ended or not)
+ * livestreams
+ * - Instead of downloading a 10MB manifest when you can generate one which is 1 or 2MB
+ * large
+ * - Also, this manifest isn't used at all by modern YouTube clients.
*
- *
- *
- *
- * Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
- *
- *
- * @return a stream builder helper to build {@link AudioStream}s
*/
@Nonnull
- 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);
- }
+ private > DeliveryData buildDeliveryData(final ItagInfo itagInfo) {
+ final ItagFormatDeliveryData iDeliveryData = itagInfo.getItagFormat().deliveryData();
+ if (iDeliveryData instanceof ProgressiveHTTPItagFormatDeliveryData) {
+ return new SimpleProgressiveHTTPDeliveryDataImpl(itagInfo.getStreamUrl());
+ } else if (iDeliveryData instanceof HLSItagFormatDeliveryData) {
+ return new SimpleHLSDeliveryDataImpl(itagInfo.getStreamUrl());
+ }
- return builder.build();
- };
- }
+ // DASH
+ // Duration in seconds used as fallback inside the dashManifestCreators
+ long durationInSec;
+ try {
+ durationInSec = getLength();
+ } catch (final ParsingException e) {
+ durationInSec = -1;
+ }
- /**
- * 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
- * {@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:
- *
- * - the height returned by the {@link ItagItem} + {@code p} + the frame rate if
- * it is more than 30;
- * - the default resolution string from the {@link ItagItem};
- * - an empty string.
- *
- *
- * - 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 stream builder helper will set the video
- * streams as video-only streams
- * @return a stream builder helper to build {@link VideoStream}s
- */
- @Nonnull
- private java.util.function.Function getVideoStreamBuilderHelper(
- final boolean areStreamsVideoOnly) {
- 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
- : "");
-
- 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 new SimpleDASHManifestDeliveryDataImpl(
+ getDashManifestCreatorConstructor(itagInfo).apply(itagInfo, durationInSec));
+ }
- return builder.build();
- };
+ private BiFunction, Long, DashManifestCreator> getDashManifestCreatorConstructor(
+ final ItagInfo> itagInfo
+ ) {
+ if ("FORMAT_STREAM_TYPE_OTF".equalsIgnoreCase(itagInfo.getType())) {
+ return YoutubeOtfDashManifestCreator::new;
+ } else if (isPostLive()) {
+ return YoutubePostLiveStreamDvrDashManifestCreator::new;
+ }
+ return YoutubeProgressiveDashManifestCreator::new;
}
@Nonnull
- private java.util.stream.Stream getStreamsFromStreamingDataKey(
- final String videoId,
+ private > Stream> buildItagInfoForItags(
final JsonObject streamingData,
final String streamingDataKey,
- @Nonnull final ItagItem.ItagType itagTypeWanted,
+ final I[] itagFormats,
@Nonnull final String contentPlaybackNonce) {
if (streamingData == null || !streamingData.has(streamingDataKey)) {
- return java.util.stream.Stream.empty();
+ return Stream.of();
}
- return streamingData.getArray(streamingDataKey).stream()
+ final String videoId;
+ try {
+ videoId = getId();
+ } catch (final ParsingException ignored) {
+ return Stream.of();
+ }
+ return streamingData.getArray(streamingDataKey)
+ .stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
+ .filter(formatData ->
+ ItagFormatRegistry.isSupported(
+ itagFormats,
+ formatData.getInt("itag")))
.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
+ final I itagFormat = ItagFormatRegistry.getById(
+ itagFormats,
+ formatData.getInt("itag"));
+
+ return buildItagInfo(videoId, formatData, itagFormat, contentPlaybackNonce);
+ } catch (final Exception ignored) {
+ return null;
}
- return null;
})
.filter(Objects::nonNull);
}
- private ItagInfo buildAndAddItagInfoToList(
+ private > ItagInfo buildItagInfo(
@Nonnull final String videoId,
@Nonnull final JsonObject formatData,
- @Nonnull final ItagItem itagItem,
- @Nonnull final ItagItem.ItagType itagType,
- @Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
+ @Nonnull final I itagFormat,
+ @Nonnull final String contentPlaybackNonce
+ ) throws ParsingException {
+
+ // Build url
String streamUrl;
if (formatData.has("url")) {
streamUrl = formatData.getString("url");
} else {
// This url has an obfuscated signature
- final String cipherString = formatData.has(CIPHER)
+ final Map cipher = Parser.compatParseMap(formatData.has(CIPHER)
? formatData.getString(CIPHER)
- : formatData.getString(SIGNATURE_CIPHER);
- final Map cipher = Parser.compatParseMap(
- cipherString);
+ : formatData.getString(SIGNATURE_CIPHER));
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+ deobfuscateSignature(cipher.get("s"));
}
@@ -1393,59 +1361,55 @@ private ItagInfo buildAndAddItagInfoToList(
// Decrypt the n parameter if it is present
streamUrl = tryDecryptUrl(streamUrl, videoId);
- final JsonObject initRange = formatData.getObject("initRange");
- final JsonObject indexRange = formatData.getObject("indexRange");
+ final ItagInfo itagInfo = new ItagInfo<>(itagFormat, streamUrl);
+
+ if (itagFormat instanceof BaseAudioItagFormat) {
+ final Integer averageBitrate = getNullableInteger(formatData, "averageBitrate");
+ if (averageBitrate != null) {
+ itagInfo.setAverageBitrate((int) Math.round(averageBitrate / 1000d));
+ }
+ // YouTube returns the audio sample rate as a string
+ try {
+ itagInfo.setAudioSampleRate(
+ Integer.parseInt(formatData.getString("audioSampleRate")));
+ } catch (final Exception ignore) {
+ // Ignore errors - leave default value
+ }
+ itagInfo.setAudioChannels(getNullableInteger(formatData, "audioChannels"));
+ }
+ if (itagFormat instanceof VideoItagFormat) {
+ itagInfo.setHeight(getNullableInteger(formatData, "height"));
+ itagInfo.setWidth(getNullableInteger(formatData, "width"));
+ itagInfo.setFps(getNullableInteger(formatData, "fps"));
+ }
+
+ itagInfo.setBitRate(getNullableInteger(formatData, "bitrate"));
+ itagInfo.setQuality(formatData.getString("quality"));
+
final String mimeType = formatData.getString("mimeType", "");
- final String codec = mimeType.contains("codecs")
- ? mimeType.split("\"")[1] : "";
-
- 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"));
- } 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",
- // Most audio streams have two audio channels, so use this value if the real
- // count cannot be extracted
- // Doing this prevents an exception when generating the
- // AudioChannelConfiguration element of DASH manifests of audio streams in
- // YoutubeDashManifestCreatorUtils
- 2));
- }
-
- // 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);
-
- if (streamType == StreamType.VIDEO_STREAM) {
- itagInfo.setIsUrl(!formatData.getString("type", "")
- .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);
+ itagInfo.setCodec(mimeType.contains("codecs")
+ ? mimeType.split("\"")[1]
+ : null);
+
+ itagInfo.setInitRange(ItagInfoRangeHelper.buildFrom(formatData.getObject("initRange")));
+ itagInfo.setIndexRange(ItagInfoRangeHelper.buildFrom(formatData.getObject("indexRange")));
+
+ // YouTube returns the content length and the approximate duration as strings
+ try {
+ itagInfo.setContentLength(
+ Long.parseLong(formatData.getString("contentLength")));
+ } catch (final Exception ignored) {
+ // Ignore errors - leave default value
}
+ try {
+ itagInfo.setApproxDurationMs(
+ Long.parseLong(formatData.getString("approxDurationMs")));
+ } catch (final Exception ignored) {
+ // Ignore errors - leave default value
+ }
+
+ itagInfo.setType(formatData.getString("type"));
+ itagInfo.setTargetDurationSec(getNullableInteger(formatData, "targetDurationSec"));
return itagInfo;
}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
index d731729519..239b254a63 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamInfoItemExtractor.java
@@ -14,7 +14,6 @@
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
-import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
@@ -46,7 +45,7 @@
public class YoutubeStreamInfoItemExtractor implements StreamInfoItemExtractor {
private final JsonObject videoInfo;
private final TimeAgoParser timeAgoParser;
- private StreamType cachedStreamType;
+ private Boolean cachedIsLive;
/**
* Creates an extractor of StreamInfoItems from a YouTube page.
@@ -61,19 +60,23 @@ public YoutubeStreamInfoItemExtractor(final JsonObject videoInfoItem,
}
@Override
- public StreamType getStreamType() {
- if (cachedStreamType != null) {
- return cachedStreamType;
+ public boolean isLive() {
+ if (cachedIsLive != null) {
+ return cachedIsLive;
}
+ cachedIsLive = determineIfIsLive();
+ return cachedIsLive;
+ }
+
+ private boolean determineIfIsLive() {
final JsonArray badges = videoInfo.getArray("badges");
for (final Object badge : badges) {
final JsonObject badgeRenderer
= ((JsonObject) badge).getObject("metadataBadgeRenderer");
- if (badgeRenderer.getString("style", "").equals("BADGE_STYLE_TYPE_LIVE_NOW")
- || badgeRenderer.getString("label", "").equals("LIVE NOW")) {
- cachedStreamType = StreamType.LIVE_STREAM;
- return cachedStreamType;
+ if ("BADGE_STYLE_TYPE_LIVE_NOW".equals(badgeRenderer.getString("style"))
+ || "LIVE NOW".equals(badgeRenderer.getString("label"))) {
+ return true;
}
}
@@ -82,13 +85,11 @@ public StreamType getStreamType() {
.getObject("thumbnailOverlayTimeStatusRenderer")
.getString("style", "");
if (style.equalsIgnoreCase("LIVE")) {
- cachedStreamType = StreamType.LIVE_STREAM;
- return cachedStreamType;
+ return true;
}
}
- cachedStreamType = StreamType.VIDEO_STREAM;
- return cachedStreamType;
+ return false;
}
@Override
@@ -118,7 +119,7 @@ public String getName() throws ParsingException {
@Override
public long getDuration() throws ParsingException {
- if (getStreamType() == StreamType.LIVE_STREAM || isPremiere()) {
+ if (isLive() || isPremiere()) {
return -1;
}
@@ -212,7 +213,7 @@ public boolean isUploaderVerified() throws ParsingException {
@Nullable
@Override
public String getTextualUploadDate() throws ParsingException {
- if (getStreamType().equals(StreamType.LIVE_STREAM)) {
+ if (isLive()) {
return null;
}
@@ -232,7 +233,7 @@ public String getTextualUploadDate() throws ParsingException {
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
- if (getStreamType().equals(StreamType.LIVE_STREAM)) {
+ if (isLive()) {
return null;
}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java
new file mode 100644
index 0000000000..26df3ff3c7
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/DASHItagFormatDeliveryData.java
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery;
+
+public interface DASHItagFormatDeliveryData extends ItagFormatDeliveryData {
+ // Just a marker interface for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java
new file mode 100644
index 0000000000..aed19acaa2
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/HLSItagFormatDeliveryData.java
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery;
+
+public interface HLSItagFormatDeliveryData extends ItagFormatDeliveryData {
+ // Just a marker interface for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java
new file mode 100644
index 0000000000..c66bb41736
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ItagFormatDeliveryData.java
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery;
+
+public interface ItagFormatDeliveryData {
+ // Just a marker interface
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java
new file mode 100644
index 0000000000..825f702f5c
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/ProgressiveHTTPItagFormatDeliveryData.java
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery;
+
+public interface ProgressiveHTTPItagFormatDeliveryData extends ItagFormatDeliveryData {
+ // Just a marker interface for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java
new file mode 100644
index 0000000000..6f41ac7ad8
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleDASHItagFormatDeliveryData.java
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.DASHItagFormatDeliveryData;
+
+public class SimpleDASHItagFormatDeliveryData implements DASHItagFormatDeliveryData {
+ // Just a marker for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java
new file mode 100644
index 0000000000..8fedc97336
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleHLSItagFormatDeliveryData.java
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData;
+
+public class SimpleHLSItagFormatDeliveryData implements HLSItagFormatDeliveryData {
+ // Just a marker for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java
new file mode 100644
index 0000000000..70f90a8e3d
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleItagDeliveryDataBuilder.java
@@ -0,0 +1,23 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.DASHItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.HLSItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData;
+
+public final class SimpleItagDeliveryDataBuilder {
+ private SimpleItagDeliveryDataBuilder() {
+ // No impl
+ }
+
+ public static ProgressiveHTTPItagFormatDeliveryData progressiveHTTP() {
+ return new SimpleProgressiveHTTPItagFormatDeliveryData();
+ }
+
+ public static HLSItagFormatDeliveryData hls() {
+ return new SimpleHLSItagFormatDeliveryData();
+ }
+
+ public static DASHItagFormatDeliveryData dash() {
+ return new SimpleDASHItagFormatDeliveryData();
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java
new file mode 100644
index 0000000000..db805f874a
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/delivery/simpleimpl/SimpleProgressiveHTTPItagFormatDeliveryData.java
@@ -0,0 +1,8 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ProgressiveHTTPItagFormatDeliveryData;
+
+public class SimpleProgressiveHTTPItagFormatDeliveryData
+ implements ProgressiveHTTPItagFormatDeliveryData {
+ // Just a marker for now
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java
new file mode 100644
index 0000000000..e4268a24f5
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/AudioItagFormat.java
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format;
+
+import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat;
+
+public interface AudioItagFormat extends ItagFormat, BaseAudioItagFormat {
+ // Nothing additional to implement
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java
new file mode 100644
index 0000000000..d1faef556f
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/BaseAudioItagFormat.java
@@ -0,0 +1,8 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format;
+
+public interface BaseAudioItagFormat {
+ /**
+ * Average audio bitrate in KBit/s
+ */
+ int averageBitrate();
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java
new file mode 100644
index 0000000000..dc9136ee02
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/ItagFormat.java
@@ -0,0 +1,21 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.streamdata.format.MediaFormat;
+
+import javax.annotation.Nonnull;
+
+public interface ItagFormat {
+ int id();
+
+ /**
+ * The (container) media format, e.g. mp3 for audio streams or webm for video(+audio) streams.
+ *
+ * @return The (container) media format
+ */
+ @Nonnull
+ M mediaFormat();
+
+ @Nonnull
+ ItagFormatDeliveryData deliveryData();
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java
new file mode 100644
index 0000000000..396cd51d4a
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoAudioItagFormat.java
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format;
+
+public interface VideoAudioItagFormat extends VideoItagFormat, BaseAudioItagFormat {
+ // Nothing additional to implement
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java
new file mode 100644
index 0000000000..366b351700
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/VideoItagFormat.java
@@ -0,0 +1,11 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format;
+
+import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+
+import javax.annotation.Nonnull;
+
+public interface VideoItagFormat extends ItagFormat {
+ @Nonnull
+ VideoQualityData videoQualityData();
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java
new file mode 100644
index 0000000000..030230ee0a
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/registry/ItagFormatRegistry.java
@@ -0,0 +1,145 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format.registry;
+
+import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.dash;
+import static org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder.hls;
+import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.MPEG_4;
+import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.V3GPP;
+import static org.schabi.newpipe.extractor.streamdata.format.registry.VideoAudioFormatRegistry.WEBM;
+import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeight;
+import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightFps;
+import static org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData.fromHeightWidth;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.format.AudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoAudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleAudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleVideoAudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl.SimpleVideoItagFormat;
+import org.schabi.newpipe.extractor.streamdata.format.registry.AudioFormatRegistry;
+
+import java.util.stream.Stream;
+
+// CHECKSTYLE:OFF - Link is too long
+/**
+ * A registry that contains all supported YouTube itags.
+ *
+ * For additional information you may also check:
+ *
+ * - https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1195
+ * - https://gist.github.com/AgentOak/34d47c65b1d28829bb17c24c04a0096f
+ *
+ */
+// CHECKSTYLE:ON
+public final class ItagFormatRegistry {
+
+ public static final VideoAudioItagFormat[] VIDEO_AUDIO_FORMATS = new VideoAudioItagFormat[]{
+ // v-- Video-codec: mp4v; Audio-codec: aac --v
+ new SimpleVideoAudioItagFormat(17, V3GPP, fromHeightWidth(144, 176), 24),
+ // v-- Video-codec: h264; Audio-codec: aac --v
+ new SimpleVideoAudioItagFormat(18, MPEG_4, fromHeightWidth(360, 640), 96),
+ new SimpleVideoAudioItagFormat(22, MPEG_4, fromHeightWidth(720, 1280), 192),
+
+ // Note: According to yt-dl Itag 34 and 35 are flv-files
+ new SimpleVideoAudioItagFormat(34, MPEG_4, fromHeightWidth(360, 640), 128),
+ new SimpleVideoAudioItagFormat(35, MPEG_4, fromHeightWidth(480, 854), 128),
+
+ // Itag 36 is no longer used because the height is unstable and it's not returned by YT
+ // CHECKSTYLE:OFF - Link is too long
+ // see also: https://github.com/ytdl-org/youtube-dl/blob/9aa8e5340f3d5ece372b983f8e399277ca1f1fe4/youtube_dl/extractor/youtube.py#L1204
+ // CHECKSTYLE:ON
+ new SimpleVideoAudioItagFormat(37, MPEG_4, fromHeightWidth(1080, 1920), 192),
+ new SimpleVideoAudioItagFormat(38, MPEG_4, fromHeightWidth(3072, 4092), 192),
+
+ // v-- Video-codec: vp8; Audio-codec: vorbis --v
+ new SimpleVideoAudioItagFormat(43, WEBM, fromHeightWidth(360, 640), 128),
+ new SimpleVideoAudioItagFormat(44, WEBM, fromHeightWidth(480, 854), 128),
+ new SimpleVideoAudioItagFormat(45, WEBM, fromHeightWidth(720, 1280), 192),
+ new SimpleVideoAudioItagFormat(46, WEBM, fromHeightWidth(1080, 1920), 192),
+
+ // HLS (used for live streaming)
+ // v-- Video-codec: h264; Audio-codec: acc --v
+ new SimpleVideoAudioItagFormat(91, MPEG_4, fromHeight(144), 48, hls()),
+ new SimpleVideoAudioItagFormat(92, MPEG_4, fromHeight(240), 48, hls()),
+ new SimpleVideoAudioItagFormat(93, MPEG_4, fromHeight(360), 128, hls()),
+ new SimpleVideoAudioItagFormat(94, MPEG_4, fromHeight(480), 128, hls()),
+ new SimpleVideoAudioItagFormat(95, MPEG_4, fromHeight(720), 256, hls()),
+ new SimpleVideoAudioItagFormat(96, MPEG_4, fromHeight(1080), 256, hls()),
+ new SimpleVideoAudioItagFormat(132, MPEG_4, fromHeight(240), 48, hls()),
+ new SimpleVideoAudioItagFormat(151, MPEG_4, fromHeight(72), 24, hls())
+ };
+
+ public static final AudioItagFormat[] AUDIO_FORMATS = new AudioItagFormat[] {
+ // DASH MP4 audio
+ // v-- Audio-codec: aac --v
+ new SimpleAudioItagFormat(139, AudioFormatRegistry.M4A, 48, dash()),
+ new SimpleAudioItagFormat(140, AudioFormatRegistry.M4A, 128, dash()),
+ new SimpleAudioItagFormat(141, AudioFormatRegistry.M4A, 256, dash()),
+
+ // DASH WEBM audio
+ // v-- Audio-codec: vorbis --v
+ new SimpleAudioItagFormat(171, AudioFormatRegistry.WEBMA, 128, dash()),
+ new SimpleAudioItagFormat(172, AudioFormatRegistry.WEBMA, 256, dash()),
+
+ // DASH WEBM audio with opus inside
+ // v-- Audio-codec: opus --v
+ new SimpleAudioItagFormat(249, AudioFormatRegistry.WEBMA_OPUS, 50, dash()),
+ new SimpleAudioItagFormat(250, AudioFormatRegistry.WEBMA_OPUS, 70, dash()),
+ new SimpleAudioItagFormat(251, AudioFormatRegistry.WEBMA_OPUS, 160, dash())
+ };
+
+ public static final VideoItagFormat[] VIDEO_FORMATS = new VideoItagFormat[] {
+ // DASH MP4 video
+ // v-- Video-codec: h264 --v
+ new SimpleVideoItagFormat(133, MPEG_4, fromHeight(240), dash()),
+ new SimpleVideoItagFormat(134, MPEG_4, fromHeight(360), dash()),
+ new SimpleVideoItagFormat(135, MPEG_4, fromHeight(480), dash()),
+ new SimpleVideoItagFormat(136, MPEG_4, fromHeight(720), dash()),
+ new SimpleVideoItagFormat(137, MPEG_4, fromHeight(1080), dash()),
+ // Itag 138 has an unknown height and is ignored
+ new SimpleVideoItagFormat(160, MPEG_4, fromHeight(144), dash()),
+ new SimpleVideoItagFormat(212, MPEG_4, fromHeight(480), dash()),
+ new SimpleVideoItagFormat(298, MPEG_4, fromHeightFps(720, 60), dash()),
+ new SimpleVideoItagFormat(299, MPEG_4, fromHeightFps(1080, 60), dash()),
+ new SimpleVideoItagFormat(266, MPEG_4, fromHeight(2160), dash()),
+
+ // DASH WEBM video
+ // v-- Video-codec: vp9 --v
+ new SimpleVideoItagFormat(278, WEBM, fromHeight(144), dash()),
+ new SimpleVideoItagFormat(242, WEBM, fromHeight(240), dash()),
+ new SimpleVideoItagFormat(243, WEBM, fromHeight(360), dash()),
+ // Itag 244, 245 and 246 are identical?
+ new SimpleVideoItagFormat(244, WEBM, fromHeight(480), dash()),
+ new SimpleVideoItagFormat(245, WEBM, fromHeight(480), dash()),
+ new SimpleVideoItagFormat(246, WEBM, fromHeight(480), dash()),
+ new SimpleVideoItagFormat(247, WEBM, fromHeight(720), dash()),
+ new SimpleVideoItagFormat(248, WEBM, fromHeight(1080), dash()),
+ new SimpleVideoItagFormat(271, WEBM, fromHeight(1440), dash()),
+ // Itag 272 is either 3840x2160 (RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
+ new SimpleVideoItagFormat(272, WEBM, fromHeight(2160), dash()),
+
+ new SimpleVideoItagFormat(302, WEBM, fromHeightFps(720, 60), dash()),
+ new SimpleVideoItagFormat(303, WEBM, fromHeightFps(1080, 60), dash()),
+ new SimpleVideoItagFormat(308, WEBM, fromHeightFps(1440, 60), dash()),
+ new SimpleVideoItagFormat(312, WEBM, fromHeight(2160), dash()),
+ new SimpleVideoItagFormat(315, WEBM, fromHeightFps(2160, 60), dash()),
+ };
+
+ private ItagFormatRegistry() {
+ // No impl
+ }
+
+ public static boolean isSupported(final ItagFormat>[] itagFormats, final int id) {
+ return Stream.of(itagFormats)
+ .flatMap(Stream::of)
+ .anyMatch(itagFormat -> itagFormat.id() == id);
+ }
+
+ public static > I getById(final I[] itagFormats, final int id) {
+ return Stream.of(itagFormats)
+ .flatMap(Stream::of)
+ .filter(itagFormat -> itagFormat.id() == id)
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java
new file mode 100644
index 0000000000..0fad80bca4
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/AbstractSimpleItagFormat.java
@@ -0,0 +1,46 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.simpleimpl.SimpleItagDeliveryDataBuilder;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat;
+import org.schabi.newpipe.extractor.streamdata.format.MediaFormat;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+public abstract class AbstractSimpleItagFormat implements ItagFormat {
+
+ private final int id;
+ private final M mediaFormat;
+ private final ItagFormatDeliveryData deliveryData;
+
+ protected AbstractSimpleItagFormat(
+ final int id,
+ final M mediaFormat,
+ final ItagFormatDeliveryData deliveryData) {
+ this.id = id;
+ this.mediaFormat = Objects.requireNonNull(mediaFormat);
+ this.deliveryData = Objects.requireNonNull(deliveryData);
+ }
+
+ protected AbstractSimpleItagFormat(final int id, final M mediaFormat) {
+ this(id, mediaFormat, SimpleItagDeliveryDataBuilder.progressiveHTTP());
+ }
+
+ @Override
+ public int id() {
+ return id;
+ }
+
+ @Nonnull
+ @Override
+ public M mediaFormat() {
+ return mediaFormat;
+ }
+
+ @Override
+ public ItagFormatDeliveryData deliveryData() {
+ return deliveryData;
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java
new file mode 100644
index 0000000000..565b8de70f
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleAudioItagFormat.java
@@ -0,0 +1,30 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.AudioItagFormat;
+import org.schabi.newpipe.extractor.streamdata.format.AudioMediaFormat;
+
+public class SimpleAudioItagFormat extends AbstractSimpleItagFormat
+ implements AudioItagFormat {
+ private final int averageBitrate;
+
+ public SimpleAudioItagFormat(final int id,
+ final AudioMediaFormat mediaFormat,
+ final int averageBitrate,
+ final ItagFormatDeliveryData deliveryData) {
+ super(id, mediaFormat, deliveryData);
+ this.averageBitrate = averageBitrate;
+ }
+
+ public SimpleAudioItagFormat(final int id,
+ final AudioMediaFormat mediaFormat,
+ final int averageBitrate) {
+ super(id, mediaFormat);
+ this.averageBitrate = averageBitrate;
+ }
+
+ @Override
+ public int averageBitrate() {
+ return averageBitrate;
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java
new file mode 100644
index 0000000000..09dfd7ce16
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoAudioItagFormat.java
@@ -0,0 +1,35 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoAudioItagFormat;
+import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+
+import javax.annotation.Nonnull;
+
+public class SimpleVideoAudioItagFormat extends SimpleVideoItagFormat
+ implements VideoAudioItagFormat {
+ private final int averageBitrate;
+
+ public SimpleVideoAudioItagFormat(final int id,
+ @Nonnull final VideoAudioMediaFormat mediaFormat,
+ @Nonnull final VideoQualityData videoQualityData,
+ final int averageBitrate,
+ @Nonnull final ItagFormatDeliveryData deliveryData) {
+ super(id, mediaFormat, videoQualityData, deliveryData);
+ this.averageBitrate = averageBitrate;
+ }
+
+ public SimpleVideoAudioItagFormat(final int id,
+ @Nonnull final VideoAudioMediaFormat mediaFormat,
+ @Nonnull final VideoQualityData videoQualityData,
+ final int averageBitrate) {
+ super(id, mediaFormat, videoQualityData);
+ this.averageBitrate = averageBitrate;
+ }
+
+ @Override
+ public int averageBitrate() {
+ return averageBitrate;
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java
new file mode 100644
index 0000000000..82f12075fa
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/format/simpleimpl/SimpleVideoItagFormat.java
@@ -0,0 +1,37 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.format.simpleimpl;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.delivery.ItagFormatDeliveryData;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat;
+import org.schabi.newpipe.extractor.streamdata.format.VideoAudioMediaFormat;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+public class SimpleVideoItagFormat extends AbstractSimpleItagFormat
+ implements VideoItagFormat {
+ @Nonnull
+ private final VideoQualityData videoQualityData;
+
+ public SimpleVideoItagFormat(final int id,
+ @Nonnull final VideoAudioMediaFormat mediaFormat,
+ @Nonnull final VideoQualityData videoQualityData,
+ @Nonnull final ItagFormatDeliveryData deliveryData) {
+ super(id, mediaFormat, deliveryData);
+ this.videoQualityData = Objects.requireNonNull(videoQualityData);
+ }
+
+ public SimpleVideoItagFormat(final int id,
+ @Nonnull final VideoAudioMediaFormat mediaFormat,
+ @Nonnull final VideoQualityData videoQualityData) {
+ super(id, mediaFormat);
+ this.videoQualityData = Objects.requireNonNull(videoQualityData);
+ }
+
+ @Nonnull
+ @Override
+ public VideoQualityData videoQualityData() {
+ return videoQualityData;
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java
new file mode 100644
index 0000000000..13c704d4cb
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfo.java
@@ -0,0 +1,280 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.info;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.format.BaseAudioItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.ItagFormat;
+import org.schabi.newpipe.extractor.services.youtube.itag.format.VideoItagFormat;
+import org.schabi.newpipe.extractor.streamdata.stream.quality.VideoQualityData;
+
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public class ItagInfo> {
+
+ @Nonnull
+ private final I itagFormat;
+
+ @Nonnull
+ private final String streamUrl;
+
+ // region Audio
+
+ /**
+ * The average bitrate in KBit/s
+ */
+ @Nullable
+ private Integer averageBitrate;
+
+ @Nullable
+ private Integer audioSampleRate;
+
+ @Nullable
+ private Integer audioChannels;
+
+ // endregion
+
+ // region Video
+
+ @Nullable
+ private Integer width;
+
+ @Nullable
+ private Integer height;
+
+ @Nullable
+ private Integer fps;
+
+ // endregion
+
+ @Nullable
+ private Integer bitRate;
+
+ @Nullable
+ private String quality;
+
+ @Nullable
+ private String codec;
+
+ @Nullable
+ private ItagInfoRange initRange;
+
+ @Nullable
+ private ItagInfoRange indexRange;
+
+ @Nullable
+ private Long contentLength;
+
+ @Nullable
+ private Long approxDurationMs;
+
+ @Nullable
+ private String type;
+
+ // region live or post-live
+
+ @Nullable
+ private Integer targetDurationSec;
+
+ // endregion
+
+
+ public ItagInfo(
+ @Nonnull final I itagFormat,
+ @Nonnull final String streamUrl) {
+ this.itagFormat = itagFormat;
+ this.streamUrl = streamUrl;
+ }
+
+ // region Getters + Setters
+
+ @Nonnull
+ public I getItagFormat() {
+ return itagFormat;
+ }
+
+ @Nonnull
+ public String getStreamUrl() {
+ return streamUrl;
+ }
+
+ @Nullable
+ public Integer getAverageBitrate() {
+ return averageBitrate;
+ }
+
+ public void setAverageBitrate(@Nullable final Integer averageBitrate) {
+ this.averageBitrate = averageBitrate;
+ }
+
+ @Nullable
+ public Integer getAudioSampleRate() {
+ return audioSampleRate;
+ }
+
+ public void setAudioSampleRate(@Nullable final Integer audioSampleRate) {
+ this.audioSampleRate = audioSampleRate;
+ }
+
+ @Nullable
+ public Integer getAudioChannels() {
+ return audioChannels;
+ }
+
+ public void setAudioChannels(@Nullable final Integer audioChannels) {
+ this.audioChannels = audioChannels;
+ }
+
+ @Nullable
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(@Nullable final Integer width) {
+ this.width = width;
+ }
+
+ @Nullable
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(@Nullable final Integer height) {
+ this.height = height;
+ }
+
+ @Nullable
+ public Integer getFps() {
+ return fps;
+ }
+
+ public void setFps(@Nullable final Integer fps) {
+ this.fps = fps;
+ }
+
+ @Nullable
+ public Integer getBitRate() {
+ return bitRate;
+ }
+
+ public void setBitRate(@Nullable final Integer bitRate) {
+ this.bitRate = bitRate;
+ }
+
+ @Nullable
+ public String getQuality() {
+ return quality;
+ }
+
+ public void setQuality(@Nullable final String quality) {
+ this.quality = quality;
+ }
+
+ @Nullable
+ public String getCodec() {
+ return codec;
+ }
+
+ public void setCodec(@Nullable final String codec) {
+ this.codec = codec;
+ }
+
+ @Nullable
+ public ItagInfoRange getInitRange() {
+ return initRange;
+ }
+
+ public void setInitRange(@Nullable final ItagInfoRange initRange) {
+ this.initRange = initRange;
+ }
+
+ @Nullable
+ public ItagInfoRange getIndexRange() {
+ return indexRange;
+ }
+
+ public void setIndexRange(@Nullable final ItagInfoRange indexRange) {
+ this.indexRange = indexRange;
+ }
+
+ @Nullable
+ public Long getContentLength() {
+ return contentLength;
+ }
+
+ public void setContentLength(@Nullable final Long contentLength) {
+ this.contentLength = contentLength;
+ }
+
+ @Nullable
+ public Long getApproxDurationMs() {
+ return approxDurationMs;
+ }
+
+ public void setApproxDurationMs(@Nullable final Long approxDurationMs) {
+ this.approxDurationMs = approxDurationMs;
+ }
+
+ @Nullable
+ public String getType() {
+ return type;
+ }
+
+ public void setType(@Nullable final String type) {
+ this.type = type;
+ }
+
+ @Nullable
+ public Integer getTargetDurationSec() {
+ return targetDurationSec;
+ }
+
+ public void setTargetDurationSec(@Nullable final Integer targetDurationSec) {
+ this.targetDurationSec = targetDurationSec;
+ }
+
+ // endregion
+
+ /**
+ * Returns the combined averageBitrate from the current information and {@link #itagFormat}.
+ * @return averageBitRate in KBit/s or -1
+ */
+ public int getCombinedAverageBitrate() {
+ if (averageBitrate != null) {
+ return averageBitrate;
+ }
+
+ if (itagFormat instanceof BaseAudioItagFormat) {
+ return ((BaseAudioItagFormat) itagFormat).averageBitrate();
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns the combined video-quality data from the current information and {@link #itagFormat}.
+ * @return video-quality data
+ */
+ @Nonnull
+ public VideoQualityData getCombinedVideoQualityData() {
+ final Optional optVideoItagFormatQualityData =
+ itagFormat instanceof VideoItagFormat
+ ? Optional.of(((VideoItagFormat) itagFormat).videoQualityData())
+ : Optional.empty();
+
+ return new VideoQualityData(
+ Optional.ofNullable(height)
+ .orElse(optVideoItagFormatQualityData
+ .map(VideoQualityData::height)
+ .orElse(VideoQualityData.UNKNOWN)),
+ Optional.ofNullable(width)
+ .orElse(optVideoItagFormatQualityData
+ .map(VideoQualityData::width)
+ .orElse(VideoQualityData.UNKNOWN)),
+ Optional.ofNullable(fps)
+ .orElse(optVideoItagFormatQualityData
+ .map(VideoQualityData::fps)
+ .orElse(VideoQualityData.UNKNOWN))
+ );
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java
new file mode 100644
index 0000000000..6b5916985b
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/ItagInfoRange.java
@@ -0,0 +1,27 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.info;
+
+public class ItagInfoRange {
+ private final int start;
+ private final int end;
+
+ public ItagInfoRange(final int start, final int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ public int start() {
+ return start;
+ }
+
+ public int end() {
+ return end;
+ }
+
+ @Override
+ public String toString() {
+ return "ItagInfoRange{"
+ + "start=" + start
+ + ", end=" + end
+ + '}';
+ }
+}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java
new file mode 100644
index 0000000000..aad2a8325f
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/itag/info/builder/ItagInfoRangeHelper.java
@@ -0,0 +1,24 @@
+package org.schabi.newpipe.extractor.services.youtube.itag.info.builder;
+
+import com.grack.nanojson.JsonObject;
+
+import org.schabi.newpipe.extractor.services.youtube.itag.info.ItagInfoRange;
+
+import javax.annotation.Nonnull;
+
+public final class ItagInfoRangeHelper {
+ private ItagInfoRangeHelper() {
+ // No impl
+ }
+
+ public static ItagInfoRange buildFrom(@Nonnull final JsonObject jsonRangeObj) {
+ try {
+ return new ItagInfoRange(
+ Integer.parseInt(jsonRangeObj.getString("start", "-1")),
+ Integer.parseInt(jsonRangeObj.getString("end", "-1"))
+ );
+ } catch (final NumberFormatException ignored) {
+ return null;
+ }
+ }
+}
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
deleted file mode 100644
index 59cf9a3231..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/AudioStream.java
+++ /dev/null
@@ -1,383 +0,0 @@
-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 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 .
- */
-
-import org.schabi.newpipe.extractor.MediaFormat;
-import org.schabi.newpipe.extractor.services.youtube.ItagItem;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-public final class AudioStream extends Stream {
- public static final int UNKNOWN_BITRATE = -1;
-
- private final int averageBitrate;
-
- // Fields for DASH
- private int itag = ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE;
- private int bitrate;
- private int initStart;
- private int initEnd;
- private int indexStart;
- private int indexEnd;
- private String quality;
- private String codec;
- @Nullable
- private ItagItem itagItem;
-
- /**
- * Class to build {@link AudioStream} 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 manifestUrl;
- 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 AudioStream}.
- *
- *
- * It must not be 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 AudioStream}, which must not be 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 not be 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}, or {@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 not be null.
- *
- *
- *
- * The default delivery method is {@link DeliveryMethod#PROGRESSIVE_HTTP}.
- *
- *
- * @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must
- * not be null
- * @return this {@link Builder} instance
- */
- public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
- this.deliveryMethod = deliveryMethod;
- return this;
- }
-
- /**
- * Sets the URL of the manifest this stream comes from (if applicable, otherwise null).
- *
- * @param manifestUrl the URL of the manifest this stream comes from or {@code null}
- * @return this {@link Builder} instance
- */
- public Builder setManifestUrl(@Nullable final String manifestUrl) {
- this.manifestUrl = manifestUrl;
- 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 have been 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,
- manifestUrl, itagItem);
- }
- }
-
-
- /**
- * Create a new audio 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 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 manifestUrl the URL of the manifest this stream comes from (if applicable,
- * otherwise null)
- */
- @SuppressWarnings("checkstyle:ParameterNumber")
- 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 manifestUrl,
- @Nullable final ItagItem itagItem) {
- super(id, content, isUrl, format, deliveryMethod, manifestUrl);
- 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;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean equalStats(final Stream cmp) {
- return super.equalStats(cmp) && cmp instanceof AudioStream
- && averageBitrate == ((AudioStream) cmp).averageBitrate;
- }
-
- /**
- * 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;
- }
-}
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
deleted file mode 100644
index ed98935725..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/DeliveryMethod.java
+++ /dev/null
@@ -1,53 +0,0 @@
-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 {
-
- /**
- * Used for {@link Stream}s served using the progressive HTTP streaming method.
- */
- PROGRESSIVE_HTTP,
-
- /**
- * 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
- * DASH Industry Forum's website for more information about the DASH delivery method
- */
- DASH,
-
- /**
- * 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
- * about HTTP Live Streaming for more information about the HLS delivery method
- */
- HLS,
-
- /**
- * Used for {@link Stream}s served using the SmoothStreaming adaptive streaming method.
- *
- * @see Wikipedia's page about adaptive bitrate streaming,
- * section Microsoft Smooth Streaming (MSS) for more information about the
- * SmoothStreaming delivery method
- */
- SS,
-
- /**
- * Used for {@link Stream}s served via a torrent file.
- *
- * @see Wikipedia's BitTorrent's page,
- * Wikipedia's page about torrent files
- * and Bitorrent's website for more information
- * about the BitTorrent protocol
- */
- TORRENT
-}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java
new file mode 100644
index 0000000000..e02dd327b7
--- /dev/null
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Privacy.java
@@ -0,0 +1,9 @@
+package org.schabi.newpipe.extractor.stream;
+
+public enum Privacy {
+ PUBLIC,
+ UNLISTED,
+ PRIVATE,
+ INTERNAL,
+ OTHER
+}
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
deleted file mode 100644
index 04d2b3facb..0000000000
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/Stream.java
+++ /dev/null
@@ -1,207 +0,0 @@
-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 static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-
-/**
- * Abstract class which represents streams in the extractor.
- */
-public abstract class Stream implements Serializable {
- public static final int FORMAT_ID_UNKNOWN = -1;
- public static final String ID_UNKNOWN = " ";
-
- /**
- * 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.
- *
- */
- 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 manifestUrl;
-
- /**
- * Instantiates a new {@code Stream} object.
- *
- * @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
- * @param format the {@link MediaFormat}, which can be null
- * @param deliveryMethod the delivery method of the stream
- * @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 manifestUrl) {
- this.id = id;
- this.content = content;
- this.isUrl = isUrl;
- this.mediaFormat = format;
- this.deliveryMethod = deliveryMethod;
- this.manifestUrl = manifestUrl;
- }
-
- /**
- * Checks if the list already contains a stream with the same statistics.
- *
- * @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,
- final List extends Stream> streamList) {
- if (isNullOrEmpty(streamList)) {
- return false;
- }
- for (final Stream cmpStream : streamList) {
- if (stream.equalStats(cmpStream)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * 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 using only the {@link DeliveryMethod delivery method} and their ID.
- *
- *
- *
- * Note: This method always returns false if the stream passed is null.
- *
- *
- * @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 other) {
- if (other == null || mediaFormat == null || other.mediaFormat == null) {
- return false;
- }
- return mediaFormat.id == other.mediaFormat.id && deliveryMethod == other.deliveryMethod
- && isUrl == other.isUrl;
- }
-
- /**
- * Gets the identifier of this stream, e.g. the itag for YouTube.
- *
- *
- * 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 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} otherwise.
- *
- * @return the URL if the content is a URL, {@code null} otherwise
- * @deprecated Use {@link #getContent()} instead.
- */
- @Deprecated
- @Nullable
- public String getUrl() {
- return isUrl ? content : null;
- }
-
- /**
- * Gets the content or URL.
- *
- * @return the content or URL
- */
- public String getContent() {
- return content;
- }
-
- /**
- * Returns whether the content is a URL or not.
- *
- * @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;
- }
-
- /**
- * Gets the {@link MediaFormat}, which can be null.
- *
- * @return the format
- */
- @Nullable
- public MediaFormat getFormat() {
- return mediaFormat;
- }
-
- /**
- * Gets the format ID, which can be unknown.
- *
- * @return the format ID or {@link #FORMAT_ID_UNKNOWN}
- */
- public int getFormatId() {
- if (mediaFormat != null) {
- return mediaFormat.id;
- }
- return FORMAT_ID_UNKNOWN;
- }
-
- /**
- * Gets the {@link DeliveryMethod}.
- *
- * @return the delivery method
- */
- @Nonnull
- public DeliveryMethod getDeliveryMethod() {
- return deliveryMethod;
- }
-
- /**
- * Gets the URL of the manifest this stream comes from (if applicable, otherwise null).
- *
- * @return the URL of the manifest this stream comes from or {@code null}
- */
- @Nullable
- public String getManifestUrl() {
- return manifestUrl;
- }
-
- /**
- * Gets the {@link ItagItem} of a stream.
- *
- *
- * If the stream is not from YouTube, this value will always be null.
- *
- *
- * @return the {@link ItagItem} of the stream or {@code null}
- */
- @Nullable
- public abstract ItagItem getItagItem();
-}
diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
index ab922b1c98..8d9d8b5ffe 100644
--- a/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
+++ b/extractor/src/main/java/org/schabi/newpipe/extractor/stream/StreamExtractor.java
@@ -20,11 +20,10 @@
* along with NewPipe. If not, see .
*/
+import org.schabi.newpipe.extractor.Extractor;
import org.schabi.newpipe.extractor.InfoItem;
-import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.InfoItemExtractor;
-import org.schabi.newpipe.extractor.Extractor;
-import org.schabi.newpipe.extractor.MediaFormat;
+import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
@@ -32,16 +31,21 @@
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.DateWrapper;
+import org.schabi.newpipe.extractor.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.Parser;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
/**
* Scrapes information from a video/audio streaming service (eg, YouTube).
*/
@@ -50,7 +54,7 @@ public abstract class StreamExtractor extends Extractor {
public static final int NO_AGE_LIMIT = 0;
public static final long UNKNOWN_SUBSCRIBER_COUNT = -1;
- public StreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
+ protected StreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
super(service, linkHandler);
}
@@ -254,10 +258,49 @@ public String getSubChannelAvatarUrl() throws ParsingException {
}
/**
- * Get the dash mpd url. If you don't know what a dash MPD is you can read about it
+ * Defines how the current stream info should best be resolved.
+ *
+ *
+ * Service mostly offer different methods for streaming data.
+ * However the order is not always clearly defined.
+ * E.g. resolving a livestream might be better using the HLS master playlist.
+ *
+ *
+ * @return A list with the StreamResolutionMode order by priority (0 = highest priority)
+ */
+ @Nonnull
+ public List getResolverStrategyPriority() {
+ if (isLive()) {
+ return Arrays.asList(
+ StreamResolvingStrategy.HLS_MASTER_PLAYLIST_URL,
+ StreamResolvingStrategy.DASH_MPD_URL,
+ StreamResolvingStrategy.VIDEO_ONLY_AND_AUDIO_STREAMS,
+ StreamResolvingStrategy.VIDEO_AUDIO_STREAMS
+ );
+ }
+ return Arrays.asList(
+ StreamResolvingStrategy.VIDEO_ONLY_AND_AUDIO_STREAMS,
+ StreamResolvingStrategy.VIDEO_AUDIO_STREAMS,
+ StreamResolvingStrategy.HLS_MASTER_PLAYLIST_URL,
+ StreamResolvingStrategy.DASH_MPD_URL
+ );
+ }
+
+ /**
+ * Get the dash mpd url.
+ *
+ *
+ * If you don't know how DASH works take a look at
+ *
+ * here.
+ *
+ *
+ *
+ * If you don't know what a dash MPD is you can read about it
* here.
+ *
*
- * @return the url as a string or an empty string or an empty string if not available
+ * @return the url as a string or an empty string if not available
* @throws ParsingException if an error occurs while reading
*/
@Nonnull
@@ -266,15 +309,18 @@ public String getDashMpdUrl() throws ParsingException {
}
/**
- * I am not sure if this is in use, and how this is used. However the frontend is missing
- * support for HLS streams. Prove me if I am wrong. Please open an
- * issue,
- * or fix this description if you know whats up with this.
+ * Get the HLS master playlist url.
+ *
+ *
+ * If you don't know how HLS works take a look at
+ *
+ * here.
+ *
*
* @return The Url to the hls stream or an empty string if not available.
*/
@Nonnull
- public String getHlsUrl() throws ParsingException {
+ public String getHlsMasterPlaylistUrl() throws ParsingException {
return "";
}
@@ -286,10 +332,12 @@ public String getHlsUrl() throws ParsingException {
*
* @return a list of audio only streams in the format of AudioStream
*/
- public abstract List getAudioStreams() throws IOException, ExtractionException;
+ public List getAudioStreams() throws IOException, ExtractionException {
+ return Collections.emptyList();
+ }
/**
- * This should return a list of available {@link VideoStream}s.
+ * This should return a list of available {@link VideoAudioStream}s.
* Be aware this is the list of video streams which do contain an audio stream.
* You can also return null or an empty list, however be aware that if you don't return anything
* in getAudioStreams(), getVideoOnlyStreams() and getDashMpdUrl() either the Collector will
@@ -297,7 +345,9 @@ public String getHlsUrl() throws ParsingException {
*
* @return a list of combined video and streams in the format of AudioStream
*/
- public abstract List getVideoStreams() throws IOException, ExtractionException;
+ public List getVideoStreams() throws IOException, ExtractionException {
+ return Collections.emptyList();
+ }
/**
* This should return a list of available {@link VideoStream}s.
@@ -308,38 +358,38 @@ public String getHlsUrl() throws ParsingException {
*
* @return a list of video and streams in the format of AudioStream
*/
- public abstract List getVideoOnlyStreams() throws IOException, ExtractionException;
+ public List getVideoOnlyStreams() throws IOException, ExtractionException {
+ return Collections.emptyList();
+ }
/**
- * This will return a list of available {@link SubtitlesStream}s.
+ * This will return a list of available {@link SubtitleStream}s.
* If no subtitles are available an empty list can be returned.
*
* @return a list of available subtitles or an empty list
*/
@Nonnull
- public List getSubtitlesDefault() throws IOException, ExtractionException {
+ public List getSubtitles() throws IOException, ExtractionException {
return Collections.emptyList();
}
/**
- * This will return a list of available {@link SubtitlesStream}s given by a specific type.
- * If no subtitles in that specific format are available an empty list can be returned.
+ * This will return whenever there are only audio streams available.
*
- * @param format the media format by which the subtitles should be filtered
- * @return a list of available subtitles or an empty list
+ * @return true when audio only otherwise false
*/
- @Nonnull
- public List getSubtitles(final MediaFormat format)
- throws IOException, ExtractionException {
- return Collections.emptyList();
+ public boolean isAudioOnly() {
+ return false;
}
/**
- * Get the {@link StreamType}.
+ * This will return whenever the current stream is live.
*
- * @return the type of the stream
+ * @return true when live otherwise false
*/
- public abstract StreamType getStreamType() throws ParsingException;
+ public boolean isLive() {
+ return false;
+ }
/**
* Should return a list of streams related to the current handled. Many services show suggested
@@ -351,8 +401,8 @@ public List getSubtitles(final MediaFormat format)
* @return a list of InfoItems showing the related videos/streams
*/
@Nullable
- public InfoItemsCollector extends InfoItem, ? extends InfoItemExtractor>
- getRelatedItems() throws IOException, ExtractionException {
+ public InfoItemsCollector extends InfoItem, ? extends InfoItemExtractor> getRelatedItems()
+ throws IOException, ExtractionException {
return null;
}
@@ -367,9 +417,8 @@ public StreamInfoItemsCollector getRelatedStreams() throws IOException, Extracti
final InfoItemsCollector, ?> collector = getRelatedItems();
if (collector instanceof StreamInfoItemsCollector) {
return (StreamInfoItemsCollector) collector;
- } else {
- return null;
}
+ return null;
}
/**
@@ -414,33 +463,33 @@ protected long getTimestampSeconds(final String regexPattern) throws ParsingExce
return -2;
}
- if (!timestamp.isEmpty()) {
+ if (timestamp.isEmpty()) {
+ return 0;
+ }
+
+ try {
+ String secondsString = "";
+ String minutesString = "";
+ String hoursString = "";
try {
- String secondsString = "";
- String minutesString = "";
- String hoursString = "";
- try {
- secondsString = Parser.matchGroup1("(\\d+)s", timestamp);
- minutesString = Parser.matchGroup1("(\\d+)m", timestamp);
- hoursString = Parser.matchGroup1("(\\d+)h", timestamp);
- } catch (final Exception e) {
- // it could be that time is given in another method
- if (secondsString.isEmpty() && minutesString.isEmpty()) {
- // if nothing was obtained, treat as unlabelled seconds
- secondsString = Parser.matchGroup1("t=(\\d+)", timestamp);
- }
+ secondsString = Parser.matchGroup1("(\\d+)s", timestamp);
+ minutesString = Parser.matchGroup1("(\\d+)m", timestamp);
+ hoursString = Parser.matchGroup1("(\\d+)h", timestamp);
+ } catch (final Exception e) {
+ // it could be that time is given in another method
+ if (secondsString.isEmpty() && minutesString.isEmpty()) {
+ // if nothing was obtained, treat as unlabelled seconds
+ secondsString = Parser.matchGroup1("t=(\\d+)", timestamp);
}
+ }
- final int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString);
- final int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString);
- final int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString);
+ final int seconds = secondsString.isEmpty() ? 0 : Integer.parseInt(secondsString);
+ final int minutes = minutesString.isEmpty() ? 0 : Integer.parseInt(minutesString);
+ final int hours = hoursString.isEmpty() ? 0 : Integer.parseInt(hoursString);
- return seconds + (60L * minutes) + (3600L * hours);
- } catch (final ParsingException e) {
- throw new ParsingException("Could not get timestamp.", e);
- }
- } else {
- return 0;
+ return seconds + (60L * minutes) + (3600L * hours);
+ } catch (final ParsingException e) {
+ throw new ParsingException("Could not get timestamp.", e);
}
}
@@ -553,12 +602,4 @@ public List getStreamSegments() throws ParsingException {
public List getMetaInfo() throws ParsingException {
return Collections.emptyList();
}
-
- public enum Privacy {
- PUBLIC,
- UNLISTED,
- PRIVATE,
- INTERNAL,
- OTHER
- }
}
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 8d3f2c5224..ebdb2538a1 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
@@ -1,5 +1,7 @@
package org.schabi.newpipe.extractor.stream;
+import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
+
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MetaInfo;
@@ -9,6 +11,10 @@
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.streamdata.stream.AudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.SubtitleStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoAudioStream;
+import org.schabi.newpipe.extractor.streamdata.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
@@ -16,11 +22,10 @@
import java.util.Collections;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
import javax.annotation.Nonnull;
-import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-
/*
* Created by Christian Schabesberger on 26.08.15.
*
@@ -46,467 +51,174 @@
*/
public class StreamInfo extends Info {
- public static class StreamExtractException extends ExtractionException {
- StreamExtractException(final String message) {
- super(message);
- }
- }
+ private String thumbnailUrl = "";
+ private String textualUploadDate;
+ private DateWrapper uploadDate;
+ private long duration = -1;
+ private int ageLimit;
+ private Description description;
+
+ private long viewCount = -1;
+ private long likeCount = -1;
+ private long dislikeCount = -1;
+
+ private String uploaderName = "";
+ private String uploaderUrl = "";
+ private String uploaderAvatarUrl = "";
+ private boolean uploaderVerified = false;
+ private long uploaderSubscriberCount = -1;
+
+ private String subChannelName = "";
+ private String subChannelUrl = "";
+ private String subChannelAvatarUrl = "";
+
+ private List streamResolvingStrategies = new ArrayList<>();
+ private List videoStreams = new ArrayList<>();
+ private List audioStreams = new ArrayList<>();
+ private List videoOnlyStreams = new ArrayList<>();
+ @Nonnull
+ private String dashMpdUrl = "";
+ @Nonnull
+ private String hlsMasterPlaylistUrl = "";
+
+ private final boolean audioOnly;
+ private final boolean live;
+
+ private List relatedItems = new ArrayList<>();
+
+ private long startPosition = 0;
+ private List subtitles = new ArrayList<>();
+
+ private String host = "";
+ private Privacy privacy;
+ private String category = "";
+ private String licence = "";
+ private String supportInfo = "";
+ private Locale language = null;
+ private List tags = new ArrayList<>();
+ private List streamSegments = new ArrayList<>();
+ private List metaInfo = new ArrayList<>();
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
public StreamInfo(final int serviceId,
final String url,
final String originalUrl,
- final StreamType streamType,
final String id,
final String name,
- final int ageLimit) {
+ final int ageLimit,
+ final boolean audioOnly,
+ final boolean live) {
super(serviceId, id, url, originalUrl, name);
- this.streamType = streamType;
this.ageLimit = ageLimit;
+ this.audioOnly = audioOnly;
+ this.live = live;
}
- public static StreamInfo getInfo(final String url) throws IOException, ExtractionException {
- return getInfo(NewPipe.getServiceByUrl(url), url);
+ /**
+ * Preview frames, e.g. for the storyboard / seekbar thumbnail preview
+ */
+ private List