Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59a2c1ad0 |
@@ -3,8 +3,10 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico">
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
<title>${name}</title>
|
<title>${name}</title>
|
||||||
</head><body>
|
</head><body>
|
||||||
<h2>${name}</h2><a href="/camera/${id}"><img src="/camera/${id}"/></a><hr/>
|
<h2>${name}</h2><a href="/camera/${id}"><img style="height: 640px" src="/camera/${id}"/></a><hr/>
|
||||||
<button onclick="toggle_audio()" id=audio disabled=true>Audio: disabled</button>
|
<button onclick="toggle_audio()" id=audio disabled=true>Audio: disabled</button>
|
||||||
|
<button onclick="fetch('/rotate/${id}')">Rotate</button>
|
||||||
|
<button onclick="fetch('/mirror/${id}')">Mirror</button>
|
||||||
<script>
|
<script>
|
||||||
const alaw_to_s16_table = [
|
const alaw_to_s16_table = [
|
||||||
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
|
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
|
||||||
@@ -49,13 +51,11 @@
|
|||||||
audio_context.resume();
|
audio_context.resume();
|
||||||
|
|
||||||
const evtSource = new EventSource("/audio/${id}");
|
const evtSource = new EventSource("/audio/${id}");
|
||||||
if (${debug}) {
|
evtSource.onopen = (e) => {
|
||||||
evtSource.onopen = (e) => {
|
console.log("evtsource open");
|
||||||
console.log("evtsource open");
|
}
|
||||||
}
|
evtSource.onerror = (e) => {
|
||||||
evtSource.onerror = (e) => {
|
console.log("evtsource error", e);
|
||||||
console.log("evtsource error", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let endsAt = 0;
|
let endsAt = 0;
|
||||||
let startAt = 0;
|
let startAt = 0;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Create a minimal EXIF segment with orientation
|
||||||
|
export const createExifOrientation = (orientation: number) => {
|
||||||
|
const tiffHeader = Buffer.from("49492A0008000000", "hex");
|
||||||
|
const ifdEntry = Buffer.concat([
|
||||||
|
Buffer.from("0100", "hex"), // Number of IFD entries
|
||||||
|
Buffer.from("1201030001000000", "hex"), // Tag, Type, Count
|
||||||
|
Buffer.from(orientation.toString(16).padStart(2, "0"), "hex"), // Orientation value
|
||||||
|
Buffer.from("0000", "hex"), // No more IFDs
|
||||||
|
Buffer.from("0000000000", "hex"), // padding??
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exifData = Buffer.concat([Buffer.from("457869660000", "hex"), tiffHeader, ifdEntry]);
|
||||||
|
const segmentLength = Buffer.from([(exifData.length + 2) >> 8, (exifData.length + 2) & 0xff]);
|
||||||
|
const exifHeader = Buffer.concat([Buffer.from("FFE1", "hex"), segmentLength]);
|
||||||
|
|
||||||
|
return Buffer.concat([exifHeader, exifData]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addExifToJpeg = (jpegData: Buffer, exifSegment: Buffer) => {
|
||||||
|
// Check for existing EXIF (simplified check)
|
||||||
|
if (jpegData.includes(Buffer.from("FFE1", "hex"))) {
|
||||||
|
throw new Error("JPEG already contains EXIF segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const soiEnd = 2; // After FFD8
|
||||||
|
const modifiedJpeg = Buffer.concat([jpegData.subarray(0, soiEnd), exifSegment, jpegData.subarray(soiEnd)]);
|
||||||
|
return modifiedJpeg;
|
||||||
|
};
|
||||||
+34
-3
@@ -7,6 +7,7 @@ import { opt } from "./options.js";
|
|||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial, SendDevStatus } from "./impl.js";
|
import { DevSerial, SendDevStatus } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
||||||
|
import { addExifToJpeg, createExifOrientation } from "./exif.js";
|
||||||
|
|
||||||
// @ts-expect-error TS2307
|
// @ts-expect-error TS2307
|
||||||
import favicon from "./cam.ico.gz";
|
import favicon from "./cam.ico.gz";
|
||||||
@@ -21,6 +22,15 @@ const sessions: Record<string, Session> = {};
|
|||||||
// Text file containing the mapping of camera names.
|
// Text file containing the mapping of camera names.
|
||||||
const nameFile = "cameras.txt";
|
const nameFile = "cameras.txt";
|
||||||
|
|
||||||
|
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
||||||
|
const oMap = [1, 8, 3, 6];
|
||||||
|
const oMapMirror = [2, 7, 4, 5];
|
||||||
|
const orientations = [1, 2, 3, 4, 5, 6, 7, 8].reduce((acc, cur) => {
|
||||||
|
return { [cur]: createExifOrientation(cur), ...acc };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let camSettings: { [key: string]: { orientation: number; mirror: boolean } } = {};
|
||||||
|
|
||||||
// Reads the mapping of serial numbers to camera names from the text file.
|
// Reads the mapping of serial numbers to camera names from the text file.
|
||||||
const cameraNames = Object.assign(
|
const cameraNames = Object.assign(
|
||||||
{},
|
{},
|
||||||
@@ -90,7 +100,23 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.url.startsWith("/camera/")) {
|
if (req.url.startsWith("/rotate/")) {
|
||||||
|
let devId = req.url.split("/")[2];
|
||||||
|
let curPos = camSettings[devId]?.orientation || 0;
|
||||||
|
let nextPos = (curPos + 1) % 4;
|
||||||
|
logger.info(`Rotating ${devId} to ${nextPos}`);
|
||||||
|
camSettings[devId].orientation = nextPos;
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url.startsWith("/mirror/")) {
|
||||||
|
let devId = req.url.split("/")[2];
|
||||||
|
logger.info(`Mirroring ${devId}`);
|
||||||
|
camSettings[devId].mirror = !camSettings[devId].mirror;
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url.startsWith("/camera/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
logger.info(`Video stream requested for camera ${devId}`);
|
logger.info(`Video stream requested for camera ${devId}`);
|
||||||
let s = sessions[devId];
|
let s = sessions[devId];
|
||||||
@@ -151,8 +177,12 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
||||||
|
|
||||||
s.eventEmitter.on("frame", () => {
|
s.eventEmitter.on("frame", () => {
|
||||||
const assembled = Buffer.concat(s.curImage);
|
// Add an EXIF header to indicate if the image should be rotated or mirrored
|
||||||
|
let orientation = camSettings[dev.devId].orientation;
|
||||||
|
orientation = camSettings[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
||||||
|
const exifSegment = orientations[orientation];
|
||||||
|
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
|
||||||
|
const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]);
|
||||||
responses[dev.devId].forEach((res) => {
|
responses[dev.devId].forEach((res) => {
|
||||||
res.write(header);
|
res.write(header);
|
||||||
res.write(assembled);
|
res.write(assembled);
|
||||||
@@ -175,6 +205,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
sessions[dev.devId] = s;
|
sessions[dev.devId] = s;
|
||||||
|
camSettings[dev.devId] = { orientation: 0, mirror: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Starting HTTP server on port ${port}`);
|
logger.info(`Starting HTTP server on port ${port}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user