Loading libraries/scrcpy-decoder-webcodecs/src/index.ts 0 → 100644 +1 −0 Original line number Diff line number Diff line export * from "./video/index.js"; libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts 0 → 100644 +101 −0 Original line number Diff line number Diff line import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import { Av1 } from "@yume-chan/scrcpy"; import type { CodecDecoder } from "./type.js"; import { decimalTwoDigits } from "./utils.js"; export class Av1Codec implements CodecDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { this.#decoder = decoder; this.#updateSize = updateSize; } #configure(data: Uint8Array) { const parser = new Av1(data); const sequenceHeader = parser.searchSequenceHeaderObu(); if (!sequenceHeader) { return; } const { seq_profile: seqProfile, seq_level_idx: [seqLevelIdx = 0], max_frame_width_minus_1, max_frame_height_minus_1, color_config: { BitDepth, mono_chrome: monoChrome, subsampling_x: subsamplingX, subsampling_y: subsamplingY, chroma_sample_position: chromaSamplePosition, color_description_present_flag, }, } = sequenceHeader; let colorPrimaries: Av1.ColorPrimaries; let transferCharacteristics: Av1.TransferCharacteristics; let matrixCoefficients: Av1.MatrixCoefficients; let colorRange: boolean; if (color_description_present_flag) { ({ color_primaries: colorPrimaries, transfer_characteristics: transferCharacteristics, matrix_coefficients: matrixCoefficients, color_range: colorRange, } = sequenceHeader.color_config); } else { colorPrimaries = Av1.ColorPrimaries.Bt709; transferCharacteristics = Av1.TransferCharacteristics.Bt709; matrixCoefficients = Av1.MatrixCoefficients.Bt709; colorRange = false; } const width = max_frame_width_minus_1 + 1; const height = max_frame_height_minus_1 + 1; this.#updateSize(width, height); 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("."); this.#decoder.configure({ codec, optimizeForLatency: true, }); } decode(packet: ScrcpyMediaStreamPacket): void { if (packet.type === "configuration") { return; } this.#configure(packet.data); this.#decoder.decode( new EncodedVideoChunk({ // Treat `undefined` as `key`, otherwise it won't decode. type: packet.keyframe === false ? "delta" : "key", timestamp: 0, data: packet.data, }), ); } } libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts 0 → 100644 +42 −0 Original line number Diff line number Diff line import { h264ParseConfiguration } from "@yume-chan/scrcpy"; import { H26xDecoder } from "./h26x.js"; import { hexTwoDigits } from "./utils.js"; export class H264Decoder extends H26xDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { super(decoder); this.#decoder = decoder; this.#updateSize = updateSize; } override configure(data: Uint8Array): void { const { profileIndex, constraintSet, levelIndex, croppedWidth, croppedHeight, } = h264ParseConfiguration(data); this.#updateSize(croppedWidth, croppedHeight); // https://www.rfc-editor.org/rfc/rfc6381#section-3.3 // ISO Base Media File Format Name Space const codec = "avc1." + hexTwoDigits(profileIndex) + hexTwoDigits(constraintSet) + hexTwoDigits(levelIndex); this.#decoder.configure({ codec: codec, optimizeForLatency: true, }); } } libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import { getUint32LittleEndian } from "@yume-chan/no-data-view"; import { h265ParseConfiguration } from "@yume-chan/scrcpy"; import { H26xDecoder } from "./h26x.js"; import { hexDigits } from "./utils.js"; export class H265Decoder extends H26xDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { super(decoder); this.#decoder = decoder; this.#updateSize = updateSize; } override configure(data: Uint8Array): void { const { generalProfileSpace, generalProfileIndex, generalProfileCompatibilitySet, generalTierFlag, generalLevelIndex, generalConstraintSet, croppedWidth, croppedHeight, } = h265ParseConfiguration(data); this.#updateSize(croppedWidth, croppedHeight); const codec = [ "hev1", ["", "A", "B", "C"][generalProfileSpace]! + generalProfileIndex.toString(), hexDigits(getUint32LittleEndian(generalProfileCompatibilitySet, 0)), (generalTierFlag ? "H" : "L") + generalLevelIndex.toString(), ...Array.from(generalConstraintSet, hexDigits), ].join("."); this.#decoder.configure({ codec, optimizeForLatency: true, }); } } libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts 0 → 100644 +45 −0 Original line number Diff line number Diff line import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import type { CodecDecoder } from "./type.js"; export abstract class H26xDecoder implements CodecDecoder { #config: Uint8Array | undefined; #decoder: VideoDecoder; constructor(decoder: VideoDecoder) { this.#decoder = decoder; } abstract configure(data: Uint8Array): void; decode(packet: ScrcpyMediaStreamPacket): void { if (packet.type === "configuration") { this.#config = packet.data; this.configure(packet.data); return; } // For H.264 and H.265, when the stream is in Annex B format // (which Scrcpy uses, as Android MediaCodec produces), // configuration data needs to be combined with the first frame data. // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type let data: Uint8Array; if (this.#config !== undefined) { data = new Uint8Array(this.#config.length + packet.data.length); data.set(this.#config, 0); data.set(packet.data, this.#config.length); this.#config = undefined; } else { data = packet.data; } this.#decoder.decode( new EncodedVideoChunk({ // Treat `undefined` as `key`, otherwise won't decode. type: packet.keyframe === false ? "delta" : "key", timestamp: 0, data, }), ); } } Loading
libraries/scrcpy-decoder-webcodecs/src/index.ts 0 → 100644 +1 −0 Original line number Diff line number Diff line export * from "./video/index.js";
libraries/scrcpy-decoder-webcodecs/src/video/codec/av1.ts 0 → 100644 +101 −0 Original line number Diff line number Diff line import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import { Av1 } from "@yume-chan/scrcpy"; import type { CodecDecoder } from "./type.js"; import { decimalTwoDigits } from "./utils.js"; export class Av1Codec implements CodecDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { this.#decoder = decoder; this.#updateSize = updateSize; } #configure(data: Uint8Array) { const parser = new Av1(data); const sequenceHeader = parser.searchSequenceHeaderObu(); if (!sequenceHeader) { return; } const { seq_profile: seqProfile, seq_level_idx: [seqLevelIdx = 0], max_frame_width_minus_1, max_frame_height_minus_1, color_config: { BitDepth, mono_chrome: monoChrome, subsampling_x: subsamplingX, subsampling_y: subsamplingY, chroma_sample_position: chromaSamplePosition, color_description_present_flag, }, } = sequenceHeader; let colorPrimaries: Av1.ColorPrimaries; let transferCharacteristics: Av1.TransferCharacteristics; let matrixCoefficients: Av1.MatrixCoefficients; let colorRange: boolean; if (color_description_present_flag) { ({ color_primaries: colorPrimaries, transfer_characteristics: transferCharacteristics, matrix_coefficients: matrixCoefficients, color_range: colorRange, } = sequenceHeader.color_config); } else { colorPrimaries = Av1.ColorPrimaries.Bt709; transferCharacteristics = Av1.TransferCharacteristics.Bt709; matrixCoefficients = Av1.MatrixCoefficients.Bt709; colorRange = false; } const width = max_frame_width_minus_1 + 1; const height = max_frame_height_minus_1 + 1; this.#updateSize(width, height); 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("."); this.#decoder.configure({ codec, optimizeForLatency: true, }); } decode(packet: ScrcpyMediaStreamPacket): void { if (packet.type === "configuration") { return; } this.#configure(packet.data); this.#decoder.decode( new EncodedVideoChunk({ // Treat `undefined` as `key`, otherwise it won't decode. type: packet.keyframe === false ? "delta" : "key", timestamp: 0, data: packet.data, }), ); } }
libraries/scrcpy-decoder-webcodecs/src/video/codec/h264.ts 0 → 100644 +42 −0 Original line number Diff line number Diff line import { h264ParseConfiguration } from "@yume-chan/scrcpy"; import { H26xDecoder } from "./h26x.js"; import { hexTwoDigits } from "./utils.js"; export class H264Decoder extends H26xDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { super(decoder); this.#decoder = decoder; this.#updateSize = updateSize; } override configure(data: Uint8Array): void { const { profileIndex, constraintSet, levelIndex, croppedWidth, croppedHeight, } = h264ParseConfiguration(data); this.#updateSize(croppedWidth, croppedHeight); // https://www.rfc-editor.org/rfc/rfc6381#section-3.3 // ISO Base Media File Format Name Space const codec = "avc1." + hexTwoDigits(profileIndex) + hexTwoDigits(constraintSet) + hexTwoDigits(levelIndex); this.#decoder.configure({ codec: codec, optimizeForLatency: true, }); } }
libraries/scrcpy-decoder-webcodecs/src/video/codec/h265.ts 0 → 100644 +47 −0 Original line number Diff line number Diff line import { getUint32LittleEndian } from "@yume-chan/no-data-view"; import { h265ParseConfiguration } from "@yume-chan/scrcpy"; import { H26xDecoder } from "./h26x.js"; import { hexDigits } from "./utils.js"; export class H265Decoder extends H26xDecoder { #decoder: VideoDecoder; #updateSize: (width: number, height: number) => void; constructor( decoder: VideoDecoder, updateSize: (width: number, height: number) => void, ) { super(decoder); this.#decoder = decoder; this.#updateSize = updateSize; } override configure(data: Uint8Array): void { const { generalProfileSpace, generalProfileIndex, generalProfileCompatibilitySet, generalTierFlag, generalLevelIndex, generalConstraintSet, croppedWidth, croppedHeight, } = h265ParseConfiguration(data); this.#updateSize(croppedWidth, croppedHeight); const codec = [ "hev1", ["", "A", "B", "C"][generalProfileSpace]! + generalProfileIndex.toString(), hexDigits(getUint32LittleEndian(generalProfileCompatibilitySet, 0)), (generalTierFlag ? "H" : "L") + generalLevelIndex.toString(), ...Array.from(generalConstraintSet, hexDigits), ].join("."); this.#decoder.configure({ codec, optimizeForLatency: true, }); } }
libraries/scrcpy-decoder-webcodecs/src/video/codec/h26x.ts 0 → 100644 +45 −0 Original line number Diff line number Diff line import type { ScrcpyMediaStreamPacket } from "@yume-chan/scrcpy"; import type { CodecDecoder } from "./type.js"; export abstract class H26xDecoder implements CodecDecoder { #config: Uint8Array | undefined; #decoder: VideoDecoder; constructor(decoder: VideoDecoder) { this.#decoder = decoder; } abstract configure(data: Uint8Array): void; decode(packet: ScrcpyMediaStreamPacket): void { if (packet.type === "configuration") { this.#config = packet.data; this.configure(packet.data); return; } // For H.264 and H.265, when the stream is in Annex B format // (which Scrcpy uses, as Android MediaCodec produces), // configuration data needs to be combined with the first frame data. // https://www.w3.org/TR/webcodecs-avc-codec-registration/#encodedvideochunk-type let data: Uint8Array; if (this.#config !== undefined) { data = new Uint8Array(this.#config.length + packet.data.length); data.set(this.#config, 0); data.set(packet.data, this.#config.length); this.#config = undefined; } else { data = packet.data; } this.#decoder.decode( new EncodedVideoChunk({ // Treat `undefined` as `key`, otherwise won't decode. type: packet.keyframe === false ? "delta" : "key", timestamp: 0, data, }), ); } }