From c66924501ee54352f5fb04ea09d53d8ed95a6a48 Mon Sep 17 00:00:00 2001 From: Linda Malm <109201562+malmen237@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:14:10 +0200 Subject: [PATCH] Feat/specify multiview layout when doing production-config(#16) * feat: added another mv-preset and updated code to be arr instead of single-object * feat: added optional style to be added to the options-dropdown * feat: first draft of a multiview-layout setting * feat: possible to create new layouts as multiview-previews and add it to productions --- src/api/manager/presets.ts | 11 + src/api/mongoClient/dbClient.ts | 3 +- src/api/mongoClient/defaults/preset.ts | 388 ++++++++++++------ src/app/api/manager/presets/route.ts | 28 +- src/app/production/[id]/page.tsx | 1 + .../ConfigureOutputModal.tsx | 177 +++++--- .../modal/configureOutputModal/Input.tsx | 5 +- .../MultiviewLayoutSettings.tsx | 171 ++++++++ .../MultiviewSettings.tsx | 66 ++- .../modal/configureOutputModal/Options.tsx | 24 +- .../startProduction/ConfigureOutputButton.tsx | 6 +- src/hooks/multiviewPreset.ts | 16 + src/hooks/useConfigureMultiviewLayout.tsx | 65 +++ src/hooks/useSetupMultiviewLayout.tsx | 35 ++ src/i18n/locales/en.ts | 8 + src/i18n/locales/sv.ts | 8 + 16 files changed, 810 insertions(+), 202 deletions(-) create mode 100644 src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx create mode 100644 src/hooks/useConfigureMultiviewLayout.tsx create mode 100644 src/hooks/useSetupMultiviewLayout.tsx diff --git a/src/api/manager/presets.ts b/src/api/manager/presets.ts index 57fd66d..5fc45e2 100644 --- a/src/api/manager/presets.ts +++ b/src/api/manager/presets.ts @@ -18,6 +18,7 @@ export async function getMultiviewPresets(): Promise { const db = await getDatabase(); return await db.collection('multiviews').find({}).toArray(); } + export async function getMultiviewPreset( id: string ): Promise> { @@ -26,6 +27,16 @@ export async function getMultiviewPreset( .collection('multiviews') .findOne({ _id: new ObjectId(id) })) as WithId; } + +export async function putMultiviewPreset( + newMultiviewPreset: MultiviewPreset +): Promise { + const db = await getDatabase(); + await db + .collection('multiviews') + .insertOne({ ...newMultiviewPreset, _id: new ObjectId() }); +} + export async function putPreset( id: string, preset: PresetWithId diff --git a/src/api/mongoClient/dbClient.ts b/src/api/mongoClient/dbClient.ts index ab1780c..4ab4ccc 100644 --- a/src/api/mongoClient/dbClient.ts +++ b/src/api/mongoClient/dbClient.ts @@ -50,7 +50,8 @@ async function bootstrapDbCollections(db: Db) { const multiviews = await db.collection('multiviews').countDocuments(); if (multiviews === 0) { Log().info('Bootstrapping database with default multiview'); - await db.collection('multiviews').insertOne(defaultMultiview); + + await db.collection('multiviews').insertMany(defaultMultiview); } else { await migrateMultiviewPresets(db); } diff --git a/src/api/mongoClient/defaults/preset.ts b/src/api/mongoClient/defaults/preset.ts index 7c91e82..e3022f6 100644 --- a/src/api/mongoClient/defaults/preset.ts +++ b/src/api/mongoClient/defaults/preset.ts @@ -181,125 +181,273 @@ export const ldOnlyPreset = { ] }; -export const defaultMultiview = { - _id: new ObjectId('6582ff61987fa290e66ba95c'), - name: '10 inputs HD', - layout: { - output_height: 1080, - output_width: 1920, - views: [ - { - input_slot: 1002, - x: 0, - y: 0, - height: 540, - width: 960, - label: 'Preview' - }, - { - input_slot: 1001, - x: 960, - y: 0, - height: 540, - width: 960, - label: 'Program' - }, - { - input_slot: 1, - x: 0, - y: 540, - height: 270, - width: 384, - label: 'Input 1' - }, - { - input_slot: 2, - x: 384, - y: 540, - height: 270, - width: 384, - label: 'Input 2' - }, - { - input_slot: 3, - x: 768, - y: 540, - height: 270, - width: 384, - label: 'Input 3' - }, - { - input_slot: 4, - x: 1152, - y: 540, - height: 270, - width: 384, - label: 'Input 4' - }, - { - input_slot: 5, - x: 1536, - y: 540, - height: 270, - width: 384, - label: 'Input 5' - }, - { - input_slot: 6, - x: 0, - y: 810, - height: 270, - width: 384, - label: 'Input 6' - }, - { - input_slot: 7, - x: 384, - y: 810, - height: 270, - width: 384, - label: 'Input 7' - }, - { - input_slot: 8, - x: 768, - y: 810, - height: 270, - width: 384, - label: 'Input 8' - }, - { - input_slot: 9, - x: 1152, - y: 810, - height: 270, - width: 384, - label: 'Input 9' - }, - { - input_slot: 10, - x: 1536, - y: 810, - height: 270, - width: 384, - label: 'Input 10' - } - ] +export const defaultMultiview = [ + { + _id: new ObjectId('6582ff61987fa290e66ba95c'), + name: '10 inputs HD', + layout: { + output_height: 1080, + output_width: 1920, + views: [ + { + input_slot: 1002, + x: 0, + y: 0, + height: 540, + width: 960, + label: 'Preview' + }, + { + input_slot: 1001, + x: 960, + y: 0, + height: 540, + width: 960, + label: 'Program' + }, + { + input_slot: 1, + x: 0, + y: 540, + height: 270, + width: 384, + label: 'Input 1' + }, + { + input_slot: 2, + x: 384, + y: 540, + height: 270, + width: 384, + label: 'Input 2' + }, + { + input_slot: 3, + x: 768, + y: 540, + height: 270, + width: 384, + label: 'Input 3' + }, + { + input_slot: 4, + x: 1152, + y: 540, + height: 270, + width: 384, + label: 'Input 4' + }, + { + input_slot: 5, + x: 1536, + y: 540, + height: 270, + width: 384, + label: 'Input 5' + }, + { + input_slot: 6, + x: 0, + y: 810, + height: 270, + width: 384, + label: 'Input 6' + }, + { + input_slot: 7, + x: 384, + y: 810, + height: 270, + width: 384, + label: 'Input 7' + }, + { + input_slot: 8, + x: 768, + y: 810, + height: 270, + width: 384, + label: 'Input 8' + }, + { + input_slot: 9, + x: 1152, + y: 810, + height: 270, + width: 384, + label: 'Input 9' + }, + { + input_slot: 10, + x: 1536, + y: 810, + height: 270, + width: 384, + label: 'Input 10' + } + ] + }, + output: { + format: 'MPEG-TS-SRT', + frame_rate_d: 1, + frame_rate_n: 50, + local_ip: '0.0.0.0', + local_port: 1234, + remote_ip: '0.0.0.0', + remote_port: 1234, + srt_mode: 'listener', + srt_latency_ms: 120, + srt_passphrase: '', + video_format: 'AVC', + video_kilobit_rate: 5000, + speed_quality_balance: 'balanced', + pic_mode: 'pic_mode_ip' + } }, - output: { - format: 'MPEG-TS-SRT', - frame_rate_d: 1, - frame_rate_n: 50, - local_ip: '0.0.0.0', - local_port: 1234, - remote_ip: '0.0.0.0', - remote_port: 1234, - srt_mode: 'listener', - srt_latency_ms: 120, - srt_passphrase: '', - video_format: 'AVC', - video_kilobit_rate: 5000, - speed_quality_balance: 'balanced', - pic_mode: 'pic_mode_ip' + { + _id: new ObjectId('65cb266c00fecda4a1faf977'), + name: '12 inputs HD', + layout: { + output_height: 1080, + output_width: 1920, + views: [ + { + input_slot: 1002, + x: 0, + y: 0, + height: 540, + width: 960, + label: 'Preview' + }, + { + input_slot: 1001, + x: 960, + y: 0, + height: 540, + width: 960, + label: 'Program' + }, + { + input_slot: 1, + x: 0, + y: 540, + height: 270, + width: 384, + label: 'Input 1' + }, + { + input_slot: 2, + x: 384, + y: 540, + height: 270, + width: 384, + label: 'Input 2' + }, + { + input_slot: 3, + x: 768, + y: 540, + height: 270, + width: 384, + label: 'Input 3' + }, + { + input_slot: 4, + x: 1152, + y: 540, + height: 270, + width: 384, + label: 'Input 4' + }, + { + input_slot: 5, + x: 1536, + y: 540, + height: 270, + width: 384, + label: 'Input 5' + }, + { + input_slot: 6, + x: 0, + y: 810, + height: 270, + width: 384, + label: 'Input 6' + }, + { + input_slot: 7, + x: 384, + y: 810, + height: 270, + width: 384, + label: 'Input 7' + }, + { + input_slot: 8, + x: 768, + y: 810, + height: 270, + width: 384, + label: 'Input 8' + }, + { + input_slot: 9, + x: 1152, + y: 810, + height: 270, + width: 384, + label: 'Input 9' + }, + { + input_slot: 10, + x: 1536, + y: 810, + height: 135, + width: 192, + label: 'VS' + }, + { + input_slot: 11, + x: 1728, + y: 810, + height: 135, + width: 192, + label: 'UR' + }, + { + input_slot: 12, + x: 1536, + y: 945, + height: 135, + width: 192, + label: 'OV' + }, + { + input_slot: 13, + x: 1728, + y: 945, + height: 135, + width: 192, + label: 'CG' + } + ] + }, + output: { + format: 'MPEG-TS-SRT', + frame_rate_d: 1, + frame_rate_n: 50, + local_ip: '0.0.0.0', + local_port: 4567, + remote_ip: '0.0.0.0', + remote_port: 1234, + srt_mode: 'listener', + srt_latency_ms: 60, + srt_passphrase: '', + video_format: 'AVC', + video_kilobit_rate: 5000, + speed_quality_balance: 'balanced', + pic_mode: 'pic_mode_ip' + } } -}; +]; diff --git a/src/app/api/manager/presets/route.ts b/src/app/api/manager/presets/route.ts index 1f24176..228f165 100644 --- a/src/app/api/manager/presets/route.ts +++ b/src/app/api/manager/presets/route.ts @@ -1,6 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getPresets } from '../../../../api/manager/presets'; +import { + getPresets, + putMultiviewPreset +} from '../../../../api/manager/presets'; import { isAuthenticated } from '../../../../api/manager/auth'; +import { Log } from '../../../../api/logger'; +import { MultiviewPreset } from '../../../../interfaces/preset'; export async function GET(request: NextRequest): Promise { if (!(await isAuthenticated())) { @@ -17,3 +22,24 @@ export async function GET(request: NextRequest): Promise { }); } } + +export async function PUT(request: NextRequest): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + const body = (await request.json()) as MultiviewPreset; + const newMultiviewPreset = await putMultiviewPreset(body); + return await new NextResponse(JSON.stringify(newMultiviewPreset), { + status: 200 + }); + } catch (error) { + Log().warn('Could not update preset', error); + return new NextResponse(`Error searching DB! Error: ${error}`, { + status: 500 + }); + } +} diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index dd67b62..53c2f4c 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -638,6 +638,7 @@ export default function ProductionConfiguration({ params }: PageProps) { disabled={productionSetup?.isActive || locked} preset={selectedPreset} updatePreset={updatePreset} + production={productionSetup} /> void; updatePreset: (preset: Preset) => void; + production: Production | undefined; }; export function ConfigureOutputModal({ open, preset, onClose, - updatePreset + updatePreset, + production }: ConfigureOutputModalProps) { const defaultState = (pipelines: PipelineSettings[]) => { const streamsPerPipe = pipelines.map((pipe, i) => { @@ -70,6 +75,10 @@ export function ConfigureOutputModal({ const [portDuplicateIndexes, setPortDuplicateIndexes] = useState( [] ); + const [layoutModalOpen, setLayoutModalOpen] = useState(null); + const [newMultiviewPreset, setNewMultiviewPreset] = + useState(null); + const addNewPreset = usePutMultiviewPreset(); const t = useTranslate(); useEffect(() => { @@ -87,6 +96,7 @@ export function ConfigureOutputModal({ }, [preset]); const clearInputs = () => { + setLayoutModalOpen(null); setMultiviews(preset.pipelines[0].multiviews || []); setOutputStreams(defaultState(preset.pipelines)); onClose(); @@ -130,6 +140,19 @@ export function ConfigureOutputModal({ onClose(); }; + const onUpdateLayoutPreset = () => { + if (!newMultiviewPreset) { + toast.error(t('preset.no_updated_layout')); + return; + } + addNewPreset(newMultiviewPreset); + setLayoutModalOpen(null); + }; + + const closeLayoutModal = () => { + setLayoutModalOpen(null); + }; + const streamsToProgramOutputs = ( pipelineIndex: number, outputStreams?: OutputStream[] @@ -264,74 +287,94 @@ export function ConfigureOutputModal({ return ( clearInputs()}> -
- {preset.pipelines.map((pipeline, i) => { - return ( - o.pipelineIndex === i)} - addStream={addStream} - updateStream={updateStream} - updateStreams={updateStreams} - deleteStream={deleteStream} - /> - ); - })} - {multiviews && - multiviews.length > 0 && - multiviews.map((singleItem, index) => { + {!layoutModalOpen && ( +
+ {preset.pipelines.map((pipeline, i) => { return ( -
-
-
- - handleUpdateMultiview(input, index) - } - portDuplicateError={ - portDuplicateIndexes.length > 0 - ? portDuplicateIndexes.includes(index) - : false - } - /> -
1 ? 'justify-between' : 'justify-end' - }`} - > - {multiviews.length > 1 && ( - - )} - {multiviews.length === index + 1 && ( - - )} -
-
-
+ o.pipelineIndex === i)} + addStream={addStream} + updateStream={updateStream} + updateStreams={updateStreams} + deleteStream={deleteStream} + /> ); })} -
- clearInputs()} onSave={onSave} /> + {multiviews && + multiviews.length > 0 && + multiviews.map((singleItem, index) => { + return ( +
+
+
+ + setLayoutModalOpen(input) + } + lastItem={multiviews.length === index + 1} + multiview={singleItem} + handleUpdateMultiview={(input) => + handleUpdateMultiview(input, index) + } + portDuplicateError={ + portDuplicateIndexes.length > 0 + ? portDuplicateIndexes.includes(index) + : false + } + /> +
1 + ? 'justify-between' + : 'justify-end' + }`} + > + {multiviews.length > 1 && ( + + )} + {multiviews.length === index + 1 && ( + + )} +
+
+
+ ); + })} +
+ )} + {!!layoutModalOpen && ( + + setNewMultiviewPreset(newLayout) + } + /> + )} + (layoutModalOpen ? closeLayoutModal() : clearInputs())} + onSave={() => (layoutModalOpen ? onUpdateLayoutPreset() : onSave())} + />
); } diff --git a/src/components/modal/configureOutputModal/Input.tsx b/src/components/modal/configureOutputModal/Input.tsx index 25c0954..a2b0155 100644 --- a/src/components/modal/configureOutputModal/Input.tsx +++ b/src/components/modal/configureOutputModal/Input.tsx @@ -8,6 +8,7 @@ interface IInput { onKeyDown?: (e: KeyboardEvent) => void; size?: 'small' | 'large'; inputError?: boolean; + placeholder?: string; } export default function Input({ @@ -17,7 +18,8 @@ export default function Input({ type = 'text', onKeyDown, size = 'small', - inputError + inputError, + placeholder }: IInput) { const errorCss = 'border-red-500 focus:border-red-500 focus:outline'; @@ -34,6 +36,7 @@ export default function Input({ } pl-2 pt-1 pb-1 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:border-gray-400 focus:outline-none ${ inputError ? errorCss : '' }`} + placeholder={placeholder ? placeholder : ''} /> ); diff --git a/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx b/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx new file mode 100644 index 0000000..9a37c87 --- /dev/null +++ b/src/components/modal/configureOutputModal/MultiviewLayoutSettings.tsx @@ -0,0 +1,171 @@ +import { useEffect, useState } from 'react'; +import { useMultiviewPresets } from '../../../hooks/multiviewPreset'; +import Options from './Options'; +import { MultiviewPreset } from '../../../interfaces/preset'; +import { useTranslate } from '../../../i18n/useTranslate'; +import { + MultiviewViewsWithId, + useSetupMultiviewLayout +} from '../../../hooks/useSetupMultiviewLayout'; +import { Production } from '../../../interfaces/production'; +import { useConfigureMultiviewLayout } from '../../../hooks/useConfigureMultiviewLayout'; +import { SourceReference } from '../../../interfaces/Source'; +import Input from './Input'; + +type ChangeLayout = { + defaultLabel?: string; + source?: SourceReference; + id: number; +}; + +export default function MultiviewLayoutSettings({ + // configMode sets the mode of the configuration to create or edit, not implemented yet + configMode, + production, + setNewMultiviewPreset +}: { + configMode: string; + production: Production | undefined; + setNewMultiviewPreset: (preset: MultiviewPreset | null) => void; +}) { + const [selectedMultiviewPreset, setSelectedMultiviewPreset] = + useState(null); + const [changedLayout, setChangedLayout] = useState(null); + const [newPresetName, setNewPresetName] = useState(null); + const [multiviewPresets, loading] = useMultiviewPresets(); + const { multiviewPresetLayout } = useSetupMultiviewLayout( + selectedMultiviewPreset + ); + const { multiviewLayout } = useConfigureMultiviewLayout( + selectedMultiviewPreset, + changedLayout?.defaultLabel, + changedLayout?.source, + changedLayout?.id, + configMode, + newPresetName + ); + const t = useTranslate(); + + const multiviewPresetNames = multiviewPresets?.map((preset) => preset.name) + ? multiviewPresets?.map((preset) => preset.name) + : []; + + useEffect(() => { + setNewPresetName(null); + }, [configMode]); + + useEffect(() => { + if (multiviewPresets && multiviewPresets[0]) { + setSelectedMultiviewPreset(multiviewPresets[0]); + } + }, [multiviewPresets]); + + useEffect(() => { + if (multiviewLayout) { + setSelectedMultiviewPreset(multiviewLayout); + setNewMultiviewPreset(multiviewLayout); + } else { + setSelectedMultiviewPreset(null); + setNewMultiviewPreset(null); + } + }, [multiviewLayout]); + + const handlePresetUpdate = (name: string) => { + const presetLayout = multiviewPresets?.find( + (singlePreset) => singlePreset.name === name + ); + setNewPresetName(name); + if (presetLayout) { + setSelectedMultiviewPreset(presetLayout); + } + }; + + const handleChange = (id: number | undefined, value: string) => { + if (production && id && multiviewPresets) { + // Remove 2 from id to remove id for Preview- and Program-view + // Add 1 to index to get the correct input_slot + const idFirstInputView = id - 2 + 1; + const defaultLabel = multiviewPresets[0].layout.views.find( + (item) => item.input_slot === idFirstInputView + )?.label; + production.sources.map((source) => { + if (value === '') { + setChangedLayout({ defaultLabel, id }); + } + if (source.label === value) { + setChangedLayout({ source, id }); + } + }); + } + }; + + const renderPresetModel = () => { + if (multiviewPresetLayout) { + return ( +
+ {multiviewPresetLayout.layout.views.map( + (singleView: MultiviewViewsWithId) => { + const { x, y, width, height, label, id } = singleView; + const previewView = singleView.input_slot === 1002; + const programView = singleView.input_slot === 1001; + + return ( +
+ {production && (previewView || programView) && ( +

{label}

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

{t('preset.multiview_output_settings')}

- handleSetSelectedMultiviewPreset(value)} - /> +
+ handleSetSelectedMultiviewPreset(value)} + /> + {lastItem && ( + // TODO: When possible to edit layout, uncomment the following code and remove the button below + + // <> + // + // {modalOpen && ( + //
+ // + // + //
+ // )} + // + )} +
void; + columnStyle?: boolean; } -export default function Options({ label, options, value, update }: IOPtions) { +export default function Options({ + label, + options, + value, + update, + columnStyle +}: IOptions) { + const t = useTranslate(); return ( -
+