Improve documentation and adress most of the requested changes

Also fix some issues in several places, in the code and the documentation.
This commit is contained in:
TiA4f8R 2022-03-15 11:19:13 +01:00
parent 6985167e63
commit aa4c10e751
No known key found for this signature in database
GPG Key ID: E6D3E7F5949450DD
20 changed files with 759 additions and 522 deletions

View File

@ -120,9 +120,9 @@ public class BandcampStreamExtractor extends StreamExtractor {
public String getThumbnailUrl() throws ParsingException {
if (albumJson.isNull("art_id")) {
return EMPTY_STRING;
} else {
return getImageUrl(albumJson.getLong("art_id"), true);
}
return getImageUrl(albumJson.getLong("art_id"), true);
}
@Nonnull

View File

@ -12,15 +12,16 @@ import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@ -58,9 +59,9 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
final JsonObject roomObject = rooms.getObject(r);
if (getId().equals(conferenceObject.getString("slug") + "/"
+ roomObject.getString("slug"))) {
this.conference = conferenceObject;
this.group = groupObject;
this.room = roomObject;
conference = conferenceObject;
group = groupObject;
room = roomObject;
return;
}
}
@ -109,122 +110,120 @@ public class MediaCCCLiveStreamExtractor extends StreamExtractor {
* Get the URL of the first DASH stream found.
*
* <p>
* There can be several DASH streams, so the URL of the first found is returned by this method.
* There can be several DASH streams, so the URL of the first one found is returned by this
* method.
* </p>
*
* <p>
* You can find the other video DASH streams by using {@link #getVideoStreams()}
* You can find the other DASH video streams by using {@link #getVideoStreams()}
* </p>
*/
@Nonnull
@Override
public String getDashMpdUrl() throws ParsingException {
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
final JsonObject stream = room.getArray(STREAMS).getObject(s);
final JsonObject urls = stream.getObject(URLS);
if (urls.has("dash")) {
return urls.getObject("dash").getString(URL, EMPTY_STRING);
}
}
return EMPTY_STRING;
return getManifestOfDeliveryMethodWanted("dash");
}
/**
* Get the URL of the first HLS stream found.
*
* <p>
* There can be several HLS streams, so the URL of the first found is returned by this method.
* There can be several HLS streams, so the URL of the first one found is returned by this
* method.
* </p>
*
* <p>
* You can find the other video HLS streams by using {@link #getVideoStreams()}
* You can find the other HLS video streams by using {@link #getVideoStreams()}
* </p>
*/
@Nonnull
@Override
public String getHlsUrl() {
for (int s = 0; s < room.getArray(STREAMS).size(); s++) {
final JsonObject stream = room.getArray(STREAMS).getObject(s);
final JsonObject urls = stream.getObject(URLS);
if (urls.has("hls")) {
return urls.getObject("hls").getString(URL, EMPTY_STRING);
}
}
return EMPTY_STRING;
return getManifestOfDeliveryMethodWanted("hls");
}
@Nonnull
private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryMethod) {
return room.getArray(STREAMS).stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(streamObject -> streamObject.getObject(URLS))
.filter(urls -> urls.has(deliveryMethod))
.map(urls -> urls.getObject(deliveryMethod).getString(URL, EMPTY_STRING))
.findFirst()
.orElse(EMPTY_STRING);
}
@Override
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
final List<AudioStream> audioStreams = new ArrayList<>();
IntStream.range(0, room.getArray(STREAMS).size())
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
.filter(streamJsonObject -> streamJsonObject.getString("type").equals("audio"))
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
.forEach(type -> {
final JsonObject urlObject = streamJsonObject.getObject(URLS)
.getObject(type);
// The DASH manifest will be extracted with getDashMpdUrl
if (!type.equals("dash")) {
final AudioStream.Builder builder = new AudioStream.Builder()
.setId(urlObject.getString("tech", ID_UNKNOWN))
.setContent(urlObject.getString(URL), true)
.setAverageBitrate(UNKNOWN_BITRATE);
if (type.equals("hls")) {
// We don't know with the type string what media format will
// have HLS streams.
// However, the tech string may contain some information
// about the media format used.
builder.setDeliveryMethod(DeliveryMethod.HLS);
} else {
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
}
return getStreams("audio",
dto -> {
final AudioStream.Builder builder = new AudioStream.Builder()
.setId(dto.getUrlValue().getString("tech", ID_UNKNOWN))
.setContent(dto.getUrlValue().getString(URL), true)
.setAverageBitrate(UNKNOWN_BITRATE);
audioStreams.add(builder.build());
}
}));
if ("hls".equals(dto.getUrlKey())) {
// We don't know with the type string what media format will
// have HLS streams.
// However, the tech string may contain some information
// about the media format used.
return builder.setDeliveryMethod(DeliveryMethod.HLS)
.build();
}
return audioStreams;
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey()))
.build();
});
}
@Override
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
final List<VideoStream> videoStreams = new ArrayList<>();
IntStream.range(0, room.getArray(STREAMS).size())
.mapToObj(s -> room.getArray(STREAMS).getObject(s))
.filter(stream -> stream.getString("type").equals("video"))
.forEachOrdered(streamJsonObject -> streamJsonObject.getObject(URLS).keySet()
.forEach(type -> {
final String resolution =
streamJsonObject.getArray("videoSize").getInt(0)
+ "x"
+ streamJsonObject.getArray("videoSize").getInt(1);
final JsonObject urlObject = streamJsonObject.getObject(URLS)
.getObject(type);
// The DASH manifest will be extracted with getDashMpdUrl
if (!type.equals("dash")) {
final VideoStream.Builder builder = new VideoStream.Builder()
.setId(urlObject.getString("tech", ID_UNKNOWN))
.setContent(urlObject.getString(URL), true)
.setIsVideoOnly(false)
.setResolution(resolution);
return getStreams("video",
dto -> {
final JsonArray videoSize = dto.getStreamJsonObj().getArray("videoSize");
if (type.equals("hls")) {
// We don't know with the type string what media format will
// have HLS streams.
// However, the tech string may contain some information
// about the media format used.
builder.setDeliveryMethod(DeliveryMethod.HLS);
} else {
builder.setMediaFormat(MediaFormat.getFromSuffix(type));
}
final VideoStream.Builder builder = new VideoStream.Builder()
.setId(dto.getUrlValue().getString("tech", ID_UNKNOWN))
.setContent(dto.getUrlValue().getString(URL), true)
.setIsVideoOnly(false)
.setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1));
videoStreams.add(builder.build());
}
}));
if ("hls".equals(dto.getUrlKey())) {
// We don't know with the type string what media format will
// have HLS streams.
// However, the tech string may contain some information
// about the media format used.
return builder.setDeliveryMethod(DeliveryMethod.HLS)
.build();
}
return videoStreams;
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.getUrlKey()))
.build();
});
}
private <T extends Stream> List<T> getStreams(
@Nonnull final String streamType,
@Nonnull final Function<MediaCCCLiveStreamMapperDTO, T> converter) {
return room.getArray(STREAMS).stream()
// Ensure that we use only process JsonObjects
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
// Only process audio streams
.filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type")))
// Flatmap Urls and ensure that we use only process JsonObjects
.flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream()
.filter(e -> e.getValue() instanceof JsonObject)
.map(e -> new MediaCCCLiveStreamMapperDTO(
streamJsonObj,
e.getKey(),
(JsonObject) e.getValue())))
// The DASH manifest will be extracted with getDashMpdUrl
.filter(dto -> !"dash".equals(dto.getUrlKey()))
// Convert
.map(converter)
.collect(Collectors.toList());
}
@Override

View File

@ -0,0 +1,29 @@
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
import com.grack.nanojson.JsonObject;
final class MediaCCCLiveStreamMapperDTO {
private final JsonObject streamJsonObj;
private final String urlKey;
private final JsonObject urlValue;
MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj,
final String urlKey,
final JsonObject urlValue) {
this.streamJsonObj = streamJsonObj;
this.urlKey = urlKey;
this.urlValue = urlValue;
}
JsonObject getStreamJsonObj() {
return streamJsonObj;
}
String getUrlKey() {
return urlKey;
}
JsonObject getUrlValue() {
return urlValue;
}
}

View File

@ -102,7 +102,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
final JsonObject recording = recordings.getObject(i);
final String mimeType = recording.getString("mime_type");
if (mimeType.startsWith("audio")) {
// First we need to resolve the actual video data from CDN
// First we need to resolve the actual video data from the CDN
final MediaFormat mediaFormat;
if (mimeType.endsWith("opus")) {
mediaFormat = MediaFormat.OPUS;
@ -115,7 +115,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
}
// Don't use the containsSimilarStream method because it will always return
// false so if there are multiples audio streams available, only the first will
// false. So if there are multiple audio streams available, only the first one will
// be extracted in this case.
audioStreams.add(new AudioStream.Builder()
.setId(recording.getString("filename", ID_UNKNOWN))
@ -136,7 +136,7 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
final JsonObject recording = recordings.getObject(i);
final String mimeType = recording.getString("mime_type");
if (mimeType.startsWith("video")) {
// First we need to resolve the actual video data from CDN
// First we need to resolve the actual video data from the CDN
final MediaFormat mediaFormat;
if (mimeType.endsWith("webm")) {
@ -148,7 +148,8 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
}
// Don't use the containsSimilarStream method because it will remove the
// extraction of some video versions (mostly languages)
// extraction of some video versions (mostly languages). So if there are multiple
// video streams available, only the first one will be extracted in this case.
videoStreams.add(new VideoStream.Builder()
.setId(recording.getString("filename", ID_UNKNOWN))
.setContent(recording.getString("recording_url"), true)

View File

@ -220,10 +220,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
if (getStreamType() == StreamType.VIDEO_STREAM
&& !isNullOrEmpty(json.getObject(FILES))) {
return json.getObject(FILES).getString(PLAYLIST_URL, EMPTY_STRING);
} else {
return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL,
EMPTY_STRING);
}
return json.getArray(STREAMING_PLAYLISTS).getObject(0).getString(PLAYLIST_URL,
EMPTY_STRING);
}
@Override
@ -231,7 +231,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
assertPageFetched();
/*
Some videos have audio streams, some videos don't have audio streams.
Some videos have audio streams; others don't.
So an audio stream may be available if a video stream is available.
Audio streams are also not returned as separated streams for livestreams.
That's why the extraction of audio streams is only run when there are video streams
@ -435,23 +435,21 @@ public class PeertubeStreamExtractor extends StreamExtractor {
private void extractLiveVideoStreams() throws ParsingException {
try {
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
for (final Object s : streamingPlaylists) {
if (!(s instanceof JsonObject)) {
continue;
}
final JsonObject stream = (JsonObject) s;
// Don't use the containsSimilarStream method because it will always return false
// so if there are multiples HLS URLs returned, only the first will be extracted in
// this case.
videoStreams.add(new VideoStream.Builder()
.setId(String.valueOf(stream.getInt("id", -1)))
.setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true)
.setIsVideoOnly(false)
.setResolution(EMPTY_STRING)
.setMediaFormat(MediaFormat.MPEG_4)
.setDeliveryMethod(DeliveryMethod.HLS)
.build());
}
streamingPlaylists.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(stream -> new VideoStream.Builder()
.setId(String.valueOf(stream.getInt("id", -1)))
.setContent(stream.getString(PLAYLIST_URL, EMPTY_STRING), true)
.setIsVideoOnly(false)
.setResolution(EMPTY_STRING)
.setMediaFormat(MediaFormat.MPEG_4)
.setDeliveryMethod(DeliveryMethod.HLS)
.build())
// Don't use the containsSimilarStream method because it will always return
// false so if there are multiples HLS URLs returned, only the first will be
// extracted in this case.
.forEachOrdered(videoStreams::add);
} catch (final Exception e) {
throw new ParsingException("Could not get video streams", e);
}
@ -463,14 +461,11 @@ public class PeertubeStreamExtractor extends StreamExtractor {
// HLS streams
try {
final JsonArray streamingPlaylists = json.getArray(STREAMING_PLAYLISTS);
for (final Object p : streamingPlaylists) {
if (!(p instanceof JsonObject)) {
continue;
}
final JsonObject playlist = (JsonObject) p;
final String playlistUrl = playlist.getString(PLAYLIST_URL);
getStreamsFromArray(playlist.getArray(FILES), playlistUrl);
for (final JsonObject playlist : json.getArray(STREAMING_PLAYLISTS).stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.collect(Collectors.toList())) {
getStreamsFromArray(playlist.getArray(FILES), playlist.getString(PLAYLIST_URL));
}
} catch (final Exception e) {
throw new ParsingException("Could not get streams", e);
@ -481,39 +476,31 @@ public class PeertubeStreamExtractor extends StreamExtractor {
final String playlistUrl) throws ParsingException {
try {
/*
Starting with version 3.4.0 of PeerTube, HLS playlist of stream resolutions contain the
UUID of the stream, so we can't use the same system to get HLS playlist URL of streams
without fetching the master playlist.
These UUIDs are the same that the ones returned into the fileUrl and fileDownloadUrl
Starting with version 3.4.0 of PeerTube, the HLS playlist of stream resolutions
contains the UUID of the streams, so we can't use the same method to get the URL of
the HLS playlist without fetching the master playlist.
These UUIDs are the same as the ones returned into the fileUrl and fileDownloadUrl
strings.
*/
final boolean isInstanceUsingRandomUuidsForHlsStreams = !isNullOrEmpty(playlistUrl)
&& playlistUrl.endsWith("-master.m3u8");
for (final Object s : streams) {
if (!(s instanceof JsonObject)) {
continue;
}
final JsonObject stream = (JsonObject) s;
final String resolution = JsonUtils.getString(stream, "resolution.label");
final String url;
final String idSuffix;
for (final JsonObject stream : streams.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.collect(Collectors.toList())) {
// Extract stream version of streams first
if (stream.has(FILE_URL)) {
url = JsonUtils.getString(stream, FILE_URL);
idSuffix = FILE_URL;
} else {
url = JsonUtils.getString(stream, FILE_DOWNLOAD_URL);
idSuffix = FILE_DOWNLOAD_URL;
}
final String url = JsonUtils.getString(stream,
stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL);
if (isNullOrEmpty(url)) {
// Not a valid stream URL
return;
}
final String resolution = JsonUtils.getString(stream, "resolution.label");
final String idSuffix = stream.has(FILE_URL) ? FILE_URL : FILE_DOWNLOAD_URL;
if (resolution.toLowerCase().contains("audio")) {
// An audio stream
addNewAudioStream(stream, isInstanceUsingRandomUuidsForHlsStreams, resolution,
@ -535,12 +522,9 @@ public class PeertubeStreamExtractor extends StreamExtractor {
@Nonnull final String idSuffix,
@Nonnull final String format,
@Nonnull final String url) throws ParsingException {
final String streamUrl;
if (FILE_DOWNLOAD_URL.equals(idSuffix)) {
streamUrl = JsonUtils.getString(streamJsonObject, FILE_URL);
} else {
streamUrl = url;
}
final String streamUrl = FILE_DOWNLOAD_URL.equals(idSuffix)
? JsonUtils.getString(streamJsonObject, FILE_URL)
: url;
return streamUrl.replace("-fragmented." + format, ".m3u8");
}
@ -593,7 +577,7 @@ public class PeertubeStreamExtractor extends StreamExtractor {
}
}
// Add finally torrent URLs
// Finally, add torrent URLs
final String torrentUrl = JsonUtils.getString(streamJsonObject, "torrentUrl");
if (!isNullOrEmpty(torrentUrl)) {
audioStreams.add(new AudioStream.Builder()
@ -627,14 +611,10 @@ public class PeertubeStreamExtractor extends StreamExtractor {
// Then add HLS streams
if (!isNullOrEmpty(playlistUrl)) {
final String hlsStreamUrl;
if (isInstanceUsingRandomUuidsForHlsStreams) {
hlsStreamUrl = getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix,
extension, url);
} else {
hlsStreamUrl = playlistUrl.replace("master", JsonUtils.getNumber(
streamJsonObject, RESOLUTION_ID).toString());
}
final String hlsStreamUrl = isInstanceUsingRandomUuidsForHlsStreams
? getHlsPlaylistUrlFromFragmentedFileUrl(streamJsonObject, idSuffix, extension,
url)
: getHlsPlaylistUrlFromMasterPlaylist(streamJsonObject, playlistUrl);
final VideoStream videoStream = new VideoStream.Builder()
.setId(id + "-" + DeliveryMethod.HLS)

View File

@ -233,11 +233,10 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
@Nullable
private String getDownloadUrl(@Nonnull final String trackId)
throws IOException, ExtractionException {
final Downloader dl = NewPipe.getDownloader();
final JsonObject downloadJsonObject;
final String response = NewPipe.getDownloader().get(SOUNDCLOUD_API_V2_URL + "tracks/"
+ trackId + "/download" + "?client_id=" + clientId()).responseBody();
final String response = dl.get(SOUNDCLOUD_API_V2_URL + "tracks/" + trackId
+ "/download" + "?client_id=" + clientId()).responseBody();
final JsonObject downloadJsonObject;
try {
downloadJsonObject = JsonParser.object().from(response);
} catch (final JsonParserException e) {
@ -293,7 +292,7 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
}
}
} catch (final Exception ignored) {
// Something went wrong when parsing this transcoding, don't add it to the
// Something went wrong when parsing this transcoding URL, so don't add it to the
// audioStreams
}
}
@ -304,11 +303,15 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
*
* <p>
* A track can have the {@code downloadable} boolean set to {@code true}, but it doesn't mean
* we can download it: if the value of the {@code has_download_left} boolean is true, the track
* can be downloaded; otherwise not.
* we can download it.
* </p>
*
* @param audioStreams the audio streams to which add the downloadable file
* <p>
* If the value of the {@code has_download_left} boolean is {@code true}, the track can be
* downloaded, and not otherwise.
* </p>
*
* @param audioStreams the audio streams to which the downloadable file is added
*/
public void extractDownloadableFileIfAvailable(final List<AudioStream> audioStreams) {
if (track.getBoolean("downloadable") && track.getBoolean("has_downloads_left")) {
@ -332,9 +335,9 @@ public class SoundcloudStreamExtractor extends StreamExtractor {
* Parses a SoundCloud HLS manifest to get a single URL of HLS streams.
*
* <p>
* This method downloads the provided manifest URL, find all web occurrences in the manifest,
* get the last segment URL, changes its segment range to {@code 0/track-length} and return
* this string.
* This method downloads the provided manifest URL, finds all web occurrences in the manifest,
* gets the last segment URL, changes its segment range to {@code 0/track-length}, and return
* this as a string.
* </p>
*
* @param hlsManifestUrl the URL of the manifest to be parsed

View File

@ -27,7 +27,6 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@ -52,10 +51,26 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Class to extract streams from a DASH manifest.
*
* <p>
* Note that this class relies on the YouTube's {@link ItagItem} class and should be made generic
* in order to be used on other services.
* </p>
*
* <p>
* This class is not used by the extractor itself, as all streams are supported by the extractor.
* </p>
*/
public final class DashMpdParser {
private DashMpdParser() {
}
/**
* Exception class which is thrown when something went wrong when using
* {@link DashMpdParser#getStreams(String)}.
*/
public static class DashMpdParsingException extends ParsingException {
DashMpdParsingException(final String message, final Exception e) {
@ -63,15 +78,21 @@ public final class DashMpdParser {
}
}
/**
* Class which represents the result of a DASH MPD file parsing by {@link DashMpdParser}.
*
* <p>
* The result contains video, video-only and audio streams.
* </p>
*/
public static class Result {
private final List<VideoStream> videoStreams;
private final List<VideoStream> videoOnlyStreams;
private final List<AudioStream> audioStreams;
public Result(final List<VideoStream> videoStreams,
final List<VideoStream> videoOnlyStreams,
final List<AudioStream> audioStreams) {
Result(final List<VideoStream> videoStreams,
final List<VideoStream> videoOnlyStreams,
final List<AudioStream> audioStreams) {
this.videoStreams = videoStreams;
this.videoOnlyStreams = videoOnlyStreams;
this.audioStreams = audioStreams;
@ -90,19 +111,22 @@ public final class DashMpdParser {
}
}
// TODO: Make this class generic and decouple from YouTube's ItagItem class.
/**
* Will try to download and parse the DASH manifest (using {@link StreamInfo#getDashMpdUrl()}),
* adding items that are listed in the {@link ItagItem} class.
* <p>
* It has video, video only and audio streams.
* <p>
* Info about DASH MPD can be found here
* This method will try to download and parse the YouTube DASH MPD manifest URL provided to get
* supported {@link AudioStream}s and {@link VideoStream}s.
*
* @param dashMpdUrl URL to the DASH MPD
* <p>
* The parser supports video, video-only and audio streams.
* </p>
*
* @param dashMpdUrl the URL of the DASH MPD manifest
* @return a {@link Result} which contains all video, video-only and audio streams extracted
* and supported by the extractor (so the ones for which {@link ItagItem#isSupported(int)}
* returns {@code true}).
* @throws DashMpdParsingException if something went wrong when downloading or parsing the
* manifest
* @see <a href="https://www.brendanlong.com/the-structure-of-an-mpeg-dash-mpd.html">
* www.brendanlog.com</a>
* www.brendanlong.com's page about the structure of an MPEG-DASH MPD manifest</a>
*/
@Nonnull
public static Result getStreams(final String dashMpdUrl)
@ -188,7 +212,7 @@ public final class DashMpdParser {
throws TransformerException {
final Element mpdElement = (Element) document.getElementsByTagName("MPD").item(0);
// Clone element so we can freely modify it
// Clone the element so we can freely modify it
final Element adaptationSet = (Element) representation.getParentNode();
final Element adaptationSetClone = (Element) adaptationSet.cloneNode(true);

View File

@ -1,37 +0,0 @@
package org.schabi.newpipe.extractor.services.youtube;
import javax.annotation.Nonnull;
import java.io.Serializable;
public final class ItagInfo implements Serializable {
@Nonnull
private final String content;
@Nonnull
private final ItagItem itagItem;
private boolean isUrl;
public ItagInfo(@Nonnull final String content,
@Nonnull final ItagItem itagItem) {
this.content = content;
this.itagItem = itagItem;
}
public void setIsUrl(final boolean isUrl) {
this.isUrl = isUrl;
}
@Nonnull
public String getContent() {
return content;
}
@Nonnull
public ItagItem getItagItem() {
return itagItem;
}
public boolean getIsUrl() {
return isUrl;
}
}

View File

@ -237,11 +237,15 @@ public class ItagItem implements Serializable {
}
/**
* Get the frame rate per second.
* Get the frame rate.
*
* <p>
* It defaults to the standard value associated with this itag and is set to the {@code fps}
* value returned in the corresponding itag in the YouTube player response.
* It is set to the {@code fps} value returned in the corresponding itag in the YouTube player
* response.
* </p>
*
* <p>
* It defaults to the standard value associated with this itag.
* </p>
*
* <p>
@ -249,28 +253,24 @@ public class ItagItem implements Serializable {
* #FPS_NOT_APPLICABLE_OR_UNKNOWN} is returned for non video itags.
* </p>
*
* @return the frame rate per second or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
* @return the frame rate or {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN}
*/
public int getFps() {
return fps;
}
/**
* Set the frame rate per second.
* Set the frame rate.
*
* <p>
* It is only known for video itags, so {@link #FPS_NOT_APPLICABLE_OR_UNKNOWN} is set/used for
* non video itags or if the sample rate value is less than or equal to 0.
* </p>
*
* @param fps the frame rate per second
* @param fps the frame rate
*/
public void setFps(final int fps) {
if (fps > 0) {
this.fps = fps;
} else {
this.fps = FPS_NOT_APPLICABLE_OR_UNKNOWN;
}
this.fps = fps > 0 ? fps : FPS_NOT_APPLICABLE_OR_UNKNOWN;
}
public int getInitStart() {
@ -314,13 +314,13 @@ public class ItagItem implements Serializable {
}
/**
* Get the resolution string associated to this {@code ItagItem}.
* Get the resolution string associated with this {@code ItagItem}.
*
* <p>
* It is only known for video itags.
* </p>
*
* @return the resolution string associated to this {@code ItagItem} or
* @return the resolution string associated with this {@code ItagItem} or
* {@code null}.
*/
@Nullable
@ -361,7 +361,7 @@ public class ItagItem implements Serializable {
*
* <p>
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is returned for non audio
* itags or if the sample rate is unknown.
* itags, or if the sample rate is unknown.
* </p>
*
* @return the sample rate or {@link #SAMPLE_RATE_UNKNOWN}
@ -374,8 +374,8 @@ public class ItagItem implements Serializable {
* Set the sample rate.
*
* <p>
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non video
* itags or if the sample rate value is less than or equal to 0.
* It is only known for audio itags, so {@link #SAMPLE_RATE_UNKNOWN} is set/used for non audio
* itags, or if the sample rate value is less than or equal to 0.
* </p>
*
* @param sampleRate the sample rate of an audio itag
@ -392,8 +392,8 @@ public class ItagItem implements Serializable {
* Get the number of audio channels.
*
* <p>
* It is only known for audio streams, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
* returned for video streams or if it is unknown.
* It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
* returned for non audio itags, or if it is unknown.
* </p>
*
* @return the number of audio channels or {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN}
@ -406,28 +406,26 @@ public class ItagItem implements Serializable {
* Set the number of audio channels.
*
* <p>
* It is only known for audio itag, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
* set/used for non audio itags or if the {@code audioChannels} value is less than or equal to
* It is only known for audio itags, so {@link #AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN} is
* set/used for non audio itags, or if the {@code audioChannels} value is less than or equal to
* 0.
* </p>
*
* @param audioChannels the number of audio channels of an audio itag
*/
public void setAudioChannels(final int audioChannels) {
if (audioChannels > 0) {
this.audioChannels = audioChannels;
} else {
this.audioChannels = AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
}
this.audioChannels = audioChannels > 0
? audioChannels
: AUDIO_CHANNELS_NOT_APPLICABLE_OR_UNKNOWN;
}
/**
* Get the {@code targetDurationSec} value.
*
* <p>
* This value is an average time in seconds of sequences duration of livestreams and ended
* livestreams. It is only returned for these stream types by YouTube and makes no sense for
* videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for video streams.
* This value is the average time in seconds of the duration of sequences of livestreams and
* ended livestreams. It is only returned by YouTube for these stream types, and makes no sense
* for videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} is returned for those.
* </p>
*
* @return the {@code targetDurationSec} value or {@link #TARGET_DURATION_SEC_UNKNOWN}
@ -440,25 +438,23 @@ public class ItagItem implements Serializable {
* Set the {@code targetDurationSec} value.
*
* <p>
* This value is an average time in seconds of sequences duration of livestreams and ended
* livestreams.
* This value is the average time in seconds of the duration of sequences of livestreams and
* ended livestreams.
* </p>
*
* <p>
* It is only returned for these stream types by YouTube and makes no sense for
* videos, so {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if
* this value is less than or equal to 0.
* It is only returned for these stream types by YouTube and makes no sense for videos, so
* {@link #TARGET_DURATION_SEC_UNKNOWN} will be set/used for video streams or if this value is
* less than or equal to 0.
* </p>
*
* @param targetDurationSec the target duration of a segment of streams which are using the
* live delivery method type
*/
public void setTargetDurationSec(final int targetDurationSec) {
if (targetDurationSec > 0) {
this.targetDurationSec = targetDurationSec;
} else {
this.targetDurationSec = TARGET_DURATION_SEC_UNKNOWN;
}
this.targetDurationSec = targetDurationSec > 0
? targetDurationSec
: TARGET_DURATION_SEC_UNKNOWN;
}
/**
@ -487,11 +483,9 @@ public class ItagItem implements Serializable {
* milliseconds
*/
public void setApproxDurationMs(final long approxDurationMs) {
if (approxDurationMs > 0) {
this.approxDurationMs = approxDurationMs;
} else {
this.approxDurationMs = APPROX_DURATION_MS_UNKNOWN;
}
this.approxDurationMs = approxDurationMs > 0
? approxDurationMs
: APPROX_DURATION_MS_UNKNOWN;
}
/**
@ -519,10 +513,6 @@ public class ItagItem implements Serializable {
* @param contentLength the content length of a DASH progressive stream
*/
public void setContentLength(final long contentLength) {
if (contentLength > 0) {
this.contentLength = contentLength;
} else {
this.contentLength = CONTENT_LENGTH_UNKNOWN;
}
this.contentLength = contentLength > 0 ? contentLength : CONTENT_LENGTH_UNKNOWN;
}
}

View File

@ -35,7 +35,6 @@ import static org.schabi.newpipe.extractor.utils.Utils.*;
* It relies on external classes from the {@link org.w3c.dom} and {@link javax.xml} packages.
* </p>
*/
@SuppressWarnings({"ConstantConditions", "unused"})
public final class YoutubeDashManifestCreator {
/**
@ -115,6 +114,7 @@ public final class YoutubeDashManifestCreator {
* </p>
*/
PROGRESSIVE,
/**
* YouTube's OTF delivery method which uses a sequence parameter to get segments of
* streams.
@ -124,12 +124,14 @@ public final class YoutubeDashManifestCreator {
* metadata needed to build the stream source (sidx boxes, segment length, segment count,
* duration, ...)
* </p>
*
* <p>
* Only used for videos; mostly those with a small amount of views, or ended livestreams
* which have just been re-encoded as normal videos.
* </p>
*/
OTF,
/**
* YouTube's delivery method for livestreams which uses a sequence parameter to get
* segments of streams.
@ -139,6 +141,7 @@ public final class YoutubeDashManifestCreator {
* metadata (sidx boxes, segment length, ...), which make no need of an initialization
* segment.
* </p>
*
* <p>
* Only used for livestreams (ended or running).
* </p>
@ -225,27 +228,27 @@ public final class YoutubeDashManifestCreator {
*/
@Nonnull
public static String createDashManifestFromOtfStreamingUrl(
@Nonnull String otfBaseStreamingUrl,
@Nonnull final String otfBaseStreamingUrl,
@Nonnull final ItagItem itagItem,
final long durationSecondsFallback)
throws YoutubeDashManifestCreationException {
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
if (GENERATED_OTF_MANIFESTS.containsKey(otfBaseStreamingUrl)) {
return GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl).getSecond();
return Objects.requireNonNull(GENERATED_OTF_MANIFESTS.get(otfBaseStreamingUrl))
.getSecond();
}
final String originalOtfBaseStreamingUrl = otfBaseStreamingUrl;
String realOtfBaseStreamingUrl = otfBaseStreamingUrl;
// Try to avoid redirects when streaming the content by saving the last URL we get
// from video servers.
final Response response = getInitializationResponse(otfBaseStreamingUrl,
final Response response = getInitializationResponse(realOtfBaseStreamingUrl,
itagItem, DeliveryType.OTF);
otfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
realOtfBaseStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
.replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING);
final int responseCode = response.responseCode();
if (responseCode != 200) {
throw new YoutubeDashManifestCreationException(
"Unable to create the DASH manifest: could not get the initialization URL of the OTF stream: response code "
+ responseCode);
"Unable to create the DASH manifest: could not get the initialization URL of "
+ "the OTF stream: response code " + responseCode);
}
final String[] segmentDuration;
@ -266,7 +269,8 @@ public final class YoutubeDashManifestCreator {
}
} catch (final Exception e) {
throw new YoutubeDashManifestCreationException(
"Unable to generate the DASH manifest: could not get the duration of segments", e);
"Unable to generate the DASH manifest: could not get the duration of segments",
e);
}
final Document document = generateDocumentAndMpdElement(segmentDuration, DeliveryType.OTF,
@ -278,7 +282,7 @@ public final class YoutubeDashManifestCreator {
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
generateAudioChannelConfigurationElement(document, itagItem);
}
generateSegmentTemplateElement(document, otfBaseStreamingUrl, DeliveryType.OTF);
generateSegmentTemplateElement(document, realOtfBaseStreamingUrl, DeliveryType.OTF);
generateSegmentTimelineElement(document);
collectSegmentsData(segmentDuration);
generateSegmentElementsForOtfStreams(document);
@ -286,7 +290,7 @@ public final class YoutubeDashManifestCreator {
SEGMENTS_DURATION.clear();
DURATION_REPETITIONS.clear();
return buildResult(originalOtfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS);
return buildResult(otfBaseStreamingUrl, document, GENERATED_OTF_MANIFESTS);
}
/**
@ -358,36 +362,37 @@ public final class YoutubeDashManifestCreator {
*/
@Nonnull
public static String createDashManifestFromPostLiveStreamDvrStreamingUrl(
@Nonnull String postLiveStreamDvrStreamingUrl,
@Nonnull final String postLiveStreamDvrStreamingUrl,
@Nonnull final ItagItem itagItem,
final int targetDurationSec,
final long durationSecondsFallback)
throws YoutubeDashManifestCreationException {
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
if (GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.containsKey(postLiveStreamDvrStreamingUrl)) {
return GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(postLiveStreamDvrStreamingUrl)
.getSecond();
return Objects.requireNonNull(GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS.get(
postLiveStreamDvrStreamingUrl)).getSecond();
}
final String originalPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl;
String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl;
final String streamDuration;
final String segmentCount;
if (targetDurationSec <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the targetDurationSec value is less than or equal to 0 (" + targetDurationSec + ")");
"Could not generate the DASH manifest: the targetDurationSec value is less "
+ "than or equal to 0 (" + targetDurationSec + ")");
}
try {
// Try to avoid redirects when streaming the content by saving the latest URL we get
// from video servers.
final Response response = getInitializationResponse(postLiveStreamDvrStreamingUrl,
final Response response = getInitializationResponse(realPostLiveStreamDvrStreamingUrl,
itagItem, DeliveryType.LIVE);
postLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
realPostLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
.replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING);
final int responseCode = response.responseCode();
if (responseCode != 200) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code "
"Could not generate the DASH manifest: could not get the initialization "
+ "segment of the post-live-DVR stream: response code "
+ responseCode);
}
@ -396,15 +401,18 @@ public final class YoutubeDashManifestCreator {
segmentCount = responseHeaders.get("X-Head-Seqnum").get(0);
} catch (final IndexOutOfBoundsException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR streaming URL", e);
"Could not generate the DASH manifest: could not get the value of the "
+ "X-Head-Time-Millis or the X-Head-Seqnum header of the post-live-DVR"
+ "streaming URL", e);
}
if (isNullOrEmpty(segmentCount)) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the number of segments of the post-live-DVR stream");
"Could not generate the DASH manifest: could not get the number of segments of"
+ "the post-live-DVR stream");
}
final Document document = generateDocumentAndMpdElement(new String[]{streamDuration},
final Document document = generateDocumentAndMpdElement(new String[] {streamDuration},
DeliveryType.LIVE, itagItem, durationSecondsFallback);
generatePeriodElement(document);
generateAdaptationSetElement(document, itagItem);
@ -413,11 +421,12 @@ public final class YoutubeDashManifestCreator {
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
generateAudioChannelConfigurationElement(document, itagItem);
}
generateSegmentTemplateElement(document, postLiveStreamDvrStreamingUrl, DeliveryType.LIVE);
generateSegmentTemplateElement(document, realPostLiveStreamDvrStreamingUrl,
DeliveryType.LIVE);
generateSegmentTimelineElement(document);
generateSegmentElementForPostLiveDvrStreams(document, targetDurationSec, segmentCount);
return buildResult(originalPostLiveStreamDvrStreamingUrl, document,
return buildResult(postLiveStreamDvrStreamingUrl, document,
GENERATED_POST_LIVE_DVR_STREAMS_MANIFESTS);
}
@ -486,13 +495,14 @@ public final class YoutubeDashManifestCreator {
@Nonnull final ItagItem itagItem,
final long durationSecondsFallback) throws YoutubeDashManifestCreationException {
if (GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.containsKey(progressiveStreamingBaseUrl)) {
return GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(progressiveStreamingBaseUrl)
.getSecond();
return Objects.requireNonNull(GENERATED_PROGRESSIVE_STREAMS_MANIFESTS.get(
progressiveStreamingBaseUrl)).getSecond();
}
if (durationSecondsFallback <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the durationSecondsFallback value is less than or equal to 0 (" + durationSecondsFallback + ")");
"Could not generate the DASH manifest: the durationSecondsFallback value is"
+ "less than or equal to 0 (" + durationSecondsFallback + ")");
}
final Document document = generateDocumentAndMpdElement(new String[]{},
@ -508,7 +518,8 @@ public final class YoutubeDashManifestCreator {
generateSegmentBaseElement(document, itagItem);
generateInitializationElement(document, itagItem);
return buildResult(progressiveStreamingBaseUrl, document, GENERATED_PROGRESSIVE_STREAMS_MANIFESTS);
return buildResult(progressiveStreamingBaseUrl, document,
GENERATED_PROGRESSIVE_STREAMS_MANIFESTS);
}
/**
@ -564,7 +575,8 @@ public final class YoutubeDashManifestCreator {
return downloader.post(baseStreamingUrl, headers, emptyBody);
} catch (final IOException | ExtractionException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: error when trying to get the ANDROID streaming post-live-DVR URL response", e);
"Could not generate the DASH manifest: error when trying to get the "
+ "ANDROID streaming post-live-DVR URL response", e);
}
}
@ -579,10 +591,12 @@ public final class YoutubeDashManifestCreator {
} catch (final IOException | ExtractionException e) {
if (isAnAndroidStreamingUrl) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: error when trying to get the ANDROID streaming URL response", e);
"Could not generate the DASH manifest: error when trying to get the "
+ "ANDROID streaming URL response", e);
} else {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: error when trying to get the streaming URL response", e);
"Could not generate the DASH manifest: error when trying to get the "
+ "streaming URL response", e);
}
}
}
@ -658,16 +672,18 @@ public final class YoutubeDashManifestCreator {
if (responseCode != 200) {
if (deliveryType == DeliveryType.LIVE) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the initialization URL of the post-live-DVR stream: response code "
+ responseCode);
"Could not generate the DASH manifest: could not get the "
+ "initialization URL of the post-live-DVR stream: "
+ "response code " + responseCode);
} else if (deliveryType == DeliveryType.OTF) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the initialization URL of the OTF stream: response code "
"Could not generate the DASH manifest: could not get the "
+ "initialization URL of the OTF stream: response code "
+ responseCode);
} else {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not fetch the URL of the progressive stream: response code "
+ responseCode);
"Could not generate the DASH manifest: could not fetch the URL of "
+ "the progressive stream: response code " + responseCode);
}
}
@ -678,7 +694,8 @@ public final class YoutubeDashManifestCreator {
"Content-Type"));
} catch (final NullPointerException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: could not get the Content-Type header from the streaming URL", e);
"Could not generate the DASH manifest: could not get the Content-Type "
+ "header from the streaming URL", e);
}
// The response body is the redirection URL
@ -692,16 +709,19 @@ public final class YoutubeDashManifestCreator {
if (redirectsCount >= MAXIMUM_REDIRECT_COUNT) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: too many redirects when trying to get the WEB streaming URL response");
"Could not generate the DASH manifest: too many redirects when trying to "
+ "get the WEB streaming URL response");
}
// This should never be reached, but is required because we don't want to return null
// here
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: error when trying to get the WEB streaming URL response");
"Could not generate the DASH manifest: error when trying to get the WEB "
+ "streaming URL response");
} catch (final IOException | ExtractionException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: error when trying to get the WEB streaming URL response", e);
"Could not generate the DASH manifest: error when trying to get the WEB "
+ "streaming URL response", e);
}
}
@ -731,7 +751,8 @@ public final class YoutubeDashManifestCreator {
}
} catch (final NumberFormatException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: unable to get the segments of the stream", e);
"Could not generate the DASH manifest: unable to get the segments of the "
+ "stream", e);
}
}
@ -767,7 +788,8 @@ public final class YoutubeDashManifestCreator {
return streamLengthMs;
} catch (final NumberFormatException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: unable to get the length of the stream", e);
"Could not generate the DASH manifest: unable to get the length of the stream",
e);
}
}
@ -778,6 +800,7 @@ public final class YoutubeDashManifestCreator {
* The generated {@code <MPD>} element looks like the manifest returned into the player
* response of videos with OTF streams:
* </p>
*
* <p>
* {@code <MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
* xmlns="urn:mpeg:DASH:schema:MPD:2011"
@ -787,6 +810,7 @@ public final class YoutubeDashManifestCreator {
* (where {@code $duration$} represents the duration in seconds (a number with 3 digits after
* the decimal point)
* </p>
*
* <p>
* If the duration is an integer or a double with less than 3 digits after the decimal point,
* it will be converted into a double with 3 digits after the decimal point.
@ -859,8 +883,10 @@ public final class YoutubeDashManifestCreator {
streamDuration = durationSecondsFallback * 1000;
} else {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the MPD element of the DASH manifest to the document: "
+ "the duration of the stream could not be determined and the durationSecondsFallback is less than or equal to 0");
"Could not generate or append the MPD element of the DASH "
+ "manifest to the document: the duration of the stream "
+ "could not be determined and the "
+ "durationSecondsFallback is less than or equal to 0");
}
}
}
@ -870,7 +896,8 @@ public final class YoutubeDashManifestCreator {
mpdElement.setAttributeNode(mediaPresentationDurationAttribute);
} catch (final Exception e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the MPD element of the DASH manifest to the document", e);
"Could not generate or append the MPD element of the DASH manifest to the "
+ "document", e);
}
return document;
@ -898,7 +925,8 @@ public final class YoutubeDashManifestCreator {
mpdElement.appendChild(periodElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the Period element of the DASH manifest to the document", e);
"Could not generate or append the Period element of the DASH manifest to the "
+ "document", e);
}
}
@ -921,7 +949,8 @@ public final class YoutubeDashManifestCreator {
@Nonnull final ItagItem itagItem)
throws YoutubeDashManifestCreationException {
try {
final Element periodElement = (Element) document.getElementsByTagName("Period").item(0);
final Element periodElement = (Element) document.getElementsByTagName("Period")
.item(0);
final Element adaptationSetElement = document.createElement("AdaptationSet");
final Attr idAttribute = document.createAttribute("id");
@ -931,21 +960,25 @@ public final class YoutubeDashManifestCreator {
final MediaFormat mediaFormat = itagItem.getMediaFormat();
if (mediaFormat == null || isNullOrEmpty(mediaFormat.mimeType)) {
throw new YoutubeDashManifestCreationException(
"Could not generate the AdaptationSet element of the DASH manifest to the document: the MediaFormat or the mime type of the MediaFormat of the ItagItem is null or empty");
"Could not generate the AdaptationSet element of the DASH manifest to the "
+ "document: the MediaFormat or the mime type of the MediaFormat "
+ "of the ItagItem is null or empty");
}
final Attr mimeTypeAttribute = document.createAttribute("mimeType");
mimeTypeAttribute.setValue(mediaFormat.mimeType);
adaptationSetElement.setAttributeNode(mimeTypeAttribute);
final Attr subsegmentAlignmentAttribute = document.createAttribute("subsegmentAlignment");
final Attr subsegmentAlignmentAttribute = document.createAttribute(
"subsegmentAlignment");
subsegmentAlignmentAttribute.setValue("true");
adaptationSetElement.setAttributeNode(subsegmentAlignmentAttribute);
periodElement.appendChild(adaptationSetElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the AdaptationSet element of the DASH manifest to the document", e);
"Could not generate or append the AdaptationSet element of the DASH manifest "
+ "to the document", e);
}
}
@ -956,9 +989,11 @@ public final class YoutubeDashManifestCreator {
* <p>
* This element, with its attributes and values, is:
* </p>
*
* <p>
* {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>}
* </p>
*
* <p>
* The {@code <AdaptationSet>} element needs to be generated before this element with
* {@link #generateAdaptationSetElement(Document, ItagItem)}).
@ -967,7 +1002,8 @@ public final class YoutubeDashManifestCreator {
* @param document the {@link Document} on which the the {@code <Role>} element will be
* appended
* @throws YoutubeDashManifestCreationException if something goes wrong when generating or
* appending the {@code <Role>} element to the document
* appending the {@code <Role>} element to the
* document
*/
private static void generateRoleElement(@Nonnull final Document document)
throws YoutubeDashManifestCreationException {
@ -987,7 +1023,8 @@ public final class YoutubeDashManifestCreator {
adaptationSetElement.appendChild(roleElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the Role element of the DASH manifest to the document", e);
"Could not generate or append the Role element of the DASH manifest to the "
+ "document", e);
}
}
@ -1018,7 +1055,9 @@ public final class YoutubeDashManifestCreator {
final int id = itagItem.id;
if (id <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the Representation element of the DASH manifest to the document: the id of the ItagItem is less than or equal to 0");
"Could not generate the Representation element of the DASH manifest to "
+ "the document: the id of the ItagItem is less than or equal to "
+ "0");
}
final Attr idAttribute = document.createAttribute("id");
idAttribute.setValue(String.valueOf(id));
@ -1027,7 +1066,8 @@ public final class YoutubeDashManifestCreator {
final String codec = itagItem.getCodec();
if (isNullOrEmpty(codec)) {
throw new YoutubeDashManifestCreationException(
"Could not generate the AdaptationSet element of the DASH manifest to the document: the codecs value is null or empty");
"Could not generate the AdaptationSet element of the DASH manifest to the "
+ "document: the codec value is null or empty");
}
final Attr codecsAttribute = document.createAttribute("codecs");
codecsAttribute.setValue(codec);
@ -1044,7 +1084,9 @@ public final class YoutubeDashManifestCreator {
final int bitrate = itagItem.getBitrate();
if (bitrate <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the Representation element of the DASH manifest to the document: the bitrate of the ItagItem is less than or equal to 0");
"Could not generate the Representation element of the DASH manifest to "
+ "the document: the bitrate of the ItagItem is less than or "
+ "equal to 0");
}
final Attr bandwidthAttribute = document.createAttribute("bandwidth");
bandwidthAttribute.setValue(String.valueOf(bitrate));
@ -1057,7 +1099,9 @@ public final class YoutubeDashManifestCreator {
final int width = itagItem.getWidth();
if (height <= 0 && width <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the Representation element of the DASH manifest to the document: the width and the height of the ItagItem are less than or equal to 0");
"Could not generate the Representation element of the DASH manifest "
+ "to the document: the width and the height of the ItagItem "
+ "are less than or equal to 0");
}
if (width > 0) {
@ -1087,7 +1131,8 @@ public final class YoutubeDashManifestCreator {
adaptationSetElement.appendChild(representationElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the Representation element of the DASH manifest to the document", e);
"Could not generate or append the Representation element of the DASH manifest "
+ "to the document", e);
}
}
@ -1098,6 +1143,7 @@ public final class YoutubeDashManifestCreator {
* <p>
* This method is only used when generating DASH manifests of audio streams.
* </p>
*
* <p>
* It will produce the following element:
* <br>
@ -1108,6 +1154,7 @@ public final class YoutubeDashManifestCreator {
* (where {@code audioChannelsValue} is get from the {@link ItagItem} passed as the second
* parameter of this method)
* </p>
*
* <p>
* The {@code <Representation>} element needs to be generated before this element with
* {@link #generateRepresentationElement(Document, ItagItem)}).
@ -1139,7 +1186,8 @@ public final class YoutubeDashManifestCreator {
final int audioChannels = itagItem.getAudioChannels();
if (audioChannels <= 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the audioChannels value is less than or equal to 0 (" + audioChannels + ")");
"Could not generate the DASH manifest: the audioChannels value is less "
+ "than or equal to 0 (" + audioChannels + ")");
}
valueAttribute.setValue(String.valueOf(itagItem.getAudioChannels()));
audioChannelConfigurationElement.setAttributeNode(valueAttribute);
@ -1147,7 +1195,8 @@ public final class YoutubeDashManifestCreator {
representationElement.appendChild(audioChannelConfigurationElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the AudioChannelConfiguration element of the DASH manifest to the document", e);
"Could not generate or append the AudioChannelConfiguration element of the "
+ "DASH manifest to the document", e);
}
}
@ -1158,6 +1207,7 @@ public final class YoutubeDashManifestCreator {
* <p>
* This method is only used when generating DASH manifests from progressive streams.
* </p>
*
* <p>
* The {@code <Representation>} element needs to be generated before this element with
* {@link #generateRepresentationElement(Document, ItagItem)}).
@ -1182,7 +1232,8 @@ public final class YoutubeDashManifestCreator {
representationElement.appendChild(baseURLElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the BaseURL element of the DASH manifest to the document", e);
"Could not generate or append the BaseURL element of the DASH manifest to the "
+ "document", e);
}
}
@ -1193,6 +1244,7 @@ public final class YoutubeDashManifestCreator {
* <p>
* This method is only used when generating DASH manifests from progressive streams.
* </p>
*
* <p>
* It generates the following element:
* <br>
@ -1201,6 +1253,7 @@ public final class YoutubeDashManifestCreator {
* (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
* as the second parameter)
* </p>
*
* <p>
* The {@code <Representation>} element needs to be generated before this element with
* {@link #generateRepresentationElement(Document, ItagItem)}).
@ -1227,12 +1280,14 @@ public final class YoutubeDashManifestCreator {
final int indexStart = itagItem.getIndexStart();
if (indexStart < 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the indexStart value of the ItagItem is less than to 0 (" + indexStart + ")");
"Could not generate the DASH manifest: the indexStart value of the "
+ "ItagItem is less than to 0 (" + indexStart + ")");
}
final int indexEnd = itagItem.getIndexEnd();
if (indexEnd < 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the indexEnd value of the ItagItem is less than to 0 (" + indexStart + ")");
"Could not generate the DASH manifest: the indexEnd value of the ItagItem "
+ "is less than to 0 (" + indexStart + ")");
}
indexRangeAttribute.setValue(indexStart + "-" + indexEnd);
@ -1241,7 +1296,8 @@ public final class YoutubeDashManifestCreator {
representationElement.appendChild(segmentBaseElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the SegmentBase element of the DASH manifest to the document", e);
"Could not generate or append the SegmentBase element of the DASH manifest to "
+ "the document", e);
}
}
@ -1252,6 +1308,7 @@ public final class YoutubeDashManifestCreator {
* <p>
* This method is only used when generating DASH manifests from progressive streams.
* </p>
*
* <p>
* It generates the following element:
* <br>
@ -1260,6 +1317,7 @@ public final class YoutubeDashManifestCreator {
* (where {@code indexStart} and {@code indexEnd} are gotten from the {@link ItagItem} passed
* as the second parameter)
* </p>
*
* <p>
* The {@code <SegmentBase>} element needs to be generated before this element with
* {@link #generateSegmentBaseElement(Document, ItagItem)}).
@ -1286,12 +1344,14 @@ public final class YoutubeDashManifestCreator {
final int initStart = itagItem.getInitStart();
if (initStart < 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the initStart value of the ItagItem is less than to 0 (" + initStart + ")");
"Could not generate the DASH manifest: the initStart value of the "
+ "ItagItem is less than to 0 (" + initStart + ")");
}
final int initEnd = itagItem.getInitEnd();
if (initEnd < 0) {
throw new YoutubeDashManifestCreationException(
"Could not generate the DASH manifest: the initEnd value of the ItagItem is less than to 0 (" + initEnd + ")");
"Could not generate the DASH manifest: the initEnd value of the ItagItem "
+ "is less than to 0 (" + initEnd + ")");
}
rangeAttribute.setValue(initStart + "-" + initEnd);
@ -1300,7 +1360,8 @@ public final class YoutubeDashManifestCreator {
segmentBaseElement.appendChild(initializationElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the Initialization element of the DASH manifest to the document", e);
"Could not generate or append the Initialization element of the DASH manifest "
+ "to the document", e);
}
}
@ -1311,6 +1372,7 @@ public final class YoutubeDashManifestCreator {
* <p>
* This method is only used when generating DASH manifests from OTF and post-live-DVR streams.
* </p>
*
* <p>
* It will produce a {@code <SegmentTemplate>} element with the following attributes:
* <ul>
@ -1372,7 +1434,8 @@ public final class YoutubeDashManifestCreator {
representationElement.appendChild(segmentTemplateElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the SegmentTemplate element of the DASH manifest to the document", e);
"Could not generate or append the SegmentTemplate element of the DASH "
+ "manifest to the document", e);
}
}
@ -1401,7 +1464,8 @@ public final class YoutubeDashManifestCreator {
segmentTemplateElement.appendChild(segmentTimelineElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the SegmentTimeline element of the DASH manifest to the document", e);
"Could not generate or append the SegmentTimeline element of the DASH "
+ "manifest to the document", e);
}
}
@ -1413,16 +1477,20 @@ public final class YoutubeDashManifestCreator {
* so we just have to loop into {@link #SEGMENTS_DURATION} and {@link #DURATION_REPETITIONS}
* to generate the following element for each duration:
* </p>
*
* <p>
* {@code <S d="segmentDuration" r="durationRepetition" />}
* </p>
*
* <p>
* If there is no repetition of the duration between two segments, the {@code r} attribute is
* not added to the {@code S} element.
* </p>
*
* <p>
* These elements will be appended as children of the {@code <SegmentTimeline>} element.
* </p>
*
* <p>
* The {@code <SegmentTimeline>} element needs to be generated before this element with
* {@link #generateSegmentTimelineElement(Document)}.
@ -1462,7 +1530,8 @@ public final class YoutubeDashManifestCreator {
} catch (final DOMException | IllegalStateException | IndexOutOfBoundsException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the segment (S) elements of the DASH manifest to the document", e);
"Could not generate or append the segment (S) elements of the DASH manifest "
+ "to the document", e);
}
}
@ -1507,7 +1576,8 @@ public final class YoutubeDashManifestCreator {
segmentTimelineElement.appendChild(sElement);
} catch (final DOMException e) {
throw new YoutubeDashManifestCreationException(
"Could not generate or append the segment (S) elements of the DASH manifest to the document", e);
"Could not generate or append the segment (S) elements of the DASH manifest "
+ "to the document", e);
}
}

View File

@ -1596,9 +1596,9 @@ public final class YoutubeParsingHelper {
}
/**
* Check if the streaming URL is a URL from the YouTube {@code WEB} client.
* Check if the streaming URL is from the YouTube {@code WEB} client.
*
* @param url the streaming URL on which check if it's a {@code WEB} streaming URL.
* @param url the streaming URL to be checked.
* @return true if it's a {@code WEB} streaming URL, false otherwise
*/
public static boolean isWebStreamingUrl(@Nonnull final String url) {
@ -1620,7 +1620,7 @@ public final class YoutubeParsingHelper {
/**
* Check if the streaming URL is a URL from the YouTube {@code ANDROID} client.
*
* @param url the streaming URL on which check if it's a {@code ANDROID} streaming URL.
* @param url the streaming URL to be checked.
* @return true if it's a {@code ANDROID} streaming URL, false otherwise
*/
public static boolean isAndroidStreamingUrl(@Nonnull final String url) {

View File

@ -0,0 +1,80 @@
package org.schabi.newpipe.extractor.services.youtube.extractors;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import javax.annotation.Nonnull;
import java.io.Serializable;
/**
* Class to build easier {@link org.schabi.newpipe.extractor.stream.Stream}s for
* {@link YoutubeStreamExtractor}.
*
* <p>
* It stores, per stream:
* <ul>
* <li>its content (the URL/the base URL of streams);</li>
* <li>whether its content is the URL the content itself or the base URL;</li>
* <li>its associated {@link ItagItem}.</li>
* </ul>
* </p>
*/
final class ItagInfo implements Serializable {
@Nonnull
private final String content;
@Nonnull
private final ItagItem itagItem;
private boolean isUrl;
/**
* Creates a new {@code ItagInfo} instance.
*
* @param content the content of the stream, which must be not null
* @param itagItem the {@link ItagItem} associated with the stream, which must be not null
*/
ItagInfo(@Nonnull final String content,
@Nonnull final ItagItem itagItem) {
this.content = content;
this.itagItem = itagItem;
}
/**
* Sets whether the stream is a URL.
*
* @param isUrl whether the content is a URL
*/
void setIsUrl(final boolean isUrl) {
this.isUrl = isUrl;
}
/**
* Gets the content stored in this {@code ItagInfo} instance, which is either the URL to the
* content itself or the base URL.
*
* @return the content stored in this {@code ItagInfo} instance
*/
@Nonnull
String getContent() {
return content;
}
/**
* Gets the {@link ItagItem} associated with this {@code ItagInfo} instance.
*
* @return the {@link ItagItem} associated with this {@code ItagInfo} instance, which is not
* null
*/
@Nonnull
ItagItem getItagItem() {
return itagItem;
}
/**
* Gets whether the content stored is the URL to the content itself or the base URL of it.
*
* @return whether the content stored is the URL to the content itself or the base URL of it
* @see #getContent() for more details
*/
boolean getIsUrl() {
return isUrl;
}
}

View File

@ -25,6 +25,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.RACY_CHECK_OK;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.VIDEO_ID;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.createDesktopPlayerBody;
import static org.schabi.newpipe.extractor.services.youtube.ItagItem.CONTENT_LENGTH_UNKNOWN;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.fixThumbnailUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateContentPlaybackNonce;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.generateTParameter;
@ -66,7 +67,6 @@ import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
import org.schabi.newpipe.extractor.services.youtube.ItagInfo;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptExtractor;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
@ -666,9 +666,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
}
private void setStreamType() {
if (playerResponse.getObject("playabilityStatus").has("liveStreamability")
|| playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) {
if (playerResponse.getObject("playabilityStatus").has("liveStreamability")) {
streamType = StreamType.LIVE_STREAM;
} else if (playerResponse.getObject("videoDetails").getBoolean("isPostLiveDvr", false)) {
streamType = StreamType.POST_LIVE_STREAM;
} else {
streamType = StreamType.VIDEO_STREAM;
}
@ -1171,7 +1172,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
for (final Pair<JsonObject, String> pair : streamingDataAndCpnLoopList) {
itagInfos.addAll(getStreamsFromStreamingDataKey(pair.getFirst(), streamingDataKey,
itagTypeWanted, streamType, pair.getSecond()));
itagTypeWanted, pair.getSecond()));
}
final List<T> streamList = new ArrayList<>();
@ -1189,6 +1190,32 @@ public class YoutubeStreamExtractor extends StreamExtractor {
}
}
/**
* Get the {@link StreamBuilderHelper} which will be used to build {@link AudioStream}s in
* {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)}
*
* <p>
* The {@code StreamBuilderHelper} will set the following attributes in the
* {@link AudioStream}s built:
* <ul>
* <li>the {@link ItagItem}'s id of the stream as its id;</li>
* <li>{@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and
* and as the value of {@code isUrl};</li>
* <li>the media format returned by the {@link ItagItem} as its media format;</li>
* <li>its average bitrate with the value returned by {@link
* ItagItem#getAverageBitrate()};</li>
* <li>the {@link ItagItem};</li>
* <li>the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams
* and ended streams.</li>
* </ul>
* </p>
*
* <p>
* Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
* </p>
*
* @return a {@link StreamBuilderHelper} to build {@link AudioStream}s
*/
@Nonnull
private StreamBuilderHelper<AudioStream> getAudioStreamBuilderHelper() {
return new StreamBuilderHelper<AudioStream>() {
@ -1203,9 +1230,11 @@ public class YoutubeStreamExtractor extends StreamExtractor {
.setAverageBitrate(itagItem.getAverageBitrate())
.setItagItem(itagItem);
if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) {
// YouTube uses the DASH delivery method for videos on OTF streams and
// for all streams of post-live streams and live streams
if (streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.POST_LIVE_STREAM
|| !itagInfo.getIsUrl()) {
// For YouTube videos on OTF streams and for all streams of post-live streams
// and live streams, only the DASH delivery method can be used.
builder.setDeliveryMethod(DeliveryMethod.DASH);
}
@ -1214,6 +1243,40 @@ public class YoutubeStreamExtractor extends StreamExtractor {
};
}
/**
* Get the {@link StreamBuilderHelper} which will be used to build {@link VideoStream}s in
* {@link #getItags(String, ItagItem.ItagType, StreamBuilderHelper, String)}
*
* <p>
* The {@code StreamBuilderHelper} will set the following attributes in the
* {@link VideoStream}s built:
* <ul>
* <li>the {@link ItagItem}'s id of the stream as its id;</li>
* <li>{@link ItagInfo#getContent()} and {@link ItagInfo#getIsUrl()} as its content and
* and as the value of {@code isUrl};</li>
* <li>the media format returned by the {@link ItagItem} as its media format;</li>
* <li>whether it is video-only with the {@code areStreamsVideoOnly} parameter</li>
* <li>the {@link ItagItem};</li>
* <li>the resolution, by trying to use, in this order:
* <ol>
* <li>the height returned by the {@link ItagItem} + {@code p} + the frame rate if
* it is more than 30;</li>
* <li>the default resolution string from the {@link ItagItem};</li>
* <li>an {@link Utils#EMPTY_STRING empty string}.</li>
* </ol>
* </li>
* <li>the {@link DeliveryMethod#DASH DASH delivery method}, for OTF streams, live streams
* and ended streams.</li>
* </ul>
*
* <p>
* Note that the {@link ItagItem} comes from an {@link ItagInfo} instance.
* </p>
*
* @param areStreamsVideoOnly whether the {@link StreamBuilderHelper} will set the video
* streams as video-only streams
* @return a {@link StreamBuilderHelper} to build {@link VideoStream}s
*/
@Nonnull
private StreamBuilderHelper<VideoStream> getVideoStreamBuilderHelper(
final boolean areStreamsVideoOnly) {
@ -1241,12 +1304,13 @@ public class YoutubeStreamExtractor extends StreamExtractor {
builder.setResolution(stringBuilder.toString());
} else {
final String resolutionString = itagItem.getResolutionString();
builder.setResolution(resolutionString != null ? resolutionString : "");
builder.setResolution(resolutionString != null ? resolutionString
: EMPTY_STRING);
}
if (streamType != StreamType.VIDEO_STREAM || !itagInfo.getIsUrl()) {
// YouTube uses the DASH delivery method for videos on OTF streams and
// for all streams of post-live streams and live streams
// For YouTube videos on OTF streams and for all streams of post-live streams
// and live streams, only the DASH delivery method can be used.
builder.setDeliveryMethod(DeliveryMethod.DASH);
}
@ -1260,15 +1324,15 @@ public class YoutubeStreamExtractor extends StreamExtractor {
final JsonObject streamingData,
final String streamingDataKey,
@Nonnull final ItagItem.ItagType itagTypeWanted,
@Nonnull final StreamType contentStreamType,
@Nonnull final String contentPlaybackNonce) {
@Nonnull final String contentPlaybackNonce) throws ParsingException {
if (streamingData == null || !streamingData.has(streamingDataKey)) {
return Collections.emptyList();
}
final String videoId = getId();
final List<ItagInfo> itagInfos = new ArrayList<>();
final JsonArray formats = streamingData.getArray(streamingDataKey);
for (int i = 0; i < formats.size(); i++) {
for (int i = 0; i != formats.size(); ++i) {
final JsonObject formatData = formats.getObject(i);
final int itag = formatData.getInt("itag");
@ -1279,79 +1343,10 @@ public class YoutubeStreamExtractor extends StreamExtractor {
try {
final ItagItem itagItem = ItagItem.getItag(itag);
final ItagItem.ItagType itagType = itagItem.itagType;
if (itagItem.itagType != itagTypeWanted) {
continue;
if (itagType == itagTypeWanted) {
buildAndAddItagInfoToList(videoId, itagInfos, formatData, itagItem,
itagType, contentPlaybackNonce);
}
String streamUrl;
if (formatData.has("url")) {
streamUrl = formatData.getString("url") + "&cpn="
+ contentPlaybackNonce;
} else {
// This url has an obfuscated signature
final String cipherString = formatData.has(CIPHER)
? formatData.getString(CIPHER)
: formatData.getString(SIGNATURE_CIPHER);
final Map<String, String> cipher = Parser.compatParseMap(
cipherString);
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+ deobfuscateSignature(cipher.get("s"));
}
if (isWebStreamingUrl(streamUrl)) {
streamUrl = tryDecryptUrl(streamUrl, getId()) + "&cver="
+ getClientVersion();
}
final JsonObject initRange = formatData.getObject("initRange");
final JsonObject indexRange = formatData.getObject("indexRange");
final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
final String codec = mimeType.contains("codecs")
? mimeType.split("\"")[1] : EMPTY_STRING;
itagItem.setBitrate(formatData.getInt("bitrate"));
itagItem.setWidth(formatData.getInt("width"));
itagItem.setHeight(formatData.getInt("height"));
itagItem.setInitStart(Integer.parseInt(initRange.getString("start",
"-1")));
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end",
"-1")));
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start",
"-1")));
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end",
"-1")));
itagItem.setQuality(formatData.getString("quality"));
itagItem.setCodec(codec);
if (contentStreamType != StreamType.VIDEO_STREAM) {
itagItem.setTargetDurationSec(formatData.getInt(
"targetDurationSec"));
}
if (itagType == ItagItem.ItagType.VIDEO
|| itagType == ItagItem.ItagType.VIDEO_ONLY) {
itagItem.setFps(formatData.getInt("fps"));
}
if (itagType == ItagItem.ItagType.AUDIO) {
itagItem.setSampleRate(Integer.parseInt(formatData.getString(
"audioSampleRate")));
itagItem.setAudioChannels(formatData.getInt("audioChannels"));
}
itagItem.setContentLength(Long.parseLong(formatData.getString(
"contentLength", "-1")));
final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem);
if (contentStreamType == StreamType.VIDEO_STREAM) {
itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING)
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF"));
} else {
// We are currently not able to generate DASH manifests for running
// livestreams, so because of the requirements of StreamInfo
// objects, return these streams as DASH URL streams (even if they
// are not playable).
// Ended livestreams are returned as non URL streams
itagInfo.setIsUrl(contentStreamType != StreamType.POST_LIVE_STREAM);
}
itagInfos.add(itagInfo);
} catch (final IOException | ExtractionException ignored) {
}
}
@ -1359,6 +1354,83 @@ public class YoutubeStreamExtractor extends StreamExtractor {
return itagInfos;
}
private void buildAndAddItagInfoToList(
@Nonnull final String videoId,
@Nonnull final List<ItagInfo> itagInfos,
@Nonnull final JsonObject formatData,
@Nonnull final ItagItem itagItem,
@Nonnull final ItagItem.ItagType itagType,
@Nonnull final String contentPlaybackNonce) throws IOException, ExtractionException {
String streamUrl;
if (formatData.has("url")) {
streamUrl = formatData.getString("url");
} else {
// This url has an obfuscated signature
final String cipherString = formatData.has(CIPHER)
? formatData.getString(CIPHER)
: formatData.getString(SIGNATURE_CIPHER);
final Map<String, String> cipher = Parser.compatParseMap(
cipherString);
streamUrl = cipher.get("url") + "&" + cipher.get("sp") + "="
+ deobfuscateSignature(cipher.get("s"));
}
// Add the content playback nonce to the stream URL
streamUrl += "&" + CPN + "=" + contentPlaybackNonce;
if (isWebStreamingUrl(streamUrl)) {
streamUrl = tryDecryptUrl(streamUrl, videoId) + "&cver=" + getClientVersion();
}
final JsonObject initRange = formatData.getObject("initRange");
final JsonObject indexRange = formatData.getObject("indexRange");
final String mimeType = formatData.getString("mimeType", EMPTY_STRING);
final String codec = mimeType.contains("codecs")
? mimeType.split("\"")[1] : EMPTY_STRING;
itagItem.setBitrate(formatData.getInt("bitrate"));
itagItem.setWidth(formatData.getInt("width"));
itagItem.setHeight(formatData.getInt("height"));
itagItem.setInitStart(Integer.parseInt(initRange.getString("start", "-1")));
itagItem.setInitEnd(Integer.parseInt(initRange.getString("end", "-1")));
itagItem.setIndexStart(Integer.parseInt(indexRange.getString("start", "-1")));
itagItem.setIndexEnd(Integer.parseInt(indexRange.getString("end", "-1")));
itagItem.setQuality(formatData.getString("quality"));
itagItem.setCodec(codec);
if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) {
itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec"));
}
if (itagType == ItagItem.ItagType.VIDEO || itagType == ItagItem.ItagType.VIDEO_ONLY) {
itagItem.setFps(formatData.getInt("fps"));
}
if (itagType == ItagItem.ItagType.AUDIO) {
// YouTube return the audio sample rate as a string
itagItem.setSampleRate(Integer.parseInt(formatData.getString("audioSampleRate")));
itagItem.setAudioChannels(formatData.getInt("audioChannels"));
}
// YouTube return the content length as a string
itagItem.setContentLength(Long.parseLong(formatData.getString("contentLength",
String.valueOf(CONTENT_LENGTH_UNKNOWN))));
final ItagInfo itagInfo = new ItagInfo(streamUrl, itagItem);
if (streamType == StreamType.VIDEO_STREAM) {
itagInfo.setIsUrl(!formatData.getString("type", EMPTY_STRING)
.equalsIgnoreCase("FORMAT_STREAM_TYPE_OTF"));
} else {
// We are currently not able to generate DASH manifests for running
// livestreams, so because of the requirements of StreamInfo
// objects, return these streams as DASH URL streams (even if they
// are not playable).
// Ended livestreams are returned as non URL streams
itagInfo.setIsUrl(streamType != StreamType.POST_LIVE_STREAM);
}
itagInfos.add(itagInfo);
}
@Nonnull
@Override

View File

@ -68,10 +68,10 @@ public final class AudioStream extends Stream {
}
/**
* Set the identifier of the {@link SubtitlesStream}.
* Set the identifier of the {@link AudioStream}.
*
* <p>
* It <b>must be not null</b> and should be non empty.
* It <b>must not be null</b> and should be non empty.
* </p>
*
* <p>
@ -79,7 +79,7 @@ public final class AudioStream extends Stream {
* Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
* </p>
*
* @param id the identifier of the {@link SubtitlesStream}, which must be not null
* @param id the identifier of the {@link AudioStream}, which must not be null
* @return this {@link Builder} instance
*/
public Builder setId(@Nonnull final String id) {
@ -91,7 +91,7 @@ public final class AudioStream extends Stream {
* Set the content of the {@link AudioStream}.
*
* <p>
* It must be non null and should be non empty.
* It must not be null, and should be non empty.
* </p>
*
* @param content the content of the {@link AudioStream}
@ -111,8 +111,8 @@ public final class AudioStream extends Stream {
* <p>
* It should be one of the audio {@link MediaFormat}s ({@link MediaFormat#M4A M4A},
* {@link MediaFormat#WEBMA WEBMA}, {@link MediaFormat#MP3 MP3}, {@link MediaFormat#OPUS
* OPUS}, {@link MediaFormat#OGG OGG}, {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but can
* be {@code null} if the media format could not be determined.
* OPUS}, {@link MediaFormat#OGG OGG}, or {@link MediaFormat#WEBMA_OPUS WEBMA_OPUS}) but
* can be {@code null} if the media format could not be determined.
* </p>
*
* <p>
@ -131,7 +131,7 @@ public final class AudioStream extends Stream {
* Set the {@link DeliveryMethod} of the {@link AudioStream}.
*
* <p>
* It must be not null.
* It must not be null.
* </p>
*
* <p>
@ -139,7 +139,7 @@ public final class AudioStream extends Stream {
* </p>
*
* @param deliveryMethod the {@link DeliveryMethod} of the {@link AudioStream}, which must
* be not null
* not be null
* @return this {@link Builder} instance
*/
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
@ -151,8 +151,8 @@ public final class AudioStream extends Stream {
* Set the base URL of the {@link AudioStream}.
*
* <p>
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
* they have been parsed.
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
* from which the URLs have been parsed.
* </p>
*
* <p>
@ -213,7 +213,7 @@ public final class AudioStream extends Stream {
*
* @return a new {@link AudioStream} using the builder's current values
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}) or
* {@code deliveryMethod} have been not set or set as {@code null}
* {@code deliveryMethod} have been not set, or have been set as {@code null}
*/
@Nonnull
public AudioStream build() {
@ -244,8 +244,8 @@ public final class AudioStream extends Stream {
/**
* Create a new audio stream.
*
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
* would be the itag
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
* this would be the itag
* @param content the content or the URL of the stream, depending on whether isUrl is
* true
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
@ -258,6 +258,7 @@ public final class AudioStream extends Stream {
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
* information)
*/
@SuppressWarnings("checkstyle:ParameterNumber")
private AudioStream(@Nonnull final String id,
@Nonnull final String content,
final boolean isUrl,

View File

@ -13,25 +13,43 @@ public enum DeliveryMethod {
PROGRESSIVE_HTTP,
/**
* Enum constant which represents the use of the DASH adaptive streaming method to fetch a
* {@link Stream stream}.
* Enum constant which represents the use of the DASH (Dynamic Adaptive Streaming over HTTP)
* adaptive streaming method to fetch a {@link Stream stream}.
*
* @see <a href="https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP">the
* Dynamic Adaptive Streaming over HTTP Wikipedia page</a> and <a href="https://dashif.org/">
* DASH Industry Forum's website</a> for more information about the DASH delivery method
*/
DASH,
/**
* Enum constant which represents the use of the HLS adaptive streaming method to fetch a
* {@link Stream stream}.
* Enum constant which represents the use of the HLS (HTTP Live Streaming) adaptive streaming
* method to fetch a {@link Stream stream}.
*
* @see <a href="https://en.wikipedia.org/wiki/HTTP_Live_Streaming">the HTTP Live Streaming
* page</a> and <a href="https://developer.apple.com/streaming">Apple's developers website page
* about HTTP Live Streaming</a> for more information about the HLS delivery method
*/
HLS,
/**
* Enum constant which represents the use of the SmoothStreaming adaptive streaming method to
* fetch a {@link Stream stream}.
*
* @see <a href="https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming
* #Microsoft_Smooth_Streaming_(MSS)">Wikipedia's page about adaptive bitrate streaming,
* section <i>Microsoft Smooth Streaming (MSS)</i></a> for more information about the
* SmoothStreaming delivery method
*/
SS,
/**
* Enum constant which represents the use of a torrent to fetch a {@link Stream stream}.
* Enum constant which represents the use of a torrent file to fetch a {@link Stream stream}.
*
* @see <a href="https://en.wikipedia.org/wiki/BitTorrent">Wikipedia's BitTorrent's page</a>,
* <a href="https://en.wikipedia.org/wiki/Torrent_file">Wikipedia's page about torrent files
* </a> and <a href=""https://www.bittorrent.org/></a> for more information about the
* BitTorrent protocol
*/
TORRENT
}

View File

@ -19,11 +19,11 @@ public abstract class Stream implements Serializable {
public static final String ID_UNKNOWN = " ";
/**
* An integer to represent that the itag id returned is not available (only for YouTube, this
* An integer to represent that the itag ID returned is not available (only for YouTube; this
* should never happen) or not applicable (for other services than YouTube).
*
* <p>
* An itag should not have a negative value so {@code -1} is used for this constant.
* An itag should not have a negative value, so {@code -1} is used for this constant.
* </p>
*/
public static final int ITAG_NOT_AVAILABLE_OR_NOT_APPLICABLE = -1;
@ -38,8 +38,8 @@ public abstract class Stream implements Serializable {
/**
* Instantiates a new {@code Stream} object.
*
* @param id the ID which uniquely identifies the file, e.g. for YouTube this would
* be the itag
* @param id the identifier which uniquely identifies the file, e.g. for YouTube
* this would be the itag
* @param content the content or URL, depending on whether isUrl is true
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
* manifest
@ -63,10 +63,10 @@ public abstract class Stream implements Serializable {
}
/**
* Checks if the list already contains one stream with equals stats.
* Checks if the list already contains a stream with the same statistics.
*
* @param stream the stream which will be compared to the streams in the stream list
* @param streamList the list of {@link Stream Streams} which will be compared
* @param stream the stream to be compared against the streams in the stream list
* @param streamList the list of {@link Stream}s which will be compared
* @return whether the list already contains one stream with equals stats
*/
public static boolean containSimilarStream(final Stream stream,
@ -83,16 +83,16 @@ public abstract class Stream implements Serializable {
}
/**
* Reveals whether two streams have the same stats ({@link MediaFormat media format} and
* Reveals whether two streams have the same statistics ({@link MediaFormat media format} and
* {@link DeliveryMethod delivery method}).
*
* <p>
* If the {@link MediaFormat media format} of the stream is unknown, the streams are compared
* by only using the {@link DeliveryMethod delivery method} and their id.
* by using only the {@link DeliveryMethod delivery method} and their ID.
* </p>
*
* <p>
* Note: This method always returns always false if the stream passed is null.
* Note: This method always returns false if the stream passed is null.
* </p>
*
* @param cmp the stream object to be compared to this stream object
@ -118,9 +118,12 @@ public abstract class Stream implements Serializable {
/**
* Reveals whether two streams are equal.
*
* @param cmp the stream object to be compared to this stream object
* @return whether streams are equal
* @param cmp a {@link Stream} object to be compared to this {@link Stream} instance.
* @return whether the compared streams are equal
* @deprecated Use {@link #equalStats(Stream)} to compare statistics of two streams and
* {@link #equals(Object)} to compare the equality of two streams instead.
*/
@Deprecated
public boolean equals(final Stream cmp) {
return equalStats(cmp) && content.equals(cmp.content);
}
@ -129,19 +132,19 @@ public abstract class Stream implements Serializable {
* Gets the identifier of this stream, e.g. the itag for YouTube.
*
* <p>
* It should be normally unique but {@link #ID_UNKNOWN} may be returned as the identifier if
* one used by the stream extractor cannot be extracted, if the extractor uses a value from a
* streaming service.
* It should normally be unique, but {@link #ID_UNKNOWN} may be returned as the identifier if
* the one used by the stream extractor cannot be extracted, which could happen if the
* extractor uses a value from a streaming service.
* </p>
*
* @return the id (which may be {@link #ID_UNKNOWN})
* @return the identifier (which may be {@link #ID_UNKNOWN})
*/
public String getId() {
return id;
}
/**
* Gets the URL of this stream if the content is a URL, or {@code null} if that's the not case.
* Gets the URL of this stream if the content is a URL, or {@code null} otherwise.
*
* @return the URL if the content is a URL, {@code null} otherwise
* @deprecated Use {@link #getContent()} instead.
@ -162,10 +165,10 @@ public abstract class Stream implements Serializable {
}
/**
* Returns if the content is a URL or not.
* Returns whether the content is a URL or not.
*
* @return {@code true} if the content of this stream content is a URL, {@code false}
* if it is the actual content
* @return {@code true} if the content of this stream is a URL, {@code false} if it's the
* actual content
*/
public boolean isUrl() {
return isUrl;
@ -182,9 +185,9 @@ public abstract class Stream implements Serializable {
}
/**
* Gets the format id, which can be unknown.
* Gets the format ID, which can be unknown.
*
* @return the format id or {@link #FORMAT_ID_UNKNOWN}
* @return the format ID or {@link #FORMAT_ID_UNKNOWN}
*/
public int getFormatId() {
if (mediaFormat != null) {
@ -208,7 +211,7 @@ public abstract class Stream implements Serializable {
*
* <p>
* If the stream is not a DASH stream or an HLS stream, this value will always be null.
* It may be also null for these streams too.
* It may also be null for these streams too.
* </p>
*
* @return the base URL of the stream or {@code null}
@ -222,7 +225,7 @@ public abstract class Stream implements Serializable {
* Gets the {@link ItagItem} of a stream.
*
* <p>
* If the stream is not a YouTube stream, this value will always be null.
* If the stream is not from YouTube, this value will always be null.
* </p>
*
* @return the {@link ItagItem} of the stream or {@code null}
@ -242,11 +245,14 @@ public abstract class Stream implements Serializable {
final Stream stream = (Stream) obj;
return id.equals(stream.id) && mediaFormat == stream.mediaFormat
&& deliveryMethod == stream.deliveryMethod;
&& deliveryMethod == stream.deliveryMethod
&& content.equals(stream.content)
&& isUrl == stream.isUrl
&& Objects.equals(baseUrl, stream.baseUrl);
}
@Override
public int hashCode() {
return Objects.hash(id, mediaFormat, deliveryMethod);
return Objects.hash(id, mediaFormat, deliveryMethod, content, isUrl, baseUrl);
}
}
}

View File

@ -17,10 +17,10 @@ public enum StreamType {
NONE,
/**
* Enum constant to indicate that the stream type of stream content is a video.
* Enum constant to indicate that the stream type of stream content is a live video.
*
* <p>
* Note that contents <strong>can contain audio streams</strong> even if they also contain
* Note that contents <strong>may contain audio streams</strong> even if they also contain
* video streams (video-only or video with audio, depending of the stream/the content/the
* service).
* </p>
@ -46,23 +46,22 @@ public enum StreamType {
*
* <p>
* Note that contents <strong>can contain audio live streams</strong> even if they also contain
* live video streams (video-only or video with audio, depending of the stream/the content/the
* service).
* live video streams (so video-only or video with audio, depending on the stream/the content/
* the service).
* </p>
*/
LIVE_STREAM,
/**
* Enum constant to indicate that the stream type of stream content is a live audio content.
* Enum constant to indicate that the stream type of stream content is a live audio.
*
* <p>
* Note that contents returned as live audio streams should not return live video streams.
* </p>
*
* <p>
* So, in order to prevent unexpected behaviors, stream extractors which are returning this
* stream type for a content should ensure that no live video stream is returned for this
* content.
* To prevent unexpected behavior, stream extractors which are returning this stream type for a
* content should ensure that no live video stream is returned along with it.
* </p>
*/
AUDIO_LIVE_STREAM,
@ -72,10 +71,10 @@ public enum StreamType {
* ended live video stream.
*
* <p>
* Note that most of ended live video (or audio) contents may be extracted as
* {@link #VIDEO_STREAM regular video contents} (or
* {@link #AUDIO_STREAM regular audio contents}) later, because the service may encode them
* again later as normal video/audio streams. That's the case for example on YouTube.
* Note that most of the content of an ended live video (or audio) may be extracted as {@link
* #VIDEO_STREAM regular video contents} (or {@link #AUDIO_STREAM regular audio contents})
* later, because the service may encode them again later as normal video/audio streams. That's
* the case on YouTube, for example.
* </p>
*
* <p>

View File

@ -35,7 +35,7 @@ public final class SubtitlesStream extends Stream {
private Boolean autoGenerated;
/**
* Create a new {@link Builder} instance with its default values.
* Create a new {@link Builder} instance with default values.
*/
public Builder() {
}
@ -43,7 +43,7 @@ public final class SubtitlesStream extends Stream {
/**
* Set the identifier of the {@link SubtitlesStream}.
*
* @param id the identifier of the {@link SubtitlesStream}, which should be not null
* @param id the identifier of the {@link SubtitlesStream}, which should not be null
* (otherwise the fallback to create the identifier will be used when building
* the builder)
* @return this {@link Builder} instance
@ -57,10 +57,10 @@ public final class SubtitlesStream extends Stream {
* Set the content of the {@link SubtitlesStream}.
*
* <p>
* It must be non null and should be non empty.
* It must not be null, and should be non empty.
* </p>
*
* @param content the content of the {@link SubtitlesStream}
* @param content the content of the {@link SubtitlesStream}, which must not be null
* @param isUrl whether the content is a URL
* @return this {@link Builder} instance
*/
@ -78,7 +78,7 @@ public final class SubtitlesStream extends Stream {
* It should be one of the subtitles {@link MediaFormat}s ({@link MediaFormat#SRT SRT},
* {@link MediaFormat#TRANSCRIPT1 TRANSCRIPT1}, {@link MediaFormat#TRANSCRIPT2
* TRANSCRIPT2}, {@link MediaFormat#TRANSCRIPT3 TRANSCRIPT3}, {@link MediaFormat#TTML
* TTML}, {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could
* TTML}, or {@link MediaFormat#VTT VTT}) but can be {@code null} if the media format could
* not be determined.
* </p>
*
@ -99,7 +99,7 @@ public final class SubtitlesStream extends Stream {
* Set the {@link DeliveryMethod} of the {@link SubtitlesStream}.
*
* <p>
* It must be not null.
* It must not be null.
* </p>
*
* <p>
@ -107,7 +107,7 @@ public final class SubtitlesStream extends Stream {
* </p>
*
* @param deliveryMethod the {@link DeliveryMethod} of the {@link SubtitlesStream}, which
* must be not null
* must not be null
* @return this {@link Builder} instance
*/
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
@ -119,8 +119,8 @@ public final class SubtitlesStream extends Stream {
* Set the base URL of the {@link SubtitlesStream}.
*
* <p>
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
* they have been parsed.
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
* from which the URLs have been parsed.
* </p>
*
* <p>
@ -139,7 +139,7 @@ public final class SubtitlesStream extends Stream {
* Set the language code of the {@link SubtitlesStream}.
*
* <p>
* It <b>must be not null</b> and should be not an empty string.
* It <b>must not be null</b> and should not be an empty string.
* </p>
*
* @param languageCode the language code of the {@link SubtitlesStream}
@ -151,10 +151,10 @@ public final class SubtitlesStream extends Stream {
}
/**
* Set whether the subtitles have been generated by the streaming service.
* Set whether the subtitles have been auto-generated by the streaming service.
*
* @param autoGenerated whether the subtitles have been generated by the streaming
* service
* service
* @return this {@link Builder} instance
*/
public Builder setAutoGenerated(final boolean autoGenerated) {
@ -172,13 +172,13 @@ public final class SubtitlesStream extends Stream {
*
* <p>
* If no identifier has been set, an identifier will be generated using the language code
* and the media format suffix if the media format is known
* and the media format suffix, if the media format is known.
* </p>
*
* @return a new {@link SubtitlesStream} using the builder's current values
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}),
* {@code deliveryMethod}, {@code languageCode} or the {@code isAutogenerated} have been
* not set or set as {@code null}
* not set, or have been set as {@code null}
*/
@Nonnull
public SubtitlesStream build() {
@ -219,28 +219,29 @@ public final class SubtitlesStream extends Stream {
/**
* Create a new subtitles stream.
*
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
* would be the itag
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
* this would be the itag
* @param content the content or the URL of the stream, depending on whether isUrl is
* true
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
* manifest
* @param format the {@link MediaFormat} used by the stream
* @param mediaFormat the {@link MediaFormat} used by the stream
* @param deliveryMethod the {@link DeliveryMethod} of the stream
* @param languageCode the language code of the stream
* @param autoGenerated whether the subtitles are auto-generated by the streaming service
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
* information)
*/
@SuppressWarnings("checkstyle:ParameterNumber")
private SubtitlesStream(@Nonnull final String id,
@Nonnull final String content,
final boolean isUrl,
@Nullable final MediaFormat format,
@Nullable final MediaFormat mediaFormat,
@Nonnull final DeliveryMethod deliveryMethod,
@Nonnull final String languageCode,
final boolean autoGenerated,
@Nullable final String baseUrl) {
super(id, content, isUrl, format, deliveryMethod, baseUrl);
super(id, content, isUrl, mediaFormat, deliveryMethod, baseUrl);
/*
* Locale.forLanguageTag only for Android API >= 21
@ -262,7 +263,7 @@ public final class SubtitlesStream extends Stream {
}
this.code = languageCode;
this.format = format;
this.format = mediaFormat;
this.autoGenerated = autoGenerated;
}

View File

@ -81,7 +81,7 @@ public final class VideoStream extends Stream {
* Set the identifier of the {@link VideoStream}.
*
* <p>
* It <b>must be not null</b> and should be non empty.
* It must not be null, and should be non empty.
* </p>
*
* <p>
@ -89,7 +89,7 @@ public final class VideoStream extends Stream {
* Stream#ID_UNKNOWN ID_UNKNOWN} of the {@link Stream} class.
* </p>
*
* @param id the identifier of the {@link VideoStream}, which must be not null
* @param id the identifier of the {@link VideoStream}, which must not be null
* @return this {@link Builder} instance
*/
public Builder setId(@Nonnull final String id) {
@ -101,7 +101,7 @@ public final class VideoStream extends Stream {
* Set the content of the {@link VideoStream}.
*
* <p>
* It must be non null and should be non empty.
* It must not be null, and should be non empty.
* </p>
*
* @param content the content of the {@link VideoStream}
@ -120,8 +120,8 @@ public final class VideoStream extends Stream {
*
* <p>
* It should be one of the video {@link MediaFormat}s ({@link MediaFormat#MPEG_4 MPEG_4},
* {@link MediaFormat#v3GPP v3GPP}, {@link MediaFormat#WEBM WEBM}) but can be {@code null}
* if the media format could not be determined.
* {@link MediaFormat#v3GPP v3GPP}, or {@link MediaFormat#WEBM WEBM}) but can be {@code
* null} if the media format could not be determined.
* </p>
*
* <p>
@ -140,7 +140,7 @@ public final class VideoStream extends Stream {
* Set the {@link DeliveryMethod} of the {@link VideoStream}.
*
* <p>
* It must be not null.
* It must not be null.
* </p>
*
* <p>
@ -148,7 +148,7 @@ public final class VideoStream extends Stream {
* </p>
*
* @param deliveryMethod the {@link DeliveryMethod} of the {@link VideoStream}, which must
* be not null
* not be null
* @return this {@link Builder} instance
*/
public Builder setDeliveryMethod(@Nonnull final DeliveryMethod deliveryMethod) {
@ -160,8 +160,8 @@ public final class VideoStream extends Stream {
* Set the base URL of the {@link VideoStream}.
*
* <p>
* Base URLs are for instance, for non-URLs content, the DASH or HLS manifest from which
* they have been parsed.
* For non-URL contents, the base URL is, for instance, a link to the DASH or HLS manifest
* from which the URLs have been parsed.
* </p>
*
* <p>
@ -245,8 +245,8 @@ public final class VideoStream extends Stream {
*
* @return a new {@link VideoStream} using the builder's current values
* @throws IllegalStateException if {@code id}, {@code content} (and so {@code isUrl}),
* {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set or
* set as {@code null}
* {@code deliveryMethod}, {@code isVideoOnly} or {@code resolution} have been not set, or
* have been set as {@code null}
*/
@Nonnull
public VideoStream build() {
@ -289,8 +289,8 @@ public final class VideoStream extends Stream {
/**
* Create a new video stream.
*
* @param id the ID which uniquely identifies the stream, e.g. for YouTube this
* would be the itag
* @param id the identifier which uniquely identifies the stream, e.g. for YouTube
* this would be the itag
* @param content the content or the URL of the stream, depending on whether isUrl is
* true
* @param isUrl whether content is the URL or the actual content of e.g. a DASH
@ -303,6 +303,7 @@ public final class VideoStream extends Stream {
* @param baseUrl the base URL of the stream (see {@link Stream#getBaseUrl()} for more
* information)
*/
@SuppressWarnings("checkstyle:ParameterNumber")
private VideoStream(@Nonnull final String id,
@Nonnull final String content,
final boolean isUrl,

View File

@ -53,7 +53,7 @@ class YoutubeDashManifestCreatorTest {
// Setting a higher number may let Google video servers return a lot of 403s
private static final int MAXIMUM_NUMBER_OF_STREAMS_TO_TEST = 3;
public static class testGenerationOfOtfAndProgressiveManifests {
public static class TestGenerationOfOtfAndProgressiveManifests {
private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM";
private static YoutubeStreamExtractor extractor;