NewPipeExtractor/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/dashmanifestcreators/YoutubeProgressiveDashManif...

245 lines
11 KiB
Java

package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators;
import org.schabi.newpipe.extractor.services.youtube.DeliveryType;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.utils.ManifestCreatorCache;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.annotation.Nonnull;
import java.util.Objects;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.BASE_URL;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.INITIALIZATION;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.MPD;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.REPRESENTATION;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_BASE;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration;
/**
* Class which generates DASH manifests of {@link DeliveryType#PROGRESSIVE YouTube progressive}
* streams.
*/
public final class YoutubeProgressiveDashManifestCreator {
/**
* Cache of DASH manifests generated for progressive streams.
*/
private static final ManifestCreatorCache<String, String> PROGRESSIVE_STREAMS_CACHE
= new ManifestCreatorCache<>();
private YoutubeProgressiveDashManifestCreator() {
}
/**
* Create DASH manifests from a YouTube progressive stream.
*
* <p>
* Progressive streams are YouTube DASH streams which work with range requests and without the
* need to get a manifest.
* </p>
*
* <p>
* They can be found on all videos, and for all streams for most of videos which come from a
* YouTube partner, and on videos with a large number of views.
* </p>
*
* <p>This method needs:
* <ul>
* <li>the base URL of the stream (which, if you try to access to it, returns the whole
* stream, after redirects, and if the URL is valid);</li>
* <li>an {@link ItagItem}, which needs to contain the following information:
* <ul>
* <li>its type (see {@link ItagItem.ItagType}), to identify if the content is
* an audio or a video stream;</li>
* <li>its bitrate;</li>
* <li>its mime type;</li>
* <li>its codec(s);</li>
* <li>for an audio stream: its audio channels;</li>
* <li>for a video stream: its width and height.</li>
* </ul>
* </li>
* <li>the duration of the video (parameter {@code durationSecondsFallback}), which
* will be used as the stream duration if the duration could not be parsed from the
* {@link ItagItem}.</li>
* </ul>
* </p>
*
* @param progressiveStreamingBaseUrl the base URL of the progressive stream, which must not be
* null
* @param itagItem the {@link ItagItem} corresponding to the stream, which
* must not be null
* @param durationSecondsFallback the duration of the progressive stream which will be used
* if the duration could not be extracted from the
* {@link ItagItem}
* @return the manifest generated into a string
*/
@Nonnull
public static String fromProgressiveStreamingUrl(
@Nonnull final String progressiveStreamingBaseUrl,
@Nonnull final ItagItem itagItem,
final long durationSecondsFallback) throws CreationException {
if (PROGRESSIVE_STREAMS_CACHE.containsKey(progressiveStreamingBaseUrl)) {
return Objects.requireNonNull(
PROGRESSIVE_STREAMS_CACHE.get(progressiveStreamingBaseUrl)).getSecond();
}
final long itagItemDuration = itagItem.getApproxDurationMs();
final long streamDuration;
if (itagItemDuration != -1) {
streamDuration = itagItemDuration;
} else {
if (durationSecondsFallback > 0) {
streamDuration = durationSecondsFallback * 1000;
} else {
throw CreationException.couldNotAddElement(MPD, "the duration of the stream "
+ "could not be determined and durationSecondsFallback is <= 0");
}
}
final Document document = generateDocumentAndDoCommonElementsGeneration(itagItem,
streamDuration);
generateBaseUrlElement(document, progressiveStreamingBaseUrl);
generateSegmentBaseElement(document, itagItem);
generateInitializationElement(document, itagItem);
return buildAndCacheResult(progressiveStreamingBaseUrl, document,
PROGRESSIVE_STREAMS_CACHE);
}
/**
* @return the cache of DASH manifests generated for progressive streams
*/
public static ManifestCreatorCache<String, String> getCache() {
return PROGRESSIVE_STREAMS_CACHE;
}
/**
* Generate the {@code <BaseURL>} element, appended as a child of the
* {@code <Representation>} element.
*
* <p>
* The {@code <Representation>} element needs to be generated before this element with
* {@link YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}).
* </p>
*
* @param document the {@link Document} on which the {@code <BaseURL>} element will
* be appended
* @param baseUrl the base URL of the stream, which must not be null and will be set as the
* content of the {@code <BaseURL>} element
*/
private static void generateBaseUrlElement(@Nonnull final Document document,
@Nonnull final String baseUrl)
throws CreationException {
try {
final Element representationElement = (Element) document.getElementsByTagName(
REPRESENTATION).item(0);
final Element baseURLElement = document.createElement(BASE_URL);
baseURLElement.setTextContent(baseUrl);
representationElement.appendChild(baseURLElement);
} catch (final DOMException e) {
throw CreationException.couldNotAddElement(BASE_URL, e);
}
}
/**
* Generate the {@code <SegmentBase>} element, appended as a child of the
* {@code <Representation>} element.
*
* <p>
* It generates the following element:
* <br>
* {@code <SegmentBase indexRange="indexStart-indexEnd" />}
* <br>
* (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 YoutubeDashManifestCreatorsUtils#generateRepresentationElement(Document, ItagItem)}),
* and the {@code BaseURL} element with {@link #generateBaseUrlElement(Document, String)}
* should be generated too.
* </p>
*
* @param document the {@link Document} on which the {@code <SegmentBase>} element will be
* appended
* @param itagItem the {@link ItagItem} to use, which must not be null
*/
private static void generateSegmentBaseElement(@Nonnull final Document document,
@Nonnull final ItagItem itagItem)
throws CreationException {
try {
final Element representationElement = (Element) document.getElementsByTagName(
REPRESENTATION).item(0);
final Element segmentBaseElement = document.createElement(SEGMENT_BASE);
final Attr indexRangeAttribute = document.createAttribute("indexRange");
if (itagItem.getIndexStart() < 0 || itagItem.getIndexEnd() < 0) {
throw CreationException.couldNotAddElement(SEGMENT_BASE,
"ItagItem's indexStart or " + "indexEnd are < 0: "
+ itagItem.getIndexStart() + "-" + itagItem.getIndexEnd());
}
indexRangeAttribute.setValue(itagItem.getIndexStart() + "-" + itagItem.getIndexEnd());
segmentBaseElement.setAttributeNode(indexRangeAttribute);
representationElement.appendChild(segmentBaseElement);
} catch (final DOMException e) {
throw CreationException.couldNotAddElement(SEGMENT_BASE, e);
}
}
/**
* Generate the {@code <Initialization>} element, appended as a child of the
* {@code <SegmentBase>} element.
*
* <p>
* It generates the following element:
* <br>
* {@code <Initialization range="initStart-initEnd"/>}
* <br>
* (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)}).
* </p>
*
* @param document the {@link Document} on which the {@code <Initialization>} element will
* be appended
* @param itagItem the {@link ItagItem} to use, which must not be null
*/
private static void generateInitializationElement(@Nonnull final Document document,
@Nonnull final ItagItem itagItem)
throws CreationException {
try {
final Element segmentBaseElement = (Element) document.getElementsByTagName(
SEGMENT_BASE).item(0);
final Element initializationElement = document.createElement(INITIALIZATION);
final Attr rangeAttribute = document.createAttribute("range");
if (itagItem.getInitStart() < 0 || itagItem.getInitEnd() < 0) {
throw CreationException.couldNotAddElement(INITIALIZATION,
"ItagItem's initStart and/or " + "initEnd are/is < 0: "
+ itagItem.getInitStart() + "-" + itagItem.getInitEnd());
}
rangeAttribute.setValue(itagItem.getInitStart() + "-" + itagItem.getInitEnd());
initializationElement.setAttributeNode(rangeAttribute);
segmentBaseElement.appendChild(initializationElement);
} catch (final DOMException e) {
throw CreationException.couldNotAddElement(INITIALIZATION, e);
}
}
}