mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-25 21:53:16 -05:00
164c8c5b21
* run node 12, 14, and 16 (next to be supported) on CI. This will catch node version-specific problems like that solved in dad9bdc. * mention 12 and 14 in build instructions and link to instructions for installing that version. * follow this in Dockerfile, installing version 14. This addresses a "Cannot find module 'worker_threads'" error introduced in 39a63e0, which (inadvisedly) upgraded gzipper 4->5 in addition to the material-ui upgrade. * use utf-8 encoding rather than ascii in live part parser. Those builds apparently don't support ascii. iThey must use "small-icu" or have ICU disabled, as described here: https://nodejs.org/api/util.html#util_encodings_supported_when_node_js_is_built_with_the_small_icu_option
75 lines
2.1 KiB
TypeScript
75 lines
2.1 KiB
TypeScript
// This file is part of Moonfire NVR, a security camera network video recorder.
|
|
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
|
|
|
export interface Part {
|
|
mimeType: string;
|
|
videoSampleEntryId: number;
|
|
body: Uint8Array;
|
|
}
|
|
|
|
interface ParseSuccess {
|
|
status: "success";
|
|
part: Part;
|
|
}
|
|
|
|
interface ParseError {
|
|
status: "error";
|
|
errorMessage: string;
|
|
}
|
|
|
|
const DECODER = new TextDecoder("utf-8");
|
|
const CR = "\r".charCodeAt(0);
|
|
const NL = "\n".charCodeAt(0);
|
|
|
|
type ParseResult = ParseSuccess | ParseError;
|
|
|
|
/// Parses a live stream message.
|
|
export function parsePart(raw: Uint8Array): ParseResult {
|
|
// Parse into headers and body.
|
|
const headers = new Headers();
|
|
let pos = 0;
|
|
while (true) {
|
|
const cr = raw.indexOf(CR, pos);
|
|
if (cr === -1 || raw.length === cr + 1 || raw[cr + 1] !== NL) {
|
|
return {
|
|
status: "error",
|
|
errorMessage: "header that never ends (no '\\r\\n')!",
|
|
};
|
|
}
|
|
const line = DECODER.decode(raw.slice(pos, cr));
|
|
pos = cr + 2;
|
|
if (line.length === 0) {
|
|
break;
|
|
}
|
|
const colon = line.indexOf(":");
|
|
if (colon === -1 || line.length === colon + 1 || line[colon + 1] !== " ") {
|
|
return {
|
|
status: "error",
|
|
errorMessage: "invalid name/value separator (no ': ')!",
|
|
};
|
|
}
|
|
const name = line.substring(0, colon);
|
|
const value = line.substring(colon + 2);
|
|
headers.append(name, value);
|
|
}
|
|
const body = raw.slice(pos);
|
|
|
|
const mimeType = headers.get("Content-Type");
|
|
if (mimeType === null) {
|
|
return { status: "error", errorMessage: "no Content-Type" };
|
|
}
|
|
const videoSampleEntryIdStr = headers.get("X-Video-Sample-Entry-Id");
|
|
if (videoSampleEntryIdStr === null) {
|
|
return { status: "error", errorMessage: "no X-Video-Sample-Entry-Id" };
|
|
}
|
|
const videoSampleEntryId = parseInt(videoSampleEntryIdStr, 10);
|
|
if (isNaN(videoSampleEntryId)) {
|
|
return { status: "error", errorMessage: "invalid X-Video-Sample-Entry-Id" };
|
|
}
|
|
return {
|
|
status: "success",
|
|
part: { mimeType, videoSampleEntryId, body },
|
|
};
|
|
}
|