Merge pull request #1026 from AudricV/audio-streams-descriptive-and-locale-properties

Add descriptive and locale properties to audio streams
This commit is contained in:
Stypox 2023-03-01 11:15:46 +01:00 committed by GitHub
commit 6bdd698c25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1907 additions and 47 deletions

View File

@ -24,6 +24,7 @@ import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.LocaleCompat;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -114,15 +115,24 @@ public class MediaCCCStreamExtractor extends StreamExtractor {
mediaFormat = null; mediaFormat = null;
} }
// Not checking containsSimilarStream here, since MediaCCC does not provide enough final AudioStream.Builder builder = new AudioStream.Builder()
// information to decide whether two streams are similar. Hence that method would
// always return false, e.g. even for different language variations.
audioStreams.add(new AudioStream.Builder()
.setId(recording.getString("filename", ID_UNKNOWN)) .setId(recording.getString("filename", ID_UNKNOWN))
.setContent(recording.getString("recording_url"), true) .setContent(recording.getString("recording_url"), true)
.setMediaFormat(mediaFormat) .setMediaFormat(mediaFormat)
.setAverageBitrate(UNKNOWN_BITRATE) .setAverageBitrate(UNKNOWN_BITRATE);
.build());
final String language = recording.getString("language");
// If the language contains a - symbol, this means that the stream has an audio
// track with multiple languages, so there is no specific language for this stream
// Don't set the audio language in this case
if (language != null && !language.contains("-")) {
builder.setAudioLocale(LocaleCompat.forLanguageTag(language));
}
// Not checking containsSimilarStream here, since MediaCCC does not provide enough
// information to decide whether two streams are similar. Hence that method would
// always return false, e.g. even for different language variations.
audioStreams.add(builder.build());
} }
} }
return audioStreams; return audioStreams;

View File

@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.Serializable; import java.io.Serializable;
import java.util.Locale;
import static org.schabi.newpipe.extractor.MediaFormat.M4A; import static org.schabi.newpipe.extractor.MediaFormat.M4A;
import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4; import static org.schabi.newpipe.extractor.MediaFormat.MPEG_4;
@ -198,6 +199,10 @@ public class ItagItem implements Serializable {
this.targetDurationSec = itagItem.targetDurationSec; this.targetDurationSec = itagItem.targetDurationSec;
this.approxDurationMs = itagItem.approxDurationMs; this.approxDurationMs = itagItem.approxDurationMs;
this.contentLength = itagItem.contentLength; this.contentLength = itagItem.contentLength;
this.audioTrackId = itagItem.audioTrackId;
this.audioTrackName = itagItem.audioTrackName;
this.isDescriptiveAudio = itagItem.isDescriptiveAudio;
this.audioLocale = itagItem.audioLocale;
} }
public MediaFormat getMediaFormat() { public MediaFormat getMediaFormat() {
@ -246,6 +251,9 @@ public class ItagItem implements Serializable {
private long contentLength = CONTENT_LENGTH_UNKNOWN; private long contentLength = CONTENT_LENGTH_UNKNOWN;
private String audioTrackId; private String audioTrackId;
private String audioTrackName; private String audioTrackName;
private boolean isDescriptiveAudio;
@Nullable
private Locale audioLocale;
public int getBitrate() { public int getBitrate() {
return bitrate; return bitrate;
@ -569,7 +577,7 @@ public class ItagItem implements Serializable {
/** /**
* Get the {@code audioTrackName} of the stream, if present. * Get the {@code audioTrackName} of the stream, if present.
* *
* @return the {@code audioTrackName} of the stream or null * @return the {@code audioTrackName} of the stream or {@code null}
*/ */
@Nullable @Nullable
public String getAudioTrackName() { public String getAudioTrackName() {
@ -577,11 +585,53 @@ public class ItagItem implements Serializable {
} }
/** /**
* Set the {@code audioTrackName} of the stream. * Set the {@code audioTrackName} of the stream, if present.
* *
* @param audioTrackName the {@code audioTrackName} of the stream * @param audioTrackName the {@code audioTrackName} of the stream or {@code null}
*/ */
public void setAudioTrackName(@Nullable final String audioTrackName) { public void setAudioTrackName(@Nullable final String audioTrackName) {
this.audioTrackName = audioTrackName; this.audioTrackName = audioTrackName;
} }
/**
* Return whether the stream is a descriptive audio.
*
* @return whether the stream is a descriptive audio
*/
public boolean isDescriptiveAudio() {
return isDescriptiveAudio;
}
/**
* Set whether the stream is a descriptive audio.
*
* @param isDescriptiveAudio whether the stream is a descriptive audio
*/
public void setIsDescriptiveAudio(final boolean isDescriptiveAudio) {
this.isDescriptiveAudio = isDescriptiveAudio;
}
/**
* Return the audio {@link Locale} of the stream, if known.
*
* @return the audio {@link Locale} of the stream, if known, or {@code null} if that's not the
* case
*/
@Nullable
public Locale getAudioLocale() {
return audioLocale;
}
/**
* Set the audio {@link Locale} of the stream.
*
* <p>
* If it is unknown, {@code null} could be passed, which is the default value.
* </p>
*
* @param audioLocale the audio {@link Locale} of the stream, which could be {@code null}
*/
public void setAudioLocale(@Nullable final Locale audioLocale) {
this.audioLocale = audioLocale;
}
} }

View File

@ -124,7 +124,7 @@ public final class YoutubeDashManifestCreatorsUtils {
* <li>{@code Period} (using {@link #generatePeriodElement(Document)});</li> * <li>{@code Period} (using {@link #generatePeriodElement(Document)});</li>
* <li>{@code AdaptationSet} (using {@link #generateAdaptationSetElement(Document, * <li>{@code AdaptationSet} (using {@link #generateAdaptationSetElement(Document,
* ItagItem)});</li> * ItagItem)});</li>
* <li>{@code Role} (using {@link #generateRoleElement(Document)});</li> * <li>{@code Role} (using {@link #generateRoleElement(Document, ItagItem)});</li>
* <li>{@code Representation} (using {@link #generateRepresentationElement(Document, * <li>{@code Representation} (using {@link #generateRepresentationElement(Document,
* ItagItem)});</li> * ItagItem)});</li>
* <li>and, for audio streams, {@code AudioChannelConfiguration} (using * <li>and, for audio streams, {@code AudioChannelConfiguration} (using
@ -144,7 +144,7 @@ public final class YoutubeDashManifestCreatorsUtils {
generatePeriodElement(doc); generatePeriodElement(doc);
generateAdaptationSetElement(doc, itagItem); generateAdaptationSetElement(doc, itagItem);
generateRoleElement(doc); generateRoleElement(doc, itagItem);
generateRepresentationElement(doc, itagItem); generateRepresentationElement(doc, itagItem);
if (itagItem.itagType == ItagItem.ItagType.AUDIO) { if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
generateAudioChannelConfigurationElement(doc, itagItem); generateAudioChannelConfigurationElement(doc, itagItem);
@ -208,7 +208,7 @@ public final class YoutubeDashManifestCreatorsUtils {
* {@link #generateDocumentAndMpdElement(long)}. * {@link #generateDocumentAndMpdElement(long)}.
* </p> * </p>
* *
* @param doc the {@link Document} on which the the {@code <Period>} element will be appended * @param doc the {@link Document} on which the {@code <Period>} element will be appended
*/ */
public static void generatePeriodElement(@Nonnull final Document doc) public static void generatePeriodElement(@Nonnull final Document doc)
throws CreationException { throws CreationException {
@ -249,6 +249,16 @@ public final class YoutubeDashManifestCreatorsUtils {
"the MediaFormat or its mime type is null or empty"); "the MediaFormat or its mime type is null or empty");
} }
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
final Locale audioLocale = itagItem.getAudioLocale();
if (audioLocale != null) {
final String audioLanguage = audioLocale.getLanguage();
if (!audioLanguage.isEmpty()) {
setAttribute(adaptationSetElement, doc, "lang", audioLanguage);
}
}
}
setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType()); setAttribute(adaptationSetElement, doc, "mimeType", mediaFormat.getMimeType());
setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true"); setAttribute(adaptationSetElement, doc, "subsegmentAlignment", "true");
@ -267,7 +277,8 @@ public final class YoutubeDashManifestCreatorsUtils {
* </p> * </p>
* *
* <p> * <p>
* {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>} * {@code <Role schemeIdUri="urn:mpeg:DASH:role:2011" value="VALUE"/>}, where {@code VALUE} is
* {@code main} for videos and audios and {@code alternate} for descriptive audio
* </p> * </p>
* *
* <p> * <p>
@ -275,9 +286,11 @@ public final class YoutubeDashManifestCreatorsUtils {
* {@link #generateAdaptationSetElement(Document, ItagItem)}). * {@link #generateAdaptationSetElement(Document, ItagItem)}).
* </p> * </p>
* *
* @param doc the {@link Document} on which the the {@code <Role>} element will be appended * @param doc the {@link Document} on which the {@code <Role>} element will be appended
* @param itagItem the {@link ItagItem} corresponding to the stream, which must not be null
*/ */
public static void generateRoleElement(@Nonnull final Document doc) public static void generateRoleElement(@Nonnull final Document doc,
@Nonnull final ItagItem itagItem)
throws CreationException { throws CreationException {
try { try {
final Element adaptationSetElement = (Element) doc.getElementsByTagName( final Element adaptationSetElement = (Element) doc.getElementsByTagName(
@ -285,7 +298,8 @@ public final class YoutubeDashManifestCreatorsUtils {
final Element roleElement = doc.createElement(ROLE); final Element roleElement = doc.createElement(ROLE);
setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011"); setAttribute(roleElement, doc, "schemeIdUri", "urn:mpeg:DASH:role:2011");
setAttribute(roleElement, doc, "value", "main"); setAttribute(roleElement, doc, "value", itagItem.isDescriptiveAudio()
? "alternate" : "main");
adaptationSetElement.appendChild(roleElement); adaptationSetElement.appendChild(roleElement);
} catch (final DOMException e) { } catch (final DOMException e) {
@ -302,7 +316,7 @@ public final class YoutubeDashManifestCreatorsUtils {
* {@link #generateAdaptationSetElement(Document, ItagItem)}). * {@link #generateAdaptationSetElement(Document, ItagItem)}).
* </p> * </p>
* *
* @param doc the {@link Document} on which the the {@code <SegmentTimeline>} element will be * @param doc the {@link Document} on which the {@code <SegmentTimeline>} element will be
* appended * appended
* @param itagItem the {@link ItagItem} to use, which must not be null * @param itagItem the {@link ItagItem} to use, which must not be null
*/ */
@ -522,7 +536,7 @@ public final class YoutubeDashManifestCreatorsUtils {
* {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}. * {@link #generateSegmentTemplateElement(Document, String, DeliveryType)}.
* </p> * </p>
* *
* @param doc the {@link Document} on which the the {@code <SegmentTimeline>} element will be * @param doc the {@link Document} on which the {@code <SegmentTimeline>} element will be
* appended * appended
*/ */
public static void generateSegmentTimelineElement(@Nonnull final Document doc) public static void generateSegmentTimelineElement(@Nonnull final Document doc)

View File

@ -82,6 +82,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.JsonUtils; import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.LocaleCompat;
import org.schabi.newpipe.extractor.utils.Pair; import org.schabi.newpipe.extractor.utils.Pair;
import org.schabi.newpipe.extractor.utils.Parser; import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
@ -1309,6 +1310,8 @@ public class YoutubeStreamExtractor extends StreamExtractor {
.setAverageBitrate(itagItem.getAverageBitrate()) .setAverageBitrate(itagItem.getAverageBitrate())
.setAudioTrackId(itagItem.getAudioTrackId()) .setAudioTrackId(itagItem.getAudioTrackId())
.setAudioTrackName(itagItem.getAudioTrackName()) .setAudioTrackName(itagItem.getAudioTrackName())
.setAudioLocale(itagItem.getAudioLocale())
.setIsDescriptive(itagItem.isDescriptiveAudio())
.setItagItem(itagItem); .setItagItem(itagItem);
if (streamType == StreamType.LIVE_STREAM if (streamType == StreamType.LIVE_STREAM
@ -1454,9 +1457,6 @@ public class YoutubeStreamExtractor extends StreamExtractor {
itagItem.setQuality(formatData.getString("quality")); itagItem.setQuality(formatData.getString("quality"));
itagItem.setCodec(codec); itagItem.setCodec(codec);
itagItem.setAudioTrackId(formatData.getObject("audioTrack").getString("id"));
itagItem.setAudioTrackName(formatData.getObject("audioTrack").getString("displayName"));
if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) { if (streamType == StreamType.LIVE_STREAM || streamType == StreamType.POST_LIVE_STREAM) {
itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec")); itagItem.setTargetDurationSec(formatData.getInt("targetDurationSec"));
} }
@ -1473,6 +1473,27 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// AudioChannelConfiguration element of DASH manifests of audio streams in // AudioChannelConfiguration element of DASH manifests of audio streams in
// YoutubeDashManifestCreatorUtils // YoutubeDashManifestCreatorUtils
2)); 2));
final String audioTrackId = formatData.getObject("audioTrack")
.getString("id");
if (!isNullOrEmpty(audioTrackId)) {
itagItem.setAudioTrackId(audioTrackId);
final int audioTrackIdLastLocaleCharacter = audioTrackId.indexOf(".");
if (audioTrackIdLastLocaleCharacter != -1) {
// Audio tracks IDs are in the form LANGUAGE_CODE.TRACK_NUMBER
itagItem.setAudioLocale(LocaleCompat.forLanguageTag(
audioTrackId.substring(0, audioTrackIdLastLocaleCharacter)));
}
}
itagItem.setAudioTrackName(formatData.getObject("audioTrack")
.getString("displayName"));
// Descriptive audio tracks
// This information is also provided as a protobuf object in the formatData
itagItem.setIsDescriptiveAudio(streamUrl.contains("acont%3Ddescriptive")
// Support "decoded" URLs
|| streamUrl.contains("acont=descriptive"));
} }
// YouTube return the content length and the approximate duration as strings // YouTube return the content length and the approximate duration as strings

View File

@ -25,6 +25,7 @@ import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
public final class AudioStream extends Stream { public final class AudioStream extends Stream {
@ -43,8 +44,14 @@ public final class AudioStream extends Stream {
private String codec; private String codec;
// Fields about the audio track id/name // Fields about the audio track id/name
private String audioTrackId; @Nullable
private String audioTrackName; private final String audioTrackId;
@Nullable
private final String audioTrackName;
@Nullable
private final Locale audioLocale;
private final boolean isDescriptive;
@Nullable @Nullable
private ItagItem itagItem; private ItagItem itagItem;
@ -67,6 +74,9 @@ public final class AudioStream extends Stream {
@Nullable @Nullable
private String audioTrackName; private String audioTrackName;
@Nullable @Nullable
private Locale audioLocale;
private boolean isDescriptive;
@Nullable
private ItagItem itagItem; private ItagItem itagItem;
/** /**
@ -185,7 +195,11 @@ public final class AudioStream extends Stream {
/** /**
* Set the audio track id of the {@link AudioStream}. * Set the audio track id of the {@link AudioStream}.
* *
* @param audioTrackId the audio track id of the {@link AudioStream} * <p>
* The default value is {@code null}.
* </p>
*
* @param audioTrackId the audio track id of the {@link AudioStream}, which can be null
* @return this {@link Builder} instance * @return this {@link Builder} instance
*/ */
public Builder setAudioTrackId(@Nullable final String audioTrackId) { public Builder setAudioTrackId(@Nullable final String audioTrackId) {
@ -196,7 +210,11 @@ public final class AudioStream extends Stream {
/** /**
* Set the audio track name of the {@link AudioStream}. * Set the audio track name of the {@link AudioStream}.
* *
* @param audioTrackName the audio track name of the {@link AudioStream} * <p>
* The default value is {@code null}.
* </p>
*
* @param audioTrackName the audio track name of the {@link AudioStream}, which can be null
* @return this {@link Builder} instance * @return this {@link Builder} instance
*/ */
public Builder setAudioTrackName(@Nullable final String audioTrackName) { public Builder setAudioTrackName(@Nullable final String audioTrackName) {
@ -204,6 +222,44 @@ public final class AudioStream extends Stream {
return this; return this;
} }
/**
* Set whether this {@link AudioStream} is a descriptive audio.
*
* <p>
* A descriptive audio is an audio in which descriptions of visual elements of a video are
* added in the original audio, with the goal to make a video more accessible to blind and
* visually impaired people.
* </p>
*
* <p>
* The default value is {@code false}.
* </p>
*
* @param isDescriptive whether this {@link AudioStream} is a descriptive audio
* @return this {@link Builder} instance
* @see <a href="https://en.wikipedia.org/wiki/Audio_description">
* https://en.wikipedia.org/wiki/Audio_description</a>
*/
public Builder setIsDescriptive(final boolean isDescriptive) {
this.isDescriptive = isDescriptive;
return this;
}
/**
* Set the {@link Locale} of the audio which represents its language.
*
* <p>
* The default value is {@code null}, which means that the {@link Locale} is unknown.
* </p>
*
* @param audioLocale the {@link Locale} of the audio, which could be {@code null}
* @return this {@link Builder} instance
*/
public Builder setAudioLocale(@Nullable final Locale audioLocale) {
this.audioLocale = audioLocale;
return this;
}
/** /**
* Set the {@link ItagItem} corresponding to the {@link AudioStream}. * Set the {@link ItagItem} corresponding to the {@link AudioStream}.
* *
@ -257,7 +313,8 @@ public final class AudioStream extends Stream {
} }
return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate, return new AudioStream(id, content, isUrl, mediaFormat, deliveryMethod, averageBitrate,
manifestUrl, audioTrackId, audioTrackName, itagItem); manifestUrl, audioTrackId, audioTrackName, audioLocale, isDescriptive,
itagItem);
} }
} }
@ -277,6 +334,7 @@ public final class AudioStream extends Stream {
* {@link #UNKNOWN_BITRATE}) * {@link #UNKNOWN_BITRATE})
* @param audioTrackId the id of the audio track * @param audioTrackId the id of the audio track
* @param audioTrackName the name of the audio track * @param audioTrackName the name of the audio track
* @param audioLocale the {@link Locale} of the audio stream, representing its language
* @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null * @param itagItem the {@link ItagItem} corresponding to the stream, which cannot be null
* @param manifestUrl the URL of the manifest this stream comes from (if applicable, * @param manifestUrl the URL of the manifest this stream comes from (if applicable,
* otherwise null) * otherwise null)
@ -291,6 +349,8 @@ public final class AudioStream extends Stream {
@Nullable final String manifestUrl, @Nullable final String manifestUrl,
@Nullable final String audioTrackId, @Nullable final String audioTrackId,
@Nullable final String audioTrackName, @Nullable final String audioTrackName,
@Nullable final Locale audioLocale,
final boolean isDescriptive,
@Nullable final ItagItem itagItem) { @Nullable final ItagItem itagItem) {
super(id, content, isUrl, format, deliveryMethod, manifestUrl); super(id, content, isUrl, format, deliveryMethod, manifestUrl);
if (itagItem != null) { if (itagItem != null) {
@ -307,6 +367,8 @@ public final class AudioStream extends Stream {
this.averageBitrate = averageBitrate; this.averageBitrate = averageBitrate;
this.audioTrackId = audioTrackId; this.audioTrackId = audioTrackId;
this.audioTrackName = audioTrackName; this.audioTrackName = audioTrackName;
this.audioLocale = audioLocale;
this.isDescriptive = isDescriptive;
} }
/** /**
@ -316,7 +378,9 @@ public final class AudioStream extends Stream {
public boolean equalStats(final Stream cmp) { public boolean equalStats(final Stream cmp) {
return super.equalStats(cmp) && cmp instanceof AudioStream return super.equalStats(cmp) && cmp instanceof AudioStream
&& averageBitrate == ((AudioStream) cmp).averageBitrate && averageBitrate == ((AudioStream) cmp).averageBitrate
&& Objects.equals(audioTrackId, ((AudioStream) cmp).audioTrackId); && Objects.equals(audioTrackId, ((AudioStream) cmp).audioTrackId)
&& isDescriptive == ((AudioStream) cmp).isDescriptive
&& Objects.equals(audioLocale, ((AudioStream) cmp).audioLocale);
} }
/** /**
@ -421,15 +485,44 @@ public final class AudioStream extends Stream {
} }
/** /**
* Get the name of the audio track. * Get the name of the audio track, which may be {@code null} if this information is not
* provided by the service.
* *
* @return the name of the audio track * @return the name of the audio track or {@code null}
*/ */
@Nullable @Nullable
public String getAudioTrackName() { public String getAudioTrackName() {
return audioTrackName; return audioTrackName;
} }
/**
* Get the {@link Locale} of the audio representing the language of the stream, which is
* {@code null} if the audio language of this stream is not known.
*
* @return the {@link Locale} of the audio or {@code null}
*/
@Nullable
public Locale getAudioLocale() {
return audioLocale;
}
/**
* Returns whether this stream is a descriptive audio.
*
* <p>
* A descriptive audio is an audio in which descriptions of visual elements of a video are
* added in the original audio, with the goal to make a video more accessible to blind and
* visually impaired people.
* </p>
*
* @return {@code true} this audio stream is a descriptive audio, {@code false} otherwise
* @see <a href="https://en.wikipedia.org/wiki/Audio_description">
* https://en.wikipedia.org/wiki/Audio_description</a>
*/
public boolean isDescriptive() {
return isDescriptive;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View File

@ -7,16 +7,20 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest; import org.schabi.newpipe.extractor.services.DefaultStreamExtractorTest;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor; import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.LocaleCompat;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.schabi.newpipe.extractor.ServiceList.MediaCCC; import static org.schabi.newpipe.extractor.ServiceList.MediaCCC;
/** /**
@ -85,7 +89,11 @@ public class MediaCCCStreamExtractorTest {
@Test @Test
public void testAudioStreams() throws Exception { public void testAudioStreams() throws Exception {
super.testAudioStreams(); super.testAudioStreams();
assertEquals(2, extractor.getAudioStreams().size()); final List<AudioStream> audioStreams = extractor.getAudioStreams();
assertEquals(2, audioStreams.size());
final Locale expectedLocale = LocaleCompat.forLanguageTag("deu");
assertTrue(audioStreams.stream().allMatch(audioStream ->
Objects.equals(audioStream.getAudioLocale(), expectedLocale)));
} }
} }
@ -155,7 +163,11 @@ public class MediaCCCStreamExtractorTest {
@Test @Test
public void testAudioStreams() throws Exception { public void testAudioStreams() throws Exception {
super.testAudioStreams(); super.testAudioStreams();
assertEquals(2, extractor.getAudioStreams().size()); final List<AudioStream> audioStreams = extractor.getAudioStreams();
assertEquals(2, audioStreams.size());
final Locale expectedLocale = LocaleCompat.forLanguageTag("eng");
assertTrue(audioStreams.stream().allMatch(audioStream ->
Objects.equals(audioStream.getAudioLocale(), expectedLocale)));
} }
@Override @Override

View File

@ -20,6 +20,7 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader; import java.io.StringReader;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Random; import java.util.Random;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -191,7 +192,7 @@ class YoutubeDashManifestCreatorsTest {
() -> assertMpdElement(document), () -> assertMpdElement(document),
() -> assertPeriodElement(document), () -> assertPeriodElement(document),
() -> assertAdaptationSetElement(document, itagItem), () -> assertAdaptationSetElement(document, itagItem),
() -> assertRoleElement(document), () -> assertRoleElement(document, itagItem),
() -> assertRepresentationElement(document, itagItem), () -> assertRepresentationElement(document, itagItem),
() -> { () -> {
if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) {
@ -220,10 +221,19 @@ class YoutubeDashManifestCreatorsTest {
@Nonnull final ItagItem itagItem) { @Nonnull final ItagItem itagItem) {
final Element element = assertGetElement(document, ADAPTATION_SET, PERIOD); final Element element = assertGetElement(document, ADAPTATION_SET, PERIOD);
assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType"); assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType");
if (itagItem.itagType == ItagItem.ItagType.AUDIO) {
final Locale itagAudioLocale = itagItem.getAudioLocale();
if (itagAudioLocale != null) {
assertAttrEquals(itagAudioLocale.getLanguage(), element, "lang");
}
}
} }
private void assertRoleElement(@Nonnull final Document document) { private void assertRoleElement(@Nonnull final Document document,
assertGetElement(document, ROLE, ADAPTATION_SET); @Nonnull final ItagItem itagItem) {
final Element element = assertGetElement(document, ROLE, ADAPTATION_SET);
assertAttrEquals(itagItem.isDescriptiveAudio() ? "alternate" : "main", element, "value");
} }
private void assertRepresentationElement(@Nonnull final Document document, private void assertRepresentationElement(@Nonnull final Document document,

View File

@ -21,6 +21,7 @@
package org.schabi.newpipe.extractor.services.youtube.stream; package org.schabi.newpipe.extractor.services.youtube.stream;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -48,6 +49,7 @@ import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamSegment; import org.schabi.newpipe.extractor.stream.StreamSegment;
import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.utils.LocaleCompat;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@ -55,6 +57,8 @@ import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -546,24 +550,43 @@ public class YoutubeStreamExtractorDefaultTest {
@Test @Test
void testCheckAudioStreams() throws Exception { void testCheckAudioStreams() throws Exception {
assertTrue(extractor.getAudioStreams().size() > 0); final List<AudioStream> audioStreams = extractor.getAudioStreams();
assertFalse(audioStreams.isEmpty());
for (final AudioStream audioStream : extractor.getAudioStreams()) { for (final AudioStream stream : audioStreams) {
assertNotNull(audioStream.getAudioTrackName()); assertNotNull(stream.getAudioTrackName());
} }
assertTrue( assertTrue(audioStreams.stream()
extractor.getAudioStreams() .anyMatch(audioStream -> "English".equals(audioStream.getAudioTrackName())));
.stream()
.anyMatch(audioStream -> audioStream.getAudioTrackName().equals("English"))
);
assertTrue( final Locale hindiLocale = LocaleCompat.forLanguageTag("hi");
extractor.getAudioStreams() assertTrue(audioStreams.stream()
.stream() .anyMatch(audioStream ->
.anyMatch(audioStream -> audioStream.getAudioTrackName().equals("Hindi")) Objects.equals(audioStream.getAudioLocale(), hindiLocale)));
); }
}
public static class DescriptiveAudio {
private static final String ID = "TjxC-evzxdk";
private static final String URL = BASE_URL + ID;
private static StreamExtractor extractor;
@BeforeAll
public static void setUp() throws Exception {
YoutubeTestsUtils.ensureStateless();
NewPipe.init(DownloaderFactory.getDownloader(RESOURCE_PATH + "descriptiveAudio"));
extractor = YouTube.getStreamExtractor(URL);
extractor.fetchPage();
} }
@Test
void testCheckDescriptiveAudio() throws Exception {
assertFalse(extractor.getAudioStreams().isEmpty());
assertTrue(extractor.getAudioStreams()
.stream()
.anyMatch(AudioStream::isDescriptive));
}
} }
} }

View File

@ -0,0 +1,77 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/iframe_api",
"headers": {
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-security-policy-report-only": [
"base-uri \u0027self\u0027;default-src \u0027self\u0027 https: blob:;font-src https: data:;img-src https: data: android-webview-video-poster:;media-src blob: https:;object-src \u0027none\u0027;script-src \u0027nonce-n5UoXCJvOauVo_kO_mAFJg\u0027 \u0027unsafe-inline\u0027 \u0027strict-dynamic\u0027 https: http: \u0027unsafe-eval\u0027;style-src https: \u0027unsafe-inline\u0027;report-uri /cspreport"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"cross-origin-resource-policy": [
"cross-origin"
],
"date": [
"Mon, 30 Jan 2023 18:31:11 GMT"
],
"expires": [
"Mon, 30 Jan 2023 18:31:11 GMT"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dDcMnDslDdHw; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"DEVICE_INFO\u003dChxOekU1TkRVeE5EWXlOVFkwTkRBNE16QXlNZz09EO+Z4J4GGO+Z4J4G; Domain\u003d.youtube.com; Expires\u003dSat, 29-Jul-2023 18:31:11 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003dx01Dzzf4XZk; Domain\u003d.youtube.com; Expires\u003dSat, 29-Jul-2023 18:31:11 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"CONSENT\u003dPENDING+527; expires\u003dWed, 29-Jan-2025 18:31:11 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "var scriptUrl \u003d \u0027https:\\/\\/www.youtube.com\\/s\\/player\\/4248d311\\/www-widgetapi.vflset\\/www-widgetapi.js\u0027;try{var ttPolicy\u003dwindow.trustedTypes.createPolicy(\"youtube-widget-api\",{createScriptURL:function(x){return x}});scriptUrl\u003dttPolicy.createScriptURL(scriptUrl)}catch(e){}var YT;if(!window[\"YT\"])YT\u003d{loading:0,loaded:0};var YTConfig;if(!window[\"YTConfig\"])YTConfig\u003d{\"host\":\"https://www.youtube.com\"};\nif(!YT.loading){YT.loading\u003d1;(function(){var l\u003d[];YT.ready\u003dfunction(f){if(YT.loaded)f();else l.push(f)};window.onYTReady\u003dfunction(){YT.loaded\u003d1;for(var i\u003d0;i\u003cl.length;i++)try{l[i]()}catch(e$0){}};YT.setConfig\u003dfunction(c){for(var k in c)if(c.hasOwnProperty(k))YTConfig[k]\u003dc[k]};var a\u003ddocument.createElement(\"script\");a.type\u003d\"text/javascript\";a.id\u003d\"www-widgetapi-script\";a.src\u003dscriptUrl;a.async\u003dtrue;var c\u003ddocument.currentScript;if(c){var n\u003dc.nonce||c.getAttribute(\"nonce\");if(n)a.setAttribute(\"nonce\",n)}var b\u003d\ndocument.getElementsByTagName(\"script\")[0];b.parentNode.insertBefore(a,b)})()};\n",
"latestUrl": "https://www.youtube.com/iframe_api"
}
}

View File

@ -0,0 +1,82 @@
{
"request": {
"httpMethod": "GET",
"url": "https://www.youtube.com/sw.js",
"headers": {
"Referer": [
"https://www.youtube.com"
],
"Origin": [
"https://www.youtube.com"
],
"Accept-Language": [
"en-GB, en;q\u003d0.9"
]
},
"localization": {
"languageCode": "en",
"countryCode": "GB"
}
},
"response": {
"responseCode": 200,
"responseMessage": "",
"responseHeaders": {
"access-control-allow-credentials": [
"true"
],
"access-control-allow-origin": [
"https://www.youtube.com"
],
"alt-svc": [
"h3\u003d\":443\"; ma\u003d2592000,h3-29\u003d\":443\"; ma\u003d2592000"
],
"cache-control": [
"private, max-age\u003d0"
],
"content-type": [
"text/javascript; charset\u003dutf-8"
],
"cross-origin-opener-policy-report-only": [
"same-origin; report-to\u003d\"youtube_main\""
],
"date": [
"Mon, 30 Jan 2023 18:31:14 GMT"
],
"expires": [
"Mon, 30 Jan 2023 18:31:14 GMT"
],
"p3p": [
"CP\u003d\"This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl\u003den-GB for more info.\""
],
"permissions-policy": [
"ch-ua-arch\u003d*, ch-ua-bitness\u003d*, ch-ua-full-version\u003d*, ch-ua-full-version-list\u003d*, ch-ua-model\u003d*, ch-ua-wow64\u003d*, ch-ua-platform\u003d*, ch-ua-platform-version\u003d*"
],
"report-to": [
"{\"group\":\"youtube_main\",\"max_age\":2592000,\"endpoints\":[{\"url\":\"https://csp.withgoogle.com/csp/report-to/youtube_main\"}]}"
],
"server": [
"ESF"
],
"set-cookie": [
"YSC\u003dlneu_eWU8iY; Domain\u003d.youtube.com; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"VISITOR_INFO1_LIVE\u003d; Domain\u003d.youtube.com; Expires\u003dTue, 05-May-2020 18:31:14 GMT; Path\u003d/; Secure; HttpOnly; SameSite\u003dnone",
"CONSENT\u003dPENDING+588; expires\u003dWed, 29-Jan-2025 18:31:14 GMT; path\u003d/; domain\u003d.youtube.com; Secure"
],
"strict-transport-security": [
"max-age\u003d31536000"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"0"
]
},
"responseBody": "\n self.addEventListener(\u0027install\u0027, event \u003d\u003e {\n event.waitUntil(self.skipWaiting());\n });\n self.addEventListener(\u0027activate\u0027, event \u003d\u003e {\n event.waitUntil(\n self.clients.claim().then(() \u003d\u003e self.registration.unregister()));\n });\n ",
"latestUrl": "https://www.youtube.com/sw.js"
}
}