googlevideo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +66 -0
- package/bundle/index.cjs +7134 -0
- package/bundle/index.cjs.map +7 -0
- package/dist/protos/generated/misc/common.d.ts +47 -0
- package/dist/protos/generated/misc/common.js +365 -0
- package/dist/protos/generated/video_streaming/buffered_range.d.ts +50 -0
- package/dist/protos/generated/video_streaming/buffered_range.js +403 -0
- package/dist/protos/generated/video_streaming/client_abr_state.d.ts +69 -0
- package/dist/protos/generated/video_streaming/client_abr_state.js +647 -0
- package/dist/protos/generated/video_streaming/crypto_params.d.ts +35 -0
- package/dist/protos/generated/video_streaming/crypto_params.js +143 -0
- package/dist/protos/generated/video_streaming/encrypted_player_request.d.ts +35 -0
- package/dist/protos/generated/video_streaming/encrypted_player_request.js +219 -0
- package/dist/protos/generated/video_streaming/format_initialization_metadata.d.ts +35 -0
- package/dist/protos/generated/video_streaming/format_initialization_metadata.js +207 -0
- package/dist/protos/generated/video_streaming/media_capabilities.d.ts +43 -0
- package/dist/protos/generated/video_streaming/media_capabilities.js +315 -0
- package/dist/protos/generated/video_streaming/media_header.d.ts +49 -0
- package/dist/protos/generated/video_streaming/media_header.js +328 -0
- package/dist/protos/generated/video_streaming/next_request_policy.d.ts +30 -0
- package/dist/protos/generated/video_streaming/next_request_policy.js +132 -0
- package/dist/protos/generated/video_streaming/onesie_header.d.ts +45 -0
- package/dist/protos/generated/video_streaming/onesie_header.js +339 -0
- package/dist/protos/generated/video_streaming/onesie_header_type.d.ts +32 -0
- package/dist/protos/generated/video_streaming/onesie_header_type.js +182 -0
- package/dist/protos/generated/video_streaming/onesie_player_request.d.ts +31 -0
- package/dist/protos/generated/video_streaming/onesie_player_request.js +122 -0
- package/dist/protos/generated/video_streaming/onesie_player_response.d.ts +29 -0
- package/dist/protos/generated/video_streaming/onesie_player_response.js +121 -0
- package/dist/protos/generated/video_streaming/onesie_request.d.ts +36 -0
- package/dist/protos/generated/video_streaming/onesie_request.js +199 -0
- package/dist/protos/generated/video_streaming/playback_cookie.d.ts +30 -0
- package/dist/protos/generated/video_streaming/playback_cookie.js +110 -0
- package/dist/protos/generated/video_streaming/playback_start_policy.d.ts +31 -0
- package/dist/protos/generated/video_streaming/playback_start_policy.js +155 -0
- package/dist/protos/generated/video_streaming/proxy_status.d.ts +16 -0
- package/dist/protos/generated/video_streaming/proxy_status.js +86 -0
- package/dist/protos/generated/video_streaming/request_cancellation_policy.d.ts +33 -0
- package/dist/protos/generated/video_streaming/request_cancellation_policy.js +173 -0
- package/dist/protos/generated/video_streaming/sabr_error.d.ts +26 -0
- package/dist/protos/generated/video_streaming/sabr_error.js +77 -0
- package/dist/protos/generated/video_streaming/sabr_redirect.d.ts +25 -0
- package/dist/protos/generated/video_streaming/sabr_redirect.js +61 -0
- package/dist/protos/generated/video_streaming/stream_protection_status.d.ts +26 -0
- package/dist/protos/generated/video_streaming/stream_protection_status.js +77 -0
- package/dist/protos/generated/video_streaming/streamer_context.d.ts +89 -0
- package/dist/protos/generated/video_streaming/streamer_context.js +915 -0
- package/dist/protos/generated/video_streaming/time_range.d.ts +27 -0
- package/dist/protos/generated/video_streaming/time_range.js +101 -0
- package/dist/protos/generated/video_streaming/video_playback_abr_request.d.ts +69 -0
- package/dist/protos/generated/video_streaming/video_playback_abr_request.js +659 -0
- package/dist/src/core/ChunkedDataBuffer.d.ts +20 -0
- package/dist/src/core/ChunkedDataBuffer.js +82 -0
- package/dist/src/core/ServerAbrStream.d.ts +38 -0
- package/dist/src/core/ServerAbrStream.js +272 -0
- package/dist/src/core/UMP.d.ts +10 -0
- package/dist/src/core/UMP.js +110 -0
- package/dist/src/core/index.d.ts +3 -0
- package/dist/src/core/index.js +3 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/utils/EventEmitterLike.d.ts +8 -0
- package/dist/src/utils/EventEmitterLike.js +50 -0
- package/dist/src/utils/Protos.d.ts +23 -0
- package/dist/src/utils/Protos.js +23 -0
- package/dist/src/utils/helpers.d.ts +63 -0
- package/dist/src/utils/helpers.js +101 -0
- package/dist/src/utils/index.d.ts +4 -0
- package/dist/src/utils/index.js +3 -0
- package/dist/src/utils/types.d.ts +65 -0
- package/dist/src/utils/types.js +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export class ChunkedDataBuffer {
|
|
2
|
+
constructor(chunks = []) {
|
|
3
|
+
this.chunks = [];
|
|
4
|
+
this.currentChunkOffset = this.currentChunkIndex = 0;
|
|
5
|
+
this.currentDataView = undefined;
|
|
6
|
+
this.totalLength = 0;
|
|
7
|
+
chunks.forEach((chunk) => {
|
|
8
|
+
this.append(chunk);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
getLength() {
|
|
12
|
+
return this.totalLength;
|
|
13
|
+
}
|
|
14
|
+
append(chunk) {
|
|
15
|
+
if (this.canMergeWithLastChunk(chunk)) {
|
|
16
|
+
const lastChunk = this.chunks[this.chunks.length - 1];
|
|
17
|
+
this.chunks[this.chunks.length - 1] = new Uint8Array(lastChunk.buffer, lastChunk.byteOffset, lastChunk.length + chunk.length);
|
|
18
|
+
this.resetFocus();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
this.totalLength += chunk.length;
|
|
24
|
+
}
|
|
25
|
+
split(position) {
|
|
26
|
+
const extractedBuffer = new ChunkedDataBuffer();
|
|
27
|
+
const remainingBuffer = new ChunkedDataBuffer();
|
|
28
|
+
const iterator = this.chunks[Symbol.iterator]();
|
|
29
|
+
let item = iterator.next();
|
|
30
|
+
while (!item.done) {
|
|
31
|
+
const chunk = item.value;
|
|
32
|
+
if (position >= chunk.length) {
|
|
33
|
+
extractedBuffer.append(chunk);
|
|
34
|
+
position -= chunk.length;
|
|
35
|
+
}
|
|
36
|
+
else if (position > 0) {
|
|
37
|
+
extractedBuffer.append(new Uint8Array(chunk.buffer, chunk.byteOffset, position));
|
|
38
|
+
remainingBuffer.append(new Uint8Array(chunk.buffer, chunk.byteOffset + position, chunk.length - position));
|
|
39
|
+
position = 0;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
remainingBuffer.append(chunk);
|
|
43
|
+
}
|
|
44
|
+
item = iterator.next();
|
|
45
|
+
}
|
|
46
|
+
return { extractedBuffer, remainingBuffer };
|
|
47
|
+
}
|
|
48
|
+
isFocused(position) {
|
|
49
|
+
return position >= this.currentChunkOffset && position < this.currentChunkOffset + this.chunks[this.currentChunkIndex].length;
|
|
50
|
+
}
|
|
51
|
+
focus(position) {
|
|
52
|
+
if (!this.isFocused(position)) {
|
|
53
|
+
if (position < this.currentChunkOffset)
|
|
54
|
+
this.resetFocus();
|
|
55
|
+
while (this.currentChunkOffset + this.chunks[this.currentChunkIndex].length <= position &&
|
|
56
|
+
this.currentChunkIndex < this.chunks.length - 1) {
|
|
57
|
+
this.currentChunkOffset += this.chunks[this.currentChunkIndex].length;
|
|
58
|
+
this.currentChunkIndex += 1;
|
|
59
|
+
}
|
|
60
|
+
this.currentDataView = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
canReadBytes(position, length) {
|
|
64
|
+
return position + length <= this.totalLength;
|
|
65
|
+
}
|
|
66
|
+
getUint8(position) {
|
|
67
|
+
this.focus(position);
|
|
68
|
+
return this.chunks[this.currentChunkIndex][position - this.currentChunkOffset];
|
|
69
|
+
}
|
|
70
|
+
canMergeWithLastChunk(chunk) {
|
|
71
|
+
if (this.chunks.length === 0)
|
|
72
|
+
return false;
|
|
73
|
+
const lastChunk = this.chunks[this.chunks.length - 1];
|
|
74
|
+
return (lastChunk.buffer === chunk.buffer &&
|
|
75
|
+
lastChunk.byteOffset + lastChunk.length === chunk.byteOffset);
|
|
76
|
+
}
|
|
77
|
+
resetFocus() {
|
|
78
|
+
this.currentDataView = undefined;
|
|
79
|
+
this.currentChunkIndex = 0;
|
|
80
|
+
this.currentChunkOffset = 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EventEmitterLike } from '../utils/index.js';
|
|
2
|
+
import type { InitOptions, ServerAbrResponse, ServerAbrStreamOptions } from '../utils/types.js';
|
|
3
|
+
export declare class ServerAbrStream extends EventEmitterLike {
|
|
4
|
+
private fetchFunction;
|
|
5
|
+
private serverAbrStreamingUrl;
|
|
6
|
+
private videoPlaybackUstreamerConfig;
|
|
7
|
+
private poToken?;
|
|
8
|
+
private playbackCookie?;
|
|
9
|
+
private totalDurationMs;
|
|
10
|
+
private initializedFormats;
|
|
11
|
+
private formatsByKey;
|
|
12
|
+
private headerIdToFormatKeyMap;
|
|
13
|
+
private previousSequences;
|
|
14
|
+
constructor(args: ServerAbrStreamOptions);
|
|
15
|
+
on(event: 'end', listener: (streamData: ServerAbrResponse) => void): void;
|
|
16
|
+
on(event: 'data', listener: (streamData: ServerAbrResponse) => void): void;
|
|
17
|
+
on(event: 'error', listener: (error: Error) => void): void;
|
|
18
|
+
once(event: 'end', listener: (streamData: ServerAbrResponse) => void): void;
|
|
19
|
+
once(event: 'data', listener: (streamData: ServerAbrResponse) => void): void;
|
|
20
|
+
once(event: 'error', listener: (error: Error) => void): void;
|
|
21
|
+
/**
|
|
22
|
+
* Initializes the server ABR stream with the provided options.
|
|
23
|
+
* @param args - The initialization options.
|
|
24
|
+
*/
|
|
25
|
+
init(args: InitOptions): Promise<void>;
|
|
26
|
+
private fetchMedia;
|
|
27
|
+
/**
|
|
28
|
+
* Parses the UMP response data and updates the initialized formats.
|
|
29
|
+
* @param response - The UMP response data as a byte array.
|
|
30
|
+
*/
|
|
31
|
+
parseUMPResponse(response: Uint8Array): Promise<ServerAbrResponse>;
|
|
32
|
+
private processMediaHeader;
|
|
33
|
+
private processMediaData;
|
|
34
|
+
private processEndOfMedia;
|
|
35
|
+
private processNextRequestPolicy;
|
|
36
|
+
private processFormatInitialization;
|
|
37
|
+
private processSabrRedirect;
|
|
38
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { UMP } from './UMP.js';
|
|
2
|
+
import { ChunkedDataBuffer } from './ChunkedDataBuffer.js';
|
|
3
|
+
import { EventEmitterLike, PART, QUALITY, base64ToU8, getFormatKey } from '../utils/index.js';
|
|
4
|
+
import { ClientAbrState_MediaType } from '../../protos/generated/video_streaming/client_abr_state.js';
|
|
5
|
+
import { VideoPlaybackAbrRequest } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
|
|
6
|
+
import { MediaHeader } from '../../protos/generated/video_streaming/media_header.js';
|
|
7
|
+
import { NextRequestPolicy } from '../../protos/generated/video_streaming/next_request_policy.js';
|
|
8
|
+
import { FormatInitializationMetadata } from '../../protos/generated/video_streaming/format_initialization_metadata.js';
|
|
9
|
+
import { SabrRedirect } from '../../protos/generated/video_streaming/sabr_redirect.js';
|
|
10
|
+
import { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
|
|
11
|
+
import { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
|
|
12
|
+
import { PlaybackCookie } from '../../protos/generated/video_streaming/playback_cookie.js';
|
|
13
|
+
const DEFAULT_QUALITY = QUALITY.HD720;
|
|
14
|
+
export class ServerAbrStream extends EventEmitterLike {
|
|
15
|
+
constructor(args) {
|
|
16
|
+
super();
|
|
17
|
+
this.initializedFormats = [];
|
|
18
|
+
this.formatsByKey = new Map();
|
|
19
|
+
this.headerIdToFormatKeyMap = new Map();
|
|
20
|
+
this.previousSequences = new Map();
|
|
21
|
+
this.fetchFunction = args.fetch || fetch;
|
|
22
|
+
this.serverAbrStreamingUrl = args.serverAbrStreamingUrl;
|
|
23
|
+
this.videoPlaybackUstreamerConfig = args.videoPlaybackUstreamerConfig;
|
|
24
|
+
this.poToken = args.poToken;
|
|
25
|
+
this.totalDurationMs = args.durationMs;
|
|
26
|
+
}
|
|
27
|
+
on(event, listener) {
|
|
28
|
+
super.on(event, listener);
|
|
29
|
+
}
|
|
30
|
+
once(event, listener) {
|
|
31
|
+
super.once(event, listener);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Initializes the server ABR stream with the provided options.
|
|
35
|
+
* @param args - The initialization options.
|
|
36
|
+
*/
|
|
37
|
+
async init(args) {
|
|
38
|
+
const { audioFormats, videoFormats, clientAbrState: initialState } = args;
|
|
39
|
+
const firstVideoFormat = videoFormats ? videoFormats[0] : undefined;
|
|
40
|
+
const clientAbrState = {
|
|
41
|
+
lastManualDirection: 0,
|
|
42
|
+
timeSinceLastManualFormatSelectionMs: 0,
|
|
43
|
+
quality: videoFormats.length === 1 ? firstVideoFormat?.width : DEFAULT_QUALITY,
|
|
44
|
+
selectedQualityHeight: videoFormats.length === 1 ? firstVideoFormat?.width : DEFAULT_QUALITY,
|
|
45
|
+
startTimeMs: 0,
|
|
46
|
+
visibility: 0,
|
|
47
|
+
mediaType: ClientAbrState_MediaType.MEDIA_TYPE_DEFAULT,
|
|
48
|
+
...initialState
|
|
49
|
+
};
|
|
50
|
+
const audioFormatIds = audioFormats.map((fmt) => ({
|
|
51
|
+
itag: fmt.itag,
|
|
52
|
+
lastModified: parseInt(fmt.lastModified),
|
|
53
|
+
xtags: fmt.xtags
|
|
54
|
+
}));
|
|
55
|
+
const videoFormatIds = videoFormats.map((fmt) => ({
|
|
56
|
+
itag: fmt.itag,
|
|
57
|
+
lastModified: parseInt(fmt.lastModified),
|
|
58
|
+
xtags: fmt.xtags
|
|
59
|
+
}));
|
|
60
|
+
if (typeof clientAbrState.startTimeMs !== 'number')
|
|
61
|
+
throw new Error('Invalid media start time');
|
|
62
|
+
try {
|
|
63
|
+
while (clientAbrState.startTimeMs < this.totalDurationMs) {
|
|
64
|
+
const data = await this.fetchMedia({ clientAbrState, audioFormatIds, videoFormatIds });
|
|
65
|
+
this.emit('data', data);
|
|
66
|
+
if (data.sabrError)
|
|
67
|
+
break;
|
|
68
|
+
const mainFormat = clientAbrState.mediaType === ClientAbrState_MediaType.MEDIA_TYPE_DEFAULT
|
|
69
|
+
? data.initializedFormats.find((fmt) => fmt.mimeType?.includes('video'))
|
|
70
|
+
: data.initializedFormats[0];
|
|
71
|
+
for (const fmt of data.initializedFormats) {
|
|
72
|
+
this.previousSequences.set(`${fmt.formatId.itag};${fmt.formatId.lastModified};`, fmt.sequenceList.map((seq) => seq.sequenceNumber || 0));
|
|
73
|
+
}
|
|
74
|
+
if (!mainFormat ||
|
|
75
|
+
mainFormat.sequenceCount ===
|
|
76
|
+
mainFormat.sequenceList[mainFormat.sequenceList.length - 1]?.sequenceNumber) {
|
|
77
|
+
this.emit('end', data);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
clientAbrState.startTimeMs += mainFormat.sequenceList.reduce((acc, seq) => acc + (seq.durationMs || 0), 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
this.emit('error', error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async fetchMedia(args) {
|
|
88
|
+
const { clientAbrState, audioFormatIds, videoFormatIds } = args;
|
|
89
|
+
const body = VideoPlaybackAbrRequest.encode({
|
|
90
|
+
clientAbrState: clientAbrState,
|
|
91
|
+
selectedAudioFormatIds: audioFormatIds,
|
|
92
|
+
selectedVideoFormatIds: videoFormatIds,
|
|
93
|
+
selectedFormatIds: this.initializedFormats.map((fmt) => fmt.formatId),
|
|
94
|
+
videoPlaybackUstreamerConfig: base64ToU8(this.videoPlaybackUstreamerConfig),
|
|
95
|
+
streamerContext: {
|
|
96
|
+
field5: [],
|
|
97
|
+
field6: [],
|
|
98
|
+
poToken: this.poToken ? base64ToU8(this.poToken) : undefined,
|
|
99
|
+
playbackCookie: this.playbackCookie ? PlaybackCookie.encode(this.playbackCookie).finish() : undefined,
|
|
100
|
+
clientInfo: {
|
|
101
|
+
clientName: 1,
|
|
102
|
+
clientVersion: '2.2040620.05.00',
|
|
103
|
+
osName: 'Windows',
|
|
104
|
+
osVersion: '10.0'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
bufferedRanges: this.initializedFormats.map((fmt) => fmt._state),
|
|
108
|
+
field1000: []
|
|
109
|
+
}).finish();
|
|
110
|
+
const response = await this.fetchFunction(this.serverAbrStreamingUrl, { method: 'POST', body });
|
|
111
|
+
const data = await response.arrayBuffer();
|
|
112
|
+
if (response.status !== 200 || !data.byteLength)
|
|
113
|
+
throw new Error(`Received an invalid response from the server: ${response.status}`);
|
|
114
|
+
return this.parseUMPResponse(new Uint8Array(data));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Parses the UMP response data and updates the initialized formats.
|
|
118
|
+
* @param response - The UMP response data as a byte array.
|
|
119
|
+
*/
|
|
120
|
+
async parseUMPResponse(response) {
|
|
121
|
+
this.headerIdToFormatKeyMap.clear();
|
|
122
|
+
this.initializedFormats.forEach((format) => {
|
|
123
|
+
format.sequenceList = [];
|
|
124
|
+
format.mediaChunks = [];
|
|
125
|
+
});
|
|
126
|
+
let sabrError;
|
|
127
|
+
let sabrRedirect;
|
|
128
|
+
let streamProtectionStatus;
|
|
129
|
+
const ump = new UMP(new ChunkedDataBuffer([response]));
|
|
130
|
+
ump.parse((part) => {
|
|
131
|
+
const data = part.data.chunks[0];
|
|
132
|
+
switch (part.type) {
|
|
133
|
+
case PART.MEDIA_HEADER:
|
|
134
|
+
this.processMediaHeader(data);
|
|
135
|
+
break;
|
|
136
|
+
case PART.MEDIA:
|
|
137
|
+
this.processMediaData(part.data);
|
|
138
|
+
break;
|
|
139
|
+
case PART.MEDIA_END:
|
|
140
|
+
this.processEndOfMedia(part.data);
|
|
141
|
+
break;
|
|
142
|
+
case PART.NEXT_REQUEST_POLICY:
|
|
143
|
+
this.processNextRequestPolicy(data);
|
|
144
|
+
break;
|
|
145
|
+
case PART.FORMAT_INITIALIZATION_METADATA:
|
|
146
|
+
this.processFormatInitialization(data);
|
|
147
|
+
break;
|
|
148
|
+
case PART.SABR_ERROR:
|
|
149
|
+
sabrError = SabrError.decode(data);
|
|
150
|
+
break;
|
|
151
|
+
case PART.SABR_REDIRECT:
|
|
152
|
+
sabrRedirect = this.processSabrRedirect(data);
|
|
153
|
+
break;
|
|
154
|
+
case PART.STREAM_PROTECTION_STATUS:
|
|
155
|
+
streamProtectionStatus = StreamProtectionStatus.decode(data);
|
|
156
|
+
break;
|
|
157
|
+
default:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
initializedFormats: this.initializedFormats,
|
|
163
|
+
streamProtectionStatus,
|
|
164
|
+
sabrRedirect,
|
|
165
|
+
sabrError
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
processMediaHeader(data) {
|
|
169
|
+
const mediaHeader = MediaHeader.decode(data);
|
|
170
|
+
if (!mediaHeader.formatId)
|
|
171
|
+
return;
|
|
172
|
+
const formatKey = getFormatKey(mediaHeader.formatId);
|
|
173
|
+
let currentFormat = this.formatsByKey.get(formatKey);
|
|
174
|
+
if (!currentFormat) {
|
|
175
|
+
this.initializedFormats.push({
|
|
176
|
+
formatId: mediaHeader.formatId,
|
|
177
|
+
formatKey,
|
|
178
|
+
durationMs: mediaHeader.durationMs,
|
|
179
|
+
mimeType: undefined,
|
|
180
|
+
sequenceCount: undefined,
|
|
181
|
+
sequenceList: [],
|
|
182
|
+
mediaChunks: [],
|
|
183
|
+
_state: {
|
|
184
|
+
formatId: mediaHeader.formatId,
|
|
185
|
+
startTimeMs: 0,
|
|
186
|
+
durationMs: 0,
|
|
187
|
+
startSegmentIndex: 1,
|
|
188
|
+
endSegmentIndex: 0
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
this.formatsByKey.set(formatKey, this.initializedFormats[this.initializedFormats.length - 1]);
|
|
192
|
+
currentFormat = this.formatsByKey.get(formatKey);
|
|
193
|
+
}
|
|
194
|
+
// 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).
|
|
195
|
+
if (mediaHeader.sequenceNumber !== undefined && this.previousSequences.get(formatKey)?.includes(mediaHeader.sequenceNumber))
|
|
196
|
+
return;
|
|
197
|
+
// Save the header's ID so we can identify its stream data later.
|
|
198
|
+
if (mediaHeader.headerId !== undefined) {
|
|
199
|
+
if (!this.headerIdToFormatKeyMap.has(mediaHeader.headerId)) {
|
|
200
|
+
this.headerIdToFormatKeyMap.set(mediaHeader.headerId, formatKey);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!currentFormat.sequenceList.some((seq) => seq.sequenceNumber === (mediaHeader.sequenceNumber || 0))) {
|
|
204
|
+
currentFormat.sequenceList.push({
|
|
205
|
+
itag: mediaHeader.itag,
|
|
206
|
+
formatId: mediaHeader.formatId,
|
|
207
|
+
isInitSegment: mediaHeader.isInitSeg,
|
|
208
|
+
durationMs: mediaHeader.durationMs,
|
|
209
|
+
startMs: mediaHeader.startMs,
|
|
210
|
+
startDataRange: mediaHeader.startDataRange,
|
|
211
|
+
sequenceNumber: mediaHeader.sequenceNumber,
|
|
212
|
+
contentLength: mediaHeader.contentLength,
|
|
213
|
+
timeRange: mediaHeader.timeRange
|
|
214
|
+
});
|
|
215
|
+
if (typeof mediaHeader.sequenceNumber === 'number') {
|
|
216
|
+
currentFormat._state.durationMs += mediaHeader.durationMs || 0;
|
|
217
|
+
currentFormat._state.endSegmentIndex += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
processMediaData(data) {
|
|
222
|
+
const headerId = data.getUint8(0);
|
|
223
|
+
const streamData = data.split(1).remainingBuffer;
|
|
224
|
+
const formatKey = this.headerIdToFormatKeyMap.get(headerId);
|
|
225
|
+
if (!formatKey)
|
|
226
|
+
return;
|
|
227
|
+
const currentFormat = this.formatsByKey.get(formatKey);
|
|
228
|
+
if (!currentFormat)
|
|
229
|
+
return;
|
|
230
|
+
currentFormat.mediaChunks.push(streamData.chunks[0]);
|
|
231
|
+
}
|
|
232
|
+
processEndOfMedia(data) {
|
|
233
|
+
const headerId = data.getUint8(0);
|
|
234
|
+
this.headerIdToFormatKeyMap.delete(headerId);
|
|
235
|
+
}
|
|
236
|
+
processNextRequestPolicy(data) {
|
|
237
|
+
const nextRequestPolicy = NextRequestPolicy.decode(data);
|
|
238
|
+
this.playbackCookie = nextRequestPolicy.playbackCookie;
|
|
239
|
+
}
|
|
240
|
+
processFormatInitialization(data) {
|
|
241
|
+
const formatInitializationMetadata = FormatInitializationMetadata.decode(data);
|
|
242
|
+
if (!formatInitializationMetadata.formatId)
|
|
243
|
+
return;
|
|
244
|
+
const formatKey = getFormatKey(formatInitializationMetadata.formatId);
|
|
245
|
+
if (!this.formatsByKey.has(formatKey)) {
|
|
246
|
+
this.initializedFormats.push({
|
|
247
|
+
formatId: formatInitializationMetadata.formatId,
|
|
248
|
+
formatKey: getFormatKey(formatInitializationMetadata.formatId),
|
|
249
|
+
durationMs: formatInitializationMetadata.durationMs,
|
|
250
|
+
mimeType: formatInitializationMetadata.mimeType,
|
|
251
|
+
sequenceCount: formatInitializationMetadata.field4,
|
|
252
|
+
sequenceList: [],
|
|
253
|
+
mediaChunks: [],
|
|
254
|
+
_state: {
|
|
255
|
+
formatId: formatInitializationMetadata.formatId,
|
|
256
|
+
startTimeMs: 0,
|
|
257
|
+
durationMs: 0,
|
|
258
|
+
startSegmentIndex: 1,
|
|
259
|
+
endSegmentIndex: 0
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
this.formatsByKey.set(formatKey, this.initializedFormats[this.initializedFormats.length - 1]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
processSabrRedirect(data) {
|
|
266
|
+
const sabrRedirect = SabrRedirect.decode(data);
|
|
267
|
+
if (!sabrRedirect.url)
|
|
268
|
+
throw new Error('Invalid SABR redirect');
|
|
269
|
+
this.serverAbrStreamingUrl = sabrRedirect.url;
|
|
270
|
+
return sabrRedirect;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Part } from '../index.js';
|
|
2
|
+
import type { ChunkedDataBuffer } from './ChunkedDataBuffer.js';
|
|
3
|
+
export declare class UMP {
|
|
4
|
+
private chunkedDataBuffer;
|
|
5
|
+
constructor(chunkedDataBuffer: ChunkedDataBuffer);
|
|
6
|
+
parse(handlePart: (part: Part) => void): void;
|
|
7
|
+
readVarInt(offset: number): [number, number];
|
|
8
|
+
canReadFromCurrentChunk(offset: number, length: number): boolean;
|
|
9
|
+
getCurrentDataView(): DataView;
|
|
10
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export class UMP {
|
|
2
|
+
constructor(chunkedDataBuffer) {
|
|
3
|
+
this.chunkedDataBuffer = chunkedDataBuffer;
|
|
4
|
+
}
|
|
5
|
+
parse(handlePart) {
|
|
6
|
+
while (true) {
|
|
7
|
+
let offset = 0;
|
|
8
|
+
const [partType, newOffset] = this.readVarInt(offset);
|
|
9
|
+
offset = newOffset;
|
|
10
|
+
const [partSize, finalOffset] = this.readVarInt(offset);
|
|
11
|
+
offset = finalOffset;
|
|
12
|
+
if (partType < 0 || partSize < 0)
|
|
13
|
+
break;
|
|
14
|
+
// Note that we don't handle cases like this YET..
|
|
15
|
+
if (!this.chunkedDataBuffer.canReadBytes(offset, partSize))
|
|
16
|
+
break;
|
|
17
|
+
const splitResult = this.chunkedDataBuffer.split(offset).remainingBuffer.split(partSize);
|
|
18
|
+
offset = 0;
|
|
19
|
+
handlePart({
|
|
20
|
+
type: partType,
|
|
21
|
+
size: partSize,
|
|
22
|
+
data: splitResult.extractedBuffer
|
|
23
|
+
});
|
|
24
|
+
this.chunkedDataBuffer = splitResult.remainingBuffer;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
readVarInt(offset) {
|
|
28
|
+
let byteLength;
|
|
29
|
+
if (this.chunkedDataBuffer.canReadBytes(offset, 1)) {
|
|
30
|
+
const firstByte = this.chunkedDataBuffer.getUint8(offset);
|
|
31
|
+
// Determine the length of the val
|
|
32
|
+
if (firstByte < 128) {
|
|
33
|
+
byteLength = 1;
|
|
34
|
+
}
|
|
35
|
+
else if (firstByte < 192) {
|
|
36
|
+
byteLength = 2;
|
|
37
|
+
}
|
|
38
|
+
else if (firstByte < 224) {
|
|
39
|
+
byteLength = 3;
|
|
40
|
+
}
|
|
41
|
+
else if (firstByte < 240) {
|
|
42
|
+
byteLength = 4;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
byteLength = 5;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
byteLength = 0;
|
|
50
|
+
}
|
|
51
|
+
if (byteLength < 1 || !this.chunkedDataBuffer.canReadBytes(offset, byteLength)) {
|
|
52
|
+
return [-1, offset];
|
|
53
|
+
}
|
|
54
|
+
let value;
|
|
55
|
+
// Now read it based on the length
|
|
56
|
+
switch (byteLength) {
|
|
57
|
+
case 1:
|
|
58
|
+
value = this.chunkedDataBuffer.getUint8(offset++);
|
|
59
|
+
break;
|
|
60
|
+
case 2: {
|
|
61
|
+
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
|
|
62
|
+
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
|
|
63
|
+
value = (byte1 & 0x3f) + 64 * byte2;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 3: {
|
|
67
|
+
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
|
|
68
|
+
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
|
|
69
|
+
const byte3 = this.chunkedDataBuffer.getUint8(offset++);
|
|
70
|
+
value = (byte1 & 0x1f) + 32 * (byte2 + 256 * byte3);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 4: {
|
|
74
|
+
const byte1 = this.chunkedDataBuffer.getUint8(offset++);
|
|
75
|
+
const byte2 = this.chunkedDataBuffer.getUint8(offset++);
|
|
76
|
+
const byte3 = this.chunkedDataBuffer.getUint8(offset++);
|
|
77
|
+
const byte4 = this.chunkedDataBuffer.getUint8(offset++);
|
|
78
|
+
value = (byte1 & 0x0f) + 16 * (byte2 + 256 * (byte3 + 256 * byte4));
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
default: {
|
|
82
|
+
const tempOffset = offset + 1;
|
|
83
|
+
this.chunkedDataBuffer.focus(tempOffset);
|
|
84
|
+
if (this.canReadFromCurrentChunk(tempOffset, 4)) {
|
|
85
|
+
value = this.getCurrentDataView().getUint32(tempOffset - this.chunkedDataBuffer.currentChunkOffset, true);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const byte3 = this.chunkedDataBuffer.getUint8(tempOffset + 2) + 256 * this.chunkedDataBuffer.getUint8(tempOffset + 3);
|
|
89
|
+
value =
|
|
90
|
+
this.chunkedDataBuffer.getUint8(tempOffset) +
|
|
91
|
+
256 * (this.chunkedDataBuffer.getUint8(tempOffset + 1) + 256 * byte3);
|
|
92
|
+
}
|
|
93
|
+
offset += 5;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [value, offset];
|
|
98
|
+
}
|
|
99
|
+
canReadFromCurrentChunk(offset, length) {
|
|
100
|
+
this.chunkedDataBuffer.isFocused(offset);
|
|
101
|
+
return offset - this.chunkedDataBuffer.currentChunkOffset + length <= this.chunkedDataBuffer.chunks[this.chunkedDataBuffer.currentChunkIndex].length;
|
|
102
|
+
}
|
|
103
|
+
getCurrentDataView() {
|
|
104
|
+
if (!this.chunkedDataBuffer.currentDataView) {
|
|
105
|
+
const currentChunk = this.chunkedDataBuffer.chunks[this.chunkedDataBuffer.currentChunkIndex];
|
|
106
|
+
this.chunkedDataBuffer.currentDataView = new DataView(currentChunk.buffer, currentChunk.byteOffset, currentChunk.length);
|
|
107
|
+
}
|
|
108
|
+
return this.chunkedDataBuffer.currentDataView;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class EventEmitterLike extends EventTarget {
|
|
2
|
+
#private;
|
|
3
|
+
constructor();
|
|
4
|
+
emit(type: string, ...args: any[]): void;
|
|
5
|
+
on(type: string, listener: (...args: any[]) => void): void;
|
|
6
|
+
once(type: string, listener: (...args: any[]) => void): void;
|
|
7
|
+
off(type: string, listener: (...args: any[]) => void): void;
|
|
8
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _EventEmitterLike_legacy_listeners;
|
|
7
|
+
import { CustomEvent } from './index.js';
|
|
8
|
+
export class EventEmitterLike extends EventTarget {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
_EventEmitterLike_legacy_listeners.set(this, new Map());
|
|
12
|
+
}
|
|
13
|
+
emit(type, ...args) {
|
|
14
|
+
const event = new CustomEvent(type, { detail: args });
|
|
15
|
+
this.dispatchEvent(event);
|
|
16
|
+
}
|
|
17
|
+
on(type, listener) {
|
|
18
|
+
const wrapper = (ev) => {
|
|
19
|
+
if (ev instanceof CustomEvent) {
|
|
20
|
+
listener(...ev.detail);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
listener(ev);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
__classPrivateFieldGet(this, _EventEmitterLike_legacy_listeners, "f").set(listener, wrapper);
|
|
27
|
+
this.addEventListener(type, wrapper);
|
|
28
|
+
}
|
|
29
|
+
once(type, listener) {
|
|
30
|
+
const wrapper = (ev) => {
|
|
31
|
+
if (ev instanceof CustomEvent) {
|
|
32
|
+
listener(...ev.detail);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
listener(ev);
|
|
36
|
+
}
|
|
37
|
+
this.off(type, listener);
|
|
38
|
+
};
|
|
39
|
+
__classPrivateFieldGet(this, _EventEmitterLike_legacy_listeners, "f").set(listener, wrapper);
|
|
40
|
+
this.addEventListener(type, wrapper);
|
|
41
|
+
}
|
|
42
|
+
off(type, listener) {
|
|
43
|
+
const wrapper = __classPrivateFieldGet(this, _EventEmitterLike_legacy_listeners, "f").get(listener);
|
|
44
|
+
if (wrapper) {
|
|
45
|
+
this.removeEventListener(type, wrapper);
|
|
46
|
+
__classPrivateFieldGet(this, _EventEmitterLike_legacy_listeners, "f").delete(listener);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_EventEmitterLike_legacy_listeners = new WeakMap();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { FormatInitializationMetadata } from '../../protos/generated/video_streaming/format_initialization_metadata.js';
|
|
2
|
+
export { BufferedRange } from '../../protos/generated/video_streaming/buffered_range.js';
|
|
3
|
+
export { MediaHeader } from '../../protos/generated/video_streaming/media_header.js';
|
|
4
|
+
export { NextRequestPolicy } from '../../protos/generated/video_streaming/next_request_policy.js';
|
|
5
|
+
export { PlaybackCookie } from '../../protos/generated/video_streaming/playback_cookie.js';
|
|
6
|
+
export { PlaybackStartPolicy } from '../../protos/generated/video_streaming/playback_start_policy.js';
|
|
7
|
+
export { RequestCancellationPolicy } from '../../protos/generated/video_streaming/request_cancellation_policy.js';
|
|
8
|
+
export { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
|
|
9
|
+
export { SabrRedirect } from '../../protos/generated/video_streaming/sabr_redirect.js';
|
|
10
|
+
export { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
|
|
11
|
+
export { VideoPlaybackAbrRequest } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
|
|
12
|
+
export { OnesieRequest } from '../../protos/generated/video_streaming/onesie_request.js';
|
|
13
|
+
export { EncryptedPlayerRequest } from '../../protos/generated/video_streaming/encrypted_player_request.js';
|
|
14
|
+
export { OnesieHeader } from '../../protos/generated/video_streaming/onesie_header.js';
|
|
15
|
+
export { OnesieHeaderType } from '../../protos/generated/video_streaming/onesie_header_type.js';
|
|
16
|
+
export { OnesiePlayerRequest } from '../../protos/generated/video_streaming/onesie_player_request.js';
|
|
17
|
+
export { OnesiePlayerResponse } from '../../protos/generated/video_streaming/onesie_player_response.js';
|
|
18
|
+
export { ClientAbrState } from '../../protos/generated/video_streaming/client_abr_state.js';
|
|
19
|
+
export { StreamerContext } from '../../protos/generated/video_streaming/streamer_context.js';
|
|
20
|
+
export { ProxyStatus } from '../../protos/generated/video_streaming/proxy_status.js';
|
|
21
|
+
export { MediaCapabilities } from '../../protos/generated/video_streaming/media_capabilities.js';
|
|
22
|
+
export { CryptoParams } from '../../protos/generated/video_streaming/crypto_params.js';
|
|
23
|
+
export { ClientAbrState_MediaType as MediaType } from '../../protos/generated/video_streaming/client_abr_state.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { FormatInitializationMetadata } from '../../protos/generated/video_streaming/format_initialization_metadata.js';
|
|
2
|
+
export { BufferedRange } from '../../protos/generated/video_streaming/buffered_range.js';
|
|
3
|
+
export { MediaHeader } from '../../protos/generated/video_streaming/media_header.js';
|
|
4
|
+
export { NextRequestPolicy } from '../../protos/generated/video_streaming/next_request_policy.js';
|
|
5
|
+
export { PlaybackCookie } from '../../protos/generated/video_streaming/playback_cookie.js';
|
|
6
|
+
export { PlaybackStartPolicy } from '../../protos/generated/video_streaming/playback_start_policy.js';
|
|
7
|
+
export { RequestCancellationPolicy } from '../../protos/generated/video_streaming/request_cancellation_policy.js';
|
|
8
|
+
export { SabrError } from '../../protos/generated/video_streaming/sabr_error.js';
|
|
9
|
+
export { SabrRedirect } from '../../protos/generated/video_streaming/sabr_redirect.js';
|
|
10
|
+
export { StreamProtectionStatus } from '../../protos/generated/video_streaming/stream_protection_status.js';
|
|
11
|
+
export { VideoPlaybackAbrRequest } from '../../protos/generated/video_streaming/video_playback_abr_request.js';
|
|
12
|
+
export { OnesieRequest } from '../../protos/generated/video_streaming/onesie_request.js';
|
|
13
|
+
export { EncryptedPlayerRequest } from '../../protos/generated/video_streaming/encrypted_player_request.js';
|
|
14
|
+
export { OnesieHeader } from '../../protos/generated/video_streaming/onesie_header.js';
|
|
15
|
+
export { OnesieHeaderType } from '../../protos/generated/video_streaming/onesie_header_type.js';
|
|
16
|
+
export { OnesiePlayerRequest } from '../../protos/generated/video_streaming/onesie_player_request.js';
|
|
17
|
+
export { OnesiePlayerResponse } from '../../protos/generated/video_streaming/onesie_player_response.js';
|
|
18
|
+
export { ClientAbrState } from '../../protos/generated/video_streaming/client_abr_state.js';
|
|
19
|
+
export { StreamerContext } from '../../protos/generated/video_streaming/streamer_context.js';
|
|
20
|
+
export { ProxyStatus } from '../../protos/generated/video_streaming/proxy_status.js';
|
|
21
|
+
export { MediaCapabilities } from '../../protos/generated/video_streaming/media_capabilities.js';
|
|
22
|
+
export { CryptoParams } from '../../protos/generated/video_streaming/crypto_params.js';
|
|
23
|
+
export { ClientAbrState_MediaType as MediaType } from '../../protos/generated/video_streaming/client_abr_state.js';
|