From 928c2c3b820db68980e8be68f5d1aa389a5ef8ff Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 3 Jul 2026 21:24:36 +0200 Subject: [PATCH 1/7] fix: seed youtube sabr player time --- .../youtube/sabr/YoutubeSabrProbe.java | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrProbe.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrProbe.java index 7be16751..5f970c35 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrProbe.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrProbe.java @@ -51,9 +51,22 @@ public static YoutubeSabrInfo fetchSabrInfo(@Nonnull final String videoId, @Nullable final String playerPoToken, @Nullable final String visitorDataOverride) throws IOException, ExtractionException { + return fetchSabrInfo(videoId, profile, localization, contentCountry, playerPoToken, + visitorDataOverride, null); + } + + @Nonnull + public static YoutubeSabrInfo fetchSabrInfo(@Nonnull final String videoId, + @Nonnull final YoutubeSabrClientProfile profile, + @Nonnull final Localization localization, + @Nonnull final ContentCountry contentCountry, + @Nullable final String playerPoToken, + @Nullable final String visitorDataOverride, + @Nullable final Integer startTimeSecs) + throws IOException, ExtractionException { final String cpn = YoutubeParsingHelper.generateContentPlaybackNonce(); final JsonObject playerResponse = fetchPlayerResponse(videoId, profile, localization, - contentCountry, cpn, playerPoToken, visitorDataOverride); + contentCountry, cpn, playerPoToken, visitorDataOverride, startTimeSecs); return fromPlayerResponse(videoId, profile, cpn, playerResponse, visitorDataOverride); } @@ -243,13 +256,14 @@ private static YoutubeSabrProbeResult postMediaRequest( private static JsonObject fetchPlayerResponse(@Nonnull final String videoId, @Nonnull final YoutubeSabrClientProfile profile, @Nonnull final Localization localization, - @Nonnull final ContentCountry contentCountry, - @Nonnull final String cpn, - @Nullable final String playerPoToken, - @Nullable final String visitorDataOverride) + @Nonnull final ContentCountry contentCountry, + @Nonnull final String cpn, + @Nullable final String playerPoToken, + @Nullable final String visitorDataOverride, + @Nullable final Integer startTimeSecs) throws IOException, ExtractionException { final byte[] body = createPlayerBody(videoId, profile, localization, contentCountry, - cpn, playerPoToken, visitorDataOverride); + cpn, playerPoToken, visitorDataOverride, startTimeSecs); final String url = getInnertubeBaseUrl(profile) + PLAYER + "?" + YoutubeParsingHelper.DISABLE_PRETTY_PRINT_PARAMETER; final Response response = NewPipe.getDownloader().post(url, @@ -265,7 +279,8 @@ private static byte[] createPlayerBody(@Nonnull final String videoId, @Nonnull final ContentCountry contentCountry, @Nonnull final String cpn, @Nullable final String playerPoToken, - @Nullable final String visitorDataOverride) + @Nullable final String visitorDataOverride, + @Nullable final Integer startTimeSecs) throws ParsingException { final JsonBuilder builder = JsonObject.builder() .object("context") @@ -338,6 +353,10 @@ private static byte[] createPlayerBody(@Nonnull final String videoId, .value(YoutubeParsingHelper.CONTENT_CHECK_OK, true) .value(YoutubeParsingHelper.RACY_CHECK_OK, true); + if (startTimeSecs != null && startTimeSecs >= 0) { + builder.value("startTimeSecs", startTimeSecs); + } + if (playerPoToken != null && !playerPoToken.isEmpty()) { builder.object("serviceIntegrityDimensions") .value("poToken", playerPoToken) From aa236e6897b658d4357fc2245a8d6b3e7a852d25 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 3 Jul 2026 21:24:51 +0200 Subject: [PATCH 2/7] fix: carry youtube sabr player state --- .../sabr/YoutubeSabrRequestBuilder.java | 74 +++++---- .../youtube/sabr/YoutubeSabrStreamState.java | 140 ++++++++++++++++-- 2 files changed, 167 insertions(+), 47 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrRequestBuilder.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrRequestBuilder.java index 0a02d10d..a5b376ea 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrRequestBuilder.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrRequestBuilder.java @@ -33,11 +33,21 @@ static byte[] buildFirstMediaRequest(@Nonnull final YoutubeSabrInfo info, } final SabrProto.Writer request = new SabrProto.Writer(); - request.writeMessage(1, buildClientAbrState(audioFormat, videoFormat, 0, false, + final boolean writeFirstRequestPlaybackState = streamState != null + && streamState.shouldWriteFirstRequestPlaybackState(); + final long playerTimeMs = writeFirstRequestPlaybackState ? streamState.getPlayerTimeMs() : 0; + request.writeMessage(1, buildClientAbrState(audioFormat, videoFormat, playerTimeMs, false, streamState == null ? ENABLED_TRACK_TYPES_VIDEO_AND_AUDIO : streamState.getEnabledTrackTypesBitfield(), streamState)); + if (writeFirstRequestPlaybackState) { + writeCurrentFormatSelections(request, audioFormat, videoFormat, streamState); + writeBufferedRanges(request, streamState); + if (streamState.shouldWriteTopLevelPlayerTimeMs()) { + request.writeUInt64(4, playerTimeMs); + } + } request.writeBytes(5, decodeBase64(ustreamerConfig)); writePreferredFormats(request, info, audioFormat, videoFormat, streamState); request.writeMessage(19, streamState == null @@ -72,6 +82,22 @@ private static byte[] buildFollowUpMediaRequestLocked(@Nonnull final YoutubeSabr final SabrProto.Writer request = new SabrProto.Writer(); request.writeMessage(1, buildClientAbrState(audioFormat, videoFormat, playerTimeMs, true, streamState.getEnabledTrackTypesBitfield(), streamState)); + writeCurrentFormatSelections(request, audioFormat, videoFormat, streamState); + writeBufferedRanges(request, streamState); + if (streamState.shouldWriteTopLevelPlayerTimeMs()) { + request.writeUInt64(4, playerTimeMs); + } + request.writeBytes(5, decodeBase64(ustreamerConfig)); + writePreferredFormats(request, info, audioFormat, videoFormat, streamState); + request.writeMessage(19, buildStreamerContext(info, streamState)); + return request.toByteArray(); + } + + private static void writeCurrentFormatSelections( + @Nonnull final SabrProto.Writer request, + @Nonnull final YoutubeSabrFormat audioFormat, + @Nonnull final YoutubeSabrFormat videoFormat, + @Nonnull final YoutubeSabrStreamState streamState) { if (streamState.shouldSelectVideoFormatBeforeAudio() && streamState.shouldSelectVideoFormat()) { request.writeMessage(2, SabrProto.formatId(videoFormat)); } @@ -81,47 +107,19 @@ private static byte[] buildFollowUpMediaRequestLocked(@Nonnull final YoutubeSabr if (!streamState.shouldSelectVideoFormatBeforeAudio() && streamState.shouldSelectVideoFormat()) { request.writeMessage(2, SabrProto.formatId(videoFormat)); } + } + + private static void writeBufferedRanges(@Nonnull final SabrProto.Writer request, + @Nonnull final YoutubeSabrStreamState streamState) { final List bufferedRanges = streamState.getBufferedRanges(); for (final SabrBufferedRange range : bufferedRanges) { request.writeMessage(3, range.toProto(streamState.shouldWriteBufferedRangeTimeRange())); } - if (streamState.shouldWriteTopLevelPlayerTimeMs()) { - request.writeUInt64(4, playerTimeMs); - } - request.writeBytes(5, decodeBase64(ustreamerConfig)); - writePreferredFormats(request, info, audioFormat, videoFormat, streamState); - request.writeMessage(19, buildStreamerContext(info, streamState)); - return request.toByteArray(); - } - - @Nonnull - private static byte[] buildClientAbrState(@Nonnull final YoutubeSabrFormat audioFormat, - @Nonnull final YoutubeSabrFormat videoFormat) { - return buildClientAbrState(audioFormat, videoFormat, 0, false); - } - - @Nonnull - private static byte[] buildClientAbrState(@Nonnull final YoutubeSabrFormat audioFormat, - @Nonnull final YoutubeSabrFormat videoFormat, - final long playerTimeMs, - final boolean includeFollowUpState) { - return buildClientAbrState(audioFormat, videoFormat, playerTimeMs, includeFollowUpState, - ENABLED_TRACK_TYPES_VIDEO_AND_AUDIO); - } - - @Nonnull - private static byte[] buildClientAbrState(@Nonnull final YoutubeSabrFormat audioFormat, - @Nonnull final YoutubeSabrFormat videoFormat, - final long playerTimeMs, - final boolean includeFollowUpState, - final int enabledTrackTypesBitfield) { - return buildClientAbrState(audioFormat, videoFormat, playerTimeMs, includeFollowUpState, - enabledTrackTypesBitfield, null); } @Nonnull private static byte[] buildClientAbrState(@Nonnull final YoutubeSabrFormat audioFormat, - @Nonnull final YoutubeSabrFormat videoFormat, + @Nonnull final YoutubeSabrFormat videoFormat, final long playerTimeMs, final boolean includeFollowUpState, final int enabledTrackTypesBitfield, @@ -224,21 +222,21 @@ private static void writePreferredFormats(@Nonnull final SabrProto.Writer reques return; } for (final YoutubeSabrFormat format : info.getFormats()) { - if (format.isAudio() && streamState.shouldSelectAudioFormat()) { + if (format.isAudio() && streamState.shouldPreferAudioFormat()) { request.writeMessage(16, SabrProto.formatId(format)); } } for (final YoutubeSabrFormat format : info.getFormats()) { - if (format.isVideo() && streamState.shouldSelectVideoFormat()) { + if (format.isVideo() && streamState.shouldPreferVideoFormat()) { request.writeMessage(17, SabrProto.formatId(format)); } } return; } - if (streamState == null || streamState.shouldSelectAudioFormat()) { + if (streamState == null || streamState.shouldPreferAudioFormat()) { request.writeMessage(16, SabrProto.formatId(audioFormat)); } - if (streamState == null || streamState.shouldSelectVideoFormat()) { + if (streamState == null || streamState.shouldPreferVideoFormat()) { request.writeMessage(17, SabrProto.formatId(videoFormat)); } } diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrStreamState.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrStreamState.java index 41fdb616..ef734fe2 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrStreamState.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrStreamState.java @@ -35,6 +35,9 @@ public final class YoutubeSabrStreamState { private volatile int enabledTrackTypesBitfield = YoutubeSabrRequestBuilder.ENABLED_TRACK_TYPES_VIDEO_AND_AUDIO; private volatile boolean selectAudioFormat = true; private volatile boolean selectVideoFormat = true; + private volatile boolean preferAudioFormat = true; + private volatile boolean preferVideoFormat = true; + private boolean writeFirstRequestPlaybackState; private boolean writeTopLevelPlayerTimeMs = true; private int clientViewportWidth = -1; private int clientViewportHeight = -1; @@ -159,13 +162,27 @@ public List getBufferedRanges() { return ranges; } - public void setBufferedRangesOverride( + void setBufferedRangesOverride( @Nullable final List bufferedRangesOverride) { this.bufferedRangesOverride = bufferedRangesOverride == null ? null : new ArrayList<>(bufferedRangesOverride); } + @Nullable + SabrBufferedRange getObservedBufferedRange( + @Nonnull final YoutubeSabrFormat format) { + return progressForItag(format.getItag()).getObservedBufferedRange(); + } + + void setWriteFirstRequestPlaybackState(final boolean writeFirstRequestPlaybackState) { + this.writeFirstRequestPlaybackState = writeFirstRequestPlaybackState; + } + + boolean shouldWriteFirstRequestPlaybackState() { + return writeFirstRequestPlaybackState; + } + public long getPlayerTimeMs() { if (playerTimeMsOverride >= 0) { return playerTimeMsOverride; @@ -362,11 +379,19 @@ public void setBufferedRangeSegmentIndexOffsets(final int startSegmentIndexOffse } public synchronized void setRequestTrackMode(final int enabledTrackTypesBitfield, - final boolean selectAudioFormat, - final boolean selectVideoFormat) { + final boolean selectAudioFormat, + final boolean selectVideoFormat) { this.enabledTrackTypesBitfield = enabledTrackTypesBitfield; this.selectAudioFormat = selectAudioFormat; this.selectVideoFormat = selectVideoFormat; + this.preferAudioFormat = selectAudioFormat; + this.preferVideoFormat = selectVideoFormat; + } + + synchronized void setPreferredTrackTypes(final boolean videoActive, + final boolean audioActive) { + preferAudioFormat = audioActive; + preferVideoFormat = videoActive; } public void setActiveTrackTypes(final boolean videoActive, final boolean audioActive) { @@ -448,6 +473,14 @@ boolean shouldSelectVideoFormat() { return selectVideoFormat; } + boolean shouldPreferAudioFormat() { + return preferAudioFormat; + } + + boolean shouldPreferVideoFormat() { + return preferVideoFormat; + } + public void setWriteTopLevelPlayerTimeMs(final boolean writeTopLevelPlayerTimeMs) { this.writeTopLevelPlayerTimeMs = writeTopLevelPlayerTimeMs; } @@ -673,6 +706,9 @@ private static final class FormatProgress { private long observedStartMs = -1; private long observedEndMs = -1; private long lastObservedDurationMs = -1; + private final Map observedTimings = new LinkedHashMap<>(); + private long observedDurationTotalMs; + private int observedTimingCount; @Nullable private SabrFormatInitializationMetadata metadata; @Nullable @@ -688,12 +724,15 @@ private FormatProgress(@Nonnull final YoutubeSabrFormat format) { private boolean observeMetadata(@Nonnull final SabrFormatInitializationMetadata metadata) { this.metadata = metadata; final long previousEndSegment = endSegment; - endSegment = metadata.getEndSegmentNumber(); + final long metadataEndSegment = metadata.getEndSegmentNumber(); + if (metadataEndSegment > 0) { + endSegment = metadataEndSegment; + } if (metadata.getDurationUnits() > 0 && metadata.getDurationTimescale() > 0 - && metadata.getEndSegmentNumber() > 0) { + && metadataEndSegment > 0) { final long totalMs = metadata.getDurationUnits() * 1000L / metadata.getDurationTimescale(); - averageDurationMs = Math.max(1L, totalMs / metadata.getEndSegmentNumber()); + averageDurationMs = Math.max(1L, totalMs / metadataEndSegment); } else if (endSegment > 0 && format.getApproxDurationMs() > 0) { // The init metadata gives the segment count but no per-segment timing for this // format (seen on some YouTube responses). Derive the average from the format's @@ -797,6 +836,7 @@ private boolean observeHeader(@Nonnull final SabrMediaHeader header) { lastObservedDurationMs = header.getDurationMs(); } if (header.getStartMs() >= 0 && header.getDurationMs() > 0) { + observeTiming(seq, header.getStartMs(), header.getDurationMs()); if (observedStartMs < 0 || header.getStartMs() < observedStartMs) { observedStartMs = header.getStartMs(); } @@ -808,6 +848,22 @@ private boolean observeHeader(@Nonnull final SabrMediaHeader header) { return false; } + private void observeTiming(final int sequenceNumber, + final long startMs, + final long durationMs) { + final ObservedSegmentTiming previous = observedTimings.put(sequenceNumber, + new ObservedSegmentTiming(sequenceNumber, startMs, durationMs)); + if (previous == null) { + observedDurationTotalMs += durationMs; + observedTimingCount++; + } else { + observedDurationTotalMs += durationMs - previous.durationMs; + } + if (observedTimingCount > 0 && observedDurationTotalMs > 0) { + averageDurationMs = Math.max(1L, observedDurationTotalMs / observedTimingCount); + } + } + private void addBufferedRange(@Nonnull final List ranges, final boolean lastOnlyRange, final boolean lastOnlyRangeUseObservedTiming, @@ -840,6 +896,20 @@ private void addBufferedRange(@Nonnull final List ranges, applySegmentIndexOffset(contiguousMaxSegment, endSegmentIndexOffset), 1000)); } + @Nullable + private SabrBufferedRange getObservedBufferedRange() { + if (firstObservedSegment <= 0 || lastObservedSegment <= 0) { + return null; + } + final long startMs = observedStartMs >= 0 ? observedStartMs : getSegmentStartMs(firstObservedSegment); + final long endMs = observedEndMs > startMs ? observedEndMs : getSegmentEndMs(lastObservedSegment); + if (endMs <= startMs) { + return null; + } + return new SabrBufferedRange(itag, lastModified, xtags, startMs, endMs - startMs, + firstObservedSegment, lastObservedSegment, 1000); + } + private int applySegmentIndexOffset(final int segmentIndex, final int segmentIndexOffset) { return Math.max(0, segmentIndex + segmentIndexOffset); @@ -855,6 +925,10 @@ private long getBufferedEndMs() { } private long getSegmentStartMs(final int sequenceNumber) { + final ObservedSegmentTiming observed = observedTimings.get(sequenceNumber); + if (observed != null) { + return observed.startMs; + } if (sequenceNumber <= 1) { return 0; } @@ -868,6 +942,10 @@ private long getSegmentStartMs(final int sequenceNumber) { } private long getSegmentEndMs(final int sequenceNumber) { + final ObservedSegmentTiming observed = observedTimings.get(sequenceNumber); + if (observed != null) { + return observed.endMs(); + } if (segmentIndex != null) { final SabrSegmentIndex.Entry entry = segmentIndex.getEntry(sequenceNumber); if (entry != null) { @@ -884,24 +962,46 @@ private int getSegmentNumberAtOrAfterTimeMs(final long timeMs) { if (timeMs <= 0) { return 1; } + for (final ObservedSegmentTiming observed : observedTimings.values()) { + if (observed.contains(timeMs)) { + return observed.sequenceNumber; + } + } if (segmentIndex != null) { for (int i = 1; i <= segmentIndex.size(); i++) { final SabrSegmentIndex.Entry entry = segmentIndex.getEntry(i); - if (entry != null && entry.getEndMs() >= timeMs) { + if (entry != null && entry.getStartMs() <= timeMs && timeMs < entry.getEndMs()) { return entry.getSequenceNumber(); } } return Math.max(1, segmentIndex.size()); } final long durationMs = Math.max(1, averageDurationMs); - final long sequenceNumber = (timeMs + durationMs - 1) / durationMs; + long sequenceNumber = timeMs / durationMs + 1; + if (endSegment > 0) { + sequenceNumber = Math.min(sequenceNumber, endSegment); + } return sequenceNumber > Integer.MAX_VALUE ? Integer.MAX_VALUE : Math.max(1, (int) sequenceNumber); } private void assumeBufferedUntil(final int endSegment) { - maxSegment = Math.max(maxSegment, endSegment); + if (endSegment <= 0) { + return; + } + if (endSegment > contiguousMaxSegment) { + contiguousMaxSegment = endSegment; + aheadOfContiguous.removeIf(seq -> seq <= contiguousMaxSegment); + while (aheadOfContiguous.remove(contiguousMaxSegment + 1)) { + contiguousMaxSegment++; + } + firstObservedSegment = -1; + lastObservedSegment = -1; + observedStartMs = -1; + observedEndMs = -1; + } + maxSegment = Math.max(maxSegment, contiguousMaxSegment); } private void rewindBufferedTo(final int fromSegment) { @@ -952,4 +1052,26 @@ private boolean isComplete() { return initReceived && endSegment > 0 && maxSegment >= endSegment; } } + + private static final class ObservedSegmentTiming { + private final int sequenceNumber; + private final long startMs; + private final long durationMs; + + private ObservedSegmentTiming(final int sequenceNumber, + final long startMs, + final long durationMs) { + this.sequenceNumber = sequenceNumber; + this.startMs = startMs; + this.durationMs = durationMs; + } + + private long endMs() { + return startMs + durationMs; + } + + private boolean contains(final long timeMs) { + return startMs <= timeMs && timeMs < endMs(); + } + } } From 4a48b27ab8073f8e440211a7c2ca24c3df25812f Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 3 Jul 2026 21:25:16 +0200 Subject: [PATCH 3/7] fix: fetch youtube sabr media by player time --- .../youtube/sabr/YoutubeSabrSession.java | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java index bdecc73e..db2cc83a 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java @@ -8,6 +8,7 @@ import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Deque; @@ -186,6 +187,76 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, : ":seq=" + request.getSequenceNumber())); } + @Nonnull + public List fetchMediaAt(final long playerTimeMs, + final boolean videoActive, + final boolean audioActive, + @Nonnull final Localization localization) + throws IOException, ExtractionException { + final List requests = new ArrayList<>(); + if (videoActive) { + requests.add(mediaRequestAt(videoFormat, playerTimeMs)); + } + if (audioActive) { + requests.add(mediaRequestAt(audioFormat, playerTimeMs)); + } + if (requests.isEmpty()) { + return Collections.emptyList(); + } + streamState.setActiveTrackTypes(videoActive, audioActive); + final long requestPlayerTimeMs = Math.max(0, playerTimeMs); + final List segments = new ArrayList<>(); + for (final SabrSegmentRequest request : requests) { + final SabrMediaSegment cachedSegment = segmentCache.get(cacheKey(request)); + if (cachedSegment != null) { + segments.add(cachedSegment); + } + } + for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT + && !hasMediaForAllRequests(segments, requests, requestPlayerTimeMs); attempts++) { + final SabrSegmentRequest request = firstMissingMediaRequest(segments, requests, + requestPlayerTimeMs); + if (request == null) { + break; + } + failIfKnownOutOfBounds(request); + prepareForTargetedMediaSegment(request, playerTimeMs, videoActive, audioActive); + try { + segments.addAll(pumpOnce(localization)); + } finally { + clearTargetedMediaRequestState(videoActive, audioActive); + } + } + if (!hasMediaForAllRequests(segments, requests, requestPlayerTimeMs)) { + throw new SabrProtocolException("SABR media incomplete at playerTimeMs=" + + requestPlayerTimeMs + ", received=" + summarizeSegments(segments)); + } + return segments; + } + + @Nonnull + public SabrMediaSegment fetchMediaSegmentAt(@Nonnull final SabrSegmentRequest request, + final long playerTimeMs, + final boolean videoActive, + final boolean audioActive, + @Nonnull final Localization localization) + throws IOException, ExtractionException { + if (request.isInitializationSegment()) { + return fetchSegment(request, localization); + } + final SabrMediaSegment cachedSegment = segmentCache.get(cacheKey(request)); + if (cachedSegment != null) { + return cachedSegment; + } + failIfKnownOutOfBounds(request); + prepareForTargetedMediaSegment(request, playerTimeMs, videoActive, audioActive); + try { + return fetchSegment(request, localization); + } finally { + clearTargetedMediaRequestState(videoActive, audioActive); + } + } + @Nonnull public YoutubeSabrProbeResult fetchNextResponse(@Nonnull final Localization localization) throws IOException, ExtractionException { @@ -444,6 +515,138 @@ public int getRequestNumber() { return requestNumber; } + private void prepareForTargetedMediaSegment(@Nonnull final SabrSegmentRequest request, + final long playerTimeMs, + final boolean videoActive, + final boolean audioActive) { + if (request.isInitializationSegment()) { + return; + } + + final YoutubeSabrFormat targetFormat = request.getFormat(); + final YoutubeSabrFormat companionFormat = getCompanionFormat(targetFormat); + final long requestPlayerTimeMs = Math.max(0, playerTimeMs); + streamState.setWriteFirstRequestPlaybackState(true); + streamState.setWriteTopLevelPlayerTimeMs(false); + streamState.setWriteLastManualSelectedResolution(targetFormat.isVideo()); + streamState.setLastOnlyRange(targetFormat, false); + streamState.setLastOnlyRange(companionFormat, false); + final List bufferedRanges = new ArrayList<>(); + final SabrBufferedRange targetRange = streamState.getObservedBufferedRange(targetFormat); + if (targetRange != null) { + bufferedRanges.add(targetRange); + } + bufferedRanges.add(SabrBufferedRange.full(companionFormat)); + streamState.setBufferedRangesOverride(bufferedRanges); + streamState.setRequestTrackMode(targetFormat.isVideo() + ? YoutubeSabrStreamState.TRACK_MODE_VIDEO_ONLY + : YoutubeSabrStreamState.TRACK_MODE_AUDIO_ONLY, + audioActive, videoActive); + streamState.setPreferredTrackTypes(videoActive, audioActive); + streamState.setSelectVideoFormatBeforeAudio(targetFormat.isAudio()); + streamState.setFullyBuffered(targetFormat, false); + streamState.setFullyBuffered(companionFormat, true); + + final int companionSequence = streamState.getSegmentNumberAtOrAfterTimeMs( + companionFormat, requestPlayerTimeMs); + if (request.getSequenceNumber() <= streamState.getMaxSegment(targetFormat)) { + streamState.rewindBufferedTo(targetFormat, request.getSequenceNumber()); + streamState.rewindBufferedTo(companionFormat, companionSequence); + evictOutsideSeekWindow(requestPlayerTimeMs); + } else if (request.getSequenceNumber() > streamState.getMaxSegment(targetFormat) + 1) { + streamState.jumpBufferedTo(targetFormat, request.getSequenceNumber()); + streamState.jumpBufferedTo(companionFormat, companionSequence); + evictOutsideSeekWindow(requestPlayerTimeMs); + } else { + streamState.assumeBufferedUntil(targetFormat, request.getSequenceNumber() - 1); + streamState.assumeBufferedUntil(companionFormat, companionSequence); + evictOutsideSeekWindow(requestPlayerTimeMs); + } + streamState.setPlayerTimeMs(requestPlayerTimeMs); + } + + private void clearTargetedMediaRequestState(final boolean videoActive, + final boolean audioActive) { + streamState.setWriteFirstRequestPlaybackState(false); + streamState.setWriteTopLevelPlayerTimeMs(true); + streamState.setWriteLastManualSelectedResolution(false); + streamState.setBufferedRangesOverride(null); + streamState.clearPlayerTimeMsOverride(); + streamState.setFullyBuffered(audioFormat, false); + streamState.setFullyBuffered(videoFormat, false); + streamState.setSelectVideoFormatBeforeAudio(false); + streamState.setActiveTrackTypes(videoActive, audioActive); + } + + @Nonnull + private SabrSegmentRequest mediaRequestAt(@Nonnull final YoutubeSabrFormat format, + final long playerTimeMs) { + final int sequenceNumber = Math.max(1, + streamState.getSegmentNumberAtOrAfterTimeMs(format, Math.max(0, playerTimeMs))); + return SabrSegmentRequest.media(format, sequenceNumber); + } + + private static boolean hasMediaForRequest(@Nonnull final List segments, + @Nonnull final SabrSegmentRequest request, + final long playerTimeMs) { + for (final SabrMediaSegment segment : segments) { + final SabrMediaHeader header = segment.getHeader(); + if (request.matches(header) || coversPlayerTime(header, request, playerTimeMs)) { + return true; + } + } + return false; + } + + private static boolean coversPlayerTime(@Nonnull final SabrMediaHeader header, + @Nonnull final SabrSegmentRequest request, + final long playerTimeMs) { + return !header.isInitSegment() + && header.getItag() == request.getFormat().getItag() + && header.getStartMs() >= 0 + && header.getDurationMs() > 0 + && header.getStartMs() <= playerTimeMs + && playerTimeMs < header.getStartMs() + header.getDurationMs(); + } + + private static boolean hasMediaForAllRequests( + @Nonnull final List segments, + @Nonnull final List requests, + final long playerTimeMs) { + return firstMissingMediaRequest(segments, requests, playerTimeMs) == null; + } + + @Nullable + private static SabrSegmentRequest firstMissingMediaRequest( + @Nonnull final List segments, + @Nonnull final List requests, + final long playerTimeMs) { + for (final SabrSegmentRequest request : requests) { + if (!hasMediaForRequest(segments, request, playerTimeMs)) { + return request; + } + } + return null; + } + + @Nonnull + private static String summarizeSegments(@Nonnull final List segments) { + if (segments.isEmpty()) { + return "[]"; + } + final StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < segments.size(); i++) { + if (i > 0) { + builder.append(','); + } + final SabrMediaHeader header = segments.get(i).getHeader(); + builder.append(header.getItag()); + builder.append(header.isInitSegment() ? ":init" : ":seq=" + header.getSequenceNumber()); + } + builder.append(']'); + return builder.toString(); + } + public void prepareForMediaSegment(@Nonnull final SabrSegmentRequest request) { if (request.isInitializationSegment()) { return; From bd60c505c7eebb652cce633c1acc36e19c819a75 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 4 Jul 2026 13:28:03 +0200 Subject: [PATCH 4/7] fix(youtube): fetch sabr initialization segments --- .../youtube/sabr/YoutubeSabrSession.java | 151 +++++++++++------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java index 8179d558..44785b52 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java @@ -103,82 +103,94 @@ public YoutubeSabrSession(@Nonnull final YoutubeSabrInfo info, @Nonnull public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, - @Nonnull final Localization localization) + @Nonnull final Localization localization) throws IOException, ExtractionException { final SabrMediaSegment cachedSegment = segmentCache.get(cacheKey(request)); if (cachedSegment != null) { return cachedSegment; } + if (request.isInitializationSegment()) { + prepareForInitializationSegment(request); + } failIfKnownOutOfBounds(request); boolean targetPrepared = maybePrepareForDistantMediaSegment(request); - int policyOnlyResponses = 0; - for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { - final YoutubeSabrProbeResult result = fetchNextResponse(localization); - final SabrDecodedResponse decoded = result.getDecodedResponse(); - final List integrityIssues = decoded.getIntegrityIssues(); - if (!integrityIssues.isEmpty()) { - if (isRecoverableIncompleteMediaResponse(integrityIssues)) { - if (recoverFromIncompleteMediaResponse(localization, decoded)) { - continue; + try { + int policyOnlyResponses = 0; + for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { + final YoutubeSabrProbeResult result = fetchNextResponse(localization); + final SabrDecodedResponse decoded = result.getDecodedResponse(); + final List integrityIssues = decoded.getIntegrityIssues(); + if (!integrityIssues.isEmpty()) { + if (isRecoverableIncompleteMediaResponse(integrityIssues)) { + if (recoverFromIncompleteMediaResponse(localization, decoded)) { + continue; + } + throw new SabrProtocolException("SABR media integrity issue while fetching " + + describeRequest(request) + ": " + integrityIssues); } throw new SabrProtocolException("SABR media integrity issue while fetching " + describeRequest(request) + ": " + integrityIssues); } - throw new SabrProtocolException("SABR media integrity issue while fetching " - + describeRequest(request) + ": " + integrityIssues); - } - consecutiveIntegrityFailures = 0; - streamState.ingest(decoded); - final List segments = result.getSegments(); - for (final SabrMediaSegment segment : segments) { - streamState.ingest(segment); - segmentCache.put(cacheKey(segment), segment); - } - final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); - if (segment != null) { - return segment; - } - failIfKnownOutOfBounds(request); - if (!targetPrepared) { - targetPrepared = maybePrepareForDistantMediaSegment(request); - } - if (decoded.getSabrErrorDetails() != null) { - throw new SabrProtocolException("SABR error while fetching " - + describeRequest(request) + ": " + decoded.getSabrErrorDetails().summarize()); - } - if (decoded.isReloadRequested()) { - if (maybeReload(localization)) { - continue; + consecutiveIntegrityFailures = 0; + streamState.ingest(decoded); + final List segments = result.getSegments(); + for (final SabrMediaSegment segment : segments) { + streamState.ingest(segment); + segmentCache.put(cacheKey(segment), segment); } - throw new SabrProtocolException("SABR requested player reload while fetching " - + describeRequest(request) + " (reload budget spent): " - + decoded.summarizeNoMediaResponse()); - } - if (decoded.isProtectionBoundaryNoMediaResponse()) { - if (applyPoTokenForProtectedResponse()) { - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); + final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); + if (segment != null) { + return segment; + } + failIfKnownOutOfBounds(request); + if (!targetPrepared) { + targetPrepared = maybePrepareForDistantMediaSegment(request); + } + if (decoded.getSabrErrorDetails() != null) { + throw new SabrProtocolException("SABR error while fetching " + + describeRequest(request) + ": " + + decoded.getSabrErrorDetails().summarize()); + } + if (decoded.isReloadRequested()) { + if (maybeReload(localization)) { + continue; } - continue; + throw new SabrProtocolException("SABR requested player reload while fetching " + + describeRequest(request) + " (reload budget spent): " + + decoded.summarizeNoMediaResponse()); } - throw new SabrProtocolException("SABR protected no-media response while fetching " - + describeRequest(request) + ": " + decoded.summarizeNoMediaResponse()); - } - if (decoded.isPolicyOnlyResponse()) { - policyOnlyResponses++; - if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { - throw new SabrProtocolException("SABR repeated policy-only responses while fetching " + if (decoded.isProtectionBoundaryNoMediaResponse()) { + if (applyPoTokenForProtectedResponse()) { + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + continue; + } + throw new SabrProtocolException("SABR protected no-media response while fetching " + describeRequest(request) + ": " + decoded.summarizeNoMediaResponse()); } - } else if (!segments.isEmpty()) { - policyOnlyResponses = 0; - } - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); + if (decoded.isPolicyOnlyResponse()) { + policyOnlyResponses++; + if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { + throw new SabrProtocolException( + "SABR repeated policy-only responses while fetching " + + describeRequest(request) + ": " + + decoded.summarizeNoMediaResponse()); + } + } else if (!segments.isEmpty()) { + policyOnlyResponses = 0; + } + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + if (streamState.isComplete()) { + break; + } } - if (streamState.isComplete()) { - break; + } finally { + if (request.isInitializationSegment()) { + clearInitializationSegmentState(); } } throw new SabrProtocolException("Requested SABR segment was not returned: itag=" @@ -566,6 +578,29 @@ private void prepareForTargetedMediaSegment(@Nonnull final SabrSegmentRequest re streamState.setPlayerTimeMs(requestPlayerTimeMs); } + private void prepareForInitializationSegment(@Nonnull final SabrSegmentRequest request) { + final YoutubeSabrFormat targetFormat = request.getFormat(); + streamState.setWriteFirstRequestPlaybackState(true); + streamState.setWriteTopLevelPlayerTimeMs(false); + streamState.setWriteLastManualSelectedResolution(targetFormat.isVideo()); + streamState.setBufferedRangesOverride(Collections.emptyList()); + streamState.setRequestTrackMode(targetFormat.isVideo() + ? YoutubeSabrStreamState.TRACK_MODE_VIDEO_ONLY + : YoutubeSabrStreamState.TRACK_MODE_AUDIO_ONLY, + false, false); + streamState.setPreferredTrackTypes(true, true); + streamState.setPlayerTimeMs(streamState.getPlayerTimeMs()); + } + + private void clearInitializationSegmentState() { + streamState.setWriteFirstRequestPlaybackState(false); + streamState.setWriteTopLevelPlayerTimeMs(true); + streamState.setWriteLastManualSelectedResolution(false); + streamState.setBufferedRangesOverride(null); + streamState.clearPlayerTimeMsOverride(); + streamState.setActiveTrackTypes(true, true); + } + private void clearTargetedMediaRequestState(final boolean videoActive, final boolean audioActive) { streamState.setWriteFirstRequestPlaybackState(false); From 6f33162610dffd0f8fdc45f433efc003f6eee388 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 4 Jul 2026 16:16:11 +0200 Subject: [PATCH 5/7] fix(youtube): retry sabr init at stream start --- .../youtube/sabr/YoutubeSabrSession.java | 147 ++++++++++-------- 1 file changed, 79 insertions(+), 68 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java index 44785b52..30ad8e19 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java @@ -109,83 +109,93 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, if (cachedSegment != null) { return cachedSegment; } - if (request.isInitializationSegment()) { - prepareForInitializationSegment(request); - } - failIfKnownOutOfBounds(request); - - boolean targetPrepared = maybePrepareForDistantMediaSegment(request); + final boolean initializationSegment = request.isInitializationSegment(); + final long initialPlayerTimeMs = initializationSegment ? streamState.getPlayerTimeMs() : -1; + final int initRounds = initializationSegment && initialPlayerTimeMs > 0 ? 2 : 1; try { - int policyOnlyResponses = 0; - for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { - final YoutubeSabrProbeResult result = fetchNextResponse(localization); - final SabrDecodedResponse decoded = result.getDecodedResponse(); - final List integrityIssues = decoded.getIntegrityIssues(); - if (!integrityIssues.isEmpty()) { - if (isRecoverableIncompleteMediaResponse(integrityIssues)) { - if (recoverFromIncompleteMediaResponse(localization, decoded)) { - continue; + for (int initRound = 0; initRound < initRounds; initRound++) { + if (initializationSegment) { + prepareForInitializationSegment(request, + initRound == 0 ? initialPlayerTimeMs : 0); + } + failIfKnownOutOfBounds(request); + + boolean targetPrepared = maybePrepareForDistantMediaSegment(request); + int policyOnlyResponses = 0; + for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { + final YoutubeSabrProbeResult result = fetchNextResponse(localization); + final SabrDecodedResponse decoded = result.getDecodedResponse(); + final List integrityIssues = decoded.getIntegrityIssues(); + if (!integrityIssues.isEmpty()) { + if (isRecoverableIncompleteMediaResponse(integrityIssues)) { + if (recoverFromIncompleteMediaResponse(localization, decoded)) { + continue; + } + throw new SabrProtocolException( + "SABR media integrity issue while fetching " + + describeRequest(request) + ": " + integrityIssues); } - throw new SabrProtocolException("SABR media integrity issue while fetching " + throw new SabrProtocolException( + "SABR media integrity issue while fetching " + describeRequest(request) + ": " + integrityIssues); } - throw new SabrProtocolException("SABR media integrity issue while fetching " - + describeRequest(request) + ": " + integrityIssues); - } - consecutiveIntegrityFailures = 0; - streamState.ingest(decoded); - final List segments = result.getSegments(); - for (final SabrMediaSegment segment : segments) { - streamState.ingest(segment); - segmentCache.put(cacheKey(segment), segment); - } - final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); - if (segment != null) { - return segment; - } - failIfKnownOutOfBounds(request); - if (!targetPrepared) { - targetPrepared = maybePrepareForDistantMediaSegment(request); - } - if (decoded.getSabrErrorDetails() != null) { - throw new SabrProtocolException("SABR error while fetching " - + describeRequest(request) + ": " - + decoded.getSabrErrorDetails().summarize()); - } - if (decoded.isReloadRequested()) { - if (maybeReload(localization)) { - continue; + consecutiveIntegrityFailures = 0; + streamState.ingest(decoded); + final List segments = result.getSegments(); + for (final SabrMediaSegment segment : segments) { + streamState.ingest(segment); + segmentCache.put(cacheKey(segment), segment); } - throw new SabrProtocolException("SABR requested player reload while fetching " - + describeRequest(request) + " (reload budget spent): " - + decoded.summarizeNoMediaResponse()); - } - if (decoded.isProtectionBoundaryNoMediaResponse()) { - if (applyPoTokenForProtectedResponse()) { - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); + final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); + if (segment != null) { + return segment; + } + failIfKnownOutOfBounds(request); + if (!targetPrepared) { + targetPrepared = maybePrepareForDistantMediaSegment(request); + } + if (decoded.getSabrErrorDetails() != null) { + throw new SabrProtocolException("SABR error while fetching " + + describeRequest(request) + ": " + + decoded.getSabrErrorDetails().summarize()); + } + if (decoded.isReloadRequested()) { + if (maybeReload(localization)) { + continue; } - continue; + throw new SabrProtocolException("SABR requested player reload while fetching " + + describeRequest(request) + " (reload budget spent): " + + decoded.summarizeNoMediaResponse()); } - throw new SabrProtocolException("SABR protected no-media response while fetching " - + describeRequest(request) + ": " + decoded.summarizeNoMediaResponse()); - } - if (decoded.isPolicyOnlyResponse()) { - policyOnlyResponses++; - if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { + if (decoded.isProtectionBoundaryNoMediaResponse()) { + if (applyPoTokenForProtectedResponse()) { + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + continue; + } throw new SabrProtocolException( - "SABR repeated policy-only responses while fetching " + "SABR protected no-media response while fetching " + describeRequest(request) + ": " + decoded.summarizeNoMediaResponse()); } - } else if (!segments.isEmpty()) { - policyOnlyResponses = 0; - } - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); - } - if (streamState.isComplete()) { - break; + if (decoded.isPolicyOnlyResponse()) { + policyOnlyResponses++; + if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { + throw new SabrProtocolException( + "SABR repeated policy-only responses while fetching " + + describeRequest(request) + ": " + + decoded.summarizeNoMediaResponse()); + } + } else if (!segments.isEmpty()) { + policyOnlyResponses = 0; + } + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + if (streamState.isComplete()) { + break; + } } } } finally { @@ -578,7 +588,8 @@ private void prepareForTargetedMediaSegment(@Nonnull final SabrSegmentRequest re streamState.setPlayerTimeMs(requestPlayerTimeMs); } - private void prepareForInitializationSegment(@Nonnull final SabrSegmentRequest request) { + private void prepareForInitializationSegment(@Nonnull final SabrSegmentRequest request, + final long playerTimeMs) { final YoutubeSabrFormat targetFormat = request.getFormat(); streamState.setWriteFirstRequestPlaybackState(true); streamState.setWriteTopLevelPlayerTimeMs(false); @@ -589,7 +600,7 @@ private void prepareForInitializationSegment(@Nonnull final SabrSegmentRequest r : YoutubeSabrStreamState.TRACK_MODE_AUDIO_ONLY, false, false); streamState.setPreferredTrackTypes(true, true); - streamState.setPlayerTimeMs(streamState.getPlayerTimeMs()); + streamState.setPlayerTimeMs(playerTimeMs); } private void clearInitializationSegmentState() { From 63e9dffbdd90b1f673406f19cbbeada35ee74a49 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 4 Jul 2026 17:32:36 +0200 Subject: [PATCH 6/7] fix(youtube): retry sabr init after errors --- .../youtube/sabr/YoutubeSabrSession.java | 147 ++++++++++-------- 1 file changed, 79 insertions(+), 68 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java index 30ad8e19..c3ab9313 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java @@ -112,90 +112,98 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, final boolean initializationSegment = request.isInitializationSegment(); final long initialPlayerTimeMs = initializationSegment ? streamState.getPlayerTimeMs() : -1; final int initRounds = initializationSegment && initialPlayerTimeMs > 0 ? 2 : 1; + SabrProtocolException initRoundError = null; try { for (int initRound = 0; initRound < initRounds; initRound++) { - if (initializationSegment) { - prepareForInitializationSegment(request, - initRound == 0 ? initialPlayerTimeMs : 0); - } - failIfKnownOutOfBounds(request); - - boolean targetPrepared = maybePrepareForDistantMediaSegment(request); - int policyOnlyResponses = 0; - for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { - final YoutubeSabrProbeResult result = fetchNextResponse(localization); - final SabrDecodedResponse decoded = result.getDecodedResponse(); - final List integrityIssues = decoded.getIntegrityIssues(); - if (!integrityIssues.isEmpty()) { - if (isRecoverableIncompleteMediaResponse(integrityIssues)) { - if (recoverFromIncompleteMediaResponse(localization, decoded)) { - continue; + try { + if (initializationSegment) { + prepareForInitializationSegment(request, + initRound == 0 ? initialPlayerTimeMs : 0); + } + failIfKnownOutOfBounds(request); + + boolean targetPrepared = maybePrepareForDistantMediaSegment(request); + int policyOnlyResponses = 0; + for (int attempts = 0; attempts < MAX_REQUESTS_PER_SEGMENT; attempts++) { + final YoutubeSabrProbeResult result = fetchNextResponse(localization); + final SabrDecodedResponse decoded = result.getDecodedResponse(); + final List integrityIssues = decoded.getIntegrityIssues(); + if (!integrityIssues.isEmpty()) { + if (isRecoverableIncompleteMediaResponse(integrityIssues)) { + if (recoverFromIncompleteMediaResponse(localization, decoded)) { + continue; + } + throw new SabrProtocolException( + "SABR media integrity issue while fetching " + + describeRequest(request) + ": " + integrityIssues); } throw new SabrProtocolException( "SABR media integrity issue while fetching " + describeRequest(request) + ": " + integrityIssues); } - throw new SabrProtocolException( - "SABR media integrity issue while fetching " - + describeRequest(request) + ": " + integrityIssues); - } - consecutiveIntegrityFailures = 0; - streamState.ingest(decoded); - final List segments = result.getSegments(); - for (final SabrMediaSegment segment : segments) { - streamState.ingest(segment); - segmentCache.put(cacheKey(segment), segment); - } - final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); - if (segment != null) { - return segment; - } - failIfKnownOutOfBounds(request); - if (!targetPrepared) { - targetPrepared = maybePrepareForDistantMediaSegment(request); - } - if (decoded.getSabrErrorDetails() != null) { - throw new SabrProtocolException("SABR error while fetching " - + describeRequest(request) + ": " - + decoded.getSabrErrorDetails().summarize()); - } - if (decoded.isReloadRequested()) { - if (maybeReload(localization)) { - continue; + consecutiveIntegrityFailures = 0; + streamState.ingest(decoded); + final List segments = result.getSegments(); + for (final SabrMediaSegment segment : segments) { + streamState.ingest(segment); + segmentCache.put(cacheKey(segment), segment); } - throw new SabrProtocolException("SABR requested player reload while fetching " - + describeRequest(request) + " (reload budget spent): " - + decoded.summarizeNoMediaResponse()); - } - if (decoded.isProtectionBoundaryNoMediaResponse()) { - if (applyPoTokenForProtectedResponse()) { - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); + final SabrMediaSegment segment = segmentCache.get(cacheKey(request)); + if (segment != null) { + return segment; + } + failIfKnownOutOfBounds(request); + if (!targetPrepared) { + targetPrepared = maybePrepareForDistantMediaSegment(request); + } + if (decoded.getSabrErrorDetails() != null) { + throw new SabrProtocolException("SABR error while fetching " + + describeRequest(request) + ": " + + decoded.getSabrErrorDetails().summarize()); + } + if (decoded.isReloadRequested()) { + if (maybeReload(localization)) { + continue; } - continue; + throw new SabrProtocolException("SABR requested player reload while fetching " + + describeRequest(request) + " (reload budget spent): " + + decoded.summarizeNoMediaResponse()); } - throw new SabrProtocolException( - "SABR protected no-media response while fetching " - + describeRequest(request) + ": " - + decoded.summarizeNoMediaResponse()); - } - if (decoded.isPolicyOnlyResponse()) { - policyOnlyResponses++; - if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { + if (decoded.isProtectionBoundaryNoMediaResponse()) { + if (applyPoTokenForProtectedResponse()) { + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + continue; + } throw new SabrProtocolException( - "SABR repeated policy-only responses while fetching " + "SABR protected no-media response while fetching " + describeRequest(request) + ": " + decoded.summarizeNoMediaResponse()); } - } else if (!segments.isEmpty()) { - policyOnlyResponses = 0; - } - if (decoded.getBackoffTimeMs() > 0) { - sleepBackoff(decoded.getBackoffTimeMs()); + if (decoded.isPolicyOnlyResponse()) { + policyOnlyResponses++; + if (policyOnlyResponses >= MAX_POLICY_ONLY_RESPONSES_PER_SEGMENT) { + throw new SabrProtocolException( + "SABR repeated policy-only responses while fetching " + + describeRequest(request) + ": " + + decoded.summarizeNoMediaResponse()); + } + } else if (!segments.isEmpty()) { + policyOnlyResponses = 0; + } + if (decoded.getBackoffTimeMs() > 0) { + sleepBackoff(decoded.getBackoffTimeMs()); + } + if (streamState.isComplete()) { + break; + } } - if (streamState.isComplete()) { - break; + } catch (final SabrProtocolException e) { + if (!initializationSegment || initRound + 1 >= initRounds) { + throw e; } + initRoundError = e; } } } finally { @@ -203,6 +211,9 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, clearInitializationSegmentState(); } } + if (initRoundError != null) { + throw initRoundError; + } throw new SabrProtocolException("Requested SABR segment was not returned: itag=" + request.getFormat().getItag() + (request.isInitializationSegment() From 08449d95169352e439050d87fa459f60d6f1770c Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 4 Jul 2026 18:28:53 +0200 Subject: [PATCH 7/7] fix(youtube): fetch sabr init before seek state --- .../youtube/sabr/YoutubeSabrSession.java | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java index c3ab9313..768303e9 100644 --- a/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java +++ b/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/sabr/YoutubeSabrSession.java @@ -46,6 +46,7 @@ public final class YoutubeSabrSession { private final YoutubeSabrStreamState streamState; @Nullable private final SabrPoTokenProvider poTokenProvider; + private final boolean allowColdStartInitializationFetch; private final Map segmentCache = new ConcurrentHashMap<>(); private String serverAbrStreamingUrl; private int requestNumber; @@ -79,6 +80,14 @@ public YoutubeSabrSession(@Nonnull final YoutubeSabrInfo info, @Nonnull final YoutubeSabrFormat audioFormat, @Nonnull final YoutubeSabrFormat videoFormat, @Nullable final SabrPoTokenProvider poTokenProvider) { + this(info, audioFormat, videoFormat, poTokenProvider, true); + } + + private YoutubeSabrSession(@Nonnull final YoutubeSabrInfo info, + @Nonnull final YoutubeSabrFormat audioFormat, + @Nonnull final YoutubeSabrFormat videoFormat, + @Nullable final SabrPoTokenProvider poTokenProvider, + final boolean allowColdStartInitializationFetch) { if (!audioFormat.isAudio()) { throw new IllegalArgumentException("SABR audio format must be audio: itag=" + audioFormat.getItag()); @@ -98,12 +107,13 @@ public YoutubeSabrSession(@Nonnull final YoutubeSabrInfo info, this.videoFormat = videoFormat; this.streamState = new YoutubeSabrStreamState(audioFormat, videoFormat); this.poTokenProvider = poTokenProvider; + this.allowColdStartInitializationFetch = allowColdStartInitializationFetch; this.serverAbrStreamingUrl = info.getServerAbrStreamingUrl(); } @Nonnull - public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, - @Nonnull final Localization localization) + public synchronized SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, + @Nonnull final Localization localization) throws IOException, ExtractionException { final SabrMediaSegment cachedSegment = segmentCache.get(cacheKey(request)); if (cachedSegment != null) { @@ -111,6 +121,13 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, } final boolean initializationSegment = request.isInitializationSegment(); final long initialPlayerTimeMs = initializationSegment ? streamState.getPlayerTimeMs() : -1; + if (initializationSegment && initialPlayerTimeMs > 0 && allowColdStartInitializationFetch) { + final SabrMediaSegment coldStartSegment = tryFetchInitializationSegmentFromColdStart( + request, localization); + if (coldStartSegment != null) { + return coldStartSegment; + } + } final int initRounds = initializationSegment && initialPlayerTimeMs > 0 ? 2 : 1; SabrProtocolException initRoundError = null; try { @@ -222,10 +239,10 @@ public SabrMediaSegment fetchSegment(@Nonnull final SabrSegmentRequest request, } @Nonnull - public List fetchMediaAt(final long playerTimeMs, - final boolean videoActive, - final boolean audioActive, - @Nonnull final Localization localization) + public synchronized List fetchMediaAt(final long playerTimeMs, + final boolean videoActive, + final boolean audioActive, + @Nonnull final Localization localization) throws IOException, ExtractionException { final List requests = new ArrayList<>(); if (videoActive) { @@ -292,7 +309,7 @@ public SabrMediaSegment fetchMediaSegmentAt(@Nonnull final SabrSegmentRequest re } @Nonnull - public YoutubeSabrProbeResult fetchNextResponse(@Nonnull final Localization localization) + public synchronized YoutubeSabrProbeResult fetchNextResponse(@Nonnull final Localization localization) throws IOException, ExtractionException { final YoutubeSabrProbeResult result; if (requestNumber == 0) { @@ -353,7 +370,7 @@ private boolean maybeReload(@Nonnull final Localization localization) * server feed the track that is behind the play head, so neither track starves the other. */ @Nonnull - public List pumpOnce(@Nonnull final Localization localization) + public synchronized List pumpOnce(@Nonnull final Localization localization) throws IOException, ExtractionException { final YoutubeSabrProbeResult result = fetchNextResponse(localization); final SabrDecodedResponse decoded = result.getDecodedResponse(); @@ -623,6 +640,34 @@ private void clearInitializationSegmentState() { streamState.setActiveTrackTypes(true, true); } + @Nullable + private SabrMediaSegment tryFetchInitializationSegmentFromColdStart( + @Nonnull final SabrSegmentRequest request, + @Nonnull final Localization localization) throws IOException, ExtractionException { + try { + final YoutubeSabrSession initSession = new YoutubeSabrSession( + info, audioFormat, videoFormat, poTokenProvider, false); + initSession.streamState.setPoToken(streamState.getPoToken()); + final SabrMediaSegment segment = initSession.fetchSegment(request, localization); + ingestColdStartInitializationSegment(initSession, audioFormat); + ingestColdStartInitializationSegment(initSession, videoFormat); + final SabrMediaSegment cachedSegment = segmentCache.get(cacheKey(request)); + return cachedSegment == null ? segment : cachedSegment; + } catch (final IOException | ExtractionException ignored) { + return null; + } + } + + private void ingestColdStartInitializationSegment(@Nonnull final YoutubeSabrSession initSession, + @Nonnull final YoutubeSabrFormat format) { + final SabrMediaSegment segment = initSession.getCachedSegment( + SabrSegmentRequest.initialization(format)); + if (segment != null) { + segmentCache.put(cacheKey(segment), segment); + streamState.ingest(segment); + } + } + private void clearTargetedMediaRequestState(final boolean videoActive, final boolean audioActive) { streamState.setWriteFirstRequestPlaybackState(false);