diff --git a/.env.sample b/.env.sample index 8c3990fc..da9f7260 100644 --- a/.env.sample +++ b/.env.sample @@ -14,3 +14,6 @@ BCRYPT_SALT_ROUNDS=${BCRYPT_SALT_ROUNDS:-10} # i18n UI_LANG=${UI_LANG:-en} + +# Mediaplayer - path on the system controller +MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 73c7afff..26159b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -35,7 +36,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", @@ -2512,6 +2514,14 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -11233,6 +11243,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3d341165..7b3597c1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "pretty:format": "prettier --write .", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "next lint", - "dev": "./update_gui_version.sh && next dev", + "dev": "next dev", "build": "next build", "start": "next start", "version:rc": "npm version prerelease --preid=rc", @@ -32,6 +32,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -48,7 +49,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts new file mode 100644 index 00000000..6f51466f --- /dev/null +++ b/src/api/ateliereLive/websocket.ts @@ -0,0 +1,45 @@ +import WebSocket from 'ws'; + +function createWebSocket(): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://${process.env.AGILE_WEBSOCKET}`); + ws.on('error', reject); + ws.on('open', () => { + // const send = ws.send.bind(ws); + // ws.send = (message) => { + // console.debug(`[websocket] sending message: ${message}`); + // send(message); + // }; + resolve(ws); + }); + }); +} + +export async function createControlPanelWebSocket() { + const ws = await createWebSocket(); + return { + createHtml: (input: number) => { + ws.send('html reset'); + ws.send(`html create ${input} 1920 1080`); + setTimeout(() => { + ws.send( + `html load ${input} ${process.env.NEXTAUTH_URL}/html_input?input=${input}` + ); + }, 1000); + }, + createMediaplayer: (input: number) => { + ws.send('media reset'); + ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`); + ws.send(`media play ${input}`); + }, + closeHtml: (input: number) => { + ws.send(`html close ${input}`); + }, + closeMediaplayer: (input: number) => { + ws.send(`media close ${input}`); + }, + close: () => { + ws.close(); + } + }; +} diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e68524ad..714d822d 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -2,6 +2,7 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Production, ProductionWithId } from '../../interfaces/production'; import { Log } from '../logger'; +import { SourceReference, Type } from '../../interfaces/Source'; export async function getProductions(): Promise { const db = await getDatabase(); diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 8bb83e80..a54fb38a 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -1,6 +1,6 @@ import inventory from './mocks/inventory.json'; import { Source } from '../../interfaces/Source'; -import { ObjectId } from 'mongodb'; +import { ObjectId, OptionalId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -9,37 +9,44 @@ export function getMockedSources() { export async function postSource(data: Source): Promise { const db = await getDatabase(); - return (await db.collection('inventory').insertOne(data)) - .insertedId as ObjectId; + const insertData: OptionalId> & { _id?: ObjectId } = { + ...data, + _id: typeof data._id === 'string' ? new ObjectId(data._id) : data._id + }; + const result = await db.collection('inventory').insertOne(insertData); + return result.insertedId as ObjectId; } export async function getSources() { const db = await getDatabase(); return await db.collection('inventory').find().toArray(); } - export async function getSourcesByIds(_ids: string[]) { const db = await getDatabase().catch(() => { - throw "Can't connect to Database"; - }); - const objectIds = _ids.map((id: string) => { - return new ObjectId(id); + throw new Error("Can't connect to Database"); }); - return ( - await db - .collection('inventory') - .find({ - _id: { - $in: objectIds - } - }) - .toArray() - ).sort( - (a, b) => - _ids.findIndex((id) => a._id.equals(id)) - - _ids.findIndex((id) => b._id.equals(id)) - ); + const objectIds = _ids.map((id: string) => new ObjectId(id)); + + const sources = await db + .collection('inventory') + .find({ + _id: { + $in: objectIds + } + }) + .toArray(); + + return sources.sort((a, b) => { + const findIndex = (id: ObjectId | string) => + _ids.findIndex((originalId) => + id instanceof ObjectId + ? id.equals(new ObjectId(originalId)) + : id === originalId + ); + + return findIndex(a._id) - findIndex(b._id); + }); } export async function updateSource(source: any) { diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 7538be78..8edc5a9e 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -49,6 +49,8 @@ import { Result } from '../../interfaces/result'; import { Monitoring } from '../../interfaces/monitoring'; import { getDatabase } from '../mongoClient/dbClient'; import { updatedMonitoringForProduction } from './job/syncMonitoring'; +import { createControlPanelWebSocket } from '../ateliereLive/websocket'; +import { ObjectId } from 'mongodb'; const isUsed = (pipeline: ResourcesPipelineResponse) => { const hasStreams = pipeline.streams.length > 0; @@ -89,7 +91,7 @@ async function connectIngestSources( source.ingest_source_name, false ); - const audioSettings = await getAudioMapping(source._id); + const audioSettings = await getAudioMapping(new ObjectId(source._id)); const newAudioMapping = audioSettings?.audio_stream?.audio_mapping; const audioMapping = newAudioMapping?.length ? newAudioMapping : [[0, 1]]; @@ -308,6 +310,14 @@ export async function stopProduction( (p) => p.pipeline_id ); + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + for (const source of production.sources) { for (const stream_uuid of source.stream_uuids || []) { await deleteStreamByUuid(stream_uuid).catch((error) => { @@ -316,6 +326,11 @@ export async function stopProduction( } } + htmlSources.map((source) => controlPanelWS.closeHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.closeMediaplayer(source.input_slot) + ); + for (const id of pipelineIds) { Log().info(`Stopping pipeline '${id}'`); if (!id) continue; @@ -449,10 +464,30 @@ export async function startProduction( // Try to setup streams from ingest(s) to pipeline(s) start try { // Get sources from the DB + // Skapa en createHtmlWebSocket, spara + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + + htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.createMediaplayer(source.input_slot) + ); + + controlPanelWS.close(); + + // Nedan behöver göras efter att vi har skapat en produktion + // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( - production.sources.map((source) => { - return source._id.toString(); - }) + production.sources + .filter((source) => source._id !== undefined) + .map((source) => { + return source._id!.toString(); + }) ).catch((error) => { if (error === "Can't connect to Database") { throw "Can't connect to Database"; @@ -720,7 +755,7 @@ export async function startProduction( ...production, sources: production.sources.map((source) => { const streamsForSource = streams?.filter( - (stream) => stream.source_id === source._id.toString() + (stream) => stream.source_id === source._id?.toString() ); return { ...source, diff --git a/src/app/html_input/page.tsx b/src/app/html_input/page.tsx new file mode 100644 index 00000000..81cfaa52 --- /dev/null +++ b/src/app/html_input/page.tsx @@ -0,0 +1,10 @@ +import { PageProps } from '../../../.next/types/app/html_input/page'; + +export default function HtmlInput({ searchParams: { input } }: PageProps) { + return ( +
+

HTML INPUT

+

{input}

+
+ ); +} diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index be3112c7..0b4259df 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -3,14 +3,15 @@ import React, { useEffect, useState, KeyboardEvent } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; import SourceListItem from '../../../components/sourceListItem/SourceListItem'; import FilterOptions from '../../../components/filter/FilterOptions'; -import { AddSource } from '../../../components/addSource/AddSource'; +import { AddInput } from '../../../components/addInput/AddInput'; import { IconX } from '@tabler/icons-react'; import { useSources } from '../../../hooks/sources/useSources'; import { AddSourceStatus, DeleteSourceStatus, SourceReference, - SourceWithId + SourceWithId, + Type } from '../../../interfaces/Source'; import { useGetProduction, usePutProduction } from '../../../hooks/productions'; import { Production } from '../../../interfaces/production'; @@ -40,8 +41,9 @@ import { RemoveSourceModal } from '../../../components/modal/RemoveSourceModal'; import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; -import { ISource } from '../../../hooks/useDragableItems'; import { useMultiviews } from '../../../hooks/multiviews'; +import { v4 as uuidv4 } from 'uuid'; +import { Select } from '../../../components/select/Select'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -51,6 +53,9 @@ export default function ProductionConfiguration({ params }: PageProps) { const [filteredSources, setFilteredSources] = useState( new Map() ); + const [selectedValue, setSelectedValue] = useState( + t('production.add_other_source_type') + ); const [addSourceModal, setAddSourceModal] = useState(false); const [removeSourceModal, setRemoveSourceModal] = useState(false); const [selectedSource, setSelectedSource] = useState< @@ -59,6 +64,8 @@ export default function ProductionConfiguration({ params }: PageProps) { const [selectedSourceRef, setSelectedSourceRef] = useState< SourceReference | undefined >(); + const [sourceReferenceToAdd, setSourceReferenceToAdd] = + useState(); const [createStream, loadingCreateStream] = useCreateStream(); const [deleteStream, loadingDeleteStream] = useDeleteStream(); //PRODUCTION @@ -88,11 +95,39 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + const isAddButtonDisabled = + selectedValue !== 'HTML' && selectedValue !== 'Media Player'; + useEffect(() => { refreshPipelines(); refreshControlPanels(); }, [productionSetup?.isActive]); + // TODO: Väldigt lik den för ingest_source --> ändra?? + const addSourceToProduction = (type: Type) => { + const newSource: SourceReference = { + _id: uuidv4(), + type: type, + label: type === 'html' ? 'HTML Input' : 'Media Player Source', + input_slot: getFirstEmptySlot() + }; + + setSourceReferenceToAdd(newSource); + + if (productionSetup) { + const updatedSetup = addSetupItem(newSource, productionSetup); + + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + refreshProduction(); + setAddSourceModal(false); + setSourceReferenceToAdd(undefined); + }); + setAddSourceStatus(undefined); + } + }; + const setSelectedControlPanel = (controlPanel: string[]) => { setProductionSetup((prevState) => { if (!prevState) return; @@ -219,6 +254,12 @@ export default function ProductionConfiguration({ params }: PageProps) { setFilteredSources(sources); }, [sources]); + useEffect(() => { + if (selectedValue === t('production.source')) { + setInventoryVisible(true); + } + }, [selectedValue]); + const updatePreset = (preset: Preset) => { if (!productionSetup?._id) return; putProduction(productionSetup?._id.toString(), { @@ -379,6 +420,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const updatedSetup = addSetupItem( { _id: source._id.toString(), + type: 'ingest_source', label: source.ingest_source_name, input_slot: getFirstEmptySlot() }, @@ -456,8 +498,9 @@ export default function ProductionConfiguration({ params }: PageProps) { } if (result.ok) { if (result.value.success) { - const sourceToAdd = { + const sourceToAdd: SourceReference = { _id: result.value.streams[0].source_id, + type: 'ingest_source', label: selectedSource.name, stream_uuids: result.value.streams.map((r) => r.stream_uuid), input_slot: getFirstEmptySlot() @@ -601,6 +644,7 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(undefined); setDeleteSourceStatus(undefined); }; + return ( <> @@ -700,15 +744,12 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); }} - onSourceUpdate={( - source: SourceReference, - sourceItem: ISource - ) => { - sourceItem.label = source.label; + onSourceUpdate={(source: SourceReference) => { updateSource(source, productionSetup); }} onSourceRemoval={(source: SourceReference) => { @@ -719,6 +760,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const updatedSetup = removeSetupItem( { _id: source._id, + type: source.type, label: source.label, input_slot: source.input_slot }, @@ -748,15 +790,43 @@ export default function ProductionConfiguration({ params }: PageProps) { )} )} - { - setInventoryVisible(true); - }} - /> +
+ setInventoryVisible(true)} + disabled={ + productionSetup?.production_settings === undefined || + productionSetup.production_settings === null + } + /> +
+ + {options.map((value) => ( + + ))} + + ); +}; diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 6d1ecd85..246e7c22 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -2,20 +2,22 @@ import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; import { IconTrash } from '@tabler/icons-react'; -import { SourceReference } from '../../interfaces/Source'; +import { SourceReference, Type } from '../../interfaces/Source'; import { SourceThumbnail } from './SourceThumbnail'; import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source: ISource; + source?: ISource; label: string; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src: string; + src?: string; + sourceRef?: SourceReference; + type: Type; }; export default function SourceCard({ @@ -26,9 +28,13 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + sourceRef, + type }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); + const [sourceLabel, setSourceLabel] = useState( + sourceRef?.label || source?.name + ); const t = useTranslate(); @@ -37,20 +43,29 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - // if (source.name === label) { - // return; - // } - if (sourceLabel.length === 0) { - setSourceLabel(source.name); + if (sourceLabel?.length === 0) { + if (source) { + setSourceLabel(source.name); + } else if (sourceRef) { + setSourceLabel(sourceRef.label); + } } - onSourceUpdate( - { + + if (source) { + onSourceUpdate({ _id: source._id.toString(), - label: sourceLabel, + type: 'ingest_source', + label: sourceLabel || source.name, input_slot: source.input_slot - }, - source - ); + }); + } else if (sourceRef) { + onSourceUpdate({ + _id: sourceRef._id, + type: sourceRef.type, + label: sourceLabel || sourceRef.label, + input_slot: sourceRef.input_slot + }); + } }; const handleKeyDown = (event: KeyboardEvent) => { @@ -77,25 +92,59 @@ export default function SourceCard({ onBlur={saveText} />
- -

- {t('source.ingest', { - ingest: source.ingest_name - })} -

- + {source && source.src && ( + + )} + {!source && sourceRef && } + {(sourceRef || source) && ( +

+ {t('source.input_slot', { + input_slot: + sourceRef?.input_slot?.toString() || + source?.input_slot?.toString() || + '' + })} +

+ )} + + {source && ( +

+ {t('source.ingest', { + ingest: source.ingest_name + })} +

+ )} + {(source || sourceRef) && ( + + )}
); } diff --git a/src/components/sourceCard/SourceThumbnail.tsx b/src/components/sourceCard/SourceThumbnail.tsx index 5aa7114f..b5e5bcbe 100644 --- a/src/components/sourceCard/SourceThumbnail.tsx +++ b/src/components/sourceCard/SourceThumbnail.tsx @@ -2,18 +2,19 @@ import Image from 'next/image'; import { useState } from 'react'; -import { Source } from '../../interfaces/Source'; +import { Source, Type } from '../../interfaces/Source'; import { IconExclamationCircle } from '@tabler/icons-react'; type SourceThumbnailProps = { - source: Source; - src: string; + source?: Source; + src?: string; + type?: Type; }; -export function SourceThumbnail({ source, src }: SourceThumbnailProps) { +export function SourceThumbnail({ source, src, type }: SourceThumbnailProps) { const [loaded, setLoaded] = useState(false); - if (source.status === 'gone') { + if (source && source.status === 'gone') { return (
@@ -22,20 +23,37 @@ export function SourceThumbnail({ source, src }: SourceThumbnailProps) { } return ( - Preview Thumbnail setLoaded(true)} - onError={() => setLoaded(true)} - placeholder="empty" - width={0} - height={0} - sizes="20vh" - style={{ - width: 'auto', - height: '100%' - }} - /> + <> + {(type === 'ingest_source' || !type) && src && ( + Preview Thumbnail setLoaded(true)} + onError={() => setLoaded(true)} + placeholder="empty" + width={0} + height={0} + sizes="20vh" + style={{ + width: 'auto', + height: '100%' + }} + /> + )} + {(type === 'html' || type === 'mediaplayer') && ( + +

+ {type === 'html' ? 'HTML' : 'Media Player'} +

+
+ )} + ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9666bccc..d8615784 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -5,108 +5,73 @@ import { SourceReference } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; -import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; - export default function SourceCards({ productionSetup, + sourceRef, updateProduction, onSourceUpdate, onSourceRemoval }: { productionSetup: Production; + sourceRef?: SourceReference; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); - const currentOrder: SourceReference[] = items.map((source) => { - return { - _id: source._id.toString(), - label: source.label, - input_slot: source.input_slot, - stream_uuids: source.stream_uuids - }; - }); - const gridItems: React.JSX.Element[] = []; - let tempItems = [...items]; - let firstEmptySlot = items.length + 1; + if (loading || !items) return null; + + // Filter SourceReference and ISource objects correctly + const sourceReferences = items.filter( + (item): item is SourceReference => item.type !== 'ingest_source' + ); - if (!items || items.length === 0) return null; - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - if (!items.some((source) => source.input_slot === i + 1)) { - firstEmptySlot = i + 1; - break; - } - } - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - // console.log(`On input slot: ${i + 1}`); - // console.log(`Checking sources:`); - // console.log(tempItems); - tempItems.every((source) => { - if (source.input_slot === i + 1) { - // console.log(`Found source on input slot: ${i + 1}`); - // console.log(`Removing source "${source.name}" from sources list`); - tempItems = tempItems.filter((i) => i._id !== source._id); - // console.log(`Adding source "${source.name}" to grid`); - if (!productionSetup.isActive) { - gridItems.push( - - - setSelectingText(isSelecting) - } - /> - - ); - } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); - } - return false; - } else { - // console.log(`No source found on input slot: ${i + 1}`); - // console.log(`Adding empty slot to grid`); - if (productionSetup.isActive) { - gridItems.push( - - ); - } + const isISource = (source: SourceReference | ISource): source is ISource => { + // Use properties unique to ISource to check the type + return 'src' in source; + }; + + const gridItems = items.map((source) => { + const isSource = isISource(source); + + return ( + + {isSource ? ( + setSelectingText(isSelecting)} + type={'ingest_source'} + /> + ) : ( + setSelectingText(isSelecting)} + type={source.type} + /> + )} + + ); + }); - return false; - } - }); - } return <>{gridItems}; } diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf7..c8b724e5 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceWithId } from '../../interfaces/Source'; +import { Source, SourceReference, SourceWithId } from '../../interfaces/Source'; import { PreviewThumbnail } from './PreviewThumbnail'; import { getSourceThumbnail } from '../../utils/source'; import videoSettings from '../../utils/videoSettings'; @@ -95,7 +95,7 @@ function InventoryListItem({ : [] ); } - }, [source.audio_stream.audio_mapping]); + }, [source?.audio_stream.audio_mapping]); return (
  • a.input_slot - b.input_slot) }; - return { ...updatedSetup, sources: [ ...productionSetup.sources, { _id: source._id, + type: source.type, label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index e3164fe9..cdaf4612 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -11,7 +11,8 @@ export function usePostProduction() { isActive: false, name, sources: [], - selectedPresetRef: undefined + html: [], + mediaplayers: [] }) }); if (response.ok) { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 427ffbf1..4cc3a03b 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -9,57 +9,82 @@ export interface ISource extends SourceWithId { stream_uuids?: string[]; src: string; } + export function useDragableItems( sources: SourceReference[] -): [ISource[], (originId: string, destinationId: string) => void, boolean] { +): [ + (SourceReference | ISource)[], + (originId: string, destinationId: string) => void, + boolean +] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState( + const [items, setItems] = useState<(SourceReference | ISource)[]>( sources.flatMap((ref) => { const source = inventorySources.get(ref._id); if (!source) return []; return { ...source, + _id: ref._id, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) + src: getSourceThumbnail(source), + ingest_source_name: source.ingest_source_name, + ingest_name: source.ingest_name, + video_stream: source.video_stream, + audio_stream: source.audio_stream, + status: source.status, + type: source.type, + tags: source.tags, + name: source.name }; }) ); useEffect(() => { - setItems( - sources.flatMap((ref) => { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) - }; - }) - ); + const updatedItems = sources.map((ref) => { + const source = inventorySources.get(ref._id); + if (!source) return { ...ref }; + return { + ...ref, + _id: ref._id, + status: source.status, + name: source.name, + type: source.type, + tags: source.tags, + ingest_name: source.ingest_name, + ingest_source_name: source.ingest_source_name, + ingest_type: source.ingest_type, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + video_stream: source.video_stream, + audio_stream: source.audio_stream, + lastConnected: source.lastConnected + }; + }); + setItems(updatedItems); }, [sources, inventorySources]); const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((i) => i._id.toString() === originId); + const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => item._id.toString() === destinationId ); + if (!originSource || !destinationSource) return; - const originInputSlot = originSource.input_slot; - const destinationInputSlot = destinationSource.input_slot; - originSource.input_slot = destinationInputSlot; - destinationSource.input_slot = originInputSlot; - const updatedItems = [ - ...items.filter( - (i) => i._id !== originSource._id && i._id !== destinationSource._id - ), - originSource, - destinationSource - ].sort((a, b) => a.input_slot - b.input_slot); + + const updatedItems = items + .map((item) => { + if (item._id === originSource._id) + return { ...item, input_slot: destinationSource.input_slot }; + if (item._id === destinationSource._id) + return { ...item, input_slot: originSource.input_slot }; + return item; + }) + .sort((a, b) => a.input_slot - b.input_slot); + setItems(updatedItems); }; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e57..596e25c8 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -46,7 +46,8 @@ export const en = { orig: 'Original Name: {{name}}', metadata: 'Source Metadata', location_unknown: 'Unknown', - last_connected: 'Last connection' + last_connected: 'Last connection', + input_slot: 'Input slot: {{input_slot}}' }, delete_source_status: { delete_stream: 'Delete stream', @@ -63,14 +64,17 @@ export const en = { }, production_configuration: 'Production Configuration', production: { - add_source: 'Add Source', + add_source: 'Add ingest', select_preset: 'Select Preset', clear_selection: 'Clear Selection', started: 'Production started: {{name}}', failed: 'Production start failed: {{name}}', stopped: 'Production stopped: {{name}}', stop_failed: 'Production stop failed: {{name}}', - missing_multiview: 'Missing multiview reference in selected preset' + missing_multiview: 'Missing multiview reference in selected preset', + source: 'Source', + add: 'Add', + add_other_source_type: 'Add other source type' }, create_new: 'Create New', default_prod_placeholder: 'My New Configuration', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index b33112df..bf9f42c8 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -48,7 +48,8 @@ export const sv = { orig: 'Enhetsnamn: {{name}}', metadata: 'Käll-metadata', location_unknown: 'Okänd', - last_connected: 'Senast uppkoppling' + last_connected: 'Senast uppkoppling', + input_slot: 'Ingång: {{input_slot}}' }, delete_source_status: { delete_stream: 'Radera ström', @@ -65,14 +66,17 @@ export const sv = { }, production_configuration: 'Produktionskonfiguration', production: { - add_source: 'Lägg till källa', + add_source: 'Lägg till ingång', select_preset: 'Välj produktionsmall', clear_selection: 'Rensa val', started: 'Produktion startad: {{name}}', failed: 'Start av produktion misslyckades: {{name}}', stopped: 'Produktion stoppad: {{name}}', stop_failed: 'Stopp av produktion misslyckades: {{name}}', - missing_multiview: 'Saknar referens till en multiview i valt preset' + missing_multiview: 'Saknar referens till en multiview i valt preset', + source: 'Källa', + add: 'Lägg till', + add_other_source_type: 'Lägg till annan källtyp' }, create_new: 'Skapa ny', default_prod_placeholder: 'Min Nya Konfiguration', diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index e59afa4a..1aec774f 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,6 +1,7 @@ import { ObjectId, WithId } from 'mongodb'; export type SourceType = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; +export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { height?: number; width?: number; @@ -16,7 +17,7 @@ export type AudioStream = { export type Numbers = number | number[]; export interface Source { - _id?: ObjectId; + _id?: ObjectId | string; status: SourceStatus; name: string; type: SourceType; @@ -34,6 +35,7 @@ export interface Source { export interface SourceReference { _id: string; + type: Type; label: string; stream_uuids?: string[]; input_slot: number; diff --git a/src/middleware.ts b/src/middleware.ts index 7724e3b7..cac08478 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -41,4 +41,4 @@ export default withAuth(function middleware(req) { } }); -export const config = { matcher: ['/', '/((?!api|images).*)/'] }; +export const config = { matcher: ['/', '/((?!api|images|html_input).*)/'] };