From fa006bf4ee19704e3ce0b36dcac09ef55db29153 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 09:24:02 +0200 Subject: [PATCH 01/41] feat: support media and html sources in prod and web sockets --- .env.sample | 3 + package-lock.json | 32 +++- package.json | 6 +- src/api/ateliereLive/websocket.ts | 45 ++++++ src/api/manager/productions.ts | 1 + src/api/manager/sources.ts | 51 ++++--- src/api/manager/workflow.ts | 45 +++++- src/app/html_input/page.tsx | 10 ++ src/app/production/[id]/page.tsx | 106 ++++++++++--- src/components/addInput/AddInput.tsx | 30 ++++ src/components/addSource/AddSource.tsx | 24 --- src/components/dragElement/DragItem.tsx | 51 ++++--- src/components/filter/FilterDropdown.tsx | 5 +- src/components/filter/FilterOptions.tsx | 8 + src/components/modal/AddSourceModal.tsx | 2 +- src/components/select/Select.tsx | 27 ++++ src/components/sourceCard/SourceCard.tsx | 121 ++++++++++----- src/components/sourceCard/SourceThumbnail.tsx | 58 +++++--- src/components/sourceCards/SourceCards.tsx | 139 +++++++----------- .../sourceListItem/SourceListItem.tsx | 4 +- src/hooks/items/addSetupItem.ts | 3 +- src/hooks/productions.ts | 3 +- src/hooks/useDragableItems.ts | 83 +++++++---- src/i18n/locales/en.ts | 10 +- src/i18n/locales/sv.ts | 10 +- src/interfaces/Source.ts | 4 +- src/middleware.ts | 2 +- 27 files changed, 602 insertions(+), 281 deletions(-) create mode 100644 src/api/ateliereLive/websocket.ts create mode 100644 src/app/html_input/page.tsx create mode 100644 src/components/addInput/AddInput.tsx delete mode 100644 src/components/addSource/AddSource.tsx create mode 100644 src/components/select/Select.tsx diff --git a/.env.sample b/.env.sample index 8c3990f..da9f726 100644 --- a/.env.sample +++ b/.env.sample @@ -14,3 +14,6 @@ BCRYPT_SALT_ROUNDS=${BCRYPT_SALT_ROUNDS:-10} # i18n UI_LANG=${UI_LANG:-en} + +# Mediaplayer - path on the system controller +MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 73c7aff..26159b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -35,7 +36,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", @@ -2512,6 +2514,14 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -11233,6 +11243,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 3d34116..7b3597c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "pretty:format": "prettier --write .", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "next lint", - "dev": "./update_gui_version.sh && next dev", + "dev": "next dev", "build": "next build", "start": "next start", "version:rc": "npm version prerelease --preid=rc", @@ -32,6 +32,7 @@ "@sinclair/typebox": "^0.25.24", "@tabler/icons": "^2.22.0", "@tabler/icons-react": "^2.20.0", + "@types/ws": "^8.5.12", "bcrypt": "^5.1.0", "cron": "^2.3.1", "date-fns": "^2.30.0", @@ -48,7 +49,8 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "tailwind-merge": "^1.13.2", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "ws": "^8.18.0" }, "devDependencies": { "@commitlint/cli": "^17.4.2", diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts new file mode 100644 index 0000000..6f51466 --- /dev/null +++ b/src/api/ateliereLive/websocket.ts @@ -0,0 +1,45 @@ +import WebSocket from 'ws'; + +function createWebSocket(): Promise { + return new Promise((resolve, reject) => { + const ws = new WebSocket(`ws://${process.env.AGILE_WEBSOCKET}`); + ws.on('error', reject); + ws.on('open', () => { + // const send = ws.send.bind(ws); + // ws.send = (message) => { + // console.debug(`[websocket] sending message: ${message}`); + // send(message); + // }; + resolve(ws); + }); + }); +} + +export async function createControlPanelWebSocket() { + const ws = await createWebSocket(); + return { + createHtml: (input: number) => { + ws.send('html reset'); + ws.send(`html create ${input} 1920 1080`); + setTimeout(() => { + ws.send( + `html load ${input} ${process.env.NEXTAUTH_URL}/html_input?input=${input}` + ); + }, 1000); + }, + createMediaplayer: (input: number) => { + ws.send('media reset'); + ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`); + ws.send(`media play ${input}`); + }, + closeHtml: (input: number) => { + ws.send(`html close ${input}`); + }, + closeMediaplayer: (input: number) => { + ws.send(`media close ${input}`); + }, + close: () => { + ws.close(); + } + }; +} diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e68524a..714d822 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -2,6 +2,7 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Production, ProductionWithId } from '../../interfaces/production'; import { Log } from '../logger'; +import { SourceReference, Type } from '../../interfaces/Source'; export async function getProductions(): Promise { const db = await getDatabase(); diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 8bb83e8..a54fb38 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -1,6 +1,6 @@ import inventory from './mocks/inventory.json'; import { Source } from '../../interfaces/Source'; -import { ObjectId } from 'mongodb'; +import { ObjectId, OptionalId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -9,37 +9,44 @@ export function getMockedSources() { export async function postSource(data: Source): Promise { const db = await getDatabase(); - return (await db.collection('inventory').insertOne(data)) - .insertedId as ObjectId; + const insertData: OptionalId> & { _id?: ObjectId } = { + ...data, + _id: typeof data._id === 'string' ? new ObjectId(data._id) : data._id + }; + const result = await db.collection('inventory').insertOne(insertData); + return result.insertedId as ObjectId; } export async function getSources() { const db = await getDatabase(); return await db.collection('inventory').find().toArray(); } - export async function getSourcesByIds(_ids: string[]) { const db = await getDatabase().catch(() => { - throw "Can't connect to Database"; - }); - const objectIds = _ids.map((id: string) => { - return new ObjectId(id); + throw new Error("Can't connect to Database"); }); - return ( - await db - .collection('inventory') - .find({ - _id: { - $in: objectIds - } - }) - .toArray() - ).sort( - (a, b) => - _ids.findIndex((id) => a._id.equals(id)) - - _ids.findIndex((id) => b._id.equals(id)) - ); + const objectIds = _ids.map((id: string) => new ObjectId(id)); + + const sources = await db + .collection('inventory') + .find({ + _id: { + $in: objectIds + } + }) + .toArray(); + + return sources.sort((a, b) => { + const findIndex = (id: ObjectId | string) => + _ids.findIndex((originalId) => + id instanceof ObjectId + ? id.equals(new ObjectId(originalId)) + : id === originalId + ); + + return findIndex(a._id) - findIndex(b._id); + }); } export async function updateSource(source: any) { diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 7538be7..8edc5a9 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -49,6 +49,8 @@ import { Result } from '../../interfaces/result'; import { Monitoring } from '../../interfaces/monitoring'; import { getDatabase } from '../mongoClient/dbClient'; import { updatedMonitoringForProduction } from './job/syncMonitoring'; +import { createControlPanelWebSocket } from '../ateliereLive/websocket'; +import { ObjectId } from 'mongodb'; const isUsed = (pipeline: ResourcesPipelineResponse) => { const hasStreams = pipeline.streams.length > 0; @@ -89,7 +91,7 @@ async function connectIngestSources( source.ingest_source_name, false ); - const audioSettings = await getAudioMapping(source._id); + const audioSettings = await getAudioMapping(new ObjectId(source._id)); const newAudioMapping = audioSettings?.audio_stream?.audio_mapping; const audioMapping = newAudioMapping?.length ? newAudioMapping : [[0, 1]]; @@ -308,6 +310,14 @@ export async function stopProduction( (p) => p.pipeline_id ); + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + for (const source of production.sources) { for (const stream_uuid of source.stream_uuids || []) { await deleteStreamByUuid(stream_uuid).catch((error) => { @@ -316,6 +326,11 @@ export async function stopProduction( } } + htmlSources.map((source) => controlPanelWS.closeHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.closeMediaplayer(source.input_slot) + ); + for (const id of pipelineIds) { Log().info(`Stopping pipeline '${id}'`); if (!id) continue; @@ -449,10 +464,30 @@ export async function startProduction( // Try to setup streams from ingest(s) to pipeline(s) start try { // Get sources from the DB + // Skapa en createHtmlWebSocket, spara + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + + htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); + mediaPlayerSources.map((source) => + controlPanelWS.createMediaplayer(source.input_slot) + ); + + controlPanelWS.close(); + + // Nedan behöver göras efter att vi har skapat en produktion + // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( - production.sources.map((source) => { - return source._id.toString(); - }) + production.sources + .filter((source) => source._id !== undefined) + .map((source) => { + return source._id!.toString(); + }) ).catch((error) => { if (error === "Can't connect to Database") { throw "Can't connect to Database"; @@ -720,7 +755,7 @@ export async function startProduction( ...production, sources: production.sources.map((source) => { const streamsForSource = streams?.filter( - (stream) => stream.source_id === source._id.toString() + (stream) => stream.source_id === source._id?.toString() ); return { ...source, diff --git a/src/app/html_input/page.tsx b/src/app/html_input/page.tsx new file mode 100644 index 0000000..81cfaa5 --- /dev/null +++ b/src/app/html_input/page.tsx @@ -0,0 +1,10 @@ +import { PageProps } from '../../../.next/types/app/html_input/page'; + +export default function HtmlInput({ searchParams: { input } }: PageProps) { + return ( +
+

HTML INPUT

+

{input}

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

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

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

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

+ )} + + {source && ( +

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

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

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

+
+ )} + ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9666bcc..d861578 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -5,108 +5,73 @@ import { SourceReference } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; -import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; - export default function SourceCards({ productionSetup, + sourceRef, updateProduction, onSourceUpdate, onSourceRemoval }: { productionSetup: Production; + sourceRef?: SourceReference; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); - const currentOrder: SourceReference[] = items.map((source) => { - return { - _id: source._id.toString(), - label: source.label, - input_slot: source.input_slot, - stream_uuids: source.stream_uuids - }; - }); - const gridItems: React.JSX.Element[] = []; - let tempItems = [...items]; - let firstEmptySlot = items.length + 1; + if (loading || !items) return null; + + // Filter SourceReference and ISource objects correctly + const sourceReferences = items.filter( + (item): item is SourceReference => item.type !== 'ingest_source' + ); - if (!items || items.length === 0) return null; - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - if (!items.some((source) => source.input_slot === i + 1)) { - firstEmptySlot = i + 1; - break; - } - } - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - // console.log(`On input slot: ${i + 1}`); - // console.log(`Checking sources:`); - // console.log(tempItems); - tempItems.every((source) => { - if (source.input_slot === i + 1) { - // console.log(`Found source on input slot: ${i + 1}`); - // console.log(`Removing source "${source.name}" from sources list`); - tempItems = tempItems.filter((i) => i._id !== source._id); - // console.log(`Adding source "${source.name}" to grid`); - if (!productionSetup.isActive) { - gridItems.push( - - - setSelectingText(isSelecting) - } - /> - - ); - } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); - } - return false; - } else { - // console.log(`No source found on input slot: ${i + 1}`); - // console.log(`Adding empty slot to grid`); - if (productionSetup.isActive) { - gridItems.push( - - ); - } + const isISource = (source: SourceReference | ISource): source is ISource => { + // Use properties unique to ISource to check the type + return 'src' in source; + }; + + const gridItems = items.map((source) => { + const isSource = isISource(source); + + return ( + + {isSource ? ( + setSelectingText(isSelecting)} + type={'ingest_source'} + /> + ) : ( + setSelectingText(isSelecting)} + type={source.type} + /> + )} + + ); + }); - return false; - } - }); - } return <>{gridItems}; } diff --git a/src/components/sourceListItem/SourceListItem.tsx b/src/components/sourceListItem/SourceListItem.tsx index 6e9aadf..c8b724e 100644 --- a/src/components/sourceListItem/SourceListItem.tsx +++ b/src/components/sourceListItem/SourceListItem.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Source, SourceWithId } from '../../interfaces/Source'; +import { Source, SourceReference, SourceWithId } from '../../interfaces/Source'; import { PreviewThumbnail } from './PreviewThumbnail'; import { getSourceThumbnail } from '../../utils/source'; import videoSettings from '../../utils/videoSettings'; @@ -95,7 +95,7 @@ function InventoryListItem({ : [] ); } - }, [source.audio_stream.audio_mapping]); + }, [source?.audio_stream.audio_mapping]); return (
  • a.input_slot - b.input_slot) }; - return { ...updatedSetup, sources: [ ...productionSetup.sources, { _id: source._id, + type: source.type, label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index e3164fe..cdaf461 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -11,7 +11,8 @@ export function usePostProduction() { isActive: false, name, sources: [], - selectedPresetRef: undefined + html: [], + mediaplayers: [] }) }); if (response.ok) { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 427ffbf..4cc3a03 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -9,57 +9,82 @@ export interface ISource extends SourceWithId { stream_uuids?: string[]; src: string; } + export function useDragableItems( sources: SourceReference[] -): [ISource[], (originId: string, destinationId: string) => void, boolean] { +): [ + (SourceReference | ISource)[], + (originId: string, destinationId: string) => void, + boolean +] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState( + const [items, setItems] = useState<(SourceReference | ISource)[]>( sources.flatMap((ref) => { const source = inventorySources.get(ref._id); if (!source) return []; return { ...source, + _id: ref._id, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) + src: getSourceThumbnail(source), + ingest_source_name: source.ingest_source_name, + ingest_name: source.ingest_name, + video_stream: source.video_stream, + audio_stream: source.audio_stream, + status: source.status, + type: source.type, + tags: source.tags, + name: source.name }; }) ); useEffect(() => { - setItems( - sources.flatMap((ref) => { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) - }; - }) - ); + const updatedItems = sources.map((ref) => { + const source = inventorySources.get(ref._id); + if (!source) return { ...ref }; + return { + ...ref, + _id: ref._id, + status: source.status, + name: source.name, + type: source.type, + tags: source.tags, + ingest_name: source.ingest_name, + ingest_source_name: source.ingest_source_name, + ingest_type: source.ingest_type, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + video_stream: source.video_stream, + audio_stream: source.audio_stream, + lastConnected: source.lastConnected + }; + }); + setItems(updatedItems); }, [sources, inventorySources]); const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((i) => i._id.toString() === originId); + const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => item._id.toString() === destinationId ); + if (!originSource || !destinationSource) return; - const originInputSlot = originSource.input_slot; - const destinationInputSlot = destinationSource.input_slot; - originSource.input_slot = destinationInputSlot; - destinationSource.input_slot = originInputSlot; - const updatedItems = [ - ...items.filter( - (i) => i._id !== originSource._id && i._id !== destinationSource._id - ), - originSource, - destinationSource - ].sort((a, b) => a.input_slot - b.input_slot); + + const updatedItems = items + .map((item) => { + if (item._id === originSource._id) + return { ...item, input_slot: destinationSource.input_slot }; + if (item._id === destinationSource._id) + return { ...item, input_slot: originSource.input_slot }; + return item; + }) + .sort((a, b) => a.input_slot - b.input_slot); + setItems(updatedItems); }; diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 1d0e7e5..596e25c 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -46,7 +46,8 @@ export const en = { orig: 'Original Name: {{name}}', metadata: 'Source Metadata', location_unknown: 'Unknown', - last_connected: 'Last connection' + last_connected: 'Last connection', + input_slot: 'Input slot: {{input_slot}}' }, delete_source_status: { delete_stream: 'Delete stream', @@ -63,14 +64,17 @@ export const en = { }, production_configuration: 'Production Configuration', production: { - add_source: 'Add Source', + add_source: 'Add ingest', select_preset: 'Select Preset', clear_selection: 'Clear Selection', started: 'Production started: {{name}}', failed: 'Production start failed: {{name}}', stopped: 'Production stopped: {{name}}', stop_failed: 'Production stop failed: {{name}}', - missing_multiview: 'Missing multiview reference in selected preset' + missing_multiview: 'Missing multiview reference in selected preset', + source: 'Source', + add: 'Add', + add_other_source_type: 'Add other source type' }, create_new: 'Create New', default_prod_placeholder: 'My New Configuration', diff --git a/src/i18n/locales/sv.ts b/src/i18n/locales/sv.ts index b33112d..bf9f42c 100644 --- a/src/i18n/locales/sv.ts +++ b/src/i18n/locales/sv.ts @@ -48,7 +48,8 @@ export const sv = { orig: 'Enhetsnamn: {{name}}', metadata: 'Käll-metadata', location_unknown: 'Okänd', - last_connected: 'Senast uppkoppling' + last_connected: 'Senast uppkoppling', + input_slot: 'Ingång: {{input_slot}}' }, delete_source_status: { delete_stream: 'Radera ström', @@ -65,14 +66,17 @@ export const sv = { }, production_configuration: 'Produktionskonfiguration', production: { - add_source: 'Lägg till källa', + add_source: 'Lägg till ingång', select_preset: 'Välj produktionsmall', clear_selection: 'Rensa val', started: 'Produktion startad: {{name}}', failed: 'Start av produktion misslyckades: {{name}}', stopped: 'Produktion stoppad: {{name}}', stop_failed: 'Stopp av produktion misslyckades: {{name}}', - missing_multiview: 'Saknar referens till en multiview i valt preset' + missing_multiview: 'Saknar referens till en multiview i valt preset', + source: 'Källa', + add: 'Lägg till', + add_other_source_type: 'Lägg till annan källtyp' }, create_new: 'Skapa ny', default_prod_placeholder: 'Min Nya Konfiguration', diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index e59afa4..1aec774 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,6 +1,7 @@ import { ObjectId, WithId } from 'mongodb'; export type SourceType = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; +export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { height?: number; width?: number; @@ -16,7 +17,7 @@ export type AudioStream = { export type Numbers = number | number[]; export interface Source { - _id?: ObjectId; + _id?: ObjectId | string; status: SourceStatus; name: string; type: SourceType; @@ -34,6 +35,7 @@ export interface Source { export interface SourceReference { _id: string; + type: Type; label: string; stream_uuids?: string[]; input_slot: number; diff --git a/src/middleware.ts b/src/middleware.ts index 7724e3b..cac0847 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -41,4 +41,4 @@ export default withAuth(function middleware(req) { } }); -export const config = { matcher: ['/', '/((?!api|images).*)/'] }; +export const config = { matcher: ['/', '/((?!api|images|html_input).*)/'] }; From b9f4c4c9d06c5a79ed64eda703213921806a606b Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 02/41] WIP! feat: add media & html websocket connections --- src/api/manager/workflow.ts | 16 ++++++++++++++++ src/interfaces/production.ts | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 8edc5a9..f7a3da0 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -77,6 +77,10 @@ async function connectIngestSources( let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; + console.log('connectIngestSources - productionSettings', productionSettings); + console.log('connectIngestSources - sources', sources); + console.log('connectIngestSources - usedPorts', usedPorts); + for (const source of sources) { input_slot = input_slot + 1; const ingestUuid = await getUuidFromIngestName( @@ -460,6 +464,8 @@ export async function startProduction( await initDedicatedPorts(); + console.log('startProduction - production', production); + let streams: SourceToPipelineStream[] = []; // Try to setup streams from ingest(s) to pipeline(s) start try { @@ -479,6 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); + console.log('startProduction - production', production); // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket @@ -496,11 +503,13 @@ export async function startProduction( throw "Can't get source!"; }); + console.log('startProduction - production', production); // Lookup pipeline UUIDs from pipeline names and insert to production_settings await insertPipelineUuid(production_settings).catch((error) => { throw error; }); + console.log('startProduction - production', production); // Fetch expanded pipeline objects from Ateliere Live const pipelinesToUsePromises = production_settings.pipelines.map( (pipeline) => { @@ -508,11 +517,13 @@ export async function startProduction( } ); const pipelinesToUse = await Promise.all(pipelinesToUsePromises); + console.log('startProduction - pipelinesToUse', pipelinesToUse); // Check if pipelines are already in use by another production const hasAlreadyUsedPipeline = pipelinesToUse.filter((pipeline) => isUsed(pipeline) ); + console.log('startProduction - production', production); if (hasAlreadyUsedPipeline.length > 0) { Log().error( @@ -524,6 +535,7 @@ export async function startProduction( (p) => p.name )}`; } + console.log('startProduction - hasAlreadyUsedPipeline', hasAlreadyUsedPipeline); const resetPipelinePromises = production_settings.pipelines.map( (pipeline) => { @@ -533,6 +545,7 @@ export async function startProduction( await Promise.all(resetPipelinePromises).catch((error) => { throw `Failed to reset pipelines: ${error}`; }); + console.log('startProduction - resetPipelinePromises', resetPipelinePromises); // Fetch all control panels from Ateliere Live const allControlPanels = await getControlPanels(); @@ -546,6 +559,7 @@ export async function startProduction( const hasAlreadyUsedControlPanel = controlPanelsToUse.filter( (controlPanel) => controlPanel.outgoing_connections.length > 0 ); + console.log('startProduction - hasAlreadyUsedControlPanel', hasAlreadyUsedControlPanel); if (hasAlreadyUsedControlPanel.length > 0) { Log().error( @@ -572,6 +586,7 @@ export async function startProduction( return pipeline.uuid; }) ); + console.log('startProduction - stopPipelines', production_settings.pipelines); streams = await connectIngestSources( production_settings, @@ -596,6 +611,7 @@ export async function startProduction( error: 'Could not setup streams: Unexpected error occured' }; } + console.log('startProduction - streams', streams); return { ok: false, value: [ diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d965..b979a5c 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From d1f6cbfa49077c8b17528ed69674122a8dd5203f Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 3 Sep 2024 16:57:15 +0200 Subject: [PATCH 03/41] feat: support to add media player and html sources --- src/api/ateliereLive/websocket.ts | 2 +- src/components/sourceCards/SourceCards.tsx | 1 + src/interfaces/production.ts | 14 -------------- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466..4e26e8d 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - }; + } } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index d861578..0b94651 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -20,6 +20,7 @@ export default function SourceCards({ onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const referenceItems = productionSetup.sources; const [selectingText, setSelectingText] = useState(false); if (loading || !items) return null; diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5c..eb4d965 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 8b9d4aee85899ad3cf7715e43aa7d4fc793b98c4 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 4 Sep 2024 21:12:14 +0200 Subject: [PATCH 04/41] fix: fix drag and drop and lint --- src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/workflow.ts | 3 +-- src/components/sourceCards/SourceCards.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 4e26e8d..6f51466 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - } + }; } diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index f7a3da0..fa1764a 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -485,8 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); - console.log('startProduction - production', production); - + // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 0b94651..6cdb291 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,7 +19,7 @@ export default function SourceCards({ onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const [items, moveItems] = useDragableItems(productionSetup.sources); const referenceItems = productionSetup.sources; const [selectingText, setSelectingText] = useState(false); From 03b6a7db4d58b98da755c90a92139e33dfca44f7 Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 05/41] WIP! feat: add media & html websocket connections --- src/interfaces/production.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d965..b979a5c 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From ca1f8e692eb89d95c23e31e94dc3b032b448ad11 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 3 Sep 2024 16:57:15 +0200 Subject: [PATCH 06/41] feat: support to add media player and html sources --- src/api/ateliereLive/websocket.ts | 2 +- src/app/production/[id]/page.tsx | 2 + src/components/sourceCard/SourceCard.tsx | 2 +- src/hooks/useDragableItems.ts | 75 +++++++++++++++++++++++- src/interfaces/production.ts | 14 ----- 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466..4e26e8d 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - }; + } } diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 0b4259d..26b8d13 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -521,11 +521,13 @@ export default function ProductionConfiguration({ params }: PageProps) { } }; + // TODO: HTML och MEDIA PLAYER KÄLLOR TAS INTE BORT const handleRemoveSource = async () => { if ( productionSetup && productionSetup.isActive && selectedSourceRef && + // Gör det här att sourcen inte tas bort ordentligt? selectedSourceRef.stream_uuids ) { const multiviews = diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 246e7c2..f2b9446 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -83,7 +83,7 @@ export default function SourceCard({
    { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 4cc3a03..c213f18 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -41,6 +41,31 @@ export function useDragableItems( }) ); + const [refItems, setRefItems] = useState( + sources.flatMap((ref) => { + return {... ref} + }) + ); + + const [allItems, setAllItems] = useState<(SourceReference | ISource)[]>( + sources.flatMap((ref) => { + if (ref.type === 'ingest_source') { + const source = inventorySources.get(ref._id); + if (!source) return []; + return { + ...source, + _id: ref._id, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + }; + } else { + return {...ref} + } + }) + ) + useEffect(() => { const updatedItems = sources.map((ref) => { const source = inventorySources.get(ref._id); @@ -67,6 +92,52 @@ export function useDragableItems( setItems(updatedItems); }, [sources, inventorySources]); + useEffect(() => { + setAllItems( + sources.flatMap((ref) => { + if (ref.type === 'ingest_source') { + const source = inventorySources.get(ref._id); + if (!source) return []; + return { + ...source, + _id: ref._id, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + }; + } else { + return {...ref} + } + }) + ) + }) + + // useEffect(() => { + // setIngestSources( + // sources.flatMap((ref) => { + // const source = inventorySources.get(ref._id); + // if (!source) return []; + // return { + // ...source, + // _id: ref._id, + // label: ref.label, + // input_slot: ref.input_slot, + // stream_uuids: ref.stream_uuids, + // src: getSourceThumbnail(source), + // }; + // }) + // ); + // }, [sources, inventorySources]); + + // useEffect(() => { + // setRefSources(sources.filter((ref) => ref.type !== 'ingest_source')); + // }, [sources]) + + // useEffect(() => { + // setAllSources([...refSources, ...ingestSources]); + // }, [refSources, ingestSources]); + const moveItem = (originId: string, destinationId: string) => { const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( @@ -88,5 +159,5 @@ export function useDragableItems( setItems(updatedItems); }; - return [items, moveItem, loading]; -} + return [allItems, moveItems, loading]; +} \ No newline at end of file diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5c..eb4d965 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From ca856ef95394804823e2811b530b2ac24b4747b4 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 4 Sep 2024 21:12:14 +0200 Subject: [PATCH 07/41] fix: fix drag and drop and lint --- src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/workflow.ts | 2 +- src/components/sourceCards/SourceCards.tsx | 3 +- src/hooks/useDragableItems.ts | 73 +--------------------- 4 files changed, 4 insertions(+), 76 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 4e26e8d..6f51466 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -41,5 +41,5 @@ export async function createControlPanelWebSocket() { close: () => { ws.close(); } - } + }; } diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index fa1764a..6dc8ba0 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -485,7 +485,7 @@ export async function startProduction( ); controlPanelWS.close(); - + // Nedan behöver göras efter att vi har skapat en produktion // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 6cdb291..d861578 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,8 +19,7 @@ export default function SourceCards({ onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItems] = useDragableItems(productionSetup.sources); - const referenceItems = productionSetup.sources; + const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); if (loading || !items) return null; diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index c213f18..5cf6d5a 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -41,31 +41,6 @@ export function useDragableItems( }) ); - const [refItems, setRefItems] = useState( - sources.flatMap((ref) => { - return {... ref} - }) - ); - - const [allItems, setAllItems] = useState<(SourceReference | ISource)[]>( - sources.flatMap((ref) => { - if (ref.type === 'ingest_source') { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - _id: ref._id, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - }; - } else { - return {...ref} - } - }) - ) - useEffect(() => { const updatedItems = sources.map((ref) => { const source = inventorySources.get(ref._id); @@ -92,52 +67,6 @@ export function useDragableItems( setItems(updatedItems); }, [sources, inventorySources]); - useEffect(() => { - setAllItems( - sources.flatMap((ref) => { - if (ref.type === 'ingest_source') { - const source = inventorySources.get(ref._id); - if (!source) return []; - return { - ...source, - _id: ref._id, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - }; - } else { - return {...ref} - } - }) - ) - }) - - // useEffect(() => { - // setIngestSources( - // sources.flatMap((ref) => { - // const source = inventorySources.get(ref._id); - // if (!source) return []; - // return { - // ...source, - // _id: ref._id, - // label: ref.label, - // input_slot: ref.input_slot, - // stream_uuids: ref.stream_uuids, - // src: getSourceThumbnail(source), - // }; - // }) - // ); - // }, [sources, inventorySources]); - - // useEffect(() => { - // setRefSources(sources.filter((ref) => ref.type !== 'ingest_source')); - // }, [sources]) - - // useEffect(() => { - // setAllSources([...refSources, ...ingestSources]); - // }, [refSources, ingestSources]); - const moveItem = (originId: string, destinationId: string) => { const originSource = items.find((item) => item._id.toString() === originId); const destinationSource = items.find( @@ -160,4 +89,4 @@ export function useDragableItems( }; return [allItems, moveItems, loading]; -} \ No newline at end of file +} From 6b5ee1196bdbc1517bb1b9e815db67ca0e4adb27 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 09:24:02 +0200 Subject: [PATCH 08/41] feat: add additional filtering --- src/api/ateliereLive/ingest.ts | 19 ++++++ .../manager/sources/resources/[id]/route.ts | 21 +++++++ .../api/manager/sources/resources/route.ts | 18 ++++++ src/components/filter/FilterOptions.tsx | 1 + src/hooks/sources/useResources.tsx | 59 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 src/app/api/manager/sources/resources/[id]/route.ts create mode 100644 src/app/api/manager/sources/resources/route.ts create mode 100644 src/hooks/sources/useResources.tsx diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index 0c7b521..c02fa28 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -5,6 +5,7 @@ import { } from '../../../types/ateliere-live'; import { LIVE_BASE_API_PATH } from '../../constants'; import { getAuthorizationHeader } from './utils/authheader'; +import { ResourcesSourceResponse } from '../../../types/agile-live'; // TODO: create proper cache... const INGEST_UUID_CACHE: Map = new Map(); @@ -74,6 +75,24 @@ export async function getIngests(): Promise { throw await response.json(); } +export async function getCompleteIngests(): Promise { + const response = await fetch( + new URL(LIVE_BASE_API_PATH + `/ingests?expand=true`, process.env.LIVE_URL), + { + headers: { + authorization: getAuthorizationHeader() + }, + next: { + revalidate: 0 + } + } + ); + if (response.ok) { + return response.json(); + } + throw await response.json(); +} + export async function getIngest( uuid: string ): Promise { diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts new file mode 100644 index 0000000..da6c235 --- /dev/null +++ b/src/app/api/manager/sources/resources/[id]/route.ts @@ -0,0 +1,21 @@ +import { NextResponse, NextRequest } from "next/server"; +import { getIngestSources } from "../../../../../../api/agileLive/ingest"; +import { isAuthenticated } from "../../../../../../api/manager/auth"; + +type Params = { + id: string; +}; + +export async function GET(request: NextRequest, { params }: { params: Params }): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + return NextResponse.json(await getIngestSources(params.id)); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }); + } +} diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts new file mode 100644 index 0000000..5a94267 --- /dev/null +++ b/src/app/api/manager/sources/resources/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { isAuthenticated } from '../../../../../api/manager/auth'; +import { getCompleteIngests } from '../../../../../api/agileLive/ingest'; + + +export async function GET(): Promise { + if (!(await isAuthenticated())) { + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + + try { + return NextResponse.json(await getCompleteIngests()); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }) + } +} diff --git a/src/components/filter/FilterOptions.tsx b/src/components/filter/FilterOptions.tsx index 67798fc..8752d44 100644 --- a/src/components/filter/FilterOptions.tsx +++ b/src/components/filter/FilterOptions.tsx @@ -5,6 +5,7 @@ import FilterDropdown from './FilterDropdown'; import { ClickAwayListener } from '@mui/base'; import { SourceWithId } from '../../interfaces/Source'; import { FilterContext } from '../inventory/FilterContext'; +import { useResources } from '../../hooks/sources/useResources'; type FilterOptionsProps = { onFilteredSources: (sources: Map) => void; diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx new file mode 100644 index 0000000..d6b37b7 --- /dev/null +++ b/src/hooks/sources/useResources.tsx @@ -0,0 +1,59 @@ +import { + ResourcesIngestResponse, + ResourcesSourceResponse +} from '../../../types/agile-live'; +import { useState, useEffect } from 'react'; + +export function useResources() { + const [ingests, setIngests] = useState([]); + const [resources, setResources] = useState([]); + + useEffect(() => { + let isMounted = true; + + const fetchSources = async () => { + try { + const response = await fetch(`/api/manager/sources/resourceSource`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }); + + if (!response.ok) { + throw new Error('Error'); + } + + const ing = await response.json(); + if (isMounted) { + setIngests(ing); + } + } catch (e) { + console.log('ERROR'); + } + }; + fetchSources(); + + return () => { + isMounted = false; + }; + }, []); + + useEffect(() => { + if (ingests) { + for (let i = 0; i < ingests.length; i++) { + const id = ingests[i].uuid; + if (id) { + fetch(`/api/manager/resources/${id}`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }).then(async (response) => { + console.log('RESPONSE: ', response); + const sources = await response.json(); + setResources(sources); + }); + } + } + } + }, [ingests]); + + return [resources]; +} From 474d203b2952f5bf16cc23bd05a8974a8785dd00 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 11:00:58 +0200 Subject: [PATCH 09/41] fix: handle active + source filter, reduce code --- src/api/ateliereLive/ingest.ts | 1 - .../api/manager/sources/resources/route.ts | 4 +-- src/hooks/sources/useResources.tsx | 33 +++++++------------ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index c02fa28..c942083 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -5,7 +5,6 @@ import { } from '../../../types/ateliere-live'; import { LIVE_BASE_API_PATH } from '../../constants'; import { getAuthorizationHeader } from './utils/authheader'; -import { ResourcesSourceResponse } from '../../../types/agile-live'; // TODO: create proper cache... const INGEST_UUID_CACHE: Map = new Map(); diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts index 5a94267..8932a0d 100644 --- a/src/app/api/manager/sources/resources/route.ts +++ b/src/app/api/manager/sources/resources/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { isAuthenticated } from '../../../../../api/manager/auth'; -import { getCompleteIngests } from '../../../../../api/agileLive/ingest'; +import { getIngests } from '../../../../../api/agileLive/ingest'; export async function GET(): Promise { @@ -11,7 +11,7 @@ export async function GET(): Promise { } try { - return NextResponse.json(await getCompleteIngests()); + return NextResponse.json(await getIngests()); } catch (e) { return new NextResponse(e?.toString(), { status: 404 }) } diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx index d6b37b7..607c34a 100644 --- a/src/hooks/sources/useResources.tsx +++ b/src/hooks/sources/useResources.tsx @@ -1,37 +1,27 @@ import { - ResourcesIngestResponse, - ResourcesSourceResponse + ResourcesSourceResponse, + ResourcesCompactIngestResponse } from '../../../types/agile-live'; import { useState, useEffect } from 'react'; export function useResources() { - const [ingests, setIngests] = useState([]); + const [ingests, setIngests] = useState([]); const [resources, setResources] = useState([]); useEffect(() => { let isMounted = true; - const fetchSources = async () => { - try { - const response = await fetch(`/api/manager/sources/resourceSource`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }); - - if (!response.ok) { - throw new Error('Error'); - } - + const getIngests = async () => + await fetch(`/api/manager/sources/resources`, { + method: 'GET', + headers: [['x-api-key', `Bearer apisecretkey`]] + }).then(async (response) => { const ing = await response.json(); if (isMounted) { setIngests(ing); } - } catch (e) { - console.log('ERROR'); - } - }; - fetchSources(); - + }); + getIngests(); return () => { isMounted = false; }; @@ -42,11 +32,10 @@ export function useResources() { for (let i = 0; i < ingests.length; i++) { const id = ingests[i].uuid; if (id) { - fetch(`/api/manager/resources/${id}`, { + fetch(`/api/manager/sources/resources/${id}`, { method: 'GET', headers: [['x-api-key', `Bearer apisecretkey`]] }).then(async (response) => { - console.log('RESPONSE: ', response); const sources = await response.json(); setResources(sources); }); From 77ba4e1abd704aeaff7b9d9b65f3456f0a3e74c7 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 23 Aug 2024 11:39:23 +0200 Subject: [PATCH 10/41] fix: linting --- .../manager/sources/resources/[id]/route.ts | 21 +++++++++++-------- .../api/manager/sources/resources/route.ts | 19 ++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts index da6c235..e1b68b7 100644 --- a/src/app/api/manager/sources/resources/[id]/route.ts +++ b/src/app/api/manager/sources/resources/[id]/route.ts @@ -1,18 +1,21 @@ -import { NextResponse, NextRequest } from "next/server"; -import { getIngestSources } from "../../../../../../api/agileLive/ingest"; -import { isAuthenticated } from "../../../../../../api/manager/auth"; +import { NextResponse, NextRequest } from 'next/server'; +import { getIngestSources } from '../../../../../../api/agileLive/ingest'; +import { isAuthenticated } from '../../../../../../api/manager/auth'; type Params = { id: string; }; -export async function GET(request: NextRequest, { params }: { params: Params }): Promise { +export async function GET( + request: NextRequest, + { params }: { params: Params } +): Promise { if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + try { return NextResponse.json(await getIngestSources(params.id)); } catch (e) { diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts index 8932a0d..6420069 100644 --- a/src/app/api/manager/sources/resources/route.ts +++ b/src/app/api/manager/sources/resources/route.ts @@ -2,17 +2,16 @@ import { NextResponse } from 'next/server'; import { isAuthenticated } from '../../../../../api/manager/auth'; import { getIngests } from '../../../../../api/agileLive/ingest'; - export async function GET(): Promise { if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - + return new NextResponse(`Not Authorized!`, { + status: 403 + }); + } + try { - return NextResponse.json(await getIngests()); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }) - } + return NextResponse.json(await getIngests()); + } catch (e) { + return new NextResponse(e?.toString(), { status: 404 }); + } } From aaf3069e8d6db5e3276b690245fb112b8526b55d Mon Sep 17 00:00:00 2001 From: Saelmala Date: Mon, 26 Aug 2024 10:40:02 +0200 Subject: [PATCH 11/41] fix: add ingest type to database --- src/api/manager/job/syncInventory.ts | 3 +- .../manager/sources/resources/[id]/route.ts | 24 ---------- .../api/manager/sources/resources/route.ts | 17 ------- src/components/filter/FilterOptions.tsx | 1 - src/hooks/sources/useResources.tsx | 48 ------------------- 5 files changed, 2 insertions(+), 91 deletions(-) delete mode 100644 src/app/api/manager/sources/resources/[id]/route.ts delete mode 100644 src/app/api/manager/sources/resources/route.ts delete mode 100644 src/hooks/sources/useResources.tsx diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 2e813d0..383e5c1 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -86,7 +86,8 @@ export async function runSyncInventory() { const apiSource = apiSources.find((source) => { return ( source.ingest_name === inventorySource.ingest_name && - source.ingest_source_name === inventorySource.ingest_source_name + source.ingest_source_name === inventorySource.ingest_source_name && + source.ingest_type === inventorySource.type ); }); if (!apiSource) { diff --git a/src/app/api/manager/sources/resources/[id]/route.ts b/src/app/api/manager/sources/resources/[id]/route.ts deleted file mode 100644 index e1b68b7..0000000 --- a/src/app/api/manager/sources/resources/[id]/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextResponse, NextRequest } from 'next/server'; -import { getIngestSources } from '../../../../../../api/agileLive/ingest'; -import { isAuthenticated } from '../../../../../../api/manager/auth'; - -type Params = { - id: string; -}; - -export async function GET( - request: NextRequest, - { params }: { params: Params } -): Promise { - if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - - try { - return NextResponse.json(await getIngestSources(params.id)); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }); - } -} diff --git a/src/app/api/manager/sources/resources/route.ts b/src/app/api/manager/sources/resources/route.ts deleted file mode 100644 index 6420069..0000000 --- a/src/app/api/manager/sources/resources/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from 'next/server'; -import { isAuthenticated } from '../../../../../api/manager/auth'; -import { getIngests } from '../../../../../api/agileLive/ingest'; - -export async function GET(): Promise { - if (!(await isAuthenticated())) { - return new NextResponse(`Not Authorized!`, { - status: 403 - }); - } - - try { - return NextResponse.json(await getIngests()); - } catch (e) { - return new NextResponse(e?.toString(), { status: 404 }); - } -} diff --git a/src/components/filter/FilterOptions.tsx b/src/components/filter/FilterOptions.tsx index 8752d44..67798fc 100644 --- a/src/components/filter/FilterOptions.tsx +++ b/src/components/filter/FilterOptions.tsx @@ -5,7 +5,6 @@ import FilterDropdown from './FilterDropdown'; import { ClickAwayListener } from '@mui/base'; import { SourceWithId } from '../../interfaces/Source'; import { FilterContext } from '../inventory/FilterContext'; -import { useResources } from '../../hooks/sources/useResources'; type FilterOptionsProps = { onFilteredSources: (sources: Map) => void; diff --git a/src/hooks/sources/useResources.tsx b/src/hooks/sources/useResources.tsx deleted file mode 100644 index 607c34a..0000000 --- a/src/hooks/sources/useResources.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - ResourcesSourceResponse, - ResourcesCompactIngestResponse -} from '../../../types/agile-live'; -import { useState, useEffect } from 'react'; - -export function useResources() { - const [ingests, setIngests] = useState([]); - const [resources, setResources] = useState([]); - - useEffect(() => { - let isMounted = true; - - const getIngests = async () => - await fetch(`/api/manager/sources/resources`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }).then(async (response) => { - const ing = await response.json(); - if (isMounted) { - setIngests(ing); - } - }); - getIngests(); - return () => { - isMounted = false; - }; - }, []); - - useEffect(() => { - if (ingests) { - for (let i = 0; i < ingests.length; i++) { - const id = ingests[i].uuid; - if (id) { - fetch(`/api/manager/sources/resources/${id}`, { - method: 'GET', - headers: [['x-api-key', `Bearer apisecretkey`]] - }).then(async (response) => { - const sources = await response.json(); - setResources(sources); - }); - } - } - } - }, [ingests]); - - return [resources]; -} From 93f9048815193464733eebbbf944251d6738ef86 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 28 Aug 2024 17:14:15 +0200 Subject: [PATCH 12/41] feat: first commit of source-delete, with basic styling and fetches --- .../inventory/editView/EditView.tsx | 4 ++- src/hooks/sources/useDeleteSource.tsx | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/hooks/sources/useDeleteSource.tsx diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index 80d498a..df237b7 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,12 +1,13 @@ import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; import { IconExclamationCircle } from '@tabler/icons-react'; +import { useDeleteSource } from '../../../hooks/sources/useDeleteSource'; export default function EditView({ source, @@ -20,6 +21,7 @@ export default function EditView({ removeInventorySource: (source: SourceWithId) => void; }) { const [loaded, setLoaded] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); const src = useMemo(() => getSourceThumbnail(source), [source]); return ( diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx new file mode 100644 index 0000000..e7a33f2 --- /dev/null +++ b/src/hooks/sources/useDeleteSource.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { SourceWithId } from '../../interfaces/Source'; + +export function useDeleteSource(source: SourceWithId | null) { + const [loading, setLoading] = useState(true); + const [deleteComplete, setDeleteComplete] = useState(false); + + useEffect(() => { + if (source && source.status === 'gone') { + setLoading(true); + setDeleteComplete(false); + // Source to be deleted: + console.log('source._id', source); + fetch(`/api/manager/inventory/${source._id}`, { + method: 'DELETE', + // TODO: Implement api key + headers: [['x-api-key', `Bearer apisecretkey`]] + }) + .then((response) => { + if (response.ok) { + setLoading(false); + setDeleteComplete(true); + } + }) + .catch((e) => { + console.log(`Failed to delete source-item: ${e}`); + }); + } else { + setLoading(false); + setDeleteComplete(false); + } + }, [source]); + return [loading, deleteComplete]; +} From 0acd37ae027b1ab2adbff0c470964f774fc954ba Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:39:22 +0200 Subject: [PATCH 13/41] feat: added creation-date and function to set correct status when getting sources from api --- src/api/manager/job/syncInventory.ts | 3 ++- src/interfaces/Source.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 383e5c1..774c9d7 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -41,7 +41,8 @@ async function getSourcesFromAPI(): Promise { audio_stream: { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate - } + }, + createdAt: new Date() } satisfies SourceWithoutLastConnected) ); } diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 1aec774..1102030 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -30,6 +30,7 @@ export interface Source { ingest_type: string; video_stream: VideoStream; audio_stream: AudioStream; + createdAt: Date; lastConnected: Date; } From 9d19b685e645aa497c13935c47e389d37f9ddbd4 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:41:14 +0200 Subject: [PATCH 14/41] feat: updated server-fetch to be put, not possible to delete --- src/hooks/sources/useDeleteSource.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx index e7a33f2..d67e9ae 100644 --- a/src/hooks/sources/useDeleteSource.tsx +++ b/src/hooks/sources/useDeleteSource.tsx @@ -9,18 +9,22 @@ export function useDeleteSource(source: SourceWithId | null) { if (source && source.status === 'gone') { setLoading(true); setDeleteComplete(false); - // Source to be deleted: - console.log('source._id', source); + fetch(`/api/manager/inventory/${source._id}`, { - method: 'DELETE', + method: 'PUT', // TODO: Implement api key headers: [['x-api-key', `Bearer apisecretkey`]] }) .then((response) => { - if (response.ok) { + if (!response.ok) { setLoading(false); setDeleteComplete(true); + return response.text().then((message) => { + throw new Error(`Error ${response.status}: ${message}`); + }); } + setLoading(false); + setDeleteComplete(true); }) .catch((e) => { console.log(`Failed to delete source-item: ${e}`); From ae74ea99d8aa3a94d65740e4dcfbd3a3121eb6d3 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 14:45:20 +0200 Subject: [PATCH 15/41] feat: moved logic out of component to inventory-page and added purge-flag --- src/components/inventory/Inventory.tsx | 3 ++- src/components/inventory/editView/EditView.tsx | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index dacb262..280a099 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -8,6 +8,7 @@ import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; import FilterContext from './FilterContext'; +import { useDeleteSource } from '../../hooks/sources/useDeleteSource'; import styles from './Inventory.module.scss'; export default function Inventory() { @@ -82,7 +83,7 @@ export default function Inventory() {
      - {getSourcesToDisplay(filteredSources)} + {loading ? '' : getSourcesToDisplay(filteredSources)}
  • diff --git a/src/components/inventory/editView/EditView.tsx b/src/components/inventory/editView/EditView.tsx index df237b7..80d498a 100644 --- a/src/components/inventory/editView/EditView.tsx +++ b/src/components/inventory/editView/EditView.tsx @@ -1,13 +1,12 @@ import Image from 'next/image'; import { getSourceThumbnail } from '../../../utils/source'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import EditViewContext from '../EditViewContext'; import GeneralSettings from './GeneralSettings'; import { SourceWithId } from '../../../interfaces/Source'; import UpdateButtons from './UpdateButtons'; import AudioChannels from './AudioChannels/AudioChannels'; import { IconExclamationCircle } from '@tabler/icons-react'; -import { useDeleteSource } from '../../../hooks/sources/useDeleteSource'; export default function EditView({ source, @@ -21,7 +20,6 @@ export default function EditView({ removeInventorySource: (source: SourceWithId) => void; }) { const [loaded, setLoaded] = useState(false); - const [itemToDelete, setItemToDelete] = useState(null); const src = useMemo(() => getSourceThumbnail(source), [source]); return ( From ebb2d201c6d934b696dd0fbda6a1868370dcf389 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Fri, 30 Aug 2024 17:07:12 +0200 Subject: [PATCH 16/41] fix: simplified the delete-source-hook --- src/hooks/sources/useDeleteSource.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx index d67e9ae..29c4bb1 100644 --- a/src/hooks/sources/useDeleteSource.tsx +++ b/src/hooks/sources/useDeleteSource.tsx @@ -1,13 +1,14 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; +import { CallbackHook } from '../types'; -export function useDeleteSource(source: SourceWithId | null) { - const [loading, setLoading] = useState(true); +export function useDeleteSource(): CallbackHook< + (source: SourceWithId) => void +> { const [deleteComplete, setDeleteComplete] = useState(false); - useEffect(() => { + const setSourceToPurge = (source: SourceWithId) => { if (source && source.status === 'gone') { - setLoading(true); setDeleteComplete(false); fetch(`/api/manager/inventory/${source._id}`, { @@ -17,22 +18,19 @@ export function useDeleteSource(source: SourceWithId | null) { }) .then((response) => { if (!response.ok) { - setLoading(false); setDeleteComplete(true); return response.text().then((message) => { throw new Error(`Error ${response.status}: ${message}`); }); } - setLoading(false); setDeleteComplete(true); }) .catch((e) => { console.log(`Failed to delete source-item: ${e}`); }); } else { - setLoading(false); setDeleteComplete(false); } - }, [source]); - return [loading, deleteComplete]; + }; + return [setSourceToPurge, deleteComplete]; } From 89d6ec88ffd21ad209da1bcf20a2cebb9f2182b2 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 07:20:20 +0200 Subject: [PATCH 17/41] fix: rebase to main --- src/api/manager/job/syncInventory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index 774c9d7..cdfc0cb 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -42,7 +42,7 @@ async function getSourcesFromAPI(): Promise { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate }, - createdAt: new Date() + createdAt: new Date() } satisfies SourceWithoutLastConnected) ); } From f3ecb17a3b7dcc15c3ec2cbb264712773f1bf336 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 12:35:40 +0200 Subject: [PATCH 18/41] fix: removed created-at and used last-connected instead --- src/api/manager/job/syncInventory.ts | 6 ++---- src/interfaces/Source.ts | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/api/manager/job/syncInventory.ts b/src/api/manager/job/syncInventory.ts index cdfc0cb..2e813d0 100644 --- a/src/api/manager/job/syncInventory.ts +++ b/src/api/manager/job/syncInventory.ts @@ -41,8 +41,7 @@ async function getSourcesFromAPI(): Promise { audio_stream: { number_of_channels: source?.audio_stream?.number_of_channels, sample_rate: source?.audio_stream?.sample_rate - }, - createdAt: new Date() + } } satisfies SourceWithoutLastConnected) ); } @@ -87,8 +86,7 @@ export async function runSyncInventory() { const apiSource = apiSources.find((source) => { return ( source.ingest_name === inventorySource.ingest_name && - source.ingest_source_name === inventorySource.ingest_source_name && - source.ingest_type === inventorySource.type + source.ingest_source_name === inventorySource.ingest_source_name ); }); if (!apiSource) { diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 1102030..1aec774 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -30,7 +30,6 @@ export interface Source { ingest_type: string; video_stream: VideoStream; audio_stream: AudioStream; - createdAt: Date; lastConnected: Date; } From 88ff05183b6b53a870cf9e7f9ea8e4ccd6f41f54 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 12:47:29 +0200 Subject: [PATCH 19/41] fix: added better error-handling and made more logical naming of code --- src/components/inventory/Inventory.tsx | 1 - src/hooks/sources/useDeleteSource.tsx | 36 ------------------- src/hooks/sources/useSetSourceToPerge.tsx | 44 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 37 deletions(-) delete mode 100644 src/hooks/sources/useDeleteSource.tsx create mode 100644 src/hooks/sources/useSetSourceToPerge.tsx diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index 280a099..c7c7670 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -8,7 +8,6 @@ import SourceListItem from '../../components/sourceListItem/SourceListItem'; import { SourceWithId } from '../../interfaces/Source'; import EditView from './editView/EditView'; import FilterContext from './FilterContext'; -import { useDeleteSource } from '../../hooks/sources/useDeleteSource'; import styles from './Inventory.module.scss'; export default function Inventory() { diff --git a/src/hooks/sources/useDeleteSource.tsx b/src/hooks/sources/useDeleteSource.tsx deleted file mode 100644 index 29c4bb1..0000000 --- a/src/hooks/sources/useDeleteSource.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useState } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; -import { CallbackHook } from '../types'; - -export function useDeleteSource(): CallbackHook< - (source: SourceWithId) => void -> { - const [deleteComplete, setDeleteComplete] = useState(false); - - const setSourceToPurge = (source: SourceWithId) => { - if (source && source.status === 'gone') { - setDeleteComplete(false); - - fetch(`/api/manager/inventory/${source._id}`, { - method: 'PUT', - // TODO: Implement api key - headers: [['x-api-key', `Bearer apisecretkey`]] - }) - .then((response) => { - if (!response.ok) { - setDeleteComplete(true); - return response.text().then((message) => { - throw new Error(`Error ${response.status}: ${message}`); - }); - } - setDeleteComplete(true); - }) - .catch((e) => { - console.log(`Failed to delete source-item: ${e}`); - }); - } else { - setDeleteComplete(false); - } - }; - return [setSourceToPurge, deleteComplete]; -} diff --git a/src/hooks/sources/useSetSourceToPerge.tsx b/src/hooks/sources/useSetSourceToPerge.tsx new file mode 100644 index 0000000..37ce471 --- /dev/null +++ b/src/hooks/sources/useSetSourceToPerge.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { SourceWithId } from '../../interfaces/Source'; +import { CallbackHook } from '../types'; +import { Log } from '../../api/logger'; + +export function useSetSourceToPerge(): CallbackHook< + (source: SourceWithId) => void +> { + const [reloadList, setReloadList] = useState(false); + + const removeInventorySource = (source: SourceWithId) => { + if (source && source.status === 'gone') { + setReloadList(false); + + fetch(`/api/manager/inventory/${source._id}`, { + method: 'PUT', + // TODO: Implement api key + headers: [['x-api-key', `Bearer apisecretkey`]] + }) + .then((response) => { + if (!response.ok) { + setReloadList(true); + Log().error( + `Failed to set ${source.name} with id: ${source._id} to purge` + ); + } else { + console.log( + `${source.name} with id: ${source._id} is set to purge` + ); + } + setReloadList(true); + }) + .catch((e) => { + Log().error( + `Failed to set ${source.name} with id: ${source._id} to purge: ${e}` + ); + throw `Failed to set ${source.name} with id: ${source._id} to purge: ${e}`; + }); + } else { + setReloadList(false); + } + }; + return [removeInventorySource, reloadList]; +} From 824d700c20c1b3134ad2befd3325958cecef6b50 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 5 Sep 2024 13:59:59 +0200 Subject: [PATCH 20/41] fix: corrected misspelling --- src/hooks/sources/useSetSourceToPerge.tsx | 44 ----------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/hooks/sources/useSetSourceToPerge.tsx diff --git a/src/hooks/sources/useSetSourceToPerge.tsx b/src/hooks/sources/useSetSourceToPerge.tsx deleted file mode 100644 index 37ce471..0000000 --- a/src/hooks/sources/useSetSourceToPerge.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from 'react'; -import { SourceWithId } from '../../interfaces/Source'; -import { CallbackHook } from '../types'; -import { Log } from '../../api/logger'; - -export function useSetSourceToPerge(): CallbackHook< - (source: SourceWithId) => void -> { - const [reloadList, setReloadList] = useState(false); - - const removeInventorySource = (source: SourceWithId) => { - if (source && source.status === 'gone') { - setReloadList(false); - - fetch(`/api/manager/inventory/${source._id}`, { - method: 'PUT', - // TODO: Implement api key - headers: [['x-api-key', `Bearer apisecretkey`]] - }) - .then((response) => { - if (!response.ok) { - setReloadList(true); - Log().error( - `Failed to set ${source.name} with id: ${source._id} to purge` - ); - } else { - console.log( - `${source.name} with id: ${source._id} is set to purge` - ); - } - setReloadList(true); - }) - .catch((e) => { - Log().error( - `Failed to set ${source.name} with id: ${source._id} to purge: ${e}` - ); - throw `Failed to set ${source.name} with id: ${source._id} to purge: ${e}`; - }); - } else { - setReloadList(false); - } - }; - return [removeInventorySource, reloadList]; -} From 0389ac5b01b7abdab9d38e56c3857a9d350c4473 Mon Sep 17 00:00:00 2001 From: Benjamin Wallberg Date: Mon, 26 Aug 2024 15:40:56 +0200 Subject: [PATCH 21/41] WIP! feat: add media & html websocket connections --- src/interfaces/production.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index eb4d965..b979a5c 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,11 +3,25 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; +interface HtmlReference { + _id: string; + input_slot: number; + label: string; +} + +interface MediaplayerReference { + _id: string; + input_slot: number; + label: string; +} + export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; + html: HtmlReference[]; + mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 1f579c58bf77c00cb01d0db2b3fff1091d829ef4 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 15:14:06 +0200 Subject: [PATCH 22/41] fix: able to start prod w wevsocket sources --- .env.sample | 1 + src/api/ateliereLive/websocket.ts | 2 +- src/api/manager/productions.ts | 28 ++++++- src/api/manager/sources.ts | 6 +- src/api/manager/workflow.ts | 29 +++---- src/app/production/[id]/page.tsx | 92 ++++++++-------------- src/components/modal/AddSourceModal.tsx | 1 - src/components/sourceCards/SourceCards.tsx | 3 + src/hooks/productions.ts | 4 +- src/hooks/sources/useAddSource.tsx | 39 +++++++++ src/hooks/sources/useSources.tsx | 6 +- src/hooks/useGetFirstEmptySlot.ts | 37 +++++++++ src/interfaces/Source.ts | 2 +- 13 files changed, 158 insertions(+), 92 deletions(-) create mode 100644 src/hooks/sources/useAddSource.tsx create mode 100644 src/hooks/useGetFirstEmptySlot.ts diff --git a/.env.sample b/.env.sample index da9f726..3ff9058 100644 --- a/.env.sample +++ b/.env.sample @@ -4,6 +4,7 @@ MONGODB_URI=${MONGODB_URI:-mongodb://api:password@localhost:27017/live-gui} # Ateliere Live System Controlleer LIVE_URL=${LIVE_URL:-https://localhost:8080} LIVE_CREDENTIALS=${LIVE_CREDENTIALS:-admin:admin} +CONTROL_PANEL_WS==${} # This ENV variable disables SSL Verification, use if the above LIVE_URL doesn't have a proper certificate NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-1} diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 6f51466..df8b47a 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -2,7 +2,7 @@ import WebSocket from 'ws'; function createWebSocket(): Promise { return new Promise((resolve, reject) => { - const ws = new WebSocket(`ws://${process.env.AGILE_WEBSOCKET}`); + const ws = new WebSocket(`ws://${process.env.CONTROL_PANEL_WS}`); ws.on('error', reject); ws.on('open', () => { // const send = ws.send.bind(ws); diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index 714d822..e364776 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -2,7 +2,6 @@ import { Db, ObjectId, UpdateResult } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; import { Production, ProductionWithId } from '../../interfaces/production'; import { Log } from '../logger'; -import { SourceReference, Type } from '../../interfaces/Source'; export async function getProductions(): Promise { const db = await getDatabase(); @@ -29,14 +28,29 @@ export async function setProductionsIsActiveFalse(): Promise< export async function putProduction( id: string, production: Production -): Promise { +): Promise { const db = await getDatabase(); + const newSourceId = new ObjectId().toString(); + + const sources = production.sources + ? production.sources.flatMap((singleSource) => { + return singleSource._id + ? singleSource + : { + _id: newSourceId, + type: singleSource.type, + label: singleSource.label, + input_slot: singleSource.input_slot + }; + }) + : []; + await db.collection('productions').findOneAndReplace( { _id: new ObjectId(id) }, { name: production.name, isActive: production.isActive, - sources: production.sources, + sources: sources, production_settings: production.production_settings } ); @@ -44,6 +58,14 @@ export async function putProduction( if (!production.isActive) { deleteMonitoring(db, id); } + + return { + _id: new ObjectId(id).toString(), + name: production.name, + isActive: production.isActive, + sources: sources, + production_settings: production.production_settings + }; } export async function postProduction(data: Production): Promise { diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index a54fb38..853f43c 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -1,6 +1,6 @@ import inventory from './mocks/inventory.json'; import { Source } from '../../interfaces/Source'; -import { ObjectId, OptionalId } from 'mongodb'; +import { ObjectId, OptionalId, WithId } from 'mongodb'; import { getDatabase } from '../mongoClient/dbClient'; export function getMockedSources() { @@ -21,7 +21,9 @@ export async function getSources() { const db = await getDatabase(); return await db.collection('inventory').find().toArray(); } -export async function getSourcesByIds(_ids: string[]) { +export async function getSourcesByIds( + _ids: string[] +): Promise[]> { const db = await getDatabase().catch(() => { throw new Error("Can't connect to Database"); }); diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 6dc8ba0..89808ac 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -1,3 +1,4 @@ +import { SourceWithId } from './../../interfaces/Source'; import { Production, ProductionSettings, @@ -35,7 +36,7 @@ import { ResourcesSenderNetworkEndpoint } from '../../../types/ateliere-live'; import { getSourcesByIds } from './sources'; -import { SourceWithId, SourceToPipelineStream } from '../../interfaces/Source'; +import { SourceToPipelineStream } from '../../interfaces/Source'; import { getAvailablePortsForIngest, getCurrentlyUsedPorts, @@ -77,10 +78,6 @@ async function connectIngestSources( let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; - console.log('connectIngestSources - productionSettings', productionSettings); - console.log('connectIngestSources - sources', sources); - console.log('connectIngestSources - usedPorts', usedPorts); - for (const source of sources) { input_slot = input_slot + 1; const ingestUuid = await getUuidFromIngestName( @@ -464,8 +461,6 @@ export async function startProduction( await initDedicatedPorts(); - console.log('startProduction - production', production); - let streams: SourceToPipelineStream[] = []; // Try to setup streams from ingest(s) to pipeline(s) start try { @@ -478,7 +473,6 @@ export async function startProduction( const mediaPlayerSources = production.sources.filter( (source) => source.type === 'mediaplayer' ); - htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); mediaPlayerSources.map((source) => controlPanelWS.createMediaplayer(source.input_slot) @@ -490,7 +484,10 @@ export async function startProduction( // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( production.sources - .filter((source) => source._id !== undefined) + .filter( + (source) => + source._id !== undefined && source.type === 'ingest_source' + ) .map((source) => { return source._id!.toString(); }) @@ -502,13 +499,11 @@ export async function startProduction( throw "Can't get source!"; }); - console.log('startProduction - production', production); // Lookup pipeline UUIDs from pipeline names and insert to production_settings await insertPipelineUuid(production_settings).catch((error) => { throw error; }); - console.log('startProduction - production', production); // Fetch expanded pipeline objects from Ateliere Live const pipelinesToUsePromises = production_settings.pipelines.map( (pipeline) => { @@ -516,13 +511,11 @@ export async function startProduction( } ); const pipelinesToUse = await Promise.all(pipelinesToUsePromises); - console.log('startProduction - pipelinesToUse', pipelinesToUse); // Check if pipelines are already in use by another production const hasAlreadyUsedPipeline = pipelinesToUse.filter((pipeline) => isUsed(pipeline) ); - console.log('startProduction - production', production); if (hasAlreadyUsedPipeline.length > 0) { Log().error( @@ -534,7 +527,6 @@ export async function startProduction( (p) => p.name )}`; } - console.log('startProduction - hasAlreadyUsedPipeline', hasAlreadyUsedPipeline); const resetPipelinePromises = production_settings.pipelines.map( (pipeline) => { @@ -544,7 +536,6 @@ export async function startProduction( await Promise.all(resetPipelinePromises).catch((error) => { throw `Failed to reset pipelines: ${error}`; }); - console.log('startProduction - resetPipelinePromises', resetPipelinePromises); // Fetch all control panels from Ateliere Live const allControlPanels = await getControlPanels(); @@ -558,7 +549,6 @@ export async function startProduction( const hasAlreadyUsedControlPanel = controlPanelsToUse.filter( (controlPanel) => controlPanel.outgoing_connections.length > 0 ); - console.log('startProduction - hasAlreadyUsedControlPanel', hasAlreadyUsedControlPanel); if (hasAlreadyUsedControlPanel.length > 0) { Log().error( @@ -585,7 +575,6 @@ export async function startProduction( return pipeline.uuid; }) ); - console.log('startProduction - stopPipelines', production_settings.pipelines); streams = await connectIngestSources( production_settings, @@ -610,7 +599,6 @@ export async function startProduction( error: 'Could not setup streams: Unexpected error occured' }; } - console.log('startProduction - streams', streams); return { ok: false, value: [ @@ -774,8 +762,9 @@ export async function startProduction( ); return { ...source, - stream_uuids: streamsForSource?.map((s) => s.stream_uuid), - input_slot: streamsForSource[0].input_slot + stream_uuids: + streamsForSource?.map((s) => s.stream_uuid) || undefined, + input_slot: source.input_slot }; }), isActive: true diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 26b8d13..a96d3cb 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -1,4 +1,5 @@ 'use client'; + import React, { useEffect, useState, KeyboardEvent } from 'react'; import { PageProps } from '../../../../.next/types/app/production/[id]/page'; import SourceListItem from '../../../components/sourceListItem/SourceListItem'; @@ -42,7 +43,8 @@ import { useDeleteStream, useCreateStream } from '../../../hooks/streams'; import { MonitoringButton } from '../../../components/button/MonitoringButton'; import { useGetMultiviewPreset } from '../../../hooks/multiviewPreset'; import { useMultiviews } from '../../../hooks/multiviews'; -import { v4 as uuidv4 } from 'uuid'; +import { useAddSource } from '../../../hooks/sources/useAddSource'; +import { useGetFirstEmptySlot } from '../../../hooks/useGetFirstEmptySlot'; import { Select } from '../../../components/select/Select'; export default function ProductionConfiguration({ params }: PageProps) { @@ -95,6 +97,10 @@ export default function ProductionConfiguration({ params }: PageProps) { const [deleteSourceStatus, setDeleteSourceStatus] = useState(); + // Create source + const [firstEmptySlot] = useGetFirstEmptySlot(); + const [addSource] = useAddSource(); + const isAddButtonDisabled = selectedValue !== 'HTML' && selectedValue !== 'Media Player'; @@ -103,29 +109,24 @@ export default function ProductionConfiguration({ params }: PageProps) { refreshControlPanels(); }, [productionSetup?.isActive]); - // TODO: Väldigt lik den för ingest_source --> ändra?? const addSourceToProduction = (type: Type) => { - const newSource: SourceReference = { - _id: uuidv4(), + const input: SourceReference = { type: type, label: type === 'html' ? 'HTML Input' : 'Media Player Source', - input_slot: getFirstEmptySlot() + input_slot: firstEmptySlot(productionSetup) }; - setSourceReferenceToAdd(newSource); - - if (productionSetup) { - const updatedSetup = addSetupItem(newSource, productionSetup); + if (!productionSetup) return; + addSource(input, productionSetup).then((updatedSetup) => { if (!updatedSetup) return; + setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - refreshProduction(); - setAddSourceModal(false); - setSourceReferenceToAdd(undefined); - }); - setAddSourceStatus(undefined); - } + refreshProduction(); + setAddSourceModal(false); + setSelectedSource(undefined); + }); + setAddSourceStatus(undefined); }; const setSelectedControlPanel = (controlPanel: string[]) => { @@ -403,6 +404,8 @@ export default function ProductionConfiguration({ params }: PageProps) { ); } + + // Adding source to a production, both in setup-mode and in live-mode function getSourcesToDisplay( filteredSources: Map ): React.ReactNode[] { @@ -417,23 +420,18 @@ export default function ProductionConfiguration({ params }: PageProps) { setSelectedSource(source); setAddSourceModal(true); } else if (productionSetup) { - const updatedSetup = addSetupItem( - { - _id: source._id.toString(), - type: 'ingest_source', - label: source.ingest_source_name, - input_slot: getFirstEmptySlot() - }, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setAddSourceModal(false); - setSelectedSource(undefined); - } - ); + const input: SourceReference = { + _id: source._id.toString(), + type: 'ingest_source', + label: source.ingest_source_name, + input_slot: firstEmptySlot(productionSetup) + }; + addSource(input, productionSetup).then((updatedSetup) => { + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + setAddSourceModal(false); + setSelectedSource(undefined); + }); } }} /> @@ -441,28 +439,6 @@ export default function ProductionConfiguration({ params }: PageProps) { }); } - const getFirstEmptySlot = () => { - if (!productionSetup) throw 'no_production'; - let firstEmptySlot = productionSetup.sources.length + 1; - if (productionSetup.sources.length === 0) { - return firstEmptySlot; - } - for ( - let i = 0; - i < - productionSetup.sources[productionSetup.sources.length - 1].input_slot; - i++ - ) { - if ( - !productionSetup.sources.some((source) => source.input_slot === i + 1) - ) { - firstEmptySlot = i + 1; - break; - } - } - return firstEmptySlot; - }; - const handleAddSource = async () => { setAddSourceStatus(undefined); if ( @@ -477,11 +453,10 @@ export default function ProductionConfiguration({ params }: PageProps) { ) : false) ) { - const firstEmptySlot = getFirstEmptySlot(); const result = await createStream( selectedSource, productionSetup, - firstEmptySlot ? firstEmptySlot : productionSetup.sources.length + 1 + firstEmptySlot(productionSetup) ); if (!result.ok) { if (!result.value) { @@ -503,7 +478,7 @@ export default function ProductionConfiguration({ params }: PageProps) { type: 'ingest_source', label: selectedSource.name, stream_uuids: result.value.streams.map((r) => r.stream_uuid), - input_slot: getFirstEmptySlot() + input_slot: firstEmptySlot(productionSetup) }; const updatedSetup = addSetupItem(sourceToAdd, productionSetup); if (!updatedSetup) return; @@ -521,7 +496,6 @@ export default function ProductionConfiguration({ params }: PageProps) { } }; - // TODO: HTML och MEDIA PLAYER KÄLLOR TAS INTE BORT const handleRemoveSource = async () => { if ( productionSetup && diff --git a/src/components/modal/AddSourceModal.tsx b/src/components/modal/AddSourceModal.tsx index d81c171..236b3cb 100644 --- a/src/components/modal/AddSourceModal.tsx +++ b/src/components/modal/AddSourceModal.tsx @@ -26,7 +26,6 @@ export function AddSourceModal({ return (
    -

    HEJ

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

    {status && }
    diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index d861578..9af6ec4 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -6,6 +6,7 @@ import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; + export default function SourceCards({ productionSetup, sourceRef, @@ -37,6 +38,8 @@ export default function SourceCards({ const gridItems = items.map((source) => { const isSource = isISource(source); + if (!source._id) return; + return ( => { + return async (id: string, production: Production): Promise => { const response = await fetch(`/api/manager/productions/${id}`, { method: 'PUT', // TODO: Implement api key @@ -45,7 +45,7 @@ export function usePutProduction() { body: JSON.stringify(production) }); if (response.ok) { - return; + return response.json(); } throw await response.text(); }; diff --git a/src/hooks/sources/useAddSource.tsx b/src/hooks/sources/useAddSource.tsx new file mode 100644 index 0000000..c04c1bb --- /dev/null +++ b/src/hooks/sources/useAddSource.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { addSetupItem } from '../items/addSetupItem'; +import { CallbackHook } from '../types'; +import { Production } from '../../interfaces/production'; +import { usePutProduction } from '../productions'; +import { SourceReference } from '../../interfaces/Source'; + +export function useAddSource(): CallbackHook< + ( + input: SourceReference, + productionSetup: Production + ) => Promise +> { + const [loading, setLoading] = useState(true); + const putProduction = usePutProduction(); + + const addSource = async ( + input: SourceReference, + productionSetup: Production + ) => { + const updatedSetup = addSetupItem( + { + _id: input._id ? input._id : undefined, + type: input.type, + label: input.label, + input_slot: input.input_slot + }, + productionSetup + ); + + if (!updatedSetup) return; + + const res = await putProduction(updatedSetup._id.toString(), updatedSetup); + console.log('res', res); + return res; + }; + + return [addSource, loading]; +} diff --git a/src/hooks/sources/useSources.tsx b/src/hooks/sources/useSources.tsx index 1b58418..8b44ea2 100644 --- a/src/hooks/sources/useSources.tsx +++ b/src/hooks/sources/useSources.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { SourceWithId } from '../../interfaces/Source'; export function useSources( - deleteComplete?: boolean, + reloadList?: boolean, updatedSource?: SourceWithId ): [Map, boolean] { const [sources, setSources] = useState>( @@ -11,7 +11,7 @@ export function useSources( const [loading, setLoading] = useState(true); useEffect(() => { - if (!updatedSource || deleteComplete) { + if (!updatedSource || reloadList) { fetch('/api/manager/sources?mocked=false', { method: 'GET', // TODO: Implement api key @@ -34,6 +34,6 @@ export function useSources( } sources.set(updatedSource._id.toString(), updatedSource); setSources(new Map(sources)); - }, [updatedSource, deleteComplete]); + }, [updatedSource, reloadList]); return [sources, loading]; } diff --git a/src/hooks/useGetFirstEmptySlot.ts b/src/hooks/useGetFirstEmptySlot.ts new file mode 100644 index 0000000..8cda182 --- /dev/null +++ b/src/hooks/useGetFirstEmptySlot.ts @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import { Production } from '../interfaces/production'; +import { CallbackHook } from './types'; + +export function useGetFirstEmptySlot(): CallbackHook< + (productionSetup?: Production | undefined) => number +> { + const [loading, setLoading] = useState(true); + + const findFirstEmptySlot = (productionSetup: Production | undefined) => { + if (!productionSetup) throw 'no_production'; + + if (productionSetup) { + let firstEmptySlotTemp = productionSetup.sources.length + 1; + if (productionSetup.sources.length === 0) { + return firstEmptySlotTemp; + } + for ( + let i = 0; + i < + productionSetup.sources[productionSetup.sources.length - 1].input_slot; + i++ + ) { + if ( + !productionSetup.sources.some((source) => source.input_slot === i + 1) + ) { + firstEmptySlotTemp = i + 1; + break; + } + } + return firstEmptySlotTemp; + } else { + return 0; + } + }; + return [findFirstEmptySlot, loading]; +} diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 1aec774..2e9935b 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -34,7 +34,7 @@ export interface Source { } export interface SourceReference { - _id: string; + _id?: string; type: Type; label: string; stream_uuids?: string[]; From 0f2d040cdb8c782107f60dd77f25a24252f21e0d Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 19:58:43 +0200 Subject: [PATCH 23/41] fix: solved errors and conflicts --- .../pipelines/multiviews/multiviews.ts | 7 +- src/api/manager/productions.ts | 2 + src/api/manager/sources.ts | 1 - src/app/production/[id]/page.tsx | 3 +- src/components/inventory/Inventory.tsx | 2 +- src/components/sourceCard/SourceCard.tsx | 123 +++++---------- src/components/sourceCards/SourceCards.tsx | 143 +++++++++++------- src/hooks/useDragableItems.ts | 89 ++++------- 8 files changed, 166 insertions(+), 204 deletions(-) diff --git a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts index 5f24a86..ac76f6c 100644 --- a/src/api/ateliereLive/pipelines/multiviews/multiviews.ts +++ b/src/api/ateliereLive/pipelines/multiviews/multiviews.ts @@ -64,13 +64,12 @@ export async function createMultiviewForPipeline( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion productionSettings.pipelines[multiviewIndex].pipeline_id!; const sources = await getSourcesByIds( - sourceRefs.map((ref) => ref._id.toString()) + sourceRefs.map((ref) => (ref._id ? ref._id.toString() : '')) ); const sourceRefsWithLabels = sourceRefs.map((ref) => { + const refId = ref._id ? ref._id.toString() : ''; if (!ref.label) { - const source = sources.find( - (source) => source._id.toString() === ref._id.toString() - ); + const source = sources.find((source) => source._id.toString() === refId); ref.label = source?.name || ''; } return ref; diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index e364776..12bec1a 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -64,6 +64,8 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, + html: [], + mediaplayers: [], production_settings: production.production_settings }; } diff --git a/src/api/manager/sources.ts b/src/api/manager/sources.ts index 853f43c..4e77f39 100644 --- a/src/api/manager/sources.ts +++ b/src/api/manager/sources.ts @@ -27,7 +27,6 @@ export async function getSourcesByIds( const db = await getDatabase().catch(() => { throw new Error("Can't connect to Database"); }); - const objectIds = _ids.map((id: string) => new ObjectId(id)); const sources = await db diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index a96d3cb..7355155 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -118,7 +118,6 @@ export default function ProductionConfiguration({ params }: PageProps) { if (!productionSetup) return; addSource(input, productionSetup).then((updatedSetup) => { - if (!updatedSetup) return; setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); @@ -720,7 +719,7 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); diff --git a/src/components/inventory/Inventory.tsx b/src/components/inventory/Inventory.tsx index c7c7670..dacb262 100644 --- a/src/components/inventory/Inventory.tsx +++ b/src/components/inventory/Inventory.tsx @@ -82,7 +82,7 @@ export default function Inventory() {
      - {loading ? '' : getSourcesToDisplay(filteredSources)} + {getSourcesToDisplay(filteredSources)}
    diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index f2b9446..6eed4e2 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -8,16 +8,14 @@ import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source?: ISource; + source: ISource; label: string; - onSourceUpdate: (source: SourceReference) => void; + onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src?: string; - sourceRef?: SourceReference; - type: Type; + src: string; }; export default function SourceCard({ @@ -28,13 +26,9 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style, - sourceRef, - type + style }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState( - sourceRef?.label || source?.name - ); + const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); const t = useTranslate(); @@ -43,29 +37,21 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - if (sourceLabel?.length === 0) { - if (source) { - setSourceLabel(source.name); - } else if (sourceRef) { - setSourceLabel(sourceRef.label); - } + // if (source.name === label) { + // return; + // } + if (sourceLabel.length === 0) { + setSourceLabel(source.name); } - - if (source) { - onSourceUpdate({ + onSourceUpdate( + { _id: source._id.toString(), - type: 'ingest_source', - label: sourceLabel || source.name, + label: sourceLabel, + type: source.ingest_type as Type, input_slot: source.input_slot - }); - } else if (sourceRef) { - onSourceUpdate({ - _id: sourceRef._id, - type: sourceRef.type, - label: sourceLabel || sourceRef.label, - input_slot: sourceRef.input_slot - }); - } + }, + source + ); }; const handleKeyDown = (event: KeyboardEvent) => { @@ -83,7 +69,7 @@ export default function SourceCard({
    { @@ -92,59 +78,26 @@ export default function SourceCard({ onBlur={saveText} />
    - {source && source.src && ( - - )} - {!source && sourceRef && } - {(sourceRef || source) && ( -

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

    - )} - - {source && ( -

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

    - )} - {(source || sourceRef) && ( - - )} + +

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

    + ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9af6ec4..04101d0 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -1,80 +1,113 @@ 'use client'; import React, { useState } from 'react'; -import { SourceReference } from '../../interfaces/Source'; +import { SourceReference, Type } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; +import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; export default function SourceCards({ productionSetup, - sourceRef, updateProduction, onSourceUpdate, onSourceRemoval }: { productionSetup: Production; - sourceRef?: SourceReference; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference) => void; + onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; onSourceRemoval: (source: SourceReference) => void; }) { const [items, moveItem, loading] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); + const currentOrder: SourceReference[] = items.map((source) => { + return { + _id: source._id.toString(), + label: source.label, + type: source.ingest_type as Type, + input_slot: source.input_slot, + stream_uuids: source.stream_uuids + }; + }); - if (loading || !items) return null; - - // Filter SourceReference and ISource objects correctly - const sourceReferences = items.filter( - (item): item is SourceReference => item.type !== 'ingest_source' - ); - - const isISource = (source: SourceReference | ISource): source is ISource => { - // Use properties unique to ISource to check the type - return 'src' in source; - }; - - const gridItems = items.map((source) => { - const isSource = isISource(source); - - if (!source._id) return; + const gridItems: React.JSX.Element[] = []; + let tempItems = [...items]; + let firstEmptySlot = items.length + 1; - return ( - - {isSource ? ( - setSelectingText(isSelecting)} - type={'ingest_source'} - /> - ) : ( - setSelectingText(isSelecting)} - type={source.type} - /> - )} - - ); - }); + if (!items || items.length === 0) return null; + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + if (!items.some((source) => source.input_slot === i + 1)) { + firstEmptySlot = i + 1; + break; + } + } + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + // console.log(`On input slot: ${i + 1}`); + // console.log(`Checking sources:`); + // console.log(tempItems); + tempItems.every((source) => { + if (source.input_slot === i + 1) { + // console.log(`Found source on input slot: ${i + 1}`); + // console.log(`Removing source "${source.name}" from sources list`); + tempItems = tempItems.filter((i) => i._id !== source._id); + // console.log(`Adding source "${source.name}" to grid`); + if (!productionSetup.isActive) { + gridItems.push( + + + setSelectingText(isSelecting) + } + /> + + ); + } else { + gridItems.push( + + setSelectingText(isSelecting) + } + /> + ); + } + return false; + } else { + // console.log(`No source found on input slot: ${i + 1}`); + // console.log(`Adding empty slot to grid`); + if (productionSetup.isActive) { + gridItems.push( + + ); + } + return false; + } + }); + } return <>{gridItems}; } diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 5cf6d5a..95601dc 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -9,84 +9,61 @@ export interface ISource extends SourceWithId { stream_uuids?: string[]; src: string; } - export function useDragableItems( sources: SourceReference[] -): [ - (SourceReference | ISource)[], - (originId: string, destinationId: string) => void, - boolean -] { +): [ISource[], (originId: string, destinationId: string) => void, boolean] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState<(SourceReference | ISource)[]>( + const [items, setItems] = useState( sources.flatMap((ref) => { - const source = inventorySources.get(ref._id); + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); if (!source) return []; return { ...source, - _id: ref._id, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - ingest_source_name: source.ingest_source_name, - ingest_name: source.ingest_name, - video_stream: source.video_stream, - audio_stream: source.audio_stream, - status: source.status, - type: source.type, - tags: source.tags, - name: source.name + src: getSourceThumbnail(source) }; }) ); useEffect(() => { - const updatedItems = sources.map((ref) => { - const source = inventorySources.get(ref._id); - if (!source) return { ...ref }; - return { - ...ref, - _id: ref._id, - status: source.status, - name: source.name, - type: source.type, - tags: source.tags, - ingest_name: source.ingest_name, - ingest_source_name: source.ingest_source_name, - ingest_type: source.ingest_type, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source), - video_stream: source.video_stream, - audio_stream: source.audio_stream, - lastConnected: source.lastConnected - }; - }); - setItems(updatedItems); + setItems( + sources.flatMap((ref) => { + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); + if (!source) return []; + return { + ...source, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source) + }; + }) + ); }, [sources, inventorySources]); const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((item) => item._id.toString() === originId); + const originSource = items.find((i) => i._id.toString() === originId); const destinationSource = items.find( - (item) => item._id.toString() === destinationId + (i) => i._id.toString() === destinationId ); - if (!originSource || !destinationSource) return; - - const updatedItems = items - .map((item) => { - if (item._id === originSource._id) - return { ...item, input_slot: destinationSource.input_slot }; - if (item._id === destinationSource._id) - return { ...item, input_slot: originSource.input_slot }; - return item; - }) - .sort((a, b) => a.input_slot - b.input_slot); - + const originInputSlot = originSource.input_slot; + const destinationInputSlot = destinationSource.input_slot; + originSource.input_slot = destinationInputSlot; + destinationSource.input_slot = originInputSlot; + const updatedItems = [ + ...items.filter( + (i) => i._id !== originSource._id && i._id !== destinationSource._id + ), + originSource, + destinationSource + ].sort((a, b) => a.input_slot - b.input_slot); setItems(updatedItems); }; - return [allItems, moveItems, loading]; + return [items, moveItem, loading]; } From 13772e4d7de50037dfc72a737c0f513471491686 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 20:12:07 +0200 Subject: [PATCH 24/41] fix: added missing types --- src/hooks/items/addSetupItem.ts | 6 +++--- src/interfaces/production.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index b341c44..e330a71 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,5 +1,5 @@ -import { SourceReference } from '../../interfaces/Source'; -import { Production } from '../../interfaces/production'; +import { SourceReference, Type } from '../../interfaces/Source'; +import { HtmlReference, MediaplayerReference, Production } from '../../interfaces/production'; export function addSetupItem( source: SourceReference, @@ -15,7 +15,7 @@ export function addSetupItem( { _id: source._id, type: source.type, - label: source.label, + label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot } diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index b979a5c..9db620d 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,13 +3,13 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -interface HtmlReference { +export interface HtmlReference { _id: string; input_slot: number; label: string; } -interface MediaplayerReference { +export interface MediaplayerReference { _id: string; input_slot: number; label: string; From bcdb35bcda66c0c92e20e91c340a4e7c284191a2 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Wed, 11 Sep 2024 20:14:59 +0200 Subject: [PATCH 25/41] fix: lint error --- src/hooks/items/addSetupItem.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index e330a71..a8e046f 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,5 +1,9 @@ import { SourceReference, Type } from '../../interfaces/Source'; -import { HtmlReference, MediaplayerReference, Production } from '../../interfaces/production'; +import { + HtmlReference, + MediaplayerReference, + Production +} from '../../interfaces/production'; export function addSetupItem( source: SourceReference, @@ -15,7 +19,7 @@ export function addSetupItem( { _id: source._id, type: source.type, - label: source.label, + label: source.label, stream_uuids: source.stream_uuids, input_slot: source.input_slot } From c60a4786e1cd5b5fb9f9088dff7cde6d0a5e695f Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 14:38:29 +0200 Subject: [PATCH 26/41] fix: solved conflicts and problems with 'media-html-inputs'-branch --- src/api/ateliereLive/ingest.ts | 18 --- src/api/manager/productions.ts | 2 - src/app/production/[id]/page.tsx | 4 - src/components/dragElement/DragItem.tsx | 5 +- src/components/sourceCard/SourceCard.tsx | 123 ++++++++++++------ src/components/sourceCards/SourceCards.tsx | 140 +++++++-------------- src/hooks/items/addSetupItem.ts | 8 +- src/hooks/productions.ts | 4 +- src/hooks/sources/useAddSource.tsx | 1 - src/hooks/useDragableItems.ts | 87 ++++++++----- src/interfaces/production.ts | 14 --- 11 files changed, 187 insertions(+), 219 deletions(-) diff --git a/src/api/ateliereLive/ingest.ts b/src/api/ateliereLive/ingest.ts index c942083..0c7b521 100644 --- a/src/api/ateliereLive/ingest.ts +++ b/src/api/ateliereLive/ingest.ts @@ -74,24 +74,6 @@ export async function getIngests(): Promise { throw await response.json(); } -export async function getCompleteIngests(): Promise { - const response = await fetch( - new URL(LIVE_BASE_API_PATH + `/ingests?expand=true`, process.env.LIVE_URL), - { - headers: { - authorization: getAuthorizationHeader() - }, - next: { - revalidate: 0 - } - } - ); - if (response.ok) { - return response.json(); - } - throw await response.json(); -} - export async function getIngest( uuid: string ): Promise { diff --git a/src/api/manager/productions.ts b/src/api/manager/productions.ts index 12bec1a..e364776 100644 --- a/src/api/manager/productions.ts +++ b/src/api/manager/productions.ts @@ -64,8 +64,6 @@ export async function putProduction( name: production.name, isActive: production.isActive, sources: sources, - html: [], - mediaplayers: [], production_settings: production.production_settings }; } diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 7355155..0f54b14 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -66,8 +66,6 @@ export default function ProductionConfiguration({ params }: PageProps) { const [selectedSourceRef, setSelectedSourceRef] = useState< SourceReference | undefined >(); - const [sourceReferenceToAdd, setSourceReferenceToAdd] = - useState(); const [createStream, loadingCreateStream] = useCreateStream(); const [deleteStream, loadingDeleteStream] = useDeleteStream(); //PRODUCTION @@ -119,7 +117,6 @@ export default function ProductionConfiguration({ params }: PageProps) { if (!productionSetup) return; addSource(input, productionSetup).then((updatedSetup) => { if (!updatedSetup) return; - setSourceReferenceToAdd(updatedSetup.sources[0]); setProductionSetup(updatedSetup); refreshProduction(); setAddSourceModal(false); @@ -719,7 +716,6 @@ export default function ProductionConfiguration({ params }: PageProps) { {productionSetup?.sources && sources.size > 0 && ( { updateProduction(productionSetup._id, updated); diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index 4888298..7f7364f 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -1,9 +1,8 @@ -import React, { ReactElement, memo, useEffect, useRef } from 'react'; +import React, { ReactElement, memo, useRef } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; -import { v4 as uuidv4 } from 'uuid'; interface IDrag { id: ObjectId | string; @@ -59,7 +58,7 @@ const DragItem: React.FC = memo( ...productionSetup, sources: currentOrder.map((source) => ({ ...source, - _id: source._id || uuidv4() // Ensure ID consistency + _id: source._id || undefined })) }; diff --git a/src/components/sourceCard/SourceCard.tsx b/src/components/sourceCard/SourceCard.tsx index 6eed4e2..9645e9c 100644 --- a/src/components/sourceCard/SourceCard.tsx +++ b/src/components/sourceCard/SourceCard.tsx @@ -1,5 +1,4 @@ 'use client'; - import React, { ChangeEvent, KeyboardEvent, useState } from 'react'; import { IconTrash } from '@tabler/icons-react'; import { SourceReference, Type } from '../../interfaces/Source'; @@ -8,14 +7,16 @@ import { useTranslate } from '../../i18n/useTranslate'; import { ISource } from '../../hooks/useDragableItems'; type SourceCardProps = { - source: ISource; + source?: ISource; label: string; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; onSelectingText: (bool: boolean) => void; forwardedRef?: React.LegacyRef; style?: object; - src: string; + src?: string; + sourceRef?: SourceReference; + type: Type; }; export default function SourceCard({ @@ -26,10 +27,13 @@ export default function SourceCard({ onSelectingText, forwardedRef, src, - style + style, + sourceRef, + type }: SourceCardProps) { - const [sourceLabel, setSourceLabel] = useState(label ? label : source.name); - + const [sourceLabel, setSourceLabel] = useState( + sourceRef?.label || source?.name + ); const t = useTranslate(); const updateText = (event: ChangeEvent) => { @@ -37,29 +41,34 @@ export default function SourceCard({ }; const saveText = () => { onSelectingText(false); - // if (source.name === label) { - // return; - // } - if (sourceLabel.length === 0) { - setSourceLabel(source.name); + if (sourceLabel?.length === 0) { + if (source) { + setSourceLabel(source.name); + } else if (sourceRef) { + setSourceLabel(sourceRef.label); + } } - onSourceUpdate( - { + if (source) { + onSourceUpdate({ _id: source._id.toString(), - label: sourceLabel, - type: source.ingest_type as Type, + type: 'ingest_source', + label: sourceLabel || source.name, input_slot: source.input_slot - }, - source - ); + }); + } else if (sourceRef) { + onSourceUpdate({ + _id: sourceRef._id, + type: sourceRef.type, + label: sourceLabel || sourceRef.label, + input_slot: sourceRef.input_slot + }); + } }; - const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Enter') { event.currentTarget.blur(); } }; - return (
    - -

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

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

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

    + )} + {source && ( +

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

    + )} + {(source || sourceRef) && ( + + )} ); } diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 04101d0..7255b70 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -1,13 +1,10 @@ 'use client'; - import React, { useState } from 'react'; -import { SourceReference, Type } from '../../interfaces/Source'; +import { SourceReference } from '../../interfaces/Source'; import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; -import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; - export default function SourceCards({ productionSetup, updateProduction, @@ -16,98 +13,55 @@ export default function SourceCards({ }: { productionSetup: Production; updateProduction: (updated: Production) => void; - onSourceUpdate: (source: SourceReference, sourceItem: ISource) => void; + onSourceUpdate: (source: SourceReference) => void; onSourceRemoval: (source: SourceReference) => void; }) { - const [items, moveItem, loading] = useDragableItems(productionSetup.sources); + const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); - const currentOrder: SourceReference[] = items.map((source) => { - return { - _id: source._id.toString(), - label: source.label, - type: source.ingest_type as Type, - input_slot: source.input_slot, - stream_uuids: source.stream_uuids - }; - }); + if (!items) return null; + const sourceReferences = items.filter( + (item): item is SourceReference => item.type !== 'ingest_source' + ); + const isISource = (source: SourceReference | ISource): source is ISource => { + return 'src' in source; + }; - const gridItems: React.JSX.Element[] = []; - let tempItems = [...items]; - let firstEmptySlot = items.length + 1; - - if (!items || items.length === 0) return null; - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - if (!items.some((source) => source.input_slot === i + 1)) { - firstEmptySlot = i + 1; - break; - } - } - for (let i = 0; i < items[items.length - 1].input_slot; i++) { - // console.log(`On input slot: ${i + 1}`); - // console.log(`Checking sources:`); - // console.log(tempItems); - tempItems.every((source) => { - if (source.input_slot === i + 1) { - // console.log(`Found source on input slot: ${i + 1}`); - // console.log(`Removing source "${source.name}" from sources list`); - tempItems = tempItems.filter((i) => i._id !== source._id); - // console.log(`Adding source "${source.name}" to grid`); - if (!productionSetup.isActive) { - gridItems.push( - - - setSelectingText(isSelecting) - } - /> - - ); - } else { - gridItems.push( - - setSelectingText(isSelecting) - } - /> - ); - } - return false; - } else { - // console.log(`No source found on input slot: ${i + 1}`); - // console.log(`Adding empty slot to grid`); - if (productionSetup.isActive) { - gridItems.push( - - ); - } - - return false; - } - }); - } + const gridItems = items.map((source) => { + const id = source._id ? source._id : ''; + const isSource = isISource(source); + return ( + + {isSource ? ( + setSelectingText(isSelecting)} + type={'ingest_source'} + /> + ) : ( + setSelectingText(isSelecting)} + type={source.type} + /> + )} + + ); + }); return <>{gridItems}; } diff --git a/src/hooks/items/addSetupItem.ts b/src/hooks/items/addSetupItem.ts index a8e046f..b341c44 100644 --- a/src/hooks/items/addSetupItem.ts +++ b/src/hooks/items/addSetupItem.ts @@ -1,9 +1,5 @@ -import { SourceReference, Type } from '../../interfaces/Source'; -import { - HtmlReference, - MediaplayerReference, - Production -} from '../../interfaces/production'; +import { SourceReference } from '../../interfaces/Source'; +import { Production } from '../../interfaces/production'; export function addSetupItem( source: SourceReference, diff --git a/src/hooks/productions.ts b/src/hooks/productions.ts index 8e29cd1..cdfe918 100644 --- a/src/hooks/productions.ts +++ b/src/hooks/productions.ts @@ -10,9 +10,7 @@ export function usePostProduction() { body: JSON.stringify({ isActive: false, name, - sources: [], - html: [], - mediaplayers: [] + sources: [] }) }); if (response.ok) { diff --git a/src/hooks/sources/useAddSource.tsx b/src/hooks/sources/useAddSource.tsx index c04c1bb..cdd66d6 100644 --- a/src/hooks/sources/useAddSource.tsx +++ b/src/hooks/sources/useAddSource.tsx @@ -31,7 +31,6 @@ export function useAddSource(): CallbackHook< if (!updatedSetup) return; const res = await putProduction(updatedSetup._id.toString(), updatedSetup); - console.log('res', res); return res; }; diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index 95601dc..a31a09e 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { SourceReference, SourceWithId } from '../interfaces/Source'; import { useSources } from './sources/useSources'; import { getSourceThumbnail } from '../utils/source'; - export interface ISource extends SourceWithId { label: string; input_slot: number; @@ -11,59 +10,79 @@ export interface ISource extends SourceWithId { } export function useDragableItems( sources: SourceReference[] -): [ISource[], (originId: string, destinationId: string) => void, boolean] { +): [ + (SourceReference | ISource)[], + (originId: string, destinationId: string) => void, + boolean +] { const [inventorySources, loading] = useSources(); - const [items, setItems] = useState( + const [items, setItems] = useState<(SourceReference | ISource)[]>( sources.flatMap((ref) => { const refId = ref._id ? ref._id : ''; const source = inventorySources.get(refId); if (!source) return []; return { ...source, + _id: refId, label: ref.label, input_slot: ref.input_slot, stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) + src: getSourceThumbnail(source), + ingest_source_name: source.ingest_source_name, + ingest_name: source.ingest_name, + video_stream: source.video_stream, + audio_stream: source.audio_stream, + status: source.status, + type: source.type, + tags: source.tags, + name: source.name }; }) ); - useEffect(() => { - setItems( - sources.flatMap((ref) => { - const refId = ref._id ? ref._id : ''; - const source = inventorySources.get(refId); - if (!source) return []; - return { - ...source, - label: ref.label, - input_slot: ref.input_slot, - stream_uuids: ref.stream_uuids, - src: getSourceThumbnail(source) - }; - }) - ); + const updatedItems = sources.map((ref) => { + const refId = ref._id ? ref._id : ''; + const source = inventorySources.get(refId); + if (!source) return { ...ref }; + return { + ...ref, + _id: refId, + status: source.status, + name: source.name, + type: source.type, + tags: source.tags, + ingest_name: source.ingest_name, + ingest_source_name: source.ingest_source_name, + ingest_type: source.ingest_type, + label: ref.label, + input_slot: ref.input_slot, + stream_uuids: ref.stream_uuids, + src: getSourceThumbnail(source), + video_stream: source.video_stream, + audio_stream: source.audio_stream, + lastConnected: source.lastConnected + }; + }); + setItems(updatedItems); }, [sources, inventorySources]); - const moveItem = (originId: string, destinationId: string) => { - const originSource = items.find((i) => i._id.toString() === originId); + const originSource = items.find( + (item) => (item._id ? item._id.toString() : '') === originId + ); const destinationSource = items.find( - (i) => i._id.toString() === destinationId + (item) => (item._id ? item._id.toString() : '') === destinationId ); if (!originSource || !destinationSource) return; - const originInputSlot = originSource.input_slot; - const destinationInputSlot = destinationSource.input_slot; - originSource.input_slot = destinationInputSlot; - destinationSource.input_slot = originInputSlot; - const updatedItems = [ - ...items.filter( - (i) => i._id !== originSource._id && i._id !== destinationSource._id - ), - originSource, - destinationSource - ].sort((a, b) => a.input_slot - b.input_slot); + const updatedItems = items + .map((item) => { + if (item._id === originSource._id) + return { ...item, input_slot: destinationSource.input_slot }; + if (item._id === destinationSource._id) + return { ...item, input_slot: originSource.input_slot }; + return item; + }) + .sort((a, b) => a.input_slot - b.input_slot); setItems(updatedItems); }; - return [items, moveItem, loading]; } diff --git a/src/interfaces/production.ts b/src/interfaces/production.ts index 9db620d..eb4d965 100644 --- a/src/interfaces/production.ts +++ b/src/interfaces/production.ts @@ -3,25 +3,11 @@ import { SourceReference } from './Source'; import { ControlConnection } from './controlConnections'; import { PipelineSettings } from './pipeline'; -export interface HtmlReference { - _id: string; - input_slot: number; - label: string; -} - -export interface MediaplayerReference { - _id: string; - input_slot: number; - label: string; -} - export interface Production { _id: string; isActive: boolean; name: string; sources: SourceReference[]; - html: HtmlReference[]; - mediaplayers: MediaplayerReference[]; production_settings: ProductionSettings; } From 230837f7a46f3dfb42f3cbaeba77b57a67d397e9 Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 16:00:30 +0200 Subject: [PATCH 27/41] fix: updated code, with dnd not working --- src/app/production/[id]/page.tsx | 1 + src/components/dragElement/DragItem.tsx | 1 + src/components/sourceCards/SourceCards.tsx | 10 +++++++--- .../startProduction/StartProductionButton.tsx | 5 +++++ src/hooks/pipelines.ts | 1 + src/hooks/useDragableItems.ts | 4 ++-- src/interfaces/Source.ts | 4 ++-- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 0f54b14..20c9a63 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -661,6 +661,7 @@ export default function ProductionConfiguration({ params }: PageProps) { diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index 7f7364f..ba68cd0 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -3,6 +3,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; +import { ISource } from '../../hooks/useDragableItems'; interface IDrag { id: ObjectId | string; diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 7255b70..cc1e149 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -19,13 +19,17 @@ export default function SourceCards({ const [items, moveItem] = useDragableItems(productionSetup.sources); const [selectingText, setSelectingText] = useState(false); if (!items) return null; - const sourceReferences = items.filter( - (item): item is SourceReference => item.type !== 'ingest_source' - ); const isISource = (source: SourceReference | ISource): source is ISource => { return 'src' in source; }; + const sourceReferences = items.filter( + // (item): item is SourceReference => item.type !== 'ingest_source' + (item) => + (item as SourceReference).type === 'html' || + (item as SourceReference).type === 'mediaplayer' + ); + const gridItems = items.map((source) => { const id = source._id ? source._id : ''; const isSource = isISource(source); diff --git a/src/components/startProduction/StartProductionButton.tsx b/src/components/startProduction/StartProductionButton.tsx index aca65a4..81d2e84 100644 --- a/src/components/startProduction/StartProductionButton.tsx +++ b/src/components/startProduction/StartProductionButton.tsx @@ -17,15 +17,18 @@ import { usePutProduction } from '../../hooks/productions'; import toast from 'react-hot-toast'; import { useDeleteMonitoring } from '../../hooks/monitoring'; import { useMultiviewPresets } from '../../hooks/multiviewPreset'; +import { SourceWithId } from '../../interfaces/Source'; type StartProductionButtonProps = { production: Production | undefined; + sources: Map; disabled: boolean; refreshProduction: () => void; }; export function StartProductionButton({ production, + sources, disabled, refreshProduction }: StartProductionButtonProps) { @@ -45,6 +48,8 @@ export function StartProductionButton({ const onClick = () => { if (!production) return; + console.log('sources', sources); + console.log('production', production); const hasUndefinedPipeline = production.production_settings.pipelines.some( (p) => !p.pipeline_name ); diff --git a/src/hooks/pipelines.ts b/src/hooks/pipelines.ts index f9ef473..baa52ff 100644 --- a/src/hooks/pipelines.ts +++ b/src/hooks/pipelines.ts @@ -33,6 +33,7 @@ export function usePipeline( setLoading(true); getPipeline(id) .then((pipeline) => { + console.log('pipeline', pipeline); setPipeline(pipeline); }) .catch((error) => { diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index a31a09e..a914799 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -33,7 +33,7 @@ export function useDragableItems( video_stream: source.video_stream, audio_stream: source.audio_stream, status: source.status, - type: source.type, + media_element: source.media_element, tags: source.tags, name: source.name }; @@ -49,7 +49,7 @@ export function useDragableItems( _id: refId, status: source.status, name: source.name, - type: source.type, + media_element: source.media_element, tags: source.tags, ingest_name: source.ingest_name, ingest_source_name: source.ingest_source_name, diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 2e9935b..5df0e6f 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,5 +1,5 @@ import { ObjectId, WithId } from 'mongodb'; -export type SourceType = 'camera' | 'graphics' | 'microphone'; +export type MediaElement = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { @@ -20,7 +20,7 @@ export interface Source { _id?: ObjectId | string; status: SourceStatus; name: string; - type: SourceType; + media_element: MediaElement; tags: { location: string; [key: string]: string | undefined; From 975bebbdc7890eb54a3bcfccc86b633008f577dd Mon Sep 17 00:00:00 2001 From: malmen237 Date: Thu, 12 Sep 2024 16:57:01 +0200 Subject: [PATCH 28/41] fix: orders dnd correct when starting production --- src/components/dragElement/DragItem.tsx | 1 - src/components/sourceCards/SourceCards.tsx | 9 +-------- src/hooks/useDragableItems.ts | 4 ++-- src/interfaces/Source.ts | 4 ++-- 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/components/dragElement/DragItem.tsx b/src/components/dragElement/DragItem.tsx index ba68cd0..7f7364f 100644 --- a/src/components/dragElement/DragItem.tsx +++ b/src/components/dragElement/DragItem.tsx @@ -3,7 +3,6 @@ import { useDrag, useDrop } from 'react-dnd'; import { SourceReference } from '../../interfaces/Source'; import { ObjectId } from 'mongodb'; import { Production } from '../../interfaces/production'; -import { ISource } from '../../hooks/useDragableItems'; interface IDrag { id: ObjectId | string; diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index cc1e149..9f360d7 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -23,13 +23,6 @@ export default function SourceCards({ return 'src' in source; }; - const sourceReferences = items.filter( - // (item): item is SourceReference => item.type !== 'ingest_source' - (item) => - (item as SourceReference).type === 'html' || - (item as SourceReference).type === 'mediaplayer' - ); - const gridItems = items.map((source) => { const id = source._id ? source._id : ''; const isSource = isISource(source); @@ -39,7 +32,7 @@ export default function SourceCards({ id={id} onMoveItem={moveItem} previousOrder={productionSetup.sources} - currentOrder={sourceReferences} + currentOrder={items as SourceReference[]} productionSetup={productionSetup} updateProduction={updateProduction} selectingText={selectingText} diff --git a/src/hooks/useDragableItems.ts b/src/hooks/useDragableItems.ts index a914799..a31a09e 100644 --- a/src/hooks/useDragableItems.ts +++ b/src/hooks/useDragableItems.ts @@ -33,7 +33,7 @@ export function useDragableItems( video_stream: source.video_stream, audio_stream: source.audio_stream, status: source.status, - media_element: source.media_element, + type: source.type, tags: source.tags, name: source.name }; @@ -49,7 +49,7 @@ export function useDragableItems( _id: refId, status: source.status, name: source.name, - media_element: source.media_element, + type: source.type, tags: source.tags, ingest_name: source.ingest_name, ingest_source_name: source.ingest_source_name, diff --git a/src/interfaces/Source.ts b/src/interfaces/Source.ts index 5df0e6f..2e9935b 100644 --- a/src/interfaces/Source.ts +++ b/src/interfaces/Source.ts @@ -1,5 +1,5 @@ import { ObjectId, WithId } from 'mongodb'; -export type MediaElement = 'camera' | 'graphics' | 'microphone'; +export type SourceType = 'camera' | 'graphics' | 'microphone'; export type SourceStatus = 'ready' | 'new' | 'gone' | 'purge'; export type Type = 'ingest_source' | 'html' | 'mediaplayer'; export type VideoStream = { @@ -20,7 +20,7 @@ export interface Source { _id?: ObjectId | string; status: SourceStatus; name: string; - media_element: MediaElement; + type: SourceType; tags: { location: string; [key: string]: string | undefined; From f9fc4af4a04ad489a1336f1a19ef3054d210ad7b Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 13 Sep 2024 09:13:05 +0200 Subject: [PATCH 29/41] fix: add back previous functionality and empty slot card --- src/components/sourceCards/SourceCards.tsx | 137 +++++++++++++++------ 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/src/components/sourceCards/SourceCards.tsx b/src/components/sourceCards/SourceCards.tsx index 9f360d7..c20b8c8 100644 --- a/src/components/sourceCards/SourceCards.tsx +++ b/src/components/sourceCards/SourceCards.tsx @@ -5,6 +5,7 @@ import { Production } from '../../interfaces/production'; import DragItem from '../dragElement/DragItem'; import SourceCard from '../sourceCard/SourceCard'; import { ISource, useDragableItems } from '../../hooks/useDragableItems'; +import { EmptySlotCard } from '../emptySlotCard/EmptySlotCard'; export default function SourceCards({ productionSetup, updateProduction, @@ -23,42 +24,104 @@ export default function SourceCards({ return 'src' in source; }; - const gridItems = items.map((source) => { - const id = source._id ? source._id : ''; - const isSource = isISource(source); - return ( - - {isSource ? ( - setSelectingText(isSelecting)} - type={'ingest_source'} - /> - ) : ( - setSelectingText(isSelecting)} - type={source.type} - /> - )} - - ); - }); + const gridItems: React.JSX.Element[] = []; + let tempItems = [...items]; + let firstEmptySlot = items.length + 1; + + if (!items || items.length === 0) return null; + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + if (!items.some((source) => source.input_slot === i + 1)) { + firstEmptySlot = i + 1; + break; + } + } + + for (let i = 0; i < items[items.length - 1].input_slot; i++) { + tempItems.every((source) => { + const id = source._id ? source._id : ''; + const isSource = isISource(source); + if (source.input_slot === i + 1) { + tempItems = tempItems.filter((i) => i._id !== source._id); + if (!productionSetup.isActive) { + gridItems.push( + + {isSource ? ( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) : ( + + setSelectingText(isSelecting) + } + type={source.type} + /> + )} + + ); + } else { + isSource + ? gridItems.push( + + setSelectingText(isSelecting) + } + type={'ingest_source'} + /> + ) + : gridItems.push( + + setSelectingText(isSelecting) + } + type={source.type} + /> + ); + } + return false; + } else { + if (productionSetup.isActive) { + gridItems.push( + + ); + } + return false; + } + }); + } return <>{gridItems}; } From fd63506b23ce0029f9c027a989d80db7ee7fc306 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Fri, 13 Sep 2024 11:00:33 +0200 Subject: [PATCH 30/41] fix: can remove html/mediaplayer during production --- src/app/production/[id]/page.tsx | 114 +++++++++++++++---------------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 20c9a63..87371c9 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -493,13 +493,7 @@ export default function ProductionConfiguration({ params }: PageProps) { }; const handleRemoveSource = async () => { - if ( - productionSetup && - productionSetup.isActive && - selectedSourceRef && - // Gör det här att sourcen inte tas bort ordentligt? - selectedSourceRef.stream_uuids - ) { + if (productionSetup && productionSetup.isActive && selectedSourceRef) { const multiviews = productionSetup.production_settings.pipelines[0].multiviews; @@ -511,9 +505,60 @@ export default function ProductionConfiguration({ params }: PageProps) { ) ); - if (!viewToUpdate) { - if (!productionSetup.production_settings.pipelines[0].pipeline_id) + if (selectedSourceRef.stream_uuids) { + if (!viewToUpdate) { + if (!productionSetup.production_settings.pipelines[0].pipeline_id) + return; + + const result = await deleteStream( + selectedSourceRef.stream_uuids, + productionSetup, + selectedSourceRef.input_slot + ); + + if (!result.ok) { + if (!result.value) { + setDeleteSourceStatus({ + success: false, + steps: [{ step: 'unexpected', success: false }] + }); + } else { + setDeleteSourceStatus({ success: false, steps: result.value }); + const didDeleteStream = result.value.some( + (step) => step.step === 'delete_stream' && step.success + ); + if (didDeleteStream) { + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + if (!updatedSetup) return; + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then( + () => { + setSelectedSourceRef(undefined); + } + ); + return; + } + } + return; + } + + const updatedSetup = removeSetupItem( + selectedSourceRef, + productionSetup + ); + + if (!updatedSetup) return; + + setProductionSetup(updatedSetup); + putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { + setRemoveSourceModal(false); + setSelectedSourceRef(undefined); + }); return; + } const result = await deleteStream( selectedSourceRef.stream_uuids, @@ -539,61 +584,12 @@ export default function ProductionConfiguration({ params }: PageProps) { ); if (!updatedSetup) return; setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then( - () => { - setSelectedSourceRef(undefined); - } - ); + putProduction(updatedSetup._id.toString(), updatedSetup); return; } } return; } - - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - - if (!updatedSetup) return; - - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup).then(() => { - setRemoveSourceModal(false); - setSelectedSourceRef(undefined); - }); - return; - } - - const result = await deleteStream( - selectedSourceRef.stream_uuids, - productionSetup, - selectedSourceRef.input_slot - ); - - if (!result.ok) { - if (!result.value) { - setDeleteSourceStatus({ - success: false, - steps: [{ step: 'unexpected', success: false }] - }); - } else { - setDeleteSourceStatus({ success: false, steps: result.value }); - const didDeleteStream = result.value.some( - (step) => step.step === 'delete_stream' && step.success - ); - if (didDeleteStream) { - const updatedSetup = removeSetupItem( - selectedSourceRef, - productionSetup - ); - if (!updatedSetup) return; - setProductionSetup(updatedSetup); - putProduction(updatedSetup._id.toString(), updatedSetup); - return; - } - } - return; } const updatedSetup = removeSetupItem(selectedSourceRef, productionSetup); if (!updatedSetup) return; From ab1c93a0ec40170b195b92db57dbb7386dd05ec9 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 17 Sep 2024 11:54:54 +0200 Subject: [PATCH 31/41] fix: use correct input slot --- src/api/ateliereLive/websocket.ts | 7 ++-- src/api/manager/workflow.ts | 61 ++++++++++++++++++------------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index df8b47a..18dda0a 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -38,8 +38,9 @@ export async function createControlPanelWebSocket() { closeMediaplayer: (input: number) => { ws.send(`media close ${input}`); }, - close: () => { - ws.close(); - } + close: () => + setTimeout(() => { + ws.close(); + }, 1000) }; } diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 89808ac..b74bf4b 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -1,4 +1,4 @@ -import { SourceWithId } from './../../interfaces/Source'; +import { SourceReference, SourceWithId } from './../../interfaces/Source'; import { Production, ProductionSettings, @@ -71,15 +71,18 @@ const isUsed = (pipeline: ResourcesPipelineResponse) => { }; async function connectIngestSources( + productionSources: SourceReference[], productionSettings: ProductionSettings, sources: SourceWithId[], usedPorts: Set ) { - let input_slot = 0; const sourceToPipelineStreams: SourceToPipelineStream[] = []; + let input_slot = 0; for (const source of sources) { - input_slot = input_slot + 1; + input_slot = + productionSources.find((s) => s._id === source._id.toString()) + ?.input_slot || 5; const ingestUuid = await getUuidFromIngestName( source.ingest_name, false @@ -143,7 +146,7 @@ async function connectIngestSources( }; try { Log().info( - `Connecting '${source.ingest_name}/${ingestUuid}}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` + `Connecting '${source.ingest_name}/${ingestUuid}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` ); Log().debug(stream); const result = await connectIngestToPipeline(stream).catch((error) => { @@ -327,10 +330,15 @@ export async function stopProduction( } } - htmlSources.map((source) => controlPanelWS.closeHtml(source.input_slot)); - mediaPlayerSources.map((source) => - controlPanelWS.closeMediaplayer(source.input_slot) - ); + for (const source of htmlSources) { + controlPanelWS.closeHtml(source.input_slot); + } + + for (const source of mediaPlayerSources) { + controlPanelWS.closeMediaplayer(source.input_slot); + } + + controlPanelWS.close(); for (const id of pipelineIds) { Log().info(`Stopping pipeline '${id}'`); @@ -465,23 +473,6 @@ export async function startProduction( // Try to setup streams from ingest(s) to pipeline(s) start try { // Get sources from the DB - // Skapa en createHtmlWebSocket, spara - const controlPanelWS = await createControlPanelWebSocket(); - const htmlSources = production.sources.filter( - (source) => source.type === 'html' - ); - const mediaPlayerSources = production.sources.filter( - (source) => source.type === 'mediaplayer' - ); - htmlSources.map((source) => controlPanelWS.createHtml(source.input_slot)); - mediaPlayerSources.map((source) => - controlPanelWS.createMediaplayer(source.input_slot) - ); - - controlPanelWS.close(); - - // Nedan behöver göras efter att vi har skapat en produktion - // TODO: Hämta production.sources, för varje html-reference --> create i createHtmlWebSocket, för varje mediaplayer i production.sources skapa en createWebSocket const sources = await getSourcesByIds( production.sources .filter( @@ -575,8 +566,8 @@ export async function startProduction( return pipeline.uuid; }) ); - streams = await connectIngestSources( + production.sources, production_settings, sources, usedPorts @@ -649,6 +640,24 @@ export async function startProduction( }; } // Try to connect control panels and pipeline-to-pipeline connections end + const controlPanelWS = await createControlPanelWebSocket(); + const htmlSources = production.sources.filter( + (source) => source.type === 'html' + ); + const mediaPlayerSources = production.sources.filter( + (source) => source.type === 'mediaplayer' + ); + + for (const source of htmlSources) { + controlPanelWS.createHtml(source.input_slot); + } + + for (const source of mediaPlayerSources) { + controlPanelWS.createMediaplayer(source.input_slot); + } + + controlPanelWS.close(); + // Try to setup pipeline outputs start try { for (const pipeline of production_settings.pipelines) { From 4575dbc8881c05a7d1244fe3ffa7d0068eb5295c Mon Sep 17 00:00:00 2001 From: Saelmala Date: Tue, 17 Sep 2024 12:00:41 +0200 Subject: [PATCH 32/41] fix: clean up input_slot definition --- src/api/manager/workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index b74bf4b..8627fdd 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -82,7 +82,7 @@ async function connectIngestSources( for (const source of sources) { input_slot = productionSources.find((s) => s._id === source._id.toString()) - ?.input_slot || 5; + ?.input_slot || input_slot + 1; const ingestUuid = await getUuidFromIngestName( source.ingest_name, false From a093a3bfaaa554b753b83e8113821611e765a36e Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 18 Sep 2024 16:29:28 +0200 Subject: [PATCH 33/41] fix: bug-fix black screen multiview --- src/api/manager/workflow.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/api/manager/workflow.ts b/src/api/manager/workflow.ts index 8627fdd..7b9e392 100644 --- a/src/api/manager/workflow.ts +++ b/src/api/manager/workflow.ts @@ -95,6 +95,7 @@ async function connectIngestSources( source.ingest_source_name, false ); + const audioSettings = await getAudioMapping(new ObjectId(source._id)); const newAudioMapping = audioSettings?.audio_stream?.audio_mapping; const audioMapping = newAudioMapping?.length ? newAudioMapping : [[0, 1]]; @@ -114,6 +115,7 @@ async function connectIngestSources( Log().info( `Allocated port ${availablePort} on '${source.ingest_name}' for ${source.ingest_source_name}` ); + const stream: PipelineStreamSettings = { pipeline_id: pipeline.pipeline_id!, alignment_ms: pipeline.alignment_ms, @@ -144,6 +146,7 @@ async function connectIngestSources( } ] }; + try { Log().info( `Connecting '${source.ingest_name}/${ingestUuid}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'` @@ -156,6 +159,7 @@ async function connectIngestSources( ); throw `Source '${source.ingest_name}/${ingestUuid}:${source.ingest_source_name}' failed to connect to '${pipeline.pipeline_name}/${pipeline.pipeline_id}': ${error.message}`; }); + usedPorts.add(availablePort); sourceToPipelineStreams.push({ source_id: source._id.toString(), @@ -322,14 +326,6 @@ export async function stopProduction( (source) => source.type === 'mediaplayer' ); - for (const source of production.sources) { - for (const stream_uuid of source.stream_uuids || []) { - await deleteStreamByUuid(stream_uuid).catch((error) => { - Log().error('Failed to delete stream! \nError: ', error); - }); - } - } - for (const source of htmlSources) { controlPanelWS.closeHtml(source.input_slot); } @@ -340,6 +336,14 @@ export async function stopProduction( controlPanelWS.close(); + for (const source of production.sources) { + for (const stream_uuid of source.stream_uuids || []) { + await deleteStreamByUuid(stream_uuid).catch((error) => { + Log().error('Failed to delete stream! \nError: ', error); + }); + } + } + for (const id of pipelineIds) { Log().info(`Stopping pipeline '${id}'`); if (!id) continue; @@ -379,6 +383,7 @@ export async function stopProduction( }; } } + try { await removePipelineStreams(id).catch((error) => { Log().error( @@ -433,7 +438,13 @@ export async function stopProduction( } } Log().info(`Pipeline '${id}' stopped`); + + const pipelines = await getPipelines(); + const pipelineFeedbackStreams = pipelines.find( + (p) => p.uuid === id + )?.feedback_streams; } + if ( !disconnectConnectionsStatus.ok || !removePipelineStreamsStatus.ok || @@ -477,7 +488,8 @@ export async function startProduction( production.sources .filter( (source) => - source._id !== undefined && source.type === 'ingest_source' + (source._id !== undefined && source.type !== 'html') || + source.type !== 'mediaplayer' ) .map((source) => { return source._id!.toString(); @@ -695,7 +707,6 @@ export async function startProduction( error: e }; } // Try to setup pipeline outputs end - // Try to setup multiviews start try { if (!production.production_settings.pipelines[0].multiviews) { From 2ece8ea546f5edce915919fb6ddc3353d65ca1b7 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Wed, 18 Sep 2024 16:30:40 +0200 Subject: [PATCH 34/41] fix: move websocket reset and play/load Instead load html and play media player while running using netcat --- src/api/ateliereLive/websocket.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/api/ateliereLive/websocket.ts b/src/api/ateliereLive/websocket.ts index 18dda0a..0cbe35d 100644 --- a/src/api/ateliereLive/websocket.ts +++ b/src/api/ateliereLive/websocket.ts @@ -19,24 +19,18 @@ export async function createControlPanelWebSocket() { const ws = await createWebSocket(); return { createHtml: (input: number) => { - ws.send('html reset'); ws.send(`html create ${input} 1920 1080`); - setTimeout(() => { - ws.send( - `html load ${input} ${process.env.NEXTAUTH_URL}/html_input?input=${input}` - ); - }, 1000); }, createMediaplayer: (input: number) => { - ws.send('media reset'); ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`); - ws.send(`media play ${input}`); }, closeHtml: (input: number) => { ws.send(`html close ${input}`); + ws.send('html reset'); }, closeMediaplayer: (input: number) => { ws.send(`media close ${input}`); + ws.send('media reset'); }, close: () => setTimeout(() => { From 015fcc5debd9c7c7308fa404b1ea90dcc462a1e7 Mon Sep 17 00:00:00 2001 From: Saelmala Date: Thu, 19 Sep 2024 09:25:06 +0200 Subject: [PATCH 35/41] fix: disable select if no selected config --- src/app/production/[id]/page.tsx | 4 ++++ src/components/filter/SortSelect.tsx | 21 --------------------- src/components/select/Select.tsx | 13 ++++++++----- 3 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 src/components/filter/SortSelect.tsx diff --git a/src/app/production/[id]/page.tsx b/src/app/production/[id]/page.tsx index 87371c9..4fc3cd0 100644 --- a/src/app/production/[id]/page.tsx +++ b/src/app/production/[id]/page.tsx @@ -768,6 +768,10 @@ export default function ProductionConfiguration({ params }: PageProps) { />
    - {options.map((value) => ( - - ))} - - ); -}; diff --git a/src/components/select/Select.tsx b/src/components/select/Select.tsx index 3d4fb7b..0871ed2 100644 --- a/src/components/select/Select.tsx +++ b/src/components/select/Select.tsx @@ -1,18 +1,21 @@ -type SortSelectProps = { +type SelectProps = { value: string; - onChange: (e: React.ChangeEvent) => void; options: readonly string[]; classNames?: string; + disabled?: boolean; + onChange: (e: React.ChangeEvent) => void; }; export const Select = ({ value, - onChange, options, - classNames -}: SortSelectProps) => { + classNames, + disabled, + onChange +}: SelectProps) => { return (