From bcec5aa9bccc8950142ec2a1126343a309cdc603 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 26 Feb 2024 01:32:02 +0100 Subject: [PATCH 1/6] keyboard shortcuts for the client --- src/client/main.ts | 79 ++++++++++++++++++++++--------- src/server/JGCPReceiveMessages.ts | 12 +---- src/server/control.ts | 6 +-- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/src/client/main.ts b/src/client/main.ts index 2f06d14..b431cba 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -20,37 +20,41 @@ function open_sequence(e: Event) { const reader = new FileReader(); reader.onload = function(e) { - const raw_data = e.target?.result; - - ws.send(JSON.stringify({ + const message: JGCPRecv.OpenSequence = { command: "open_sequence", - sequence: raw_data - })); + sequence: e.target?.result as string + }; + + ws.send(JSON.stringify(message)); }; reader.readAsText(file); } document.querySelector("#input_open_sequence")?.addEventListener("change", open_sequence); -function button_navigate(type: JGCPRecv.NavigateType, steps: -1 | 1) { - ws.send(JSON.stringify({ +function navigate(type: JGCPRecv.NavigateType, steps: -1 | 1) { + const command: JGCPRecv.Navigate = { command: "navigate", type, steps, client_id: client_id - })); + }; + + ws.send(JSON.stringify((command))); } -document.querySelector("#navigate_item_prev")?.addEventListener("click", () => { button_navigate("item", -1); }); -document.querySelector("#navigate_item_next")?.addEventListener("click", () => { button_navigate("item", 1); }); -document.querySelector("#navigate_slide_prev")?.addEventListener("click", () => { button_navigate("slide", -1); }); -document.querySelector("#navigate_slide_next")?.addEventListener("click", () => { button_navigate("slide", 1); }); +document.querySelector("#navigate_item_prev")?.addEventListener("click", () => { navigate("item", -1); }); +document.querySelector("#navigate_item_next")?.addEventListener("click", () => { navigate("item", 1); }); +document.querySelector("#navigate_slide_prev")?.addEventListener("click", () => { navigate("slide", -1); }); +document.querySelector("#navigate_slide_next")?.addEventListener("click", () => { navigate("slide", 1); }); function button_visibility(visibility: boolean) { - ws.send(JSON.stringify({ + const message: JGCPRecv.SetVisibility = { command: "set_visibility", visibility, client_id: client_id - })); + }; + + ws.send(JSON.stringify(message)); } document.querySelector("#set_visibility_hide")?.addEventListener("click", () => { button_visibility(false); }); document.querySelector("#set_visibility_show")?.addEventListener("click", () => { button_visibility(true); }); @@ -114,11 +118,13 @@ function request_item_slides(item: number) { // clear the selected item document.querySelector(".sequence_item_container.selected")?.classList.remove("selected"); - ws.send(JSON.stringify({ + const command: JGCPRecv.RequestItemSlides = { command: "request_item_slides", item: item, client_id: client_id - })); + }; + + ws.send(JSON.stringify(command)); } } @@ -329,13 +335,15 @@ function select_item(item: number) { } function request_item_slide_select(item: number, slide: number) { - ws.send(JSON.stringify({ + const message: JGCPRecv.SelectItemSlide = { command: "select_item_slide", item: item, slide: slide, client_id: client_id - })); -} + }; + + ws.send(JSON.stringify(message)); +} function set_active_slide(scroll: boolean = false) { // deselect the previous active slide @@ -468,10 +476,6 @@ function ws_connect() { ws.addEventListener("message", (event: MessageEvent) => { const data: JGCPSend.Message = JSON.parse(event.data as string) as JGCPSend.Message; - - console.dir(data); - - const command_parser_map = { sequence_items: display_items, item_slides: display_item_slides, @@ -521,3 +525,32 @@ let active_item_slide = { item: 0, slide: 0 }; + +document.addEventListener("keydown", (event) => { + // exit on composing + if (event.isComposing || event.keyCode === 229) { + return; + } + + if (!event.repeat) { + switch (event.code) { + case "PageUp": + case "ArrowLeft": + navigate("slide", -1); + break; + case "PageDown": + case "ArrowRight": + navigate("slide", 1); + break; + case "ArrowUp": + navigate("item", -1); + break; + case "ArrowDown": + navigate("item", 1); + break; + default: + console.debug(event.code); + break; + } + } +}); \ No newline at end of file diff --git a/src/server/JGCPReceiveMessages.ts b/src/server/JGCPReceiveMessages.ts index 77a9d02..6315e0a 100644 --- a/src/server/JGCPReceiveMessages.ts +++ b/src/server/JGCPReceiveMessages.ts @@ -17,16 +17,10 @@ export interface OpenSequence extends Base { * request for the slides of a specific item */ export interface RequestItemSlides extends Base { - command: "request-item_slides"; + command: "request_item_slides"; item: number; } -export interface ItemSlideSelect extends Base { - command: "select-item-slide"; - item: number; - slide: number; -} - /** * The different navigation-types */ @@ -53,11 +47,9 @@ export interface SelectItemSlide extends Base { command: "select_item_slide"; item: number; slide: number; - } /** * Uniun of the different JGCP-messages */ -export type Message = RequestItemSlides | SetVisibility | OpenSequence | Navigate | SelectItemSlide; - +export type Message = RequestItemSlides | SetVisibility | OpenSequence | Navigate | SelectItemSlide; \ No newline at end of file diff --git a/src/server/control.ts b/src/server/control.ts index cd42894..4888e61 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -35,10 +35,10 @@ class Control { }; // mapping of the websocket-messages to the functions - private readonly ws_function_map = { + private readonly ws_function_map: Record void | Promise> = { open_sequence: (msg: JGCPRecv.OpenSequence, ws: WebSocket) => this.open_sequence(msg?.sequence, ws), request_item_slides: (msg: JGCPRecv.RequestItemSlides, ws: WebSocket) => this.get_item_slides(msg?.item, msg?.client_id, ws), - select_item_slide: (msg: JGCPRecv.ItemSlideSelect, ws: WebSocket) => this.select_item_slide(msg?.item, msg?.slide, msg?.client_id, ws), + select_item_slide: (msg: JGCPRecv.SelectItemSlide, ws: WebSocket) => this.select_item_slide(msg?.item, msg?.slide, msg?.client_id, ws), navigate: (msg: JGCPRecv.Navigate, ws: WebSocket) => this.navigate(msg?.type, msg?.steps, msg?.client_id, ws), set_visibility: (msg: JGCPRecv.SetVisibility, ws: WebSocket) => this.set_visibility(msg.visibility, msg.client_id, ws) }; @@ -288,7 +288,7 @@ class Control { ws_send_response(`command '${data.command}' is not implemented`, false, ws); } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - this.ws_function_map[data.command](data, ws); + void this.ws_function_map[data.command](data, ws); } } } From 577029bd02bc9f0bdadc84a77b187fb6aea70ad2 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Mon, 26 Feb 2024 18:35:58 +0100 Subject: [PATCH 2/6] improved media-handling in the server --- .eslintignore | 1 + README.md | 2 +- casparcg-templates/JohnCG/Countdown.html | 7 + src/client/main.ts | 43 +++--- src/server/JGCPSendMessages.ts | 18 +-- src/server/Sequence.ts | 82 +++++----- src/server/SequenceItems/CommandComment.ts | 65 +++----- src/server/SequenceItems/Comment.ts | 35 ++--- src/server/SequenceItems/Countdown.ts | 97 +++++------- src/server/SequenceItems/Image.ts | 53 ++----- src/server/SequenceItems/SequenceItem.ts | 166 ++++++++++----------- src/server/SequenceItems/Song.ts | 100 ++++++------- src/server/control.ts | 5 +- src/server/servers/http-server.ts | 8 + src/templates/Countdown.ts | 16 ++ 15 files changed, 308 insertions(+), 390 deletions(-) diff --git a/.eslintignore b/.eslintignore index 8cbc828..b45f885 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,6 @@ node_modules out dist casparcg-templates +client license-generator.ts license-reporter.config.ts \ No newline at end of file diff --git a/README.md b/README.md index eef7cb6..f66faed 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,4 @@ Generate lyric-graphics and play them out through CasparCG. - command-comment: define commands / names which get loaded straigth from the start and can be shown anytime - song: transmit template in server->client message instead of hardcoding it into the client - fix "Buffer() is deprecated" -- generate background-image-b64 only if needed \ No newline at end of file +- remove transition from html-template, handle it through casparcg instead \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index ddb3ab9..88a2466 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -57,6 +57,13 @@ transition: opacity 0.5s; } + + #underline { + position: relative; + width:100%; + top: -0.25em; + border-bottom: 0.1em solid; + } diff --git a/src/client/main.ts b/src/client/main.ts index b431cba..f3bd3eb 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -6,6 +6,10 @@ import { ClientItemSlides } from "../server/SequenceItems/SequenceItem"; import * as JGCPSend from "../server/JGCPSendMessages"; import * as JGCPRecv from "../server/JGCPReceiveMessages.js"; import { ActiveItemSlide } from "../server/Sequence.js"; +import { ClientSongSlides } from "../server/SequenceItems/Song.js"; +import { ClientCountdownSlides } from "../server/SequenceItems/Countdown.js"; +import { ClientImageSlides } from "../server/SequenceItems/Image.js"; +import { ClientCommandCommentSlides } from "../server/SequenceItems/CommandComment.js"; const config = { websocket: { @@ -92,6 +96,13 @@ function display_items(data: JGCPSend.Sequence) { // if it's a Countdown-Object, insert the time if (item.type === "Countdown") { + const title_map = { + clock: "Clock", + stopwatch: "Stopwatch", + duration: "Countdown (duration)", + end_time: "Countdown (end time)" + }; + div_sequence_item.innerText = item.Caption.replace("%s", item.Time); } else { div_sequence_item.innerText = item.Caption; @@ -139,16 +150,16 @@ function display_item_slides(data: JGCPSend.ItemSlides) { switch (data.type) { case "Song": - part_arrays = create_song_slides(data as JGCPSend.SongSlides); + part_arrays = create_song_slides(data as ClientSongSlides); break; case "Countdown": - part_arrays = create_image_countdown_slides(data as JGCPSend.CountdownSlides); + part_arrays = create_image_countdown_slides(data as ClientCountdownSlides); break; case "Image": - part_arrays = create_image_countdown_slides(data as JGCPSend.ImageSlides); + part_arrays = create_image_countdown_slides(data as ClientImageSlides); break; case "CommandComment": - part_arrays = create_template_slides(data as JGCPSend.CommandCommentSlides); + part_arrays = create_template_slides(data as ClientCommandCommentSlides); break; default: console.error(`'${data.type}' is not supported`); @@ -161,7 +172,7 @@ function display_item_slides(data: JGCPSend.ItemSlides) { set_active_slide(data.client_id === client_id); } -function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { +function create_song_slides(data: ClientSongSlides): HTMLDivElement[] { let slide_counter: number = 0; // create the individual arrays for parallel processing @@ -216,7 +227,7 @@ function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { return part_arrays; } -function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend.ImageSlides): HTMLDivElement[] { +function create_image_countdown_slides(data: ClientCountdownSlides | ClientImageSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -240,7 +251,7 @@ function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend return [div_slide_part]; } -function create_template_slides(data: JGCPSend.CommandCommentSlides): HTMLDivElement[] { +function create_template_slides(data: ClientCommandCommentSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -270,21 +281,11 @@ function create_slide_object(data: ClientItemSlides, number: number) { const slide_object = document.createElement("object"); - const template_data: object & { template?: { template: string, data: object } } = data.slides_template; - - switch (data.type) { - case "Song": - slide_object.data = "Templates/JohnCG/Song.html"; - break; - case "Countdown": - slide_object.data = "Templates/JohnCG/Countdown.html"; - break; - case "CommandComment": - slide_object.data = `Templates/${data.slides_template.template.template}.html`; - break; + if (data.template !== undefined) { + slide_object.data = "Templates/" + data.template.template; } - slide_object.style.backgroundImage = `url("${data.slides_template.background_image?.replace(/\\/g, "\\\\") ?? ""}")`; + slide_object.style.backgroundImage = `url("${data.media_b64}")`; slide_object.classList.add("slide"); @@ -301,7 +302,7 @@ function create_slide_object(data: ClientItemSlides, number: number) { }); slide_object.addEventListener("load", () => { - slide_object.contentWindow?.update(JSON.stringify({ ...template_data?.template.data, mute_transition: true })); + slide_object.contentWindow?.update(JSON.stringify({ ...data.template.data, mute_transition: true })); switch (data.type) { case "Song": diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 8489cf2..158296d 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,8 +1,5 @@ import * as SequenceClass from "../server/Sequence"; -import { ClientCountdownSlides } from "../server/SequenceItems/Countdown"; -import { ClientSongSlides } from "../server/SequenceItems/Song"; -import { ClientCommandCommentSlides } from "./SequenceItems/CommandComment"; -import { ClientImageSlides } from "./SequenceItems/Image"; +import { ClientItemSlides } from "./SequenceItems/SequenceItem"; /** * Base interface for sent JGCP-messages @@ -41,18 +38,7 @@ interface ItemSlidesBase extends Base{ command: "item_slides"; } -export type SongSlides = ItemSlidesBase & ClientSongSlides; - -export type CountdownSlides = ItemSlidesBase & ClientCountdownSlides; - -export type ImageSlides = ItemSlidesBase & ClientImageSlides; - -export type CommandCommentSlides = ItemSlidesBase & ClientCommandCommentSlides; - -// temporary until full feature set -export type NotImplementedSlides = ItemSlidesBase & { type: string; item: number; }; - -export type ItemSlides = SongSlides | CountdownSlides | NotImplementedSlides; +export type ItemSlides = ClientItemSlides & ItemSlidesBase; export interface Clear extends Base { command: "clear"; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 33eb4e8..afeea13 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -4,7 +4,7 @@ import mime from "mime-types"; import { XMLParser } from "fast-xml-parser"; import { SongElement } from "./SequenceItems/SongFile"; -import { ClientItemSlides, ItemProps, ItemPropsBase, ItemRenderObject, SequenceItem } from "./SequenceItems/SequenceItem"; +import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItem } from "./SequenceItems/SequenceItem"; import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; @@ -224,7 +224,7 @@ class Sequence { const props: CommandCommentProps = { ...item_data, type: "CommandComment", - ...caption_json as { template: string, data?: object } + template: caption_json as { template: string, data: object } }; this.sequence_items.push(new CommandComment(props)); @@ -377,45 +377,52 @@ class Sequence { // eslint-disable-next-line @typescript-eslint/no-misused-promises connections.forEach(async (connection) => { - // generate the render-object - const render_object = await this.active_sequence_item.create_render_object(); - - await this.casparcg_load_media(connection, render_object); + await this.casparcg_load_media(connection); if (!media_only) { - void this.casparcg_load_template(connection, render_object); + void this.casparcg_load_template(connection); } }); } - private casparcg_load_media(casparcg_connection: CasparCGConnection, render_object: ItemRenderObject): Promise> { + private casparcg_load_media(casparcg_connection: CasparCGConnection): Promise> { + let media = this.active_sequence_item.props.media; + + let casparcg_media: string; + // if a media-file is defined, load it - if (render_object.media) { - // make it all uppercase and remove the extension to match casparcg-clips and make sure it uses forward slashes - const req_name = render_object.media.replace(/\.[^(\\.]+$/, "").toUpperCase().replace(/\\/g, "/"); + if (media) { + // test wether it is a color-string + const test_rgb_string = media.match(/^#(?[\dA-Fa-f]{2})?(?(?:[\dA-Fa-f]{2}){3})$/); - let media_result: ClipInfo | undefined; + // if it is an rgb-string, put the alpha-value at the beginning (something something CasparCG) + if (test_rgb_string) { + media = `#${test_rgb_string.groups.alpha ?? ""}${test_rgb_string.groups.rgb}`; + } else { + // make it all uppercase and remove the extension to match casparcg-clips + const req_name = media.replace(/\.[^(\\.]+$/, "").toUpperCase(); - // check all the casparcg-files, wether they contain a media-file that matches the path - for (const m of casparcg_connection.media) { - const media_file = m.clip.toUpperCase().replace(/\\/, "/"); + // check all the casparcg-files, wether they contain a media-file that matches the path + for (const m of casparcg_connection.media) { + const media_file = m.clip.toUpperCase().replace(/\\/, "/"); - if (req_name.endsWith(media_file)) { - media_result = m; - break; + if (req_name.endsWith(media_file)) { + casparcg_media = m.clip; + break; + } } } // if a matching media-file was found, use it - if (media_result !== undefined) { + if (casparcg_media) { // if the state is "visible", play it directly if (this.visibility) { return casparcg_connection.connection.play({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0], - clip: media_result.clip, - transition: !render_object.mute_transition ? this.casparcg_transition : undefined + clip: casparcg_media, + transition: this.casparcg_transition /* eslint-enable @typescript-eslint/naming-convention */ }); } else { @@ -424,24 +431,17 @@ class Sequence { /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0], - clip: media_result.clip, - transition: !render_object.mute_transition ? this.casparcg_transition : undefined + clip: casparcg_media, + transition: this.casparcg_transition /* eslint-enable @typescript-eslint/naming-convention */ }); } } else { - const command = this.visibility ? Commands.PlayHtml : Commands.LoadbgHtml; - - return casparcg_connection.connection.executeCommand({ - /* eslint-disable @typescript-eslint/naming-convention */ - command: command, - params: { - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0], - url: JSON.stringify(render_object.background_image), - transition: !render_object.mute_transition ? this.casparcg_transition : undefined - /* eslint-enable @typescript-eslint/naming-convention */ - } + return casparcg_connection.connection.play({ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[0], + clip: media.replace(/^(?\w:)\//, "$//"), + transition: this.casparcg_transition }); } } else { @@ -451,24 +451,24 @@ class Sequence { channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0], clip: "EMPTY", - transition: !render_object.mute_transition ? this.casparcg_transition : undefined + transition: this.casparcg_transition /* eslint-enable @typescript-eslint/naming-convention */ }); } } - private casparcg_load_template(casparcg_connection: CasparCGConnection, render_object: ItemRenderObject): Promise> { + private casparcg_load_template(casparcg_connection: CasparCGConnection): Promise> { // if a template was specified, load it - if (render_object.template !== undefined) { + if (this.active_sequence_item.template !== undefined) { return casparcg_connection.connection.cgAdd({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[1], cgLayer: 0, playOnLoad: this.casparcg_visibility, - template: render_object.template.template, + template: this.active_sequence_item.template.template, // escape quotation-marks by hand, since the old chrom-version of casparcg appears to have a bug - data: JSON.stringify(JSON.stringify(render_object.template.data, (_key, val: unknown) => { + data: JSON.stringify(JSON.stringify(this.active_sequence_item.template.data, (_key, val: unknown) => { if (typeof val === "string") { return val.replace("\"", "\\u0022"); } else { @@ -484,7 +484,7 @@ class Sequence { channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[1], clip: "EMPTY", - transition: !render_object.mute_transition ? this.casparcg_transition : undefined + transition: this.casparcg_transition /* eslint-enable @typescript-eslint/naming-convention */ }); } diff --git a/src/server/SequenceItems/CommandComment.ts b/src/server/SequenceItems/CommandComment.ts index b727dfb..6731a6e 100644 --- a/src/server/SequenceItems/CommandComment.ts +++ b/src/server/SequenceItems/CommandComment.ts @@ -1,23 +1,19 @@ -import { ClientItemSlides, ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; -export interface CommandCommentProps extends ItemPropsBase { - type: "CommandComment"; +export interface CommandCommentTemplate { template: string; - data?: object; + data: object | undefined; } -export interface ClientCommandCommentSlides extends ClientItemSlidesBase { +export interface CommandCommentProps extends ItemPropsBase { type: "CommandComment"; - slides_template: CommandCommentRenderObject & { mute_transition: true }; + template: CommandCommentTemplate; } -export interface CommandCommentRenderObject extends ItemRenderObjectBase { +export interface ClientCommandCommentSlides extends ClientItemSlidesBase { type: "CommandComment"; - caspar_type: "template"; - template: { - template: string; - data: object; - }; + media_b64?: undefined; + template: CommandCommentTemplate; } export default class CommandComment extends SequenceItemBase { @@ -30,41 +26,22 @@ export default class CommandComment extends SequenceItemBase { this.item_props = props; - this.item_props.Caption = `Template: "${this.props.template.toUpperCase()}"`; + this.item_props.Caption = `Template: "${this.props.template.template.toUpperCase()}"`; - if (this.props.data) { - this.item_props.Caption += ` (${JSON.stringify(this.props.data, undefined, " ").slice(3, -2)})`; + if (this.props.template.data) { + this.item_props.Caption += ` (${JSON.stringify(this.props.template.data, undefined, " ").slice(3, -2)})`; } } - async create_client_object_item_slides(): Promise { - return { + create_client_object_item_slides(): Promise { + const { Caption: title, template } = this.props; + + return Promise.resolve({ type: "CommandComment", - title: this.props.Caption, + title, item: this.props.item, slides: [], - slides_template: { - ...await this.create_render_object(), - mute_transition: true - } - }; - } - - create_render_object(): Promise { - return new Promise((resolve) => { - resolve({ - type: "CommandComment", - caspar_type: "template", - slide: 0, - slides: [{ - template: this.props.template, - data: this.props.data - }], - template: { - template: this.props.template, - data: this.props.data - } - }); + template }); } @@ -76,10 +53,6 @@ export default class CommandComment extends SequenceItemBase { set_active_slide(): number { return 0; } - - protected get_background_image(): Promise { - return new Promise((resolve) => resolve("")); - } get active_slide(): number { return 0; @@ -88,4 +61,8 @@ export default class CommandComment extends SequenceItemBase { get props(): CommandCommentProps { return this.item_props; } + + get template(): CommandCommentTemplate { + return this.props.template; + } } \ No newline at end of file diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index 219e3af..bd2d028 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -1,4 +1,4 @@ -import { ClientItemSlidesBase, ItemProps, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemProps, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; export interface CommentProps extends ItemPropsBase { type: "Comment"; @@ -7,11 +7,6 @@ export interface CommentProps extends ItemPropsBase { export interface ClientCommentSlides extends ClientItemSlidesBase { type: "Comment"; - slides_template: CommentRenderObject & { mute_transition: true; }; -} - -export interface CommentRenderObject extends ItemRenderObjectBase { - caspar_type: "media"; } export default class Comment extends SequenceItemBase { @@ -28,24 +23,16 @@ export default class Comment extends SequenceItemBase { } create_client_object_item_slides(): Promise { - return new Promise((resolve) => { - resolve({ - type: "Comment", - title: this.props.Caption, - item: this.props.item, - slides: [], - slides_template: { - caspar_type: "media", - slides: [], - slide: 0, - mute_transition: true - } - }); + return Promise.resolve({ + type: "Comment", + title: this.props.Caption, + item: this.props.item, + slides: [] }); } create_render_object(): Promise { - return new Promise((resolve) => resolve(undefined)); + return Promise.resolve(undefined); } navigate_slide(steps: number): number { @@ -56,10 +43,6 @@ export default class Comment extends SequenceItemBase { set_active_slide(): number { return 0; } - - protected get_background_image(): Promise { - return new Promise((resolve) => resolve("")); - } get active_slide(): number { return -1; @@ -68,4 +51,8 @@ export default class Comment extends SequenceItemBase { get props(): ItemProps { return this.item_props; } + + get template(): undefined { + return undefined; + } } \ No newline at end of file diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 460d93e..3a4b489 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -1,7 +1,5 @@ -import path from "path"; import { convert_color_to_hex } from "../Sequence"; -import Config from "../config"; -import { ClientItemSlidesBase, DeepPartial, FontFormat, ItemPropsBase, ItemRenderObjectBase, ItemTemplateData, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, DeepPartial, FontFormat, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; const countdown_mode_items = ["duration", "end_time", "stopwatch", "clock"]; type CountdownMode = (typeof countdown_mode_items)[number]; @@ -16,14 +14,16 @@ interface CountdownSequenceItemProps extends ItemPropsBase { /* eslint-enable @typescript-eslint/naming-convention */ } +export interface CountdownTemplate { + template: "JohnCG/Countdown"; + data: CountdownTemplateData; +} + export interface CountdownProps extends CountdownSequenceItemProps { - font_format: FontFormat; - position: CountdownPosition; - mode: CountdownMode; - show_seconds: boolean; // eslint-disable-next-line @typescript-eslint/naming-convention fileName?: string; background_color?: string; + template: CountdownTemplate; } export interface ClientCountdownSlides extends ClientItemSlidesBase { @@ -32,10 +32,11 @@ export interface ClientCountdownSlides extends ClientItemSlidesBase { time: string; mode: CountdownMode; }], - slides_template: CountdownRenderObject & { mute_transition: true; }; + media_b64: string; + template: CountdownTemplate; } -export interface CountdownTemplateData extends ItemTemplateData { +export interface CountdownTemplateData { position: CountdownPosition; font_format: FontFormat; time: string; @@ -43,14 +44,6 @@ export interface CountdownTemplateData extends ItemTemplateData { mode: CountdownMode; } -export interface CountdownRenderObject extends ItemRenderObjectBase { - type: "Countdown"; - template: { - template: "JohnCG/Countdown", - data: CountdownTemplateData - } -} - // data from in the hex-string of the countdown, uses CSS-notation for easy translation in the renderer interface CountdownData { mode: CountdownMode; @@ -83,17 +76,25 @@ export default class Countdown extends SequenceItemBase { this.item_props = { ...props, - position: { - x: hex_data.x, - y: hex_data.y - }, - show_seconds: hex_data.show_seconds, - font_format: hex_data.font_format, - mode: hex_data.mode, // eslint-disable-next-line @typescript-eslint/naming-convention - fileName: hex_data.background_image, - background_color: hex_data.background_color + background_image: hex_data.background_image, + background_color: hex_data.background_color, + template: { + template: "JohnCG/Countdown", + data: { + font_format: hex_data.font_format, + show_seconds: hex_data.show_seconds, + mode: hex_data.mode, + position: { + x: hex_data.x, + y: hex_data.y + }, + time: props.Time + } + } }; + + this.item_props.media = this.get_background_image(this.props.background_image); } navigate_slide(steps: number): number { @@ -125,50 +126,18 @@ export default class Countdown extends SequenceItemBase { }; return { - title: `${title_map[this.props.mode]}: ${this.props.Time}`, + title: `${title_map[this.template.data.mode]}: ${this.props.Time}`, type: this.props.type, item: this.props.item, slides: [{ - mode: this.props.mode, + mode: this.template.data.mode, time: this.props.Time }], - slides_template: { - ...await this.create_render_object(true), - mute_transition: true - } + media_b64: await this.get_media_b64(true), + template: this.props.template }; } - async create_render_object(proxy?: boolean): Promise { - return { - type: "Countdown", - background_image: await this.get_background_image(proxy), - slide: 0, - slides: [], - template: { - template: "JohnCG/Countdown", - data: { - time: this.props.Time, - font_format: this.props.font_format, - position: this.props.position, - show_seconds: this.props.show_seconds, - mode: this.props.mode - } - } - }; - } - - protected async get_background_image(proxy?: boolean): Promise { - // check wether the images have yet been laoded - if (this.props.BackgroundImage === undefined) { - const image_path = path.join(Config.path.background_image, this.props.fileName ?? ""); - - await this.load_background_images(image_path, this.props.background_color); - } - - return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; - } - get active_slide(): number { // always return 0, because there is only 1 slide return 0; @@ -177,6 +146,10 @@ export default class Countdown extends SequenceItemBase { get props(): CountdownProps { return this.item_props; } + + get template(): CountdownTemplate { + return this.props.template; + } } function parse_hex_data(data_hex: string): CountdownData { diff --git a/src/server/SequenceItems/Image.ts b/src/server/SequenceItems/Image.ts index 8569b27..17e63a5 100644 --- a/src/server/SequenceItems/Image.ts +++ b/src/server/SequenceItems/Image.ts @@ -1,26 +1,19 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import path from "path"; -import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, SequenceItemBase } from "./SequenceItem"; -import Config from "../config"; +import { ClientItemSlidesBase, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; +import { get_song_path } from "./Song"; export interface ImageProps extends ItemPropsBase { /* eslint-disable @typescript-eslint/naming-convention */ type: "Image"; FileName: string; - /* eslint-enable @typescript-eslint/naming-convention */ -} - -export interface ImageRenderObject extends ItemRenderObjectBase { - caspar_type: "media"; - file_name: string; - slides: []; media: string; + /* eslint-enable @typescript-eslint/naming-convention */ } export interface ClientImageSlides extends ClientItemSlidesBase { type: "Image"; - slides: [], - slides_template: ImageRenderObject & { mute_transition: true; }; + media_b64: string; + template?: undefined; } export default class Image extends SequenceItemBase { @@ -32,6 +25,8 @@ export default class Image extends SequenceItemBase { super(); this.item_props = props; + + this.item_props.media = get_song_path(this.props.FileName); } set_active_slide(slide?: number): number { @@ -46,21 +41,7 @@ export default class Image extends SequenceItemBase { type: "Image", item: this.props.item, slides: [], - slides_template: { - ...await this.create_render_object(true), - mute_transition: true - } - }; - } - - async create_render_object(proxy?: boolean): Promise { - return { - caspar_type: "media", - slide: 0, - slides: [], - file_name: this.props.FileName, - media: this.props.FileName, - background_image: await this.get_background_image(proxy) + media_b64: await this.get_media_b64(true) }; } @@ -78,20 +59,6 @@ export default class Image extends SequenceItemBase { return steps; } - protected async get_background_image(proxy?: boolean): Promise { - // check wether the images have yet been laoded - if (this.props.BackgroundImage === undefined) { - // if the filename is absolute, use it as an filename, if not resolve it relative to the song-path (because Songbeamer) - const filename = path.isAbsolute(this.props.FileName) ? - this.props.FileName - : path.resolve(Config.path.song, this.props.FileName); - - await this.load_background_images(filename); - } - - return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; - } - get active_slide(): number { return 0; } @@ -99,4 +66,8 @@ export default class Image extends SequenceItemBase { get props(): ImageProps { return this.item_props; } + + get template(): undefined { + return undefined; + } } \ No newline at end of file diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 1a7eccc..3f36f43 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -2,11 +2,13 @@ import { promises as fs } from "fs"; import mime from "mime-types"; import sharp, { AvifOptions, FormatEnum, GifOptions, HeifOptions, Jp2Options, JpegOptions, JxlOptions, OutputOptions, PngOptions, TiffOptions, WebpOptions } from "sharp"; -import Song, { ClientSongSlides, SongProps, SongRenderObject } from "./Song"; -import Countdown, { ClientCountdownSlides, CountdownProps, CountdownRenderObject } from "./Countdown"; -import Comment, { ClientCommentSlides, CommentProps, CommentRenderObject } from "./Comment"; -import Image, { ClientImageSlides, ImageProps, ImageRenderObject } from "./Image"; -import CommandComment, { ClientCommandCommentSlides, CommandCommentProps, CommandCommentRenderObject } from "./CommandComment"; +import Song, { ClientSongSlides, SongProps, SongTemplate } from "./Song"; +import Countdown, { ClientCountdownSlides, CountdownProps, CountdownTemplate } from "./Countdown"; +import Comment, { ClientCommentSlides, CommentProps } from "./Comment"; +import Image, { ClientImageSlides, ImageProps } from "./Image"; +import CommandComment, { ClientCommandCommentSlides, CommandCommentProps, CommandCommentTemplate } from "./CommandComment"; +import Config from "../config"; +import path from "path"; export type SequenceItem = Song | Countdown | Comment | Image | CommandComment; @@ -14,6 +16,8 @@ export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; }; +export type Template = CountdownTemplate | SongTemplate | CommandCommentTemplate; + export interface ItemPropsBase { /* eslint-disable @typescript-eslint/naming-convention */ type: string; @@ -22,41 +26,25 @@ export interface ItemPropsBase { Color: string; item: number; selectable: boolean; - BackgroundImage?: { - orig: string; - proxy: string; - } + background_color?: string; + background_image?: string; + media?: string; + template?: Template; /* eslint-enable @typescript-eslint/naming-convention */ } export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps | CommandCommentProps; -export interface ItemTemplateData { - mute_transition?: boolean; -} - -// interface for a renderer-object -export interface ItemRenderObjectBase { +export interface ClientItemSlidesBase { + type: string; + title: string; + item: number; slides: Array; - slide: number; - media?: string; + media_b64?: string; template?: { template: string; data: object; }; - background_image?: string; - background_color?: string; - mute_transition?: boolean; -} - -export type ItemRenderObject = SongRenderObject | CountdownRenderObject | CommentRenderObject | ImageRenderObject | CommandCommentRenderObject; - -export interface ClientItemSlidesBase { - type: string; - title: string; - item: number; - slides: object; - slides_template: ItemRenderObject & { mute_transition: true; }; } export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides | ClientCommandCommentSlides; @@ -67,7 +55,7 @@ export interface FontFormat { fontSize: number; fontWeight?: "bold"; fontStyle?: "italic"; - fontDecoration?: "underline"; + textDecoration?: "underline"; color: string; /* eslint-enable @typescript-eslint/naming-convention */ } @@ -76,7 +64,7 @@ export abstract class SequenceItemBase { protected abstract item_props: ItemProps; protected abstract slide_count: number; - abstract create_render_object(proxy?: boolean, slide?: number); + // abstract create_render_object(proxy?: boolean, slide?: number); abstract create_client_object_item_slides(): Promise; abstract set_active_slide(slide?: number): number; @@ -107,22 +95,28 @@ export abstract class SequenceItemBase { return slide; } - protected async load_background_images(image_path?: string, background_color?: string) { + async get_media_b64(proxy: boolean = false): Promise { let img: sharp.Sharp = undefined; - let img_proxy: sharp.Sharp = undefined; + + // if no background-color is specified, set it to transparent + const background_color = this.props.background_color ?? "#00000000"; - if (image_path !== undefined) { + if (this.media !== undefined) { try { - img = sharp(await fs.readFile(image_path)); + img = sharp(await fs.readFile(this.media)); + + // if a proxy is requested, downscale teh image + if (proxy) { + img.resize(240); + } - img_proxy = img.clone().resize(240); } catch (e) { - ""; + /* empty */ } } - // if the image_buffer is still undefined, try to use the backgroundColor - if (img === undefined && background_color !== undefined) { + // if the image_buffer is still undefined, use the backgroundColor + if (img === undefined) { img = sharp({ create: { width: 1, @@ -131,54 +125,60 @@ export abstract class SequenceItemBase { background: background_color } }).png(); - - // copy the the image to the proxy buffer, since only 1px anyway - img_proxy = img; } - if (img !== undefined && img_proxy !== undefined) { - this.item_props.BackgroundImage = { - // if there is no image-path, the mime-type is PNG, since we created them from the background-color - orig: `data:${mime.lookup(image_path ?? ".png")};base64,` + (await (img.toBuffer())).toString("base64"), - proxy: `data:${mime.lookup(image_path ?? ".png")};base64,` + (await (img_proxy.toBuffer())).toString("base64") - }; - - const sharp_formats: [keyof FormatEnum, - options?: - | OutputOptions - | JpegOptions - | PngOptions - | WebpOptions - | AvifOptions - | HeifOptions - | JxlOptions - | GifOptions - | Jp2Options - | TiffOptions - ][] - = [ - ["webp", { lossless: true }], - ["jpg", { quality: 100 }] - ]; - - // check wether the base64-string is too long - while (this.item_props.BackgroundImage.orig.length > 2097152) { - const [format, options] = sharp_formats.shift(); - this.item_props.BackgroundImage = { - // if there is no image-path, the mime-type is PNG, since we created them from the background-color - orig: `data:${mime.lookup(`.${format}`)};base64,` + (await (img.toFormat(format, options)).toBuffer()).toString("base64"), - proxy: `data:${mime.lookup(`.${format}`)};base64,` + (await (img_proxy.jpeg({ quality: 100 }).toBuffer())).toString("base64") - }; - } - - } else { - this.item_props.BackgroundImage = { orig: "", proxy: "" }; + const pack_b64_string = async (img: sharp.Sharp, path: string = this.props.background_image) => `data:${mime.lookup(path)};base64,` + (await img.toBuffer()).toString("base64"); + + let ret_string: string = await pack_b64_string(img); + + const sharp_formats: [keyof FormatEnum, + options?: + | OutputOptions + | JpegOptions + | PngOptions + | WebpOptions + | AvifOptions + | HeifOptions + | JxlOptions + | GifOptions + | Jp2Options + | TiffOptions + ][] + = [ + ["webp", { lossless: true }], + ["jpg", { quality: 100 }] + ]; + + while (ret_string.length > 2097152) { + const [format, options] = sharp_formats.shift(); + + ret_string = await pack_b64_string(img.toFormat(format, options), `.${format}`); } + + return ret_string; } - get props(): ItemProps { - return this.item_props; + abstract get props(): ItemProps; + + get media(): string { + return this.props.media; + } + + protected resolve_image_path(img_path: string): string { + const return_path = path.isAbsolute(img_path) ? img_path : path.resolve(Config.path.background_image, img_path); + + return return_path.replaceAll("\\", "/"); + } + + protected get_background_image(img_path: string = this.props.background_image): string { + // if it is not defined, return the backgroundcolor instead + if (img_path === undefined) { + // if the background-color too isn't defined, return transparency + return this.props.background_color ?? "#00000000"; + } else { + return this.resolve_image_path(img_path); + } } - protected abstract get_background_image(proxy?: boolean): Promise; + abstract get template(): Template; } diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index 9b3bd92..1034fc3 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -1,9 +1,14 @@ -import { ClientItemSlidesBase, ItemPropsBase, ItemRenderObjectBase, ItemTemplateData, SequenceItemBase } from "./SequenceItem"; +import { ClientItemSlidesBase, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; import SongFile, { ItemPartClient, LyricPart, LyricPartClient, TitlePart } from "./SongFile"; import path from "path"; import Config from "../config"; +export interface SongTemplate { + template: "JohnCG/Song"; + data: SongTemplateData; +} + export interface SongProps extends ItemPropsBase { /* eslint-disable @typescript-eslint/naming-convention */ type: "Song"; @@ -11,6 +16,8 @@ export interface SongProps extends ItemPropsBase { VerseOrder?: string[]; Language?: number; PrimaryLanguage?: number; + media?: string; + template?: SongTemplate; /* eslint-enable @typescript-eslint/naming-convention */ } @@ -24,25 +31,17 @@ export interface LyricSlide { export type ItemSlide = LyricSlide | TitleSlide; -export interface SongTemplateData extends ItemTemplateData { +export interface SongTemplateData { slides: ItemSlide[]; languages: number[]; slide: number; } -export interface SongRenderObject extends ItemRenderObjectBase { - caspar_type: "template"; - type: "Song"; - template: { - template: "JohnCG/Song", - data: SongTemplateData - } -} - export interface ClientSongSlides extends ClientItemSlidesBase { type: "Song" slides: ItemPartClient[]; - slides_template: SongRenderObject & { mute_transition: true; }; + media_b64: string; + template: SongTemplate; } export default class Song extends SequenceItemBase { @@ -102,6 +101,15 @@ export default class Song extends SequenceItemBase { } } } + + // store the media + this.item_props.media = this.get_background_image(this.song_file.metadata.BackgroundImage); + + // create the template data + this.item_props.template = { + template: "JohnCG/Song", + data: this.create_template_data() + }; } get_verse_order(): string[] { @@ -114,32 +122,15 @@ export default class Song extends SequenceItemBase { } } - async create_render_object(proxy?: boolean, slide?: number): Promise { - if (slide === undefined) { - slide = this.active_slide; - } - - slide = this.validate_slide_number(slide); + create_template_data(slide?: number) { + slide = this.active_slide ?? this.validate_slide_number(slide); - const return_object: SongRenderObject = { - type: "Song", - caspar_type: "template", + const return_object: SongTemplateData = { slide, slides: [ this.song_file.part_title ], - background_image: await this.get_background_image(proxy), - template: { - template: "JohnCG/Song", - data: { - languages: this.languages, - slides: [ - this.song_file.part_title - ], - slide - } - }, - media: this.song_file.metadata.BackgroundImage + languages: this.languages }; // add the individual parts to the output-object @@ -156,15 +147,15 @@ export default class Song extends SequenceItemBase { // if a part is not available, skip it if (part !== undefined){ // add the individual slides of the part to the output object - for (const slide of part.slides) { - const slide_obj = { + part.slides.forEach((slide) => { + + const slide_obj: LyricSlide = { type: part.type, data: slide }; - + return_object.slides.push(slide_obj); - return_object.template.data.slides.push(slide_obj); - } + }); } } @@ -214,14 +205,12 @@ export default class Song extends SequenceItemBase { const return_item: ClientSongSlides = { type: "Song", title: this.item_props.Caption, - item: this.item_props.item, + item: this.props.item, slides: [ this.song_file.get_title_client() ], - slides_template: { - ...await this.create_render_object(true, 0), - mute_transition: true - } + media_b64: await this.get_media_b64(true), + template: this.props.template }; for (const part_name of this.get_verse_order()) { @@ -244,21 +233,24 @@ export default class Song extends SequenceItemBase { return return_item; } - protected async get_background_image(proxy?: boolean): Promise { - // check wether the images have yet been laoded - if (this.props.BackgroundImage === undefined) { - const image_path = path.join(Config.path.background_image, this.song_file.metadata.BackgroundImage ?? ""); - - await this.load_background_images(image_path); - } - - return this.props.BackgroundImage[proxy ? "proxy" : "orig"]; + get props(): SongProps { + return this.item_props; } get active_slide(): number { return this.active_slide_number; } + + get template(): SongTemplate { + const template = structuredClone(this.props.template); + template.data.slide = this.active_slide; + + return template; + } } -function get_song_path(song_path: string): string { - return path.join(Config.path.song, song_path); + +export function get_song_path(song_path: string): string { + const return_path = path.isAbsolute(song_path) ? song_path : path.resolve(Config.path.song, song_path); + + return return_path.replaceAll("\\", "/"); } diff --git a/src/server/control.ts b/src/server/control.ts index 4888e61..f6b0b80 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -35,7 +35,7 @@ class Control { }; // mapping of the websocket-messages to the functions - private readonly ws_function_map: Record void | Promise> = { + private readonly ws_function_map = { open_sequence: (msg: JGCPRecv.OpenSequence, ws: WebSocket) => this.open_sequence(msg?.sequence, ws), request_item_slides: (msg: JGCPRecv.RequestItemSlides, ws: WebSocket) => this.get_item_slides(msg?.item, msg?.client_id, ws), select_item_slide: (msg: JGCPRecv.SelectItemSlide, ws: WebSocket) => this.select_item_slide(msg?.item, msg?.slide, msg?.client_id, ws), @@ -287,8 +287,7 @@ class Control { } else if (!Object.keys(this.ws_function_map).includes(data.command)) { ws_send_response(`command '${data.command}' is not implemented`, false, ws); } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - void this.ws_function_map[data.command](data, ws); + void this.ws_function_map[data.command](data as never, ws); } } } diff --git a/src/server/servers/http-server.ts b/src/server/servers/http-server.ts index 3f5ce95..eb55806 100644 --- a/src/server/servers/http-server.ts +++ b/src/server/servers/http-server.ts @@ -32,6 +32,14 @@ class HTTPServer { case /^\/Templates\//.test(request.url): resource_dir = Config.casparcg.templates; request.url = request.url.replace(/\/Templates\//, ""); + + // check, wether the file exits + try { + fs.accessSync(path.join(resource_dir, request.url)); + } catch (e) { + // if it doesn't exist, add an html-extension + request.url += ".html"; + } break; // serve the background-images case /^\/BackgroundImage\//.test(request.url): diff --git a/src/templates/Countdown.ts b/src/templates/Countdown.ts index be2067e..f55b0b2 100644 --- a/src/templates/Countdown.ts +++ b/src/templates/Countdown.ts @@ -54,6 +54,8 @@ function update(str_args: string) { const time_div = document.querySelector("#time"); if (time_div === null) { return; + } else { + time_div.innerHTML = ""; } const colon_hm: HTMLSpanElement = document.createElement("span"); @@ -77,6 +79,12 @@ function update(str_args: string) { } }); + // create an overlaying div for the underline + const underline_div = document.createElement("div"); + underline_div.classList.add("underline"); + underline_div.id = "underline"; + time_div.append(underline_div); + // if the position is undefined, set them to center if (data.position.x === undefined) { data.position.x = 50; @@ -92,6 +100,14 @@ function update(str_args: string) { fontSize: `${data.font_format.fontSize}em` }; + // remove the underline property and show / hide the underline div + if (this_format.textDecoration === "underline") { + delete this_format.textDecoration; + document.querySelector("#underline").style.display = ""; + } else { + document.querySelector("#underline").style.display = "none"; + } + Object.entries(this_format).forEach(([key, val]) => { time_div.style[key] = val; }); From e9fd4fd8b159686d7f9a5160f1fa9dedc04771a7 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Tue, 27 Feb 2024 17:30:48 +0100 Subject: [PATCH 3/6] Moved CasparCG in the control-class and support for different aspect-ratios --- README.md | 9 +- casparcg-templates/JohnCG/Countdown.html | 2 +- client/main.css | 49 +++++---- package.json | 4 +- src/client/main.ts | 42 ++++---- src/server/JGCPSendMessages.ts | 15 ++- src/server/Sequence.ts | 120 +++++++++-------------- src/server/SequenceItems/Comment.ts | 4 - src/server/SequenceItems/Countdown.ts | 4 +- src/server/SequenceItems/SequenceItem.ts | 1 - src/server/control.ts | 87 +++++++++++++++- src/templates/Countdown.ts | 4 +- src/templates/Song.ts | 6 +- 13 files changed, 209 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index f66faed..2082e21 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Generate lyric-graphics and play them out through CasparCG. - move the file `song.html` into your CasparCG template directory ## roadmap -- implement other sequence-items than song +- implement other sequence-items than song (Missing: Video, Bible, Psalm, PowerPoint, PDF, Diashow / Multi-Image, Music) - client: information about connection (active / reconnecting / ...) - companion integration (buttons for song parts -> send name to casparcg) - try to get the template and client to use the settings file (CSS has default values, send song data overwrites them) @@ -18,16 +18,11 @@ Generate lyric-graphics and play them out through CasparCG. - client communication with osc over websocket? - add support for NodeCG - add CLI output to server -- CasparCG: split text and image in two layers: enables text without background - create dummy-sequence-items for unsupported ones - disable buttons, when no sequence is loaded - implement all countdown modes - countdown: save in server wether it is finished -- add option to change template-directory -- make sequence-comments another design in client - client-messages: create message-log, group same - build-script in node / integrate with license-generator - command-comment: define commands / names which get loaded straigth from the start and can be shown anytime -- song: transmit template in server->client message instead of hardcoding it into the client -- fix "Buffer() is deprecated" -- remove transition from html-template, handle it through casparcg instead \ No newline at end of file +- fix "Buffer() is deprecated" \ No newline at end of file diff --git a/casparcg-templates/JohnCG/Countdown.html b/casparcg-templates/JohnCG/Countdown.html index 88a2466..4bbc1a5 100644 --- a/casparcg-templates/JohnCG/Countdown.html +++ b/casparcg-templates/JohnCG/Countdown.html @@ -25,7 +25,7 @@ } #time { - text-shadow: calc(1em / 16) calc(1em / 16) calc(1em / 8) black; + /* text-shadow: calc(1em / 16) calc(1em / 16) calc(1em / 8) black; */ position: absolute; diff --git a/client/main.css b/client/main.css index c80f8a4..1360817 100644 --- a/client/main.css +++ b/client/main.css @@ -132,8 +132,6 @@ div.button, div.button > * { #sequence { width: 24rem; - - resize: horizontal; } .header { @@ -196,6 +194,11 @@ div.button, div.button > * { text-wrap: nowrap; } +.sequence_item.Comment { + color: rgb(154, 153, 150); + font-style: italic; +} + .sequence_item_container.selectable:hover > .sequence_item { background-color: rgb(79, 83, 94); } @@ -237,54 +240,60 @@ div.button, div.button > * { position: relative; } -.slide_container > div { +.slide_container > img { + display: block; + + height: 9rem; +} + +.slide_container > object { position: absolute; top: 0; left: 0; + + justify-content: center; + align-items: center; + display: flex; + flex-direction: column; + width: 100%; height: 100%; +} + +.slide_container > div { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; cursor: pointer; } .slide { - justify-content: center; - align-items: center; - display: flex; - flex-direction: column; - - width: 16rem; - height: 9rem; - border: 1px solid white; border-radius: 0.125rem; - - background-repeat: no-repeat; - background-position: center; - background-size: cover; - - cursor: pointer; } .slide:hover { border-width: 2px; - margin: -1px; /* difference in border width between selected and non-selected */ + margin: -2px; /* difference in border width between selected and non-selected */ } .slide.active { border-color: red; border-width: 2px; - margin: -1px; /* difference in border width between selected and non-selected */ + margin: -2px; /* difference in border width between selected and non-selected */ } .slide.active:hover { border-color: red; border-width: 4px; - margin: -3px; /* difference in border width between selected and non-selected */ + margin: -4px; /* difference in border width between selected and non-selected */ } .lyric_line { diff --git a/package.json b/package.json index 8a67de8..5ae23de 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=src/server/tsconfig.json --platform=node --minify --bundle", "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --minify", "watch-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --sourcemap --watch", - "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome71 --minify", - "watch-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome71 --sourcemap --watch" + "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome117 --minify", + "watch-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome117 --sourcemap --watch" } } diff --git a/src/client/main.ts b/src/client/main.ts index f3bd3eb..f0a2608 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -71,7 +71,7 @@ function display_items(data: JGCPSend.Sequence) { // initialize init(); - for (const item of data.sequence_items) { + data.sequence_items.forEach((item) => { const div_sequence_item_container = document.createElement("div"); div_sequence_item_container.classList.add("sequence_item_container"); div_sequence_item_container.dataset.item_number = item.item.toString(); @@ -93,6 +93,7 @@ function display_items(data: JGCPSend.Sequence) { const div_sequence_item = document.createElement("div"); div_sequence_item.classList.add("sequence_item"); + div_sequence_item.classList.add(item.type); // if it's a Countdown-Object, insert the time if (item.type === "Countdown") { @@ -110,7 +111,7 @@ function display_items(data: JGCPSend.Sequence) { div_sequence_item_container.append(div_sequence_item); div_sequence_items?.append(div_sequence_item_container); - } + }); // display the visibility state display_visibility_state(data.metadata.visibility); @@ -150,19 +151,19 @@ function display_item_slides(data: JGCPSend.ItemSlides) { switch (data.type) { case "Song": - part_arrays = create_song_slides(data as ClientSongSlides); + part_arrays = create_song_slides(data); break; case "Countdown": - part_arrays = create_image_countdown_slides(data as ClientCountdownSlides); + part_arrays = create_image_countdown_slides(data); break; case "Image": - part_arrays = create_image_countdown_slides(data as ClientImageSlides); + part_arrays = create_image_countdown_slides(data); break; case "CommandComment": - part_arrays = create_template_slides(data as ClientCommandCommentSlides); + part_arrays = create_template_slides(data as JGCPSend.ItemSlides); break; default: - console.error(`'${data.type}' is not supported`); + console.error(`'${data["type"]}' is not supported`); } part_arrays.forEach((slide_part) => { @@ -172,7 +173,7 @@ function display_item_slides(data: JGCPSend.ItemSlides) { set_active_slide(data.client_id === client_id); } -function create_song_slides(data: ClientSongSlides): HTMLDivElement[] { +function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { let slide_counter: number = 0; // create the individual arrays for parallel processing @@ -227,7 +228,7 @@ function create_song_slides(data: ClientSongSlides): HTMLDivElement[] { return part_arrays; } -function create_image_countdown_slides(data: ClientCountdownSlides | ClientImageSlides): HTMLDivElement[] { +function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend.ImageSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -251,7 +252,7 @@ function create_image_countdown_slides(data: ClientCountdownSlides | ClientImage return [div_slide_part]; } -function create_template_slides(data: ClientCommandCommentSlides): HTMLDivElement[] { +function create_template_slides(data: JGCPSend.ItemSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); div_slide_part.classList.add("slide_part"); @@ -275,24 +276,29 @@ function create_template_slides(data: ClientCommandCommentSlides): HTMLDivElemen return [div_slide_part]; } -function create_slide_object(data: ClientItemSlides, number: number) { +function create_slide_object(data: JGCPSend.ItemSlides, number: number) { const div_slide_container = document.createElement("div"); div_slide_container.classList.add("slide_container"); + const img_media = document.createElement("img"); + img_media.src = data.media_b64 ?? ""; + + // add an error listener, to set the opacity to zero (avoids error symbol) + img_media.addEventListener("error", () => { + img_media.style.opacity = "0"; + }); + + img_media.style.aspectRatio = (data.resolution.width / data.resolution.height).toString(); + div_slide_container.append(img_media); + const slide_object = document.createElement("object"); if (data.template !== undefined) { slide_object.data = "Templates/" + data.template.template; } - slide_object.style.backgroundImage = `url("${data.media_b64}")`; - - slide_object.classList.add("slide"); - div_slide_container.append(slide_object); - slide_object.dataset.slide_number = number.toString(); - // register click event div_slide_container.addEventListener("click", () => { request_item_slide_select( @@ -314,6 +320,8 @@ function create_slide_object(data: ClientItemSlides, number: number) { }); const catcher = document.createElement("div"); + catcher.classList.add("slide"); + catcher.dataset.slide_number = number.toString(); div_slide_container.append(catcher); diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 158296d..4ed18c8 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -1,5 +1,9 @@ import * as SequenceClass from "../server/Sequence"; -import { ClientItemSlides } from "./SequenceItems/SequenceItem"; +import { ClientCommandCommentSlides } from "./SequenceItems/CommandComment"; +import { ClientCommentSlides } from "./SequenceItems/Comment"; +import { ClientCountdownSlides } from "./SequenceItems/Countdown"; +import { ClientImageSlides } from "./SequenceItems/Image"; +import { ClientSongSlides } from "./SequenceItems/Song"; /** * Base interface for sent JGCP-messages @@ -36,9 +40,16 @@ export interface State extends Base { interface ItemSlidesBase extends Base{ client_id: string; command: "item_slides"; + resolution: SequenceClass.CasparCGResolution; } -export type ItemSlides = ClientItemSlides & ItemSlidesBase; +export type SongSlides = ClientSongSlides & ItemSlidesBase; +export type CountdownSlides = ClientCountdownSlides & ItemSlidesBase; +export type ImageSlides = ClientImageSlides & ItemSlidesBase; +export type CommandCommentSlides = ClientCommandCommentSlides & ItemSlidesBase; +export type CommentSlides = ClientCommentSlides & ItemSlidesBase; + +export type ItemSlides = SongSlides | CountdownSlides | ImageSlides | CommandCommentSlides | CommentSlides; export interface Clear extends Base { command: "clear"; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index afeea13..47ea92c 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -1,7 +1,6 @@ import path from "path"; -import { APIRequest, CasparCG, ClipInfo, Commands, TransitionParameters } from "casparcg-connection"; +import { APIRequest, Commands, TransitionParameters } from "casparcg-connection"; import mime from "mime-types"; -import { XMLParser } from "fast-xml-parser"; import { SongElement } from "./SequenceItems/SongFile"; import { ClientItemSlides, ItemProps, ItemPropsBase, SequenceItem } from "./SequenceItems/SequenceItem"; @@ -9,12 +8,13 @@ import Song, { SongProps } from "./SequenceItems/Song"; import * as JGCPSend from "./JGCPSendMessages"; -import Config, { CasparCGConnectionSettings } from "./config"; +import Config from "./config"; import Countdown, { CountdownProps } from "./SequenceItems/Countdown"; import Comment, { CommentProps } from "./SequenceItems/Comment"; import Image, { ImageProps } from "./SequenceItems/Image"; import { TransitionType } from "casparcg-connection/dist/enums"; import CommandComment, { CommandCommentProps } from "./SequenceItems/CommandComment"; +import { CasparCGConnection } from "./control"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -29,21 +29,9 @@ interface ActiveItemSlide { slide: number } -interface CasparCGPathsSettings { - /* eslint-disable @typescript-eslint/naming-convention */ - "data-path": string; - "initial-path": string; - "log-path": string; - "media-path": string; - "template-path": string; - /* eslint-enable @typescript-eslint/naming-convention */ -} - -interface CasparCGConnection { - connection: CasparCG, - settings: CasparCGConnectionSettings, - paths: CasparCGPathsSettings, - media: ClipInfo[] +export interface CasparCGResolution { + width: number; + height: number; } class Sequence { @@ -63,42 +51,23 @@ class Sequence { /* eslint-enable @typescript-eslint/naming-convention */ }; - constructor(sequence: string) { + constructor(sequence: string, casparcg_connections: CasparCGConnection[]) { this.parse_sequence(sequence); - const xml_parser = new XMLParser(); - - // create the casparcg-connections - // eslint-disable-next-line @typescript-eslint/no-misused-promises - Config.casparcg.connections.forEach(async (connection_setting) => { - const connection: CasparCG = new CasparCG({ - ...connection_setting, - // eslint-disable-next-line @typescript-eslint/naming-convention - autoConnect: true - }); - - const casparcg_connection: CasparCGConnection = { - connection, - settings: connection_setting, - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - paths: xml_parser.parse((await (await connection.infoPaths()).request)?.data as string ?? "")?.paths as CasparCGPathsSettings, - media: (await (await connection.cls()).request)?.data ?? [] - }; + this.casparcg_connections = casparcg_connections; + this.casparcg_connections.forEach((casparcg_connection) => { // add a listener to send send the current-slide on connection - connection.addListener("connect", () => { + casparcg_connection.connection.addListener("connect", () => { // load the active-item this.casparcg_load_item(casparcg_connection); }); // clear the previous casparcg-output on the layers this.casparcg_clear_layers(casparcg_connection); - + // load the first slide this.casparcg_load_item(casparcg_connection); - - // add the connection to the stored connections - this.casparcg_connections.push(casparcg_connection); }); this.set_active_item(0, 0); @@ -108,8 +77,9 @@ class Sequence { this.casparcg_clear_layers(); this.casparcg_connections.forEach((casparcg_connection) => { - casparcg_connection.connection.removeAllListeners(); - casparcg_connection.connection.disconnect(); + // casparcg_connection.connection.removeAllListeners(); + + casparcg_connection.connection.removeListener("connect"); }); } @@ -227,12 +197,16 @@ class Sequence { template: caption_json as { template: string, data: object } }; + props.type = "CommandComment"; + this.sequence_items.push(new CommandComment(props)); } else { throw new SyntaxError(); } } catch (e) { if (e instanceof SyntaxError) { + item_data.type = "Comment"; + this.sequence_items.push(new Comment(item_data as CommentProps)); } } @@ -386,9 +360,7 @@ class Sequence { } private casparcg_load_media(casparcg_connection: CasparCGConnection): Promise> { - let media = this.active_sequence_item.props.media; - - let casparcg_media: string; + let media = this.active_sequence_item.props.media?.replace(/^(?\w:)\//, "$//"); // if a media-file is defined, load it if (media) { @@ -407,45 +379,35 @@ class Sequence { const media_file = m.clip.toUpperCase().replace(/\\/, "/"); if (req_name.endsWith(media_file)) { - casparcg_media = m.clip; + media = m.clip; break; } } } - // if a matching media-file was found, use it - if (casparcg_media) { - // if the state is "visible", play it directly - if (this.visibility) { - return casparcg_connection.connection.play({ - /* eslint-disable @typescript-eslint/naming-convention */ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0], - clip: casparcg_media, - transition: this.casparcg_transition - /* eslint-enable @typescript-eslint/naming-convention */ - }); - } else { - // if the current stat is invisible, only load it in the background - return casparcg_connection.connection.loadbg({ - /* eslint-disable @typescript-eslint/naming-convention */ - channel: casparcg_connection.settings.channel, - layer: casparcg_connection.settings.layers[0], - clip: casparcg_media, - transition: this.casparcg_transition - /* eslint-enable @typescript-eslint/naming-convention */ - }); - } - } else { + // if the state is "visible", play it directly + if (this.visibility) { return casparcg_connection.connection.play({ + /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, layer: casparcg_connection.settings.layers[0], - clip: media.replace(/^(?\w:)\//, "$//"), + clip: media, transition: this.casparcg_transition + /* eslint-enable @typescript-eslint/naming-convention */ + }); + } else { + // if the current stat is invisible, only load it in the background + return casparcg_connection.connection.loadbg({ + /* eslint-disable @typescript-eslint/naming-convention */ + channel: casparcg_connection.settings.channel, + layer: casparcg_connection.settings.layers[0], + clip: media, + transition: this.casparcg_transition + /* eslint-enable @typescript-eslint/naming-convention */ }); } } else { - // clear the media-output + // no media-file selected -> clear the media-output return casparcg_connection.connection.play({ /* eslint-disable @typescript-eslint/naming-convention */ channel: casparcg_connection.settings.channel, @@ -588,7 +550,15 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item // remove line-breaks value = value.replace(/'\s\+\s+'/gm, ""); // un-escape escaped characters - value = value.replace(/'#(\d+)'/gm, (match, group: string) => String.fromCharCode(Number(group))); + value = value.replace(/'((?:#(?:\d+))+)'/gm, (match, group: string) => { + const chars = group.split("#").slice(1); + + let return_string = ""; + + chars.forEach((char) => return_string += String.fromCharCode(Number(char))); + + return return_string; + }); const return_props: { [P in keyof ItemProps]?: ItemProps[P]; } = {}; diff --git a/src/server/SequenceItems/Comment.ts b/src/server/SequenceItems/Comment.ts index bd2d028..44f1384 100644 --- a/src/server/SequenceItems/Comment.ts +++ b/src/server/SequenceItems/Comment.ts @@ -31,10 +31,6 @@ export default class Comment extends SequenceItemBase { }); } - create_render_object(): Promise { - return Promise.resolve(undefined); - } - navigate_slide(steps: number): number { // return the steps, since there are no slides to navigate return steps; diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 3a4b489..2169998 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -153,7 +153,7 @@ export default class Countdown extends SequenceItemBase { } function parse_hex_data(data_hex: string): CountdownData { - const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D650604(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; + const regex_curse = /(?:546578745374796C6573060[1-3](?42)?(?49)?(?55)?|54657874436F6C6F72(?:(?:04|0707)(?[0-9A-F]{6}|(?:[0-9A-F]{2})+?)0)|466F6E744E616D65(?:0[A-F0-9])+(?(?:[A-F0-9]{2})+?)09|547970020(?[0-3])09|506F736974696F6E5802(?[A-F0-9]{2})09|506F736974696F6E5902(?[A-F0-9]{2})08|466F6E7453697A6502(?[A-F0-9]{2})0F|4261636B67726F756E64496D616765(?:[A-F0-9]{4}636F6C6F723A2F2F244646(?[A-F0-9]{12})|(?:[A-F0-9]{2})*?0[0-F]{3}(?(?:[0-9A-F]{2})+?))0|53686F775365636F6E647308(?.))/g; const data: DeepPartial = { @@ -230,4 +230,4 @@ function parse_hex_data(data_hex: string): CountdownData { } return data as CountdownData; -} \ No newline at end of file +} diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index 3f36f43..a207d68 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -64,7 +64,6 @@ export abstract class SequenceItemBase { protected abstract item_props: ItemProps; protected abstract slide_count: number; - // abstract create_render_object(proxy?: boolean, slide?: number); abstract create_client_object_item_slides(): Promise; abstract set_active_slide(slide?: number): number; diff --git a/src/server/control.ts b/src/server/control.ts index f6b0b80..60fc3d9 100644 --- a/src/server/control.ts +++ b/src/server/control.ts @@ -1,17 +1,41 @@ import WebSocket, { RawData } from "ws"; -import Sequence from "./Sequence"; +import Sequence, { CasparCGResolution } from "./Sequence"; import OSCServer, { OSCFunctionMap, OSCServerArguments } from "./servers/osc-server"; import WebsocketServer, { WebsocketServerArguments, WebsocketMessageHandler } from "./servers/websocket-server"; import * as JGCPSend from "./JGCPSendMessages"; import * as JGCPRecv from "./JGCPReceiveMessages"; +import { CasparCG, ClipInfo } from "casparcg-connection"; +import Config, { CasparCGConnectionSettings } from "./config"; +import { XMLParser } from "fast-xml-parser"; + +interface CasparCGPathsSettings { + /* eslint-disable @typescript-eslint/naming-convention */ + "data-path": string; + "initial-path": string; + "log-path": string; + "media-path": string; + "template-path": string; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export interface CasparCGConnection { + connection: CasparCG; + settings: CasparCGConnectionSettings; + paths: CasparCGPathsSettings; + media: ClipInfo[]; + resolution: CasparCGResolution; + framerate: number; +} class Control { private sequence: Sequence; private ws_server: WebsocketServer; private osc_server: OSCServer; + readonly casparcg_connections: CasparCGConnection[] = []; + // mapping of the OSC-commands to the functions private readonly osc_function_map: OSCFunctionMap = { control: { @@ -57,6 +81,64 @@ class Control { // initialize the osc server this.osc_server = new OSCServer(osc_server_parameters, this.osc_function_map); + + const xml_parser = new XMLParser(); + + // create the casparcg-connections + // eslint-disable-next-line @typescript-eslint/no-misused-promises + Config.casparcg.connections.forEach(async (connection_setting) => { + const connection: CasparCG = new CasparCG({ + ...connection_setting, + // eslint-disable-next-line @typescript-eslint/naming-convention + autoConnect: true + }); + + const casparcg_config = (await (await connection.infoConfig()).request).data; + + let resolution: CasparCGResolution; + let framerate: number; + + const video_mode_regex_results = casparcg_config.channels[connection_setting.channel - 1].videoMode.match(/(?:(?dci)?(?\d+)(?:x(?\d+))?[pi](?\d{4})|(?PAL|NTSC))/); + + // if the resolution is given as PAL or NTSC, convert it + if (video_mode_regex_results.groups.mode) { + switch (video_mode_regex_results.groups.mode) { + case "PAL": + resolution = { + width: 720, + height: 576 + }; + framerate = 25; + break; + case "NTSC": + resolution = { + width: 720, + height: 480 + }; + framerate = 29.97; + break; + } + } else { + resolution = { + width: Number(video_mode_regex_results.groups.width), + height: Number(video_mode_regex_results.groups.height ?? Number(video_mode_regex_results.groups.width) / 16 * 9) + }; + framerate = Number(video_mode_regex_results.groups.framerate) / 100; + } + + const casparcg_connection: CasparCGConnection = { + connection, + settings: connection_setting, + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + paths: xml_parser.parse((await (await connection.infoPaths()).request)?.data as string ?? "")?.paths as CasparCGPathsSettings, + media: (await (await connection.cls()).request)?.data ?? [], + resolution, + framerate + }; + + // add the connection to the stored connections + this.casparcg_connections.push(casparcg_connection); + }); } /** @@ -67,7 +149,7 @@ class Control { // if there was already a sequence open, call it's destroy function this.sequence?.destroy(); - this.sequence = new Sequence(sequence); + this.sequence = new Sequence(sequence, this.casparcg_connections); // send the sequence to all clients const response_sequence_items: JGCPSend.Sequence = { @@ -95,6 +177,7 @@ class Control { const message: JGCPSend.ItemSlides = { command: "item_slides", client_id, + resolution: this.sequence.casparcg_connections[0].resolution, ...await this.sequence.create_client_object_item_slides(item) }; diff --git a/src/templates/Countdown.ts b/src/templates/Countdown.ts index f55b0b2..a2e90a0 100644 --- a/src/templates/Countdown.ts +++ b/src/templates/Countdown.ts @@ -9,7 +9,7 @@ const spans: { hours: [], minutes: [] }; -let data: CountdownTemplateData; +let data: CountdownTemplateData & { mute_transition: boolean }; const end_time = new Date(); @@ -21,7 +21,7 @@ function update(str_args: string) { // clear the old-state clearInterval(update_interval); - data = JSON.parse(str_args) as CountdownTemplateData; + data = JSON.parse(str_args) as CountdownTemplateData & { mute_transition: boolean }; // if requested, diable transition-effects const main_div = document.querySelector("div#main"); diff --git a/src/templates/Song.ts b/src/templates/Song.ts index 10646a9..1f5c220 100644 --- a/src/templates/Song.ts +++ b/src/templates/Song.ts @@ -1,6 +1,6 @@ import { ItemSlide, SongTemplateData } from "../server/SequenceItems/Song"; -let data: SongTemplateData; +let data: SongTemplateData & { mute_transition: boolean }; let active_slide = 0; let slide_count = 0; @@ -9,7 +9,7 @@ let slide_count = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars function update(s_data: string) { // parse the transferred data into json - data = JSON.parse(s_data) as SongTemplateData; + data = JSON.parse(s_data) as SongTemplateData & { mute_transition: boolean }; // get the div for the display and storage const div_container = document.querySelector("div#container"); @@ -163,7 +163,7 @@ function create_slide(slide_data: ItemSlide, languages: number[]) { languages.forEach((language, index) => { const div_title = document.createElement("div"); div_title.classList.add(`language_${index}`); - div_title.innerText = slide_data.title[language]; + div_title.innerText = slide_data.title[language] ?? ""; title_container.append(div_title); }); From f97040d9f866c4d7c061346f944da3af648f7dc2 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Wed, 28 Feb 2024 01:31:52 +0100 Subject: [PATCH 4/6] added support for PDFs --- client/main.css | 10 +- package-lock.json | 531 ++++++++++++++++++++++- package.json | 4 + src/client/main.ts | 75 +++- src/server/JGCPSendMessages.ts | 4 +- src/server/Sequence.ts | 36 +- src/server/SequenceItems/Countdown.ts | 2 +- src/server/SequenceItems/Image.ts | 4 +- src/server/SequenceItems/PDF.ts | 135 ++++++ src/server/SequenceItems/SequenceItem.ts | 21 +- src/server/SequenceItems/Song.ts | 8 +- src/templates/Countdown.ts | 11 +- src/templates/Song.ts | 10 +- 13 files changed, 786 insertions(+), 65 deletions(-) create mode 100644 src/server/SequenceItems/PDF.ts diff --git a/client/main.css b/client/main.css index 1360817..c529d97 100644 --- a/client/main.css +++ b/client/main.css @@ -172,6 +172,11 @@ div.button, div.button > * { cursor: pointer; } +.sequence_item_container:not(.selectable) { + color: rgb(154, 153, 150); + font-style: italic; +} + .sequence_item_container:first-of-type { margin-top: 0.125rem; } @@ -194,11 +199,6 @@ div.button, div.button > * { text-wrap: nowrap; } -.sequence_item.Comment { - color: rgb(154, 153, 150); - font-style: italic; -} - .sequence_item_container.selectable:hover > .sequence_item { background-color: rgb(79, 83, 94); } diff --git a/package-lock.json b/package-lock.json index ce51b88..194e61a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "ALPHA-1.1.0", "license": "MIT", "dependencies": { + "canvas": "^2.11.2", "casparcg-connection": "^6.2.1", "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", "node-osc": "^9.1.0", + "pdfjs-dist": "^4.0.379", "sharp": "^0.33.2", + "tmp": "^0.2.1", "ws": "^8.16.0" }, "bin": { @@ -24,6 +27,7 @@ "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", "@types/node-osc": "^6.0.3", + "@types/tmp": "^0.2.6", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -623,6 +627,25 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -698,6 +721,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.10", "dev": true, @@ -1007,6 +1036,11 @@ "node": ">=4.2.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/acorn": { "version": "8.11.3", "dev": true, @@ -1026,6 +1060,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "dev": true, @@ -1043,7 +1088,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1063,6 +1107,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -1078,7 +1139,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/binpack": { @@ -1088,7 +1148,6 @@ }, "node_modules/brace-expansion": { "version": "1.1.11", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1114,6 +1173,20 @@ "node": ">=6" } }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/casparcg-connection": { "version": "6.2.1", "license": "MIT", @@ -1141,6 +1214,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1176,6 +1257,14 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/commander": { "version": "9.5.0", "dev": true, @@ -1186,9 +1275,13 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, "license": "MIT" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1205,7 +1298,6 @@ }, "node_modules/debug": { "version": "4.3.4", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -1219,11 +1311,27 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, "license": "MIT" }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/detect-libc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", @@ -1593,14 +1701,76 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, "license": "ISC" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -1628,11 +1798,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, "node_modules/globals": { "version": "13.24.0", "dev": true, @@ -1679,6 +1844,23 @@ "node": ">=8" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "license": "MIT", @@ -1722,13 +1904,17 @@ }, "node_modules/inflight": { "version": "1.0.6", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -1746,7 +1932,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -1876,6 +2061,28 @@ "node": "14 || >=16.14" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/merge2": { "version": "1.4.1", "dev": true, @@ -1913,9 +2120,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1933,16 +2150,73 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", - "dev": true, "license": "MIT" }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-osc": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/node-osc/-/node-osc-9.1.0.tgz", @@ -1954,9 +2228,41 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2038,7 +2344,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2076,6 +2381,27 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.0.379", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.0.379.tgz", + "integrity": "sha512-6H0Gv1nna+wmrr3CakaKlZ4rbrL8hvGIFAgg4YcoFuGC0HC4B2DVjXEGTFjJEjLlf8nYi3C3/MYRcM5bNx0elA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, "node_modules/picomatch": { "version": "2.3.1", "dev": true, @@ -2136,6 +2462,19 @@ ], "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -2155,7 +2494,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -2189,6 +2527,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT" @@ -2220,9 +2577,10 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/sharp": { "version": "0.33.2", @@ -2294,6 +2652,35 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -2310,6 +2697,14 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2377,7 +2772,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2426,11 +2820,46 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -2442,6 +2871,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.2.1", "dev": true, @@ -2504,6 +2938,25 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2519,6 +2972,32 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2615,7 +3094,6 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -2655,6 +3133,11 @@ "node": ">=4.0" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, diff --git a/package.json b/package.json index 5ae23de..17e78b4 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,22 @@ "main": "server/src/main.ts", "license": "MIT", "dependencies": { + "canvas": "^2.11.2", "casparcg-connection": "^6.2.1", "fast-xml-parser": "^4.3.4", "iconv-lite": "^0.6.3", "mime-types": "^2.1.35", "node-osc": "^9.1.0", + "pdfjs-dist": "^4.0.379", "sharp": "^0.33.2", + "tmp": "^0.2.1", "ws": "^8.16.0" }, "devDependencies": { "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", "@types/node-osc": "^6.0.3", + "@types/tmp": "^0.2.6", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", diff --git a/src/client/main.ts b/src/client/main.ts index f0a2608..847e4b3 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -1,15 +1,10 @@ import MessageLog from "./message_box.js"; import { ItemPartClient } from "../server/SequenceItems/SongFile"; -import { ClientItemSlides } from "../server/SequenceItems/SequenceItem"; import * as JGCPSend from "../server/JGCPSendMessages"; import * as JGCPRecv from "../server/JGCPReceiveMessages.js"; import { ActiveItemSlide } from "../server/Sequence.js"; -import { ClientSongSlides } from "../server/SequenceItems/Song.js"; -import { ClientCountdownSlides } from "../server/SequenceItems/Countdown.js"; -import { ClientImageSlides } from "../server/SequenceItems/Image.js"; -import { ClientCommandCommentSlides } from "../server/SequenceItems/CommandComment.js"; const config = { websocket: { @@ -162,6 +157,9 @@ function display_item_slides(data: JGCPSend.ItemSlides) { case "CommandComment": part_arrays = create_template_slides(data as JGCPSend.ItemSlides); break; + case "PDF": + part_arrays = create_pdf_slides(data as JGCPSend.PDFSlides); + break; default: console.error(`'${data["type"]}' is not supported`); } @@ -214,7 +212,7 @@ function create_song_slides(data: JGCPSend.SongSlides): HTMLDivElement[] { const object_iter_array = object_iter_array_proto.map((ii) => { return { index: ii, - slide: create_slide_object(data, slides_start + ii) + slide: create_template_object(data, slides_start + ii) }; }); @@ -245,13 +243,40 @@ function create_image_countdown_slides(data: JGCPSend.CountdownSlides | JGCPSend div_slide_part_header.innerText = data.title; - const obj = create_slide_object(data, 0); + const obj = create_template_object(data, 0); div_slides_view.append(obj); return [div_slide_part]; } +function create_pdf_slides(data: JGCPSend.PDFSlides): HTMLDivElement[] { + // create the container for the part + const div_slide_part = document.createElement("div"); + div_slide_part.classList.add("slide_part"); + + // create the header of the part and append it to the part-container + const div_slide_part_header = document.createElement("div"); + div_slide_part_header.classList.add("header"); + div_slide_part.append(div_slide_part_header); + + // create the slides-view and append it to the part container + const div_slides_view = document.createElement("div"); + div_slides_view.classList.add("slides_view"); + div_slide_part.append(div_slides_view); + + div_slide_part_header.innerText = data.title; + + data.slides.forEach((slide, index) => { + const obj = create_media_object(data, index); + + div_slides_view.append(obj); + }); + + + return [div_slide_part]; +} + function create_template_slides(data: JGCPSend.ItemSlides): HTMLDivElement[] { // create the container for the part const div_slide_part = document.createElement("div"); @@ -269,14 +294,14 @@ function create_template_slides(data: JGCPSend.ItemSlides): HTMLDivElement[] { div_slide_part_header.innerText = data.title; - const obj = create_slide_object(data, 0); + const obj = create_template_object(data, 0); div_slides_view.append(obj); return [div_slide_part]; } -function create_slide_object(data: JGCPSend.ItemSlides, number: number) { +function create_template_object(data: JGCPSend.ItemSlides, number: number): HTMLDivElement { const div_slide_container = document.createElement("div"); div_slide_container.classList.add("slide_container"); @@ -328,6 +353,38 @@ function create_slide_object(data: JGCPSend.ItemSlides, number: number) { return div_slide_container; } +function create_media_object(data: JGCPSend.PDFSlides, number: number): HTMLDivElement { + const div_slide_container = document.createElement("div"); + div_slide_container.classList.add("slide_container"); + + const img_media = document.createElement("img"); + img_media.src = data.slides[number] ?? ""; + + // add an error listener, to set the opacity to zero (avoids error symbol) + img_media.addEventListener("error", () => { + img_media.style.opacity = "0"; + }); + + img_media.style.aspectRatio = (data.resolution.width / data.resolution.height).toString(); + div_slide_container.append(img_media); + + // register click event + div_slide_container.addEventListener("click", () => { + request_item_slide_select( + Number(document.querySelector("div.sequence_item_container.selected")?.dataset.item_number), + number + ); + }); + + const catcher = document.createElement("div"); + catcher.classList.add("slide"); + catcher.dataset.slide_number = number.toString(); + + div_slide_container.append(catcher); + + return div_slide_container; +} + function select_item(item: number) { // store the selected item selected_item_number = item; diff --git a/src/server/JGCPSendMessages.ts b/src/server/JGCPSendMessages.ts index 4ed18c8..0341bd6 100644 --- a/src/server/JGCPSendMessages.ts +++ b/src/server/JGCPSendMessages.ts @@ -3,6 +3,7 @@ import { ClientCommandCommentSlides } from "./SequenceItems/CommandComment"; import { ClientCommentSlides } from "./SequenceItems/Comment"; import { ClientCountdownSlides } from "./SequenceItems/Countdown"; import { ClientImageSlides } from "./SequenceItems/Image"; +import { ClientPDFSlides } from "./SequenceItems/PDF"; import { ClientSongSlides } from "./SequenceItems/Song"; /** @@ -48,8 +49,9 @@ export type CountdownSlides = ClientCountdownSlides & ItemSlidesBase; export type ImageSlides = ClientImageSlides & ItemSlidesBase; export type CommandCommentSlides = ClientCommandCommentSlides & ItemSlidesBase; export type CommentSlides = ClientCommentSlides & ItemSlidesBase; +export type PDFSlides = ClientPDFSlides & ItemSlidesBase; -export type ItemSlides = SongSlides | CountdownSlides | ImageSlides | CommandCommentSlides | CommentSlides; +export type ItemSlides = SongSlides | CountdownSlides | ImageSlides | CommandCommentSlides | CommentSlides | PDFSlides; export interface Clear extends Base { command: "clear"; diff --git a/src/server/Sequence.ts b/src/server/Sequence.ts index 47ea92c..a668404 100644 --- a/src/server/Sequence.ts +++ b/src/server/Sequence.ts @@ -15,6 +15,7 @@ import Image, { ImageProps } from "./SequenceItems/Image"; import { TransitionType } from "casparcg-connection/dist/enums"; import CommandComment, { CommandCommentProps } from "./SequenceItems/CommandComment"; import { CasparCGConnection } from "./control"; +import PDF, { PDFProps } from "./SequenceItems/PDF"; interface ClientSequenceItems { sequence_items: ItemProps[]; @@ -183,6 +184,9 @@ class Sequence { case "Image": this.sequence_items.push(new Image(item_data as ImageProps)); break; + case "PDF": + this.sequence_items.push(new PDF(item_data as PDFProps)); + break; default: // if it wasn't caught by other cases, it is either a comment or not implemented yet -> if there is no file specified, treat it as comment if (!Object.keys(item_data).includes("FileName")) { @@ -360,7 +364,7 @@ class Sequence { } private casparcg_load_media(casparcg_connection: CasparCGConnection): Promise> { - let media = this.active_sequence_item.props.media?.replace(/^(?\w:)\//, "$//"); + let media = this.active_sequence_item.media?.replace(/^(?\w:)\//, "$//"); // if a media-file is defined, load it if (media) { @@ -454,6 +458,11 @@ class Sequence { private casparcg_select_slide(slide: number): void { this.casparcg_connections.forEach((casparcg_connection) => { + // if the item has multiple media-files, load the new one + if (this.active_sequence_item.props.media.length > 1) { + void this.casparcg_load_media(casparcg_connection); + } + // jump to the slide-number in casparcg void casparcg_connection.connection.cgInvoke({ /* eslint-disable @typescript-eslint/naming-convention */ @@ -572,19 +581,24 @@ function parse_item_value_string(key: string, value: string): { [P in keyof Item // split csv-line into an array return_props["VerseOrder"] = value.split(",") as SongElement[]; break; - case "FileName": - // assume the type from the file-extension - if (path.extname(value) === ".sng") { - return_props.type = "Song"; - } else { - const mime_type = mime.lookup(value); - - if (mime_type ? mime_type.split("/", 1)[0] === "image" : false) { + case "FileName": { + // assume the type from the mime-type + const mime_type = mime.lookup(value); + switch (true) { + case mime_type ? mime_type.split("/", 1)[0] === "image" : false: return_props.type = "Image"; - } + break; + case mime_type === "application/pdf": + return_props.type = "PDF"; + break; + case !mime_type: + if (path.extname(value) === ".sng") { + return_props.type = "Song"; + } + break; } return_props[key] = value; - break; + } break; case "Color": if (value.substring(0, 2) === "cl") { return_props[key] = convert_color_to_hex(value); diff --git a/src/server/SequenceItems/Countdown.ts b/src/server/SequenceItems/Countdown.ts index 2169998..933d301 100644 --- a/src/server/SequenceItems/Countdown.ts +++ b/src/server/SequenceItems/Countdown.ts @@ -94,7 +94,7 @@ export default class Countdown extends SequenceItemBase { } }; - this.item_props.media = this.get_background_image(this.props.background_image); + this.item_props.media = [this.get_background_image(this.props.background_image)]; } navigate_slide(steps: number): number { diff --git a/src/server/SequenceItems/Image.ts b/src/server/SequenceItems/Image.ts index 17e63a5..384fa8c 100644 --- a/src/server/SequenceItems/Image.ts +++ b/src/server/SequenceItems/Image.ts @@ -6,7 +6,7 @@ export interface ImageProps extends ItemPropsBase { /* eslint-disable @typescript-eslint/naming-convention */ type: "Image"; FileName: string; - media: string; + media: string[]; /* eslint-enable @typescript-eslint/naming-convention */ } @@ -26,7 +26,7 @@ export default class Image extends SequenceItemBase { this.item_props = props; - this.item_props.media = get_song_path(this.props.FileName); + this.item_props.media = [get_song_path(this.props.FileName)]; } set_active_slide(slide?: number): number { diff --git a/src/server/SequenceItems/PDF.ts b/src/server/SequenceItems/PDF.ts new file mode 100644 index 0000000..ba17073 --- /dev/null +++ b/src/server/SequenceItems/PDF.ts @@ -0,0 +1,135 @@ +import sharp from "sharp"; +import { ClientItemSlidesBase, ItemPropsBase, SequenceItemBase } from "./SequenceItem"; +import { get_song_path } from "./Song"; +import Canvas from "canvas"; +import tmp from "tmp"; + +export interface PDFProps extends ItemPropsBase { + /* eslint-disable @typescript-eslint/naming-convention */ + type: "PDF"; + FileName: string; + media: string[]; + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export interface ClientPDFSlides extends ClientItemSlidesBase { + type: "PDF"; + slides: string[]; + template?: undefined; +} + +export default class PDF extends SequenceItemBase { + protected item_props: PDFProps; + + protected slide_count: number = 0; + + protected active_slide_number: number = 0; + + constructor(props: PDFProps) { + super(); + + this.item_props = props; + + void (async () => { + const pdfjs = await import("pdfjs-dist"); + + this.item_props.media = []; + + const pth = get_song_path(this.props.FileName).replaceAll("/", "\\"); + + try { + const pdf = await pdfjs.getDocument(pth).promise; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + [...Array(pdf.numPages).keys()].forEach(async (index) => { + // increase the counter by one, because the pages start at 1 + const page = await pdf.getPage(index + 1); + // eslint-disable-next-line @typescript-eslint/naming-convention + const viewport = page.getViewport({ scale: 1 }); + const canvas = Canvas.createCanvas(viewport.width, viewport.height); + + await page.render({ + // eslint-disable-next-line @typescript-eslint/naming-convention + canvasContext: canvas.getContext("2d") as unknown as CanvasRenderingContext2D, + viewport + }).promise; + + const image_buffer = canvas.toBuffer(); + + // save the image into a temporary file + const tmp_file = tmp.fileSync(); + + void sharp(image_buffer).png().toFile(tmp_file.name); + + this.item_props.media[index] = tmp_file.name.replaceAll("\\", "/"); + + this.slide_count++; + }); + } catch (e) { + if (e instanceof pdfjs.MissingPDFException) + { + // set the item as not selectable + this.props.selectable = false; + + return; + } else { + throw e; + } + } + })(); + } + + set_active_slide(slide?: number): number { + this.active_slide_number = this.validate_slide_number(slide); + + return this.active_slide; + } + + async create_client_object_item_slides(): Promise { + return Promise.resolve({ + title: this.props.FileName, + type: "PDF", + item: this.props.item, + slides: await Promise.all(this.props.media.map(async (m) => await this.get_media_b64(true, m))) + }); + } + + navigate_slide(steps: number): number { + if (typeof steps !== "number") { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new TypeError(`steps ('${steps}') is no number`); + } + + if (![-1, 1].includes(steps)) { + throw new RangeError(`steps must be -1 or 1, but is ${steps}`); + } + + const new_active_slide_number = this.active_slide + steps; + let slide_steps = 0; + + // new active item has negative index -> roll over to the last slide of the previous element + if (new_active_slide_number < 0) { + slide_steps = -1; + + // index is bigger than the slide-count -> roll over to zero + } else if (new_active_slide_number >= this.slide_count) { + slide_steps = 1; + } else { + this.active_slide_number = new_active_slide_number; + } + + return slide_steps; + } + + get active_slide(): number { + return this.active_slide_number; + } + + get props(): PDFProps { + return this.item_props; + } + + get template(): undefined { + return undefined; + } +} \ No newline at end of file diff --git a/src/server/SequenceItems/SequenceItem.ts b/src/server/SequenceItems/SequenceItem.ts index a207d68..3f02fd8 100644 --- a/src/server/SequenceItems/SequenceItem.ts +++ b/src/server/SequenceItems/SequenceItem.ts @@ -9,8 +9,9 @@ import Image, { ClientImageSlides, ImageProps } from "./Image"; import CommandComment, { ClientCommandCommentSlides, CommandCommentProps, CommandCommentTemplate } from "./CommandComment"; import Config from "../config"; import path from "path"; +import PDF, { ClientPDFSlides, PDFProps } from "./PDF"; -export type SequenceItem = Song | Countdown | Comment | Image | CommandComment; +export type SequenceItem = Song | Countdown | Comment | Image | CommandComment | PDF; export type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; @@ -28,12 +29,12 @@ export interface ItemPropsBase { selectable: boolean; background_color?: string; background_image?: string; - media?: string; + media?: string[]; template?: Template; /* eslint-enable @typescript-eslint/naming-convention */ } -export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps | CommandCommentProps; +export type ItemProps = SongProps | CountdownProps | CommentProps | ImageProps | CommandCommentProps | PDFProps; export interface ClientItemSlidesBase { type: string; @@ -47,7 +48,7 @@ export interface ClientItemSlidesBase { }; } -export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides | ClientCommandCommentSlides; +export type ClientItemSlides = ClientSongSlides | ClientCountdownSlides | ClientCommentSlides | ClientImageSlides | ClientCommandCommentSlides | ClientPDFSlides; export interface FontFormat { /* eslint-disable @typescript-eslint/naming-convention */ @@ -94,15 +95,15 @@ export abstract class SequenceItemBase { return slide; } - async get_media_b64(proxy: boolean = false): Promise { + async get_media_b64(proxy: boolean = false, media: string = this.media): Promise { let img: sharp.Sharp = undefined; // if no background-color is specified, set it to transparent const background_color = this.props.background_color ?? "#00000000"; - if (this.media !== undefined) { + if (media !== undefined) { try { - img = sharp(await fs.readFile(this.media)); + img = sharp(await fs.readFile(media)); // if a proxy is requested, downscale teh image if (proxy) { @@ -160,7 +161,11 @@ export abstract class SequenceItemBase { abstract get props(): ItemProps; get media(): string { - return this.props.media; + if (this.props.media !== undefined) { + return this.props.media[this.active_slide]; + } else { + return undefined; + } } protected resolve_image_path(img_path: string): string { diff --git a/src/server/SequenceItems/Song.ts b/src/server/SequenceItems/Song.ts index 1034fc3..20dcfbc 100644 --- a/src/server/SequenceItems/Song.ts +++ b/src/server/SequenceItems/Song.ts @@ -16,7 +16,7 @@ export interface SongProps extends ItemPropsBase { VerseOrder?: string[]; Language?: number; PrimaryLanguage?: number; - media?: string; + media?: string[]; template?: SongTemplate; /* eslint-enable @typescript-eslint/naming-convention */ } @@ -103,7 +103,7 @@ export default class Song extends SequenceItemBase { } // store the media - this.item_props.media = this.get_background_image(this.song_file.metadata.BackgroundImage); + this.item_props.media = [this.get_background_image(this.song_file.metadata.BackgroundImage)]; // create the template data this.item_props.template = { @@ -247,6 +247,10 @@ export default class Song extends SequenceItemBase { return template; } + + get media(): string { + return this.props.media[0]; + } } export function get_song_path(song_path: string): string { diff --git a/src/templates/Countdown.ts b/src/templates/Countdown.ts index a2e90a0..d7d3d6f 100644 --- a/src/templates/Countdown.ts +++ b/src/templates/Countdown.ts @@ -21,7 +21,16 @@ function update(str_args: string) { // clear the old-state clearInterval(update_interval); - data = JSON.parse(str_args) as CountdownTemplateData & { mute_transition: boolean }; + try { + data = JSON.parse(str_args) as CountdownTemplateData & { mute_transition: boolean }; + } catch (error) { + if (!(error instanceof SyntaxError)) { + throw error; + } else { + return; + } + } + // if requested, diable transition-effects const main_div = document.querySelector("div#main"); diff --git a/src/templates/Song.ts b/src/templates/Song.ts index 1f5c220..7b91af1 100644 --- a/src/templates/Song.ts +++ b/src/templates/Song.ts @@ -9,7 +9,15 @@ let slide_count = 0; // eslint-disable-next-line @typescript-eslint/no-unused-vars function update(s_data: string) { // parse the transferred data into json - data = JSON.parse(s_data) as SongTemplateData & { mute_transition: boolean }; + try { + data = JSON.parse(s_data) as SongTemplateData & { mute_transition: boolean }; + } catch (error) { + if (!(error instanceof SyntaxError)) { + throw error; + } else { + return; + } + } // get the div for the display and storage const div_container = document.querySelector("div#container"); From 05401d655f71a43df865a1429b56c82fc1387cef Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Wed, 28 Feb 2024 03:45:22 +0100 Subject: [PATCH 5/6] rewrote build script in typescript --- .eslintrc | 3 +- .gitignore | 4 +- build.ps1 | 52 -------- build/scripts/build.ts | 119 ++++++++++++++++++ .../scripts/license-reporter.config.ts | 3 +- .../scripts/sea-config.json | 0 build/scripts/tsconfig.json | 7 ++ package-lock.json | 21 ++++ package.json | 6 +- src/server/config.ts | 2 +- 10 files changed, 159 insertions(+), 58 deletions(-) delete mode 100644 build.ps1 create mode 100644 build/scripts/build.ts rename license-reporter.config.ts => build/scripts/license-reporter.config.ts (96%) rename sea-config.json => build/scripts/sea-config.json (100%) create mode 100644 build/scripts/tsconfig.json diff --git a/.eslintrc b/.eslintrc index 7c55373..9705965 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,8 @@ "project": [ "src/server/tsconfig.json", "src/client/tsconfig.json", - "src/templates/tsconfig.json" + "src/templates/tsconfig.json", + "build/scripts/tsconfig.json" ] }, "extends": [ diff --git a/.gitignore b/.gitignore index e0a5ccc..63b900a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ dist client/*.js casparcg-templates/JohnCG/*.js *.map -client/webfonts/bahnschrift.ttf \ No newline at end of file +client/webfonts/bahnschrift.ttf +build/scripts/*.js +build/scripts/3rdpartylicenses.json \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 530837d..0000000 --- a/build.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -# extract the buildname from the package.json -$package_json = Get-Content -Raw package.json | ConvertFrom-Json -$build_name = "JohnCG_" + $package_json.version - -# name of the executabe -$node_exec_name = $build_name + ".exe" -$node_exec_name = "node.exe" # overwritten until there is a solution for packaging `sharp` - -# clear the dist directory -Remove-Item -Path .\dist -Recurse -New-Item -Type Directory .\dist\build -New-Item -Type Directory .\dist\$build_name - -# bundle the files -npm run build-server -npm run build-client -npm run build-templates - -# create sea-prep.blob -# node --experimental-sea-config .\sea-config.json - -# get the node executable -node -e "require('fs').copyFileSync(process.execPath, 'dist/build/$node_exec_name')" - -# disabled until there is a solution for packaging `sharp` -# # remove the signature from the node executable -# & 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe' remove /s dist/build/$node_exec_name -# # modify the node executable -# npx postject dist/build/$node_exec_name NODE_SEA_BLOB dist/build/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 - -# copy the files in the output -Copy-Item -Path .\config.json -Destination .\dist\$build_name -Exclude .eslintrc -Copy-Item -Path .\casparcg-templates -Destination .\dist\$build_name -Exclude .eslintrc,*.map -Recurse -Copy-Item -Path .\client -Destination .\dist\$build_name -Exclude .eslintrc,*.map,bahnschrift.ttf -Recurse -Copy-Item -Path .\dist\build\$node_exec_name .\dist\$build_name -Recurse - -Copy-Item -Path .\node_modules\@img -Destination .\dist\$build_name\node_modules\@img\ -Recurse -Copy-Item -Path .\dist\build\main.js -Destination .\dist\$build_name\main.js - -# create a batch file, that starts node with the main.js -New-Item -Path .\dist\$build_name\$build_name.bat -Value "node.exe main.js`npause" - -# create and copy the licenses -npx esbuild license-generator.ts --platform=node --bundle --minify --outfile=.\dist\build\license-generator.js -npx license-reporter -node .\dist\build\license-generator.js - -Copy-Item -Path .\dist\build\licenses -Destination .\dist\$build_name\ -Recurse -# Copy-Item -Path .\LICENSE -Destination .\dist\$build_name\LICENSE - -# pack the files in a .tar.gz-file -tar -cvzf .\dist\$build_name.tar.gz --directory=dist $build_name \ No newline at end of file diff --git a/build/scripts/build.ts b/build/scripts/build.ts new file mode 100644 index 0000000..05b494b --- /dev/null +++ b/build/scripts/build.ts @@ -0,0 +1,119 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import tar from "tar"; + +import { ConfigJSON } from "../../src/server/config"; + +// load the package.json +const package_json = JSON.parse(fs.readFileSync("package.json", "utf-8")) as { version: string; dependencies: string[] }; + +const build_name = "JohnCG_" + package_json.version; +// temporary method until there is a solution for packaging sharp +// const exec_name = build_name + ".exe"; +const exec_name = "node.exe"; +const build_dir = "dist/build"; +const release_dir = path.join("dist", build_name); + +// clear the build-directory +fs.rmSync("dist", { recursive: true, force: true }); +fs.mkdirSync(build_dir, { recursive: true }); +fs.mkdirSync(path.join("dist", build_name), { recursive: true }); + +const copy_build_file = (file: string, dest?: string) => fs.copyFileSync(file, path.join(build_dir, dest ?? path.basename(file))); +// const copy_build_dir = (dir: string, dest?: string, args?: fs.CopySyncOptions) => fs.cpSync(dir, path.join(build_dir, dest ?? path.basename(dir)), { recursive: true, ...args }); +const copy_release_file = (file: string, dest?: string) => fs.copyFileSync(file, path.join(release_dir, dest ?? path.basename(file))); +const copy_release_dir = (dir: string, dest?: string, args?: fs.CopySyncOptions) => fs.cpSync(dir, path.join(release_dir, dest ?? path.basename(dir)), { recursive: true, ...args }); + +// bundle the different scripts +execSync("npm run build-server"); +execSync("npm run build-client"); +execSync("npm run build-templates"); + +// temporary method until there is a solution for packaging sharp +// // create sea-prep.blob +// execSync("node --experimental-sea-config sea-config.json"); + +// get the node executable +copy_build_file(process.execPath, exec_name); + +// temporary method until there is a solution for packaging sharp +// // remove the signature from the node executable +// execSync(`'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64\\signtool.exe' remove /s dist/build/${exec_name}`); +// // modify the node executable +// execSync(`npx postject dist/build/${exec_name} NODE_SEA_BLOB dist/build/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`); + +// load the config-file, censor the file-paths and store it for the relase +const config_file = JSON.parse(fs.readFileSync("config.json", "utf-8")) as ConfigJSON; +config_file.path = { + background_image: "C:/path/to/image/directory", + song: "D:/path/to/song/directory" +}; +config_file.casparcg.templates = "e:/path/to/the/casparcg/templates/directory"; +fs.writeFileSync(path.join(release_dir, "config.json"), JSON.stringify(config_file, undefined, "\t")); + +// copy the file to the output +copy_release_file(path.join(build_dir, exec_name)); +copy_release_file(path.join(build_dir, "main.js")); + +copy_release_dir("casparcg-templates", undefined, { filter: (src) => { + switch (true) { + case path.basename(src) === ".eslintrc": + case [".js", ".map"].includes(path.extname(src)): + return false; + default: + return true; + } +} }); +copy_release_dir("client", undefined, { filter: (src) => { + switch (true) { + case [".eslintrc", "bahnschrift.ttf"].includes(path.basename(src)): + case [".js", ".map"].includes(path.extname(src)): + return false; + default: + return true; + } +} }); +copy_release_dir("node_modules/@img", path.join(release_dir, "node_modules/@img/")); + +// temporary method until there is a solution for packaging sharp +// create a batch file, that start node with the main.js +fs.writeFileSync(path.join(release_dir, build_name + ".bat"), `${exec_name} main.js\npause`); + +// create and copy the licenses +// void lr.cli(["--config=build/scripts/license-reporter.config.ts"]); +try { + execSync("npx license-reporter --config build/scripts/license-reporter.config.ts"); +} catch (e) { /* empty */ } + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface License { name: string; licenseText: string } +const licenses_orig = JSON.parse(fs.readFileSync("build/scripts/3rdpartylicenses.json", "utf-8")) as License[]; + +const licenses: Record = {}; + +licenses_orig.forEach((pack) => { + licenses[pack.name] = pack; +}); + +fs.mkdirSync("dist/build/licenses"); + +Object.keys(package_json.dependencies).forEach((pack) => { + const lic = licenses[pack]; + + try { + fs.writeFileSync(`dist/build/licenses/${lic.name}.txt`, lic.licenseText, "utf-8"); + } catch (e) { + if (lic.licenseText === undefined) { + throw new EvalError(`ERROR: no license was found for the package '${lic.name}'`); + } + } +}); + +copy_release_file("LICENSE", "LICENSE.txt"); + +// copy the licenses +copy_release_dir(path.join(build_dir, "licenses")); + +// pack the files in a .tar.gz-file +void tar.c({ gzip: true, file: release_dir + ".tar.gz" }, [release_dir]); \ No newline at end of file diff --git a/license-reporter.config.ts b/build/scripts/license-reporter.config.ts similarity index 96% rename from license-reporter.config.ts rename to build/scripts/license-reporter.config.ts index 6a7e395..400f16f 100644 --- a/license-reporter.config.ts +++ b/build/scripts/license-reporter.config.ts @@ -2,7 +2,8 @@ import { IReporterConfiguration } from "@weichwarenprojekt/license-reporter"; export const configuration: Partial = { // defaultLicenseText: undefined, - output: "dist/build/3rdpartylicenses.json", + output: "build/scripts/3rdpartylicenses.json", + ignore: ["dist/*"], overrides: [ { name: "osc" diff --git a/sea-config.json b/build/scripts/sea-config.json similarity index 100% rename from sea-config.json rename to build/scripts/sea-config.json diff --git a/build/scripts/tsconfig.json b/build/scripts/tsconfig.json new file mode 100644 index 0000000..6005d72 --- /dev/null +++ b/build/scripts/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "removeComments": true + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 194e61a..86e8494 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", "@types/node-osc": "^6.0.3", + "@types/tar": "^6.1.11", "@types/tmp": "^0.2.6", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", @@ -35,6 +36,7 @@ "esbuild": "^0.20.0", "eslint": "^8.56.0", "postject": "^1.0.0-alpha.6", + "tar": "^6.2.0", "typescript": "^5.3.3" } }, @@ -721,6 +723,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-ThA1WD8aDdVU4VLuyq5NEqriwXErF5gEIJeyT6gHBWU7JtSmW2a5qjNv3/vR82O20mW+1vhmeZJfBQPT3HCugg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/tmp": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", diff --git a/package.json b/package.json index 17e78b4..243b1f4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@types/mime-types": "^2.1.4", "@types/node": "^20.11.16", "@types/node-osc": "^6.0.3", + "@types/tar": "^6.1.11", "@types/tmp": "^0.2.6", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^6.21.0", @@ -28,12 +29,13 @@ "esbuild": "^0.20.0", "eslint": "^8.56.0", "postject": "^1.0.0-alpha.6", + "tar": "^6.2.0", "typescript": "^5.3.3" }, "scripts": { "lint": "eslint src/**/*.ts", - "build-release": "@powershell -NoProfile -ExecutionPolicy Unrestricted -Command ./build.ps1", - "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=src/server/tsconfig.json --platform=node --minify --bundle", + "build-release": "esbuild build/scripts/build.ts --outfile=build/scripts/build.js --tsconfig=build/scripts/tsconfig.json --platform=node --bundle && node build/scripts/build.js", + "build-server": "esbuild src/server/main.ts --outfile=dist/build/main.js --tsconfig=src/server/tsconfig.json --platform=node --minify --bundle --external:pdfjs-dist --external:canvas", "build-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --minify", "watch-client": "esbuild src/client/main.ts --outfile=client/main.js --tsconfig=src/client/tsconfig.json --bundle --sourcemap --watch", "build-templates": "esbuild src/templates/*.ts --outdir=casparcg-templates/JohnCG/ --tsconfig=src/templates/tsconfig.json --target=chrome117 --minify", diff --git a/src/server/config.ts b/src/server/config.ts index a1916a5..062a0a1 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -5,7 +5,7 @@ export interface CasparCGConnectionSettings { layers: [number, number]; } -interface ConfigJSON { +export interface ConfigJSON { behaviour: { show_on_load: boolean; }; From f5db5c3b75bee4429dc6d55bbde102cf9ba80359 Mon Sep 17 00:00:00 2001 From: Simon Ziegler Date: Wed, 28 Feb 2024 04:18:46 +0100 Subject: [PATCH 6/6] fixed tar packaging in building --- build/scripts/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/scripts/build.ts b/build/scripts/build.ts index 05b494b..e430f6f 100644 --- a/build/scripts/build.ts +++ b/build/scripts/build.ts @@ -116,4 +116,4 @@ copy_release_file("LICENSE", "LICENSE.txt"); copy_release_dir(path.join(build_dir, "licenses")); // pack the files in a .tar.gz-file -void tar.c({ gzip: true, file: release_dir + ".tar.gz" }, [release_dir]); \ No newline at end of file +void tar.c({ gzip: true, file: release_dir + ".tar.gz", cwd: "dist" }, [path.relative("dist", release_dir)]); \ No newline at end of file