Skip to content

El modelo buffered y el seek

Parte de SABR en el extractor. Es la pieza más sutil del extractor, y la que más a menudo se hace mal. Vive en YoutubeSabrStreamState, un FormatProgress por track.

Por qué dos valores "max"

El servidor solo envía lo que el cliente dice que le falta, así que los buffered ranges de cada petición deben ser honestos. La trampa son los huecos. Supongamos que los segmentos 1, 2, 3, 5, 6 han llegado pero el 4 se perdió:

Modelo de cabeza de buffer

Si el cliente reportara maxSegment = 6, el servidor asumiría que tiene 1–6 y enviaría el segmento 7, y el segmento 4 nunca se rellenaría, el reader secuencial se queda atascado en el hueco para siempre.

Así que FormatProgress sigue dos cabezas:

  • contiguousMaxSegment — el segmento más alto sin hueco desde el inicio. Es lo que se reporta al servidor, para que siempre envíe el segmento secuencial exacto que un reader necesita.
  • maxSegment — el segmento más alto visto en absoluto (puede estar más allá de un hueco).
  • aheadOfContiguous — un set de segmentos fuera de orden recibidos más allá del borde contiguo, esperando ser integrados.

observeHeader(seq) los mantiene: si seq == contiguousMaxSegment + 1, avanza el borde contiguo y drena aheadOfContiguous tan lejos como alcance; si seq está más allá, lo guarda en aheadOfContiguous. Este split contiguo-vs-max es el mecanismo anti-starvation central.

La ventana observed-timing

Junto a los números de segmento, FormatProgress registra una ventana temporal a partir de los headers realmente vistos: firstObservedSegment, lastObservedSegment, observedStartMs, observedEndMs, observedMaxSegment, lastObservedDurationMs. Más endSegment y averageDurationMs (de la metadata de init) y el segmentIndex parseado.

Construir los ranges

getBufferedRanges() emite un SabrBufferedRange por track (o SabrBufferedRange.full(...) si un track está marcado como totalmente bufferizado, o un override manual). La decisión interesante en addBufferedRange es si confiar en el timing observado:

canUseObservedTiming =
       observedStartMs >= 0
    && observedEndMs > observedStartMs
    && observedMaxSegment >= maxSegment
    && firstObservedSegment > 0
    && contiguousMaxSegment >= maxSegment   // el guardia no-hole

Esa última cláusula es la clave: el timing observado solo se cree cuando no hay hueco (contiguo alcanzó a max). Si no, el final observado sobreestimaría la cobertura más allá del hueco. Cuando se confía, el range usa observedStartMs / observedEndMs - observedStartMs y firstObservedSegment; si no, cae a startTime = 0, duration = getBufferedEndMs(), startIndex = 1. En ambos casos endSegmentIndex = contiguousMaxSegment, nunca maxSegment. Y getBufferedEndMs() se calcula desde contiguousMaxSegment también (un hueco significa que realmente no estamos bufferizados más allá).

El seek

Cabeza de buffer y seek

Los seeks hacia adelante al alcance son fáciles porque el modelo tiene un sesgo hacia adelante. assumeBufferedUntil(format, seq) solo eleva maxSegment; los prepareForMediaSegment / maybePrepareForDistantMediaSegment de la sesión lo usan para saltar el bookkeeping adelante y dejar que SabrSeek / el player time alineen.

Pero un seek hacia adelante lejano (un seek en frío mucho más allá de la cabeza de buffer: un skip de SponsorBlock, una reanudación desde el historial) no es gratis. prepareForMediaSegment solo eleva maxSegment; deja contiguousMaxSegment (el segmento donde el range reportado realmente termina) atrás en la cabeza vieja. Así la petición sigue anunciando el span viejo como bufferizado, el servidor sigue llenándolo, el pump hace ping-pong entre la cabeza vieja y el objetivo, y el reader puede esperar para siempre el segmento lejano. prepareForForwardJumpjumpBufferedTo(fromSegment) es la contraparte simétrica del rewind de abajo: mueve contiguousMaxSegment sobre el objetivo (plegando los segmentos de la zona objetivo que ya llegaron desordenados, descartando la ventana observada), para que el servidor transmita desde ahí y el ritmo guiado por la cabeza siga. Un seek hacia atrás posterior dentro del span saltado pasa honestamente por prepareForRewind.

Los seeks hacia atrás son el caso difícil. Tras reproducir adelante, la cabeza de buffer está alta; un seek atrás sobre un segmento ya recibido dejaría a la petición anunciando ese range como bufferizado, el servidor no envía nada, el reader se atasca. prepareForRewindrewindBufferedTo(fromSegment) repara el estado con precisión:

  1. last = max(0, fromSegment - 1).
  2. Guardia: si last >= contiguousMaxSegment, no es un rewind para este track, return.
  3. Si no shrink: maxSegment = last, contiguousMaxSegment = last, observedMaxSegment = min(observedMaxSegment, last).
  4. Drop de la ventana observada: firstObservedSegment, lastObservedSegment, observedStartMs, observedEndMs todos de vuelta a -1.

El paso 4 importa tanto como el 3: si la ventana observada sobreviviera, canUseObservedTiming podría aún reportar un final más allá del objetivo y la re-petición volvería vacía. Con ambas cabezas y la ventana observada retrocedidas, la siguiente petición pide honestamente el objetivo y el servidor lo reenvía. La sesión hace esto para el track seekeado y su compañero (audio/vídeo se mueven juntos), luego pone el player time.


Siguiente: El driver de sesión.

Creado por Priveetee para el proyecto de código abierto PipePipe