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

218 lines
11 KiB
Java

package org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.services.youtube.DeliveryType;
import org.schabi.newpipe.extractor.services.youtube.ItagItem;
import org.schabi.newpipe.extractor.utils.ManifestCreatorCache;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.ALR_YES;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.RN_0;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SEGMENT_TIMELINE;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.SQ_0;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.buildAndCacheResult;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateDocumentAndDoCommonElementsGeneration;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTemplateElement;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.generateSegmentTimelineElement;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.getInitializationResponse;
import static org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeDashManifestCreatorsUtils.setAttribute;
import static org.schabi.newpipe.extractor.utils.Utils.EMPTY_STRING;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/**
* Class which generates DASH manifests of YouTube post-live DVR streams (which use the
* {@link DeliveryType#LIVE LIVE delivery type}).
*/
public final class YoutubePostLiveStreamDvrDashManifestCreator {
/**
* Cache of DASH manifests generated for post-live-DVR streams.
*/
private static final ManifestCreatorCache<String, String> POST_LIVE_DVR_STREAMS_CACHE
= new ManifestCreatorCache<>();
private YoutubePostLiveStreamDvrDashManifestCreator() {
}
/**
* Create DASH manifests from a YouTube post-live-DVR stream/ended livestream.
*
* <p>
* Post-live-DVR streams/ended livestreams are one of the YouTube DASH specific streams which
* works with sequences and without the need to get a manifest (even if one is provided but not
* used by main clients (and is not complete for big ended livestreams because it doesn't
* return the full stream)).
* </p>
*
* <p>
* They can be found only on livestreams which have ended very recently (a few hours, most of
* the time)
* </p>
*
* <p>This method needs:
* <ul>
* <li>the base URL of the stream (which, if you try to access to it, returns HTTP
* status code 404 after redirects, and if the URL is valid);</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, which will be used if the duration could not be
* parsed from the first sequence of the stream.</li>
* </ul>
* </p>
*
* <p>In order to generate the DASH manifest, this method will:
* <ul>
* <li>request the first sequence of the stream (the base URL on which the first
* sequence parameter is appended (see {@link YoutubeDashManifestCreatorsUtils#SQ_0}))
* with a {@code POST} or {@code GET} request (depending of the client on which the
* streaming URL comes from is a mobile one ({@code POST}) or not ({@code GET}));</li>
* <li>follow its redirection(s), if any;</li>
* <li>save the last URL, remove the first sequence parameters;</li>
* <li>use the information provided in the {@link ItagItem} to generate all elements
* of the DASH manifest.</li>
* </ul>
* </p>
*
* <p>
* If the duration cannot be extracted, the {@code durationSecondsFallback} value will be used
* as the stream duration.
* </p>
*
* @param postLiveStreamDvrStreamingUrl the base URL of the post-live-DVR stream/ended
* livestream, which must not be null
* @param itagItem the {@link ItagItem} corresponding to the stream, which
* must not be null
* @param targetDurationSec the target duration of each sequence, in seconds (this
* value is returned with the {@code targetDurationSec}
* field for each stream in YouTube's player response)
* @param durationSecondsFallback the duration of the ended livestream, which will be
* used if the duration could not be extracted from the
* first sequence
* @return the manifest generated into a string
*/
@Nonnull
public static String fromPostLiveStreamDvrStreamingUrl(
@Nonnull final String postLiveStreamDvrStreamingUrl,
@Nonnull final ItagItem itagItem,
final int targetDurationSec,
final long durationSecondsFallback) throws CreationException {
if (POST_LIVE_DVR_STREAMS_CACHE.containsKey(postLiveStreamDvrStreamingUrl)) {
return Objects.requireNonNull(
POST_LIVE_DVR_STREAMS_CACHE.get(postLiveStreamDvrStreamingUrl)).getSecond();
}
String realPostLiveStreamDvrStreamingUrl = postLiveStreamDvrStreamingUrl;
final String streamDurationString;
final String segmentCount;
if (targetDurationSec <= 0) {
throw new CreationException("targetDurationSec value is <= 0: " + targetDurationSec);
}
try {
// Try to avoid redirects when streaming the content by saving the latest URL we get
// from video servers.
final Response response = getInitializationResponse(realPostLiveStreamDvrStreamingUrl,
itagItem, DeliveryType.LIVE);
realPostLiveStreamDvrStreamingUrl = response.latestUrl().replace(SQ_0, EMPTY_STRING)
.replace(RN_0, EMPTY_STRING).replace(ALR_YES, EMPTY_STRING);
final int responseCode = response.responseCode();
if (responseCode != 200) {
throw new CreationException(
"Could not get the initialization sequence: response code " + responseCode);
}
final Map<String, List<String>> responseHeaders = response.responseHeaders();
streamDurationString = responseHeaders.get("X-Head-Time-Millis").get(0);
segmentCount = responseHeaders.get("X-Head-Seqnum").get(0);
} catch (final IndexOutOfBoundsException e) {
throw new CreationException(
"Could not get the value of the X-Head-Time-Millis or the X-Head-Seqnum header",
e);
}
if (isNullOrEmpty(segmentCount)) {
throw new CreationException("Could not get the number of segments");
}
long streamDuration;
try {
streamDuration = Long.parseLong(streamDurationString);
} catch (final NumberFormatException e) {
streamDuration = durationSecondsFallback;
}
final Document doc = generateDocumentAndDoCommonElementsGeneration(itagItem,
streamDuration);
generateSegmentTemplateElement(doc, realPostLiveStreamDvrStreamingUrl,
DeliveryType.LIVE);
generateSegmentTimelineElement(doc);
generateSegmentElementForPostLiveDvrStreams(doc, targetDurationSec, segmentCount);
return buildAndCacheResult(postLiveStreamDvrStreamingUrl, doc,
POST_LIVE_DVR_STREAMS_CACHE);
}
/**
* @return the cache of DASH manifests generated for post-live-DVR streams
*/
@Nonnull
public static ManifestCreatorCache<String, String> getCache() {
return POST_LIVE_DVR_STREAMS_CACHE;
}
/**
* Generate the segment ({@code <S>}) element.
*
* <p>
* We don't know the exact duration of segments for post-live-DVR streams but an
* average instead (which is the {@code targetDurationSec} value), so we can use the following
* structure to generate the segment timeline for DASH manifests of ended livestreams:
* <br>
* {@code <S d="targetDurationSecValue" r="segmentCount" />}
* </p>
*
* @param doc the {@link Document} on which the {@code <S>} element will
* be appended
* @param targetDurationSeconds the {@code targetDurationSec} value from YouTube player
* response's stream
* @param segmentCount the number of segments, extracted by {@link
* #fromPostLiveStreamDvrStreamingUrl(String, ItagItem, int, long)}
*/
private static void generateSegmentElementForPostLiveDvrStreams(
@Nonnull final Document doc,
final int targetDurationSeconds,
@Nonnull final String segmentCount) throws CreationException {
try {
final Element segmentTimelineElement = (Element) doc.getElementsByTagName(
SEGMENT_TIMELINE).item(0);
final Element sElement = doc.createElement("S");
setAttribute(sElement, doc, "d", String.valueOf(targetDurationSeconds * 1000));
setAttribute(sElement, doc, "r", segmentCount);
segmentTimelineElement.appendChild(sElement);
} catch (final DOMException e) {
throw CreationException.couldNotAddElement("segment (S)", e);
}
}
}