mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 10:49:24 +02:00
feat(scrcpy): add some AV1 parsing
This commit is contained in:
parent
6e3114f613
commit
6cfd8c12d5
9 changed files with 991 additions and 100 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -8,6 +8,7 @@
|
||||||
"autorun",
|
"autorun",
|
||||||
"Backquote",
|
"Backquote",
|
||||||
"Bframes",
|
"Bframes",
|
||||||
|
"Bitstream",
|
||||||
"bootloader",
|
"bootloader",
|
||||||
"brotli",
|
"brotli",
|
||||||
"Callout",
|
"Callout",
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"Embedder",
|
"Embedder",
|
||||||
"entrypoints",
|
"entrypoints",
|
||||||
"fflate",
|
"fflate",
|
||||||
|
"flac",
|
||||||
"fluentui",
|
"fluentui",
|
||||||
"genymobile",
|
"genymobile",
|
||||||
"Genymobile's",
|
"Genymobile's",
|
||||||
|
@ -56,6 +58,7 @@
|
||||||
"streamsaver",
|
"streamsaver",
|
||||||
"struct",
|
"struct",
|
||||||
"struct's",
|
"struct's",
|
||||||
|
"subsampling",
|
||||||
"tcpip",
|
"tcpip",
|
||||||
"tinyh",
|
"tinyh",
|
||||||
"transferables",
|
"transferables",
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { EventEmitter } from "@yume-chan/event";
|
import { EventEmitter } from "@yume-chan/event";
|
||||||
import type {
|
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
|
||||||
ScrcpyMediaStreamDataPacket,
|
|
||||||
ScrcpyMediaStreamPacket,
|
|
||||||
} from "@yume-chan/scrcpy";
|
|
||||||
import {
|
import {
|
||||||
|
Av1,
|
||||||
ScrcpyVideoCodecId,
|
ScrcpyVideoCodecId,
|
||||||
h264ParseConfiguration,
|
h264ParseConfiguration,
|
||||||
h265ParseConfiguration,
|
h265ParseConfiguration,
|
||||||
|
type ScrcpyMediaStreamDataPacket,
|
||||||
|
type ScrcpyMediaStreamPacket,
|
||||||
} from "@yume-chan/scrcpy";
|
} from "@yume-chan/scrcpy";
|
||||||
import { getUint32LittleEndian } from "@yume-chan/no-data-view";
|
|
||||||
import type {
|
import type {
|
||||||
ScrcpyVideoDecoder,
|
ScrcpyVideoDecoder,
|
||||||
ScrcpyVideoDecoderCapability,
|
ScrcpyVideoDecoderCapability,
|
||||||
|
@ -19,11 +18,19 @@ import { BitmapFrameRenderer } from "./bitmap.js";
|
||||||
import type { FrameRenderer } from "./renderer.js";
|
import type { FrameRenderer } from "./renderer.js";
|
||||||
import { WebGLFrameRenderer } from "./webgl.js";
|
import { WebGLFrameRenderer } from "./webgl.js";
|
||||||
|
|
||||||
function toHex(value: number) {
|
function hexDigits(value: number) {
|
||||||
return value.toString(16).padStart(2, "0").toUpperCase();
|
return value.toString(16).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebCodecsDecoder implements ScrcpyVideoDecoder {
|
function hexTwoDigits(value: number) {
|
||||||
|
return value.toString(16).toUpperCase().padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function decimalTwoDigits(value: number) {
|
||||||
|
return value.toString(10).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebCodecsVideoDecoder implements ScrcpyVideoDecoder {
|
||||||
static isSupported() {
|
static isSupported() {
|
||||||
return typeof globalThis.VideoDecoder !== "undefined";
|
return typeof globalThis.VideoDecoder !== "undefined";
|
||||||
}
|
}
|
||||||
|
@ -149,9 +156,9 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
|
||||||
// ISO Base Media File Format Name Space
|
// ISO Base Media File Format Name Space
|
||||||
const codec =
|
const codec =
|
||||||
"avc1." +
|
"avc1." +
|
||||||
toHex(profileIndex) +
|
hexTwoDigits(profileIndex) +
|
||||||
toHex(constraintSet) +
|
hexTwoDigits(constraintSet) +
|
||||||
toHex(levelIndex);
|
hexTwoDigits(levelIndex);
|
||||||
this.#decoder.configure({
|
this.#decoder.configure({
|
||||||
codec: codec,
|
codec: codec,
|
||||||
optimizeForLatency: true,
|
optimizeForLatency: true,
|
||||||
|
@ -181,16 +188,57 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
|
||||||
"hev1",
|
"hev1",
|
||||||
["", "A", "B", "C"][generalProfileSpace]! +
|
["", "A", "B", "C"][generalProfileSpace]! +
|
||||||
generalProfileIndex.toString(),
|
generalProfileIndex.toString(),
|
||||||
getUint32LittleEndian(generalProfileCompatibilitySet, 0).toString(
|
hexDigits(getUint32LittleEndian(generalProfileCompatibilitySet, 0)),
|
||||||
16,
|
|
||||||
),
|
|
||||||
(generalTierFlag ? "H" : "L") + generalLevelIndex.toString(),
|
(generalTierFlag ? "H" : "L") + generalLevelIndex.toString(),
|
||||||
getUint32LittleEndian(generalConstraintSet, 0)
|
...Array.from(generalConstraintSet, hexDigits),
|
||||||
.toString(16)
|
].join(".");
|
||||||
.toUpperCase(),
|
this.#decoder.configure({
|
||||||
getUint32LittleEndian(generalConstraintSet, 4)
|
codec,
|
||||||
.toString(16)
|
optimizeForLatency: true,
|
||||||
.toUpperCase(),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#configureAv1(data: Uint8Array) {
|
||||||
|
let sequenceHeader: Av1.SequenceHeaderObu | undefined;
|
||||||
|
const av1 = new Av1(data);
|
||||||
|
for (const obu of av1.bitstream()) {
|
||||||
|
if (obu.sequence_header_obu) {
|
||||||
|
sequenceHeader = obu.sequence_header_obu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sequenceHeader) {
|
||||||
|
throw new Error("No sequence header found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
seq_profile: seqProfile,
|
||||||
|
seq_level_idx: [seqLevelIdx = 0],
|
||||||
|
color_config: {
|
||||||
|
BitDepth,
|
||||||
|
mono_chrome: monoChrome,
|
||||||
|
subsampling_x: subsamplingX,
|
||||||
|
subsampling_y: subsamplingY,
|
||||||
|
chroma_sample_position: chromaSamplePosition,
|
||||||
|
color_primaries: colorPrimaries,
|
||||||
|
transfer_characteristics: transferCharacteristics,
|
||||||
|
matrix_coefficients: matrixCoefficients,
|
||||||
|
color_range: colorRange,
|
||||||
|
},
|
||||||
|
} = sequenceHeader;
|
||||||
|
const codec = [
|
||||||
|
"av01",
|
||||||
|
seqProfile.toString(16),
|
||||||
|
decimalTwoDigits(seqLevelIdx) +
|
||||||
|
(sequenceHeader.seq_tier[0] ? "H" : "M"),
|
||||||
|
decimalTwoDigits(BitDepth),
|
||||||
|
monoChrome ? "1" : "0",
|
||||||
|
(subsamplingX ? "1" : "0") +
|
||||||
|
(subsamplingY ? "1" : "0") +
|
||||||
|
chromaSamplePosition.toString(),
|
||||||
|
decimalTwoDigits(colorPrimaries),
|
||||||
|
decimalTwoDigits(transferCharacteristics),
|
||||||
|
decimalTwoDigits(matrixCoefficients),
|
||||||
|
colorRange ? "1" : "0",
|
||||||
].join(".");
|
].join(".");
|
||||||
this.#decoder.configure({
|
this.#decoder.configure({
|
||||||
codec,
|
codec,
|
||||||
|
@ -206,6 +254,9 @@ export class WebCodecsDecoder implements ScrcpyVideoDecoder {
|
||||||
case ScrcpyVideoCodecId.H265:
|
case ScrcpyVideoCodecId.H265:
|
||||||
this.#configureH265(data);
|
this.#configureH265(data);
|
||||||
break;
|
break;
|
||||||
|
case ScrcpyVideoCodecId.AV1:
|
||||||
|
this.#configureAv1(data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
this.#config = data;
|
this.#config = data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
// cspell: ignore uvlc
|
||||||
|
// cspell: ignore interintra
|
||||||
|
// cspell: ignore superres
|
||||||
|
// cspell: ignore cdef
|
||||||
|
// cspell: ignore bitdepth
|
||||||
|
// cspell: ignore Smpte
|
||||||
|
// cspell: ignore Chromat
|
||||||
|
|
||||||
export enum AndroidAv1Profile {
|
export enum AndroidAv1Profile {
|
||||||
Main8 = 1 << 0,
|
Main8 = 1 << 0,
|
||||||
Main10 = 1 << 1,
|
Main10 = 1 << 1,
|
||||||
|
@ -31,3 +39,643 @@ export enum AndroidAv1Level {
|
||||||
Level72 = 1 << 22,
|
Level72 = 1 << 22,
|
||||||
Level73 = 1 << 23,
|
Level73 = 1 << 23,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BitReader {
|
||||||
|
#data: Uint8Array;
|
||||||
|
#byte: number;
|
||||||
|
|
||||||
|
#bytePosition: number = 0;
|
||||||
|
#bitPosition: number = 7;
|
||||||
|
|
||||||
|
get byteAligned() {
|
||||||
|
return this.#bitPosition === 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ended() {
|
||||||
|
return this.#bytePosition >= this.#data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data: Uint8Array) {
|
||||||
|
this.#data = data;
|
||||||
|
this.#byte = data[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
f1() {
|
||||||
|
const value = this.#byte >> this.#bitPosition;
|
||||||
|
this.#bitPosition -= 1;
|
||||||
|
if (this.#bitPosition < 0) {
|
||||||
|
this.#bytePosition += 1;
|
||||||
|
this.#bitPosition = 7;
|
||||||
|
this.#byte = this.#data[this.#bytePosition]!;
|
||||||
|
}
|
||||||
|
return value & 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
f(n: number) {
|
||||||
|
let value = 0;
|
||||||
|
for (; n > 0; n -= 1) {
|
||||||
|
value <<= 1;
|
||||||
|
value |= this.f1();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(n: number) {
|
||||||
|
const bytes = (n / 8) | 0;
|
||||||
|
if (bytes > 0) {
|
||||||
|
this.#bytePosition += bytes;
|
||||||
|
n -= bytes * 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
n -= this.#bitPosition + 1;
|
||||||
|
this.#bytePosition += 1;
|
||||||
|
this.#bitPosition = 7 - n;
|
||||||
|
this.#byte = this.#data[this.#bytePosition]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
readBytes(n: number) {
|
||||||
|
if (!this.byteAligned) {
|
||||||
|
throw new Error("Bytes must be byte-aligned");
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.#data.subarray(
|
||||||
|
this.#bytePosition,
|
||||||
|
this.#bytePosition + n,
|
||||||
|
);
|
||||||
|
this.#bytePosition += n;
|
||||||
|
this.#byte = this.#data[this.#bytePosition]!;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition() {
|
||||||
|
return [this.#bytePosition, this.#bitPosition] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition([bytePosition, bitPosition]: readonly [number, number]) {
|
||||||
|
this.#bytePosition = bytePosition;
|
||||||
|
this.#bitPosition = bitPosition;
|
||||||
|
this.#byte = this.#data[bytePosition]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Av1 extends BitReader {
|
||||||
|
#Leb128Bytes: number = 0;
|
||||||
|
|
||||||
|
uvlc() {
|
||||||
|
let leadingZeros = 0;
|
||||||
|
while (!this.f1()) {
|
||||||
|
leadingZeros += 1;
|
||||||
|
}
|
||||||
|
if (leadingZeros >= 32) {
|
||||||
|
return 2 ** 32 - 1;
|
||||||
|
}
|
||||||
|
const value = this.f(leadingZeros);
|
||||||
|
return value + ((1 << leadingZeros) >>> 0) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
leb128() {
|
||||||
|
if (!this.byteAligned) {
|
||||||
|
throw new Error("LEB128 must be byte-aligned");
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = 0n;
|
||||||
|
this.#Leb128Bytes = 0;
|
||||||
|
for (let i = 0n; i < 8n; i += 1n) {
|
||||||
|
const leb128_byte = this.f(8);
|
||||||
|
value |= BigInt(leb128_byte & 0x7f) << (7n * i);
|
||||||
|
this.#Leb128Bytes += 1;
|
||||||
|
if ((leb128_byte & 0x80) == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
*bitstream(): Generator<Av1.OpenBitstreamUnit, void, void> {
|
||||||
|
while (!this.ended) {
|
||||||
|
const temporal_unit_size = this.leb128();
|
||||||
|
yield* this.temporalUnit(temporal_unit_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*temporalUnit(sz: bigint): Generator<Av1.OpenBitstreamUnit, void, void> {
|
||||||
|
while (sz > 0) {
|
||||||
|
const frame_unit_size = this.leb128();
|
||||||
|
sz -= BigInt(this.#Leb128Bytes);
|
||||||
|
yield* this.frameUnit(frame_unit_size);
|
||||||
|
sz -= frame_unit_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*frameUnit(sz: bigint): Generator<Av1.OpenBitstreamUnit, void, void> {
|
||||||
|
while (sz > 0) {
|
||||||
|
const obu_length = this.leb128();
|
||||||
|
sz -= BigInt(this.#Leb128Bytes);
|
||||||
|
const obu = this.openBitstreamUnit(obu_length);
|
||||||
|
if (obu) {
|
||||||
|
yield obu;
|
||||||
|
}
|
||||||
|
sz -= obu_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#OperatingPointIdc = 0;
|
||||||
|
|
||||||
|
openBitstreamUnit(sz: bigint) {
|
||||||
|
const obu_header = this.obuHeader();
|
||||||
|
let obu_size: bigint;
|
||||||
|
if (obu_header.obu_has_size_field) {
|
||||||
|
obu_size = this.leb128();
|
||||||
|
} else {
|
||||||
|
obu_size = sz - 1n - (obu_header.obu_extension_flag ? 1n : 0n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPosition = this.getPosition();
|
||||||
|
|
||||||
|
if (
|
||||||
|
obu_header.obu_type !== Av1.ObuType.SequenceHeader &&
|
||||||
|
obu_header.obu_type !== Av1.ObuType.TemporalDelimiter &&
|
||||||
|
this.#OperatingPointIdc !== 0 &&
|
||||||
|
obu_header.obu_extension_header
|
||||||
|
) {
|
||||||
|
const inTemporalLayer = !!(
|
||||||
|
this.#OperatingPointIdc &
|
||||||
|
(1 << obu_header.obu_extension_header.temporal_id)
|
||||||
|
);
|
||||||
|
const inSpatialLayer = !!(
|
||||||
|
this.#OperatingPointIdc &
|
||||||
|
(1 << (obu_header.obu_extension_header.spatial_id + 8))
|
||||||
|
);
|
||||||
|
if (!inTemporalLayer || !inSpatialLayer) {
|
||||||
|
this.skip(Number(obu_size));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sequence_header_obu:
|
||||||
|
| ReturnType<Av1["sequenceHeaderObu"]>
|
||||||
|
| undefined;
|
||||||
|
switch (obu_header.obu_type) {
|
||||||
|
case Av1.ObuType.SequenceHeader:
|
||||||
|
sequence_header_obu = this.sequenceHeaderObu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPosition = this.getPosition();
|
||||||
|
const payloadBits =
|
||||||
|
(currentPosition[0] - startPosition[0]) * 8 +
|
||||||
|
(startPosition[1] - currentPosition[1]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
obu_size > 0 &&
|
||||||
|
obu_header.obu_type !== Av1.ObuType.TileGroup &&
|
||||||
|
obu_header.obu_type !== Av1.ObuType.TileList &&
|
||||||
|
obu_header.obu_type !== Av1.ObuType.Frame
|
||||||
|
) {
|
||||||
|
this.skip(Number(obu_size) * 8 - payloadBits);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
obu_header,
|
||||||
|
obu_size,
|
||||||
|
sequence_header_obu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
obuHeader() {
|
||||||
|
const obu_forbidden_bit = !!this.f1();
|
||||||
|
if (obu_forbidden_bit) {
|
||||||
|
throw new Error("Invalid data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const obu_type = this.f(4);
|
||||||
|
const obu_extension_flag = !!this.f1();
|
||||||
|
const obu_has_size_field = !!this.f1();
|
||||||
|
this.f1();
|
||||||
|
|
||||||
|
let obu_extension_header:
|
||||||
|
| ReturnType<Av1["obuExtensionHeader"]>
|
||||||
|
| undefined;
|
||||||
|
if (obu_extension_flag) {
|
||||||
|
obu_extension_header = this.obuExtensionHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
obu_type,
|
||||||
|
obu_extension_flag,
|
||||||
|
obu_has_size_field,
|
||||||
|
obu_extension_header,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
obuExtensionHeader() {
|
||||||
|
const temporal_id = this.f(3);
|
||||||
|
const spatial_id = this.f(2);
|
||||||
|
this.skip(3);
|
||||||
|
return { temporal_id, spatial_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly SelectScreenContentTools = 2;
|
||||||
|
static readonly SelectIntegerMv = 2;
|
||||||
|
|
||||||
|
sequenceHeaderObu() {
|
||||||
|
const seq_profile = this.f(3);
|
||||||
|
const still_picture = !!this.f1();
|
||||||
|
const reduced_still_picture_header = !!this.f1();
|
||||||
|
|
||||||
|
let timing_info_present_flag = false;
|
||||||
|
let timing_info: ReturnType<Av1["timingInfo"]> | undefined;
|
||||||
|
let decoder_model_info_present_flag = false;
|
||||||
|
let decoder_model_info: ReturnType<Av1["decoderModelInfo"]> | undefined;
|
||||||
|
let initial_display_delay_present_flag = false;
|
||||||
|
let operating_points_cnt_minus_1 = 0;
|
||||||
|
const operating_point_idc: number[] = [];
|
||||||
|
const seq_level_idx: number[] = [];
|
||||||
|
const seq_tier: number[] = [];
|
||||||
|
const decoder_model_present_for_this_op: boolean[] = [];
|
||||||
|
const initial_display_delay_present_for_this_op: boolean[] = [];
|
||||||
|
let operating_parameters_info:
|
||||||
|
| ReturnType<Av1["operatingParametersInfo"]>[]
|
||||||
|
| undefined;
|
||||||
|
let initial_display_delay_minus_1: number[] | undefined;
|
||||||
|
if (reduced_still_picture_header) {
|
||||||
|
operating_point_idc[0] = 0;
|
||||||
|
seq_level_idx[0] = this.f(5);
|
||||||
|
seq_tier[0] = 0;
|
||||||
|
decoder_model_present_for_this_op[0] = false;
|
||||||
|
initial_display_delay_present_for_this_op[0] = false;
|
||||||
|
} else {
|
||||||
|
timing_info_present_flag = !!this.f1();
|
||||||
|
if (timing_info_present_flag) {
|
||||||
|
timing_info = this.timingInfo();
|
||||||
|
decoder_model_info_present_flag = !!this.f1();
|
||||||
|
if (decoder_model_info_present_flag) {
|
||||||
|
decoder_model_info = this.decoderModelInfo();
|
||||||
|
operating_parameters_info = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initial_display_delay_present_flag = !!this.f1();
|
||||||
|
if (initial_display_delay_present_flag) {
|
||||||
|
initial_display_delay_minus_1 = [];
|
||||||
|
}
|
||||||
|
operating_points_cnt_minus_1 = this.f(5);
|
||||||
|
for (let i = 0; i <= operating_points_cnt_minus_1; i += 1) {
|
||||||
|
operating_point_idc[i] = this.f(12);
|
||||||
|
seq_level_idx[i] = this.f(5);
|
||||||
|
if (seq_level_idx[i]! > 7) {
|
||||||
|
seq_tier[i] = this.f1();
|
||||||
|
} else {
|
||||||
|
seq_tier[i] = 0;
|
||||||
|
}
|
||||||
|
if (decoder_model_info_present_flag) {
|
||||||
|
decoder_model_present_for_this_op[i] = !!this.f1();
|
||||||
|
if (decoder_model_present_for_this_op[i]) {
|
||||||
|
operating_parameters_info![i] =
|
||||||
|
this.operatingParametersInfo(decoder_model_info!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
decoder_model_present_for_this_op[i] = false;
|
||||||
|
}
|
||||||
|
if (initial_display_delay_present_flag) {
|
||||||
|
initial_display_delay_present_for_this_op[i] = !!this.f1();
|
||||||
|
if (initial_display_delay_present_for_this_op[i]) {
|
||||||
|
initial_display_delay_minus_1![i] = this.f(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatingPoint = this.chooseOperatingPoint();
|
||||||
|
this.#OperatingPointIdc = operating_point_idc[operatingPoint]!;
|
||||||
|
|
||||||
|
const frame_width_bits_minus_1 = this.f(4);
|
||||||
|
const frame_height_bits_minus_1 = this.f(4);
|
||||||
|
const max_frame_width_minus_1 = this.f(frame_width_bits_minus_1 + 1);
|
||||||
|
const max_frame_height_minus_1 = this.f(frame_height_bits_minus_1 + 1);
|
||||||
|
|
||||||
|
let frame_id_numbers_present_flag = false;
|
||||||
|
let delta_frame_id_length_minus_2: number | undefined;
|
||||||
|
let additional_frame_id_length_minus_1: number | undefined;
|
||||||
|
if (!reduced_still_picture_header) {
|
||||||
|
frame_id_numbers_present_flag = !!this.f1();
|
||||||
|
if (frame_id_numbers_present_flag) {
|
||||||
|
delta_frame_id_length_minus_2 = this.f(4);
|
||||||
|
additional_frame_id_length_minus_1 = this.f(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const use_128x128_superblock = !!this.f1();
|
||||||
|
const enable_filter_intra = !!this.f1();
|
||||||
|
const enable_intra_edge_filter = !!this.f1();
|
||||||
|
|
||||||
|
let enable_interintra_compound = false;
|
||||||
|
let enable_masked_compound = false;
|
||||||
|
let enable_warped_motion = false;
|
||||||
|
let enable_dual_filter = false;
|
||||||
|
let enable_order_hint = false;
|
||||||
|
let enable_jnt_comp = false;
|
||||||
|
let enable_ref_frame_mvs = false;
|
||||||
|
let seq_choose_screen_content_tools = false;
|
||||||
|
let seq_force_screen_content_tools = Av1.SelectScreenContentTools;
|
||||||
|
let seq_choose_integer_mv = false;
|
||||||
|
let seq_force_integer_mv = Av1.SelectIntegerMv;
|
||||||
|
let order_hint_bits_minus_1: number | undefined;
|
||||||
|
// let OrderHintBits = 0;
|
||||||
|
if (!reduced_still_picture_header) {
|
||||||
|
enable_interintra_compound = !!this.f1();
|
||||||
|
enable_masked_compound = !!this.f1();
|
||||||
|
enable_warped_motion = !!this.f1();
|
||||||
|
enable_dual_filter = !!this.f1();
|
||||||
|
|
||||||
|
enable_order_hint = !!this.f1();
|
||||||
|
if (enable_order_hint) {
|
||||||
|
enable_jnt_comp = !!this.f1();
|
||||||
|
enable_ref_frame_mvs = !!this.f1();
|
||||||
|
}
|
||||||
|
|
||||||
|
seq_choose_screen_content_tools = !!this.f1();
|
||||||
|
if (!seq_choose_screen_content_tools) {
|
||||||
|
seq_force_screen_content_tools = this.f1();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seq_force_screen_content_tools > 0) {
|
||||||
|
seq_choose_integer_mv = !!this.f1();
|
||||||
|
if (!seq_choose_integer_mv) {
|
||||||
|
seq_force_integer_mv = this.f1();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enable_order_hint) {
|
||||||
|
order_hint_bits_minus_1 = this.f(3);
|
||||||
|
// OrderHintBits = order_hint_bits_minus_1 + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enable_superres = !!this.f1();
|
||||||
|
const enable_cdef = !!this.f1();
|
||||||
|
const enable_restoration = !!this.f1();
|
||||||
|
const color_config = this.colorConfig(seq_profile);
|
||||||
|
const film_grain_params_present = !!this.f1();
|
||||||
|
|
||||||
|
return {
|
||||||
|
seq_profile,
|
||||||
|
still_picture,
|
||||||
|
reduced_still_picture_header,
|
||||||
|
timing_info_present_flag,
|
||||||
|
timing_info,
|
||||||
|
decoder_model_info_present_flag,
|
||||||
|
decoder_model_info,
|
||||||
|
initial_display_delay_present_flag,
|
||||||
|
initial_display_delay_minus_1,
|
||||||
|
operating_points_cnt_minus_1,
|
||||||
|
operating_point_idc,
|
||||||
|
seq_level_idx,
|
||||||
|
seq_tier,
|
||||||
|
decoder_model_present_for_this_op,
|
||||||
|
operating_parameters_info,
|
||||||
|
initial_display_delay_present_for_this_op,
|
||||||
|
frame_width_bits_minus_1,
|
||||||
|
frame_height_bits_minus_1,
|
||||||
|
max_frame_width_minus_1,
|
||||||
|
max_frame_height_minus_1,
|
||||||
|
frame_id_numbers_present_flag,
|
||||||
|
delta_frame_id_length_minus_2,
|
||||||
|
additional_frame_id_length_minus_1,
|
||||||
|
use_128x128_superblock,
|
||||||
|
enable_filter_intra,
|
||||||
|
enable_intra_edge_filter,
|
||||||
|
enable_interintra_compound,
|
||||||
|
enable_masked_compound,
|
||||||
|
enable_warped_motion,
|
||||||
|
enable_dual_filter,
|
||||||
|
enable_order_hint,
|
||||||
|
enable_jnt_comp,
|
||||||
|
enable_ref_frame_mvs,
|
||||||
|
seq_choose_screen_content_tools,
|
||||||
|
seq_force_screen_content_tools,
|
||||||
|
seq_choose_integer_mv,
|
||||||
|
seq_force_integer_mv,
|
||||||
|
order_hint_bits_minus_1,
|
||||||
|
enable_superres,
|
||||||
|
enable_cdef,
|
||||||
|
enable_restoration,
|
||||||
|
color_config,
|
||||||
|
film_grain_params_present,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
timingInfo() {
|
||||||
|
const num_units_in_display_tick = this.f(32);
|
||||||
|
const time_scale = this.f(32);
|
||||||
|
const equal_picture_interval = !!this.f1();
|
||||||
|
let num_ticks_per_picture_minus_1: number | undefined;
|
||||||
|
if (equal_picture_interval) {
|
||||||
|
num_ticks_per_picture_minus_1 = this.uvlc();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
num_units_in_display_tick,
|
||||||
|
time_scale,
|
||||||
|
equal_picture_interval,
|
||||||
|
num_ticks_per_picture_minus_1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderModelInfo() {
|
||||||
|
const buffer_delay_length_minus_1 = this.f(5);
|
||||||
|
const num_units_in_decoding_tick = this.f(32);
|
||||||
|
const buffer_removal_time_length_minus_1 = this.f(5);
|
||||||
|
const frame_presentation_time_length_minus_1 = this.f(5);
|
||||||
|
return {
|
||||||
|
buffer_delay_length_minus_1,
|
||||||
|
num_units_in_decoding_tick,
|
||||||
|
buffer_removal_time_length_minus_1,
|
||||||
|
frame_presentation_time_length_minus_1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
operatingParametersInfo(
|
||||||
|
decoderModelInfo: ReturnType<Av1["decoderModelInfo"]>,
|
||||||
|
) {
|
||||||
|
const n = decoderModelInfo.buffer_delay_length_minus_1 + 1;
|
||||||
|
const decoder_buffer_delay = this.f(n);
|
||||||
|
const encoder_buffer_delay = this.f(n);
|
||||||
|
const low_delay_mode_flag = !!this.f1();
|
||||||
|
return {
|
||||||
|
decoder_buffer_delay,
|
||||||
|
encoder_buffer_delay,
|
||||||
|
low_delay_mode_flag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseOperatingPoint() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
colorConfig(seq_profile: number) {
|
||||||
|
const high_bitdepth = !!this.f1();
|
||||||
|
let twelve_bit = false;
|
||||||
|
let BitDepth = 8;
|
||||||
|
if (seq_profile === 2 && high_bitdepth) {
|
||||||
|
twelve_bit = !!this.f1();
|
||||||
|
BitDepth = twelve_bit ? 12 : 10;
|
||||||
|
} else if (seq_profile <= 2) {
|
||||||
|
BitDepth = high_bitdepth ? 10 : 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mono_chrome = false;
|
||||||
|
if (seq_profile === 1) {
|
||||||
|
mono_chrome = !!this.f1();
|
||||||
|
}
|
||||||
|
|
||||||
|
// const NumPlanes = mono_chrome ? 1 : 3;
|
||||||
|
|
||||||
|
const color_description_present_flag = !!this.f1();
|
||||||
|
let color_primaries = Av1.ColorPrimaries.Unspecified;
|
||||||
|
let transfer_characteristics = Av1.TransferCharacteristics.Unspecified;
|
||||||
|
let matrix_coefficients = Av1.MatrixCoefficients.Unspecified;
|
||||||
|
if (color_description_present_flag) {
|
||||||
|
color_primaries = this.f(8);
|
||||||
|
transfer_characteristics = this.f(8);
|
||||||
|
matrix_coefficients = this.f(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_range = false;
|
||||||
|
let subsampling_x: boolean;
|
||||||
|
let subsampling_y: boolean;
|
||||||
|
let chroma_sample_position = 0;
|
||||||
|
let separate_uv_delta_q = false;
|
||||||
|
if (mono_chrome) {
|
||||||
|
color_range = !!this.f1();
|
||||||
|
subsampling_x = true;
|
||||||
|
subsampling_y = true;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
color_primaries === Av1.ColorPrimaries.Bt709 &&
|
||||||
|
transfer_characteristics === Av1.TransferCharacteristics.Srgb &&
|
||||||
|
matrix_coefficients === Av1.MatrixCoefficients.Identity
|
||||||
|
) {
|
||||||
|
color_range = true;
|
||||||
|
subsampling_x = false;
|
||||||
|
subsampling_y = false;
|
||||||
|
} else {
|
||||||
|
color_range = !!this.f1();
|
||||||
|
switch (seq_profile) {
|
||||||
|
case 0:
|
||||||
|
subsampling_x = true;
|
||||||
|
subsampling_y = true;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
subsampling_x = false;
|
||||||
|
subsampling_y = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (BitDepth == 12) {
|
||||||
|
subsampling_x = !!this.f1();
|
||||||
|
if (subsampling_x) {
|
||||||
|
subsampling_y = !!this.f1();
|
||||||
|
} else {
|
||||||
|
subsampling_y = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subsampling_x = true;
|
||||||
|
subsampling_y = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (subsampling_x && subsampling_y) {
|
||||||
|
chroma_sample_position = this.f(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
separate_uv_delta_q = !!this.f1();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
high_bitdepth,
|
||||||
|
twelve_bit,
|
||||||
|
BitDepth,
|
||||||
|
mono_chrome,
|
||||||
|
color_description_present_flag,
|
||||||
|
color_primaries,
|
||||||
|
transfer_characteristics,
|
||||||
|
matrix_coefficients,
|
||||||
|
color_range,
|
||||||
|
subsampling_x,
|
||||||
|
subsampling_y,
|
||||||
|
chroma_sample_position,
|
||||||
|
separate_uv_delta_q,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Av1 {
|
||||||
|
export type OpenBitstreamUnit = Exclude<
|
||||||
|
ReturnType<Av1["openBitstreamUnit"]>,
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type SequenceHeaderObu = ReturnType<Av1["sequenceHeaderObu"]>;
|
||||||
|
|
||||||
|
export enum ObuType {
|
||||||
|
SequenceHeader = 1,
|
||||||
|
TemporalDelimiter,
|
||||||
|
FrameHeader,
|
||||||
|
TileGroup,
|
||||||
|
Metadata,
|
||||||
|
Frame,
|
||||||
|
RedundantFrameHeader,
|
||||||
|
TileList,
|
||||||
|
Padding = 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ColorPrimaries {
|
||||||
|
Bt709 = 1,
|
||||||
|
Unspecified,
|
||||||
|
Bt470M = 4,
|
||||||
|
Bt470BG,
|
||||||
|
Bt601,
|
||||||
|
Smpte240,
|
||||||
|
GenericFilm,
|
||||||
|
Bt2020,
|
||||||
|
Xyz,
|
||||||
|
Smpte431,
|
||||||
|
Smpte432,
|
||||||
|
Ebu3213 = 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TransferCharacteristics {
|
||||||
|
Bt709 = 1,
|
||||||
|
Unspecified,
|
||||||
|
Bt470M = 4,
|
||||||
|
Bt470BG,
|
||||||
|
Bt601,
|
||||||
|
Smpte240,
|
||||||
|
Linear,
|
||||||
|
Log100,
|
||||||
|
Log100Sqrt10,
|
||||||
|
Iec61966,
|
||||||
|
Bt1361,
|
||||||
|
Srgb,
|
||||||
|
Bt2020Ten,
|
||||||
|
Bt2020Twelve,
|
||||||
|
Smpte2084,
|
||||||
|
Smpte428,
|
||||||
|
Hlg,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MatrixCoefficients {
|
||||||
|
Identity = 0,
|
||||||
|
Bt709,
|
||||||
|
Unspecified,
|
||||||
|
Fcc = 4,
|
||||||
|
Bt470BG,
|
||||||
|
Bt601,
|
||||||
|
Smpte240,
|
||||||
|
YCgCo,
|
||||||
|
Bt2020Ncl,
|
||||||
|
Bt2020Cl,
|
||||||
|
Smpte2085,
|
||||||
|
ChromatNcl,
|
||||||
|
ChromatCl,
|
||||||
|
ICtCp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,84 +3,198 @@ import { describe, expect, it } from "@jest/globals";
|
||||||
import { NaluSodbBitReader } from "./nalu.js";
|
import { NaluSodbBitReader } from "./nalu.js";
|
||||||
|
|
||||||
describe("nalu", () => {
|
describe("nalu", () => {
|
||||||
describe.only("NaluSodbReader", () => {
|
describe("NaluSodbReader", () => {
|
||||||
it("should throw error if no end bit found", () => {
|
describe("constructor", () => {
|
||||||
expect(
|
it("should set `ended` if stream is effectively empty", () => {
|
||||||
() => new NaluSodbBitReader(new Uint8Array([0b00000000])),
|
const reader = new NaluSodbBitReader(
|
||||||
).toThrowErrorMatchingInlineSnapshot(`"Stop bit not found"`);
|
new Uint8Array([0b10000000]),
|
||||||
expect(
|
);
|
||||||
() =>
|
expect(reader).toHaveProperty("ended", true);
|
||||||
new NaluSodbBitReader(
|
});
|
||||||
new Uint8Array([0b00000000, 0b00000000]),
|
|
||||||
),
|
it("should throw error if stream is empty", () => {
|
||||||
).toThrowErrorMatchingInlineSnapshot(`"Stop bit not found"`);
|
expect(
|
||||||
|
() => new NaluSodbBitReader(new Uint8Array(0)),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"Stop bit not found"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if no end bit found (single byte)", () => {
|
||||||
|
expect(
|
||||||
|
() => new NaluSodbBitReader(new Uint8Array(1)),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"Stop bit not found"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if no end bit found (multiple bytes)", () => {
|
||||||
|
expect(
|
||||||
|
() => new NaluSodbBitReader(new Uint8Array(10)),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"Stop bit not found"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw error if read after end bit", () => {
|
describe("next", () => {
|
||||||
let reader = new NaluSodbBitReader(new Uint8Array([0b10000000]));
|
it("should read bits in Big Endian (single byte)", () => {
|
||||||
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
const reader = new NaluSodbBitReader(
|
||||||
`"Bit index out of bounds"`,
|
new Uint8Array([0b10110111]),
|
||||||
);
|
);
|
||||||
|
|
||||||
reader = new NaluSodbBitReader(
|
|
||||||
new Uint8Array([0b11111111, 0b10000000]),
|
|
||||||
);
|
|
||||||
for (let i = 0; i < 8; i += 1) {
|
|
||||||
expect(reader.next()).toBe(1);
|
expect(reader.next()).toBe(1);
|
||||||
}
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read bits in Big Endian (multiple bytes)", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0b01001000, 0b10000100, 0b00010001]),
|
||||||
|
);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if read after end bit (single byte, middle)", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0b11111000]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 4; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Bit index out of bounds"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if read after end bit (single byte, end)", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0b11111111]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 7; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Bit index out of bounds"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if read after end bit (multiple bytes, start)", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0b11111111, 0b10000000]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Bit index out of bounds"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if read after end bit (multiple bytes, middle)", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0b11111111, 0b11111000]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 12; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Bit index out of bounds"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip emulation prevent byte", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([0xff, 0x00, 0x00, 0x03, 0xff, 0x80]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 16; i += 1) {
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip successive emulation prevent bytes", () => {
|
||||||
|
const reader = new NaluSodbBitReader(
|
||||||
|
new Uint8Array([
|
||||||
|
0xff, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xff, 0x80,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 32; i += 1) {
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("skip", () => {
|
||||||
|
it("should skip <8 bits in single byte", () => {
|
||||||
|
const reader = new NaluSodbBitReader(new Uint8Array([0b01000011]));
|
||||||
|
|
||||||
|
reader.skip(1);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
|
||||||
|
reader.skip(3);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"Bit index out of bounds"`,
|
`"Bit index out of bounds"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should skip emulation prevent byte", () => {
|
it("should skip <8 bits in multiple bytes", () => {
|
||||||
const reader = new NaluSodbBitReader(
|
const reader = new NaluSodbBitReader(
|
||||||
new Uint8Array([0xff, 0x00, 0x00, 0x03, 0xff, 0x80]),
|
new Uint8Array([0b00000100, 0b00101000]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reader.skip(5);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
|
||||||
|
reader.skip(3);
|
||||||
|
expect(reader.next()).toBe(1);
|
||||||
|
expect(reader.next()).toBe(0);
|
||||||
|
expect(() => reader.next()).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"Bit index out of bounds"`,
|
||||||
);
|
);
|
||||||
for (let i = 0; i < 8; i += 1) {
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 16; i += 1) {
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 8; i += 1) {
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should skip successive emulation prevent bytes", () => {
|
it("should skip >8 bits without emulation prevention byte", () => {
|
||||||
const reader = new NaluSodbBitReader(
|
const reader = new NaluSodbBitReader(
|
||||||
new Uint8Array([
|
new Uint8Array([0b00000000, 0b00100001]),
|
||||||
0xff, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0xff, 0x80,
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
for (let i = 0; i < 8; i += 1) {
|
reader.skip(10);
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 32; i += 1) {
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 8; i += 1) {
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should read bits in Big Endian", () => {
|
|
||||||
let reader = new NaluSodbBitReader(new Uint8Array([0b10110011]));
|
|
||||||
expect(reader.next()).toBe(1);
|
expect(reader.next()).toBe(1);
|
||||||
expect(reader.next()).toBe(0);
|
expect(reader.next()).toBe(0);
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
|
|
||||||
reader = new NaluSodbBitReader(new Uint8Array([0b01001100]));
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
expect(reader.next()).toBe(0);
|
|
||||||
expect(reader.next()).toBe(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -232,12 +232,15 @@ export function naluRemoveEmulation(buffer: Uint8Array) {
|
||||||
|
|
||||||
export class NaluSodbBitReader {
|
export class NaluSodbBitReader {
|
||||||
readonly #nalu: Uint8Array;
|
readonly #nalu: Uint8Array;
|
||||||
|
// logical length is `#byteLength * 8 + (7 - #stopBitIndex)`
|
||||||
readonly #byteLength: number;
|
readonly #byteLength: number;
|
||||||
readonly #stopBitIndex: number;
|
readonly #stopBitIndex: number;
|
||||||
|
|
||||||
#zeroCount = 0;
|
#zeroCount = 0;
|
||||||
#bytePosition = -1;
|
|
||||||
#bitPosition = -1;
|
// logical position is `#bytePosition * 8 + (7 - #bitPosition)`
|
||||||
|
#bytePosition = 0;
|
||||||
|
#bitPosition = 7;
|
||||||
#byte = 0;
|
#byte = 0;
|
||||||
|
|
||||||
get byteLength() {
|
get byteLength() {
|
||||||
|
@ -258,8 +261,8 @@ export class NaluSodbBitReader {
|
||||||
|
|
||||||
get ended() {
|
get ended() {
|
||||||
return (
|
return (
|
||||||
this.#bytePosition === this.#byteLength &&
|
this.#bytePosition >= this.#byteLength &&
|
||||||
this.#bitPosition === this.#stopBitIndex
|
this.#bitPosition <= this.#stopBitIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +279,7 @@ export class NaluSodbBitReader {
|
||||||
if (((byte >> j) & 1) === 1) {
|
if (((byte >> j) & 1) === 1) {
|
||||||
this.#byteLength = i;
|
this.#byteLength = i;
|
||||||
this.#stopBitIndex = j;
|
this.#stopBitIndex = j;
|
||||||
this.#readByte();
|
this.#loadByte();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,15 +288,20 @@ export class NaluSodbBitReader {
|
||||||
throw new Error("Stop bit not found");
|
throw new Error("Stop bit not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
#readByte() {
|
#loadByte() {
|
||||||
this.#byte = this.#nalu[this.#bytePosition]!;
|
this.#byte = this.#nalu[this.#bytePosition]!;
|
||||||
|
|
||||||
|
// If the current sequence is `0x000003`, skip to the next byte.
|
||||||
|
// `annexBSplitNalu` had validated the input, so don't need to check here.
|
||||||
if (this.#zeroCount === 2 && this.#byte === 3) {
|
if (this.#zeroCount === 2 && this.#byte === 3) {
|
||||||
this.#zeroCount = 0;
|
this.#zeroCount = 0;
|
||||||
this.#bytePosition += 1;
|
this.#bytePosition += 1;
|
||||||
this.#readByte();
|
this.#loadByte();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `0x00000301` becomes `0x000001`, so only the `0x03` byte needs to be skipped
|
||||||
|
// The `0x00` bytes are still returned as-is
|
||||||
if (this.#byte === 0) {
|
if (this.#byte === 0) {
|
||||||
this.#zeroCount += 1;
|
this.#zeroCount += 1;
|
||||||
} else {
|
} else {
|
||||||
|
@ -302,18 +310,19 @@ export class NaluSodbBitReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next() {
|
||||||
if (this.#bitPosition === -1) {
|
|
||||||
this.#bitPosition = 7;
|
|
||||||
this.#bytePosition += 1;
|
|
||||||
this.#readByte();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ended) {
|
if (this.ended) {
|
||||||
throw new Error("Bit index out of bounds");
|
throw new Error("Bit index out of bounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = (this.#byte >> this.#bitPosition) & 1;
|
const value = (this.#byte >> this.#bitPosition) & 1;
|
||||||
|
|
||||||
this.#bitPosition -= 1;
|
this.#bitPosition -= 1;
|
||||||
|
if (this.#bitPosition < 0) {
|
||||||
|
this.#bytePosition += 1;
|
||||||
|
this.#bitPosition = 7;
|
||||||
|
this.#loadByte();
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,12 +338,38 @@ export class NaluSodbBitReader {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
skip(length: number) {
|
#ensurePositionValid() {
|
||||||
for (let i = 0; i < length; i += 1) {
|
if (
|
||||||
this.next();
|
this.#bytePosition >= this.#byteLength &&
|
||||||
|
this.#bitPosition < this.#stopBitIndex
|
||||||
|
) {
|
||||||
|
throw new Error("Bit index out of bounds");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skip(length: number) {
|
||||||
|
if (length <= this.#bitPosition + 1) {
|
||||||
|
this.#bitPosition -= length;
|
||||||
|
this.#ensurePositionValid();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
length -= this.#bitPosition + 1;
|
||||||
|
this.#bytePosition += 1;
|
||||||
|
this.#bitPosition = 7;
|
||||||
|
this.#loadByte();
|
||||||
|
this.#ensurePositionValid();
|
||||||
|
|
||||||
|
for (; length >= 8; length -= 8) {
|
||||||
|
this.#bytePosition += 1;
|
||||||
|
this.#loadByte();
|
||||||
|
this.#ensurePositionValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#bitPosition = 7 - length;
|
||||||
|
this.#ensurePositionValid();
|
||||||
|
}
|
||||||
|
|
||||||
decodeExponentialGolombNumber(): number {
|
decodeExponentialGolombNumber(): number {
|
||||||
let length = 0;
|
let length = 0;
|
||||||
while (this.next() === 0) {
|
while (this.next() === 0) {
|
||||||
|
|
|
@ -80,7 +80,7 @@ export interface ScrcpyOptionsInit2_0
|
||||||
videoEncoder?: string | undefined;
|
videoEncoder?: string | undefined;
|
||||||
|
|
||||||
audio?: boolean;
|
audio?: boolean;
|
||||||
audioCodec?: "opus" | "aac" | "raw";
|
audioCodec?: "raw" | "opus" | "aac";
|
||||||
audioBitRate?: number;
|
audioBitRate?: number;
|
||||||
audioCodecOptions?: CodecOptions;
|
audioCodecOptions?: CodecOptions;
|
||||||
audioEncoder?: string | undefined;
|
audioEncoder?: string | undefined;
|
||||||
|
@ -90,7 +90,7 @@ export interface ScrcpyOptionsInit2_0
|
||||||
sendCodecMeta?: boolean;
|
sendCodecMeta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function omit<T extends object, K extends keyof T>(
|
export function omit<T extends object, K extends keyof T>(
|
||||||
obj: T,
|
obj: T,
|
||||||
keys: K[],
|
keys: K[],
|
||||||
): Omit<T, K> {
|
): Omit<T, K> {
|
||||||
|
|
33
libraries/scrcpy/src/options/2_3.ts
Normal file
33
libraries/scrcpy/src/options/2_3.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { ScrcpyOptions1_21 } from "./1_21.js";
|
||||||
|
import { omit } from "./2_0.js";
|
||||||
|
import { ScrcpyOptions2_2, type ScrcpyOptionsInit2_2 } from "./2_2.js";
|
||||||
|
import { ScrcpyOptionsBase } from "./types.js";
|
||||||
|
|
||||||
|
export interface ScrcpyOptionsInit2_3
|
||||||
|
extends Omit<ScrcpyOptionsInit2_2, "audioCodec"> {
|
||||||
|
audioCodec?: "raw" | "opus" | "aac" | "flac" | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScrcpyOptions2_3 extends ScrcpyOptionsBase<
|
||||||
|
ScrcpyOptionsInit2_3,
|
||||||
|
ScrcpyOptions2_2
|
||||||
|
> {
|
||||||
|
static readonly DEFAULTS = {
|
||||||
|
...ScrcpyOptions2_2.DEFAULTS,
|
||||||
|
} as const satisfies Required<ScrcpyOptionsInit2_3>;
|
||||||
|
|
||||||
|
override get defaults(): Required<ScrcpyOptionsInit2_3> {
|
||||||
|
return ScrcpyOptions2_3.DEFAULTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(init: ScrcpyOptionsInit2_3) {
|
||||||
|
super(new ScrcpyOptions2_2(omit(init, ["audioCodec"])), {
|
||||||
|
...ScrcpyOptions2_3.DEFAULTS,
|
||||||
|
...init,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override serialize(): string[] {
|
||||||
|
return ScrcpyOptions1_21.serialize(this.value, this.defaults);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,12 @@ export class ScrcpyAudioCodec implements ScrcpyOptionValue {
|
||||||
"audio/aac",
|
"audio/aac",
|
||||||
"mp4a.66",
|
"mp4a.66",
|
||||||
);
|
);
|
||||||
|
static readonly FLAC = new ScrcpyAudioCodec(
|
||||||
|
"flac",
|
||||||
|
0x66_6c_61_63,
|
||||||
|
"audio/flac",
|
||||||
|
"flac",
|
||||||
|
);
|
||||||
static readonly RAW = new ScrcpyAudioCodec(
|
static readonly RAW = new ScrcpyAudioCodec(
|
||||||
"raw",
|
"raw",
|
||||||
0x00_72_61_77,
|
0x00_72_61_77,
|
||||||
|
|
|
@ -9,6 +9,7 @@ export * from "./1_25/index.js";
|
||||||
export * from "./2_0.js";
|
export * from "./2_0.js";
|
||||||
export * from "./2_1.js";
|
export * from "./2_1.js";
|
||||||
export * from "./2_2.js";
|
export * from "./2_2.js";
|
||||||
|
export * from "./2_3.js";
|
||||||
export * from "./codec.js";
|
export * from "./codec.js";
|
||||||
export * from "./latest.js";
|
export * from "./latest.js";
|
||||||
export * from "./types.js";
|
export * from "./types.js";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue