p2p-media-loader-core
Version:
P2P Media Loader core functionality
255 lines (223 loc) • 7.55 kB
text/typescript
import * as Serialization from "./binary-serialization.js";
import { PeerCommandType, PeerCommand } from "./types.js";
const FRAME_PART_LENGTH = 4;
const commandFrameStart = stringToUtf8CodesBuffer("cstr", FRAME_PART_LENGTH);
const commandFrameEnd = stringToUtf8CodesBuffer("cend", FRAME_PART_LENGTH);
const commandDivFrameStart = stringToUtf8CodesBuffer("dstr", FRAME_PART_LENGTH);
const commandDivFrameEnd = stringToUtf8CodesBuffer("dend", FRAME_PART_LENGTH);
const startFrames = [commandFrameStart, commandDivFrameStart];
const endFrames = [commandFrameEnd, commandDivFrameEnd];
const commandFramesLength = commandFrameStart.length + commandFrameEnd.length;
export function isCommandChunk(buffer: Uint8Array) {
const { length } = commandFrameStart;
const bufferEndingToCompare = buffer.slice(-length);
return (
startFrames.some((frame) =>
areBuffersEqual(buffer, frame, FRAME_PART_LENGTH),
) &&
endFrames.some((frame) =>
areBuffersEqual(bufferEndingToCompare, frame, FRAME_PART_LENGTH),
)
);
}
function isFirstCommandChunk(buffer: Uint8Array) {
return areBuffersEqual(buffer, commandFrameStart, FRAME_PART_LENGTH);
}
function isLastCommandChunk(buffer: Uint8Array) {
return areBuffersEqual(
buffer.slice(-FRAME_PART_LENGTH),
commandFrameEnd,
FRAME_PART_LENGTH,
);
}
export class BinaryCommandJoiningError extends Error {
constructor(readonly type: "incomplete-joining" | "no-first-chunk") {
super();
}
}
export class BinaryCommandChunksJoiner {
private readonly chunks = new Serialization.ResizableUint8Array();
private status: "joining" | "completed" = "joining";
constructor(
private readonly onComplete: (commandBuffer: Uint8Array) => void,
) {}
addCommandChunk(chunk: Uint8Array) {
if (this.status === "completed") return;
const isFirstChunk = isFirstCommandChunk(chunk);
if (!this.chunks.length && !isFirstChunk) {
throw new BinaryCommandJoiningError("no-first-chunk");
}
if (this.chunks.length && isFirstChunk) {
throw new BinaryCommandJoiningError("incomplete-joining");
}
this.chunks.push(this.unframeCommandChunk(chunk));
if (!isLastCommandChunk(chunk)) return;
this.status = "completed";
this.onComplete(this.chunks.getBuffer());
}
private unframeCommandChunk(chunk: Uint8Array) {
return chunk.slice(FRAME_PART_LENGTH, chunk.length - FRAME_PART_LENGTH);
}
}
export class BinaryCommandCreator {
private readonly bytes = new Serialization.ResizableUint8Array();
private resultBuffers: Uint8Array[] = [];
private status: "creating" | "completed" = "creating";
constructor(
commandType: PeerCommandType,
private readonly maxChunkLength: number,
) {
this.bytes.push(commandType);
}
addInteger(name: string, value: number) {
this.bytes.push(name.charCodeAt(0));
const bytes = Serialization.serializeInt(BigInt(value));
this.bytes.push(bytes);
}
addSimilarIntArr(name: string, arr: number[]) {
this.bytes.push(name.charCodeAt(0));
const bytes = Serialization.serializeSimilarIntArray(
arr.map((num) => BigInt(num)),
);
this.bytes.push(bytes);
}
addString(name: string, string: string) {
this.bytes.push(name.charCodeAt(0));
const bytes = Serialization.serializeString(string);
this.bytes.push(bytes);
}
complete() {
if (!this.bytes.length) throw new Error("Buffer is empty");
if (this.status === "completed") return;
this.status = "completed";
const unframedBuffer = this.bytes.getBuffer();
if (unframedBuffer.length + commandFramesLength <= this.maxChunkLength) {
this.resultBuffers.push(
frameBuffer(unframedBuffer, commandFrameStart, commandFrameEnd),
);
return;
}
let chunksCount = Math.ceil(unframedBuffer.length / this.maxChunkLength);
if (
Math.ceil(unframedBuffer.length / chunksCount) + commandFramesLength >
this.maxChunkLength
) {
chunksCount++;
}
for (const [i, chunk] of splitBufferToEqualChunks(
unframedBuffer,
chunksCount,
)) {
if (i === 0) {
this.resultBuffers.push(
frameBuffer(chunk, commandFrameStart, commandDivFrameEnd),
);
} else if (i === chunksCount - 1) {
this.resultBuffers.push(
frameBuffer(chunk, commandDivFrameStart, commandFrameEnd),
);
} else {
this.resultBuffers.push(
frameBuffer(chunk, commandDivFrameStart, commandDivFrameEnd),
);
}
}
}
getResultBuffers(): Uint8Array[] {
if (this.status === "creating" || !this.resultBuffers.length) {
throw new Error("Command is not complete.");
}
return this.resultBuffers;
}
}
export function deserializeCommand(bytes: Uint8Array): PeerCommand {
const [commandCode] = bytes;
const deserializedCommand: Record<string, unknown> = {
c: commandCode,
};
let offset = 1;
while (offset < bytes.length) {
const name = String.fromCharCode(bytes[offset]);
offset++;
const dataType = getDataTypeFromByte(bytes[offset]);
switch (dataType) {
case Serialization.SerializedItem.Int:
{
const { number, byteLength } = Serialization.deserializeInt(
bytes.slice(offset),
);
deserializedCommand[name] = Number(number);
offset += byteLength;
}
break;
case Serialization.SerializedItem.SimilarIntArray:
{
const { numbers, byteLength } =
Serialization.deserializeSimilarIntArray(bytes.slice(offset));
deserializedCommand[name] = numbers.map((n) => Number(n));
offset += byteLength;
}
break;
case Serialization.SerializedItem.String:
{
const { string, byteLength } = Serialization.deserializeString(
bytes.slice(offset),
);
deserializedCommand[name] = string;
offset += byteLength;
}
break;
}
}
return deserializedCommand as unknown as PeerCommand;
}
function getDataTypeFromByte(byte: number): Serialization.SerializedItem {
const typeCode: Serialization.SerializedItem = byte >> 4;
if (
typeCode <= Serialization.SerializedItem.Min ||
typeCode >= Serialization.SerializedItem.Max
) {
throw new Error("Not existing type");
}
return typeCode;
}
function stringToUtf8CodesBuffer(string: string, length?: number): Uint8Array {
if (length && string.length !== length) {
throw new Error("Wrong string length");
}
const buffer = new Uint8Array(length ?? string.length);
for (let i = 0; i < string.length; i++) buffer[i] = string.charCodeAt(i);
return buffer;
}
function* splitBufferToEqualChunks(
buffer: Uint8Array,
chunksCount: number,
): Generator<[number, Uint8Array], void> {
const chunkLength = Math.ceil(buffer.length / chunksCount);
for (let i = 0; i < chunksCount; i++) {
yield [i, buffer.slice(i * chunkLength, (i + 1) * chunkLength)];
}
}
function frameBuffer(
buffer: Uint8Array,
frameStart: Uint8Array,
frameEnd: Uint8Array,
) {
const result = new Uint8Array(
buffer.length + frameStart.length + frameEnd.length,
);
result.set(frameStart);
result.set(buffer, frameStart.length);
result.set(frameEnd, frameStart.length + buffer.length);
return result;
}
function areBuffersEqual(
buffer1: Uint8Array,
buffer2: Uint8Array,
length: number,
) {
for (let i = 0; i < length; i++) {
if (buffer1[i] !== buffer2[i]) return false;
}
return true;
}