feat: init repo

This commit is contained in:
Luan
2024-09-13 13:52:28 -03:00
commit a508925216
65 changed files with 14279 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
export class ChunkedDataBuffer {
public chunks: Uint8Array[];
public currentChunkOffset: number;
public currentChunkIndex: number;
public currentDataView?: DataView;
public totalLength: number;
constructor(chunks: Uint8Array[] = []) {
this.chunks = [];
this.currentChunkOffset = this.currentChunkIndex = 0;
this.currentDataView = undefined;
this.totalLength = 0;
chunks.forEach((chunk) => {
this.append(chunk);
});
}
getLength(): number {
return this.totalLength;
}
append(chunk: Uint8Array): void {
if (this.canMergeWithLastChunk(chunk)) {
const lastChunk = this.chunks[this.chunks.length - 1];
this.chunks[this.chunks.length - 1] = new Uint8Array(
lastChunk.buffer,
lastChunk.byteOffset,
lastChunk.length + chunk.length
);
this.resetFocus();
} else {
this.chunks.push(chunk);
}
this.totalLength += chunk.length;
}
split(position: number): { extractedBuffer: ChunkedDataBuffer; remainingBuffer: ChunkedDataBuffer } {
const extractedBuffer = new ChunkedDataBuffer();
const remainingBuffer = new ChunkedDataBuffer();
const iterator = this.chunks[Symbol.iterator]();
let item = iterator.next();
while (!item.done) {
const chunk = item.value;
if (position >= chunk.length) {
extractedBuffer.append(chunk);
position -= chunk.length;
} else if (position > 0) {
extractedBuffer.append(new Uint8Array(chunk.buffer, chunk.byteOffset, position));
remainingBuffer.append(
new Uint8Array(chunk.buffer, chunk.byteOffset + position, chunk.length - position)
);
position = 0;
} else {
remainingBuffer.append(chunk);
}
item = iterator.next();
}
return { extractedBuffer, remainingBuffer };
}
isFocused(position: number): boolean {
return position >= this.currentChunkOffset && position < this.currentChunkOffset + this.chunks[this.currentChunkIndex].length;
}
focus(position: number): void {
if (!this.isFocused(position)) {
if (position < this.currentChunkOffset) this.resetFocus();
while (
this.currentChunkOffset + this.chunks[this.currentChunkIndex].length <= position &&
this.currentChunkIndex < this.chunks.length - 1
) {
this.currentChunkOffset += this.chunks[this.currentChunkIndex].length;
this.currentChunkIndex += 1;
}
this.currentDataView = undefined;
}
}
canReadBytes(position: number, length: number): boolean {
return position + length <= this.totalLength;
}
getUint8(position: number): number {
this.focus(position);
return this.chunks[this.currentChunkIndex][position - this.currentChunkOffset];
}
private canMergeWithLastChunk(chunk: Uint8Array): boolean {
if (this.chunks.length === 0) return false;
const lastChunk = this.chunks[this.chunks.length - 1];
return (
lastChunk.buffer === chunk.buffer &&
lastChunk.byteOffset + lastChunk.length === chunk.byteOffset
);
}
private resetFocus(): void {
this.currentDataView = undefined;
this.currentChunkIndex = 0;
this.currentChunkOffset = 0;
}
}

300
src/core/ServerAbrStream.ts Normal file
View File

@@ -0,0 +1,300 @@
/**
* TODO: Use camelCase for all variables and functions here (except for protobuf generated stuff)
* I was originally planning to implement this into YouTube.js, but as I started implementing more
* googlevideo related things, I realized this would be better suited as a separate module :).
*/
import { UMP } from './UMP.js';
import { EventEmitterLike, PART, base64ToU8 } from '../utils/index.js';
import { MediaInfo_MediaType } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
import { VideoPlaybackAbrRequest } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
import { MediaHeader } from '../../protos/generated/video_streaming/media_header.js';
import { NextRequestPolicy } from '../../protos/generated/video_streaming/next_request_policy.js';
import { FormatInitializationMetadata } from '../../protos/generated/video_streaming/format_initialization_metadata.js';
import { SabrRedirect } from '../../protos/generated/video_streaming/sabr_redirect.js';
import { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
import { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
import { PlaybackCookie } from '../../protos/generated/video_streaming/playback_cookie.js';
import type { FormatId } from '../../protos/generated/misc/common.js';
import type { MediaInfo } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
import type { FetchFunction, InitializedFormat, InitOptions, MediaArgs, ServerAbrResponse, ServerAbrStreamOptions } from '../utils/types.js';
import { ChunkedDataBuffer } from './ChunkedDataBuffer.js';
export class ServerAbrStream extends EventEmitterLike {
private fetch_fn: FetchFunction;
private server_abr_streaming_url: string;
private video_playback_ustreamer_config: string;
private po_token?: string;
private playback_cookie?: PlaybackCookie;
private initialized_formats: InitializedFormat[] = [];
private total_duration_ms: number;
constructor(args: ServerAbrStreamOptions) {
super();
this.fetch_fn = args.fetch || fetch;
this.server_abr_streaming_url = args.server_abr_streaming_url;
this.video_playback_ustreamer_config = args.video_playback_ustreamer_config;
this.po_token = args.po_token;
this.total_duration_ms = args.duration_ms;
}
public on(event: 'data', listener: (data: ServerAbrResponse) => void): void;
public on(event: 'error', listener: (error: Error) => void): void;
public on(event: string, listener: (...args: any[]) => void): void {
super.on(event, listener);
}
public once(event: 'data', listener: (data: ServerAbrResponse) => void): void;
public once(event: 'error', listener: (error: Error) => void): void;
public once(event: string, listener: (...args: any[]) => void): void {
super.once(event, listener);
}
public async init(args: InitOptions) {
const { audio_formats, video_formats, media_info: initial_media_info } = args;
const first_video_format = video_formats ? video_formats[0] : undefined;
const media_info: MediaInfo = {
lastManualDirection: 0,
timeSinceLastManualFormatSelectionMs: 0,
videoWidth: video_formats.length === 1 ? first_video_format?.width : 720,
iea: video_formats.length === 1 ? first_video_format?.width : 720,
startTimeMs: 0,
visibility: 0,
mediaType: MediaInfo_MediaType.MEDIA_TYPE_DEFAULT,
...initial_media_info
};
const audio_format_ids = audio_formats.map<FormatId>((fmt) => ({
itag: fmt.itag,
lastModified: parseInt(fmt.last_modified_ms),
xtags: fmt.xtags
}));
const video_format_ids = video_formats.map<FormatId>((fmt) => ({
itag: fmt.itag,
lastModified: parseInt(fmt.last_modified_ms),
xtags: fmt.xtags
}));
if (typeof media_info.startTimeMs !== 'number')
throw new Error('Invalid media start time');
try {
while (media_info.startTimeMs < this.total_duration_ms) {
const data = await this.fetchMedia({ media_info, audio_format_ids, video_format_ids });
this.emit('data', data);
if (data.sabr_error) break;
const main_format =
media_info.mediaType === MediaInfo_MediaType.MEDIA_TYPE_DEFAULT
? data.initialized_formats.find((fmt) => fmt.mime_type?.includes('video'))
: data.initialized_formats[0];
if (!main_format) break;
if (main_format?.sequence_count === main_format.sequence_list[main_format.sequence_list.length - 1].sequence_number) break;
media_info.startTimeMs += main_format.sequence_list.reduce((acc, seq) => acc + (seq.duration_ms || 0), 0);
}
} catch (error) {
this.emit('error', error);
}
}
private async fetchMedia(args: MediaArgs): Promise<ServerAbrResponse> {
const { media_info, audio_format_ids, video_format_ids } = args;
this.initialized_formats.forEach((format) => {
format.sequence_list = [];
format.media_data = new Uint8Array(0);
});
const body = VideoPlaybackAbrRequest.encode({
mediaInfo: media_info,
formatIds: this.initialized_formats.map((fmt) => fmt.format_id),
audioFormatIds: audio_format_ids,
videoFormatIds: video_format_ids,
videoPlaybackUstreamerConfig: base64ToU8(this.video_playback_ustreamer_config),
sc: {
field5: [],
field6: [],
poToken: this.po_token ? base64ToU8(this.po_token) : undefined,
playbackCookie: this.playback_cookie ? PlaybackCookie.encode(this.playback_cookie).finish() : undefined,
clientInfo: {
clientName: 1,
clientVersion: '2.2040620.05.00',
osName: 'Windows',
osVersion: '10.0'
}
},
ud: this.initialized_formats.map((fmt) => fmt._state),
field1000: []
}).finish();
const response = await this.fetch_fn(this.server_abr_streaming_url, { method: 'POST', body });
return this.processUMPResponse(response);
}
public async processUMPResponse(response: Response) {
let sabr_error: SabrError | undefined;
let stream_protection_status: StreamProtectionStatus | undefined;
const data = await response.arrayBuffer();
const ump = new UMP(new ChunkedDataBuffer([ new Uint8Array(data) ]));
ump.parse((part) => {
const data = part.data.chunks[0];
switch (part.type) {
case PART.MEDIA_HEADER:
this.processMediaHeader(data);
break;
case PART.MEDIA:
this.processMediaData(part.data);
break;
case PART.MEDIA_END:
this.processEndOfMedia(part.data);
break;
case PART.NEXT_REQUEST_POLICY:
this.processNextRequestPolicy(data);
break;
case PART.FORMAT_INITIALIZATION_METADATA:
this.processFormatInitialization(data);
break;
case PART.SABR_REDIRECT:
this.processSabrRedirect(data);
break;
case PART.SABR_ERROR:
sabr_error = SabrError.decode(data);
break;
case PART.STREAM_PROTECTION_STATUS:
stream_protection_status = StreamProtectionStatus.decode(data);
break;
default:
break;
}
});
return {
initialized_formats: this.initialized_formats,
stream_protection_status,
sabr_error
};
}
private processMediaHeader(data: Uint8Array) {
const media_header = MediaHeader.decode(data);
const target_format = this.initialized_formats.find((fmt) => fmt.format_id.itag === media_header.itag);
if (!target_format) return;
// Skip processing if this is an init segment and we've already received it.
if (media_header.isInitSeg) {
if (!target_format.init_segment) {
target_format._init_segment_media_id = media_header.headerId;
} else return;
}
// Save the header's ID so we can identify its media data later.
if (!target_format._media_data_ids.includes(media_header.headerId || 0)) {
target_format._media_data_ids.push(media_header.headerId || 0);
}
if (media_header.sequenceNumber && !target_format.sequence_list.some((seq) => seq.sequence_number === media_header.sequenceNumber)) {
target_format.sequence_list.push({
itag: media_header.itag,
format_id: media_header.formatId,
duration_ms: media_header.durationMs,
start_ms: media_header.startMs,
start_data_range: media_header.startDataRange,
sequence_number: media_header.sequenceNumber,
content_length: media_header.contentLength,
time_range: media_header.timeRange
});
// This ensures sequences are retrieved in order.
this.initialized_formats.forEach((item) => {
if (item._state && item.format_id.itag === media_header.itag) {
item._state.durationMs += media_header.durationMs || 0;
item._state.field5 += 1;
}
});
}
}
private processMediaData(data: ChunkedDataBuffer) {
const media_data_id = data.getUint8(0);
const new_data = data.split(1).remainingBuffer.chunks[0];
const target_format = this.initialized_formats.find((fmt) => fmt._media_data_ids.includes(media_data_id));
if (!target_format) return;
const isInitSegData = target_format._init_segment_media_id === media_data_id;
if (target_format.init_segment && isInitSegData)
return;
if (isInitSegData) {
target_format.init_segment = new_data;
delete target_format._init_segment_media_id;
return;
}
const combined_length = target_format.media_data.length + new_data.length;
const temp_media_data = new Uint8Array(combined_length);
temp_media_data.set(target_format.media_data);
temp_media_data.set(new_data, target_format.media_data.length);
target_format.media_data = temp_media_data;
}
private processEndOfMedia(data: ChunkedDataBuffer) {
const media_data_id = data.getUint8(0);
const target_format = this.initialized_formats.find((fmt) => fmt._media_data_ids.includes(media_data_id));
if (target_format) target_format._media_data_ids.splice(target_format._media_data_ids.indexOf(media_data_id), 1);
}
private processNextRequestPolicy(data: Uint8Array) {
const next_request_policy = NextRequestPolicy.decode(data);
this.playback_cookie = next_request_policy.playbackCookie;
}
private processFormatInitialization(data: Uint8Array) {
const format_initialization_metadata = FormatInitializationMetadata.decode(data);
if (format_initialization_metadata.formatId && !this.initialized_formats.some((item) => item.format_id.itag === format_initialization_metadata.formatId?.itag)) {
this.initialized_formats.push({
format_id: format_initialization_metadata.formatId,
duration_ms: format_initialization_metadata.durationMs,
mime_type: format_initialization_metadata.mimeType,
sequence_count: format_initialization_metadata.field4,
sequence_list: [],
media_data: new Uint8Array(),
// Only meant to be used internally.
_media_data_ids: [],
_state: {
formatId: format_initialization_metadata.formatId,
startTimeMs: 0,
durationMs: 0,
field4: 1,
field5: 0
}
});
}
}
private processSabrRedirect(data: Uint8Array) {
const sabr_redirect = SabrRedirect.decode(data);
if (!sabr_redirect.url)
throw new Error('Invalid SABR redirect');
this.server_abr_streaming_url = sabr_redirect.url;
}
}

127
src/core/UMP.ts Normal file
View File

@@ -0,0 +1,127 @@
import type { Part } from '../index.js';
import type { ChunkedDataBuffer } from './ChunkedDataBuffer.js';
export class UMP {
private chunkedDataBuffer: ChunkedDataBuffer;
constructor(chunkedDataBuffer: ChunkedDataBuffer) {
this.chunkedDataBuffer = chunkedDataBuffer;
}
public parse(handlePart: (part: Part) => void) {
while (true) {
let offset = 0;
const [ partType, newOffset ] = this.readVarInt(offset);
offset = newOffset;
const [ partSize, finalOffset ] = this.readVarInt(offset);
offset = finalOffset;
if (partType < 0 || partSize < 0)
break;
// Note that we don't handle cases like this YET..
if (!this.chunkedDataBuffer.canReadBytes(offset, partSize))
break;
const splitResult = this.chunkedDataBuffer.split(offset).remainingBuffer.split(partSize);
offset = 0;
handlePart({
type: partType,
size: partSize,
data: splitResult.extractedBuffer
});
this.chunkedDataBuffer = splitResult.remainingBuffer;
}
}
public readVarInt(offset: number): [ number, number ] {
let byteLength: number;
if (this.chunkedDataBuffer.canReadBytes(offset, 1)) {
const firstByte = this.chunkedDataBuffer.getUint8(offset);
// Determine the length of the val
if (firstByte < 128) {
byteLength = 1;
} else if (firstByte < 192) {
byteLength = 2;
} else if (firstByte < 224) {
byteLength = 3;
} else if (firstByte < 240) {
byteLength = 4;
} else {
byteLength = 5;
}
} else {
byteLength = 0;
}
if (byteLength < 1 || !this.chunkedDataBuffer.canReadBytes(offset, byteLength)) {
return [ -1, offset ];
}
let value: number;
// Now read it based on the length
switch (byteLength) {
case 1:
value = this.chunkedDataBuffer.getUint8(offset++);
break;
case 2: {
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
value = (byte1 & 0x3f) + 64 * byte2;
break;
}
case 3: {
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
const byte3 = this.chunkedDataBuffer.getUint8(offset++);
value = (byte1 & 0x1f) + 32 * (byte2 + 256 * byte3);
break;
}
case 4: {
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
const byte3 = this.chunkedDataBuffer.getUint8(offset++);
const byte4 = this.chunkedDataBuffer.getUint8(offset++);
value = (byte1 & 0x0f) + 16 * (byte2 + 256 * (byte3 + 256 * byte4));
break;
}
default: {
const tempOffset = offset + 1;
this.chunkedDataBuffer.focus(tempOffset);
if (this.canReadFromCurrentChunk(tempOffset, 4)) {
value = this.getCurrentDataView().getUint32(tempOffset - this.chunkedDataBuffer.currentChunkOffset, true);
} else {
const byte3 = this.chunkedDataBuffer.getUint8(tempOffset + 2) + 256 * this.chunkedDataBuffer.getUint8(tempOffset + 3);
value =
this.chunkedDataBuffer.getUint8(tempOffset) +
256 * (this.chunkedDataBuffer.getUint8(tempOffset + 1) + 256 * byte3);
}
offset += 5;
break;
}
}
return [ value, offset ];
}
public canReadFromCurrentChunk(offset: number, length: number): boolean {
this.chunkedDataBuffer.isFocused(offset);
return offset - this.chunkedDataBuffer.currentChunkOffset + length <= this.chunkedDataBuffer.chunks[this.chunkedDataBuffer.currentChunkIndex].length;
}
public getCurrentDataView(): DataView {
if (!this.chunkedDataBuffer.currentDataView) {
const currentChunk = this.chunkedDataBuffer.chunks[this.chunkedDataBuffer.currentChunkIndex];
this.chunkedDataBuffer.currentDataView = new DataView(currentChunk.buffer, currentChunk.byteOffset, currentChunk.length);
}
return this.chunkedDataBuffer.currentDataView;
}
}

3
src/core/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './ChunkedDataBuffer.js';
export * from './UMP.js';
export * from './ServerAbrStream.js';

4
src/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import * as GoogleVideo from './core/index.js';
export { GoogleVideo };
export default GoogleVideo;
export * from './utils/index.js';

View File

@@ -0,0 +1,47 @@
import { CustomEvent } from './index.js';
export class EventEmitterLike extends EventTarget {
#legacy_listeners = new Map<(...args: any[]) => void, EventListener>();
constructor() {
super();
}
emit(type: string, ...args: any[]) {
const event = new CustomEvent(type, { detail: args });
this.dispatchEvent(event);
}
on(type: string, listener: (...args: any[]) => void) {
const wrapper: EventListener = (ev) => {
if (ev instanceof CustomEvent) {
listener(...ev.detail as any[]);
} else {
listener(ev);
}
};
this.#legacy_listeners.set(listener, wrapper);
this.addEventListener(type, wrapper);
}
once(type: string, listener: (...args: any[]) => void) {
const wrapper: EventListener = (ev) => {
if (ev instanceof CustomEvent) {
listener(...ev.detail as any[]);
} else {
listener(ev);
}
this.off(type, listener);
};
this.#legacy_listeners.set(listener, wrapper);
this.addEventListener(type, wrapper);
}
off(type: string, listener: (...args: any[]) => void) {
const wrapper = this.#legacy_listeners.get(listener);
if (wrapper) {
this.removeEventListener(type, wrapper);
this.#legacy_listeners.delete(listener);
}
}
}

10
src/utils/Protos.ts Normal file
View File

@@ -0,0 +1,10 @@
export { FormatInitializationMetadata } from '../../protos/generated/video_streaming/format_initialization_metadata.js';
export { MediaHeader } from '../../protos/generated/video_streaming/media_header.js';
export { NextRequestPolicy } from '../../protos/generated/video_streaming/next_request_policy.js';
export { PlaybackCookie } from '../../protos/generated/video_streaming/playback_cookie.js';
export { PlaybackStartPolicy } from '../../protos/generated/video_streaming/playback_start_policy.js';
export { RequestCancellationPolicy } from '../../protos/generated/video_streaming/request_cancellation_policy.js';
export { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
export { SabrRedirect } from '../../protos/generated/video_streaming/sabr_redirect.js';
export { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
export { VideoPlaybackAbrRequest } from '../../protos/generated/video_streaming/video_playback_abr_request.js';

65
src/utils/helpers.ts Normal file
View File

@@ -0,0 +1,65 @@
export enum PART {
ONESIE_HEADER = 10,
ONESIE_DATA = 11,
MEDIA_HEADER = 20,
MEDIA = 21,
MEDIA_END = 22,
LIVE_METADATA = 31,
HOSTNAME_CHANGE_HINT = 32,
LIVE_METADATA_PROMISE = 33,
LIVE_METADATA_PROMISE_CANCELLATION = 34,
NEXT_REQUEST_POLICY = 35,
USTREAMER_VIDEO_AND_FORMAT_DATA = 36,
FORMAT_SELECTION_CONFIG = 37,
USTREAMER_SELECTED_MEDIA_STREAM = 38,
FORMAT_INITIALIZATION_METADATA = 42,
SABR_REDIRECT = 43,
SABR_ERROR = 44,
SABR_SEEK = 45,
RELOAD_PLAYER_RESPONSE = 46,
PLAYBACK_START_POLICY = 47,
ALLOWED_CACHED_FORMATS = 48,
START_BW_SAMPLING_HINT = 49,
PAUSE_BW_SAMPLING_HINT = 50,
SELECTABLE_FORMATS = 51,
REQUEST_IDENTIFIER = 52,
REQUEST_CANCELLATION_POLICY = 53,
ONESIE_PREFETCH_REJECTION = 54,
TIMELINE_CONTEXT = 55,
REQUEST_PIPELINING = 56,
SABR_CONTEXT_UPDATE = 57,
STREAM_PROTECTION_STATUS = 58,
SABR_CONTEXT_SENDING_POLICY = 59,
LAWNMOWER_POLICY = 60,
SABR_ACK = 61,
END_OF_TRACK = 62,
CACHE_LOAD_POLICY = 63,
LAWNMOWER_MESSAGING_POLICY = 64,
PREWARM_CONNECTION = 65
}
export { MediaInfo_MediaType as MediaType } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
export function u8ToBase64(u8: Uint8Array): string {
return btoa(String.fromCharCode.apply(null, Array.from(u8)));
}
export function base64ToU8(base64: string): Uint8Array {
const standard_base64 = base64.replace(/-/g, '+').replace(/_/g, '/');
const padded_base64 = standard_base64.padEnd(standard_base64.length + (4 - standard_base64.length % 4) % 4, '=');
return new Uint8Array(atob(padded_base64).split('').map((char) => char.charCodeAt(0)));
}
// See https://github.com/nodejs/node/issues/40678#issuecomment-1126944677
export class CustomEvent extends Event {
#detail;
constructor(type: string, options?: CustomEventInit<any[]>) {
super(type, options);
this.#detail = options?.detail ?? null;
}
get detail() {
return this.#detail;
}
}

4
src/utils/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './helpers.js';
export * from './EventEmitterLike.js';
export * as Protos from './Protos.js';
export type * from './types.js';

133
src/utils/types.ts Normal file
View File

@@ -0,0 +1,133 @@
import type { FormatId } from '../../protos/generated/misc/common.js';
import type { MediaHeader_TimeRange } from '../../protos/generated/video_streaming/media_header.js';
import type { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
import type { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
import type { Zpa, MediaInfo } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
import type { ChunkedDataBuffer } from '../core/index.js';
export type Part = {
type: number;
size: number;
data: ChunkedDataBuffer;
};
export type ServerAbrStreamOptions = {
fetch: FetchFunction;
server_abr_streaming_url: string;
video_playback_ustreamer_config: string;
po_token?: string;
duration_ms: number;
}
export type ServerAbrResponse = {
initialized_formats: InitializedFormat[];
stream_protection_status?: StreamProtectionStatus;
sabr_error?: SabrError;
}
export type Sequence = {
itag?: number;
format_id?: FormatId;
duration_ms?: number;
start_ms?: number;
start_data_range?: number;
sequence_number?: number;
content_length?: number;
time_range?: MediaHeader_TimeRange;
}
export type InitializedFormat = {
format_id: FormatId;
duration_ms?: number;
mime_type?: string;
sequence_count?: number;
init_segment?: Uint8Array;
sequence_list: Sequence[];
media_data: Uint8Array;
_init_segment_media_id?: number;
_media_data_ids: number[];
_state: Zpa;
}
export type InitOptions = {
audio_formats: Format[];
video_formats: Format[];
media_info?: MediaInfo;
};
export type MediaArgs = {
media_info: MediaInfo;
audio_format_ids: FormatId[];
video_format_ids: FormatId[];
}
export type Format = {
itag: number;
url?: string;
width?: number;
height?: number;
last_modified: Date;
last_modified_ms: string;
content_length?: number;
quality?: string;
xtags?: string;
drm_families?: string[];
fps?: number;
quality_label?: string;
projection_type?: 'RECTANGULAR' | 'EQUIRECTANGULAR' | 'EQUIRECTANGULAR_THREED_TOP_BOTTOM' | 'MESH';
average_bitrate?: number;
bitrate: number;
spatial_audio_type?: 'AMBISONICS_5_1' | 'AMBISONICS_QUAD' | 'FOA_WITH_NON_DIEGETIC';
target_duration_dec?: number;
fair_play_key_uri?: string;
stereo_layout?: 'LEFT_RIGHT' | 'TOP_BOTTOM';
max_dvr_duration_sec?: number;
high_replication?: boolean;
audio_quality?: string;
approx_duration_ms: number;
audio_sample_rate?: number;
audio_channels?: number;
loudness_db?: number;
signature_cipher?: string;
is_drc?: boolean;
drm_track_type?: string;
distinct_params?: string;
track_absolute_loudness_lkfs?: number;
mime_type: string;
is_type_otf: boolean;
init_range?: {
start: number;
end: number;
};
index_range?: {
start: number;
end: number;
};
cipher?: string;
audio_track?: {
audio_is_default: boolean;
display_name: string;
id: string;
};
has_audio: boolean;
has_video: boolean;
has_text: boolean;
language?: string | null;
is_dubbed?: boolean;
is_descriptive?: boolean;
is_secondary?: boolean;
is_original?: boolean;
color_info?: {
primaries?: string;
transfer_characteristics?: string;
matrix_coefficients?: string;
};
caption_track?: {
display_name: string;
vss_id: string;
language_code: string;
kind?: 'asr' | 'frc';
id: string;
};
}
export type FetchFunction = typeof fetch;