From 91f477a0f2aae410127e175494293a0467a2d7d6 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 09:24:02 +0200 Subject: [PATCH 01/10] feat: support media and html sources in prod and web sockets --- .env.sample | 4 + package-lock.json | 32 +- package.json | 6 +- .../pipelines/multiviews/multiviews.ts | 7 +- src/api/ateliereLive/websocket.ts | 40 +++ src/api/manager/productions.ts | 27 +- src/api/manager/sources.ts | 54 ++-- src/api/manager/workflow.ts | 85 +++++- src/app/html_input/page.tsx | 10 + src/app/production/[id]/page.tsx | 276 ++++++++++-------- src/components/addInput/AddInput.tsx | 30 ++ src/components/addSource/AddSource.tsx | 24 -- src/components/dragElement/DragItem.tsx | 48 +-- src/components/filter/FilterDropdown.tsx | 5 +- src/components/filter/FilterOptions.tsx | 8 + src/components/filter/SortSelect.tsx | 21 -- src/components/modal/AddSourceModal.tsx | 1 - src/components/select/Select.tsx | 30 ++ src/components/sourceCard/SourceCard.tsx | 123 +++++--- src/components/sourceCard/SourceThumbnail.tsx | 58 ++-- src/components/sourceCards/SourceCards.tsx | 111 ++++--- .../sourceListItem/SourceListItem.tsx | 4 +- .../startProduction/StartProductionButton.tsx | 5 + src/hooks/items/addSetupItem.ts | 3 +- src/hooks/pipelines.ts | 1 + src/hooks/productions.ts | 7 +- src/hooks/sources/useAddSource.tsx | 38 +++ src/hooks/sources/useSources.tsx | 6 +- src/hooks/useDragableItems.ts | 89 +++--- src/hooks/useGetFirstEmptySlot.ts | 37 +++ src/i18n/locales/en.ts | 10 +- src/i18n/locales/sv.ts | 10 +- src/interfaces/Source.ts | 6 +- src/middleware.ts | 2 +- 34 files changed, 826 insertions(+), 392 deletions(-) create mode 100644 src/api/ateliereLive/websocket.ts create mode 100644 src/app/html_input/page.tsx create mode 100644 src/components/addInput/AddInput.tsx delete mode 100644 src/components/addSource/AddSource.tsx delete mode 100644 src/components/filter/SortSelect.tsx create mode 100644 src/components/select/Select.tsx create mode 100644 src/hooks/sources/useAddSource.tsx create mode 100644 src/hooks/useGetFirstEmptySlot.ts diff --git a/.env.sample b/.env.sample index 8c3990f..3ff9058 100644 --- a/.env.sample +++ b/.env.sample @@ -4,6 +4,7 @@ MONGODB_URI=${MONGODB_URI:-mongodb://api:password@localhost:27017/live-gui} # Ateliere Live System Controlleer LIVE_URL=${LIVE_URL:-https://localhost:8080} LIVE_CREDENTIALS=${LIVE_CREDENTIALS:-admin:admin} +CONTROL_PANEL_WS==${} # This ENV variable disables SSL Verification, use if the above LIVE_URL doesn't have a proper certificate NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-1} @@ -14,3 +15,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 73c7aff..26159b5 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 3d34116..7b3597c 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/pipelines/multiviews/multiviews.ts b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts index 5f24a86..ac76f6c 100644 --- a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts +++ b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts @@ -64,13 +64,12 @@ export async function createMultiviewForPipeline( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion productionSettings.pipelines[multiviewIndex].pipeline_id!; const sources = await getSourcesByIds( - sourceRefs.map((ref) => ref._id.toString()) + sourceRefs.map((ref) => (ref._id ? ref._id.toString() : '')) ); const sourceRefsWithLabels = sourceRefs.map((ref) => { + const refId = ref._id ? ref._id.toString() : ''; if (!ref.label) { - const source = sources.find( - (source) => source._id.toString() === ref._id.toString() - ); + const source = sources.find((source) => source._id.toString() === refId); ref.label = source?.name || ''; } return ref; diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts new file mode 100644 index 0000000..0cbe35d --- /dev/null +++ b/src/api/ateliereLive/websocket.ts @@ -0,0 +1,40 @@ +import WebSocket from 'ws'; + +function createWebSocket(): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://${process.env.CONTROL_PANEL_WS}`); + 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 create ${input} 1920 1080`); + }, + createMediaplayer: (input: number) => { + ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`); + }, + closeHtml: (input: number) => { + ws.send(`html close ${input}`); + ws.send('html reset'); + }, + closeMediaplayer: (input: number) => { + ws.send(`media close ${input}`); + ws.send('media reset'); + }, + close: () => + setTimeout(() => { + ws.close(); + }, 1000) + }; +} diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e68524a..e364776 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -28,14 +28,29 @@ export async function setProductionsIsActiveFalse(): Promise< export async function putProduction( id: string, production: Production -): Promise { +): Promise { const db = await getDatabase(); + const newSourceId = new ObjectId().toString(); + + const sources = production.sources + ? production.sources.flatMap((singleSource) => { + return singleSource._id + ? singleSource + : { + _id: newSourceId, + type: singleSource.type, + label: singleSource.label, + input_slot: singleSource.input_slot + }; + }) + : []; + await db.collection('productions').findOneAndReplace( { _id: new ObjectId(id) }, { name: production.name, isActive: production.isActive, - sources: production.sources, + sources: sources, production_settings: production.production_settings } ); @@ -43,6 +58,14 @@ export async function putProduction( if (!production.isActive) { deleteMonitoring(db, id); } + + return { + _id: new ObjectId(id).toString(), + name: production.name, + isActive: production.isActive, + sources: sources, + production_settings: production.production_settings + }; } export async function postProduction(data: Production): Promise { diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 8bb83e8..4e77f39 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, WithId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -9,37 +9,45 @@ 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[]) { +export async function getSourcesByIds( + _ids: string[] +): Promise[]> { 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"); }); + const objectIds = _ids.map((id: string) => new ObjectId(id)); - 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 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 7538be7..7b9e392 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -1,3 +1,4 @@ +import { SourceReference, SourceWithId } from './../../interfaces/Source'; import { Production, ProductionSettings, @@ -35,7 +36,7 @@ import { ResourcesSenderNetworkEndpoint } from '../../../types/ateliere-live'; import { getSourcesByIds } from './sources'; -import { SourceWithId, SourceToPipelineStream } from '../../interfaces/Source'; +import { SourceToPipelineStream } from '../../interfaces/Source'; import { getAvailablePortsForIngest, getCurrentlyUsedPorts, @@ -49,6 +50,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; @@ -68,15 +71,18 @@ const isUsed = (pipeline: ResourcesPipelineResponse) => { }; async function connectIngestSources( + productionSources: SourceReference[], productionSettings: ProductionSettings, sources: SourceWithId[], usedPorts: Set ) { - let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; + let input_slot = 0; for (const source of sources) { - input_slot = input_slot + 1; + input_slot = + productionSources.find((s) => s._id === source._id.toString()) + ?.input_slot || input_slot + 1; const ingestUuid = await getUuidFromIngestName( source.ingest_name, false @@ -89,7 +95,8 @@ 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]]; @@ -108,6 +115,7 @@ async function connectIngestSources( Log().info( `Allocated port ${availablePort} on '${source.ingest_name}' for ${source.ingest_source_name}` ); + const stream: PipelineStreamSettings = { pipeline_id: pipeline.pipeline_id!, alignment_ms: pipeline.alignment_ms, @@ -138,9 +146,10 @@ async function connectIngestSources( } ] }; + try { Log().info( - `Connecting '${source.ingest_name}/${ingestUuid}}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` + `Connecting '${source.ingest_name}/${ingestUuid}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` ); Log().debug(stream); const result = await connectIngestToPipeline(stream).catch((error) => { @@ -150,6 +159,7 @@ async function connectIngestSources( ); throw `Source '${source.ingest_name}/${ingestUuid}:${source.ingest_source_name}' failed to connect to '${pipeline.pipeline_name}/${pipeline.pipeline_id}': ${error.message}`; }); + usedPorts.add(availablePort); sourceToPipelineStreams.push({ source_id: source._id.toString(), @@ -308,6 +318,24 @@ 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 htmlSources) { + controlPanelWS.closeHtml(source.input_slot); + } + + for (const source of mediaPlayerSources) { + controlPanelWS.closeMediaplayer(source.input_slot); + } + + controlPanelWS.close(); + for (const source of production.sources) { for (const stream_uuid of source.stream_uuids || []) { await deleteStreamByUuid(stream_uuid).catch((error) => { @@ -355,6 +383,7 @@ export async function stopProduction( }; } } + try { await removePipelineStreams(id).catch((error) => { Log().error( @@ -409,7 +438,13 @@ export async function stopProduction( } } Log().info(`Pipeline '${id}' stopped`); + + const pipelines = await getPipelines(); + const pipelineFeedbackStreams = pipelines.find( + (p) => p.uuid === id + )?.feedback_streams; } + if ( !disconnectConnectionsStatus.ok || !removePipelineStreamsStatus.ok || @@ -450,9 +485,15 @@ export async function startProduction( try { // Get sources from the DB const sources = await getSourcesByIds( - production.sources.map((source) => { - return source._id.toString(); - }) + production.sources + .filter( + (source) => + (source._id !== undefined && source.type !== 'html') || + source.type !== 'mediaplayer' + ) + .map((source) => { + return source._id!.toString(); + }) ).catch((error) => { if (error === "Can't connect to Database") { throw "Can't connect to Database"; @@ -537,8 +578,8 @@ export async function startProduction( return pipeline.uuid; }) ); - streams = await connectIngestSources( + production.sources, production_settings, sources, usedPorts @@ -611,6 +652,24 @@ export async function startProduction( }; } // Try to connect control panels and pipeline-to-pipeline connections end + 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 htmlSources) { + controlPanelWS.createHtml(source.input_slot); + } + + for (const source of mediaPlayerSources) { + controlPanelWS.createMediaplayer(source.input_slot); + } + + controlPanelWS.close(); + // Try to setup pipeline outputs start try { for (const pipeline of production_settings.pipelines) { @@ -648,7 +707,6 @@ export async function startProduction( error: e }; } // Try to setup pipeline outputs end - // Try to setup multiviews start try { if (!production.production_settings.pipelines[0].multiviews) { @@ -720,12 +778,13 @@ 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, - stream_uuids: streamsForSource?.map((s) => s.stream_uuid), - input_slot: streamsForSource[0].input_slot + stream_uuids: + streamsForSource?.map((s) => s.stream_uuid) || undefined, + input_slot: source.input_slot }; }), isActive: true diff --git a/src/app/html_input/page.tsx b/src/app/html_input/page.tsx new file mode 100644 index 0000000..81cfaa5 --- /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 be3112c..4fc3cd0 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,16 +1,18 @@ 'use client'; + 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 +42,10 @@ 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 { useAddSource } from '../../../hooks/sources/useAddSource'; +import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; +import { Select } from '../../../components/select/Select'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -51,6 +55,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< @@ -88,11 +95,36 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + // Create source + const [firstEmptySlot] = useGetFirstEmptySlot(); + const [addSource] = useAddSource(); + + const isAddButtonDisabled = + selectedValue !== 'HTML' && selectedValue !== 'Media Player'; + useEffect(() => { refreshPipelines(); refreshControlPanels(); }, [productionSetup?.isActive]); + const addSourceToProduction = (type: Type) => { + const input: SourceReference = { + type: type, + label: type === 'html' ? 'HTML Input' : 'Media Player Source', + input_slot: firstEmptySlot(productionSetup) + }; + + if (!productionSetup) return; + addSource(input, productionSetup).then((updatedSetup) => { + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + refreshProduction(); + setAddSourceModal(false); + setSelectedSource(undefined); + }); + setAddSourceStatus(undefined); + }; + const setSelectedControlPanel = (controlPanel: string[]) => { setProductionSetup((prevState) => { if (!prevState) return; @@ -219,6 +251,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(), { @@ -362,6 +400,8 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } + + // Adding source to a production, both in setup-mode and in live-mode function getSourcesToDisplay( filteredSources: Map ): React.ReactNode[] { @@ -376,22 +416,18 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(source); setAddSourceModal(true); } else if (productionSetup) { - const updatedSetup = addSetupItem( - { - _id: source._id.toString(), - label: source.ingest_source_name, - input_slot: getFirstEmptySlot() - }, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); + const input: SourceReference = { + _id: source._id.toString(), + type: 'ingest_source', + label: source.ingest_source_name, + input_slot: firstEmptySlot(productionSetup) + }; + addSource(input, productionSetup).then((updatedSetup) => { + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + setAddSourceModal(false); + setSelectedSource(undefined); + }); } }} /> @@ -399,28 +435,6 @@ export default function ProductionConfiguration({ params }: PageProps) { }); } - const getFirstEmptySlot = () => { - if (!productionSetup) throw 'no_production'; - let firstEmptySlot = productionSetup.sources.length + 1; - if (productionSetup.sources.length === 0) { - return firstEmptySlot; - } - for ( - let i = 0; - i < - productionSetup.sources[productionSetup.sources.length - 1].input_slot; - i++ - ) { - if ( - !productionSetup.sources.some((source) => source.input_slot === i + 1) - ) { - firstEmptySlot = i + 1; - break; - } - } - return firstEmptySlot; - }; - const handleAddSource = async () => { setAddSourceStatus(undefined); if ( @@ -435,11 +449,10 @@ export default function ProductionConfiguration({ params }: PageProps) { ) : false) ) { - const firstEmptySlot = getFirstEmptySlot(); const result = await createStream( selectedSource, productionSetup, - firstEmptySlot ? firstEmptySlot : productionSetup.sources.length + 1 + firstEmptySlot(productionSetup) ); if (!result.ok) { if (!result.value) { @@ -456,11 +469,12 @@ 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() + input_slot: firstEmptySlot(productionSetup) }; const updatedSetup = addSetupItem(sourceToAdd, productionSetup); if (!updatedSetup) return; @@ -479,12 +493,7 @@ export default function ProductionConfiguration({ params }: PageProps) { }; const handleRemoveSource = async () => { - if ( - productionSetup && - productionSetup.isActive && - selectedSourceRef && - selectedSourceRef.stream_uuids - ) { + if (productionSetup && productionSetup.isActive && selectedSourceRef) { const multiviews = productionSetup.production_settings.pipelines[0].multiviews; @@ -496,9 +505,60 @@ export default function ProductionConfiguration({ params }: PageProps) { ) ); - if (!viewToUpdate) { - if (!productionSetup.production_settings.pipelines[0].pipeline_id) + if (selectedSourceRef.stream_uuids) { + if (!viewToUpdate) { + if (!productionSetup.production_settings.pipelines[0].pipeline_id) + return; + + const result = await deleteStream( + selectedSourceRef.stream_uuids, + productionSetup, + selectedSourceRef.input_slot + ); + + if (!result.ok) { + if (!result.value) { + setDeleteSourceStatus({ + success: false, + steps: [{ step: 'unexpected', success: false }] + }); + } else { + setDeleteSourceStatus({ success: false, steps: result.value }); + const didDeleteStream = result.value.some( + (step) => step.step === 'delete_stream' && step.success + ); + if (didDeleteStream) { + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then( + () => { + setSelectedSourceRef(undefined); + } + ); + return; + } + } + return; + } + + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + + if (!updatedSetup) return; + + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + }); return; + } const result = await deleteStream( selectedSourceRef.stream_uuids, @@ -524,61 +584,12 @@ export default function ProductionConfiguration({ params }: PageProps) { ); if (!updatedSetup) return; setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setSelectedSourceRef(undefined); - } - ); + putProduction(updatedSetup._id.toString(), updatedSetup); return; } } return; } - - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - - if (!updatedSetup) return; - - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - setRemoveSourceModal(false); - setSelectedSourceRef(undefined); - }); - return; - } - - const result = await deleteStream( - selectedSourceRef.stream_uuids, - productionSetup, - selectedSourceRef.input_slot - ); - - if (!result.ok) { - if (!result.value) { - setDeleteSourceStatus({ - success: false, - steps: [{ step: 'unexpected', success: false }] - }); - } else { - setDeleteSourceStatus({ success: false, steps: result.value }); - const didDeleteStream = result.value.some( - (step) => step.step === 'delete_stream' && step.success - ); - if (didDeleteStream) { - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup); - return; - } - } - return; } const updatedSetup = removeSetupItem(selectedSourceRef, productionSetup); if (!updatedSetup) return; @@ -601,6 +612,7 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(undefined); setDeleteSourceStatus(undefined); }; + return ( <> @@ -645,6 +657,7 @@ export default function ProductionConfiguration({ params }: PageProps) { @@ -704,11 +717,7 @@ export default function ProductionConfiguration({ params }: PageProps) { updateProduction={(updated) => { updateProduction(productionSetup._id, updated); }} - onSourceUpdate={( - source: SourceReference, - sourceItem: ISource - ) => { - sourceItem.label = source.label; + onSourceUpdate={(source: SourceReference) => { updateSource(source, productionSetup); }} onSourceRemoval={(source: SourceReference) => { @@ -719,6 +728,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 +758,47 @@ 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/modal/AddSourceModal.tsx b/src/components/modal/AddSourceModal.tsx index bacb965..236b3cb 100644 --- a/src/components/modal/AddSourceModal.tsx +++ b/src/components/modal/AddSourceModal.tsx @@ -27,7 +27,6 @@ export function AddSourceModal({

{t('workflow.add_source_modal', { name })}

-
{status && }
+ {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 5aa7114..b5e5bcb 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 9666bcc..c20b8c8 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -1,13 +1,11 @@ 'use client'; - import React, { useState } from 'react'; 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'; - +import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; export default function SourceCards({ productionSetup, updateProduction, @@ -16,19 +14,15 @@ export default function SourceCards({ }: { productionSetup: Production; 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 [items, moveItem] = 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 - }; - }); + if (!items) return null; + const isISource = (source: SourceReference | ISource): source is ISource => { + return 'src' in source; + }; const gridItems: React.JSX.Element[] = []; let tempItems = [...items]; @@ -41,59 +35,81 @@ export default function SourceCards({ 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) => { + const id = source._id ? source._id : ''; + const isSource = isISource(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) - } - /> + {isSource ? ( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) : ( + + setSelectingText(isSelecting) + } + type={source.type} + /> + )} ); } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); + isSource + ? gridItems.push( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) + : gridItems.push( + + setSelectingText(isSelecting) + } + type={source.type} + /> + ); } 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( ); } - return false; } }); diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf..c8b724e 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 (
  • ; disabled: boolean; refreshProduction: () => void; }; export function StartProductionButton({ production, + sources, disabled, refreshProduction }: StartProductionButtonProps) { @@ -45,6 +48,8 @@ export function StartProductionButton({ const onClick = () => { if (!production) return; + console.log('sources', sources); + console.log('production', production); const hasUndefinedPipeline = production.production_settings.pipelines.some( (p) => !p.pipeline_name ); diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index 2dda22b..b341c44 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -14,19 +14,20 @@ export function addSetupItem( ...productionSetup.sources, { _id: source._id, + type: source.type, label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot } ].sort((a, b) => 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/pipelines.ts b/src/hooks/pipelines.ts index f9ef473..baa52ff 100644 --- a/src/hooks/pipelines.ts +++ b/src/hooks/pipelines.ts @@ -33,6 +33,7 @@ export function usePipeline( setLoading(true); getPipeline(id) .then((pipeline) => { + console.log('pipeline', pipeline); setPipeline(pipeline); }) .catch((error) => { diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index e3164fe..cdfe918 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -10,8 +10,7 @@ export function usePostProduction() { body: JSON.stringify({ isActive: false, name, - sources: [], - selectedPresetRef: undefined + sources: [] }) }); if (response.ok) { @@ -36,7 +35,7 @@ export function useGetProduction() { } export function usePutProduction() { - return async (id: string, production: Production): Promise => { + return async (id: string, production: Production): Promise => { const response = await fetch(`/api/manager/productions/${id}`, { method: 'PUT', // TODO: Implement api key @@ -44,7 +43,7 @@ export function usePutProduction() { body: JSON.stringify(production) }); if (response.ok) { - return; + return response.json(); } throw await response.text(); }; diff --git a/src/hooks/sources/useAddSource.tsx b/src/hooks/sources/useAddSource.tsx new file mode 100644 index 0000000..cdd66d6 --- /dev/null +++ b/src/hooks/sources/useAddSource.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import { addSetupItem } from '../items/addSetupItem'; +import { CallbackHook } from '../types'; +import { Production } from '../../interfaces/production'; +import { usePutProduction } from '../productions'; +import { SourceReference } from '../../interfaces/Source'; + +export function useAddSource(): CallbackHook< + ( + input: SourceReference, + productionSetup: Production + ) => Promise +> { + const [loading, setLoading] = useState(true); + const putProduction = usePutProduction(); + + const addSource = async ( + input: SourceReference, + productionSetup: Production + ) => { + const updatedSetup = addSetupItem( + { + _id: input._id ? input._id : undefined, + type: input.type, + label: input.label, + input_slot: input.input_slot + }, + productionSetup + ); + + if (!updatedSetup) return; + + const res = await putProduction(updatedSetup._id.toString(), updatedSetup); + return res; + }; + + return [addSource, loading]; +} diff --git a/src/hooks/sources/useSources.tsx b/src/hooks/sources/useSources.tsx index 1b58418..8b44ea2 100644 --- a/src/hooks/sources/useSources.tsx +++ b/src/hooks/sources/useSources.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; export function useSources( - deleteComplete?: boolean, + reloadList?: boolean, updatedSource?: SourceWithId ): [Map, boolean] { const [sources, setSources] = useState>( @@ -11,7 +11,7 @@ export function useSources( const [loading, setLoading] = useState(true); useEffect(() => { - if (!updatedSource || deleteComplete) { + if (!updatedSource || reloadList) { fetch('/api/manager/sources?mocked=false', { method: 'GET', // TODO: Implement api key @@ -34,6 +34,6 @@ export function useSources( } sources.set(updatedSource._id.toString(), updatedSource); setSources(new Map(sources)); - }, [updatedSource, deleteComplete]); + }, [updatedSource, reloadList]); return [sources, loading]; } diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 427ffbf..a31a09e 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { SourceReference, SourceWithId } from '../interfaces/Source'; import { useSources } from './sources/useSources'; import { getSourceThumbnail } from '../utils/source'; - export interface ISource extends SourceWithId { label: string; input_slot: number; @@ -11,57 +10,79 @@ export interface ISource extends SourceWithId { } 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); + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); if (!source) return []; return { ...source, + _id: refId, 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 refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); + if (!source) return { ...ref }; + return { + ...ref, + _id: refId, + 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 ? item._id.toString() : '') === originId + ); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => (item._id ? 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); }; - return [items, moveItem, loading]; } diff --git a/src/hooks/useGetFirstEmptySlot.ts b/src/hooks/useGetFirstEmptySlot.ts new file mode 100644 index 0000000..8cda182 --- /dev/null +++ b/src/hooks/useGetFirstEmptySlot.ts @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import { Production } from '../interfaces/production'; +import { CallbackHook } from './types'; + +export function useGetFirstEmptySlot(): CallbackHook< + (productionSetup?: Production | undefined) => number +> { + const [loading, setLoading] = useState(true); + + const findFirstEmptySlot = (productionSetup: Production | undefined) => { + if (!productionSetup) throw 'no_production'; + + if (productionSetup) { + let firstEmptySlotTemp = productionSetup.sources.length + 1; + if (productionSetup.sources.length === 0) { + return firstEmptySlotTemp; + } + for ( + let i = 0; + i < + productionSetup.sources[productionSetup.sources.length - 1].input_slot; + i++ + ) { + if ( + !productionSetup.sources.some((source) => source.input_slot === i + 1) + ) { + firstEmptySlotTemp = i + 1; + break; + } + } + return firstEmptySlotTemp; + } else { + return 0; + } + }; + return [findFirstEmptySlot, loading]; +} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e5..596e25c 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 b33112d..bf9f42c 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 e59afa4..2e9935b 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; @@ -33,7 +34,8 @@ export interface Source { } export interface SourceReference { - _id: string; + _id?: string; + type: Type; label: string; stream_uuids?: string[]; input_slot: number; diff --git a/src/middleware.ts b/src/middleware.ts index 7724e3b..cac0847 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).*)/'] }; From f49f98ce5161f06cd1860a9b38f003bb7cc3160b Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 10 Sep 2024 15:50:10 +0200 Subject: [PATCH 02/10] feat: add management lock --- src/app/inventory/page.tsx | 21 +---- src/app/page.tsx | 18 ++-- src/app/production/[id]/page.tsx | 87 +++++++------------ src/components/button/Button.tsx | 7 +- src/components/button/MonitoringButton.tsx | 10 ++- .../createProduction/CreateProduction.tsx | 29 +++++-- .../headerNavigation/HeaderNavigation.tsx | 2 +- .../homePageContent/HomePageContent.tsx | 28 ++++++ src/components/inventory/Inventory.tsx | 8 +- .../inventory/InventoryPageContent.tsx | 32 +++++++ .../editView/AudioChannels/AudioChannels.tsx | 4 +- .../editView/AudioChannels/NumberInput.tsx | 2 +- .../editView/AudioChannels/Outputs.tsx | 10 ++- .../inventory/editView/EditView.tsx | 23 +++-- .../inventory/editView/GeneralSettings.tsx | 19 +++- .../inventory/editView/SelectOptions.tsx | 27 ++++-- .../inventory/editView/UpdateButtons.tsx | 44 +++++++--- src/components/lockButton/LockButton.tsx | 23 +++++ .../DeleteProductionButton.tsx | 12 +-- .../productionsList/ProductionsList.tsx | 12 ++- .../productionsList/ProductionsListItem.tsx | 24 +++-- src/components/sourceCard/SourceCard.tsx | 85 ++++++------------ src/components/sourceCards/SourceCards.tsx | 79 ++++++----------- .../sourceListItem/SourceListItem.tsx | 7 +- .../startProduction/ConfigureOutputButton.tsx | 7 +- .../startProduction/StartProductionButton.tsx | 13 ++- 26 files changed, 363 insertions(+), 270 deletions(-) create mode 100644 src/components/homePageContent/HomePageContent.tsx create mode 100644 src/components/inventory/InventoryPageContent.tsx create mode 100644 src/components/lockButton/LockButton.tsx diff --git a/src/app/inventory/page.tsx b/src/app/inventory/page.tsx index 0b85164..a762e9d 100644 --- a/src/app/inventory/page.tsx +++ b/src/app/inventory/page.tsx @@ -1,22 +1,5 @@ -import { Suspense } from 'react'; -import HeaderNavigation from '../../components/headerNavigation/HeaderNavigation'; -import { useTranslate } from '../../i18n/useTranslate'; -import { LoadingCover } from '../../components/loader/LoadingCover'; -import Inventory from '../../components/inventory/Inventory'; +import { InventoryPageContent } from '../../components/inventory/InventoryPageContent'; export default function Page() { - const t = useTranslate(); - - return ( - <> - -

    - {t('inventory')} -

    -
    - }> - - - - ); + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 30467a9..f6ad017 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,15 +1,15 @@ -import React, { Suspense } from 'react'; -import ProductionsList from '../components/productionsList/ProductionsList'; -import { CreateProduction } from '../components/createProduction/CreateProduction'; -import { LoadingCover } from '../components/loader/LoadingCover'; +import React from 'react'; import Link from 'next/link'; import { Button } from '../components/button/Button'; import { useTranslate } from '../i18n/useTranslate'; +import { getProductions } from '../api/manager/productions'; +import { HomePageContent } from '../components/homePageContent/HomePageContent'; export const dynamic = 'force-dynamic'; -function Home() { +async function Home() { const t = useTranslate(); + const productions = await getProductions(); return ( <>
    @@ -22,13 +22,7 @@ function Home() {
  • -
    - -
    - }> - {/* @ts-expect-error Async Server Component: https://github.com/vercel/next.js/issues/42292 */} - - +
    ); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 4fc3cd0..06d7c43 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -43,9 +43,7 @@ import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { useMultiviews } from '../../../hooks/multiviews'; -import { useAddSource } from '../../../hooks/sources/useAddSource'; -import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; -import { Select } from '../../../components/select/Select'; +import { LockButton } from '../../../components/lockButton/LockButton'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -95,12 +93,7 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); - // Create source - const [firstEmptySlot] = useGetFirstEmptySlot(); - const [addSource] = useAddSource(); - - const isAddButtonDisabled = - selectedValue !== 'HTML' && selectedValue !== 'Media Player'; + const [isLocked, setIsLocked] = useState(true); useEffect(() => { refreshPipelines(); @@ -411,6 +404,7 @@ export default function ProductionConfiguration({ params }: PageProps) { key={`${source.ingest_source_name}-${index}`} source={source} disabled={selectedProductionItems?.includes(source._id.toString())} + isLocked={isLocked} action={(source: SourceWithId) => { if (productionSetup && productionSetup.isActive) { setSelectedSource(source); @@ -629,14 +623,21 @@ export default function ProductionConfiguration({ params }: PageProps) { } }} onBlur={() => updateConfigName(configurationName)} + disabled={isLocked} />
    + setIsLocked(!isLocked)} + />
    @@ -745,6 +745,7 @@ export default function ProductionConfiguration({ params }: PageProps) { }); } }} + isLocked={isLocked} /> {removeSourceModal && selectedSourceRef && ( )} -
    - setInventoryVisible(true)} - disabled={ - productionSetup?.production_settings === undefined || - productionSetup.production_settings === null - } - /> -
    - {options.map((option: string, index: number) => ( diff --git a/src/components/inventory/editView/UpdateButtons.tsx b/src/components/inventory/editView/UpdateButtons.tsx index 1f3309d..622f760 100644 --- a/src/components/inventory/editView/UpdateButtons.tsx +++ b/src/components/inventory/editView/UpdateButtons.tsx @@ -7,15 +7,19 @@ import { Loader } from '../../loader/Loader'; import { SourceWithId } from '../../../interfaces/Source'; import { IconTrash } from '@tabler/icons-react'; +type UpdateButtonsProps = { + source: SourceWithId; + isLocked: boolean; + removeInventorySource: (source: SourceWithId) => void; + close: () => void; +}; + export default function UpdateButtons({ + source, + isLocked, close, - removeInventorySource, - source -}: { - close: () => void; - removeInventorySource: (source: SourceWithId) => void; - source: SourceWithId; -}) { + removeInventorySource +}: UpdateButtonsProps) { const t = useTranslate(); const { saved: [saved], @@ -25,7 +29,7 @@ export default function UpdateButtons({ return (
    -
    +
    {t('saved')}
    @@ -35,16 +39,32 @@ export default function UpdateButtons({ - + ); +}; diff --git a/src/components/productionsList/DeleteProductionButton.tsx b/src/components/productionsList/DeleteProductionButton.tsx index a0dfe89..f4cada8 100644 --- a/src/components/productionsList/DeleteProductionButton.tsx +++ b/src/components/productionsList/DeleteProductionButton.tsx @@ -11,12 +11,14 @@ type DeleteProductionButtonProps = { id: string; name: string; isActive: boolean; + isLocked: boolean; }; export function DeleteProductionButton({ id, name, - isActive + isActive, + isLocked }: DeleteProductionButtonProps) { const router = useRouter(); const deleteProduction = useDeleteProduction(); @@ -38,12 +40,12 @@ export function DeleteProductionButton({ <> - )} + +

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

    +
    ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index c20b8c8..0d4b213 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -10,12 +10,14 @@ export default function SourceCards({ productionSetup, updateProduction, onSourceUpdate, - onSourceRemoval + onSourceRemoval, + isLocked }: { productionSetup: Production; updateProduction: (updated: Production) => void; onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; + isLocked: boolean; }) { const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); @@ -54,59 +56,34 @@ export default function SourceCards({ updateProduction={updateProduction} selectingText={selectingText} > - {isSource ? ( - - setSelectingText(isSelecting) - } - type={'ingest_source'} - /> - ) : ( - - setSelectingText(isSelecting) - } - type={source.type} - /> - )} + + setSelectingText(isSelecting) + } + isLocked={isLocked} + /> ); } else { - isSource - ? gridItems.push( - - setSelectingText(isSelecting) - } - type={'ingest_source'} - /> - ) - : gridItems.push( - - setSelectingText(isSelecting) - } - type={source.type} - /> - ); + gridItems.push( + + setSelectingText(isSelecting) + } + isLocked={isLocked} + /> + ); } return false; } else { diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index c8b724e..5a1521b 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -14,9 +14,10 @@ import capitalize from '../../utils/capitalize'; type SourceListItemProps = { source: SourceWithId; - action: (source: SourceWithId) => void; edit?: boolean; disabled: unknown; + isLocked: boolean; + action: (source: SourceWithId) => void; }; const getIcon = (source: Source) => { @@ -51,7 +52,8 @@ function InventoryListItem({ source, action, disabled, - edit = false + edit = false, + isLocked }: SourceListItemProps) { const t = useTranslate(); const [previewVisible, setPreviewVisible] = useState(false); @@ -169,6 +171,7 @@ function InventoryListItem({ outputRows={outputRows} rowIndex={rowIndex} max={channelsInArray[channelsInArray.length - 1]} + isLocked={isLocked} />
    ))} diff --git a/src/components/startProduction/ConfigureOutputButton.tsx b/src/components/startProduction/ConfigureOutputButton.tsx index 1ded983..cee39ab 100644 --- a/src/components/startProduction/ConfigureOutputButton.tsx +++ b/src/components/startProduction/ConfigureOutputButton.tsx @@ -6,6 +6,7 @@ import { Preset } from '../../interfaces/preset'; import { useTranslate } from '../../i18n/useTranslate'; import { Button } from '../button/Button'; import { ConfigureOutputModal } from '../modal/configureOutputModal/ConfigureOutputModal'; + type ConfigureOutputButtonProps = { preset?: Preset; disabled?: boolean; @@ -30,9 +31,11 @@ export function ConfigureOutputButton({ onClick={toggleConfigModal} disabled={!preset || disabled} hoverMessage={!preset ? t('preset.preset_necessary') : ''} - className={`min-w-fit`} + className={`min-w-fit ${ + disabled ? 'bg-button-bg/50 pointer-events-none' : 'bg-button-bg' + }`} > - + {preset && ( From 20b52e8304dc5449d30fa31077eac3b6aaa918be Mon Sep 17 00:00:00 2001 From: Lucas Maupin Date: Mon, 16 Sep 2024 09:50:31 +0200 Subject: [PATCH 03/10] feat: display thumbnail on card and fix cache issue --- src/api/ateliereLive/ingest.ts | 4 +- .../ateliereLive/pipelines/streams/streams.ts | 34 ++-- src/api/manager/workflow.ts | 7 +- .../[source_name]/thumbnail/route.ts | 1 - src/app/api/manager/streams/route.ts | 1 - src/app/api/manager/websocket/route.ts | 38 ++++ src/app/layout.tsx | 5 +- src/app/production/[id]/page.tsx | 175 ++++++++---------- src/components/button/MonitoringButton.tsx | 10 +- .../createProduction/CreateProduction.tsx | 22 +-- src/components/filter/FilterOptions.tsx | 11 +- .../headerNavigation/HeaderNavigation.tsx | 18 +- .../homePageContent/HomePageContent.tsx | 12 +- src/components/image/ImageComponent.tsx | 111 +++++++++++ src/components/inventory/Inventory.tsx | 99 +++------- .../inventory/InventoryPageContent.tsx | 12 +- .../editView/AudioChannels/AudioChannels.tsx | 6 +- .../editView/AudioChannels/Outputs.tsx | 6 +- .../inventory/editView/EditView.tsx | 59 ++---- .../inventory/editView/GeneralSettings.tsx | 14 +- .../inventory/editView/UpdateButtons.tsx | 18 +- src/components/lockButton/LockButton.tsx | 16 +- src/components/modal/AddSourceModal.tsx | 6 +- src/components/modal/Modal.tsx | 2 +- .../DeleteProductionButton.tsx | 9 +- .../productionsList/ProductionsList.tsx | 6 +- .../productionsList/ProductionsListItem.tsx | 20 +- src/components/sourceCard/SourceCard.tsx | 90 +++++---- src/components/sourceCard/SourceThumbnail.tsx | 59 ------ src/components/sourceCards/SourceCards.tsx | 28 ++- .../SourceList.module.scss} | 0 src/components/sourceList/SourceList.tsx | 90 +++++++++ .../sourceListItem/PreviewThumbnail.tsx | 31 ---- .../sourceListItem/SourceListItem.tsx | 78 +++----- .../SourceListItemThumbnail.tsx | 50 +++++ .../startProduction/StartProductionButton.tsx | 5 - .../inventory => contexts}/FilterContext.tsx | 2 +- src/contexts/GlobalContext.tsx | 50 +++++ src/hooks/pipelines.ts | 1 - src/hooks/sources/useAddSource.tsx | 2 +- src/hooks/streams.ts | 10 +- src/hooks/useWebsocket.ts | 20 ++ src/i18n/locales/en.ts | 1 + src/i18n/locales/sv.ts | 1 + 44 files changed, 700 insertions(+), 540 deletions(-) create mode 100644 src/app/api/manager/websocket/route.ts create mode 100644 src/components/image/ImageComponent.tsx delete mode 100644 src/components/sourceCard/SourceThumbnail.tsx rename src/components/{inventory/Inventory.module.scss => sourceList/SourceList.module.scss} (100%) create mode 100644 src/components/sourceList/SourceList.tsx delete mode 100644 src/components/sourceListItem/PreviewThumbnail.tsx create mode 100644 src/components/sourceListItem/SourceListItemThumbnail.tsx rename src/{components/inventory => contexts}/FilterContext.tsx (95%) create mode 100644 src/contexts/GlobalContext.tsx create mode 100644 src/hooks/useWebsocket.ts diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index 0c7b521..396ef5a 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -106,6 +106,7 @@ export async function getSourceThumbnail( process.env.LIVE_URL ), { + next: { tags: ['image'] }, method: 'POST', body: JSON.stringify({ encoder: 'auto', @@ -114,7 +115,8 @@ export async function getSourceThumbnail( width }), headers: { - authorization: getAuthorizationHeader() + authorization: getAuthorizationHeader(), + cache: 'no-store' } } ); diff --git a/src/api/ateliereLive/pipelines/streams/streams.ts b/src/api/ateliereLive/pipelines/streams/streams.ts index fc5e108..f831d37 100644 --- a/src/api/ateliereLive/pipelines/streams/streams.ts +++ b/src/api/ateliereLive/pipelines/streams/streams.ts @@ -66,6 +66,7 @@ export async function createStream( return pipeline.uuid; }) ); + const ingestUuid = await getUuidFromIngestName( source.ingest_name, false @@ -79,6 +80,7 @@ export async function createStream( source.ingest_source_name, false ); + const audioMapping = source.audio_stream.audio_mapping && source.audio_stream.audio_mapping.length > 0 @@ -86,6 +88,7 @@ export async function createStream( : [[0, 1]]; await initDedicatedPorts(); + for (const pipeline of production_settings.pipelines) { const availablePorts = getAvailablePortsForIngest( source.ingest_name, @@ -101,28 +104,29 @@ export async function createStream( Log().info( `Allocated port ${availablePort} on '${source.ingest_name}' for ${source.ingest_source_name}` ); + const stream: PipelineStreamSettings = { + ingest_id: ingestUuid, + source_id: sourceId, pipeline_id: pipeline.pipeline_id!, + input_slot: input_slot, alignment_ms: pipeline.alignment_ms, - audio_format: pipeline.audio_format, - audio_sampling_frequency: pipeline.audio_sampling_frequency, - bit_depth: pipeline.bit_depth, - convert_color_range: pipeline.convert_color_range, - encoder: pipeline.encoder, - encoder_device: pipeline.encoder_device, - format: pipeline.format, + max_network_latency_ms: pipeline.max_network_latency_ms, + width: pipeline.width, + height: pipeline.height, frame_rate_d: pipeline.frame_rate_d, frame_rate_n: pipeline.frame_rate_n, + format: pipeline.format, + encoder: pipeline.encoder, + encoder_device: pipeline.encoder_device, gop_length: pipeline.gop_length, - height: pipeline.height, - max_network_latency_ms: pipeline.max_network_latency_ms, pic_mode: pipeline.pic_mode, - speed_quality_balance: pipeline.speed_quality_balance, video_kilobit_rate: pipeline.video_kilobit_rate, - width: pipeline.width, - ingest_id: ingestUuid, - source_id: sourceId, - input_slot, + bit_depth: pipeline.bit_depth, + speed_quality_balance: pipeline.speed_quality_balance, + convert_color_range: pipeline.convert_color_range, + audio_sampling_frequency: pipeline.audio_sampling_frequency, + audio_format: pipeline.audio_format, audio_mapping: JSON.stringify(audioMapping), interfaces: [ { @@ -131,6 +135,7 @@ export async function createStream( } ] }; + try { Log().info( `Connecting '${source.ingest_name}/${ingestUuid}}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` @@ -147,6 +152,7 @@ export async function createStream( Log().info( `Stream '${result.stream_uuid}' from '${source.ingest_name}/${ingestUuid}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}' connected` ); + sourceToPipelineStreams.push({ source_id: source._id.toString(), stream_uuid: result.stream_uuid, diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 7b9e392..c05a41f 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -137,7 +137,7 @@ async function connectIngestSources( width: pipeline.width, ingest_id: ingestUuid, source_id: sourceId, - input_slot, + input_slot: input_slot, audio_mapping: JSON.stringify(audioMapping), interfaces: [ { @@ -492,7 +492,10 @@ export async function startProduction( source.type !== 'mediaplayer' ) .map((source) => { - return source._id!.toString(); + if (source._id !== undefined) { + return source._id.toString(); + } + return ''; }) ).catch((error) => { if (error === "Can't connect to Database") { diff --git a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts index 926d3e3..8d69748 100644 --- a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts +++ b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts @@ -20,7 +20,6 @@ export async function GET( status: 403 }); } - try { const ingestUuid = await getUuidFromIngestName(params.ingest_name); const sourceId = await getSourceIdFromSourceName( diff --git a/src/app/api/manager/streams/route.ts b/src/app/api/manager/streams/route.ts index 4463588..06c7ff6 100644 --- a/src/app/api/manager/streams/route.ts +++ b/src/app/api/manager/streams/route.ts @@ -15,7 +15,6 @@ export async function POST(request: NextRequest): Promise { status: 403 }); } - const data = await request.json(); const createStreamRequest = data as CreateStreamRequestBody; return await createStream( diff --git a/src/app/api/manager/websocket/route.ts b/src/app/api/manager/websocket/route.ts new file mode 100644 index 0000000..1ff9789 --- /dev/null +++ b/src/app/api/manager/websocket/route.ts @@ -0,0 +1,38 @@ +import { NextResponse, NextRequest } from 'next/server'; + +const wsUrl = `ws://${process.env.CONTROL_PANEL_WS}`; + +export async function POST(request: NextRequest): Promise { + const { action, inputSlot } = await request.json(); + + if (!wsUrl) { + return NextResponse.json( + { message: 'WebSocket URL is not defined' }, + { status: 500 } + ); + } + + return new Promise((resolve) => { + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + if (action === 'closeHtml') { + ws.send(`html close ${inputSlot}`); + ws.send('html reset'); + } else if (action === 'closeMediaplayer') { + ws.send(`media close ${inputSlot}`); + ws.send('media reset'); + } + ws.close(); + }; + + ws.onerror = (error) => { + resolve( + NextResponse.json( + { message: 'WebSocket error', error }, + { status: 500 } + ) + ); + }; + }); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eab1456..622ac7a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import { Toaster } from 'react-hot-toast'; import DefaultLayout from '../components/layout/DefaultLayout'; import './globals.css'; +import GlobalContextProvider from '../contexts/GlobalContext'; export default async function RootLayout({ children @@ -21,7 +22,9 @@ export default async function RootLayout({ } }} /> - {children} + + {children} + ); diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 06d7c43..2a66a85 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,11 +1,8 @@ 'use client'; -import React, { useEffect, useState, KeyboardEvent } from 'react'; +import React, { useEffect, useState, KeyboardEvent, useContext } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; -import SourceListItem from '../../../components/sourceListItem/SourceListItem'; -import FilterOptions from '../../../components/filter/FilterOptions'; import { AddInput } from '../../../components/addInput/AddInput'; -import { IconX } from '@tabler/icons-react'; import { useSources } from '../../../hooks/sources/useSources'; import { AddSourceStatus, @@ -20,8 +17,6 @@ import { updateSetupItem } from '../../../hooks/items/updateSetupItem'; import { removeSetupItem } from '../../../hooks/items/removeSetupItem'; import { addSetupItem } from '../../../hooks/items/addSetupItem'; import HeaderNavigation from '../../../components/headerNavigation/HeaderNavigation'; -import styles from './page.module.scss'; -import FilterProvider from '../../../components/inventory/FilterContext'; import { useGetPresets } from '../../../hooks/presets'; import { Preset } from '../../../interfaces/preset'; import SourceCards from '../../../components/sourceCards/SourceCards'; @@ -43,7 +38,13 @@ import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { useMultiviews } from '../../../hooks/multiviews'; +import SourceList from '../../../components/sourceList/SourceList'; import { LockButton } from '../../../components/lockButton/LockButton'; +import { GlobalContext } from '../../../contexts/GlobalContext'; +import { Select } from '../../../components/select/Select'; +import { useAddSource } from '../../../hooks/sources/useAddSource'; +import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; +import { useWebsocket } from '../../../hooks/useWebsocket'; export default function ProductionConfiguration({ params }: PageProps) { const t = useTranslate(); @@ -93,7 +94,14 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); - const [isLocked, setIsLocked] = useState(true); + // Create source + const [firstEmptySlot] = useGetFirstEmptySlot(); + const [addSource] = useAddSource(); + + // Websocket + const [closeWebsocket] = useWebsocket(); + + const { locked } = useContext(GlobalContext); useEffect(() => { refreshPipelines(); @@ -394,40 +402,29 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } - // Adding source to a production, both in setup-mode and in live-mode - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode[] { - return Array.from(filteredSources.values()).map((source, index) => { - return ( - { - if (productionSetup && productionSetup.isActive) { - setSelectedSource(source); - setAddSourceModal(true); - } else if (productionSetup) { - const input: SourceReference = { - _id: source._id.toString(), - type: 'ingest_source', - label: source.ingest_source_name, - input_slot: firstEmptySlot(productionSetup) - }; - addSource(input, productionSetup).then((updatedSetup) => { - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - setAddSourceModal(false); - setSelectedSource(undefined); - }); - } - }} - /> - ); - }); - } + const addSourceAction = (source: SourceWithId) => { + if (productionSetup && productionSetup.isActive) { + setSelectedSource(source); + setAddSourceModal(true); + } else if (productionSetup) { + const input: SourceReference = { + _id: source._id.toString(), + type: 'ingest_source', + label: source.ingest_source_name, + input_slot: firstEmptySlot(productionSetup) + }; + addSource(input, productionSetup).then((updatedSetup) => { + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + setAddSourceModal(false); + setSelectedSource(undefined); + }); + } + }; + + const isDisabledFunction = (source: SourceWithId): boolean => { + return selectedProductionItems?.includes(source._id.toString()); + }; const handleAddSource = async () => { setAddSourceStatus(undefined); @@ -585,7 +582,25 @@ export default function ProductionConfiguration({ params }: PageProps) { return; } } + + if ( + selectedSourceRef.type === 'html' || + selectedSourceRef.type === 'mediaplayer' + ) { + // Action specifies what websocket method to call + const action = + selectedSourceRef.type === 'html' ? 'closeHtml' : 'closeMediaplayer'; + const inputSlot = productionSetup.sources.find( + (source) => source._id === selectedSourceRef._id + )?.input_slot; + + if (!inputSlot) return; + + closeWebsocket(action, inputSlot); + } + const updatedSetup = removeSetupItem(selectedSourceRef, productionSetup); + if (!updatedSetup) return; setProductionSetup(updatedSetup); putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { @@ -623,20 +638,17 @@ export default function ProductionConfiguration({ params }: PageProps) { } }} onBlur={() => updateConfigName(configurationName)} - disabled={isLocked} + disabled={locked} />
    - setIsLocked(!isLocked)} - /> +
    @@ -668,42 +680,23 @@ export default function ProductionConfiguration({ params }: PageProps) { inventoryVisible ? 'min-w-[35%] ml-2 mt-2 max-h-[89vh]' : '' }`} > -
    -
    - -
    -
    - - ) => { - setFilteredSources(new Map(filtered)); - }} - /> - -
    -
      - {getSourcesToDisplay(filteredSources)} - {addSourceModal && selectedSource && ( - - )} -
    -
    + setInventoryVisible(false)} + isDisabledFunc={isDisabledFunction} + /> + {addSourceModal && selectedSource && ( + + )}
    {removeSourceModal && selectedSourceRef && ( { setInventoryVisible(true); @@ -776,7 +768,7 @@ export default function ProductionConfiguration({ params }: PageProps) { (pipeline, i) => { return ( ({ @@ -792,7 +784,7 @@ export default function ProductionConfiguration({ params }: PageProps) { )} {productionSetup?.production_settings && ( ({ option: controlPanel.name, available: controlPanel.outgoing_connections?.length === 0 @@ -811,10 +803,7 @@ export default function ProductionConfiguration({ params }: PageProps) {
    {productionSetup && productionSetup.isActive && (
    - +
    )}
    diff --git a/src/components/button/MonitoringButton.tsx b/src/components/button/MonitoringButton.tsx index 375844c..86483da 100644 --- a/src/components/button/MonitoringButton.tsx +++ b/src/components/button/MonitoringButton.tsx @@ -4,23 +4,17 @@ import { useTranslate } from '../../i18n/useTranslate'; import { useMonitoringError } from '../../hooks/monitoring'; import { IconLoader } from '@tabler/icons-react'; import { IconAlertTriangleFilled } from '@tabler/icons-react'; - type MonitoringButtonProps = { id: string; - isLocked: boolean; }; -export const MonitoringButton = ({ id, isLocked }: MonitoringButtonProps) => { +export const MonitoringButton = ({ id }: MonitoringButtonProps) => { const t = useTranslate(); const [hasError, loading] = useMonitoringError(id); return ( void; - isLocked: boolean; -}; - -export function CreateProduction({ onClick, isLocked }: CreateProductionProps) { +export function CreateProduction() { const router = useRouter(); const postProduction = usePostProduction(); @@ -54,19 +49,14 @@ export function CreateProduction({ onClick, isLocked }: CreateProductionProps) { {t('production_configuration')}
    - + @@ -105,11 +95,7 @@ export function CreateProduction({ onClick, isLocked }: CreateProductionProps) {
    {children}
    diff --git a/src/components/homePageContent/HomePageContent.tsx b/src/components/homePageContent/HomePageContent.tsx index 170acd0..8c35850 100644 --- a/src/components/homePageContent/HomePageContent.tsx +++ b/src/components/homePageContent/HomePageContent.tsx @@ -1,26 +1,24 @@ 'use client'; import { CreateProduction } from '../createProduction/CreateProduction'; -import { Suspense, useState } from 'react'; +import { Suspense, useContext } from 'react'; import { LoadingCover } from '../loader/LoadingCover'; import ProductionsList from '../productionsList/ProductionsList'; import { Production } from '../../interfaces/production'; +import { GlobalContext } from '../../contexts/GlobalContext'; type HomePageContentProps = { productions: Production[]; }; export const HomePageContent = ({ productions }: HomePageContentProps) => { - const [isLocked, setIsLocked] = useState(true); + const { locked } = useContext(GlobalContext); return (
    - setIsLocked(!isLocked)} - /> +
    }> - +
    diff --git a/src/components/image/ImageComponent.tsx b/src/components/image/ImageComponent.tsx new file mode 100644 index 0000000..df6acf8 --- /dev/null +++ b/src/components/image/ImageComponent.tsx @@ -0,0 +1,111 @@ +import { + PropsWithChildren, + SyntheticEvent, + useContext, + useEffect, + useRef, + useState +} from 'react'; +import Image from 'next/image'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { Loader } from '../loader/Loader'; +import { GlobalContext } from '../../contexts/GlobalContext'; +import { Type } from '../../interfaces/Source'; + +interface ImageComponentProps extends PropsWithChildren { + src?: string; + alt?: string; + type?: Type; +} + +const ImageComponent: React.FC = (props) => { + const { src, alt = 'Image', children, type } = props; + const { imageRefetchIndex } = useContext(GlobalContext); + const [imgSrc, setImgSrc] = useState(); + const [loaded, setLoaded] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState>(); + const timeout = useRef>(); + + const refetchImage = () => { + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + setError(undefined); + setLoading(true); + clearTimeout(timeout.current); + timeout.current = setTimeout(() => setLoading(false), 500); + }; + + useEffect(() => { + refetchImage(); + }, [imageRefetchIndex]); + + useEffect(() => { + setError(undefined); + setImgSrc(`${src}?refetch=${imageRefetchIndex}}`); + }, [src]); + + useEffect(() => { + return () => { + clearTimeout(timeout.current); + }; + }, []); + + return ( + <> + {(!type || type === 'ingest_source') && src && ( +
    + {((!imgSrc || error) && ( + + )) || ( + <> + {alt} { + setError(undefined); + setLoaded(false); + }} + onLoadingComplete={() => { + setLoaded(true); + }} + onError={(err) => { + setError(err); + }} + placeholder="empty" + width={0} + height={0} + sizes="20vh" + style={{ + width: 'auto', + height: '100%' + }} + /> + + + )} + {children} +
    + )} + {(type === 'html' || type === 'mediaplayer') && ( + +

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

    +
    + )} + + ); +}; + +export default ImageComponent; diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index b2d5d84..e29fcdb 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -3,28 +3,19 @@ import { useEffect, useState } from 'react'; import { useSources } from '../../hooks/sources/useSources'; import { useSetSourceToPurge } from '../../hooks/sources/useSetSourceToPurge'; -import FilterOptions from '../../components/filter/FilterOptions'; -import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; -import FilterContext from './FilterContext'; -import styles from './Inventory.module.scss'; +import SourceList from '../sourceList/SourceList'; +import { useTranslate } from '../../i18n/useTranslate'; -type InventoryProps = { - isLocked: boolean; -}; - -export default function Inventory({ isLocked }: InventoryProps) { +export default function Inventory({ locked }: { locked: boolean }) { const [removeInventorySource, reloadList] = useSetSourceToPurge(); const [updatedSource, setUpdatedSource] = useState< SourceWithId | undefined >(); const [sources] = useSources(reloadList, updatedSource); const [currentSource, setCurrentSource] = useState(); - const [filteredSources, setFilteredSources] = - useState>(sources); - - const inventoryVisible = true; + const t = useTranslate(); useEffect(() => { if (updatedSource && typeof updatedSource !== 'boolean') { @@ -39,73 +30,25 @@ export default function Inventory({ isLocked }: InventoryProps) { }, [reloadList]); const editSource = (source: SourceWithId) => { - setCurrentSource(() => source); + setCurrentSource(source); }; - - function getSourcesToDisplay( - filteredSources: Map - ): React.ReactNode { - return Array.from(filteredSources.values()).map((source, index) => { - if (source.status !== 'purge') { - return ( - { - editSource(source); - }} - isLocked={isLocked} - /> - ); - } - }); - } - return ( - -
    -
    -
    -
    - ) => - setFilteredSources(new Map(filtered)) - } - /> -
    -
      - {getSourcesToDisplay(filteredSources)} -
    -
    +
    + + {currentSource ? ( +
    + setUpdatedSource(source)} + close={() => setCurrentSource(null)} + removeInventorySource={removeInventorySource} + />
    - - {currentSource ? ( -
    - setUpdatedSource(source)} - close={() => setCurrentSource(null)} - removeInventorySource={(source) => removeInventorySource(source)} - /> -
    - ) : null} -
    - + ) : null} +
    ); } diff --git a/src/components/inventory/InventoryPageContent.tsx b/src/components/inventory/InventoryPageContent.tsx index 06e5ee6..a773ed6 100644 --- a/src/components/inventory/InventoryPageContent.tsx +++ b/src/components/inventory/InventoryPageContent.tsx @@ -1,13 +1,14 @@ 'use client'; -import { useState, Suspense } from 'react'; +import { Suspense, useContext } from 'react'; import { LockButton } from '../lockButton/LockButton'; import { useTranslate } from '../../i18n/useTranslate'; import HeaderNavigation from '../headerNavigation/HeaderNavigation'; import Inventory from './Inventory'; +import { GlobalContext } from '../../contexts/GlobalContext'; export const InventoryPageContent = () => { - const [isLocked, setIsLocked] = useState(true); const t = useTranslate(); + const { locked } = useContext(GlobalContext); return ( <> @@ -17,15 +18,12 @@ export const InventoryPageContent = () => {

    {t('inventory')}

    - setIsLocked(!isLocked)} - /> +
    - + ); diff --git a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx index 4315e64..7024fed 100644 --- a/src/components/inventory/editView/AudioChannels/AudioChannels.tsx +++ b/src/components/inventory/editView/AudioChannels/AudioChannels.tsx @@ -11,10 +11,10 @@ import { channel, mapAudio } from '../../../../utils/audioMapping'; interface IAudioChannels { source: Source; - isLocked: boolean; + locked: boolean; } -export default function AudioChannels({ source, isLocked }: IAudioChannels) { +export default function AudioChannels({ source, locked }: IAudioChannels) { type TOutputs = 'audio_mapping.outL' | 'audio_mapping.outR'; const t = useTranslate(); const outputs: TOutputs[] = ['audio_mapping.outL', 'audio_mapping.outR']; @@ -227,8 +227,8 @@ export default function AudioChannels({ source, isLocked }: IAudioChannels) { outputRows={outputRows} rowIndex={rowIndex} max={max} - isLocked={isLocked} updateRows={updateRows} + locked={locked} />
    ))} diff --git a/src/components/inventory/editView/AudioChannels/Outputs.tsx b/src/components/inventory/editView/AudioChannels/Outputs.tsx index f9dcf4b..7bf3b0e 100644 --- a/src/components/inventory/editView/AudioChannels/Outputs.tsx +++ b/src/components/inventory/editView/AudioChannels/Outputs.tsx @@ -18,7 +18,7 @@ interface IOutput { rowIndex: number; max: number; small?: boolean; - isLocked: boolean; + locked: boolean; updateRows?: (e: IEvent, rowIndex: number, index: number, id: string) => void; } @@ -28,7 +28,7 @@ export default function Outputs({ rowIndex, max, small = false, - isLocked, + locked, updateRows }: IOutput) { return ( @@ -52,7 +52,7 @@ export default function Outputs({ } relative ${styles.checkbox}`} > diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 6587c1a..ce312af 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,66 +1,41 @@ -import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; -import { IconExclamationCircle } from '@tabler/icons-react'; - -type EditViewProps = { - source: SourceWithId; - isLocked: boolean; - updateSource: (source: SourceWithId) => void; - close: () => void; - removeInventorySource: (source: SourceWithId) => void; -}; +import ImageComponent from '../../image/ImageComponent'; export default function EditView({ source, - isLocked, updateSource, close, - removeInventorySource -}: EditViewProps) { - const [loaded, setLoaded] = useState(false); - const src = useMemo(() => getSourceThumbnail(source), [source]); - + removeInventorySource, + locked +}: { + source: SourceWithId; + updateSource: (source: SourceWithId) => void; + close: () => void; + removeInventorySource: (source: SourceWithId) => void; + locked: boolean; +}) { return ( -
    - {source.status === 'gone' ? ( -
    - -
    - ) : ( - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={300} - height={0} - style={{ - objectFit: 'contain' - }} - /> - )} - - +
    +
    + +
    +
    - +
    ); diff --git a/src/components/inventory/editView/GeneralSettings.tsx b/src/components/inventory/editView/GeneralSettings.tsx index 42c137f..852efa4 100644 --- a/src/components/inventory/editView/GeneralSettings.tsx +++ b/src/components/inventory/editView/GeneralSettings.tsx @@ -1,16 +1,16 @@ import { useContext } from 'react'; import { EditViewContext, IInput } from '../EditViewContext'; -import { FilterContext } from '../FilterContext'; +import { FilterContext } from '../../../contexts/FilterContext'; import { useTranslate } from '../../../i18n/useTranslate'; import SelectOptions from './SelectOptions'; import { getHertz } from '../../../utils/stream'; import videoSettings from '../../../utils/videoSettings'; type GeneralSettingsProps = { - isLocked: boolean; + locked: boolean; }; -export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { +export default function GeneralSettings({ locked }: GeneralSettingsProps) { const { input: [input, setInput], saved: [saved, setSaved], @@ -46,11 +46,11 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { value={input.name} onChange={(e) => onChange('name', e.target.value)} className={`${ - isLocked + locked ? 'pointer-events-none bg-gray-700/50 border-gray-600/50 placeholder-gray-400/50 text-p/50' : 'pointer-events-auto bg-gray-700 border-gray-600 placeholder-gray-400 text-p' } 'cursor-pointer ml-5 border justify-center text-sm rounded-lg w-full pl-2 pt-1 pb-1 focus:ring-blue-500 focus:border-blue-500'`} - disabled={isLocked} + disabled={locked} />
    @@ -59,7 +59,7 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { name="type" options={types} selected={input.type} - disabled={isLocked} + disabled={locked} onChange={(e) => onChange('type', e.target.value.toLowerCase())} /> @@ -68,7 +68,7 @@ export default function GeneralSettings({ isLocked }: GeneralSettingsProps) { name="location" options={locations} selected={input.location} - disabled={isLocked} + disabled={locked} onChange={(e) => onChange('location', e.target.value.toLowerCase())} /> diff --git a/src/components/inventory/editView/UpdateButtons.tsx b/src/components/inventory/editView/UpdateButtons.tsx index 622f760..a7be60d 100644 --- a/src/components/inventory/editView/UpdateButtons.tsx +++ b/src/components/inventory/editView/UpdateButtons.tsx @@ -9,16 +9,16 @@ import { IconTrash } from '@tabler/icons-react'; type UpdateButtonsProps = { source: SourceWithId; - isLocked: boolean; removeInventorySource: (source: SourceWithId) => void; close: () => void; + locked: boolean; }; export default function UpdateButtons({ source, - isLocked, close, - removeInventorySource + removeInventorySource, + locked }: UpdateButtonsProps) { const t = useTranslate(); const { @@ -39,18 +39,16 @@ export default function UpdateButtons({ - + {source && !sourceRef && ( + + )} + {!source && sourceRef && } + {(source || sourceRef) && ( +

    + {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 deleted file mode 100644 index b5e5bcb..0000000 --- a/src/components/sourceCard/SourceThumbnail.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { useState } from 'react'; -import { Source, Type } from '../../interfaces/Source'; -import { IconExclamationCircle } from '@tabler/icons-react'; - -type SourceThumbnailProps = { - source?: Source; - src?: string; - type?: Type; -}; - -export function SourceThumbnail({ source, src, type }: SourceThumbnailProps) { - const [loaded, setLoaded] = useState(false); - - if (source && source.status === 'gone') { - return ( -
    - -
    - ); - } - - return ( - <> - {(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 0d4b213..3ca7a2b 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -6,18 +6,17 @@ import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; + export default function SourceCards({ productionSetup, updateProduction, onSourceUpdate, - onSourceRemoval, - isLocked + onSourceRemoval }: { productionSetup: Production; updateProduction: (updated: Production) => void; onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; - isLocked: boolean; }) { const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); @@ -57,31 +56,28 @@ export default function SourceCards({ selectingText={selectingText} > - setSelectingText(isSelecting) - } - isLocked={isLocked} + onSelectingText={(isSelecting) => setSelectingText(isSelecting)} /> ); } else { gridItems.push( - setSelectingText(isSelecting) - } - isLocked={isLocked} + onSelectingText={(isSelecting) => setSelectingText(isSelecting)} /> ); } diff --git a/src/components/inventory/Inventory.module.scss b/src/components/sourceList/SourceList.module.scss similarity index 100% rename from src/components/inventory/Inventory.module.scss rename to src/components/sourceList/SourceList.module.scss diff --git a/src/components/sourceList/SourceList.tsx b/src/components/sourceList/SourceList.tsx new file mode 100644 index 0000000..b4b0c99 --- /dev/null +++ b/src/components/sourceList/SourceList.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useState } from 'react'; +import { SourceWithId } from '../../interfaces/Source'; +import FilterContext from '../../contexts/FilterContext'; +import SourceListItem from '../sourceListItem/SourceListItem'; +import FilterOptions from '../filter/FilterOptions'; +import styles from './SourceList.module.scss'; +import { IconX } from '@tabler/icons-react'; + +interface SourceListProps { + sources: Map; + inventoryVisible?: boolean; + onClose?: () => void; + isDisabledFunc?: (source: SourceWithId) => boolean; + action?: (source: SourceWithId) => void; + actionText?: string; +} + +const SourceList: React.FC = (props) => { + const { + sources, + inventoryVisible = true, + onClose, + isDisabledFunc, + action, + actionText, + locked + } = props; + + const [filteredSources, setFilteredSources] = + useState>(sources); + + function getSourcesToDisplay( + filteredSources: Map + ): React.ReactNode { + return Array.from( + filteredSources.size > 0 ? filteredSources.values() : sources.values() + ).map((source, index) => { + return ( + + ); + }); + } + + return ( + +
    +
    +
    +
    + ) => + setFilteredSources(new Map(filtered)) + } + /> + {onClose && ( + + )} +
    +
      + {getSourcesToDisplay(filteredSources)} +
    +
    +
    +
    +
    + ); +}; + +export default SourceList; diff --git a/src/components/sourceListItem/PreviewThumbnail.tsx b/src/components/sourceListItem/PreviewThumbnail.tsx deleted file mode 100644 index dcda882..0000000 --- a/src/components/sourceListItem/PreviewThumbnail.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Image from 'next/image'; -import { useState } from 'react'; - -type PreviewProps = { src: string }; - -export const PreviewThumbnail = ({ src }: PreviewProps) => { - const [loaded, setLoaded] = useState(false); - - return ( -
    - Preview Thumbnail setLoaded(true)} - placeholder="empty" - width={0} - height={0} - sizes="20vh" - style={{ - width: 'auto', - height: '100%' - }} - /> -
    - ); -}; diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 5a1521b..55faacd 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceReference, SourceWithId } from '../../interfaces/Source'; -import { PreviewThumbnail } from './PreviewThumbnail'; -import { getSourceThumbnail } from '../../utils/source'; +import { SourceWithId } from '../../interfaces/Source'; import videoSettings from '../../utils/videoSettings'; import { getHertz } from '../../utils/stream'; import { useTranslate } from '../../i18n/useTranslate'; @@ -11,55 +9,29 @@ import Outputs from '../inventory/editView/AudioChannels/Outputs'; import { mapAudio } from '../../utils/audioMapping'; import { oneBased } from '../inventory/editView/AudioChannels/utils'; import capitalize from '../../utils/capitalize'; +import { SourceListItemThumbnail } from './SourceListItemThumbnail'; type SourceListItemProps = { source: SourceWithId; - edit?: boolean; + action?: (source: SourceWithId) => void; + actionText?: string; disabled: unknown; - isLocked: boolean; - action: (source: SourceWithId) => void; + locked: boolean; }; -const getIcon = (source: Source) => { - const isGone = source.status === 'gone'; - const className = isGone ? 'text-error' : 'text-brand'; - - const types = { - camera: ( - - ), - microphone: ( - - ), - graphics: ( - - ) - }; - - return types[source.type]; -}; - -function InventoryListItem({ +function SourceListItem({ source, action, disabled, - edit = false, - isLocked + actionText }: SourceListItemProps) { const t = useTranslate(); + const [previewVisible, setPreviewVisible] = useState(false); const [outputRows, setOutputRows] = useState< { id: string; value: string }[][] >([]); + const timeoutRef = useRef(); const { video_stream: videoStream, audio_stream: audioStream } = source; @@ -104,15 +76,10 @@ function InventoryListItem({ className={`relative w-full items-center border-b border-gray-600 ${ disabled ? 'bg-unclickable-bg' : 'hover:bg-zinc-700' }`} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} > - {source.status !== 'gone' && - source.type === 'camera' && - previewVisible && }
    -
    {getIcon(source)}
    +
    ))} @@ -180,22 +147,29 @@ function InventoryListItem({
    - {!disabled ? ( -
    - - )} - {multiviews.length === index + 1 && ( - - )} -
    -
    - + o.pipelineIndex === i)} + addStream={addStream} + updateStream={updateStream} + updateStreams={updateStreams} + deleteStream={deleteStream} + /> ); })} - - clearInputs()} onSave={onSave} /> + {multiviews && + multiviews.length > 0 && + multiviews.map((singleItem, index) => { + return ( +
    +
    +
    + + setLayoutModalOpen(input) + } + lastItem={multiviews.length === index + 1} + multiview={singleItem} + handleUpdateMultiview={(input) => + handleUpdateMultiview(input, index) + } + portDuplicateError={ + portDuplicateIndexes.length > 0 + ? portDuplicateIndexes.includes(index) + : false + } + /> +
    1 + ? 'justify-between' + : 'justify-end' + }`} + > + {multiviews.length > 1 && ( + + )} + {multiviews.length === index + 1 && ( + + )} +
    +
    +
    + ); + })} + + )} + {!!layoutModalOpen && ( + + setNewMultiviewPreset(newLayout) + } + /> + )} + (layoutModalOpen ? closeLayoutModal() : clearInputs())} + onSave={() => (layoutModalOpen ? onUpdateLayoutPreset() : onSave())} + /> ); } diff --git a/src/components/modal/configureOutputModal/Input.tsx b/src/components/modal/configureOutputModal/Input.tsx index 25c0954..a2b0155 100644 --- a/src/components/modal/configureOutputModal/Input.tsx +++ b/src/components/modal/configureOutputModal/Input.tsx @@ -8,6 +8,7 @@ interface IInput { onKeyDown?: (e: KeyboardEvent) => void; size?: 'small' | 'large'; inputError?: boolean; + placeholder?: string; } export default function Input({ @@ -17,7 +18,8 @@ export default function Input({ type = 'text', onKeyDown, size = 'small', - inputError + inputError, + placeholder }: IInput) { const errorCss = 'border-red-500 focus:border-red-500 focus:outline'; @@ -34,6 +36,7 @@ export default function Input({ } pl-2 pt-1 pb-1 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-gray-400 focus:outline-none ${ inputError ? errorCss : '' }`} + placeholder={placeholder ? placeholder : ''} /> ); diff --git a/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx b/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx new file mode 100644 index 0000000..9a37c87 --- /dev/null +++ b/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx @@ -0,0 +1,171 @@ +import { useEffect, useState } from 'react'; +import { useMultiviewPresets } from '../../../hooks/multiviewPreset'; +import Options from './Options'; +import { MultiviewPreset } from '../../../interfaces/preset'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { + MultiviewViewsWithId, + useSetupMultiviewLayout +} from '../../../hooks/useSetupMultiviewLayout'; +import { Production } from '../../../interfaces/production'; +import { useConfigureMultiviewLayout } from '../../../hooks/useConfigureMultiviewLayout'; +import { SourceReference } from '../../../interfaces/Source'; +import Input from './Input'; + +type ChangeLayout = { + defaultLabel?: string; + source?: SourceReference; + id: number; +}; + +export default function MultiviewLayoutSettings({ + // configMode sets the mode of the configuration to create or edit, not implemented yet + configMode, + production, + setNewMultiviewPreset +}: { + configMode: string; + production: Production | undefined; + setNewMultiviewPreset: (preset: MultiviewPreset | null) => void; +}) { + const [selectedMultiviewPreset, setSelectedMultiviewPreset] = + useState(null); + const [changedLayout, setChangedLayout] = useState(null); + const [newPresetName, setNewPresetName] = useState(null); + const [multiviewPresets, loading] = useMultiviewPresets(); + const { multiviewPresetLayout } = useSetupMultiviewLayout( + selectedMultiviewPreset + ); + const { multiviewLayout } = useConfigureMultiviewLayout( + selectedMultiviewPreset, + changedLayout?.defaultLabel, + changedLayout?.source, + changedLayout?.id, + configMode, + newPresetName + ); + const t = useTranslate(); + + const multiviewPresetNames = multiviewPresets?.map((preset) => preset.name) + ? multiviewPresets?.map((preset) => preset.name) + : []; + + useEffect(() => { + setNewPresetName(null); + }, [configMode]); + + useEffect(() => { + if (multiviewPresets && multiviewPresets[0]) { + setSelectedMultiviewPreset(multiviewPresets[0]); + } + }, [multiviewPresets]); + + useEffect(() => { + if (multiviewLayout) { + setSelectedMultiviewPreset(multiviewLayout); + setNewMultiviewPreset(multiviewLayout); + } else { + setSelectedMultiviewPreset(null); + setNewMultiviewPreset(null); + } + }, [multiviewLayout]); + + const handlePresetUpdate = (name: string) => { + const presetLayout = multiviewPresets?.find( + (singlePreset) => singlePreset.name === name + ); + setNewPresetName(name); + if (presetLayout) { + setSelectedMultiviewPreset(presetLayout); + } + }; + + const handleChange = (id: number | undefined, value: string) => { + if (production && id && multiviewPresets) { + // Remove 2 from id to remove id for Preview- and Program-view + // Add 1 to index to get the correct input_slot + const idFirstInputView = id - 2 + 1; + const defaultLabel = multiviewPresets[0].layout.views.find( + (item) => item.input_slot === idFirstInputView + )?.label; + production.sources.map((source) => { + if (value === '') { + setChangedLayout({ defaultLabel, id }); + } + if (source.label === value) { + setChangedLayout({ source, id }); + } + }); + } + }; + + const renderPresetModel = () => { + if (multiviewPresetLayout) { + return ( +
    + {multiviewPresetLayout.layout.views.map( + (singleView: MultiviewViewsWithId) => { + const { x, y, width, height, label, id } = singleView; + const previewView = singleView.input_slot === 1002; + const programView = singleView.input_slot === 1001; + + return ( +
    + {production && (previewView || programView) && ( +

    {label}

    + )} + {production && !previewView && !programView && ( + singleSource.label + )} + value={label ? label : ''} + update={(value) => handleChange(id, value)} + columnStyle + /> + )} +
    + ); + } + )} +
    + ); + } + }; + + return ( +
    + {renderPresetModel()} +
    + handlePresetUpdate(value)} + /> + handlePresetUpdate(value)} + placeholder={t('preset.new_preset_name')} + /> +
    +
    + ); +} diff --git a/src/components/modal/configureOutputModal/MultiviewSettings.tsx b/src/components/modal/configureOutputModal/MultiviewSettings.tsx index e742005..b64f2bd 100644 --- a/src/components/modal/configureOutputModal/MultiviewSettings.tsx +++ b/src/components/modal/configureOutputModal/MultiviewSettings.tsx @@ -6,17 +6,22 @@ import { MultiviewPreset } from '../../../interfaces/preset'; import Input from './Input'; import Options from './Options'; import toast from 'react-hot-toast'; +import { IconSettings } from '@tabler/icons-react'; type MultiviewSettingsProps = { + lastItem: boolean; multiview?: MultiviewSettings; handleUpdateMultiview: (multiview: MultiviewSettings) => void; portDuplicateError: boolean; + openConfigModal: (input: string) => void; }; export default function MultiviewSettingsConfig({ + lastItem, multiview, handleUpdateMultiview, - portDuplicateError + portDuplicateError, + openConfigModal }: MultiviewSettingsProps) { const t = useTranslate(); const [multiviewPresets, loading] = useMultiviewPresets(); @@ -24,6 +29,12 @@ export default function MultiviewSettingsConfig({ MultiviewPreset | undefined >(multiview); + // TODO: When possible to edit layout, uncomment the following code + // const [modalOpen, setModalOpen] = useState(false); + // const toggleConfigModal = () => { + // setModalOpen((state) => !state); + // }; + useEffect(() => { if (multiview) { setSelectedMultiviewPreset(multiview); @@ -142,16 +153,55 @@ export default function MultiviewSettingsConfig({ const multiviewOrPreset = multiview ? multiview : selectedMultiviewPreset; return ( -
    +

    {t('preset.multiview_output_settings')}

    - handleSetSelectedMultiviewPreset(value)} - /> +
    + handleSetSelectedMultiviewPreset(value)} + /> + {lastItem && ( + // TODO: When possible to edit layout, uncomment the following code and remove the button below + + // <> + // + // {modalOpen && ( + //
    + // + // + //
    + // )} + // + )} +
    void; + columnStyle?: boolean; } -export default function Options({ label, options, value, update }: IOPtions) { +export default function Options({ + label, + options, + value, + update, + columnStyle +}: IOptions) { + const t = useTranslate(); return ( -
    +
    { + setSelectedValue(e.target.value); + }} + /> + +
    +
    {productionSetup?.production_settings && diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index e29fcdb..ab4420e 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -38,10 +38,12 @@ export default function Inventory({ locked }: { locked: boolean }) { sources={sources} action={editSource} actionText={t('inventory_list.edit')} + locked={locked} /> {currentSource ? (
    setUpdatedSource(source)} close={() => setCurrentSource(null)} diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index ce312af..376ca20 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -25,7 +25,7 @@ export default function EditView({
    - +
    diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 8ece767..2f1d54b 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -1,11 +1,12 @@ 'use client'; -import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; +import React, { ChangeEvent, KeyboardEvent, useContext, useState } from 'react'; import { IconTrash } from '@tabler/icons-react'; -import { SourceReference } from '../../interfaces/Source'; +import { SourceReference, Type } from '../../interfaces/Source'; import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; import ImageComponent from '../image/ImageComponent'; import { getSourceThumbnail } from '../../utils/source'; +import { GlobalContext } from '../../contexts/GlobalContext'; type SourceCardProps = { source?: ISource; @@ -15,7 +16,9 @@ type SourceCardProps = { onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src: string; + src?: string; + sourceRef?: SourceReference; + type: Type; }; export default function SourceCard({ @@ -26,7 +29,9 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + sourceRef, + type }: SourceCardProps) { const [sourceLabel, setSourceLabel] = useState( sourceRef?.label || source?.name diff --git a/src/components/sourceList/SourceList.tsx b/src/components/sourceList/SourceList.tsx index b4b0c99..bb84d0c 100644 --- a/src/components/sourceList/SourceList.tsx +++ b/src/components/sourceList/SourceList.tsx @@ -15,6 +15,7 @@ interface SourceListProps { isDisabledFunc?: (source: SourceWithId) => boolean; action?: (source: SourceWithId) => void; actionText?: string; + locked: boolean; } const SourceList: React.FC = (props) => { @@ -44,6 +45,7 @@ const SourceList: React.FC = (props) => { source={source} disabled={isDisabledFunc?.(source)} action={action} + locked={locked} /> ); }); diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 55faacd..cc692b1 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -23,6 +23,7 @@ function SourceListItem({ source, action, disabled, + locked, actionText }: SourceListItemProps) { const t = useTranslate(); @@ -158,28 +159,19 @@ function SourceListItem({ >
    (disabled || !action ? '' : action(source))} + disabled ? 'text-unclickable-text' : 'text-brand' + } text-xs`} > -
    - {actionText} -
    - - -
    - ) : null} + {actionText} +
    + + +
    From 0c381d17c910c17aa6cdea4c0ee15ea94378c805 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Thu, 3 Oct 2024 16:28:05 +0200 Subject: [PATCH 08/10] fix: correct env.sample and add variable to readme --- .env.sample | 4 ++-- README.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 3ff9058..a8a087a 100644 --- a/.env.sample +++ b/.env.sample @@ -4,7 +4,7 @@ MONGODB_URI=${MONGODB_URI:-mongodb://api:password@localhost:27017/live-gui} # Ateliere Live System Controlleer LIVE_URL=${LIVE_URL:-https://localhost:8080} LIVE_CREDENTIALS=${LIVE_CREDENTIALS:-admin:admin} -CONTROL_PANEL_WS==${} +CONTROL_PANEL_WS=${ip/hostname:port} # This ENV variable disables SSL Verification, use if the above LIVE_URL doesn't have a proper certificate NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-1} @@ -17,4 +17,4 @@ BCRYPT_SALT_ROUNDS=${BCRYPT_SALT_ROUNDS:-10} UI_LANG=${UI_LANG:-en} # Mediaplayer - path on the system controller -MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4 \ No newline at end of file +MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4 diff --git a/README.md b/README.md index 96d4426..f5434e2 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Then copy the `.env.sample` file and name it `.env`, it will contain env variabl - `LIVE_URL` - The URL to the Ateliere Live system controller REST API - `LIVE_CREDENTIALS` - Credentials for the Ateliere Live system controller REST API +- `CONTROL_PANEL_WS` - Specifies the IP address or hostname and port for the control panel WebSocket server. - `NEXTAUTH_SECRET` - The secret used to encrypt the JWT Token - `NEXTAUTH_URL` - The base url for the service, eg. `http://localhost:3000`, used internally by NextAuth. From a1444ce889065e00df4b8c71f3af30c1dcf9ba48 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Tue, 8 Oct 2024 09:11:09 +0200 Subject: [PATCH 09/10] fix: removed the commented-out code and added empty lines --- src/api/ateliereLive/websocket.ts | 5 --- src/api/mongoClient/defaults/preset.ts | 2 +- .../[source_name]/thumbnail/route.ts | 1 + src/app/api/manager/streams/route.ts | 2 ++ .../MultiviewSettings.tsx | 33 ------------------- 5 files changed, 4 insertions(+), 39 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 0cbe35d..17ac784 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -5,11 +5,6 @@ function createWebSocket(): Promise { const ws = new WebSocket(`ws://${process.env.CONTROL_PANEL_WS}`); 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); }); }); diff --git a/src/api/mongoClient/defaults/preset.ts b/src/api/mongoClient/defaults/preset.ts index e3022f6..4c42410 100644 --- a/src/api/mongoClient/defaults/preset.ts +++ b/src/api/mongoClient/defaults/preset.ts @@ -306,7 +306,7 @@ export const defaultMultiview = [ }, { _id: new ObjectId('65cb266c00fecda4a1faf977'), - name: '12 inputs HD', + name: '13 inputs HD', layout: { output_height: 1080, output_width: 1920, diff --git a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts index 8d69748..926d3e3 100644 --- a/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts +++ b/src/app/api/manager/sources/[ingest_name]/[source_name]/thumbnail/route.ts @@ -20,6 +20,7 @@ export async function GET( status: 403 }); } + try { const ingestUuid = await getUuidFromIngestName(params.ingest_name); const sourceId = await getSourceIdFromSourceName( diff --git a/src/app/api/manager/streams/route.ts b/src/app/api/manager/streams/route.ts index 06c7ff6..689dd0f 100644 --- a/src/app/api/manager/streams/route.ts +++ b/src/app/api/manager/streams/route.ts @@ -15,8 +15,10 @@ export async function POST(request: NextRequest): Promise { status: 403 }); } + const data = await request.json(); const createStreamRequest = data as CreateStreamRequestBody; + return await createStream( createStreamRequest.source, createStreamRequest.production, diff --git a/src/components/modal/configureOutputModal/MultiviewSettings.tsx b/src/components/modal/configureOutputModal/MultiviewSettings.tsx index b64f2bd..85389e1 100644 --- a/src/components/modal/configureOutputModal/MultiviewSettings.tsx +++ b/src/components/modal/configureOutputModal/MultiviewSettings.tsx @@ -29,12 +29,6 @@ export default function MultiviewSettingsConfig({ MultiviewPreset | undefined >(multiview); - // TODO: When possible to edit layout, uncomment the following code - // const [modalOpen, setModalOpen] = useState(false); - // const toggleConfigModal = () => { - // setModalOpen((state) => !state); - // }; - useEffect(() => { if (multiview) { setSelectedMultiviewPreset(multiview); @@ -173,33 +167,6 @@ export default function MultiviewSettingsConfig({ > - // <> - // - // {modalOpen && ( - //
    - // - // - //
    - // )} - // )}
    From 3fbf94a4f27caf1728d2663aecc7c6545f6c8ee0 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 8 Oct 2024 09:47:25 +0200 Subject: [PATCH 10/10] fix: remove reset and restructuring --- .../ateliereLive/pipelines/streams/streams.ts | 28 +++++++++---------- src/api/ateliereLive/websocket.ts | 2 -- src/app/api/manager/websocket/route.ts | 2 -- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/api/ateliereLive/pipelines/streams/streams.ts b/src/api/ateliereLive/pipelines/streams/streams.ts index f831d37..c5fb134 100644 --- a/src/api/ateliereLive/pipelines/streams/streams.ts +++ b/src/api/ateliereLive/pipelines/streams/streams.ts @@ -106,27 +106,27 @@ export async function createStream( ); const stream: PipelineStreamSettings = { - ingest_id: ingestUuid, - source_id: sourceId, pipeline_id: pipeline.pipeline_id!, - input_slot: input_slot, alignment_ms: pipeline.alignment_ms, - max_network_latency_ms: pipeline.max_network_latency_ms, - width: pipeline.width, - height: pipeline.height, - frame_rate_d: pipeline.frame_rate_d, - frame_rate_n: pipeline.frame_rate_n, - format: pipeline.format, + audio_format: pipeline.audio_format, + audio_sampling_frequency: pipeline.audio_sampling_frequency, + bit_depth: pipeline.bit_depth, + convert_color_range: pipeline.convert_color_range, encoder: pipeline.encoder, encoder_device: pipeline.encoder_device, + format: pipeline.format, + frame_rate_d: pipeline.frame_rate_d, + frame_rate_n: pipeline.frame_rate_n, gop_length: pipeline.gop_length, + height: pipeline.height, + max_network_latency_ms: pipeline.max_network_latency_ms, pic_mode: pipeline.pic_mode, - video_kilobit_rate: pipeline.video_kilobit_rate, - bit_depth: pipeline.bit_depth, speed_quality_balance: pipeline.speed_quality_balance, - convert_color_range: pipeline.convert_color_range, - audio_sampling_frequency: pipeline.audio_sampling_frequency, - audio_format: pipeline.audio_format, + video_kilobit_rate: pipeline.video_kilobit_rate, + width: pipeline.width, + ingest_id: ingestUuid, + source_id: sourceId, + input_slot, audio_mapping: JSON.stringify(audioMapping), interfaces: [ { diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 17ac784..ab56eaa 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -21,11 +21,9 @@ export async function createControlPanelWebSocket() { }, closeHtml: (input: number) => { ws.send(`html close ${input}`); - ws.send('html reset'); }, closeMediaplayer: (input: number) => { ws.send(`media close ${input}`); - ws.send('media reset'); }, close: () => setTimeout(() => { diff --git a/src/app/api/manager/websocket/route.ts b/src/app/api/manager/websocket/route.ts index 1ff9789..66c6631 100644 --- a/src/app/api/manager/websocket/route.ts +++ b/src/app/api/manager/websocket/route.ts @@ -18,10 +18,8 @@ export async function POST(request: NextRequest): Promise { ws.onopen = () => { if (action === 'closeHtml') { ws.send(`html close ${inputSlot}`); - ws.send('html reset'); } else if (action === 'closeMediaplayer') { ws.send(`media close ${inputSlot}`); - ws.send('media reset'); } ws.close(); };