mirror of
https://github.com/LuanRT/googlevideo.git
synced 2026-06-13 08:42:31 +00:00
fix(ServerAbrStream): Ignore duplicate sequences
Ignoring them is not the best solution, but at least we don't end up with a corrupted file :P Note: This would only happen when downloading two itags (MediaType.MEDIA_TYPE_DEFAULT). Other changes: Convert field names to camelCase.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { WriteStream } from 'node:fs';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
import GoogleVideo, { MediaType } from '../../dist/src/index.js';
|
||||
import GoogleVideo, { type Format, MediaType } from '../../dist/src/index.js';
|
||||
|
||||
const innertube = await Innertube.create({ cache: new UniversalCache(true) });
|
||||
|
||||
@@ -31,17 +31,22 @@ let videoOutput: WriteStream | undefined;
|
||||
|
||||
const durationMs = info.basic_info?.duration ? info.basic_info.duration * 1000 : 0;
|
||||
|
||||
const audioFormat = info.chooseFormat({
|
||||
quality: 'best',
|
||||
format: 'webm',
|
||||
type: 'audio'
|
||||
});
|
||||
const audioFormat = info.chooseFormat({ quality: 'best', format: 'webm', type: 'audio' });
|
||||
const videoFormat = info.chooseFormat({ quality: '720p', format: 'webm', type: 'video' });
|
||||
|
||||
const videoFormat = info.chooseFormat({
|
||||
quality: '720p',
|
||||
format: 'mp4',
|
||||
type: 'video'
|
||||
});
|
||||
const selectedAudioFormat: Format = {
|
||||
itag: audioFormat.itag,
|
||||
lastModified: audioFormat.last_modified_ms,
|
||||
xtags: audioFormat.xtags
|
||||
};
|
||||
|
||||
const selectedVideoFormat: Format = {
|
||||
itag: videoFormat.itag,
|
||||
lastModified: videoFormat.last_modified_ms,
|
||||
width: videoFormat.width,
|
||||
height: videoFormat.height,
|
||||
xtags: videoFormat.xtags
|
||||
};
|
||||
|
||||
console.info(`Selected audio format: ${audioFormat.itag} (${audioFormat.audio_quality})`);
|
||||
console.info(`Selected video format: ${videoFormat.itag} (${videoFormat.quality_label})`);
|
||||
@@ -59,45 +64,45 @@ if (!serverAbrStreamingUrl)
|
||||
|
||||
const serverAbrStream = new GoogleVideo.ServerAbrStream({
|
||||
fetch: innertube.session.http.fetch_function,
|
||||
server_abr_streaming_url: serverAbrStreamingUrl,
|
||||
video_playback_ustreamer_config: videoPlaybackUstreamerConfig,
|
||||
duration_ms: durationMs
|
||||
serverAbrStreamingUrl,
|
||||
videoPlaybackUstreamerConfig: videoPlaybackUstreamerConfig,
|
||||
durationMs
|
||||
});
|
||||
|
||||
serverAbrStream.on('data', (data) => {
|
||||
let progressText = '';
|
||||
|
||||
for (const initializedFormat of data.initialized_formats) {
|
||||
const isVideo = initializedFormat.mime_type?.includes('video');
|
||||
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === initializedFormat.format_id.itag);
|
||||
for (const initializedFormat of data.initializedFormats) {
|
||||
const isVideo = initializedFormat.mimeType?.includes('video');
|
||||
const mediaFormat = info.streaming_data?.adaptive_formats.find((f) => f.itag === initializedFormat.formatId.itag);
|
||||
|
||||
if (isVideo && initializedFormat.media_data) {
|
||||
if (isVideo && initializedFormat.mediaData) {
|
||||
if (!videoOutput)
|
||||
videoOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.format_id.itag}.${determineFileExtension(initializedFormat.mime_type || '')}`);
|
||||
videoOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
|
||||
|
||||
if (initializedFormat.init_segment && !wroteVideoInitSegment) {
|
||||
videoOutput.write(initializedFormat.init_segment);
|
||||
if (initializedFormat.initSegment && !wroteVideoInitSegment) {
|
||||
videoOutput.write(initializedFormat.initSegment);
|
||||
wroteVideoInitSegment = true;
|
||||
}
|
||||
|
||||
videoOutput.write(initializedFormat.media_data);
|
||||
} else if (initializedFormat.media_data) {
|
||||
videoOutput.write(initializedFormat.mediaData);
|
||||
} else if (initializedFormat.mediaData) {
|
||||
if (!audioOutput)
|
||||
audioOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.format_id.itag}.${determineFileExtension(initializedFormat.mime_type || '')}`);
|
||||
audioOutput = createWriteStream(`${sanitizedTitle}.${initializedFormat.formatId.itag}.${determineFileExtension(initializedFormat.mimeType || '')}`);
|
||||
|
||||
if (initializedFormat.init_segment && !wroteAudioInitSegment) {
|
||||
audioOutput.write(initializedFormat.init_segment);
|
||||
if (initializedFormat.initSegment && !wroteAudioInitSegment) {
|
||||
audioOutput.write(initializedFormat.initSegment);
|
||||
wroteAudioInitSegment = true;
|
||||
}
|
||||
|
||||
audioOutput.write(initializedFormat.media_data);
|
||||
audioOutput.write(initializedFormat.mediaData);
|
||||
}
|
||||
|
||||
const fmtIdentifier = `${initializedFormat.format_id.itag}_${initializedFormat.mime_type?.split(';')[0]}`;
|
||||
const fmtIdentifier = `${initializedFormat.formatId.itag}_${initializedFormat.mimeType?.split(';')[0]}`;
|
||||
|
||||
const percentage = Math.round((initializedFormat.sequence_list.at(-1)?.start_data_range ?? 0) / (mediaFormat?.content_length ?? 0) * 100);
|
||||
const percentage = Math.round((initializedFormat.sequenceList.at(-1)?.startDataRange ?? 0) / (mediaFormat?.content_length ?? 0) * 100);
|
||||
|
||||
if (percentage !== undefined)
|
||||
if (percentage)
|
||||
progressText += `${fmtIdentifier}: ${percentage}% | `;
|
||||
}
|
||||
|
||||
@@ -111,9 +116,14 @@ serverAbrStream.on('error', (error) => {
|
||||
});
|
||||
|
||||
await serverAbrStream.init({
|
||||
audio_formats: [ audioFormat ],
|
||||
video_formats: [ videoFormat ],
|
||||
media_info: {
|
||||
audioFormats: [ selectedAudioFormat ],
|
||||
videoFormats: [ selectedVideoFormat ],
|
||||
mediaInfo: {
|
||||
/**
|
||||
* MEDIA_TYPE_DEFAULT = 0,
|
||||
* MEDIA_TYPE_AUDIO = 1,
|
||||
* MEDIA_TYPE_VIDEO = 2,
|
||||
*/
|
||||
mediaType: MediaType.MEDIA_TYPE_DEFAULT,
|
||||
startTimeMs: 0
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
@@ -23,21 +17,22 @@ import type { FetchFunction, InitializedFormat, InitOptions, MediaArgs, ServerAb
|
||||
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;
|
||||
private fetchFn: FetchFunction;
|
||||
private serverAbrStreamingUrl: string;
|
||||
private videoPlaybackUstreamerConfig: string;
|
||||
private poToken?: string;
|
||||
private playbackCookie?: PlaybackCookie;
|
||||
private initializedFormats: InitializedFormat[] = [];
|
||||
private totalDurationMs: number;
|
||||
private prevSeqs: Map<string, number[]> = new Map();
|
||||
|
||||
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;
|
||||
this.fetchFn = args.fetch || fetch;
|
||||
this.serverAbrStreamingUrl = args.serverAbrStreamingUrl;
|
||||
this.videoPlaybackUstreamerConfig = args.videoPlaybackUstreamerConfig;
|
||||
this.poToken = args.poToken;
|
||||
this.totalDurationMs = args.durationMs;
|
||||
}
|
||||
|
||||
public on(event: 'data', listener: (data: ServerAbrResponse) => void): void;
|
||||
@@ -53,53 +48,61 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
}
|
||||
|
||||
public async init(args: InitOptions) {
|
||||
const { audio_formats, video_formats, media_info: initial_media_info } = args;
|
||||
const { audioFormats, videoFormats, mediaInfo: initialMediaInfo } = args;
|
||||
|
||||
const first_video_format = video_formats ? video_formats[0] : undefined;
|
||||
|
||||
const media_info: MediaInfo = {
|
||||
const firstVideoFormat = videoFormats ? videoFormats[0] : undefined;
|
||||
|
||||
const mediaInfo: 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,
|
||||
videoWidth: videoFormats.length === 1 ? firstVideoFormat?.width : 720,
|
||||
iea: videoFormats.length === 1 ? firstVideoFormat?.width : 720,
|
||||
startTimeMs: 0,
|
||||
visibility: 0,
|
||||
mediaType: MediaInfo_MediaType.MEDIA_TYPE_DEFAULT,
|
||||
...initial_media_info
|
||||
...initialMediaInfo
|
||||
};
|
||||
|
||||
const audio_format_ids = audio_formats.map<FormatId>((fmt) => ({
|
||||
const audioFormatIds = audioFormats.map<FormatId>((fmt) => ({
|
||||
itag: fmt.itag,
|
||||
lastModified: parseInt(fmt.last_modified_ms),
|
||||
lastModified: parseInt(fmt.lastModified),
|
||||
xtags: fmt.xtags
|
||||
}));
|
||||
|
||||
const video_format_ids = video_formats.map<FormatId>((fmt) => ({
|
||||
const videoFormatIds = videoFormats.map<FormatId>((fmt) => ({
|
||||
itag: fmt.itag,
|
||||
lastModified: parseInt(fmt.last_modified_ms),
|
||||
lastModified: parseInt(fmt.lastModified),
|
||||
xtags: fmt.xtags
|
||||
}));
|
||||
|
||||
if (typeof media_info.startTimeMs !== 'number')
|
||||
if (typeof mediaInfo.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 });
|
||||
while (mediaInfo.startTimeMs < this.totalDurationMs) {
|
||||
const data = await this.fetchMedia({ mediaInfo, audioFormatIds, videoFormatIds });
|
||||
|
||||
this.emit('data', data);
|
||||
|
||||
if (data.sabr_error) break;
|
||||
if (data.sabrError) 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];
|
||||
const mainFormat =
|
||||
mediaInfo.mediaType === MediaInfo_MediaType.MEDIA_TYPE_DEFAULT
|
||||
? data.initializedFormats.find((fmt) => fmt.mimeType?.includes('video'))
|
||||
: data.initializedFormats[0];
|
||||
|
||||
if (!main_format) break;
|
||||
if (main_format?.sequence_count === main_format.sequence_list[main_format.sequence_list.length - 1].sequence_number) break;
|
||||
for (const fmt of data.initializedFormats) {
|
||||
this.prevSeqs.set(`${fmt.formatId.itag};${fmt.formatId.lastModified};`, fmt.sequenceList.map((seq) => seq.sequenceNumber || 0));
|
||||
}
|
||||
|
||||
media_info.startTimeMs += main_format.sequence_list.reduce((acc, seq) => acc + (seq.duration_ms || 0), 0);
|
||||
if (!mainFormat) break;
|
||||
if (
|
||||
mainFormat?.sequenceCount ===
|
||||
mainFormat.sequenceList[mainFormat.sequenceList.length - 1].sequenceNumber
|
||||
)
|
||||
break;
|
||||
|
||||
mediaInfo.startTimeMs += mainFormat.sequenceList.reduce((acc, seq) => acc + (seq.durationMs || 0), 0);
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit('error', error);
|
||||
@@ -107,24 +110,24 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
}
|
||||
|
||||
private async fetchMedia(args: MediaArgs): Promise<ServerAbrResponse> {
|
||||
const { media_info, audio_format_ids, video_format_ids } = args;
|
||||
const { mediaInfo, audioFormatIds, videoFormatIds } = args;
|
||||
|
||||
this.initialized_formats.forEach((format) => {
|
||||
format.sequence_list = [];
|
||||
format.media_data = new Uint8Array(0);
|
||||
this.initializedFormats.forEach((format) => {
|
||||
format.sequenceList = [];
|
||||
format.mediaData = 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),
|
||||
mediaInfo: mediaInfo,
|
||||
formatIds: this.initializedFormats.map((fmt) => fmt.formatId),
|
||||
audioFormatIds: audioFormatIds,
|
||||
videoFormatIds: videoFormatIds,
|
||||
videoPlaybackUstreamerConfig: base64ToU8(this.videoPlaybackUstreamerConfig),
|
||||
sc: {
|
||||
field5: [],
|
||||
field6: [],
|
||||
poToken: this.po_token ? base64ToU8(this.po_token) : undefined,
|
||||
playbackCookie: this.playback_cookie ? PlaybackCookie.encode(this.playback_cookie).finish() : undefined,
|
||||
poToken: this.poToken ? base64ToU8(this.poToken) : undefined,
|
||||
playbackCookie: this.playbackCookie ? PlaybackCookie.encode(this.playbackCookie).finish() : undefined,
|
||||
clientInfo: {
|
||||
clientName: 1,
|
||||
clientVersion: '2.2040620.05.00',
|
||||
@@ -132,21 +135,22 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
osVersion: '10.0'
|
||||
}
|
||||
},
|
||||
ud: this.initialized_formats.map((fmt) => fmt._state),
|
||||
ud: this.initializedFormats.map((fmt) => fmt._state),
|
||||
field1000: []
|
||||
}).finish();
|
||||
|
||||
const response = await this.fetch_fn(this.server_abr_streaming_url, { method: 'POST', body });
|
||||
const response = await this.fetchFn(this.serverAbrStreamingUrl, { method: 'POST', body });
|
||||
const data = await response.arrayBuffer();
|
||||
|
||||
return this.processUMPResponse(response);
|
||||
return this.processUMPResponse(new Uint8Array(data));
|
||||
}
|
||||
|
||||
public async processUMPResponse(response: Response) {
|
||||
let sabr_error: SabrError | undefined;
|
||||
let stream_protection_status: StreamProtectionStatus | undefined;
|
||||
public async processUMPResponse(data: Uint8Array): Promise<ServerAbrResponse> {
|
||||
let sabrError: SabrError | undefined;
|
||||
let sabrRedirect: SabrRedirect | undefined;
|
||||
let streamProtectionStatus: StreamProtectionStatus | undefined;
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const ump = new UMP(new ChunkedDataBuffer([ new Uint8Array(data) ]));
|
||||
const ump = new UMP(new ChunkedDataBuffer([ data ]));
|
||||
|
||||
ump.parse((part) => {
|
||||
const data = part.data.chunks[0];
|
||||
@@ -166,14 +170,14 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
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);
|
||||
sabrError = SabrError.decode(data);
|
||||
break;
|
||||
case PART.SABR_REDIRECT:
|
||||
sabrRedirect = this.processSabrRedirect(data);
|
||||
break;
|
||||
case PART.STREAM_PROTECTION_STATUS:
|
||||
stream_protection_status = StreamProtectionStatus.decode(data);
|
||||
streamProtectionStatus = StreamProtectionStatus.decode(data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -181,46 +185,52 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
});
|
||||
|
||||
return {
|
||||
initialized_formats: this.initialized_formats,
|
||||
stream_protection_status,
|
||||
sabr_error
|
||||
initializedFormats: this.initializedFormats,
|
||||
streamProtectionStatus,
|
||||
sabrRedirect,
|
||||
sabrError
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
const mediaHeader = MediaHeader.decode(data);
|
||||
const targetFormat = this.initializedFormats.find((fmt) => fmt.formatId.itag === mediaHeader.itag);
|
||||
|
||||
if (!target_format) return;
|
||||
if (!targetFormat) 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;
|
||||
if (mediaHeader.isInitSeg) {
|
||||
if (!targetFormat.initSegment) {
|
||||
targetFormat._initSegmentMediaId = mediaHeader.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);
|
||||
}
|
||||
// FIXME: This is a hacky workaround to prevent duplicate sequences from being added. This should be fixed in the future (preferably by figuring out how to make the server not send duplicates).
|
||||
if (mediaHeader.sequenceNumber && this.prevSeqs.get(`${targetFormat.formatId.itag};${targetFormat.formatId.lastModified};`)?.includes(mediaHeader.sequenceNumber))
|
||||
return;
|
||||
|
||||
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
|
||||
// Save the header's ID so we can identify its media data later.
|
||||
if (!targetFormat._headerIds.has(mediaHeader.headerId || 0))
|
||||
targetFormat._headerIds.add(mediaHeader.headerId || 0);
|
||||
|
||||
if (
|
||||
mediaHeader.sequenceNumber &&
|
||||
!targetFormat.sequenceList.some((seq) => seq.sequenceNumber === mediaHeader.sequenceNumber)
|
||||
) {
|
||||
targetFormat.sequenceList.push({
|
||||
itag: mediaHeader.itag,
|
||||
formatId: mediaHeader.formatId,
|
||||
durationMs: mediaHeader.durationMs,
|
||||
startMs: mediaHeader.startMs,
|
||||
startDataRange: mediaHeader.startDataRange,
|
||||
sequenceNumber: mediaHeader.sequenceNumber,
|
||||
contentLength: mediaHeader.contentLength,
|
||||
timeRange: mediaHeader.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;
|
||||
this.initializedFormats.forEach((item) => {
|
||||
if (item._state && item.formatId.itag === mediaHeader.itag) {
|
||||
item._state.durationMs += mediaHeader.durationMs || 0;
|
||||
item._state.field5 += 1;
|
||||
}
|
||||
});
|
||||
@@ -228,58 +238,60 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
}
|
||||
|
||||
private processMediaData(data: ChunkedDataBuffer) {
|
||||
const media_data_id = data.getUint8(0);
|
||||
const new_data = data.split(1).remainingBuffer.chunks[0];
|
||||
const headerId = data.getUint8(0);
|
||||
const streamData = data.split(1).remainingBuffer;
|
||||
|
||||
const target_format = this.initialized_formats.find((fmt) => fmt._media_data_ids.includes(media_data_id));
|
||||
const targetFormat = this.initializedFormats.find((fmt) => fmt._headerIds.has(headerId));
|
||||
if (!targetFormat)
|
||||
return;
|
||||
|
||||
if (!target_format) return;
|
||||
|
||||
const isInitSegData = target_format._init_segment_media_id === media_data_id;
|
||||
|
||||
if (target_format.init_segment && isInitSegData)
|
||||
const isInitSegData = targetFormat._initSegmentMediaId === headerId;
|
||||
if (targetFormat.initSegment && isInitSegData)
|
||||
return;
|
||||
|
||||
if (isInitSegData) {
|
||||
target_format.init_segment = new_data;
|
||||
delete target_format._init_segment_media_id;
|
||||
targetFormat.initSegment = streamData.chunks[0];
|
||||
delete targetFormat._initSegmentMediaId;
|
||||
return;
|
||||
}
|
||||
|
||||
const combined_length = target_format.media_data.length + new_data.length;
|
||||
const temp_media_data = new Uint8Array(combined_length);
|
||||
const combinedLength = targetFormat.mediaData.length + streamData.chunks[0].length;
|
||||
const tempMediaData = new Uint8Array(combinedLength);
|
||||
|
||||
temp_media_data.set(target_format.media_data);
|
||||
temp_media_data.set(new_data, target_format.media_data.length);
|
||||
tempMediaData.set(targetFormat.mediaData);
|
||||
tempMediaData.set(streamData.chunks[0], targetFormat.mediaData.length);
|
||||
|
||||
target_format.media_data = temp_media_data;
|
||||
targetFormat.mediaData = tempMediaData;
|
||||
}
|
||||
|
||||
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);
|
||||
const headerId = data.getUint8(0);
|
||||
const targetFormat = this.initializedFormats.find((fmt) => fmt._headerIds.has(headerId));
|
||||
if (targetFormat) targetFormat._headerIds.delete(headerId);
|
||||
}
|
||||
|
||||
private processNextRequestPolicy(data: Uint8Array) {
|
||||
const next_request_policy = NextRequestPolicy.decode(data);
|
||||
this.playback_cookie = next_request_policy.playbackCookie;
|
||||
const nextRequestPolicy = NextRequestPolicy.decode(data);
|
||||
this.playbackCookie = nextRequestPolicy.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(),
|
||||
const formatInitializationMetadata = FormatInitializationMetadata.decode(data);
|
||||
if (
|
||||
formatInitializationMetadata.formatId &&
|
||||
!this.initializedFormats.some((item) => item.formatId.itag === formatInitializationMetadata.formatId?.itag)
|
||||
) {
|
||||
this.initializedFormats.push({
|
||||
formatId: formatInitializationMetadata.formatId,
|
||||
durationMs: formatInitializationMetadata.durationMs,
|
||||
mimeType: formatInitializationMetadata.mimeType,
|
||||
sequenceCount: formatInitializationMetadata.field4,
|
||||
sequenceList: [],
|
||||
mediaData: new Uint8Array(),
|
||||
// Only meant to be used internally.
|
||||
_media_data_ids: [],
|
||||
_headerIds: new Set<number>(),
|
||||
_state: {
|
||||
formatId: format_initialization_metadata.formatId,
|
||||
formatId: formatInitializationMetadata.formatId,
|
||||
startTimeMs: 0,
|
||||
durationMs: 0,
|
||||
field4: 1,
|
||||
@@ -289,12 +301,10 @@ export class ServerAbrStream extends EventEmitterLike {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private processSabrRedirect(data: Uint8Array): SabrRedirect {
|
||||
const sabrRedirect = SabrRedirect.decode(data);
|
||||
if (!sabrRedirect.url) throw new Error('Invalid SABR redirect');
|
||||
this.serverAbrStreamingUrl = sabrRedirect.url;
|
||||
return sabrRedirect;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user