diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java index 8383d0e2b..8929185c1 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreator.java @@ -79,7 +79,7 @@ public final class YoutubeDashManifestCreator { * *

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

*/ @@ -90,7 +90,7 @@ public final class YoutubeDashManifestCreator { * *

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

*/ @@ -242,7 +242,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromOtfStreamingUrl( + public static String fromOtfStreamingUrl( @Nonnull final String otfBaseStreamingUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { @@ -376,7 +376,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromPostLiveStreamDvrStreamingUrl( + public static String fromPostLiveStreamDvrStreamingUrl( @Nonnull final String postLiveStreamDvrStreamingUrl, @Nonnull final ItagItem itagItem, final int targetDurationSec, @@ -505,7 +505,7 @@ public final class YoutubeDashManifestCreator { * the DASH manifest */ @Nonnull - public static String createDashManifestFromProgressiveStreamingUrl( + public static String fromProgressiveStreamingUrl( @Nonnull final String progressiveStreamingBaseUrl, @Nonnull final ItagItem itagItem, final long durationSecondsFallback) throws YoutubeDashManifestCreationException { diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java b/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java index bdbd59530..124d998d0 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/ExtractorAsserts.java @@ -15,6 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.schabi.newpipe.extractor.utils.Utils; + public class ExtractorAsserts { public static void assertEmptyErrors(String message, List errors) { if (!errors.isEmpty()) { @@ -64,6 +66,14 @@ public class ExtractorAsserts { } } + public static void assertNotBlank(String stringToCheck) { + assertNotBlank(stringToCheck, null); + } + + public static void assertNotBlank(String stringToCheck, @Nullable String message) { + assertFalse(Utils.isBlank(stringToCheck), message); + } + public static void assertGreater(final long expected, final long actual) { assertGreater(expected, actual, actual + " is not > " + expected); } diff --git a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java index db06307d1..c5fdf71af 100644 --- a/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java +++ b/extractor/src/test/java/org/schabi/newpipe/extractor/services/youtube/YoutubeDashManifestCreatorTest.java @@ -1,5 +1,18 @@ package org.schabi.newpipe.extractor.services.youtube; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreater; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertGreaterOrEqual; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertIsValidUrl; +import static org.schabi.newpipe.extractor.ExtractorAsserts.assertNotBlank; +import static org.schabi.newpipe.extractor.ServiceList.YouTube; +import static org.schabi.newpipe.extractor.utils.Utils.isBlank; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.schabi.newpipe.downloader.DownloaderTestImpl; @@ -7,33 +20,25 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Stream; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; +import java.io.StringReader; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import javax.annotation.Nonnull; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import java.io.StringReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; -import java.util.Random; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.schabi.newpipe.extractor.ServiceList.YouTube; -import static org.schabi.newpipe.extractor.utils.Utils.isBlank; /** - * Test for {@link YoutubeDashManifestCreator}. + * Test for {@link YoutubeDashManifestCreator}. Tests the generation of OTF and Progressive + * manifests. * *

* We cannot test the generation of DASH manifests for ended livestreams because these videos will @@ -58,570 +63,280 @@ import static org.schabi.newpipe.extractor.utils.Utils.isBlank; */ class YoutubeDashManifestCreatorTest { // Setting a higher number may let Google video servers return a lot of 403s - private static final int MAXIMUM_NUMBER_OF_STREAMS_TO_TEST = 3; - - public static class TestGenerationOfOtfAndProgressiveManifests { - private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; - private static YoutubeStreamExtractor extractor; - - @BeforeAll - public static void setUp() throws Exception { - YoutubeParsingHelper.resetClientVersionAndKey(); - YoutubeParsingHelper.setNumberGenerator(new Random(1)); - NewPipe.init(DownloaderTestImpl.getInstance()); - extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url); - extractor.fetchPage(); - } - - @Test - void testOtfStreamsANewEraOfOpen() throws Exception { - testStreams(DeliveryMethod.DASH, - extractor.getVideoOnlyStreams()); - testStreams(DeliveryMethod.DASH, - extractor.getAudioStreams()); - // This should not happen because there are no video stream with audio which use the - // DASH delivery method (YouTube OTF stream type) - try { - testStreams(DeliveryMethod.DASH, - extractor.getVideoStreams()); - } catch (final Exception e) { - assertEquals(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, - e.getClass(), "The exception thrown was not the one excepted: " - + e.getClass().getName() - + "was thrown instead of YoutubeDashManifestCreationException"); - } - } - - @Test - void testProgressiveStreamsANewEraOfOpen() throws Exception { - testStreams(DeliveryMethod.PROGRESSIVE_HTTP, - extractor.getVideoOnlyStreams()); - testStreams(DeliveryMethod.PROGRESSIVE_HTTP, - extractor.getAudioStreams()); - // This exception should be always thrown, as we are not able to generate DASH - // manifests of video formats with audio - final List videoStreams = extractor.getVideoStreams(); - if (!videoStreams.isEmpty()) { - assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, - () -> testStreams(DeliveryMethod.PROGRESSIVE_HTTP, videoStreams), - "The exception thrown for the generation of DASH manifests for YouTube " - + "progressive video streams with audio was not the one excepted"); - } - } - - private void testStreams(@Nonnull final DeliveryMethod deliveryMethodToTest, - @Nonnull final List streamList) - throws Exception { - int i = 0; - final int streamListSize = streamList.size(); - final boolean isDeliveryMethodToTestProgressiveHttpDeliveryMethod = - deliveryMethodToTest == DeliveryMethod.PROGRESSIVE_HTTP; - final long videoLength = extractor.getLength(); - - // Test at most the first five streams we found - while (i <= YoutubeDashManifestCreatorTest.MAXIMUM_NUMBER_OF_STREAMS_TO_TEST - && i < streamListSize) { - final Stream stream = streamList.get(i); - if (stream.getDeliveryMethod() == deliveryMethodToTest) { - final String baseUrl = stream.getContent(); - assertFalse(isBlank(baseUrl), "The base URL of the stream is empty"); - - final ItagItem itagItem = stream.getItagItem(); - assertNotNull(itagItem, "The itagItem is null"); - - final String dashManifest; - if (isDeliveryMethodToTestProgressiveHttpDeliveryMethod) { - dashManifest = YoutubeDashManifestCreator - .createDashManifestFromProgressiveStreamingUrl(baseUrl, itagItem, - videoLength); - } else if (deliveryMethodToTest == DeliveryMethod.DASH) { - dashManifest = YoutubeDashManifestCreator - .createDashManifestFromOtfStreamingUrl(baseUrl, itagItem, - videoLength); - } else { - throw new IllegalArgumentException( - "The delivery method provided is not the progressive HTTP or the DASH delivery method"); - } - testManifestGenerated(dashManifest, itagItem, - isDeliveryMethodToTestProgressiveHttpDeliveryMethod); - assertFalse(isBlank(dashManifest), "The DASH manifest is null or empty: " - + dashManifest); - } - ++i; - } - } - - private void testManifestGenerated(final String dashManifest, - @Nonnull final ItagItem itagItem, - final boolean isAProgressiveStreamingUrl) - throws Exception { - final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory - .newInstance(); - final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - final Document document = documentBuilder.parse(new InputSource( - new StringReader(dashManifest))); - - testMpdElement(document); - testPeriodElement(document); - testAdaptationSetElement(document, itagItem); - testRoleElement(document); - testRepresentationElement(document, itagItem); - if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { - testAudioChannelConfigurationElement(document, itagItem); - } - if (isAProgressiveStreamingUrl) { - testBaseUrlElement(document); - testSegmentBaseElement(document, itagItem); - testInitializationElement(document, itagItem); - } else { - testSegmentTemplateElement(document); - testSegmentTimelineAndSElements(document); - } - } - - private void testMpdElement(@Nonnull final Document document) { - final Element mpdElement = (Element) document.getElementsByTagName("MPD") - .item(0); - assertNotNull(mpdElement, "The MPD element doesn't exist"); - assertNull(mpdElement.getParentNode().getNodeValue(), "The MPD element has a parent element"); - - final String mediaPresentationDurationValue = mpdElement - .getAttribute("mediaPresentationDuration"); - assertNotNull(mediaPresentationDurationValue, - "The value of the mediaPresentationDuration attribute is empty or the corresponding attribute doesn't exist"); - assertTrue(mediaPresentationDurationValue.startsWith("PT"), - "The mediaPresentationDuration attribute of the DASH manifest is not valid"); - } - - private void testPeriodElement(@Nonnull final Document document) { - final Element periodElement = (Element) document.getElementsByTagName("Period") - .item(0); - assertNotNull(periodElement, "The Period element doesn't exist"); - assertTrue(periodElement.getParentNode().isEqualNode( - document.getElementsByTagName("MPD").item(0)), - "The MPD element doesn't contain a Period element"); - } - - private void testAdaptationSetElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { - final Element adaptationSetElement = (Element) document - .getElementsByTagName("AdaptationSet").item(0); - assertNotNull(adaptationSetElement, "The AdaptationSet element doesn't exist"); - assertTrue(adaptationSetElement.getParentNode().isEqualNode( - document.getElementsByTagName("Period").item(0)), - "The Period element doesn't contain an AdaptationSet element"); - - final String mimeTypeDashManifestValue = adaptationSetElement - .getAttribute("mimeType"); - assertFalse(isBlank(mimeTypeDashManifestValue), - "The value of the mimeType attribute is empty or the corresponding attribute doesn't exist"); - - final String mimeTypeItagItemValue = itagItem.getMediaFormat().getMimeType(); - assertFalse(isBlank(mimeTypeItagItemValue), "The mimeType of the ItagItem is empty"); - - assertEquals(mimeTypeDashManifestValue, mimeTypeItagItemValue, - "The mimeType attribute of the DASH manifest (" + mimeTypeItagItemValue - + ") is not equal to the mimeType set in the ItagItem object (" - + mimeTypeItagItemValue + ")"); - } - - private void testRoleElement(@Nonnull final Document document) { - final Element roleElement = (Element) document.getElementsByTagName("Role") - .item(0); - assertNotNull(roleElement, "The Role element doesn't exist"); - assertTrue(roleElement.getParentNode().isEqualNode( - document.getElementsByTagName("AdaptationSet").item(0)), - "The AdaptationSet element doesn't contain a Role element"); - } - - private void testRepresentationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { - final Element representationElement = (Element) document - .getElementsByTagName("Representation").item(0); - assertNotNull(representationElement, "The Representation element doesn't exist"); - assertTrue(representationElement.getParentNode().isEqualNode( - document.getElementsByTagName("AdaptationSet").item(0)), - "The AdaptationSet element doesn't contain a Representation element"); - - final String bandwidthDashManifestValue = representationElement - .getAttribute("bandwidth"); - assertFalse(isBlank(bandwidthDashManifestValue), - "The value of the bandwidth attribute is empty or the corresponding attribute doesn't exist"); - - final int bandwidthDashManifest; - try { - bandwidthDashManifest = Integer.parseInt(bandwidthDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the bandwidth attribute is not an integer", - e); - } - assertTrue(bandwidthDashManifest > 0, - "The value of the bandwidth attribute is less than or equal to 0"); - - final int bitrateItagItem = itagItem.getBitrate(); - assertTrue(bitrateItagItem > 0, - "The bitrate of the ItagItem is less than or equal to 0"); - - assertEquals(bandwidthDashManifest, bitrateItagItem, - "The value of the bandwidth attribute of the DASH manifest (" - + bandwidthDashManifest - + ") is not equal to the bitrate value set in the ItagItem object (" - + bitrateItagItem + ")"); - - final String codecsDashManifestValue = representationElement.getAttribute("codecs"); - assertFalse(isBlank(codecsDashManifestValue), - "The value of the codecs attribute is empty or the corresponding attribute doesn't exist"); - - final String codecsItagItemValue = itagItem.getCodec(); - assertFalse(isBlank(codecsItagItemValue), "The codec of the ItagItem is empty"); - - assertEquals(codecsDashManifestValue, codecsItagItemValue, - "The value of the codecs attribute of the DASH manifest (" - + codecsDashManifestValue - + ") is not equal to the codecs value set in the ItagItem object (" - + codecsItagItemValue + ")"); - - if (itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY - || itagItem.itagType == ItagItem.ItagType.VIDEO) { - testVideoItagItemAttributes(representationElement, itagItem); - } - - final String idDashManifestValue = representationElement.getAttribute("id"); - assertFalse(isBlank(idDashManifestValue), - "The value of the id attribute is empty or the corresponding attribute doesn't exist"); - - final int idDashManifest; - try { - idDashManifest = Integer.parseInt(idDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the id attribute is not an integer", - e); - } - assertTrue(idDashManifest > 0, "The value of the id attribute is less than or equal to 0"); - - final int idItagItem = itagItem.id; - assertTrue(idItagItem > 0, "The id of the ItagItem is less than or equal to 0"); - assertEquals(idDashManifest, idItagItem, - "The value of the id attribute of the DASH manifest (" + idDashManifestValue - + ") is not equal to the id of the ItagItem object (" + idItagItem - + ")"); - } - - private void testVideoItagItemAttributes(@Nonnull final Element representationElement, - @Nonnull final ItagItem itagItem) { - final String frameRateDashManifestValue = representationElement - .getAttribute("frameRate"); - assertFalse(isBlank(frameRateDashManifestValue), - "The value of the frameRate attribute is empty or the corresponding attribute doesn't exist"); - - final int frameRateDashManifest; - try { - frameRateDashManifest = Integer.parseInt(frameRateDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the frameRate attribute is not an integer", - e); - } - assertTrue(frameRateDashManifest > 0, - "The value of the frameRate attribute is less than or equal to 0"); - - final int fpsItagItem = itagItem.getFps(); - assertTrue(fpsItagItem > 0, "The fps of the ItagItem is unknown"); - - assertEquals(frameRateDashManifest, fpsItagItem, - "The value of the frameRate attribute of the DASH manifest (" - + frameRateDashManifest - + ") is not equal to the frame rate value set in the ItagItem object (" - + fpsItagItem + ")"); - - final String heightDashManifestValue = representationElement.getAttribute("height"); - assertFalse(isBlank(heightDashManifestValue), - "The value of the height attribute is empty or the corresponding attribute doesn't exist"); - - final int heightDashManifest; - try { - heightDashManifest = Integer.parseInt(heightDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the height attribute is not an integer", - e); - } - assertTrue(heightDashManifest > 0, - "The value of the height attribute is less than or equal to 0"); - - final int heightItagItem = itagItem.getHeight(); - assertTrue(heightItagItem > 0, - "The height of the ItagItem is less than or equal to 0"); - - assertEquals(heightDashManifest, heightItagItem, - "The value of the height attribute of the DASH manifest (" - + heightDashManifest - + ") is not equal to the height value set in the ItagItem object (" - + heightItagItem + ")"); - - final String widthDashManifestValue = representationElement.getAttribute("width"); - assertFalse(isBlank(widthDashManifestValue), - "The value of the width attribute is empty or the corresponding attribute doesn't exist"); - - final int widthDashManifest; - try { - widthDashManifest = Integer.parseInt(widthDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the width attribute is not an integer", - e); - } - assertTrue(widthDashManifest > 0, - "The value of the width attribute is less than or equal to 0"); - - final int widthItagItem = itagItem.getWidth(); - assertTrue(widthItagItem > 0, "The width of the ItagItem is less than or equal to 0"); - - assertEquals(widthDashManifest, widthItagItem, - "The value of the width attribute of the DASH manifest (" + widthDashManifest - + ") is not equal to the width value set in the ItagItem object (" - + widthItagItem + ")"); - } - - private void testAudioChannelConfigurationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { - final Element audioChannelConfigurationElement = (Element) document - .getElementsByTagName("AudioChannelConfiguration").item(0); - assertNotNull(audioChannelConfigurationElement, - "The AudioChannelConfiguration element doesn't exist"); - assertTrue(audioChannelConfigurationElement.getParentNode().isEqualNode( - document.getElementsByTagName("Representation").item(0)), - "The Representation element doesn't contain an AudioChannelConfiguration element"); - - final String audioChannelsDashManifestValue = audioChannelConfigurationElement - .getAttribute("value"); - assertFalse(isBlank(audioChannelsDashManifestValue), - "The value of the value attribute is empty or the corresponding attribute doesn't exist"); - - final int audioChannelsDashManifest; - try { - audioChannelsDashManifest = Integer.parseInt(audioChannelsDashManifestValue); - } catch (final NumberFormatException e) { - throw new AssertionError( - "The number of audio channels (the value attribute) is not an integer", - e); - } - assertTrue(audioChannelsDashManifest > 0, - "The number of audio channels (the value attribute) is less than or equal to 0"); - - final int audioChannelsItagItem = itagItem.getAudioChannels(); - assertTrue(audioChannelsItagItem > 0, - "The number of audio channels of the ItagItem is less than or equal to 0"); - - assertEquals(audioChannelsDashManifest, audioChannelsItagItem, - "The value of the value attribute of the DASH manifest (" - + audioChannelsDashManifest - + ") is not equal to the number of audio channels set in the ItagItem object (" - + audioChannelsItagItem + ")"); - } - - private void testSegmentTemplateElement(@Nonnull final Document document) { - final Element segmentTemplateElement = (Element) document - .getElementsByTagName("SegmentTemplate").item(0); - assertNotNull(segmentTemplateElement, "The SegmentTemplate element doesn't exist"); - assertTrue(segmentTemplateElement.getParentNode().isEqualNode( - document.getElementsByTagName("Representation").item(0)), - "The Representation element doesn't contain a SegmentTemplate element"); - - final String initializationValue = segmentTemplateElement - .getAttribute("initialization"); - assertFalse(isBlank(initializationValue), - "The value of the initialization attribute is empty or the corresponding attribute doesn't exist"); - try { - new URL(initializationValue); - } catch (final MalformedURLException e) { - throw new AssertionError("The value of the initialization attribute is not an URL", - e); - } - assertTrue(initializationValue.endsWith("&sq=0"), - "The value of the initialization attribute doesn't end with &sq=0"); - - final String mediaValue = segmentTemplateElement.getAttribute("media"); - assertFalse(isBlank(mediaValue), - "The value of the media attribute is empty or the corresponding attribute doesn't exist"); - try { - new URL(mediaValue); - } catch (final MalformedURLException e) { - throw new AssertionError("The value of the media attribute is not an URL", - e); - } - assertTrue(mediaValue.endsWith("&sq=$Number$"), - "The value of the media attribute doesn't end with &sq=$Number$"); - - final String startNumberValue = segmentTemplateElement.getAttribute("startNumber"); - assertFalse(isBlank(startNumberValue), - "The value of the startNumber attribute is empty or the corresponding attribute doesn't exist"); - assertEquals("1", startNumberValue, - "The value of the startNumber attribute is not equal to 1"); - } - - private void testSegmentTimelineAndSElements(@Nonnull final Document document) { - final Element segmentTimelineElement = (Element) document - .getElementsByTagName("SegmentTimeline").item(0); - assertNotNull(segmentTimelineElement, "The SegmentTimeline element doesn't exist"); - assertTrue(segmentTimelineElement.getParentNode().isEqualNode( - document.getElementsByTagName("SegmentTemplate").item(0)), - "The SegmentTemplate element doesn't contain a SegmentTimeline element"); - testSElements(segmentTimelineElement); - } - - private void testSElements(@Nonnull final Element segmentTimelineElement) { - final NodeList segmentTimelineElementChildren = segmentTimelineElement.getChildNodes(); - final int segmentTimelineElementChildrenLength = segmentTimelineElementChildren - .getLength(); - assertNotEquals(0, segmentTimelineElementChildrenLength, - "The DASH manifest doesn't have a segment element (S) in the SegmentTimeLine element"); - - for (int i = 0; i < segmentTimelineElementChildrenLength; i++) { - final Element sElement = (Element) segmentTimelineElement.getElementsByTagName("S") - .item(i); - - final String dValue = sElement.getAttribute("d"); - assertFalse(isBlank(dValue), - "The value of the duration of this segment (the d attribute of this S element) is empty or the corresponding attribute doesn't exist"); - - final int d; - try { - d = Integer.parseInt(dValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the d attribute is not an integer", e); - } - assertTrue(d > 0, "The value of the d attribute is less than or equal to 0"); - - final String rValue = sElement.getAttribute("r"); - // A segment duration can or can't be repeated, so test the next segment if there - // is no r attribute - if (!isBlank(rValue)) { - final int r; - try { - r = Integer.parseInt(dValue); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the r attribute is not an integer", - e); - } - assertTrue(r > 0, "The value of the r attribute is less than or equal to 0"); - } - } - } - - private void testBaseUrlElement(@Nonnull final Document document) { - final Element baseURLElement = (Element) document - .getElementsByTagName("BaseURL").item(0); - assertNotNull(baseURLElement, "The BaseURL element doesn't exist"); - assertTrue(baseURLElement.getParentNode().isEqualNode( - document.getElementsByTagName("Representation").item(0)), - "The Representation element doesn't contain a BaseURL element"); - - final String baseURLElementContentValue = baseURLElement - .getTextContent(); - assertFalse(isBlank(baseURLElementContentValue), - "The content of the BaseURL element is empty or the corresponding element has no content"); - - try { - new URL(baseURLElementContentValue); - } catch (final MalformedURLException e) { - throw new AssertionError("The content of the BaseURL element is not an URL", e); - } - } - - private void testSegmentBaseElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { - final Element segmentBaseElement = (Element) document - .getElementsByTagName("SegmentBase").item(0); - assertNotNull(segmentBaseElement, "The SegmentBase element doesn't exist"); - assertTrue(segmentBaseElement.getParentNode().isEqualNode( - document.getElementsByTagName("Representation").item(0)), - "The Representation element doesn't contain a SegmentBase element"); - - final String indexRangeValue = segmentBaseElement - .getAttribute("indexRange"); - assertFalse(isBlank(indexRangeValue), - "The value of the indexRange attribute is empty or the corresponding attribute doesn't exist"); - final String[] indexRangeParts = indexRangeValue.split("-"); - assertEquals(2, indexRangeParts.length, - "The value of the indexRange attribute is not valid"); - - final int dashManifestIndexStart; - try { - dashManifestIndexStart = Integer.parseInt(indexRangeParts[0]); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the indexRange attribute is not valid", e); - } - - final int itagItemIndexStart = itagItem.getIndexStart(); - assertTrue(itagItemIndexStart > 0, - "The indexStart of the ItagItem is less than or equal to 0"); - assertEquals(dashManifestIndexStart, itagItemIndexStart, - "The indexStart value of the indexRange attribute of the DASH manifest (" - + dashManifestIndexStart - + ") is not equal to the indexStart of the ItagItem object (" - + itagItemIndexStart + ")"); - - final int dashManifestIndexEnd; - try { - dashManifestIndexEnd = Integer.parseInt(indexRangeParts[1]); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the indexRange attribute is not valid", e); - } - - final int itagItemIndexEnd = itagItem.getIndexEnd(); - assertTrue(itagItemIndexEnd > 0, - "The indexEnd of the ItagItem is less than or equal to 0"); - - assertEquals(dashManifestIndexEnd, itagItemIndexEnd, - "The indexEnd value of the indexRange attribute of the DASH manifest (" - + dashManifestIndexEnd - + ") is not equal to the indexEnd of the ItagItem object (" - + itagItemIndexEnd + ")"); - } - - private void testInitializationElement(@Nonnull final Document document, - @Nonnull final ItagItem itagItem) { - final Element initializationElement = (Element) document - .getElementsByTagName("Initialization").item(0); - assertNotNull(initializationElement, "The Initialization element doesn't exist"); - assertTrue(initializationElement.getParentNode().isEqualNode( - document.getElementsByTagName("SegmentBase").item(0)), - "The SegmentBase element doesn't contain an Initialization element"); - - final String rangeValue = initializationElement - .getAttribute("range"); - assertFalse(isBlank(rangeValue), - "The value of the range attribute is empty or the corresponding attribute doesn't exist"); - final String[] rangeParts = rangeValue.split("-"); - assertEquals(2, rangeParts.length, "The value of the range attribute is not valid"); - - final int dashManifestInitStart; - try { - dashManifestInitStart = Integer.parseInt(rangeParts[0]); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the range attribute is not valid", e); - } - - final int itagItemInitStart = itagItem.getInitStart(); - assertTrue(itagItemInitStart >= 0, "The initStart of the ItagItem is less than 0"); - assertEquals(dashManifestInitStart, itagItemInitStart, - "The initStart value of the range attribute of the DASH manifest (" - + dashManifestInitStart - + ") is not equal to the initStart of the ItagItem object (" - + itagItemInitStart + ")"); - - final int dashManifestInitEnd; - try { - dashManifestInitEnd = Integer.parseInt(rangeParts[1]); - } catch (final NumberFormatException e) { - throw new AssertionError("The value of the indexRange attribute is not valid", e); - } - - final int itagItemInitEnd = itagItem.getInitEnd(); - assertTrue(itagItemInitEnd > 0, "The indexEnd of the ItagItem is less than or equal to 0"); - assertEquals(dashManifestInitEnd, itagItemInitEnd, - "The initEnd value of the range attribute of the DASH manifest (" - + dashManifestInitEnd - + ") is not equal to the initEnd of the ItagItem object (" - + itagItemInitEnd + ")"); + private static final int MAX_STREAMS_TO_TEST_PER_METHOD = 3; + private static final String url = "https://www.youtube.com/watch?v=DJ8GQUNUXGM"; + private static YoutubeStreamExtractor extractor; + private static long videoLength; + + @BeforeAll + public static void setUp() throws Exception { + YoutubeParsingHelper.resetClientVersionAndKey(); + YoutubeParsingHelper.setNumberGenerator(new Random(1)); + NewPipe.init(DownloaderTestImpl.getInstance()); + + extractor = (YoutubeStreamExtractor) YouTube.getStreamExtractor(url); + extractor.fetchPage(); + videoLength = extractor.getLength(); + } + + @Test + void testOtfStreams() throws Exception { + assertDashStreams(extractor.getVideoOnlyStreams()); + assertDashStreams(extractor.getAudioStreams()); + + // no video stream with audio uses the DASH delivery method (YouTube OTF stream type) + assertEquals(0, assertFilterStreams(extractor.getVideoStreams(), + DeliveryMethod.DASH).size()); + } + + @Test + void testProgressiveStreams() throws Exception { + assertProgressiveStreams(extractor.getVideoOnlyStreams()); + assertProgressiveStreams(extractor.getAudioStreams()); + + // we are not able to generate DASH manifests of video formats with audio + assertThrows(YoutubeDashManifestCreator.YoutubeDashManifestCreationException.class, + () -> assertProgressiveStreams(extractor.getVideoStreams())); + } + + private void assertDashStreams(final List streams) throws Exception { + + for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.DASH)) { + //noinspection ConstantConditions + final String manifest = YoutubeDashManifestCreator.fromOtfStreamingUrl( + stream.getContent(), stream.getItagItem(), videoLength); + assertNotBlank(manifest); + + assertManifestGenerated( + manifest, + stream.getItagItem(), + document -> assertAll( + () -> assertSegmentTemplateElement(document), + () -> assertSegmentTimelineAndSElements(document) + ) + ); } } + + private void assertProgressiveStreams(final List streams) throws Exception { + + for (final Stream stream : assertFilterStreams(streams, DeliveryMethod.PROGRESSIVE_HTTP)) { + //noinspection ConstantConditions + final String manifest = YoutubeDashManifestCreator.fromProgressiveStreamingUrl( + stream.getContent(), stream.getItagItem(), videoLength); + assertNotBlank(manifest); + + assertManifestGenerated( + manifest, + stream.getItagItem(), + document -> assertAll( + () -> assertBaseUrlElement(document), + () -> assertSegmentBaseElement(document, stream.getItagItem()), + () -> assertInitializationElement(document, stream.getItagItem()) + ) + ); + } + } + + private List assertFilterStreams(final List streams, + final DeliveryMethod deliveryMethod) { + + final List filteredStreams = streams.stream() + .filter(stream -> stream.getDeliveryMethod() == deliveryMethod) + .limit(MAX_STREAMS_TO_TEST_PER_METHOD) + .collect(Collectors.toList()); + + assertAll(filteredStreams.stream() + .flatMap(stream -> java.util.stream.Stream.of( + () -> assertNotBlank(stream.getContent()), + () -> assertNotNull(stream.getItagItem()) + )) + ); + + return filteredStreams; + } + + private void assertManifestGenerated(final String dashManifest, + final ItagItem itagItem, + final Consumer additionalAsserts) + throws Exception { + + final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + final Document document = documentBuilder.parse(new InputSource( + new StringReader(dashManifest))); + + assertAll( + () -> assertMpdElement(document), + () -> assertPeriodElement(document), + () -> assertAdaptationSetElement(document, itagItem), + () -> assertRoleElement(document), + () -> assertRepresentationElement(document, itagItem), + () -> { + if (itagItem.itagType.equals(ItagItem.ItagType.AUDIO)) { + assertAudioChannelConfigurationElement(document, itagItem); + } + }, + () -> additionalAsserts.accept(document) + ); + } + + private void assertMpdElement(@Nonnull final Document document) { + final Element element = (Element) document.getElementsByTagName("MPD").item(0); + assertNotNull(element); + assertNull(element.getParentNode().getNodeValue()); + + final String mediaPresentationDuration = element.getAttribute("mediaPresentationDuration"); + assertNotNull(mediaPresentationDuration); + assertTrue(mediaPresentationDuration.startsWith("PT")); + } + + private void assertPeriodElement(@Nonnull final Document document) { + assertGetElement(document, "Period", "MPD"); + } + + private void assertAdaptationSetElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "AdaptationSet", "Period"); + assertAttrEquals(itagItem.getMediaFormat().getMimeType(), element, "mimeType"); + } + + private void assertRoleElement(@Nonnull final Document document) { + assertGetElement(document, "Role", "AdaptationSet"); + } + + private void assertRepresentationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "Representation", "AdaptationSet"); + + assertAttrEquals(itagItem.getBitrate(), element, "bandwidth"); + assertAttrEquals(itagItem.getCodec(), element, "codecs"); + + if (itagItem.itagType == ItagItem.ItagType.VIDEO_ONLY + || itagItem.itagType == ItagItem.ItagType.VIDEO) { + assertAttrEquals(itagItem.getFps(), element, "frameRate"); + assertAttrEquals(itagItem.getHeight(), element, "height"); + assertAttrEquals(itagItem.getWidth(), element, "width"); + } + + assertAttrEquals(itagItem.id, element, "id"); + } + + private void assertAudioChannelConfigurationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, + "AudioChannelConfiguration", "Representation"); + assertAttrEquals(itagItem.getAudioChannels(), element, "value"); + } + + private void assertSegmentTemplateElement(@Nonnull final Document document) { + final Element element = assertGetElement(document, "SegmentTemplate", "Representation"); + + final String initializationValue = element.getAttribute("initialization"); + assertIsValidUrl(initializationValue); + assertTrue(initializationValue.endsWith("&sq=0")); + + final String mediaValue = element.getAttribute("media"); + assertIsValidUrl(mediaValue); + assertTrue(mediaValue.endsWith("&sq=$Number$")); + + assertEquals("1", element.getAttribute("startNumber")); + } + + private void assertSegmentTimelineAndSElements(@Nonnull final Document document) { + final Element element = assertGetElement(document, "SegmentTimeline", "SegmentTemplate"); + final NodeList childNodes = element.getChildNodes(); + assertGreater(0, childNodes.getLength()); + + assertAll(IntStream.range(0, childNodes.getLength()) + .mapToObj(childNodes::item) + .map(Element.class::cast) + .flatMap(sElement -> java.util.stream.Stream.of( + () -> assertEquals("S", sElement.getTagName()), + () -> assertGreater(0, Integer.parseInt(sElement.getAttribute("d"))), + () -> { + final String rValue = sElement.getAttribute("r"); + // A segment duration can or can't be repeated, so test the next segment + // if there is no r attribute + if (!isBlank(rValue)) { + assertGreater(0, Integer.parseInt(rValue)); + } + } + ) + ) + ); + } + + private void assertBaseUrlElement(@Nonnull final Document document) { + final Element element = assertGetElement(document, "BaseURL", "Representation"); + assertIsValidUrl(element.getTextContent()); + } + + private void assertSegmentBaseElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "SegmentBase", "Representation"); + assertRangeEquals(itagItem.getIndexStart(), itagItem.getIndexEnd(), element, "indexRange"); + } + + private void assertInitializationElement(@Nonnull final Document document, + @Nonnull final ItagItem itagItem) { + final Element element = assertGetElement(document, "Initialization", "SegmentBase"); + assertRangeEquals(itagItem.getInitStart(), itagItem.getInitEnd(), element, "range"); + } + + + private void assertAttrEquals(final int expected, + final Element element, + final String attribute) { + + final int actual = Integer.parseInt(element.getAttribute(attribute)); + assertAll( + () -> assertGreater(0, actual), + () -> assertEquals(expected, actual) + ); + } + + private void assertAttrEquals(final String expected, + final Element element, + final String attribute) { + final String actual = element.getAttribute(attribute); + assertAll( + () -> assertNotBlank(actual), + () -> assertEquals(expected, actual) + ); + } + + private void assertRangeEquals(final int expectedStart, + final int expectedEnd, + final Element element, + final String attribute) { + final String range = element.getAttribute(attribute); + assertNotBlank(range); + final String[] rangeParts = range.split("-"); + assertEquals(2, rangeParts.length); + + final int actualStart = Integer.parseInt(rangeParts[0]); + final int actualEnd = Integer.parseInt(rangeParts[1]); + + assertAll( + () -> assertGreaterOrEqual(0, actualStart), + () -> assertEquals(expectedStart, actualStart), + () -> assertGreater(0, actualEnd), + () -> assertEquals(expectedEnd, actualEnd) + ); + } + + private Element assertGetElement(final Document document, + final String tagName, + final String expectedParentTagName) { + + final Element element = (Element) document.getElementsByTagName(tagName).item(0); + assertNotNull(element); + assertTrue(element.getParentNode().isEqualNode( + document.getElementsByTagName(expectedParentTagName).item(0)), + "Element with tag name \"" + tagName + "\" does not have a parent node" + + " with tag name \"" + expectedParentTagName + "\""); + return element; + } }