Skip to content
This repository was archived by the owner on Jul 23, 2024. It is now read-only.

Commit ccbef1b

Browse files
committed
First cod3e
1 parent d048102 commit ccbef1b

15 files changed

+1692
-107
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules/
2+
dist/

package.json

+17-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@
77
"license": "UNLICENSED",
88
"private": true,
99
"devDependencies": {
10-
"typescript": "^5.4.5"
10+
"@types/core-js": "^2.5.8",
11+
"@types/jquery": "^3.5.30",
12+
"clean-webpack-plugin": "^4.0.0",
13+
"copy-webpack-plugin": "^12.0.2",
14+
"ts-loader": "^9.5.1",
15+
"typescript": "^5.4.5",
16+
"webpack": "^5.91.0",
17+
"webpack-cli": "^5.1.4"
18+
},
19+
"scripts": {
20+
"clean": "rm -rf dist",
21+
"build": "yarn run clean; NODE_ENV=dev webpack",
22+
"build:prod": "yarn run clean; NODE_ENV=prod webpack",
23+
"watch": "NODE_ENV=dev webpack --watch"
24+
},
25+
"dependencies": {
26+
"nal-extractor": "^1.0.1"
1127
}
1228
}

src/decoder/array-utils.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
function buildPattern(search: ArrayLike<unknown>): number[] {
2+
const table = new Array<number>(search.length).fill(0);
3+
let maxPrefix = 0;
4+
5+
for (let patternIndex = 1; patternIndex < search.length; ++patternIndex) {
6+
while (maxPrefix > 0 && search[patternIndex] !== search[maxPrefix]) {
7+
maxPrefix = table[maxPrefix - 1];
8+
}
9+
10+
if (search[maxPrefix] === search[patternIndex]) {
11+
++maxPrefix;
12+
}
13+
14+
table[patternIndex] = maxPrefix;
15+
}
16+
17+
return table;
18+
}
19+
20+
/**
21+
* Generic Knuth-Morris-Pratt algorithm for finding a sequence (string, subarray) in a larger sequence.
22+
*
23+
* @param text - the larger sequence of elements of type T in which to find the search sequence.
24+
* @param search - the shorter search sequence of elements of type T.
25+
* @returns an array of numbers, representing the start offsets of all occurrences of `search`
26+
* inside `text`.
27+
*/
28+
export function searchPattern<T>(text: ArrayLike<T>, search: ArrayLike<T>): number[] {
29+
const pattern = buildPattern(search);
30+
const matches: number[] = [];
31+
32+
let textIndex = 0;
33+
let patternIndex = 0;
34+
35+
while (textIndex < text.length) {
36+
if (text[textIndex] === search[patternIndex]) {
37+
++textIndex;
38+
++patternIndex;
39+
}
40+
41+
if (patternIndex === search.length) {
42+
matches.push(textIndex - patternIndex);
43+
patternIndex = pattern[patternIndex - 1];
44+
} else if (text[textIndex] !== search[patternIndex]) {
45+
if (patternIndex === 0) {
46+
++textIndex;
47+
} else {
48+
patternIndex = pattern[patternIndex - 1];
49+
}
50+
}
51+
}
52+
53+
return matches;
54+
}

src/decoder/bitstream.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { EncodedVideoChunkTransformer } from "./chunk-transformer";
2+
import { AvcNalu, NalUnitType, isTypedNalu } from "./interfaces";
3+
import { AvcNaluTransformer } from "./nalu-transformer";
4+
5+
import { parseSPS } from 'nal-extractor'
6+
7+
export class AvcBitstream {
8+
public readonly config: Promise<VideoDecoderConfig>;
9+
public readonly packets: ReadableStreamReader<EncodedVideoChunk>;
10+
11+
constructor(
12+
h264File: Blob,
13+
hardwareAcceleration: HardwareAcceleration,
14+
frameRate: number
15+
) {
16+
const [headerNALUs, remainingNALUs] = h264File.stream().pipeThrough<AvcNalu>(new TransformStream<Uint8Array, AvcNalu>(new AvcNaluTransformer())).tee();
17+
18+
this.config = AvcBitstream.parseVideoDecoderConfig(headerNALUs, hardwareAcceleration);
19+
this.packets = remainingNALUs.pipeThrough<EncodedVideoChunk>(new TransformStream<AvcNalu, EncodedVideoChunk>(new EncodedVideoChunkTransformer(frameRate))).getReader();
20+
}
21+
22+
/**
23+
* We read the first SPS NALU and construct the `VideoDecoderConfig` object from it.
24+
*
25+
* SPS NALU @see https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set
26+
* `codec` parameter @see https://www.rfc-editor.org/rfc/rfc6381#section-3
27+
* `codecWidth` and `codedHeight` parameters https://stackoverflow.com/questions/12018535/get-the-width-height-of-the-video-from-h-264-nalu
28+
*/
29+
private static async parseVideoDecoderConfig(headerNALUs: ReadableStream<AvcNalu>, hardwareAcceleration: HardwareAcceleration): Promise<VideoDecoderConfig> {
30+
const reader = headerNALUs.getReader();
31+
32+
for (let { value, done } = await reader.read(); value !== undefined && !done; { value, done } = await reader.read()) {
33+
if (isTypedNalu(value, NalUnitType.SEQUENCE_PARAMETER_SET)) {
34+
35+
const { naluBody } = value;
36+
37+
// The RBSP of a NALU begins after the first byte of its body
38+
const {
39+
profile_idc,
40+
level_idc,
41+
pic_width_in_mbs_minus1,
42+
frame_mbs_only_flag,
43+
pic_height_in_map_units_minus1,
44+
frame_cropping,
45+
} = parseSPS(naluBody.subarray(1));
46+
47+
return {
48+
codec: `avc1.${profile_idc.toString(16).padStart(2, '0')}00${level_idc.toString(16).padStart(2, '0')}}`,
49+
codedWidth: ((pic_width_in_mbs_minus1 + 1) * 16) - (frame_cropping === false ? 0 : (frame_cropping.right_offset * 2 + frame_cropping.left_offset * 2)),
50+
codedHeight: (2 - (frame_mbs_only_flag ? 1 : 0)) * ((pic_height_in_map_units_minus1 + 1) * 16) - (frame_cropping === false ? 0 : (frame_cropping.top_offset + frame_cropping.bottom_offset) * 2),
51+
hardwareAcceleration,
52+
};
53+
}
54+
}
55+
56+
throw new Error('Premature end of AVC bitstream before an SPS NALU was found');
57+
}
58+
59+
}

src/decoder/chunk-transformer.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { AvcNalu, NalUnitType } from "./interfaces";
2+
3+
const ONE_SECOND_IN_MICROS = 1e6;
4+
5+
export class EncodedVideoChunkTransformer implements Transformer<AvcNalu, EncodedVideoChunk> {
6+
private seqno = 0;
7+
private readonly duration: number;
8+
9+
/**
10+
*
11+
* @param frameRate - the decoding frame rate of this stream in frames per second.
12+
*/
13+
constructor(
14+
private readonly frameRate: number
15+
) {
16+
this.duration = Math.floor(ONE_SECOND_IN_MICROS / frameRate);
17+
}
18+
19+
private calculateFrameTimestamp(): number {
20+
return Math.floor(ONE_SECOND_IN_MICROS * (this.seqno ++) / this.frameRate);
21+
}
22+
23+
transform({ naluType, naluBody }: AvcNalu, controller: TransformStreamDefaultController<EncodedVideoChunk>): void {
24+
if (naluType === NalUnitType.CODED_SLICE_IDR || naluType === NalUnitType.CODED_SLICE_NON_IDR) {
25+
controller.enqueue(new EncodedVideoChunk({
26+
data: naluBody.subarray(1),
27+
type: naluType === NalUnitType.CODED_SLICE_IDR ? 'key' : 'delta',
28+
timestamp: this.calculateFrameTimestamp(),
29+
duration: this.duration,
30+
}));
31+
}
32+
}
33+
}

src/decoder/interfaces.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export enum NalUnitType {
2+
UNSPECIFIED_1 = 0,
3+
CODED_SLICE_NON_IDR = 1,
4+
CODED_SLICE_DATA_PARTITION_A = 2,
5+
CODED_SLICE_DATA_PARTITION_B = 3,
6+
CODED_SLICE_DATA_PARTITION_C = 4,
7+
CODED_SLICE_IDR = 5,
8+
SUPPLEMENTAL_ENHANCEMENT_INFORMATION = 6,
9+
SEQUENCE_PARAMETER_SET = 7,
10+
PICTURE_PARAMETER_SET = 8,
11+
ACCESS_UNIT_DELIMITER = 9,
12+
END_OF_SEQUENCE = 10,
13+
END_OF_STREAM = 11,
14+
FILLER_DATA = 12,
15+
SEQUENCE_PARAMETER_SET_EXTENSION = 13,
16+
PREFIX_NAL_UNIT = 14,
17+
SUBSET_SEQUENCE_PARAMETER_SET = 15,
18+
RESERVED_1 = 16,
19+
RESERVED_2 = 17,
20+
RESERVED_3 = 18,
21+
CODED_SLICE_OF_AUXILIARY_CODED_PICTURE_WITHOUT_PARTITIONING = 19,
22+
CODED_SLICE_EXTENSION = 20,
23+
CODED_SLICE_EXTENSION_FOR_DEPTH_VIEW_COMPONENTS = 21,
24+
RESERVED_4 = 22,
25+
RESERVED_5 = 23,
26+
UNSPECIFIED_2 = 24,
27+
UNSPECIFIED_3 = 25,
28+
UNSPECIFIED_4 = 26,
29+
UNSPECIFIED_5 = 27,
30+
UNSPECIFIED_6 = 28,
31+
UNSPECIFIED_7 = 29,
32+
UNSPECIFIED_8 = 30,
33+
UNSPECIFIED_9 = 31,
34+
}
35+
36+
export const NALU_PREFIX = [0x00, 0x00, 0x00, 0x01];
37+
38+
export interface AvcNalu<T extends NalUnitType = NalUnitType> {
39+
readonly naluType: T;
40+
readonly refIdc: number;
41+
readonly naluBody: Uint8Array;
42+
}
43+
44+
export function isTypedNalu<T extends NalUnitType>(nalu: AvcNalu, type: T): nalu is AvcNalu<T> {
45+
return nalu.naluType === type;
46+
}

src/decoder/nalu-transformer.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { searchPattern } from "./array-utils";
2+
import { AvcNalu, NalUnitType, NALU_PREFIX } from "./interfaces";
3+
4+
export class AvcNaluTransformer implements Transformer<Uint8Array, AvcNalu> {
5+
private readonly buffered: Uint8Array[] = [];
6+
7+
private static parseNalu(payload: Uint8Array): {
8+
naluType: NalUnitType;
9+
refIdc: number;
10+
} {
11+
return {
12+
naluType: payload[0] & 0x1f,
13+
refIdc: (payload[0] >> 5) & 0x03,
14+
};
15+
}
16+
17+
private async extractNalus(
18+
chunk: Uint8Array,
19+
): Promise<Uint8Array[]> {
20+
// Find start positions of NALUs in chunk
21+
const positions = searchPattern<number>(chunk, NALU_PREFIX);
22+
23+
// No NALU start markers found in chunk. We append it to the (unfinished) buffered NALU
24+
if (positions.length === 0) {
25+
this.buffered.push(chunk);
26+
return [];
27+
}
28+
29+
// We found at least one NALU start marker. This forms the end of the buffered NALU. The chunk may further contain more complete NALUs, which we
30+
// extract.
31+
const nalus = [
32+
new Uint8Array(await new Blob([...this.buffered, chunk.subarray(0, positions[0])]).arrayBuffer()),
33+
...positions.slice(0, positions.length - 1).map((position, index) => chunk.subarray(position, positions[index + 1])),
34+
];
35+
36+
// The last NALU start marker demarcates the start of an assumed unfinished NALU.
37+
this.buffered.splice(0, this.buffered.length, chunk.subarray(positions[positions.length - 1]));
38+
39+
return nalus;
40+
}
41+
42+
start(): void {}
43+
44+
async transform(chunk: Uint8Array, controller: TransformStreamDefaultController<AvcNalu>): Promise<void> {
45+
const nalus = await this.extractNalus(chunk);
46+
47+
nalus.forEach(naluPayload => {
48+
const naluBody = naluPayload.subarray(NALU_PREFIX.length);
49+
const { naluType, refIdc } = AvcNaluTransformer.parseNalu(naluBody);
50+
51+
controller.enqueue({ naluType, refIdc, naluBody });
52+
});
53+
}
54+
55+
async flush(controller: TransformStreamDefaultController<AvcNalu>): Promise<void> {
56+
const naluPayload = new Uint8Array(await new Blob(this.buffered).arrayBuffer());
57+
58+
const naluBody = naluPayload.subarray(NALU_PREFIX.length);
59+
const { naluType, refIdc } = AvcNaluTransformer.parseNalu(naluBody);
60+
61+
this.buffered.splice(0, this.buffered.length);
62+
63+
controller.enqueue({ naluType, refIdc, naluBody });
64+
controller.terminate();
65+
}
66+
}

src/index.html

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
4+
<head>
5+
6+
</head>
7+
8+
<body>
9+
<script src="index.js"></script>
10+
</body>
11+
</html>

src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { AvcBitstream } from "./decoder/bitstream";
2+
3+
console.log('Hello world');
4+
5+
const DEFAULT_FRAMERATE = 30;
6+
7+
new AvcBitstream(new File(['abc'], 'hooha.h264'), 'prefer-hardware', DEFAULT_FRAMERATE);

0 commit comments

Comments
 (0)