From 80116ace19cb8303f35c206c8774860da20cadd9 Mon Sep 17 00:00:00 2001 From: lucasmaupin Date: Fri, 1 Nov 2024 14:33:29 +0100 Subject: [PATCH] chore: rebase-main --- next.config.js | 1 + .../pipelines/multiviews/multiviews.ts | 35 +- src/api/ateliereLive/pipelines/pipelines.ts | 33 +- .../renderingengine/renderingengine.ts | 52 +- .../ateliereLive/pipelines/streams/streams.ts | 30 +- src/api/manager/job/syncMonitoring.ts | 15 +- src/api/manager/productions.ts | 38 +- src/api/manager/workflow.ts | 233 ++- src/app/api/manager/productions/[id]/route.ts | 5 +- .../manager/rendering-engine/html/route.ts | 5 +- .../manager/rendering-engine/media/route.ts | 5 +- src/app/api/manager/streams/route.ts | 5 +- src/app/production/[id]/page.tsx | 1245 +---------------- .../createProduction/CreateProduction.tsx | 41 +- src/components/dragElement/DragItem.tsx | 17 +- .../dropDown/ControlPanelDropDown.tsx | 37 +- src/components/dropDown/DropDown.tsx | 64 +- .../dropDown/PipelineNameDropDown.tsx | 56 +- .../layout/DefaultLayout.module.scss | 8 + src/components/layout/DefaultLayout.tsx | 5 +- .../modal/ConfigureAlignmentLatencyModal.tsx | 116 +- .../ConfigureMultiviewButton.tsx | 61 - .../ConfigureMultiviewModal.tsx | 359 ----- .../ConfigureOutputModal.tsx | 6 +- .../modal/configureOutputModal/Input.tsx | 2 +- .../modal/configureOutputModal/Options.tsx | 2 +- .../PipelineOutputConfig.tsx | 6 +- .../configureOutputModal/StreamAccordion.tsx | 6 +- .../Checkbox.tsx | 2 +- .../MultiviewLayout.tsx | 6 +- .../MultiviewLayoutSetup.tsx} | 98 +- .../MultiviewLayoutSetupButton.tsx | 52 + .../MultiviewSettings.tsx | 19 +- .../RemoveLayoutButton.tsx | 12 +- src/components/pipeline/Pipelines.tsx | 12 +- src/components/production/ProductionPage.tsx | 140 ++ .../ProductionControlConnections.tsx | 104 ++ .../production/header/ProductionHeader.tsx | 70 + .../monitoring/ProductionMonitoring.tsx | 28 + .../multiviews/ProductionMultiviews.tsx | 300 ++++ .../outputs/ProductionOutputEdit.tsx | 202 +++ .../production/outputs/ProductionOutputs.tsx | 37 + .../outputs/ProductionOutputsCard.tsx | 125 ++ .../pipelines/ProductionPipelineCard.tsx | 105 ++ .../pipelines/ProductionPipelines.tsx | 46 + .../sources/ProductionSourceList.tsx | 93 ++ .../production/sources/ProductionSources.tsx | 688 +++++++++ .../productionsList/ProductionsListItem.tsx | 69 +- src/components/section/Section.tsx | 37 + src/components/sourceCard/SourceCard.tsx | 25 +- src/components/sourceCards/SourceCards.tsx | 52 +- src/components/sourceList/SourceList.tsx | 2 +- .../sourceListItem/SourceListItem.tsx | 10 +- .../startProduction/StartProductionButton.tsx | 76 +- .../startProduction/presetDropdown.tsx | 81 +- src/hooks/items/addSetupItem.ts | 20 +- src/hooks/items/removeSetupItem.ts | 36 - src/hooks/items/updateSetupItem.ts | 8 +- src/hooks/multiviewLayout.ts | 2 +- src/hooks/multiviews.ts | 7 +- src/hooks/productions.ts | 16 +- .../renderingEngine/useCreateHtmlSource.tsx | 7 +- .../renderingEngine/useCreateMediaSource.tsx | 7 +- .../renderingEngine/useDeleteHtmlSource.tsx | 16 +- .../renderingEngine/useDeleteMediaSource.tsx | 17 +- src/hooks/streams.ts | 26 +- src/hooks/useCheckProductionPipelines.tsx | 15 +- src/hooks/useCreateInputArray.tsx | 10 +- src/hooks/useGetFirstEmptySlot.ts | 36 +- ...pdateSourceInputSlotOnMultiviewLayouts.tsx | 36 +- src/hooks/utils/useEffectNotOnMount.tsx | 17 + src/interfaces/controlConnections.ts | 2 +- src/interfaces/pipeline.ts | 7 +- src/interfaces/production.ts | 14 +- 74 files changed, 2822 insertions(+), 2456 deletions(-) create mode 100644 src/components/layout/DefaultLayout.module.scss delete mode 100644 src/components/modal/configureMultiviewModal/ConfigureMultiviewButton.tsx delete mode 100644 src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx rename src/components/modal/{configureMultiviewModal/MultiviewLayoutSettings => multiviewLayoutSetup}/Checkbox.tsx (90%) rename src/components/modal/{configureMultiviewModal/MultiviewLayoutSettings => multiviewLayoutSetup}/MultiviewLayout.tsx (90%) rename src/components/modal/{configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayoutSettings.tsx => multiviewLayoutSetup/MultiviewLayoutSetup.tsx} (77%) create mode 100644 src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetupButton.tsx rename src/components/modal/{configureMultiviewModal => multiviewLayoutSetup}/MultiviewSettings.tsx (94%) rename src/components/modal/{configureMultiviewModal/MultiviewLayoutSettings => multiviewLayoutSetup}/RemoveLayoutButton.tsx (70%) create mode 100644 src/components/production/ProductionPage.tsx create mode 100644 src/components/production/controlConnections/ProductionControlConnections.tsx create mode 100644 src/components/production/header/ProductionHeader.tsx create mode 100644 src/components/production/monitoring/ProductionMonitoring.tsx create mode 100644 src/components/production/multiviews/ProductionMultiviews.tsx create mode 100644 src/components/production/outputs/ProductionOutputEdit.tsx create mode 100644 src/components/production/outputs/ProductionOutputs.tsx create mode 100644 src/components/production/outputs/ProductionOutputsCard.tsx create mode 100644 src/components/production/pipelines/ProductionPipelineCard.tsx create mode 100644 src/components/production/pipelines/ProductionPipelines.tsx create mode 100644 src/components/production/sources/ProductionSourceList.tsx create mode 100644 src/components/production/sources/ProductionSources.tsx create mode 100644 src/components/section/Section.tsx delete mode 100644 src/hooks/items/removeSetupItem.ts create mode 100644 src/hooks/utils/useEffectNotOnMount.tsx diff --git a/next.config.js b/next.config.js index 24756994..17f56ac6 100644 --- a/next.config.js +++ b/next.config.js @@ -13,6 +13,7 @@ module.exports = { images: { minimumCacheTTL: 0 }, + reactStrictMode: false, async headers() { return [ { diff --git a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts index 738d6782..6ae528a6 100644 --- a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts +++ b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts @@ -7,7 +7,10 @@ import { getAuthorizationHeader } from '../../utils/authheader'; import { createMultiview } from '../../utils/multiview'; import { getSourcesByIds } from '../../../manager/sources'; import { Log } from '../../../logger'; -import { ProductionSettings } from '../../../../interfaces/production'; +import { + Production, + ProductionSettings +} from '../../../../interfaces/production'; import { MultiviewSettings } from '../../../../interfaces/multiview'; import { LIVE_BASE_API_PATH } from '../../../../constants'; @@ -35,14 +38,10 @@ export async function getMultiviewsForPipeline( } export async function createMultiviewForPipeline( - productionSettings: ProductionSettings, - sourceRefs: SourceReference[] + production: Production ): Promise { - const pipeline = productionSettings.pipelines.find((p) => - p.multiviews ? p.multiviews?.length > 0 : undefined - ); - const multiviewIndexArray = pipeline?.multiviews - ? pipeline.multiviews.map((p) => p.for_pipeline_idx) + const multiviewIndexArray = production?.multiviews + ? production.multiviews.map((p) => p.for_pipeline_idx) : undefined; const multiviewIndex = multiviewIndexArray?.find((p) => p !== undefined); @@ -51,22 +50,17 @@ export async function createMultiviewForPipeline( Log().error(`Did not find a specified pipeline in multiview settings`); throw `Did not find a specified pipeline in multiview settings`; } - if ( - !productionSettings.pipelines[multiviewIndex].multiviews || - productionSettings.pipelines[multiviewIndex].multiviews?.length === 0 - ) { - Log().error( - `Did not find any multiview settings in pipeline settings for: ${productionSettings.pipelines[multiviewIndex]}` - ); - throw `Did not find any multiview settings in pipeline settings for: ${productionSettings.pipelines[multiviewIndex]}`; + if (!production.multiviews || production.multiviews?.length === 0) { + Log().error(`Did not find any multiviews for: ${production.name}`); + throw `Did not find any multiviews for: ${production.name}`; } const pipelineUUID = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - productionSettings.pipelines[multiviewIndex].pipeline_id!; + production.pipelines[0].pipeline_id!; const sources = await getSourcesByIds( - sourceRefs.map((ref) => (ref._id ? ref._id.toString() : '')) + production.sources.map((ref) => (ref._id ? ref._id.toString() : '')) ); - const sourceRefsWithLabels = sourceRefs.map((ref) => { + const sourceRefsWithLabels = production.sources.map((ref) => { const refId = ref._id ? ref._id.toString() : ''; if (!ref.label) { const source = sources.find((source) => source._id.toString() === refId); @@ -76,8 +70,7 @@ export async function createMultiviewForPipeline( }); Log().info(`Creating a multiview for pipeline '${pipelineUUID}' from preset`); - const multiviewsSettings: MultiviewSettings[] = - productionSettings.pipelines[multiviewIndex].multiviews ?? []; + const multiviewsSettings: MultiviewSettings[] = production.multiviews ?? []; const createEachMultiviewer = multiviewsSettings.map( async (singleMultiviewSettings) => { diff --git a/src/api/ateliereLive/pipelines/pipelines.ts b/src/api/ateliereLive/pipelines/pipelines.ts index eb2a8089..cfcdced2 100644 --- a/src/api/ateliereLive/pipelines/pipelines.ts +++ b/src/api/ateliereLive/pipelines/pipelines.ts @@ -163,8 +163,25 @@ export async function removePipelineStreams(pipeId: string) { return { status: 200, results }; } -export async function createPipelineOutputs(pipeline: PipelineSettings) { - const outputs = pipeline.outputs; +export async function createPipelineOutputs( + pipeline: PipelineSettings, + outputsParam: PipelineOutput[] +) { + if (!pipeline.pipeline_id) return; + const pipelinesOutputs = await getPipelineOutputs(pipeline.pipeline_id).catch( + (error) => { + throw error.message; + } + ); + const outputsWithStreams = outputsParam.filter( + (output) => output.streams?.length + ); + const outputs = outputsWithStreams?.map((output, index) => { + return { + ...output, + uuid: pipelinesOutputs[index]?.uuid + }; + }); if (!outputs) return; const startOutputStreamsPromises = outputs.map((o) => { return startPipelineStream( @@ -176,14 +193,14 @@ export async function createPipelineOutputs(pipeline: PipelineSettings) { const startedOutputs = await Promise.all(startOutputStreamsPromises).catch( (error) => { Log().error( - `Failed to create outputs for pipeline '${pipeline.pipeline_name}/${pipeline.pipeline_id}'`, + `Failed to create outputs for pipeline '${pipeline.pipeline_readable_name}/${pipeline.pipeline_id}'`, error ); - throw `Failed to create outputs for pipeline '${pipeline.pipeline_name}/${pipeline.pipeline_id}': ${error.message}`; + throw `Failed to create outputs for pipeline '${pipeline.pipeline_readable_name}/${pipeline.pipeline_id}': ${error.message}`; } ); Log().info( - `Outputs for pipeline '${pipeline.pipeline_name}/${pipeline.pipeline_id}' created` + `Outputs for pipeline '${pipeline.pipeline_readable_name}/${pipeline.pipeline_id}' created` ); return startedOutputs; } @@ -198,14 +215,14 @@ export async function connectControlPanelToPipeline( }); const productionControlPanels = controlPanels.filter((c) => - control_connection.control_panel_name?.includes(c.name) + control_connection.control_panel_ids?.includes(c.uuid) ); if (!productionControlPanels || productionControlPanels.length === 0) { Log().error( - `Did not find control panel: ${control_connection.control_panel_name}` + `Did not find control panel: ${control_connection.control_panel_ids}` ); - throw `Did not find control panel: ${control_connection.control_panel_name}`; + throw `Did not find control panel: ${control_connection.control_panel_ids}`; } const pipelinesIds = pipelines.map((pipeline) => { diff --git a/src/api/ateliereLive/pipelines/renderingengine/renderingengine.ts b/src/api/ateliereLive/pipelines/renderingengine/renderingengine.ts index 68507b9d..9c6e814c 100644 --- a/src/api/ateliereLive/pipelines/renderingengine/renderingengine.ts +++ b/src/api/ateliereLive/pipelines/renderingengine/renderingengine.ts @@ -5,7 +5,6 @@ import { } from '../../../../../types/ateliere-live'; import { LIVE_BASE_API_PATH } from '../../../../constants'; import { MultiviewSettings } from '../../../../interfaces/multiview'; -import { Production } from '../../../../interfaces/production'; import { HTMLSource, MediaSource @@ -17,6 +16,7 @@ import { getMultiviewsForPipeline, updateMultiviewForPipeline } from '../multiviews/multiviews'; +import { PipelineSettings } from '../../../../interfaces/pipeline'; export async function getPipelineHtmlSources( pipelineUuid: string @@ -44,20 +44,19 @@ export async function getPipelineHtmlSources( } export async function createPipelineHtmlSource( - production: Production, + pipelines: PipelineSettings[], inputSlot: number, data: HTMLSource, source: SourceReference ) { try { - const { production_settings } = production; const htmlResults = []; - for (let i = 0; i < production_settings.pipelines.length; i++) { + for (let i = 0; i < pipelines.length; i++) { const response = await fetch( new URL( LIVE_BASE_API_PATH + - `/pipelines/${production_settings.pipelines[i].pipeline_id}/renderingengine/html`, + `/pipelines/${pipelines[i].pipeline_id}/renderingengine/html`, process.env.LIVE_URL ), { @@ -117,18 +116,16 @@ export async function createPipelineHtmlSource( } try { - if (!production.production_settings.pipelines[0].pipeline_id) { - Log().error( - `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}` - ); - throw `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}`; + if (!pipelines[0].pipeline_id) { + Log().error(`Missing pipeline_id for: ${pipelines[0].pipeline_name}`); + throw `Missing pipeline_id for: ${pipelines[0].pipeline_name}`; } const multiviewsResponse = await getMultiviewsForPipeline( - production.production_settings.pipelines[0].pipeline_id + pipelines[0].pipeline_id ); const multiviews = multiviewsResponse.filter((multiview) => { - const pipeline = production.production_settings.pipelines[0]; + const pipeline = pipelines[0]; const multiviewArray = pipeline.multiviews; if (Array.isArray(multiviewArray)) { @@ -146,9 +143,9 @@ export async function createPipelineHtmlSource( if (multiviews.length === 0 || !multiviews) { Log().error( - `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}` + `No multiview found for pipeline: ${pipelines[0].pipeline_id}` ); - throw `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}`; + throw `No multiview found for pipeline: ${pipelines[0].pipeline_id}`; } await Promise.all( @@ -188,7 +185,7 @@ export async function createPipelineHtmlSource( ]; await updateMultiviewForPipeline( - production.production_settings.pipelines[0].pipeline_id!, + pipelines[0].pipeline_id!, multiview.id, updatedViews ); @@ -303,20 +300,19 @@ export async function getPipelineMediaSources( } export async function createPipelineMediaSource( - production: Production, + pipelines: PipelineSettings[], inputSlot: number, data: MediaSource, source: SourceReference ) { try { - const { production_settings } = production; const mediaResults = []; - for (let i = 0; i < production_settings.pipelines.length; i++) { + for (let i = 0; i < pipelines.length; i++) { const response = await fetch( new URL( LIVE_BASE_API_PATH + - `/pipelines/${production_settings.pipelines[i].pipeline_id}/renderingengine/media`, + `/pipelines/${pipelines[i].pipeline_id}/renderingengine/media`, process.env.LIVE_URL ), { @@ -373,18 +369,16 @@ export async function createPipelineMediaSource( } try { - if (!production.production_settings.pipelines[0].pipeline_id) { - Log().error( - `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}` - ); - throw `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}`; + if (pipelines[0].pipeline_id) { + Log().error(`Missing pipeline_id for: ${pipelines[0].pipeline_name}`); + throw `Missing pipeline_id for: ${pipelines[0].pipeline_name}`; } const multiviewsResponse = await getMultiviewsForPipeline( - production.production_settings.pipelines[0].pipeline_id + pipelines[0].pipeline_id || '' ); const multiviews = multiviewsResponse.filter((multiview) => { - const pipeline = production.production_settings.pipelines[0]; + const pipeline = pipelines[0]; const multiviewArray = pipeline.multiviews; if (Array.isArray(multiviewArray)) { @@ -402,9 +396,9 @@ export async function createPipelineMediaSource( if (multiviews.length === 0 || !multiviews) { Log().error( - `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}` + `No multiview found for pipeline: ${pipelines[0].pipeline_id}` ); - throw `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}`; + throw `No multiview found for pipeline: ${pipelines[0].pipeline_id}`; } await Promise.all( @@ -444,7 +438,7 @@ export async function createPipelineMediaSource( ]; await updateMultiviewForPipeline( - production.production_settings.pipelines[0].pipeline_id!, + pipelines[0].pipeline_id!, multiview.id, updatedViews ); diff --git a/src/api/ateliereLive/pipelines/streams/streams.ts b/src/api/ateliereLive/pipelines/streams/streams.ts index fc183dad..8f857ab5 100644 --- a/src/api/ateliereLive/pipelines/streams/streams.ts +++ b/src/api/ateliereLive/pipelines/streams/streams.ts @@ -6,7 +6,10 @@ import { SourceWithId } from '../../../../interfaces/Source'; import { MultiviewSettings } from '../../../../interfaces/multiview'; -import { PipelineStreamSettings } from '../../../../interfaces/pipeline'; +import { + PipelineSettings, + PipelineStreamSettings +} from '../../../../interfaces/pipeline'; import { Production } from '../../../../interfaces/production'; import { Result } from '../../../../interfaces/result'; import { Log } from '../../../logger'; @@ -50,15 +53,14 @@ export async function getPipelineStreams( export async function createStream( source: SourceWithId, - production: Production, + pipelines: PipelineSettings[], input_slot: number ): Promise> { const sourceToPipelineStreams: SourceToPipelineStream[] = []; try { const allPipelines = await getPipelines(); - const { production_settings } = production; const pipelinesToUseCompact = allPipelines.filter((pipeline) => - production_settings.pipelines.some((p) => p.pipeline_id === pipeline.uuid) + pipelines.some((p) => p.pipeline_id === pipeline.uuid) ); const usedPorts = await getCurrentlyUsedPorts( @@ -89,7 +91,7 @@ export async function createStream( await initDedicatedPorts(); - for (const pipeline of production_settings.pipelines) { + for (const pipeline of pipelines) { const availablePorts = getAvailablePortsForIngest( source.ingest_name, usedPorts @@ -208,18 +210,16 @@ export async function createStream( } try { - if (!production.production_settings.pipelines[0].pipeline_id) { - Log().error( - `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}` - ); - throw `Missing pipeline_id for: ${production.production_settings.pipelines[0].pipeline_name}`; + if (!pipelines[0].pipeline_id) { + Log().error(`Missing pipeline_id for: ${pipelines[0].pipeline_name}`); + throw `Missing pipeline_id for: ${pipelines[0].pipeline_name}`; } const multiviewsResponse = await getMultiviewsForPipeline( - production.production_settings.pipelines[0].pipeline_id + pipelines[0].pipeline_id ); const multiviews = multiviewsResponse.filter((multiview) => { - const pipeline = production.production_settings.pipelines[0]; + const pipeline = pipelines[0]; const multiviewArray = pipeline.multiviews; if (Array.isArray(multiviewArray)) { @@ -237,9 +237,9 @@ export async function createStream( if (multiviews.length === 0) { Log().error( - `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}` + `No multiview found for pipeline: ${pipelines[0].pipeline_id}` ); - throw `No multiview found for pipeline: ${production.production_settings.pipelines[0].pipeline_id}`; + throw `No multiview found for pipeline: ${pipelines[0].pipeline_id}`; } multiviews.map(async (multiview) => { const views = multiview.layout.views; @@ -278,7 +278,7 @@ export async function createStream( ]; await updateMultiviewForPipeline( - production.production_settings.pipelines[0].pipeline_id!, + pipelines[0].pipeline_id!, multiview.id, updatedViews ); diff --git a/src/api/manager/job/syncMonitoring.ts b/src/api/manager/job/syncMonitoring.ts index 7619bfdb..2ab9f2ec 100644 --- a/src/api/manager/job/syncMonitoring.ts +++ b/src/api/manager/job/syncMonitoring.ts @@ -40,9 +40,8 @@ export async function updatedMonitoringForProduction( production: Production ) { const productionId = production._id; - const { production_settings } = production; - const productionPipelineUUIDs = production_settings.pipelines.map( + const productionPipelineUUIDs = production.pipelines.map( (pipeline) => pipeline.pipeline_id! ); @@ -1230,11 +1229,9 @@ const createMonitoringComponentControlPanels = async ( prevMonitoring?: Monitoring ) => { const pipelineIndex = - production.production_settings.control_connection.control_panel_endpoint - .toPipelineIdx; + production.control_connection?.control_panel_endpoint.toPipelineIdx || 0; - const pipelineId = - production.production_settings.pipelines[pipelineIndex].pipeline_id; + const pipelineId = production.pipelines[pipelineIndex].pipeline_id; const pipeline = pipelines.find((p) => p.uuid === pipelineId); @@ -1294,11 +1291,9 @@ const createMonitoringControlPanels = async ( pipelines: ResourcesPipelineResponse[] ) => { const pipelineIndex = - production.production_settings.control_connection.control_panel_endpoint - .toPipelineIdx; + production.control_connection?.control_panel_endpoint.toPipelineIdx || 0; - const pipelineId = - production.production_settings.pipelines[pipelineIndex].pipeline_id; + const pipelineId = production.pipelines[pipelineIndex].pipeline_id; const pipeline = pipelines.find((p) => p.uuid === pipelineId); diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index 716cd340..672d25ad 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -1,6 +1,6 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; -import { Production, ProductionWithId } from '../../interfaces/production'; +import { Production } from '../../interfaces/production'; import { Log } from '../logger'; export async function getProductions(): Promise { @@ -10,12 +10,12 @@ export async function getProductions(): Promise { ) as Production[]; } -export async function getProduction(id: string): Promise { +export async function getProduction(id: string): Promise { const db = await getDatabase(); return (await db .collection('productions') - .findOne({ _id: new ObjectId(id) })) as ProductionWithId; + .findOne({ _id: new ObjectId(id) })) as Production | null; } export async function setProductionsIsActiveFalse(): Promise< @@ -32,14 +32,13 @@ export async function putProduction( production: Production ): Promise { const db = await getDatabase(); - const newSourceId = new ObjectId().toString(); const sources = production.sources ? production.sources.flatMap((singleSource) => { return singleSource._id ? singleSource : { - _id: newSourceId, + _id: new ObjectId().toString(), type: singleSource.type, label: singleSource.label, input_slot: singleSource.input_slot, @@ -60,7 +59,11 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, - production_settings: production.production_settings + preset_name: production.preset_name, + pipelines: production.pipelines, + control_connection: production.control_connection, + outputs: production.outputs, + multiviews: production.multiviews } ); @@ -73,7 +76,11 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, - production_settings: production.production_settings + preset_name: production.preset_name, + pipelines: production.pipelines, + control_connection: production.control_connection, + outputs: production.outputs, + multiviews: production.multiviews }; } @@ -113,7 +120,7 @@ export async function getProductionPipelineSourceAlignment( return null; } - const pipeline = production.production_settings.pipelines.find( + const pipeline = production.pipelines.find( (p) => p.pipeline_id === pipelineId ); @@ -141,13 +148,12 @@ export async function setProductionPipelineSourceAlignment( const result = await db.collection('productions').updateOne( { _id: new ObjectId(productionId), - 'production_settings.pipelines.pipeline_id': pipelineId, - 'production_settings.pipelines.sources.source_id': sourceId + 'pipelines.pipeline_id': pipelineId, + 'pipelines.sources.source_id': sourceId }, { $set: { - 'production_settings.pipelines.$[p].sources.$[s].settings.alignment_ms': - alignment_ms + 'pipelines.$[p].sources.$[s].settings.alignment_ms': alignment_ms } }, { @@ -182,7 +188,7 @@ export async function getProductionSourceLatency( return null; } - const pipeline = production.production_settings.pipelines.find( + const pipeline = production.pipelines.find( (p) => p.pipeline_id === pipelineId ); @@ -210,12 +216,12 @@ export async function setProductionPipelineSourceLatency( const result = await db.collection('productions').updateOne( { _id: new ObjectId(productionId), - 'production_settings.pipelines.pipeline_id': pipelineId, - 'production_settings.pipelines.sources.source_id': sourceId + 'pipelines.pipeline_id': pipelineId, + 'pipelines.sources.source_id': sourceId }, { $set: { - 'production_settings.pipelines.$[p].sources.$[s].settings.max_network_latency_ms': + 'pipelines.$[p].sources.$[s].settings.max_network_latency_ms': max_network_latency_ms } }, diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 6520377b..2e9dbaec 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -1,7 +1,6 @@ import { SourceReference, SourceWithId } from './../../interfaces/Source'; import { Production, - ProductionSettings, StartProductionStep, StopProductionStep } from '../../interfaces/production'; @@ -61,6 +60,7 @@ import { deleteHtmlFromPipeline, deleteMediaFromPipeline } from '../ateliereLive/pipelines/renderingengine/renderingengine'; +import cloneDeep from 'lodash.clonedeep'; const isUsed = (pipeline: ResourcesPipelineResponse) => { const hasStreams = pipeline.streams.length > 0; @@ -81,7 +81,7 @@ const isUsed = (pipeline: ResourcesPipelineResponse) => { async function connectIngestSources( productionSources: SourceReference[], - productionSettings: ProductionSettings, + production: Production, sources: SourceWithId[], usedPorts: Set ) { @@ -109,7 +109,7 @@ async function connectIngestSources( const newAudioMapping = audioSettings?.audio_stream?.audio_mapping; const audioMapping = newAudioMapping?.length ? newAudioMapping : [[0, 1]]; - for (const pipeline of productionSettings.pipelines) { + for (const pipeline of production.pipelines) { const availablePorts = getAvailablePortsForIngest( source.ingest_name, usedPorts @@ -195,19 +195,16 @@ async function connectIngestSources( return sourceToPipelineStreams; } -async function insertPipelineUuid(productionSettings: ProductionSettings) { +async function insertPipelineUuid(production: Production) { const availablePipelines = await getPipelines().catch((error) => { - Log().error( - `Failed to get pipeline IDs for '${productionSettings.name}'`, - error - ); - throw `Failed to get pipeline IDs for '${productionSettings.name}: ${error.message}'`; + Log().error(`Failed to get pipeline IDs for '${production.name}'`, error); + throw `Failed to get pipeline IDs for '${production.name}: ${error.message}'`; }); - for (const pipelinePreset of productionSettings.pipelines) { + for (const pipelinePreset of production.pipelines) { const pipeline = availablePipelines.find( (p: ResourcesCompactPipelineResponse) => - p.name === pipelinePreset.pipeline_name + p.uuid === pipelinePreset.pipeline_id ); if (pipeline) { pipelinePreset.pipeline_id = pipeline.uuid; @@ -330,16 +327,14 @@ export async function stopProduction( success: true } as StopProductionStep }; - const pipelineIds = production.production_settings.pipelines.map( - (p) => p.pipeline_id - ); + const pipelineIds = production.pipelines.map((p) => p.pipeline_id); const productionHasRenderingEngineSources = production.sources.some( (source) => source.type === 'html' || source.type === 'mediaplayer' ); if (productionHasRenderingEngineSources) { - for (const pipeline of production.production_settings.pipelines) { + for (const pipeline of production.pipelines) { const pipelineId = pipeline.pipeline_id; if (pipelineId) { const pipelineRenderingEngine = await getPipelineRenderingEngine( @@ -350,7 +345,7 @@ export async function stopProduction( const mediaSources = pipelineRenderingEngine.media; if (htmlSources.length > 0 && htmlSources) { - for (const pipeline of production.production_settings.pipelines) { + for (const pipeline of production.pipelines) { for (const htmlSource of htmlSources) { const pipelineId = pipeline.pipeline_id; if (pipelineId !== undefined) { @@ -359,9 +354,8 @@ export async function stopProduction( } } } - if (mediaSources.length > 0 && mediaSources) { - for (const pipeline of production.production_settings.pipelines) { + for (const pipeline of production.pipelines) { for (const mediaSource of mediaSources) { const pipelineId = pipeline.pipeline_id; if (pipelineId !== undefined) { @@ -509,9 +503,8 @@ export async function stopProduction( export async function startProduction( production: Production ): Promise> { - const { production_settings } = production; Log().info( - `Starting production '${production.name}' with preset '${production_settings.name}'` + `Starting production '${production.name}' with preset '${production.preset_name}'` ); await initDedicatedPorts(); @@ -541,17 +534,15 @@ export async function startProduction( throw "Can't get source!"; }); - // Lookup pipeline UUIDs from pipeline names and insert to production_settings - await insertPipelineUuid(production_settings).catch((error) => { + // Lookup pipeline UUIDs from pipeline names and insert to production + await insertPipelineUuid(production).catch((error) => { throw error; }); // Fetch expanded pipeline objects from Ateliere Live - const pipelinesToUsePromises = production_settings.pipelines.map( - (pipeline) => { - return getPipeline(pipeline.pipeline_id!); - } - ); + const pipelinesToUsePromises = production.pipelines.map((pipeline) => { + return getPipeline(pipeline.pipeline_id!); + }); const pipelinesToUse = await Promise.all(pipelinesToUsePromises); // Check if pipelines are already in use by another production @@ -570,11 +561,9 @@ export async function startProduction( )}`; } - const resetPipelinePromises = production_settings.pipelines.map( - (pipeline) => { - return resetPipeline(pipeline.pipeline_id!); - } - ); + const resetPipelinePromises = production.pipelines.map((pipeline) => { + return resetPipeline(pipeline.pipeline_id!); + }); await Promise.all(resetPipelinePromises).catch((error) => { throw `Failed to reset pipelines: ${error}`; }); @@ -583,8 +572,8 @@ export async function startProduction( const allControlPanels = await getControlPanels(); // Check which control panels that should be used by this production const controlPanelsToUse = allControlPanels.filter((controlPanel) => - production.production_settings.control_connection.control_panel_name?.includes( - controlPanel.name + production.control_connection?.control_panel_ids?.includes( + controlPanel.uuid ) ); // Check if control panels are already in use by another production @@ -606,7 +595,7 @@ export async function startProduction( // TODO: Is this really neccessary? We have checked that pipeline and controlpanels are not in use by another production // can they still be in a state where we need to stop them? Removing streams and multiviews might be left to do though.. await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines during startup: ${error}`; }); @@ -619,13 +608,13 @@ export async function startProduction( ); streams = await connectIngestSources( production.sources, - production_settings, + production, sources, usedPorts ).catch(async (error) => { Log().error('Stopping pipelines'); await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines after production start failure: ${error}`; }); @@ -653,19 +642,21 @@ export async function startProduction( // Try to connect control panels and pipeline-to-pipeline connections start try { - // TODO: This will re-fetch pipelines from the Ateliere Live API, but we fetched them above into pipelinesToUse - await connectControlPanelToPipeline( - production_settings.control_connection, - production_settings.pipelines - ).catch(async (error) => { - Log().error('Stopping pipelines'); - await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) - ).catch((error) => { - throw `Failed to stop pipelines after production start failure: ${error}`; + if (production.control_connection) { + // TODO: This will re-fetch pipelines from the Ateliere Live API, but we fetched them above into pipelinesToUse + await connectControlPanelToPipeline( + production.control_connection, + production.pipelines + ).catch(async (error) => { + Log().error('Stopping pipelines'); + await stopPipelines( + production.pipelines.map((pipeline) => pipeline.pipeline_id!) + ).catch((error) => { + throw `Failed to stop pipelines after production start failure: ${error}`; + }); + throw error; }); - throw error; - }); + } } catch (error) { Log().error('Could not setup control panels'); Log().error(error); @@ -693,15 +684,15 @@ export async function startProduction( // Try to setup pipeline outputs start try { - for (const pipeline of production_settings.pipelines) { - await createPipelineOutputs(pipeline); + for (const [index, pipeline] of production.pipelines.entries()) { + await createPipelineOutputs(pipeline, production.outputs[index]); } } catch (e) { Log().error('Could not setup pipeline outputs'); Log().error(e); Log().error('Stopping pipelines'); await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines after production start failure: ${error}`; }); @@ -732,7 +723,7 @@ export async function startProduction( // Try to setup pipeline outputs end // Try to setup multiviews start try { - if (!production.production_settings.pipelines[0].multiviews) { + if (!production.multiviews) { Log().error( `No multiview settings specified for production: ${production.name}` ); @@ -740,31 +731,30 @@ export async function startProduction( } const runtimeMultiviews = await createMultiviewForPipeline( - production_settings, - production.sources + production ).catch(async (error) => { Log().error( - `Failed to create multiview for pipeline '${production_settings.pipelines[0].pipeline_name}/${production_settings.pipelines[0].pipeline_id}'`, + `Failed to create multiview for pipeline '${production.pipelines[0].pipeline_readable_name}/${production.pipelines[0].pipeline_id}'`, error ); Log().error('Stopping pipelines'); await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines after production start failure: ${error}`; }); - throw `Failed to create multiview for pipeline '${production_settings.pipelines[0].pipeline_name}/${production_settings.pipelines[0].pipeline_id}': ${error}`; + throw `Failed to create multiview for pipeline '${production.pipelines[0].pipeline_readable_name}/${production.pipelines[0].pipeline_id}': ${error}`; }); runtimeMultiviews.flatMap((runtimeMultiview, index) => { - const multiview = production.production_settings.pipelines[0].multiviews; + const multiview = production.pipelines[0].multiviews; if (multiview && multiview[index]) { return (multiview[index].multiview_id = runtimeMultiview.id); } }); Log().info( - `Production '${production.name}' with preset '${production_settings.name}' started` + `Production '${production.name}' with preset '${production.name}' started` ); } catch (e) { Log().error('Could not start multiviews'); @@ -812,7 +802,7 @@ export async function startProduction( input_slot: htmlSource.input_slot }; await createPipelineHtmlSource( - production, + production.pipelines, htmlSource.input_slot, htmlData, htmlSource @@ -828,7 +818,7 @@ export async function startProduction( input_slot: mediaSource.input_slot }; await createPipelineMediaSource( - production, + production.pipelines, mediaSource.input_slot, mediaData, mediaSource @@ -849,6 +839,7 @@ export async function startProduction( // Store updated production in database await putProduction(production._id.toString(), { ...production, + isActive: true, sources: production.sources.map((source) => { const streamsForSource = streams?.filter( (stream) => stream.source_id === source._id?.toString() @@ -860,46 +851,41 @@ export async function startProduction( input_slot: source.input_slot }; }), - production_settings: { - ...production.production_settings, - pipelines: await Promise.all( - production.production_settings.pipelines.map(async (pipeline) => { - const newSources = await Promise.all( - sourcesWithId.map(async (source) => { - const ingestUuid = await getUuidFromIngestName( - source.ingest_name - ); - const sourceId = await getSourceIdFromSourceName( - ingestUuid || '', - source.ingest_source_name - ); - - const currentSettings = pipeline.sources?.find( - (s) => s.source_id === sourceId - )?.settings; - - return { - source_id: sourceId || 0, - settings: { - alignment_ms: - currentSettings?.alignment_ms ?? pipeline.alignment_ms, - max_network_latency_ms: - currentSettings?.max_network_latency_ms ?? - pipeline.max_network_latency_ms - } - }; - }) - ); - return { ...pipeline, sources: newSources }; - }) - ) - }, - isActive: true + pipelines: await Promise.all( + production.pipelines.map(async (pipeline) => { + const newSources = await Promise.all( + sourcesWithId.map(async (source) => { + const ingestUuid = await getUuidFromIngestName( + source.ingest_name + ); + const sourceId = await getSourceIdFromSourceName( + ingestUuid || '', + source.ingest_source_name + ); + const currentSettings = pipeline.sources?.find( + (s) => s.source_id === sourceId + )?.settings; + + return { + source_id: sourceId || 0, + settings: { + alignment_ms: + currentSettings?.alignment_ms ?? pipeline.alignment_ms, + max_network_latency_ms: + currentSettings?.max_network_latency_ms ?? + pipeline.max_network_latency_ms + } + }; + }) + ); + return { ...pipeline, sources: newSources }; + }) + ) }).catch(async (error) => { Log().error(error); Log().error('Stopping pipelines'); await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines after production start failure: ${error}`; }); @@ -973,7 +959,7 @@ export async function startProduction( } catch (error) { Log().error('Stopping pipelines, error: ', error); await stopPipelines( - production_settings.pipelines.map((pipeline) => pipeline.pipeline_id!) + production.pipelines.map((pipeline) => pipeline.pipeline_id!) ).catch((error) => { throw `Failed to stop pipelines after production start failure: ${error}`; }); @@ -1003,7 +989,7 @@ export async function postMultiviewersOnRunningProduction( additions: MultiviewSettings[] ) { try { - const multiview = production.production_settings.pipelines[0].multiviews; + const multiview = production.multiviews; if (!multiview) { Log().error( `No multiview settings specified for production: ${production.name}` @@ -1011,25 +997,16 @@ export async function postMultiviewersOnRunningProduction( throw `No multiview settings specified for production: ${production.name}`; } - const productionSettings = { - ...production.production_settings, - pipelines: production.production_settings.pipelines.map((pipeline) => { - return { - ...pipeline, - multiviews: additions - }; - }) - }; - + const newProduction = cloneDeep(production); + newProduction.multiviews = additions; const runtimeMultiviews = await createMultiviewForPipeline( - productionSettings, - production.sources + newProduction ).catch(async (error) => { Log().error( - `Failed to create multiview for pipeline '${productionSettings.pipelines[0].pipeline_name}/${productionSettings.pipelines[0].pipeline_id}'`, + `Failed to create multiview for pipeline '${newProduction.pipelines[0].pipeline_name}/${newProduction.pipelines[0].pipeline_id}'`, error ); - throw `Failed to create multiview for pipeline '${productionSettings.pipelines[0].pipeline_name}/${productionSettings.pipelines[0].pipeline_id}': ${error}`; + throw `Failed to create multiview for pipeline '${newProduction.pipelines[0].pipeline_name}/${newProduction.pipelines[0].pipeline_id}': ${error}`; }); const multiviewsWithUpdatedId: MultiviewSettings[] = [ @@ -1044,18 +1021,10 @@ export async function postMultiviewersOnRunningProduction( await putProduction(production._id.toString(), { ...production, - production_settings: { - ...production.production_settings, - pipelines: production.production_settings.pipelines.map((pipeline) => { - return { - ...pipeline, - multiviews: multiviewsWithUpdatedId - }; - }) - } + multiviews: multiviewsWithUpdatedId }).catch(async (error) => { Log().error( - `Failed to save multiviews for pipeline '${productionSettings.pipelines[0].pipeline_name}/${productionSettings.pipelines[0].pipeline_id}' to database`, + `Failed to save multiviews for pipeline '${newProduction.pipelines[0].pipeline_name}/${newProduction.pipelines[0].pipeline_id}' to database`, error ); throw error; @@ -1116,12 +1085,9 @@ export async function putMultiviewersOnRunningProduction( updates.map(async (multiview) => { const views = multiview.layout.views; - if ( - multiview.multiview_id && - production.production_settings.pipelines[0].pipeline_id - ) { + if (multiview.multiview_id && production.pipelines[0].pipeline_id) { await updateMultiviewForPipeline( - production.production_settings.pipelines[0].pipeline_id, + production.pipelines[0].pipeline_id, multiview.multiview_id, views ); @@ -1180,18 +1146,15 @@ export async function deleteMultiviewersOnRunningProduction( removals: MultiviewSettings[] ) { try { - const pipeline = production.production_settings.pipelines.find((p) => - p.multiviews ? p.multiviews?.length > 0 : undefined - ); - const multiviewIndexArray = pipeline?.multiviews - ? pipeline.multiviews.map((p) => p.for_pipeline_idx) + const multiviewIndexArray = production.multiviews + ? production.multiviews.map((p) => p.for_pipeline_idx) : undefined; const multiviewIndex = multiviewIndexArray?.find((p) => p !== undefined); const pipelineUUID = multiviewIndex !== undefined - ? production.production_settings.pipelines[multiviewIndex].pipeline_id + ? production.pipelines[multiviewIndex].pipeline_id : undefined; if (!pipelineUUID) return; diff --git a/src/app/api/manager/productions/[id]/route.ts b/src/app/api/manager/productions/[id]/route.ts index 5f825f88..54994a69 100644 --- a/src/app/api/manager/productions/[id]/route.ts +++ b/src/app/api/manager/productions/[id]/route.ts @@ -25,8 +25,9 @@ export async function GET( const production = await getProduction(params.id); const prod = { ...production, - sources: production.sources.sort((a, b) => a.input_slot - b.input_slot), - _id: production._id.toString() + sources: + production?.sources.sort((a, b) => a.input_slot - b.input_slot) || [], + _id: production?._id.toString() }; return new NextResponse(JSON.stringify(prod), { status: 200 }); } catch (error) { diff --git a/src/app/api/manager/rendering-engine/html/route.ts b/src/app/api/manager/rendering-engine/html/route.ts index e643fbbe..ee35f93d 100644 --- a/src/app/api/manager/rendering-engine/html/route.ts +++ b/src/app/api/manager/rendering-engine/html/route.ts @@ -5,9 +5,10 @@ import { Log } from '../../../../../api/logger'; import { HTMLSource } from '../../../../../interfaces/renderingEngine'; import { Production } from '../../../../../interfaces/production'; import { SourceReference } from '../../../../../interfaces/Source'; +import { PipelineSettings } from '../../../../../interfaces/pipeline'; export type CreateHtmlRequestBody = { - production: Production; + pipelines: PipelineSettings[]; htmlBody: HTMLSource; inputSlot: number; source: SourceReference; @@ -24,7 +25,7 @@ export async function POST(request: NextRequest): Promise { const createHtmlRequest = data as CreateHtmlRequestBody; return await createPipelineHtmlSource( - createHtmlRequest.production, + createHtmlRequest.pipelines, createHtmlRequest.inputSlot, createHtmlRequest.htmlBody, createHtmlRequest.source diff --git a/src/app/api/manager/rendering-engine/media/route.ts b/src/app/api/manager/rendering-engine/media/route.ts index f21e1c5a..3e7ddaef 100644 --- a/src/app/api/manager/rendering-engine/media/route.ts +++ b/src/app/api/manager/rendering-engine/media/route.ts @@ -5,9 +5,10 @@ import { Log } from '../../../../../api/logger'; import { MediaSource } from '../../../../../interfaces/renderingEngine'; import { Production } from '../../../../../interfaces/production'; import { SourceReference } from '../../../../../interfaces/Source'; +import { PipelineSettings } from '../../../../../interfaces/pipeline'; export type CreateMediaRequestBody = { - production: Production; + pipelines: PipelineSettings[]; mediaBody: MediaSource; inputSlot: number; source: SourceReference; @@ -24,7 +25,7 @@ export async function POST(request: NextRequest): Promise { const createMediaRequest = data as CreateMediaRequestBody; return await createPipelineMediaSource( - createMediaRequest.production, + createMediaRequest.pipelines, createMediaRequest.inputSlot, createMediaRequest.mediaBody, createMediaRequest.source diff --git a/src/app/api/manager/streams/route.ts b/src/app/api/manager/streams/route.ts index 689dd0f6..d89913aa 100644 --- a/src/app/api/manager/streams/route.ts +++ b/src/app/api/manager/streams/route.ts @@ -4,9 +4,10 @@ import { SourceWithId } from '../../../../interfaces/Source'; import { Production } from '../../../../interfaces/production'; import { createStream } from '../../../../api/ateliereLive/pipelines/streams/streams'; import { Log } from '../../../../api/logger'; +import { PipelineSettings } from '../../../../interfaces/pipeline'; export type CreateStreamRequestBody = { source: SourceWithId; - production: Production; + pipelines: PipelineSettings[]; input_slot: number; }; export async function POST(request: NextRequest): Promise { @@ -21,7 +22,7 @@ export async function POST(request: NextRequest): Promise { return await createStream( createStreamRequest.source, - createStreamRequest.production, + createStreamRequest.pipelines, createStreamRequest.input_slot ) .then((response) => { diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 57986804..8122166c 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,1243 +1,6 @@ -'use client'; -import React, { - useEffect, - useState, - KeyboardEvent, - useContext, - useMemo -} from 'react'; -import { PageProps } from '../../../../.next/types/app/production/[id]/page'; -import { AddInput } from '../../../components/addInput/AddInput'; -import { useSources } from '../../../hooks/sources/useSources'; -import { - AddSourceStatus, - DeleteSourceStatus, - SourceReference, - SourceWithId -} from '../../../interfaces/Source'; -import { - useGetProduction, - usePutProduction, - useReplaceProductionSourceStreamIds -} from '../../../hooks/productions'; -import { Production } from '../../../interfaces/production'; -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 { useGetPresets } from '../../../hooks/presets'; -import { Preset } from '../../../interfaces/preset'; -import SourceCards from '../../../components/sourceCards/SourceCards'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { DndProvider } from 'react-dnd'; -import { PresetDropdown } from '../../../components/startProduction/presetDropdown'; -import { StartProductionButton } from '../../../components/startProduction/StartProductionButton'; -import { ConfigureOutputButton } from '../../../components/startProduction/ConfigureOutputButton'; -import toast from 'react-hot-toast'; -import { useTranslate } from '../../../i18n/useTranslate'; -import { usePipelines } from '../../../hooks/pipelines'; -import { useControlPanels } from '../../../hooks/controlPanels'; -import PipelineNameDropDown from '../../../components/dropDown/PipelineNameDropDown'; -import ControlPanelDropDown from '../../../components/dropDown/ControlPanelDropDown'; -import { Pipelines } from '../../../components/pipeline/Pipelines'; -import { AddSourceModal } from '../../../components/modal/AddSourceModal'; -import { RemoveSourceModal } from '../../../components/modal/RemoveSourceModal'; -import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; -import { MonitoringButton } from '../../../components/button/MonitoringButton'; -import { useGetMultiviewLayout } from '../../../hooks/multiviewLayout'; -import { useMultiviews } from '../../../hooks/multiviews'; -import SourceList from '../../../components/sourceList/SourceList'; -import { GlobalContext } from '../../../contexts/GlobalContext'; -import { Select } from '../../../components/select/Select'; -import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; -import { ConfigureMultiviewButton } from '../../../components/modal/configureMultiviewModal/ConfigureMultiviewButton'; -import { useUpdateSourceInputSlotOnMultiviewLayouts } from '../../../hooks/useUpdateSourceInputSlotOnMultiviewLayouts'; -import { useCheckProductionPipelines } from '../../../hooks/useCheckProductionPipelines'; -import { ISource } from '../../../hooks/useDragableItems'; -import { useUpdateStream } from '../../../hooks/streams'; -import { usePutProductionPipelineSourceAlignmentAndLatency } from '../../../hooks/productions'; -import { useIngestSourceId } from '../../../hooks/ingests'; -import cloneDeep from 'lodash.clonedeep'; -import { - useAddMultiviewersOnRunningProduction, - useRemoveMultiviewersOnRunningProduction, - useUpdateMultiviewersOnRunningProduction -} from '../../../hooks/workflow'; -import { MultiviewSettings } from '../../../interfaces/multiview'; -import { CreateHtmlModal } from '../../../components/modal/renderingEngineModals/CreateHtmlModal'; -import { CreateMediaModal } from '../../../components/modal/renderingEngineModals/CreateMediaModal'; -import { useDeleteHtmlSource } from '../../../hooks/renderingEngine/useDeleteHtmlSource'; -import { useDeleteMediaSource } from '../../../hooks/renderingEngine/useDeleteMediaSource'; -import { useCreateHtmlSource } from '../../../hooks/renderingEngine/useCreateHtmlSource'; -import { useCreateMediaSource } from '../../../hooks/renderingEngine/useCreateMediaSource'; -import { useRenderingEngine } from '../../../hooks/renderingEngine/useRenderingEngine'; +import ProductionPage from '../../../components/production/ProductionPage'; -export default function ProductionConfiguration({ params }: PageProps) { - const t = useTranslate(); - - //SOURCES - const [sources] = useSources(); - const [selectedValue, setSelectedValue] = useState( - t('production.add_other_source_type') - ); - const [addSourceModal, setAddSourceModal] = useState(false); - const [removeSourceModal, setRemoveSourceModal] = useState(false); - const [selectedSource, setSelectedSource] = useState< - SourceWithId | undefined - >(); - const [selectedSourceRef, setSelectedSourceRef] = useState< - SourceReference | undefined - >(); - const [createStream, loadingCreateStream] = useCreateStream(); - const [deleteStream, loadingDeleteStream] = useDeleteStream(); - - //PRODUCTION - const putProduction = usePutProduction(); - const getPresets = useGetPresets(); - const getProduction = useGetProduction(); - const replaceProductionSourceStreamIds = - useReplaceProductionSourceStreamIds(); - const [configurationName, setConfigurationName] = useState(''); - const [productionSetup, setProductionSetup] = useState(); - const [presets, setPresets] = useState(); - const [selectedPreset, setSelectedPreset] = useState(); - const selectedProductionItems = - productionSetup?.sources.map((prod) => prod._id) || []; - - //MULTIVIEWS - const getMultiviewLayout = useGetMultiviewLayout(); - const [updateMultiviewViews] = useMultiviews(); - const [updateSourceInputSlotOnMultiviewLayouts, updateMultiviewViewsLoading] = - useUpdateSourceInputSlotOnMultiviewLayouts(); - const [addMultiviewersOnRunningProduction] = - useAddMultiviewersOnRunningProduction(); - const [updateMultiviewersOnRunningProduction] = - useUpdateMultiviewersOnRunningProduction(); - const [removeMultiviewersOnRunningProduction] = - useRemoveMultiviewersOnRunningProduction(); - - //FROM LIVE API - const [pipelines, loadingPipelines, , refreshPipelines] = usePipelines(); - const [controlPanels, loadingControlPanels, , refreshControlPanels] = - useControlPanels(); - - //UI STATE - const [inventoryVisible, setInventoryVisible] = useState(false); - const [isPresetDropdownHidden, setIsPresetDropdownHidden] = useState(true); - const [addSourceStatus, setAddSourceStatus] = useState(); - const [deleteSourceStatus, setDeleteSourceStatus] = - useState(); - const [isHtmlModalOpen, setIsHtmlModalOpen] = useState(false); - const [isMediaModalOpen, setIsMediaModalOpen] = useState(false); - - // Create source - const [firstEmptySlot] = useGetFirstEmptySlot(); - - const [updateStream, loading] = useUpdateStream(); - const [getIngestSourceId, ingestSourceIdLoading] = useIngestSourceId(); - - const putProductionPipelineSourceAlignmentAndLatency = - usePutProductionPipelineSourceAlignmentAndLatency(); - - const [checkProductionPipelines] = useCheckProductionPipelines(); - - // Rendering engine - const [deleteHtmlSource, deleteHtmlLoading] = useDeleteHtmlSource(); - const [deleteMediaSource, deleteMediaLoading] = useDeleteMediaSource(); - const [createHtmlSource, createHtmlLoading] = useCreateHtmlSource(); - const [createMediaSource, createMediaLoading] = useCreateMediaSource(); - const [getRenderingEngine, renderingEngineLoading] = useRenderingEngine(); - - const { locked } = useContext(GlobalContext); - - const memoizedProduction = useMemo(() => productionSetup, [productionSetup]); - - const isAddButtonDisabled = - (selectedValue !== 'HTML' && selectedValue !== 'Media Player') || locked; - - useEffect(() => { - refreshPipelines(); - refreshControlPanels(); - }, [productionSetup?.isActive]); - - const setSelectedControlPanel = (controlPanel: string[]) => { - setProductionSetup((prevState) => { - if (!prevState) return; - putProduction(prevState._id, { - ...prevState, - production_settings: { - ...prevState?.production_settings, - control_connection: { - ...prevState?.production_settings?.control_connection, - control_panel_name: controlPanel - } - } - }); - return { - ...prevState, - production_settings: { - ...prevState?.production_settings, - control_connection: { - ...prevState?.production_settings?.control_connection, - control_panel_name: controlPanel - } - } - }; - }); - }; - - const setSelectedPipelineName = ( - pipelineIndex: number, - pipelineName?: string, - id?: string - ) => { - const selectedPresetCopy = cloneDeep(selectedPreset); - const foundPipeline = selectedPresetCopy?.pipelines[pipelineIndex]; - if (foundPipeline) { - foundPipeline.outputs = []; - foundPipeline.pipeline_name = pipelineName; - } - setSelectedPreset(selectedPresetCopy); - setProductionSetup((prevState) => { - const updatedPipelines = prevState?.production_settings.pipelines; - if (!updatedPipelines) return; - updatedPipelines[pipelineIndex].pipeline_name = pipelineName; - updatedPipelines[pipelineIndex].pipeline_id = id; - updatedPipelines[pipelineIndex].outputs = []; - putProduction(prevState._id, { - ...prevState, - production_settings: { - ...prevState?.production_settings, - pipelines: updatedPipelines - } - }); - return { - ...prevState, - production_settings: { - ...prevState?.production_settings, - pipelines: updatedPipelines - } - }; - }); - }; - - const refreshProduction = () => { - getProduction(params.id).then((config) => { - // check if production has pipelines in use or control panels in use, if so update production - const production = config.isActive - ? config - : checkProductionPipelines(config, pipelines); - - putProduction(production._id, production); - setProductionSetup(production); - setConfigurationName(production.name); - setSelectedPreset(production.production_settings); - getPresets().then((presets) => { - if (!production.production_settings) { - setPresets(presets); - } else { - const presetsExludingProductionSettings = presets.filter( - (preset) => preset._id !== production?.production_settings._id - ); - setPresets([ - ...presetsExludingProductionSettings, - production.production_settings - ]); - } - }); - }); - }; - - useEffect(() => { - refreshProduction(); - }, []); - - useEffect(() => { - if (selectedValue === t('production.source')) { - setInventoryVisible(true); - } - }, [selectedValue]); - - const updatePreset = (preset: Preset) => { - if (!productionSetup?._id) return; - - const presetMultiviews = preset.pipelines[0].multiviews; - const productionMultiviews = - productionSetup.production_settings.pipelines[0].multiviews; - - const updatedPreset = { - ...productionSetup, - production_settings: { - ...preset, - control_connection: { - ...preset.control_connection, - control_panel_name: - productionSetup.production_settings.control_connection - .control_panel_name - }, - pipelines: preset.pipelines.map((p, i) => { - return { - ...p, - pipeline_name: - productionSetup.production_settings.pipelines[i].pipeline_name - }; - }) - } - }; - - if (productionSetup.isActive && presetMultiviews && productionMultiviews) { - const productionMultiviewsMap = new Map( - productionMultiviews.map((item) => [item.multiview_id, item]) - ); - const presetMultiviewsMap = new Map( - presetMultiviews.map((item) => [item.multiview_id, item]) - ); - - const additions: MultiviewSettings[] = []; - const updates: MultiviewSettings[] = []; - - presetMultiviews.forEach((newItem) => { - const oldItem = productionMultiviewsMap.get(newItem.multiview_id); - - if (!oldItem) { - additions.push(newItem); - } else if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) { - updates.push(newItem); - } - }); - - const removals = productionMultiviews.filter( - (oldItem) => !presetMultiviewsMap.has(oldItem.multiview_id) - ); - - if (additions.length > 0) { - addMultiviewersOnRunningProduction( - (productionSetup?._id.toString(), updatedPreset), - additions - ); - } - - if (updates.length > 0) { - updateMultiviewersOnRunningProduction( - (productionSetup?._id.toString(), updatedPreset), - updates - ); - } - - if (removals.length > 0) { - removeMultiviewersOnRunningProduction( - (productionSetup?._id.toString(), updatedPreset), - removals - ); - } - } - - putProduction(productionSetup?._id.toString(), updatedPreset).then(() => { - refreshProduction(); - }); - }; - - const updateProduction = (id: string, productionSetup: Production) => { - setProductionSetup(productionSetup); - putProduction(id, productionSetup); - }; - - const handleSetPipelineSourceSettings = async ( - source: ISource, - sourceId: number, - data: { - pipeline_uuid: string; - stream_uuid: string; - alignment: number; - latency: number; - }[], - shouldRestart?: boolean, - streamUuids?: string[] - ) => { - if ( - productionSetup?._id && - source?.ingest_name && - source?.ingest_source_name - ) { - data.forEach(({ pipeline_uuid, stream_uuid, alignment, latency }) => { - putProductionPipelineSourceAlignmentAndLatency( - productionSetup._id, - pipeline_uuid, - source.ingest_name, - source.ingest_source_name, - alignment, - latency - ).then(() => refreshProduction()); - - if (productionSetup.isActive) { - updateStream(stream_uuid, alignment); - } - - const updatedProduction = { - ...productionSetup, - productionSettings: { - ...productionSetup.production_settings, - pipelines: productionSetup.production_settings.pipelines.map( - (pipeline) => { - if (pipeline.pipeline_id === pipeline_uuid) { - pipeline.sources?.map((source) => { - if (source.source_id === sourceId) { - source.settings.alignment_ms = alignment; - source.settings.max_network_latency_ms = latency; - } - }); - } - return pipeline; - } - ) - } - }; - - setProductionSetup(updatedProduction); - }); - } - - if (shouldRestart && productionSetup && streamUuids) { - const sourceToDeleteFrom = productionSetup.sources.find((source) => - source.stream_uuids?.includes(streamUuids[0]) - ); - deleteStream(streamUuids, productionSetup, sourceId) - .then(() => { - delete sourceToDeleteFrom?.stream_uuids; - }) - .then(() => - setTimeout(async () => { - const result = await createStream( - source, - productionSetup, - source.input_slot - ); - if (result.ok) { - if (result.value.success) { - const newStreamUuids = result.value.streams.map( - (r) => r.stream_uuid - ); - if (sourceToDeleteFrom?._id) { - replaceProductionSourceStreamIds( - productionSetup._id, - sourceToDeleteFrom?._id, - newStreamUuids - ).then(() => refreshProduction()); - } - } - } - }, 1500) - ); - } - }; - - const updateMultiview = ( - source: SourceReference, - updatedSetup: Production - ) => { - const pipeline = updatedSetup.production_settings.pipelines[0]; - - pipeline.multiviews?.map((singleMultiview) => { - if ( - pipeline.pipeline_id && - pipeline.multiviews && - singleMultiview.multiview_id - ) { - updateMultiviewViews( - pipeline.pipeline_id, - updatedSetup, - source, - singleMultiview - ); - } - }); - }; - - const updateSource = ( - source: SourceReference, - productionSetup: Production - ) => { - const updatedSetup = updateSetupItem(source, productionSetup); - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup); - updateMultiview(source, updatedSetup); - }; - - const updateConfigName = (nameChange: string) => { - if (productionSetup?.name === nameChange) { - return; - } - setConfigurationName(nameChange); - const updatedSetup = { - ...productionSetup, - name: nameChange - } as Production; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup); - }; - - async function updateSelectedPreset(preset?: Preset) { - if (!preset && productionSetup?._id) { - getPresets().then((presets) => { - setPresets(presets); - }); - setSelectedPreset(undefined); - return; - } - if (!preset?.default_multiview_reference) { - toast.error(t('production.missing_multiview')); - return; - } - const defaultMultiview = await getMultiviewLayout( - preset?.default_multiview_reference - ); - setSelectedPreset(preset); - - const multiview = { - layout: defaultMultiview.layout, - output: defaultMultiview.output, - name: defaultMultiview.name, - for_pipeline_idx: 0 - }; - setIsPresetDropdownHidden(true); - let controlPanelName: string[] = []; - if ( - productionSetup?.production_settings && - productionSetup?.production_settings.control_connection.control_panel_name - ) { - // Keep the control panels name array from the current production setup - controlPanelName = - productionSetup.production_settings.control_connection - .control_panel_name!; - } - - const updatedSetup = { - ...productionSetup, - production_settings: { - _id: preset._id, - name: preset.name, - control_connection: { - control_panel_endpoint: - preset.control_connection.control_panel_endpoint, - pipeline_control_connections: - preset.control_connection.pipeline_control_connections, - control_panel_name: controlPanelName - }, - pipelines: preset.pipelines - } - } as Production; - updatedSetup.production_settings.pipelines[0].multiviews = [multiview]; - setProductionSetup(updatedSetup); - } - - function addPresetComponent(preset: Preset, index: number) { - const id = `${preset.name}-${index}-id`; - return ( -
  • { - updateSelectedPreset(preset); - }} - > -
    - -
    -
  • - ); - } - - const addSourceAction = async (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) - }; - - let updatedSetup = addSetupItem(input, productionSetup); - - if (!updatedSetup) return; - - updatedSetup = await updatePipelinesWithSource(updatedSetup, source); - - setProductionSetup(updatedSetup); - await putProduction(updatedSetup._id.toString(), updatedSetup); - - setAddSourceModal(false); - setSelectedSource(undefined); - } - }; - - const updatePipelinesWithSource = async ( - productionSetup: Production, - source: SourceWithId - ): Promise => { - const updatedPipelines = await Promise.all( - productionSetup.production_settings.pipelines.map(async (pipeline) => { - const newSource = { - source_id: await getIngestSourceId( - source.ingest_name, - source.ingest_source_name - ), - settings: { - alignment_ms: pipeline.alignment_ms, - max_network_latency_ms: pipeline.max_network_latency_ms - } - }; - - const exists = pipeline.sources?.some( - (s) => s.source_id === newSource.source_id - ); - - const updatedSources = exists - ? pipeline.sources - : [...(pipeline.sources || []), newSource]; - - return { - ...pipeline, - sources: updatedSources - }; - }) - ); - - return { - ...productionSetup, - production_settings: { - ...productionSetup.production_settings, - pipelines: updatedPipelines - } - }; - }; - - const addHtmlSource = (height: number, width: number, url: string) => { - if (productionSetup) { - const sourceToAdd: SourceReference = { - type: 'html', - label: `HTML ${firstEmptySlot(productionSetup)}`, - input_slot: firstEmptySlot(productionSetup), - html_data: { - height: height, - url: url, - width: width - } - }; - const updatedSetup = addSetupItem(sourceToAdd, productionSetup); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - refreshProduction(); - }); - - if (productionSetup?.isActive && sourceToAdd.html_data) { - createHtmlSource( - productionSetup, - sourceToAdd.input_slot, - sourceToAdd.html_data, - sourceToAdd - ); - } - } - }; - - const addMediaSource = (filename: string) => { - if (productionSetup) { - const sourceToAdd: SourceReference = { - type: 'mediaplayer', - label: `Media Player ${firstEmptySlot(productionSetup)}`, - input_slot: firstEmptySlot(productionSetup), - media_data: { - filename: filename - } - }; - const updatedSetup = addSetupItem(sourceToAdd, productionSetup); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - refreshProduction(); - }); - - if (productionSetup?.isActive && sourceToAdd.media_data) { - createMediaSource( - productionSetup, - sourceToAdd.input_slot, - sourceToAdd.media_data, - sourceToAdd - ); - } - } - }; - - const isDisabledFunction = (source: SourceWithId): boolean => { - return selectedProductionItems?.includes(source._id.toString()); - }; - - const handleOpenModal = (type: 'html' | 'media') => { - if (type === 'html') { - setIsHtmlModalOpen(true); - } else if (type === 'media') { - setIsMediaModalOpen(true); - } - }; - - const handleAddSource = async () => { - setAddSourceStatus(undefined); - if ( - productionSetup && - productionSetup.isActive && - selectedSource && - (Array.isArray( - productionSetup?.production_settings.pipelines[0].multiviews - ) - ? productionSetup.production_settings.pipelines[0].multiviews.some( - (singleMultiview) => singleMultiview?.layout?.views - ) - : false) - ) { - let updatedSetup = productionSetup; - - for ( - let i = 0; - i < productionSetup.production_settings.pipelines.length; - i++ - ) { - const pipeline = productionSetup.production_settings.pipelines[i]; - - if (!pipeline.sources) { - pipeline.sources = []; - } - - const newSource = { - source_id: await getIngestSourceId( - selectedSource.ingest_name, - selectedSource.ingest_source_name - ), - settings: { - alignment_ms: pipeline.alignment_ms, - max_network_latency_ms: pipeline.max_network_latency_ms - } - }; - - updatedSetup = { - ...productionSetup, - production_settings: { - ...productionSetup.production_settings, - pipelines: productionSetup.production_settings.pipelines.map( - (p, index) => { - if (index === i) { - if (!p.sources) { - p.sources = []; - } - p.sources.push(newSource); - } - return p; - } - ) - } - } as Production; - } - - const result = await createStream( - selectedSource, - updatedSetup, - firstEmptySlot(productionSetup) - ); - if (!result.ok) { - if (!result.value) { - setAddSourceStatus({ - success: false, - steps: [{ step: 'add_stream', success: false }] - }); - } else { - setAddSourceStatus({ - success: false, - steps: result.value.steps - }); - } - } - if (result.ok) { - if (result.value.success) { - 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: firstEmptySlot(productionSetup) - }; - const updatedSetup = addSetupItem(sourceToAdd, productionSetup); - if (!updatedSetup) return; - updateSourceInputSlotOnMultiviewLayouts(updatedSetup).then( - (result) => { - if (!result) return; - setProductionSetup(result); - updateMultiview(sourceToAdd, result); - refreshProduction(); - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); - setAddSourceStatus(undefined); - } else { - setAddSourceStatus({ success: false, steps: result.value.steps }); - } - } - } - }; - - const handleRemoveSource = async () => { - if (productionSetup && productionSetup.isActive && selectedSourceRef) { - const multiviews = - productionSetup.production_settings.pipelines[0].multiviews; - - if (!multiviews || multiviews.length === 0) return; - - const viewToUpdate = multiviews.some((multiview) => - multiview.layout.views.find( - (v) => v.input_slot === selectedSourceRef.input_slot - ) - ); - - if ( - selectedSourceRef.stream_uuids && - selectedSourceRef.stream_uuids.length > 0 - ) { - 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; - updateSourceInputSlotOnMultiviewLayouts(updatedSetup).then( - (result) => { - if (!result) return; - setProductionSetup(updatedSetup); - updateMultiview(selectedSourceRef, result); - setSelectedSourceRef(undefined); - } - ); - return; - } - } - return; - } - - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - - if (!updatedSetup) return; - - updateSourceInputSlotOnMultiviewLayouts(updatedSetup).then( - (result) => { - if (!result) return; - setProductionSetup(updatedSetup); - updateMultiview(selectedSourceRef, result); - 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; - updateSourceInputSlotOnMultiviewLayouts(updatedSetup).then( - (result) => { - if (!result) return; - setProductionSetup(result); - updateMultiview(selectedSourceRef, result); - } - ); - return; - } - } - return; - } - } - - if ( - selectedSourceRef.type === 'html' || - selectedSourceRef.type === 'mediaplayer' - ) { - for ( - let i = 0; - i < productionSetup.production_settings.pipelines.length; - i++ - ) { - const pipelineId = - productionSetup.production_settings.pipelines[i].pipeline_id; - if (pipelineId) { - getRenderingEngine(pipelineId); - if (selectedSourceRef.type === 'html') { - await deleteHtmlSource( - pipelineId, - selectedSourceRef.input_slot, - productionSetup - ); - } else if (selectedSourceRef.type === 'mediaplayer') { - await deleteMediaSource( - pipelineId, - selectedSourceRef.input_slot, - productionSetup - ); - } - } - } - } - - const updatedSetup = removeSetupItem(selectedSourceRef, productionSetup); - - if (!updatedSetup) return; - updateSourceInputSlotOnMultiviewLayouts(updatedSetup).then((result) => { - if (!result) return; - setProductionSetup(result); - updateMultiview(selectedSourceRef, result); - setRemoveSourceModal(false); - setSelectedSourceRef(undefined); - }); - } - }; - - const handleAbortAddSource = () => { - setAddSourceStatus(undefined); - setAddSourceModal(false); - setSelectedSource(undefined); - }; - - const handleAbortRemoveSource = () => { - setRemoveSourceModal(false); - setSelectedSource(undefined); - setDeleteSourceStatus(undefined); - }; - - const hasSelectedPipelines = () => { - if (!productionSetup?.production_settings?.pipelines?.length) return false; - let allPipesHaveName = true; - productionSetup.production_settings.pipelines.forEach((p) => { - if (!p.pipeline_name) allPipesHaveName = false; - }); - return allPipesHaveName; - }; - - return ( - <> - - { - setConfigurationName(e.target.value); - }} - onKeyDown={(e: KeyboardEvent) => { - if (e.key.includes('Enter')) { - e.currentTarget.blur(); - } - }} - onBlur={() => updateConfigName(configurationName)} - disabled={locked} - /> -
    - { - updateSelectedPreset(undefined); - }} - > - {presets && - presets.map((item, index) => { - return addPresetComponent(item, index); - })} - - - - -
    -
    -
    -
    - setInventoryVisible(false)} - isDisabledFunc={isDisabledFunction} - locked={locked} - /> - {addSourceModal && (selectedSource || selectedSourceRef) && ( - - )} -
    -
    -
    - {productionSetup?.sources && sources.size > 0 && ( - - { - updateProduction(productionSetup._id, updated); - }} - onSourceUpdate={(source: SourceReference) => { - updateSource(source, productionSetup); - }} - onSourceRemoval={async ( - source: SourceReference, - ingestSource?: ISource - ) => { - if (productionSetup && productionSetup.isActive) { - setSelectedSourceRef(source); - setRemoveSourceModal(true); - } else if (productionSetup) { - const ingestSourceId = ingestSource - ? await getIngestSourceId( - ingestSource.ingest_name, - ingestSource.ingest_source_name - ) - : undefined; - const updatedSetup = removeSetupItem( - { - _id: source._id, - type: source.type, - label: source.label, - input_slot: source.input_slot - }, - productionSetup, - ingestSourceId - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction( - updatedSetup._id.toString(), - updatedSetup - ).then(() => { - setRemoveSourceModal(false); - setSelectedSourceRef(undefined); - }); - } - }} - loading={loading} - /> - {removeSourceModal && selectedSourceRef && ( - - )} - - )} -
    - setInventoryVisible(true)} - disabled={ - productionSetup?.production_settings === undefined || - productionSetup.production_settings === null || - locked - } - /> -
    - + handleInputChange( + pipeline.pipeline_id || '', + e.target.valueAsNumber, + 'alignment' + ) + } + /> +

    Latency (ms):

    + + handleInputChange( + pipeline.pipeline_id || '', + e.target.valueAsNumber, + 'latency' + ) + } + /> + {inputErrors[pipeline.pipeline_id || ''] && ( +

    + {t('configure_alignment_latency.error')}

    -

    Alignment (ms):

    - - handleInputChange( - pipeline.pipeline_id || '', - e.target.valueAsNumber, - 'alignment' - ) - } - /> -

    Latency (ms):

    - - handleInputChange( - pipeline.pipeline_id || '', - e.target.valueAsNumber, - 'latency' - ) - } - /> - {inputErrors[pipeline.pipeline_id || ''] && ( -

    - {t('configure_alignment_latency.error')} -

    - )} -
    - ) - )} + )} +
    + ))}
    - {preset && ( - - )} - - ); -} diff --git a/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx b/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx deleted file mode 100644 index 11b2fac3..00000000 --- a/src/components/modal/configureMultiviewModal/ConfigureMultiviewModal.tsx +++ /dev/null @@ -1,359 +0,0 @@ -import { TMultiviewLayout, Preset } from '../../../interfaces/preset'; -import { Modal } from '../Modal'; -import { useEffect, useState } from 'react'; -import { useTranslate } from '../../../i18n/useTranslate'; -import toast from 'react-hot-toast'; -import { MultiviewSettings } from '../../../interfaces/multiview'; -import { IconPlus, IconTrash } from '@tabler/icons-react'; -import { Production } from '../../../interfaces/production'; -import deepclone from 'lodash.clonedeep'; -import MultiviewSettingsConfig from './MultiviewSettings'; -import { usePutMultiviewLayout } from '../../../hooks/multiviewLayout'; -import Decision from '../configureOutputModal/Decision'; -import MultiviewLayoutSettings from './MultiviewLayoutSettings/MultiviewLayoutSettings'; -import { IconSettings } from '@tabler/icons-react'; -import { UpdateMultiviewersModal } from '../UpdateMultiviewersModal'; -import { Button } from '../../button/Button'; - -type ConfigureMultiviewModalProps = { - open: boolean; - preset: Preset; - onClose: () => void; - updatePreset: (preset: Preset) => void; - production?: Production; -}; - -export function ConfigureMultiviewModal({ - open, - preset, - onClose, - updatePreset, - production -}: ConfigureMultiviewModalProps) { - const [multiviews, setMultiviews] = useState([]); - const [portDuplicateIndexes, setPortDuplicateIndexes] = useState( - [] - ); - const [streamIdDuplicateIndexes, setStreamIdDuplicateIndexes] = useState< - number[] - >([]); - const [layoutModalOpen, setLayoutModalOpen] = useState(false); - const [refresh, setRefresh] = useState(false); - const [confirmUpdateModalOpen, setConfirmUpdateModalOpen] = useState(false); - const [newMultiviewLayout, setNewMultiviewLayout] = - useState(null); - const addNewLayout = usePutMultiviewLayout(); - const t = useTranslate(); - - useEffect(() => { - setRefresh(open); - }, [open]); - - useEffect(() => { - if (preset.pipelines[0].multiviews) { - if (!Array.isArray(preset.pipelines[0].multiviews)) { - setMultiviews([preset.pipelines[0].multiviews]); - } else { - setMultiviews(preset.pipelines[0].multiviews); - } - } - }, [preset.pipelines]); - - const clearInputs = () => { - setLayoutModalOpen(false); - setMultiviews(preset.pipelines[0].multiviews || []); - onClose(); - }; - - useEffect(() => { - runDuplicateCheck(multiviews); - }, [multiviews]); - - const onSave = () => { - if (production?.isActive && !confirmUpdateModalOpen) { - setConfirmUpdateModalOpen(true); - return; - } - - if (production?.isActive && confirmUpdateModalOpen) { - setConfirmUpdateModalOpen(false); - } - - const presetToUpdate = deepclone(preset); - const ipMissing = multiviews.some( - (multiview) => - multiview.output.local_ip === '' || !multiview.output.local_ip - ); - const portMissing = multiviews.some( - (multiview) => !multiview.output.local_port - ); - const videoKilobitRateMissing = multiviews.some( - (multiview) => !multiview.output.video_kilobit_rate - ); - - if (!multiviews) { - toast.error(t('preset.no_multiview_selected')); - return; - } - - if (ipMissing) { - toast.error(t('preset.no_ip_selected')); - return; - } - - if (videoKilobitRateMissing) { - toast.error(t('preset.no_rate_selected')); - return; - } - - if (portDuplicateIndexes.length > 0 || portMissing) { - toast.error(t('preset.no_port_selected')); - return; - } - - if (streamIdDuplicateIndexes.length > 0) { - toast.error(t('preset.unique_stream_id')); - return; - } - - presetToUpdate.pipelines[0].multiviews = multiviews.map( - (singleMultiview) => { - return { ...singleMultiview }; - } - ); - - updatePreset(presetToUpdate); - onClose(); - }; - - const onUpdateLayoutPreset = async () => { - if (newMultiviewLayout?.name === '') { - toast.error(t('preset.layout_name_missing')); - return; - } - - if (!newMultiviewLayout) { - toast.error(t('preset.no_updated_layout')); - return; - } - - await addNewLayout(newMultiviewLayout); - setNewMultiviewLayout(null); - setLayoutModalOpen(false); - setRefresh(true); - }; - - const closeLayoutModal = () => { - setNewMultiviewLayout(null); - setLayoutModalOpen(false); - setRefresh(true); - }; - - const findDuplicateValues = (mvs: MultiviewSettings[]) => { - const ports = mvs.map( - (item: MultiviewSettings) => - item.output.local_ip + ':' + item.output.local_port.toString() - ); - const streamIds = mvs.map( - (item: MultiviewSettings) => item.output.srt_stream_id - ); - const duplicatePortIndices: number[] = []; - const duplicateStreamIdIndices: number[] = []; - const seenPorts = new Set(); - const seenIds = new Set(); - - ports.forEach((port, index) => { - if (seenPorts.has(port)) { - duplicatePortIndices.push(index); - - // Also include the first occurrence if it's not already included - const firstIndex = ports.indexOf(port); - if (!duplicatePortIndices.includes(firstIndex)) { - duplicatePortIndices.push(firstIndex); - } - } else { - seenPorts.add(port); - } - }); - - streamIds.forEach((streamId, index) => { - if (streamId === '' || !streamId) { - return; - } - - if (seenIds.has(streamId)) { - duplicateStreamIdIndices.push(index); - - // Also include the first occurrence if it's not already included - const firstIndex = streamIds.indexOf(streamId); - if (!duplicateStreamIdIndices.includes(firstIndex)) { - duplicateStreamIdIndices.push(firstIndex); - } - } else { - seenIds.add(streamId); - } - }); - - return { - hasDuplicatePort: duplicatePortIndices, - hasDuplicateStreamId: duplicateStreamIdIndices - }; - }; - - const runDuplicateCheck = (mvs: MultiviewSettings[]) => { - const { hasDuplicatePort, hasDuplicateStreamId } = findDuplicateValues(mvs); - - if (hasDuplicatePort.length > 0) { - setPortDuplicateIndexes(hasDuplicatePort); - } - - if (hasDuplicateStreamId.length > 0) { - setStreamIdDuplicateIndexes(hasDuplicateStreamId); - } - - if (hasDuplicatePort.length === 0) { - setPortDuplicateIndexes([]); - } - - if (hasDuplicateStreamId.length === 0) { - setStreamIdDuplicateIndexes([]); - } - }; - - const handleUpdateMultiview = ( - multiview: MultiviewSettings, - index: number - ) => { - const updatedMultiviews = multiviews.map((item, i) => - i === index ? { ...item, ...multiview } : item - ); - - runDuplicateCheck(multiviews); - - setMultiviews(updatedMultiviews); - }; - - const addNewMultiview = (newMultiview: MultiviewSettings) => { - // Remove _id from newMultiview to avoid conflicts with existing multiviews - delete newMultiview._id; - - setMultiviews((prevMultiviews) => - prevMultiviews ? [...prevMultiviews, newMultiview] : [newMultiview] - ); - }; - - const removeNewMultiview = (index: number) => { - const newMultiviews = multiviews.filter((_, i) => i !== index); - setMultiviews(newMultiviews); - }; - - return ( - - {!layoutModalOpen && ( -
    - {multiviews && - multiviews.length > 0 && - multiviews.map((singleItem, index) => { - return ( -
    - {index !== 0 && ( -
    - )} -
    - - handleUpdateMultiview(input, index) - } - portDuplicateError={ - portDuplicateIndexes.length > 0 - ? portDuplicateIndexes.includes(index) - : false - } - streamIdDuplicateError={ - streamIdDuplicateIndexes.length > 0 - ? streamIdDuplicateIndexes.includes(index) - : false - } - refresh={refresh} - /> -
    1 - ? 'justify-between' - : 'justify-end' - }`} - > - {multiviews.length > 1 && ( - - )} - {multiviews.length === index + 1 && ( - - )} -
    -
    -
    - ); - })} -
    - )} - {layoutModalOpen && ( - - )} -
    - {!layoutModalOpen && ( - - )} - (layoutModalOpen ? closeLayoutModal() : clearInputs())} - onSave={() => (layoutModalOpen ? onUpdateLayoutPreset() : onSave())} - /> -
    - - {confirmUpdateModalOpen && ( - setConfirmUpdateModalOpen(false)} - onConfirm={() => onSave()} - /> - )} -
    - ); -} diff --git a/src/components/modal/configureOutputModal/ConfigureOutputModal.tsx b/src/components/modal/configureOutputModal/ConfigureOutputModal.tsx index 169b9d93..801aad04 100644 --- a/src/components/modal/configureOutputModal/ConfigureOutputModal.tsx +++ b/src/components/modal/configureOutputModal/ConfigureOutputModal.tsx @@ -21,9 +21,9 @@ export interface OutputStream { srtMode: string; srtPassphrase: string; port: number; - videoFormat: string; - videoBit: number; - videoKiloBit: number; + videoFormat?: string; + videoBit?: number; + videoKiloBit?: number; srt_stream_id: string; } diff --git a/src/components/modal/configureOutputModal/Input.tsx b/src/components/modal/configureOutputModal/Input.tsx index a2b01553..397d4fb7 100644 --- a/src/components/modal/configureOutputModal/Input.tsx +++ b/src/components/modal/configureOutputModal/Input.tsx @@ -33,7 +33,7 @@ export default function Input({ onChange={(e) => update(e.target.value)} className={`cursor-pointer border text-sm rounded-lg ${ size === 'small' ? 'w-6/12' : 'w-7/12' - } pl-2 pt-1 pb-1 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-gray-400 focus:outline-none ${ + } pl-2 pt-1 pb-1 bg-gray-500 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/Options.tsx b/src/components/modal/configureOutputModal/Options.tsx index a5f52369..4c48da08 100644 --- a/src/components/modal/configureOutputModal/Options.tsx +++ b/src/components/modal/configureOutputModal/Options.tsx @@ -35,7 +35,7 @@ export default function Options({ update(e.target.value); }} value={value} - className="cursor-pointer px-2 border justify-center text-sm rounded-lg w-6/12 pt-1 pb-1 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-gray-400 focus:outline-none" + className="cursor-pointer px-2 border justify-center text-sm rounded-lg w-6/12 pt-1 pb-1 bg-gray-500 border-gray-600 placeholder-gray-400 text-white focus:border-gray-400 focus:outline-none" > {emptyFirstOption && ( diff --git a/src/components/modal/configureOutputModal/PipelineOutputConfig.tsx b/src/components/modal/configureOutputModal/PipelineOutputConfig.tsx index d481ff1f..5925ac05 100644 --- a/src/components/modal/configureOutputModal/PipelineOutputConfig.tsx +++ b/src/components/modal/configureOutputModal/PipelineOutputConfig.tsx @@ -6,6 +6,7 @@ import { PipelineOutput, PipelineOutputEncoderSettings, PipelineOutputSettings, + PipelineOutputWithoutEncoderSettings, PipelineSettings } from '../../../interfaces/pipeline'; import StreamAccordion from './StreamAccordion'; @@ -142,7 +143,7 @@ const PipelineOutputConfig: React.FC = (props) => { if (!outputStreams.length) return; const convertStream = ( - stream: PipelineOutputSettings, + stream: PipelineOutputWithoutEncoderSettings, index: number ): OutputStream => { return { @@ -152,9 +153,6 @@ const PipelineOutputConfig: React.FC = (props) => { srtMode: stream.srt_mode, srtPassphrase: stream.srt_passphrase, port: stream.local_port, - videoFormat: stream.video_format, - videoBit: stream.video_bit_depth, - videoKiloBit: stream.video_kilobit_rate, srt_stream_id: stream.srt_stream_id }; }; diff --git a/src/components/modal/configureOutputModal/StreamAccordion.tsx b/src/components/modal/configureOutputModal/StreamAccordion.tsx index ad054930..5ec39e5d 100644 --- a/src/components/modal/configureOutputModal/StreamAccordion.tsx +++ b/src/components/modal/configureOutputModal/StreamAccordion.tsx @@ -30,7 +30,9 @@ export default function StreamAccordion({ return (
    -
    +
    void; diff --git a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayout.tsx b/src/components/modal/multiviewLayoutSetup/MultiviewLayout.tsx similarity index 90% rename from src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayout.tsx rename to src/components/modal/multiviewLayoutSetup/MultiviewLayout.tsx index c039ec51..c6c201c2 100644 --- a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayout.tsx +++ b/src/components/modal/multiviewLayoutSetup/MultiviewLayout.tsx @@ -1,6 +1,6 @@ -import { TListSource } from '../../../../interfaces/multiview'; -import { MultiviewPreset } from '../../../../interfaces/preset'; -import Options from '../../configureOutputModal/Options'; +import { TListSource } from '../../../interfaces/multiview'; +import { MultiviewPreset } from '../../../interfaces/preset'; +import Options from '../configureOutputModal/Options'; export default function MultiviewLayout({ multiviewPresetLayout, diff --git a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayoutSettings.tsx b/src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetup.tsx similarity index 77% rename from src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayoutSettings.tsx rename to src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetup.tsx index e3112882..deaf5d86 100644 --- a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/MultiviewLayoutSettings.tsx +++ b/src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetup.tsx @@ -1,23 +1,26 @@ import { useEffect, useState } from 'react'; -import { MultiviewPreset } from '../../../../interfaces/preset'; -import { useTranslate } from '../../../../i18n/useTranslate'; -import { useSetupMultiviewLayout } from '../../../../hooks/useSetupMultiviewLayout'; +import { MultiviewPreset } from '../../../interfaces/preset'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { useSetupMultiviewLayout } from '../../../hooks/useSetupMultiviewLayout'; import { useDeleteMultiviewLayout, useMultiviewLayouts -} from '../../../../hooks/multiviewLayout'; -import { useConfigureMultiviewLayout } from '../../../../hooks/useConfigureMultiviewLayout'; -import { Production } from '../../../../interfaces/production'; -import { TMultiviewLayout } from '../../../../interfaces/preset'; -import { useCreateInputArray } from '../../../../hooks/useCreateInputArray'; -import { TListSource } from '../../../../interfaces/multiview'; -import Options from '../../configureOutputModal/Options'; -import Input from '../../configureOutputModal/Input'; +} from '../../../hooks/multiviewLayout'; +import { useConfigureMultiviewLayout } from '../../../hooks/useConfigureMultiviewLayout'; +import { TMultiviewLayout } from '../../../interfaces/preset'; +import { useCreateInputArray } from '../../../hooks/useCreateInputArray'; +import { TListSource } from '../../../interfaces/multiview'; +import Options from '../configureOutputModal/Options'; +import Input from '../configureOutputModal/Input'; import MultiviewLayout from './MultiviewLayout'; import toast from 'react-hot-toast'; import RemoveLayoutButton from './RemoveLayoutButton'; -import { useMultiviewDefaultPresets } from '../../../../hooks/useMultiviewDefaultPresets'; +import { SourceReference } from '../../../interfaces/Source'; +import Decision from '../configureOutputModal/Decision'; +import { Modal } from '../Modal'; +import { useMultiviewDefaultPresets } from '../../../hooks/useMultiviewDefaultPresets'; import Checkbox from './Checkbox'; +import { useMultiviewPresets } from '../../../hooks/multiviewPreset'; type ChangeLayout = { defaultLabel?: string; @@ -25,14 +28,20 @@ type ChangeLayout = { viewId: string; }; -export default function MultiviewLayoutSettings({ - production, - setNewMultiviewPreset, - layoutModalOpen +export default function MultiviewLayoutSetup({ + onUpdateLayoutPreset, + productionId, + isProductionActive, + sourceList, + open, + onClose }: { - production: Production | undefined; - setNewMultiviewPreset: (preset: TMultiviewLayout | null) => void; - layoutModalOpen: boolean; + onUpdateLayoutPreset: (newLayout: TMultiviewLayout | null) => void; + productionId: string; + isProductionActive: boolean; + sourceList: SourceReference[]; + open: boolean; + onClose: () => void; }) { const [selectedMultiviewPreset, setSelectedMultiviewPreset] = useState(null); @@ -41,9 +50,9 @@ export default function MultiviewLayoutSettings({ const [isChecked, setIsChecked] = useState(false); const [changedLayout, setChangedLayout] = useState(null); const [newPresetName, setNewPresetName] = useState(null); - const { inputList } = useCreateInputArray(production); + const { inputList } = useCreateInputArray(sourceList); const [multiviewLayouts] = useMultiviewLayouts(refresh); - const sourceList = production ? production.sources : []; + const [multiviewPresets] = useMultiviewPresets(); const { multiviewDefaultPresets } = useMultiviewDefaultPresets({ sourceList, isChecked @@ -52,7 +61,7 @@ export default function MultiviewLayoutSettings({ selectedMultiviewPreset ); const { multiviewLayout } = useConfigureMultiviewLayout( - production?._id, + productionId, selectedMultiviewPreset, changedLayout?.defaultLabel, changedLayout?.source, @@ -71,7 +80,7 @@ export default function MultiviewLayoutSettings({ const availableMultiviewLayouts = multiviewLayouts?.filter( - (layout) => layout.productionId === production?._id + (layout) => layout.productionId === productionId ) || []; const multiviewLayoutNames = @@ -102,8 +111,8 @@ export default function MultiviewLayoutSettings({ // Refresh the layout list when a layout is deleted useEffect(() => { - setRefresh(layoutModalOpen); - }, [layoutModalOpen]); + setRefresh(open); + }, [open]); useEffect(() => { if (multiviewLayouts) { @@ -111,20 +120,21 @@ export default function MultiviewLayoutSettings({ } }, [multiviewLayouts]); - useEffect(() => { - if (newPresetName && selectedMultiviewPreset) { - setNewMultiviewPreset({ - ...selectedMultiviewPreset, - name: newPresetName - }); - } - }, [newPresetName, selectedMultiviewPreset, setNewMultiviewPreset]); - - useEffect(() => { + const onSave = () => { if (multiviewLayout) { setSelectedMultiviewPreset(multiviewLayout); + onUpdateLayoutPreset({ + ...multiviewLayout, + name: + multiviewLayout.name !== presetName && newPresetName !== '' + ? multiviewLayout.name + : '' + }); + } else { + setSelectedMultiviewPreset(null); + onUpdateLayoutPreset(null); } - }, [multiviewLayout]); + }; const handleLayoutUpdate = (name: string, type: string) => { const chosenLayout = availableMultiviewLayouts?.find( @@ -193,8 +203,13 @@ export default function MultiviewLayoutSettings({ } }; + const closeLayoutModal = () => { + setRefresh(true); + onClose(); + }; + return ( - <> + {selectedMultiviewPreset && (
    @@ -216,7 +231,7 @@ export default function MultiviewLayoutSettings({ removeMultiviewLayout={removeMultiviewLayout} deleteDisabled={availableMultiviewLayouts.length < 1} title={t('preset.remove_layout')} - hidden={production?.isActive || false} + hidden={isProductionActive || false} />
    @@ -256,8 +271,13 @@ export default function MultiviewLayoutSettings({

    )}
    +
    )} - +
    ); } diff --git a/src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetupButton.tsx b/src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetupButton.tsx new file mode 100644 index 00000000..e0c4fcb3 --- /dev/null +++ b/src/components/modal/multiviewLayoutSetup/MultiviewLayoutSetupButton.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useState } from 'react'; +import { IconSettings } from '@tabler/icons-react'; +import { TMultiviewLayout } from '../../../interfaces/preset'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { Button } from '../../button/Button'; +import MultiviewLayoutSetup from './MultiviewLayoutSetup'; +import { SourceReference } from '../../../interfaces/Source'; + +type MultiviewLayoutSetupButtonProps = { + onUpdateLayoutPreset: (newLayout: TMultiviewLayout | null) => void; + productionId: string; + isProductionActive: boolean; + sourceList: SourceReference[]; +}; + +export function MultiviewLayoutSetupButton({ + onUpdateLayoutPreset, + productionId, + isProductionActive, + sourceList +}: MultiviewLayoutSetupButtonProps) { + const [modalOpen, setModalOpen] = useState(false); + const toggleConfigModal = () => { + setModalOpen((state) => !state); + }; + const t = useTranslate(); + return ( + <> + + { + setModalOpen(false); + onUpdateLayoutPreset; + }} + open={modalOpen} + onClose={() => setModalOpen(false)} + /> + + ); +} diff --git a/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx b/src/components/modal/multiviewLayoutSetup/MultiviewSettings.tsx similarity index 94% rename from src/components/modal/configureMultiviewModal/MultiviewSettings.tsx rename to src/components/modal/multiviewLayoutSetup/MultiviewSettings.tsx index 9daedef4..69c8828b 100644 --- a/src/components/modal/configureMultiviewModal/MultiviewSettings.tsx +++ b/src/components/modal/multiviewLayoutSetup/MultiviewSettings.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslate } from '../../../i18n/useTranslate'; import { MultiviewSettings } from '../../../interfaces/multiview'; import { TMultiviewLayout } from '../../../interfaces/preset'; @@ -35,11 +35,16 @@ export default function MultiviewSettingsConfig({ const [multiviewLayouts] = useMultiviewLayouts(refresh); const currentValue = multiview || selectedMultiviewLayout; - const avaliableMultiviewLayouts = multiviewLayouts?.filter( - (layout) => layout.productionId === productionId || !layout.productionId - ); - const multiviewLayoutNames = - avaliableMultiviewLayouts?.map((layout) => layout.name) || []; + + const avaliableMultiviewLayouts = useMemo(() => { + return multiviewLayouts?.filter( + (layout) => layout.productionId === productionId || !layout.productionId + ); + }, [multiviewLayouts]); + + const multiviewLayoutNames = useMemo(() => { + return avaliableMultiviewLayouts?.map((layout) => layout.name) || []; + }, [multiviewLayouts]); useEffect(() => { if ( @@ -198,7 +203,7 @@ export default function MultiviewSettingsConfig({ }; return ( -
    +

    {t('preset.multiview_output_settings')}

    diff --git a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/RemoveLayoutButton.tsx b/src/components/modal/multiviewLayoutSetup/RemoveLayoutButton.tsx similarity index 70% rename from src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/RemoveLayoutButton.tsx rename to src/components/modal/multiviewLayoutSetup/RemoveLayoutButton.tsx index 26dcdb17..d059782f 100644 --- a/src/components/modal/configureMultiviewModal/MultiviewLayoutSettings/RemoveLayoutButton.tsx +++ b/src/components/modal/multiviewLayoutSetup/RemoveLayoutButton.tsx @@ -28,16 +28,12 @@ export default function RemoveLayoutButton({ return ( ); } diff --git a/src/components/pipeline/Pipelines.tsx b/src/components/pipeline/Pipelines.tsx index 7c4145a7..ceb68f36 100644 --- a/src/components/pipeline/Pipelines.tsx +++ b/src/components/pipeline/Pipelines.tsx @@ -1,19 +1,19 @@ +import { PipelineSettings } from '../../interfaces/pipeline'; import { Production } from '../../interfaces/production'; import { PipelineCard } from './PipelineCard'; type PipelinesProps = { - production?: Production; + isProductionActive: boolean; + pipelines: PipelineSettings[]; }; -export function Pipelines({ production }: PipelinesProps) { - if (!production || !production.production_settings) return null; - +export function Pipelines({ isProductionActive, pipelines }: PipelinesProps) { return (
    - {production.production_settings.pipelines?.flatMap((pipeline) => { + {pipelines?.flatMap((pipeline) => { if (pipeline.pipeline_id) { return ( diff --git a/src/components/production/ProductionPage.tsx b/src/components/production/ProductionPage.tsx new file mode 100644 index 00000000..d8aed5f8 --- /dev/null +++ b/src/components/production/ProductionPage.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React, { Suspense, useEffect, useState } from 'react'; +import { useTranslate } from '../../i18n/useTranslate'; +import ProductionMonitoring from './monitoring/ProductionMonitoring'; +import { + Production, + ProductionControlConnection +} from '../../interfaces/production'; +import { useGetProduction, usePutProduction } from '../../hooks/productions'; +import { PipelineOutput, PipelineSettings } from '../../interfaces/pipeline'; +import ProductionControlConnections from './controlConnections/ProductionControlConnections'; +import ProductionOutputs from './outputs/ProductionOutputs'; +import ProductionMultiviews from './multiviews/ProductionMultiviews'; +import { MultiviewSettings } from '../../interfaces/multiview'; +import { SourceReference } from '../../interfaces/Source'; +import ProductionSources from './sources/ProductionSources'; +import ProductionHeader from './header/ProductionHeader'; +import ProductionPipelines from './pipelines/ProductionPipelines'; +import { LoadingCover } from '../loader/LoadingCover'; + +interface ProductionPageProps { + id: string; +} + +const ProductionPage: React.FC = (props) => { + const { id } = props; + + const t = useTranslate(); + const [production, setProduction] = useState(); + const [productionName, setProductionName] = useState(''); + const [isProductionActive, setIsProductionActive] = useState(false); + const [sources, setSources] = useState([]); + const [outputs, setOutputs] = useState([]); + const [pipelines, setPipelines] = useState([]); + const [multiviews, setMultiviews] = useState([]); + const [controlConnection, setControlConnection] = + useState(); + + //PRODUCTION + const putProduction = usePutProduction(); + const getProduction = useGetProduction(); + + // FETCH PRESETS AND PRODUCTION + const fetchProduction = () => { + getProduction(id).then((prod) => { + setProduction(prod); + setProductionName(prod.name); + setIsProductionActive(prod.isActive); + setSources(prod.sources); + setPipelines(prod.pipelines); + setOutputs(prod.outputs); + setMultiviews(prod.multiviews); + setControlConnection(prod.control_connection); + // check if production has pipelines in use or control panels in use, if so update production + // const production = config.isActive + // ? config + // : checkProductionPipelines(config, pipelines); + }); + }; + + useEffect(() => { + fetchProduction(); + }, []); + + // EVERY TIME THE PRODUCTION IS UPDATE -> UPLOAD IT TO DB + useEffect(() => { + if (production?._id) { + putProduction(production._id, { + ...production, + name: productionName, + sources, + pipelines, + outputs, + multiviews, + control_connection: controlConnection + }); + } + }, [ + productionName, + sources, + pipelines, + outputs, + multiviews, + controlConnection + ]); + + return ( +
    + {(production && ( + <> + + + + + + + + + )) || ( +
    + +
    + )} +
    + ); +}; + +export default ProductionPage; diff --git a/src/components/production/controlConnections/ProductionControlConnections.tsx b/src/components/production/controlConnections/ProductionControlConnections.tsx new file mode 100644 index 00000000..bf0ccf98 --- /dev/null +++ b/src/components/production/controlConnections/ProductionControlConnections.tsx @@ -0,0 +1,104 @@ +import cloneDeep from 'lodash.clonedeep'; +import { useControlPanels } from '../../../hooks/controlPanels'; +import { ProductionControlConnection } from '../../../interfaces/production'; +import ControlPanelDropDown from '../../dropDown/ControlPanelDropDown'; +import Section from '../../section/Section'; +import { LoadingCover } from '../../loader/LoadingCover'; + +interface ProductionControlConnectionsProps { + controlConnection?: ProductionControlConnection; + onChange: (cc: any) => void; +} + +const ProductionControlConnections: React.FC< + ProductionControlConnectionsProps +> = (props) => { + const { controlConnection, onChange } = props; + + const [controlPanels, loading] = useControlPanels(); + + const onControlConnectionChange = (ids: string[]) => { + if (controlConnection) { + const newCC = cloneDeep(controlConnection); + newCC.control_panel_ids = ids; + onChange(newCC); + } + }; + + return ( +
    + {(controlConnection && controlPanels && ( +
    +
    + {!!controlPanels?.length && ( + ({ + option: controlPanel.name, + available: controlPanel.outgoing_connections?.length === 0, + id: controlPanel.uuid + }))} + initial={controlConnection.control_panel_ids} + setSelectedControlPanel={onControlConnectionChange} + /> + )} +
    +
    +
    +
    Control Panel Endpoint
    +
    +
    {'Port:'}
    +
    + {controlConnection.control_panel_endpoint.port} +
    +
    +
    +
    {'To Pipeline:'}
    +
    + {controlConnection.control_panel_endpoint.toPipelineIdx} +
    +
    +
    +
    +
    + Pipeline Control Connections +
    +
    + {controlConnection.pipeline_control_connections.map( + (pcc, index) => ( +
    +
    +
    {'Port:'}
    +
    {pcc.port}
    +
    +
    +
    {'From Pipeline:'}
    +
    + {pcc.fromPipelineIdx} +
    +
    +
    +
    {'To Pipeline:'}
    +
    + {pcc.toPipelineIdx} +
    +
    +
    + ) + )} +
    +
    +
    +
    + )) || ( +
    + +
    + )} +
    + ); +}; + +export default ProductionControlConnections; diff --git a/src/components/production/header/ProductionHeader.tsx b/src/components/production/header/ProductionHeader.tsx new file mode 100644 index 00000000..6af9882c --- /dev/null +++ b/src/components/production/header/ProductionHeader.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { KeyboardEvent, useContext, useEffect, useState } from 'react'; +import { StartProductionButton } from '../../startProduction/StartProductionButton'; +import { GlobalContext } from '../../../contexts/GlobalContext'; +import { Production } from '../../../interfaces/production'; +import { useTranslate } from '../../../i18n/useTranslate'; +import Icons from '../../icons/Icons'; + +interface ProductionHeaderProps { + production?: Production; + onProductionNameChange: (name: string) => void; + refreshProduction: () => void; + presetName: string; +} + +const ProductionHeader: React.FC = (props) => { + const { production, onProductionNameChange, refreshProduction, presetName } = + props; + const { locked } = useContext(GlobalContext); + const t = useTranslate(); + const [name, setName] = useState(production?.name || ''); + + const updateConfigName = (nameChange: string) => { + if (production?.name === nameChange) { + return; + } + setName(nameChange); + onProductionNameChange(nameChange); + }; + + return ( +
    +
    +
    + + { + setName(e.target.value); + }} + onKeyDown={(e: KeyboardEvent) => { + if (e.key.includes('Enter')) { + e.currentTarget.blur(); + } + }} + onBlur={() => updateConfigName(name)} + disabled={locked} + /> +
    +

    {presetName}

    +
    +
    + +
    +
    + ); +}; + +export default ProductionHeader; diff --git a/src/components/production/monitoring/ProductionMonitoring.tsx b/src/components/production/monitoring/ProductionMonitoring.tsx new file mode 100644 index 00000000..3c70c355 --- /dev/null +++ b/src/components/production/monitoring/ProductionMonitoring.tsx @@ -0,0 +1,28 @@ +'use client'; +import { PipelineSettings } from '../../../interfaces/pipeline'; +import { MonitoringButton } from '../../button/MonitoringButton'; +import { Pipelines } from '../../pipeline/Pipelines'; + +interface ProductionMonitoringProps { + isProductionActive: boolean; + productionId: string; + pipelines: PipelineSettings[]; +} + +const ProductionMonitoring: React.FC = (props) => { + const { isProductionActive, productionId, pipelines } = props; + + return ( +
    +
    + +
    + {isProductionActive && } +
    + ); +}; + +export default ProductionMonitoring; diff --git a/src/components/production/multiviews/ProductionMultiviews.tsx b/src/components/production/multiviews/ProductionMultiviews.tsx new file mode 100644 index 00000000..317497b0 --- /dev/null +++ b/src/components/production/multiviews/ProductionMultiviews.tsx @@ -0,0 +1,300 @@ +import { TMultiviewLayout } from '../../../interfaces/preset'; +import { useEffect, useState } from 'react'; +import { useTranslate } from '../../../i18n/useTranslate'; +import toast from 'react-hot-toast'; +import { MultiviewSettings } from '../../../interfaces/multiview'; +import { IconPlus, IconTrash } from '@tabler/icons-react'; +import { usePutMultiviewLayout } from '../../../hooks/multiviewLayout'; +import Section from '../../section/Section'; +import MultiviewSettingsConfig from '../../modal/multiviewLayoutSetup/MultiviewSettings'; +import Decision from '../../modal/configureOutputModal/Decision'; +import { UpdateMultiviewersModal } from '../../modal/UpdateMultiviewersModal'; +import { SourceReference } from '../../../interfaces/Source'; +import { MultiviewLayoutSetupButton } from '../../modal/multiviewLayoutSetup/MultiviewLayoutSetupButton'; + +type ProductionMultiviewsProps = { + productionId: string; + isProductionActive: boolean; + sources: SourceReference[]; + multiviews: MultiviewSettings[]; + updateMultiviews: (mvs: MultiviewSettings[]) => void; +}; + +export default function ProductionMultiviews(props: ProductionMultiviewsProps) { + const { + productionId, + isProductionActive, + sources, + multiviews: multiviewsProp, + updateMultiviews + } = props; + + const [multiviews, setMultiviews] = + useState(multiviewsProp); + const [portDuplicateIndexes, setPortDuplicateIndexes] = useState( + [] + ); + const [streamIdDuplicateIndexes, setStreamIdDuplicateIndexes] = useState< + number[] + >([]); + const [layoutModalOpen, setLayoutModalOpen] = useState(false); + const [refresh, setRefresh] = useState(true); + const [confirmUpdateModalOpen, setConfirmUpdateModalOpen] = useState(false); + const [newMultiviewLayout, setNewMultiviewLayout] = + useState(null); + const addNewLayout = usePutMultiviewLayout(); + const t = useTranslate(); + // const getMultiviewLayout = useGetMultiviewLayout(); + + const clearInputs = () => { + setLayoutModalOpen(false); + setMultiviews(multiviewsProp || []); + }; + + useEffect(() => { + runDuplicateCheck(multiviews); + }, [multiviews]); + + const onSave = () => { + if (isProductionActive && !confirmUpdateModalOpen) { + setConfirmUpdateModalOpen(true); + return; + } + if (isProductionActive && confirmUpdateModalOpen) { + setConfirmUpdateModalOpen(false); + } + + if (!multiviews) { + toast.error(t('preset.no_multiview_selected')); + return; + } + + if (portDuplicateIndexes.length > 0) { + toast.error(t('preset.no_port_selected')); + return; + } + + if (streamIdDuplicateIndexes.length > 0) { + toast.error(t('preset.unique_stream_id')); + return; + } + + const multiviewsToUpdate = multiviews.map((singleMultiview) => { + return { ...singleMultiview }; + }); + + updateMultiviews(multiviewsToUpdate); + }; + + const onUpdateLayoutPreset = async (newLayout: TMultiviewLayout | null) => { + if (newMultiviewLayout?.name === '') { + toast.error(t('preset.layout_name_missing')); + return; + } + + if (!newLayout) { + toast.error(t('preset.no_updated_layout')); + return; + } + + await addNewLayout(newLayout); + setLayoutModalOpen(false); + setRefresh(true); + }; + + const findDuplicateValues = (mvs: MultiviewSettings[]) => { + const ports = mvs.map( + (item: MultiviewSettings) => + item.output.local_ip + ':' + item.output.local_port.toString() + ); + const streamIds = mvs.map( + (item: MultiviewSettings) => item.output.srt_stream_id + ); + const duplicatePortIndices: number[] = []; + const duplicateStreamIdIndices: number[] = []; + const seenPorts = new Set(); + const seenIds = new Set(); + + ports.forEach((port, index) => { + if (seenPorts.has(port)) { + duplicatePortIndices.push(index); + + // Also include the first occurrence if it's not already included + const firstIndex = ports.indexOf(port); + if (!duplicatePortIndices.includes(firstIndex)) { + duplicatePortIndices.push(firstIndex); + } + } else { + seenPorts.add(port); + } + }); + + streamIds.forEach((streamId, index) => { + if (streamId === '' || !streamId) { + return; + } + + if (seenIds.has(streamId)) { + duplicateStreamIdIndices.push(index); + + // Also include the first occurrence if it's not already included + const firstIndex = streamIds.indexOf(streamId); + if (!duplicateStreamIdIndices.includes(firstIndex)) { + duplicateStreamIdIndices.push(firstIndex); + } + } else { + seenIds.add(streamId); + } + }); + + return { + hasDuplicatePort: duplicatePortIndices, + hasDuplicateStreamId: duplicateStreamIdIndices + }; + }; + + const runDuplicateCheck = (mvs: MultiviewSettings[]) => { + const { hasDuplicatePort, hasDuplicateStreamId } = findDuplicateValues(mvs); + + if (hasDuplicatePort.length > 0) { + setPortDuplicateIndexes(hasDuplicatePort); + } + + if (hasDuplicateStreamId.length > 0) { + setStreamIdDuplicateIndexes(hasDuplicateStreamId); + } + + if (hasDuplicatePort.length === 0) { + setPortDuplicateIndexes([]); + } + + if (hasDuplicateStreamId.length === 0) { + setStreamIdDuplicateIndexes([]); + } + }; + + const handleUpdateMultiview = ( + multiview: MultiviewSettings, + index: number + ) => { + const updatedMultiviews = multiviews.map((item, i) => + i === index ? { ...item, ...multiview } : item + ); + + runDuplicateCheck(multiviews); + + setMultiviews(updatedMultiviews); + }; + + const addNewMultiview = (newMultiview: MultiviewSettings) => { + // Remove _id from newMultiview to avoid conflicts with existing multiviews + delete newMultiview._id; + + setMultiviews((prevMultiviews) => + prevMultiviews ? [...prevMultiviews, newMultiview] : [newMultiview] + ); + }; + + const removeNewMultiview = (index: number) => { + const newMultiviews = multiviews.filter((_, i) => i !== index); + setMultiviews(newMultiviews); + }; + + return ( +
    +
    + {!layoutModalOpen && ( +
    + {(multiviews && + multiviews.length > 0 && + multiviews.map((singleItem, index) => { + return ( +
    + {index !== 0 && ( +
    + )} +
    + + handleUpdateMultiview(input, index) + } + portDuplicateError={ + portDuplicateIndexes.length > 0 + ? portDuplicateIndexes.includes(index) + : false + } + streamIdDuplicateError={ + streamIdDuplicateIndexes.length > 0 + ? streamIdDuplicateIndexes.includes(index) + : false + } + refresh={refresh} + /> +
    1 + ? 'justify-between' + : 'justify-end' + }`} + > + {multiviews.length > 1 && ( + + )} + {multiviews.length === index + 1 && ( + + )} +
    +
    +
    + ); + })) ||
    No Multiviews
    } +
    + )} + +
    + clearInputs()} + onSave={() => onSave()} + /> +
    + + {confirmUpdateModalOpen && ( + setConfirmUpdateModalOpen(false)} + onConfirm={() => onSave()} + /> + )} +
    +
    + ); +} diff --git a/src/components/production/outputs/ProductionOutputEdit.tsx b/src/components/production/outputs/ProductionOutputEdit.tsx new file mode 100644 index 00000000..2060bd5f --- /dev/null +++ b/src/components/production/outputs/ProductionOutputEdit.tsx @@ -0,0 +1,202 @@ +import { KeyboardEvent } from 'react'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { + PipelineOutput, + PipelineOutputEncoderSettings, + PipelineOutputWithoutEncoderSettings +} from '../../../interfaces/pipeline'; +import Input from '../../modal/configureOutputModal/Input'; +import Options from '../../modal/configureOutputModal/Options'; +import StreamAccordion from '../../modal/configureOutputModal/StreamAccordion'; +import cloneDeep from 'lodash.clonedeep'; + +const createNewStream = () => { + return { + audio_format: 'ADTS', + audio_kilobit_rate: 128, + format: 'MPEG-TS-SRT', + local_ip: '0.0.0.0', + local_port: 9000, + remote_ip: '0.0.0.0', + remote_port: 9000, + srt_latency_ms: 120, + srt_mode: 'listener', + srt_passphrase: '', + video_gop_length: 50, + srt_stream_id: '' + }; +}; + +interface ProductionOutputEditProps { + output: PipelineOutput; + onOutputChange: (output: PipelineOutput) => void; +} + +export interface OutputStream { + name: string; + pipelineIndex: number; + ip: string; + srtMode: string; + srtPassphrase: string; + port: number; + srt_stream_id: string; +} + +const ProductionOutputEdit: React.FC = (props) => { + const { output, onOutputChange } = props; + const t = useTranslate(); + + const preventCharacters = (evt: KeyboardEvent) => { + ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault(); + }; + + const handleUpdateOutputSetting = ( + key: keyof PipelineOutputEncoderSettings, + value: string | number + ) => { + const newOutput: PipelineOutput = cloneDeep(output); + // Simple workaround to set these values as numbers + if (['video_bit_depth', 'video_kilobit_rate'].includes(key)) { + newOutput.settings[key] = Number(value) as never; + } else { + newOutput.settings[key] = value as never; + } + onOutputChange(newOutput); + }; + + const getOutputFields = (output: PipelineOutput) => { + return ( +
    + handleUpdateOutputSetting('video_format', value)} + /> + + handleUpdateOutputSetting('video_bit_depth', value) + } + /> + + + handleUpdateOutputSetting('video_kilobit_rate', value) + } + /> +
    + ); + }; + + const handleAddStream = (output: PipelineOutput) => { + const newOutput: PipelineOutput = cloneDeep(output); + const newStream = createNewStream(); + newOutput.streams.push(newStream); + onOutputChange(newOutput); + }; + + const handleUpdateStream = (index: number, field: string, value: string) => { + const getInt = (val: string) => { + if (Number.isNaN(parseInt(value))) { + return 0; + } + return parseInt(val); + }; + const newOutput: PipelineOutput = cloneDeep(output); + const newStream = newOutput.streams[index]; + switch (field) { + default: + case 'port': + newStream.local_port = getInt(value); + newStream.remote_port = getInt(value); + break; + case 'srtMode': + newStream.srt_mode = value; + break; + case 'ip': + newStream.local_ip = value; + newStream.remote_ip = value; + break; + case 'srtPassphrase': + newStream.srt_passphrase = value; + break; + case 'srt_stream_id': + newStream.srt_stream_id = value; + break; + } + onOutputChange(newOutput); + }; + + const handleDeleteStream = (index: number) => { + const newOutput = cloneDeep(output); + newOutput.streams.splice(index, 1); + onOutputChange(newOutput); + }; + + const getOutputStreams = (output: PipelineOutput) => { + if (!output.streams.length) return; + + const convertStream = ( + stream: PipelineOutputWithoutEncoderSettings, + index: number + ): OutputStream => { + return { + name: `Stream ${index + 1}`, + pipelineIndex: 0, + ip: stream.local_ip, + srtMode: stream.srt_mode, + srtPassphrase: stream.srt_passphrase, + port: stream.local_port, + srt_stream_id: stream.srt_stream_id + }; + }; + return output.streams.map((stream, index) => { + return ( + handleUpdateStream(index, field, value)} + onDelete={() => { + handleDeleteStream(index); + }} + /> + ); + }); + }; + + return ( +
    +

    {output.uuid}

    + {getOutputFields(output)} +
    {getOutputStreams(output)}
    + +
    + ); +}; + +export default ProductionOutputEdit; diff --git a/src/components/production/outputs/ProductionOutputs.tsx b/src/components/production/outputs/ProductionOutputs.tsx new file mode 100644 index 00000000..c8817f5e --- /dev/null +++ b/src/components/production/outputs/ProductionOutputs.tsx @@ -0,0 +1,37 @@ +import cloneDeep from 'lodash.clonedeep'; +import { PipelineOutput, PipelineSettings } from '../../../interfaces/pipeline'; +import Section from '../../section/Section'; +import ProductionOutputsCard from './ProductionOutputsCard'; + +interface ProductionOutputsProps { + outputs: PipelineOutput[][]; + onOuputsChange: (outputs: PipelineOutput[][]) => void; + pipelines: PipelineSettings[]; +} + +const ProductionOutputs: React.FC = (props) => { + const { outputs, onOuputsChange, pipelines } = props; + + const onChange = (localOutputs: PipelineOutput[], index: number) => { + const newOutputs = cloneDeep(outputs); + newOutputs[index] = localOutputs; + onOuputsChange(newOutputs); + }; + + return ( +
    + {pipelines.map((pipeline, index) => ( + + onChange(outputs, index) + } + /> + ))} +
    + ); +}; + +export default ProductionOutputs; diff --git a/src/components/production/outputs/ProductionOutputsCard.tsx b/src/components/production/outputs/ProductionOutputsCard.tsx new file mode 100644 index 00000000..3aea5467 --- /dev/null +++ b/src/components/production/outputs/ProductionOutputsCard.tsx @@ -0,0 +1,125 @@ +import { useEffect, useState } from 'react'; +import { PipelineOutput, PipelineSettings } from '../../../interfaces/pipeline'; +import ProductionOutputEdit from './ProductionOutputEdit'; +import cloneDeep from 'lodash.clonedeep'; + +interface ProductionOutputsCardProps { + pipeline: PipelineSettings; + outputs?: PipelineOutput[]; + onOutputsChange: (outputs: PipelineOutput[]) => void; +} + +const ProductionOutputsCard: React.FC = (props) => { + const { pipeline, outputs: outputsProp, onOutputsChange } = props; + const initialOutputs = () => { + return [ + { + uuid: 'program1', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'program2', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'clean', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'aux1', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'aux2', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'iso1', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'iso2', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + }, + { + uuid: 'quad', + settings: { + video_bit_depth: pipeline.bit_depth, + video_format: pipeline.format, + video_kilobit_rate: pipeline.video_kilobit_rate + }, + streams: [] + } + ]; + }; + const [outputs, setOutputs] = useState( + outputsProp?.length ? outputsProp : initialOutputs() + ); + + useEffect(() => { + onOutputsChange(outputs); + }, [outputs]); + + const onChange = (output: PipelineOutput, index: number) => { + const newOutputs = cloneDeep(outputs); + newOutputs[index] = output; + setOutputs(newOutputs); + }; + + return ( +
    +
    + {pipeline.pipeline_readable_name + ' outputs'} +
    +
    + {outputs.map((o, index) => ( + onChange(output, index)} + /> + ))} +
    +
    + ); +}; + +export default ProductionOutputsCard; diff --git a/src/components/production/pipelines/ProductionPipelineCard.tsx b/src/components/production/pipelines/ProductionPipelineCard.tsx new file mode 100644 index 00000000..5c05c974 --- /dev/null +++ b/src/components/production/pipelines/ProductionPipelineCard.tsx @@ -0,0 +1,105 @@ +'use client'; + +import { useState } from 'react'; +import { ResourcesCompactPipelineResponse } from '../../../../types/ateliere-live'; +import { PipelineSettings } from '../../../interfaces/pipeline'; +import PipelineNameDropDown from '../../dropDown/PipelineNameDropDown'; +import cloneDeep from 'lodash.clonedeep'; + +interface PipelineCardInfoProps { + label: string; + value: string | number | boolean; +} + +export const PipelineCardInfo: React.FC = (props) => { + const { label, value } = props; + + return ( +
    +
    {label + ':'}
    +
    {value.toString()}
    +
    + ); +}; + +interface ProductionPipelineCardProps { + pipeline: PipelineSettings; + pipelines: ResourcesCompactPipelineResponse[]; + onPipelineChange: (pipeline: PipelineSettings) => void; +} + +const ProductionPipelineCard: React.FC = ( + props +) => { + const { pipeline, pipelines, onPipelineChange } = props; + + const updatePipelineID = (id: string) => { + const newPipeline = cloneDeep(pipeline); + newPipeline.pipeline_id = id; + onPipelineChange(newPipeline); + }; + + return ( +
    +
    + ({ + option: pipeline.name, + id: pipeline.uuid, + available: pipeline.streams.length === 0 + }) + )} + value={pipeline.pipeline_id || ''} + onChange={updatePipelineID} + /> +
    +
    + + + + + + + + + + + + + + +
    +
    + ); +}; + +export default ProductionPipelineCard; diff --git a/src/components/production/pipelines/ProductionPipelines.tsx b/src/components/production/pipelines/ProductionPipelines.tsx new file mode 100644 index 00000000..93951bee --- /dev/null +++ b/src/components/production/pipelines/ProductionPipelines.tsx @@ -0,0 +1,46 @@ +import cloneDeep from 'lodash.clonedeep'; +import { usePipelines } from '../../../hooks/pipelines'; +import { PipelineSettings } from '../../../interfaces/pipeline'; +import Section from '../../section/Section'; +import ProductionPipelineCard from './ProductionPipelineCard'; +import { LoadingCover } from '../../loader/LoadingCover'; + +interface ProductionPipelinesProps { + pipelines: PipelineSettings[]; + onChange: (pipelines: PipelineSettings[]) => void; +} + +const ProductionPipelines: React.FC = (props) => { + const { pipelines: productionPipelines, onChange } = props; + const [apiPipelines] = usePipelines(); + + const onPipelineChange = (pipeline: PipelineSettings, index: number) => { + const newPipelines = cloneDeep(productionPipelines); + newPipelines[index] = pipeline; + onChange(newPipelines); + }; + + return ( +
    +
    + {(!!apiPipelines?.length && + productionPipelines.map((pipeline, index) => ( + + onPipelineChange(pipeline, index) + } + /> + ))) || ( +
    + +
    + )} +
    +
    + ); +}; + +export default ProductionPipelines; diff --git a/src/components/production/sources/ProductionSourceList.tsx b/src/components/production/sources/ProductionSourceList.tsx new file mode 100644 index 00000000..d8b6b204 --- /dev/null +++ b/src/components/production/sources/ProductionSourceList.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { useState } from 'react'; +import styles from './../../sourceList/SourceList.module.scss'; +import { IconX } from '@tabler/icons-react'; +import { SourceWithId } from '../../../interfaces/Source'; +import SourceListItem from '../../sourceListItem/SourceListItem'; +import FilterContext from '../../../contexts/FilterContext'; +import FilterOptions from '../../filter/FilterOptions'; + +interface SourceListProps { + sources: Map; + inventoryVisible?: boolean; + onClose?: () => void; + isDisabledFunc?: (source: SourceWithId) => boolean; + action?: (source: SourceWithId) => void; + actionText?: string; + locked: boolean; +} + +const ProductionSourceList: 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 ProductionSourceList; diff --git a/src/components/production/sources/ProductionSources.tsx b/src/components/production/sources/ProductionSources.tsx new file mode 100644 index 00000000..887aab0e --- /dev/null +++ b/src/components/production/sources/ProductionSources.tsx @@ -0,0 +1,688 @@ +'use client'; + +import { useContext, useEffect, useMemo, useState } from 'react'; +import { useSources } from '../../../hooks/sources/useSources'; +import { + AddSourceStatus, + DeleteSourceStatus, + SourceReference, + SourceWithId +} from '../../../interfaces/Source'; +import { + useCreateStream, + useDeleteStream, + useUpdateStream +} from '../../../hooks/streams'; +import { AddSourceModal } from '../../modal/AddSourceModal'; +import { DndProvider } from 'react-dnd'; +import SourceCards from '../../sourceCards/SourceCards'; +import { RemoveSourceModal } from '../../modal/RemoveSourceModal'; +import { AddInput } from '../../addInput/AddInput'; +import { Select } from '../../select/Select'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { useDeleteHtmlSource } from '../../../hooks/renderingEngine/useDeleteHtmlSource'; +import { useDeleteMediaSource } from '../../../hooks/renderingEngine/useDeleteMediaSource'; +import { useCreateHtmlSource } from '../../../hooks/renderingEngine/useCreateHtmlSource'; +import { useCreateMediaSource } from '../../../hooks/renderingEngine/useCreateMediaSource'; +import { CreateHtmlModal } from '../../modal/renderingEngineModals/CreateHtmlModal'; +import { CreateMediaModal } from '../../modal/renderingEngineModals/CreateMediaModal'; +import { GlobalContext } from '../../../contexts/GlobalContext'; +import { + usePutProductionPipelineSourceAlignmentAndLatency, + useReplaceProductionSourceStreamIds +} from '../../../hooks/productions'; +import { useIngestSourceId } from '../../../hooks/ingests'; +import { useMultiviews } from '../../../hooks/multiviews'; +import { updateSetupItem } from '../../../hooks/items/updateSetupItem'; +import { ISource } from '../../../hooks/useDragableItems'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import ProductionSourceList from './ProductionSourceList'; +import Section from '../../section/Section'; +import { MultiviewSettings } from '../../../interfaces/multiview'; +import { PipelineSettings } from '../../../interfaces/pipeline'; +import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; +import useEffectNotOnMount from '../../../hooks/utils/useEffectNotOnMount'; +import { LoadingCover } from '../../loader/LoadingCover'; +import { Production } from '../../../interfaces/production'; + +interface ProductionSourcesProps { + sources: SourceReference[]; + updateSources: (sources: SourceReference[]) => void; + multiviewPipelineId?: string; + multiviews: MultiviewSettings[]; + isProductionActive: boolean; + pipelines: PipelineSettings[]; + updatePipelines: (pipelines: PipelineSettings[]) => void; + productionId: string; + refreshProduction: () => void; +} + +const ProductionSources: React.FC = (props) => { + const { + sources: sourcesProp, + updateSources, + multiviewPipelineId, + multiviews, + isProductionActive, + pipelines, + updatePipelines, + productionId, + refreshProduction + } = props; + + const [apiSources, sourcesLoading] = useSources(); + const [selectedSources, setSelectedSources] = useState( + sourcesProp || [] + ); + + useEffectNotOnMount(() => { + updateSources(selectedSources); + }, [selectedSources]); + + const t = useTranslate(); + const { locked } = useContext(GlobalContext); + const putProductionPipelineSourceAlignmentAndLatency = + usePutProductionPipelineSourceAlignmentAndLatency(); + const [addSourceStatus, setAddSourceStatus] = useState(); + const [deleteSourceStatus, setDeleteSourceStatus] = + useState(); + const [isHtmlModalOpen, setIsHtmlModalOpen] = useState(false); + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false); + + const [updateStream, loading] = useUpdateStream(); + const [getIngestSourceId] = useIngestSourceId(); + const [updateMultiviewViews] = useMultiviews(); + const selectedProductionItems = useMemo(() => { + return selectedSources.map((prod: any) => prod._id) || []; + }, [selectedSources]); + const [inventoryVisible, setInventoryVisible] = useState(false); + + //SOURCES + + const [selectedValue, setSelectedValue] = useState( + t('production.add_other_source_type') + ); + const [addSourceModal, setAddSourceModal] = useState(false); + const [removeSourceModal, setRemoveSourceModal] = useState(false); + const [selectedSource, setSelectedSource] = useState< + SourceWithId | undefined + >(); + const [selectedSourceRef, setSelectedSourceRef] = useState< + SourceReference | undefined + >(); + const [createStream, loadingCreateStream] = useCreateStream(); + const [deleteStream, loadingDeleteStream] = useDeleteStream(); + + // Create source + const [firstEmptySlot] = useGetFirstEmptySlot(); + + // Rendering engine + const [deleteHtmlSource, deleteHtmlLoading] = useDeleteHtmlSource(); + const [deleteMediaSource, deleteMediaLoading] = useDeleteMediaSource(); + const [createHtmlSource, createHtmlLoading] = useCreateHtmlSource(); + const [createMediaSource, createMediaLoading] = useCreateMediaSource(); + + const replaceProductionSourceStreamIds = + useReplaceProductionSourceStreamIds(); + + const updateMultiview = ( + source: SourceReference, + productionSources: SourceReference[] + ) => { + multiviews?.map((singleMultiview) => { + if (multiviewPipelineId && multiviews && singleMultiview.multiview_id) { + updateMultiviewViews( + multiviewPipelineId, + productionSources, + source, + singleMultiview + ); + } + }); + }; + + const updateSource = (source: SourceReference) => { + const updatedSources = updateSetupItem(source, selectedSources); + setSelectedSources(updatedSources); + updateMultiview(source, updatedSources); + }; + + const addSource = (source: SourceReference) => { + const newSources = [...selectedSources, source]; + setSelectedSources(newSources); + setAddSourceModal(false); + setSelectedSource(undefined); + }; + + const removeSource = (source: SourceReference) => { + const tempItems = selectedSources.filter( + (tempItem) => tempItem._id !== source._id + ); + setSelectedSources(tempItems); + }; + + const updatePipelinesWithSource = async (source: SourceWithId) => { + const updatedPipelines = await Promise.all( + pipelines.map(async (pipeline) => { + const newSource = { + source_id: await getIngestSourceId( + source.ingest_name, + source.ingest_source_name + ), + settings: { + alignment_ms: pipeline.alignment_ms, + max_network_latency_ms: pipeline.max_network_latency_ms + } + }; + + const exists = pipeline.sources?.some( + (s) => s.source_id === newSource.source_id + ); + + const updatedSources = exists + ? pipeline.sources + : [...(pipeline.sources || []), newSource]; + + return { + ...pipeline, + sources: updatedSources + }; + }) + ); + updatePipelines(updatedPipelines); + }; + + const addSourceAction = (source: SourceWithId) => { + if (isProductionActive) { + setSelectedSource(source); + setAddSourceModal(true); + } else { + const input: SourceReference = { + _id: source._id.toString(), + type: 'ingest_source', + label: source.ingest_source_name, + input_slot: firstEmptySlot(selectedSources) + }; + addSource(input); + updatePipelinesWithSource(source); + } + }; + + const addHtmlSource = (height: number, width: number, url: string) => { + const sourceToAdd: SourceReference = { + type: 'html', + label: `HTML ${firstEmptySlot(selectedSources)}`, + input_slot: firstEmptySlot(selectedSources), + html_data: { + height: height, + url: url, + width: width + } + }; + // MIGHT NEED TO REFRESH PRODUCTION @SANDRA + addSource(sourceToAdd); + + if (isProductionActive && sourceToAdd.html_data) { + createHtmlSource( + pipelines, + sourceToAdd.input_slot, + sourceToAdd.html_data, + sourceToAdd + ); + } + }; + + const addMediaSource = (filename: string) => { + const sourceToAdd: SourceReference = { + type: 'mediaplayer', + label: `Media Player ${firstEmptySlot(selectedSources)}`, + input_slot: firstEmptySlot(selectedSources), + media_data: { + filename: filename + } + }; + // MIGHT NEED TO REFRESH + addSource(sourceToAdd); + if (isProductionActive && sourceToAdd.media_data) { + createMediaSource( + pipelines, + sourceToAdd.input_slot, + sourceToAdd.media_data, + sourceToAdd + ); + } + }; + + const isDisabledFunction = (source: SourceWithId): boolean => { + return selectedProductionItems?.includes(source._id.toString()); + }; + + const handleOpenModal = (type: 'html' | 'media') => { + if (type === 'html') { + setIsHtmlModalOpen(true); + } else if (type === 'media') { + setIsMediaModalOpen(true); + } + }; + + const handleAddSource = async () => { + setAddSourceStatus(undefined); + if ( + isProductionActive && + selectedSource && + multiviews.some((singleMultiview: any) => singleMultiview?.layout?.views) + ) { + for (let i = 0; i < pipelines.length; i++) { + const pipeline = pipelines[i]; + + if (!pipeline.sources) { + pipeline.sources = []; + } + + const newSource = { + source_id: await getIngestSourceId( + selectedSource.ingest_name, + selectedSource.ingest_source_name + ), + settings: { + alignment_ms: pipeline.alignment_ms, + max_network_latency_ms: pipeline.max_network_latency_ms + } + }; + + updatePipelines( + pipelines.map((p: any, index: number) => { + if (index === i) { + p.sources.push(newSource); + } + return p; + }) + ); + } + + const result = await createStream( + selectedSource, + pipelines, + firstEmptySlot(selectedSources) + ); + if (!result.ok) { + if (!result.value) { + setAddSourceStatus({ + success: false, + steps: [{ step: 'add_stream', success: false }] + }); + } else { + setAddSourceStatus({ + success: false, + steps: result.value.steps + }); + } + } + if (result.ok) { + if (result.value.success) { + 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: firstEmptySlot(selectedSources) + }; + addSource(sourceToAdd); + setAddSourceStatus(undefined); + } else { + setAddSourceStatus({ success: false, steps: result.value.steps }); + } + } + } + }; + + const handleRemoveSource = async () => { + if (isProductionActive && selectedSourceRef) { + if (!multiviews || multiviews.length === 0) return; + + const viewToUpdate = multiviews.some((multiview: any) => + multiview.layout.views.find( + (v: any) => v.input_slot === selectedSourceRef.input_slot + ) + ); + + if ( + selectedSourceRef.stream_uuids && + selectedSourceRef.stream_uuids.length > 0 + ) { + if (!viewToUpdate) { + if (!pipelines[0].pipeline_id) return; + + const result = await deleteStream( + selectedSourceRef.stream_uuids, + pipelines, + multiviews, + selectedSources, + 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) { + removeSource(selectedSourceRef); + return; + } + } + return; + } + + removeSource(selectedSourceRef); + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + return; + } + + const result = await deleteStream( + selectedSourceRef.stream_uuids, + pipelines, + multiviews, + selectedSources, + 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) { + removeSource(selectedSourceRef); + return; + } + } + return; + } + } + + if ( + selectedSourceRef.type === 'html' || + selectedSourceRef.type === 'mediaplayer' + ) { + for (let i = 0; i < pipelines.length; i++) { + const pipelineId = pipelines[i].pipeline_id; + if (pipelineId) { + if (selectedSourceRef.type === 'html') { + await deleteHtmlSource( + pipelineId, + selectedSourceRef.input_slot, + multiviews, + selectedSources + ); + } else if (selectedSourceRef.type === 'mediaplayer') { + await deleteMediaSource( + pipelineId, + selectedSourceRef.input_slot, + multiviews, + selectedSources + ); + } + } + } + } + + removeSource(selectedSourceRef); + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + } + }; + + const handleAbortAddSource = () => { + setAddSourceStatus(undefined); + setAddSourceModal(false); + setSelectedSource(undefined); + }; + + const handleAbortRemoveSource = () => { + setRemoveSourceModal(false); + setSelectedSource(undefined); + setDeleteSourceStatus(undefined); + }; + + const handleSetPipelineSourceSettings = ( + source: ISource, + sourceId: number, + data: { + pipeline_uuid: string; + stream_uuid: string; + alignment: number; + latency: number; + }[], + shouldRestart?: boolean, + streamUuids?: string[] + ) => { + if (productionId && source?.ingest_name && source?.ingest_source_name) { + data.forEach(({ pipeline_uuid, stream_uuid, alignment, latency }) => { + putProductionPipelineSourceAlignmentAndLatency( + productionId, + pipeline_uuid, + source.ingest_name, + source.ingest_source_name, + alignment, + latency + ).then(() => refreshProduction()); + + if (isProductionActive) { + updateStream(stream_uuid, alignment); + } + + updatePipelines( + pipelines.map((pipeline: any) => { + if (pipeline.pipeline_id === pipeline_uuid) { + pipeline.sources?.map((source: any) => { + if (source.source_id === sourceId) { + source.settings.alignment_ms = alignment; + source.settings.max_network_latency_ms = latency; + } + }); + } + return pipeline; + }) + ); + }); + } + if (shouldRestart && streamUuids) { + const sourceToDeleteFrom = selectedSources.find((source) => + source.stream_uuids?.includes(streamUuids[0]) + ); + deleteStream( + streamUuids, + pipelines, + multiviews, + selectedSources, + source.input_slot + ) + .then(() => { + delete sourceToDeleteFrom?.stream_uuids; + }) + .then(() => + setTimeout(async () => { + const result = await createStream( + source, + pipelines, + source.input_slot + ); + if (result.ok) { + if (result.value.success) { + const newStreamUuids = result.value.streams.map( + (r) => r.stream_uuid + ); + if (sourceToDeleteFrom?._id) { + replaceProductionSourceStreamIds( + productionId, + sourceToDeleteFrom?._id, + newStreamUuids + ).then(() => refreshProduction()); + } + } + } + }, 1500) + ); + } + }; + + useEffect(() => { + if (selectedValue === t('production.source')) { + setInventoryVisible(true); + } + }, [selectedValue]); + + const isAddButtonDisabled = + (selectedValue !== 'HTML' && selectedValue !== 'Media Player') || locked; + + return ( +
    +
    + {sourcesLoading && } + <> +
    +
    + {selectedSources && apiSources.size > 0 && ( + + { + updateSource(source); + }} + onSourceRemoval={(source: SourceReference) => { + if (isProductionActive) { + setSelectedSourceRef(source); + setRemoveSourceModal(true); + } else { + removeSource(source); + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + } + }} + loading={loading} + /> + {removeSourceModal && selectedSourceRef && ( + + )} + + )} +
    + setInventoryVisible(true)} + disabled={false} + /> +
    +