atem-connection
Version:
Typescript Node.js library for connecting with an ATEM switcher.
286 lines • 12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasInternalMultiviewerLabelGeneration = exports.loadFont = exports.calculateGenerateMultiviewerLabelProps = exports.generateMultiviewerLabel = void 0;
const freetype2_1 = require("@julusian/freetype2");
const enums_1 = require("../enums");
const videoMode_1 = require("./videoMode");
const promises_1 = require("fs/promises");
const path = require("path");
/**
* Colour lookup table for converting 8bit grey to the atem encoding
* Note: not every colour is available, so values have been extrapolated to fill in the gaps.
* Also the background colour has been filled in for lower values, to ensure that the text doesnt accidentally remove the background
*/
const colourLookupTable = [
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 29, 85, 132, 17, 30, 30, 96, 136, 136, 186, 101, 101, 13, 19, 19, 72,
217, 217, 124, 130, 123, 47, 28, 51, 32, 192, 166, 56, 167, 193, 189, 172, 198, 198, 41, 41, 154, 81, 109, 109, 199,
60, 108, 37, 12, 12, 15, 153, 142, 142, 68, 35, 35, 173, 141, 197, 197, 144, 94, 94, 116, 42, 214, 214, 40, 46, 46,
184, 181, 127, 127, 175, 31, 31, 150, 213, 73, 95, 86, 191, 191, 117, 135, 135, 208, 22, 78, 78, 107, 179, 179, 210,
119, 58, 58, 67, 209, 209, 171, 110, 89, 89, 103, 133, 43, 202, 137, 66, 66, 39, 151, 129, 71, 200, 200, 26, 111,
128, 16, 16, 23, 23, 196, 201, 36, 106, 207, 205, 205, 69, 139, 62, 105, 90, 203, 203, 216, 54, 99, 99, 100, 88, 88,
146, 145, 74, 74, 80, 204, 204, 180, 84, 138, 212, 215, 24, 24, 63, 187, 187, 118, 148, 183, 183, 57, 102, 102, 206,
44, 152, 152, 190, 134, 134, 59, 182, 87, 87, 48, 174, 125, 104, 140, 140, 188, 92, 131, 178, 64, 70, 70, 76, 65,
79, 121, 113, 176, 176, 177, 211, 75, 75, 120, 82, 82, 93, 98, 122, 122, 53, 115, 115, 45, 55, 34, 34,
];
function fillResolutionSpec(spec) {
return {
...spec,
cornerRight: spec.corner.map((buf) => {
return Buffer.from(buf).reverse();
}),
};
}
const Res4K = fillResolutionSpec({
width: 640,
height: 100,
xPad: 16,
yPadBottom: 16,
yPadTop: 4,
fontHeight: 46,
borderColour: 0x05,
corner: [
Buffer.from([0, 0, 0, 0, 0, 0, 223, 2, 162, 220, 20]),
Buffer.from([0, 0, 0, 0, 223, 195, 20, 5, 5, 5, 5]),
Buffer.from([0, 0, 0, 7, 3, 5, 5, 110, 141, 124, 29]),
Buffer.from([0, 0, 7, 220, 5, 200, 97, 14, 14, 14, 14]),
Buffer.from([0, 223, 3, 5, 209, 29, 14, 14, 14, 14, 14]),
Buffer.from([0, 219, 5, 200, 29, 14, 14, 14, 14, 14, 14]),
Buffer.from([223, 20, 5, 97, 14, 14, 14, 14, 14, 14, 14]),
Buffer.from([2, 5, 110, 14, 14, 14, 14, 14, 14, 14, 14]),
Buffer.from([162, 5, 141, 14, 14, 14, 14, 14, 14, 14, 14]),
Buffer.from([220, 5, 124, 14, 14, 14, 14, 14, 14, 14, 14]),
Buffer.from([20, 5, 29, 14, 14, 14, 14, 14, 14, 14, 14]),
],
});
const Res1080 = fillResolutionSpec({
width: 320,
height: 50,
xPad: 10,
yPadBottom: 10,
yPadTop: 4,
fontHeight: 24,
borderColour: 0x05,
corner: [
Buffer.from([0, 0, 1, 229, 230, 20]),
Buffer.from([0, 7, 158, 5, 5, 5]),
Buffer.from([1, 158, 5, 23, 37, 101]),
Buffer.from([229, 5, 23, 29, 14, 14]),
Buffer.from([230, 5, 37, 14, 14, 14]),
Buffer.from([20, 5, 101, 14, 14, 14]),
],
});
const Res720 = fillResolutionSpec({
width: 320,
height: 40,
xPad: 6,
yPadBottom: 6,
yPadTop: 2,
fontHeight: 17,
borderColour: 170,
corner: [
Buffer.from([0, 0, 160, 169]),
Buffer.from([0, 165, 165, 169]),
Buffer.from([160, 165, 56, 14]),
Buffer.from([169, 169, 14, 14]),
],
});
// const transparentColour = 0 // encoded value
const bgColour = colourLookupTable[0]; // 'background' value
// const borderColour = 0x05 // encoded value
function calculateWidthAndTrimText(face, str, maxWidth) {
let trimmedStr = ''; // currently measured string
let advanceWidth = 0; // width including advance to next char
let textWidth = 0; // width assuming it ends after this char
for (let i = 0; i < str.length; i++) {
const ch = face.loadChar(str.charCodeAt(i), { render: false });
if (advanceWidth + ch.metrics.width / 64 > maxWidth) {
// Char makes the string too wide
break;
}
// We can keep this char
textWidth = advanceWidth + ch.metrics.width / 64;
advanceWidth = advanceWidth + ch.metrics.horiAdvance / 64;
trimmedStr += str[i];
}
return { str: trimmedStr, width: textWidth };
}
function drawTextToBuffer(face, fontScale, buffer, spec, rawText, bufferYOffset, bufferWidth) {
const fontHeight = spec.fontHeight * fontScale;
face.setPixelSizes(fontHeight, fontHeight);
const { width: textWidth, str: newStr } = calculateWidthAndTrimText(face, rawText, spec.width - spec.xPad * 2);
const boundaryWidth = Math.floor(textWidth + spec.xPad * 2);
const boundaryHeight = Math.floor(fontHeight + spec.yPadTop + spec.yPadBottom);
const bufferXOffset = Math.floor((bufferWidth - spec.width) / 2);
// Fill background of boundary, and a 2px border
const boundaryYOffset = Math.max(Math.floor((spec.height - boundaryHeight) / 2.5), 0) + bufferYOffset;
const boundaryXOffset = Math.max(Math.floor((spec.width - boundaryWidth) / 2), 0) + bufferXOffset;
function drawHorizontalLine(y, xOffset, length, colour) {
const offset = (boundaryYOffset + y) * bufferWidth + boundaryXOffset + xOffset;
buffer.fill(colour, offset, offset + length);
}
for (let y = 0; y < boundaryHeight; y++) {
const isBorder = y == 0 || y == 1 || y === boundaryHeight - 1 || y === boundaryHeight - 2;
if (isBorder) {
drawHorizontalLine(y, 0, boundaryWidth, spec.borderColour);
}
else {
drawHorizontalLine(y, 0, 2, spec.borderColour);
drawHorizontalLine(y, boundaryWidth - 2, 2, spec.borderColour);
drawHorizontalLine(y, 2, boundaryWidth - 4, bgColour);
}
}
// Patch on the rounded corners
for (let i = 0; i < spec.corner.length; i++) {
const cornerBufferLeft = spec.corner[i];
const cornerBufferRight = spec.cornerRight[i];
const offsetTopLeft = (boundaryYOffset + i) * bufferWidth + boundaryXOffset;
cornerBufferLeft.copy(buffer, offsetTopLeft);
const offsetBottomLeft = (boundaryYOffset + boundaryHeight - i - 1) * bufferWidth + boundaryXOffset;
cornerBufferLeft.copy(buffer, offsetBottomLeft);
const offsetTopRight = offsetTopLeft + boundaryWidth - cornerBufferRight.length;
cornerBufferRight.copy(buffer, offsetTopRight);
const offsetBottomRight = offsetBottomLeft + boundaryWidth - cornerBufferRight.length;
cornerBufferRight.copy(buffer, offsetBottomRight);
}
const maxLeft = boundaryXOffset + spec.width + spec.xPad;
let charLeft = boundaryXOffset + spec.xPad;
const textTop = boundaryYOffset + fontHeight + spec.yPadTop;
// Draw text characters
for (let i = 0; i < newStr.length; i++) {
face.setTransform(undefined, [charLeft * 64, 0]);
const ch = face.loadChar(newStr.charCodeAt(i), { render: true });
const endCharLeft = charLeft + ch.metrics.horiAdvance / 64;
if (endCharLeft >= maxLeft) {
// guard to avoid buffer index overflow
break;
}
const charTop = Math.floor(textTop - ch.metrics.horiBearingY / 64);
if (ch.bitmap && typeof ch.bitmapLeft === 'number') {
const bitmapLeft = Math.floor(ch.bitmapLeft);
for (let y = 0; y < ch.bitmap.height; y++) {
for (let x = 0; x < ch.bitmap.width; x++) {
const rawCol = ch.bitmap.buffer[x + y * ch.bitmap.width];
const myCol = colourLookupTable[rawCol];
if (myCol !== undefined) {
// If we have a colour, update the image
buffer[x + bitmapLeft + (y + charTop) * bufferWidth] = myCol;
}
}
}
}
charLeft = endCharLeft;
}
}
/**
* Generate a label for the multiviewer at multiple resolutions
* @param face freetype2.FontFace to draw with
* @param str String to write
* @param props Specify which resolutions to generate for
* @returns Buffer
*/
function generateMultiviewerLabel(face, fontScale, str, props) {
// Calculate the sizes
let width;
let height = 0;
// TODO UDH8K
if (props.UHD4K) {
if (!width)
width = Res4K.width;
height += Res4K.height;
}
if (props.HD1080) {
if (!width)
width = Res1080.width;
height += Res1080.height;
}
if (props.HD720) {
if (!width)
width = Res720.width;
height += Res720.height;
}
if (!width || !height)
throw new Error('At least one resolution must be chosen!');
const buffer = Buffer.alloc(width * height);
const width2 = width;
// Don't draw border when name is cleared
if (!str)
return buffer;
let yOffset = 0;
const drawRes = (spec) => {
drawTextToBuffer(face, fontScale, buffer, spec, str, yOffset, width2);
yOffset += spec.height;
};
// TODO UDH8K
if (props.UHD4K)
drawRes(Res4K);
if (props.HD1080)
drawRes(Res1080);
if (props.HD720)
drawRes(Res720);
return buffer;
}
exports.generateMultiviewerLabel = generateMultiviewerLabel;
/**
* Determine which resolutions should be included in the multiview label drawing
* @param state Current device state
* @returns Properties to draw with, or null if they could not be determined
*/
function calculateGenerateMultiviewerLabelProps(state) {
if (state && state.info.supportedVideoModes) {
const res = {
UHD8K: false,
UHD4K: false,
HD1080: false,
HD720: false,
};
const multiViewerModes = new Set();
for (const info of state.info.supportedVideoModes) {
if (!info.requiresReconfig) {
for (const mode of info.multiviewerModes) {
multiViewerModes.add(mode);
}
}
}
for (const mode of multiViewerModes.values()) {
const format = (0, videoMode_1.getVideoModeInfo)(mode)?.format;
switch (format) {
case enums_1.VideoFormat.HD720:
res.HD720 = true;
break;
case enums_1.VideoFormat.HD1080:
res.HD1080 = true;
break;
case enums_1.VideoFormat.UHD4K:
res.UHD4K = true;
break;
case enums_1.VideoFormat.UDH8K:
res.UHD8K = true;
break;
case undefined:
case enums_1.VideoFormat.SD:
// unsupported
break;
}
}
return res;
}
return null;
}
exports.calculateGenerateMultiviewerLabelProps = calculateGenerateMultiviewerLabelProps;
/**
* Load a font file to freetype2
* @param fontPath Path to file
*/
async function loadFont(fontPath) {
if (!fontPath)
fontPath = path.join(__dirname, '../../assets/roboto/Roboto-Regular.ttf');
const fontFile = await (0, promises_1.readFile)(fontPath);
return (0, freetype2_1.NewMemoryFace)(fontFile);
}
exports.loadFont = loadFont;
function hasInternalMultiviewerLabelGeneration(modelId) {
return modelId >= enums_1.Model.Mini;
}
exports.hasInternalMultiviewerLabelGeneration = hasInternalMultiviewerLabelGeneration;
//# sourceMappingURL=multiviewLabel.js.map