Files
googlevideo/src/core/SabrUmpProcessor.ts
2025-07-22 15:24:30 -03:00

316 lines
9.3 KiB
TypeScript

import { concatenateChunks, type CacheManager } from '../utils/index.js';
import { createSegmentCacheKey, fromFormat, fromMediaHeader } from '../utils/formatKeyUtils.js';
import { CompositeBuffer } from './CompositeBuffer.js';
import { UmpReader } from './UmpReader.js';
import {
FormatInitializationMetadata,
MediaHeader,
NextRequestPolicy,
ReloadPlaybackContext,
SabrContextSendingPolicy,
SabrContextUpdate,
SabrError,
SabrRedirect,
SnackbarMessage,
StreamProtectionStatus,
UMPPartId
} from '../utils/Protos.js';
import type { Part } from '../types/shared.js';
import type { SabrRequestMetadata } from '../types/sabrStreamingAdapterTypes.js';
interface Segment {
headerId?: number;
mediaHeader: MediaHeader;
complete?: boolean;
bufferedChunks: Uint8Array[];
lastChunkSize: number;
}
export interface UmpProcessingResult {
data?: Uint8Array;
done: boolean;
}
type UmpPartHandler = (part: Part) => UmpProcessingResult | undefined;
/**
* This class is responsible for reading a UMP stream, handling different part types
* (like media headers, media data, and server directives), and populating a
* metadata object with the extracted information. It is supposed to be used
* in conjunction with a {@linkcode SabrPlayerAdapter} in video player
* implementations.
*/
export class SabrUmpProcessor {
public partialPart?: Part;
private readonly formatInitMetadata: FormatInitializationMetadata[] = [];
private desiredHeaderId?: number;
private partialSegments = new Map<number, Segment>();
private readonly umpPartHandlers = new Map<UMPPartId, UmpPartHandler>([
[ UMPPartId.FORMAT_INITIALIZATION_METADATA, this.handleFormatInitMetadata.bind(this) ],
[ UMPPartId.NEXT_REQUEST_POLICY, this.handleNextRequestPolicy.bind(this) ],
[ UMPPartId.SABR_ERROR, this.handleSabrError.bind(this) ],
[ UMPPartId.SABR_REDIRECT, this.handleSabrRedirect.bind(this) ],
[ UMPPartId.SABR_CONTEXT_UPDATE, this.handleSabrContextUpdate.bind(this) ],
[ UMPPartId.SABR_CONTEXT_SENDING_POLICY, this.handleSabrContextSendingPolicy.bind(this) ],
[ UMPPartId.SNACKBAR_MESSAGE, this.handleSnackbarMessage.bind(this) ],
[ UMPPartId.STREAM_PROTECTION_STATUS, this.handleStreamProtectionStatus.bind(this) ],
[ UMPPartId.RELOAD_PLAYER_RESPONSE, this.handleReloadPlayerResponse.bind(this) ],
[ UMPPartId.MEDIA_HEADER, this.handleMediaHeader.bind(this) ],
[ UMPPartId.MEDIA, this.handleMedia.bind(this) ],
[ UMPPartId.MEDIA_END, this.handleMediaEnd.bind(this) ]
]);
constructor(
private requestMetadata: SabrRequestMetadata,
private cacheManager?: CacheManager
) { }
/**
* Processes a chunk of data from a UMP stream and updates the request context.
* @returns A promise that resolves with a processing result if a terminal part is found (e.g., MediaEnd), or undefined otherwise.
* @param value
*/
public processChunk(value: Uint8Array): Promise<UmpProcessingResult | undefined> {
return new Promise((resolve) => {
let chunk;
if (this.partialPart) {
chunk = this.partialPart.data;
chunk.append(value);
} else {
chunk = new CompositeBuffer([ value ]);
}
const ump = new UmpReader(chunk);
this.partialPart = ump.read((part: Part) => {
const handler = this.umpPartHandlers.get(part.type);
const result = handler?.(part);
if (result) {
this.partialPart = undefined;
this.desiredHeaderId = undefined;
this.partialSegments.clear();
resolve(result);
}
});
resolve(undefined);
});
}
public getSegmentInfo(): Segment | undefined {
return this.partialSegments.get(this.desiredHeaderId || 0);
}
private decodePart<T>(part: Part, decoder: { decode: (data: Uint8Array) => T }): T | undefined {
if (!part.data.chunks.length)
return undefined;
try {
return decoder.decode(concatenateChunks(part.data.chunks));
} catch {
return undefined;
}
}
private handleFormatInitMetadata(part: Part) {
const formatInitMetadata = this.decodePart(part, FormatInitializationMetadata);
if (formatInitMetadata) {
this.formatInitMetadata.push(formatInitMetadata);
}
return undefined;
}
private handleNextRequestPolicy(part: Part) {
const nextRequestPolicy = this.decodePart(part, NextRequestPolicy);
if (nextRequestPolicy) {
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
nextRequestPolicy
};
}
return undefined;
}
private handleMediaHeader(part: Part) {
const mediaHeader = this.decodePart(part, MediaHeader);
if (!mediaHeader) {
return undefined;
}
const targetFormatKey = fromFormat(this.requestMetadata.format);
const segmentFormatKey = fromMediaHeader(mediaHeader);
if (!this.requestMetadata.isSABR || segmentFormatKey === targetFormatKey) {
const segmentObj = {
headerId: mediaHeader.headerId,
mediaHeader: mediaHeader,
bufferedChunks: [],
lastChunkSize: 0
};
if (this.desiredHeaderId === undefined) {
this.desiredHeaderId = mediaHeader.headerId;
}
this.partialSegments.set(<number>mediaHeader.headerId, segmentObj);
}
return undefined;
}
private handleMedia(part: Part) {
const headerId = part.data.getUint8(0);
const buffer = part.data.split(1).remainingBuffer;
const segment = this.partialSegments.get(headerId);
if (segment) {
segment.lastChunkSize = buffer.getLength();
for (const chunk of buffer.chunks) {
segment.bufferedChunks.push(chunk);
}
}
return undefined;
}
private handleMediaEnd(part: Part): UmpProcessingResult | undefined {
const headerId = part.data.getUint8(0);
const segment = this.partialSegments.get(headerId);
if (segment && segment.headerId === this.desiredHeaderId) {
const segmentData = concatenateChunks(segment.bufferedChunks);
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
formatInitMetadata: this.formatInitMetadata,
mediaHeader: segment.mediaHeader
};
/**
* Cache initialization segments to optimize performance. SABR responses contain larger payloads,
* and caching the init segment reduces latency when switching between different quality levels
* or initializing new streams.
*/
if (this.cacheManager && this.requestMetadata.isInit && this.requestMetadata.byteRange && this.requestMetadata.format) {
this.cacheManager.setInitSegment(
createSegmentCacheKey(segment.mediaHeader, this.requestMetadata.format),
segmentData
);
return {
data: segmentData.slice(this.requestMetadata.byteRange.start, this.requestMetadata.byteRange.end + 1),
done: true
};
}
return {
data: segmentData,
done: true
};
}
}
private handleSnackbarMessage(part: Part) {
const snackbarMessage = this.decodePart(part, SnackbarMessage);
if (snackbarMessage) {
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
snackbarMessage
};
}
return undefined;
}
private handleSabrError(part: Part): UmpProcessingResult {
const sabrError = this.decodePart(part, SabrError);
this.requestMetadata.error = { sabrError };
return { done: true };
}
private handleStreamProtectionStatus(part: Part): UmpProcessingResult | undefined {
const streamProtectionStatus = this.decodePart(part, StreamProtectionStatus);
if (!streamProtectionStatus) {
return undefined;
}
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
streamProtectionStatus
};
if (streamProtectionStatus.status === 3) {
return {
done: true
};
}
return undefined;
}
private handleReloadPlayerResponse(part: Part): UmpProcessingResult | undefined {
const reloadPlaybackContext = this.decodePart(part, ReloadPlaybackContext);
if (!reloadPlaybackContext) {
return undefined;
}
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
reloadPlaybackContext
};
return {
done: true
};
}
private handleSabrRedirect(part: Part): UmpProcessingResult | undefined {
const redirect = this.decodePart(part, SabrRedirect);
if (!redirect) {
return undefined;
}
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
redirect
};
// With just UMP, redirects should be followed immediately.
if (this.requestMetadata.isUMP && !this.requestMetadata.isSABR) {
return { done: true };
}
return undefined;
}
private handleSabrContextUpdate(part: Part) {
const sabrContextUpdate = this.decodePart(part, SabrContextUpdate);
if (sabrContextUpdate) {
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
sabrContextUpdate
};
}
return undefined;
}
private handleSabrContextSendingPolicy(part: Part): UmpProcessingResult | undefined {
const sabrContextSendingPolicy = this.decodePart(part, SabrContextSendingPolicy);
if (sabrContextSendingPolicy) {
this.requestMetadata.streamInfo = {
...this.requestMetadata.streamInfo,
sabrContextSendingPolicy
};
}
return undefined;
}
}