Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support media, html sources and websocket #20

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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==${}
birme marked this conversation as resolved.
Show resolved Hide resolved
# 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}

Expand All @@ -14,3 +15,6 @@ BCRYPT_SALT_ROUNDS=${BCRYPT_SALT_ROUNDS:-10}

# i18n
UI_LANG=${UI_LANG:-en}

# Mediaplayer - path on the system controller
MEDIAPLAYER_PLACEHOLDER=/media/media_placeholder.mp4
346 changes: 200 additions & 146 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -32,23 +32,25 @@
"@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",
"dotenv": "^16.0.3",
"fastify": "^4.12.0",
"fastify": "^4.28.1",
"lodash.get": "^4.4.2",
"mongodb": "^5.8.0",
"next": "^13.5.6",
"next-auth": "^4.24.5",
"nodemon": "^2.0.20",
"nodemon": "^3.1.7",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"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",
Expand Down
4 changes: 3 additions & 1 deletion src/api/ateliereLive/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export async function getSourceThumbnail(
process.env.LIVE_URL
),
{
next: { tags: ['image'] },
method: 'POST',
body: JSON.stringify({
encoder: 'auto',
Expand All @@ -114,7 +115,8 @@ export async function getSourceThumbnail(
width
}),
headers: {
authorization: getAuthorizationHeader()
authorization: getAuthorizationHeader(),
cache: 'no-store'
}
}
);
Expand Down
7 changes: 3 additions & 4 deletions src/api/ateliereLive/pipelines/multiviews/multiviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 20 additions & 14 deletions src/api/ateliereLive/pipelines/streams/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export async function createStream(
return pipeline.uuid;
})
);

const ingestUuid = await getUuidFromIngestName(
source.ingest_name,
false
Expand All @@ -79,13 +80,15 @@ export async function createStream(
source.ingest_source_name,
false
);

const audioMapping =
source.audio_stream.audio_mapping &&
source.audio_stream.audio_mapping.length > 0
? source.audio_stream.audio_mapping
: [[0, 1]];

await initDedicatedPorts();

for (const pipeline of production_settings.pipelines) {
const availablePorts = getAvailablePortsForIngest(
source.ingest_name,
Expand All @@ -101,28 +104,29 @@ export async function createStream(
Log().info(
`Allocated port ${availablePort} on '${source.ingest_name}' for ${source.ingest_source_name}`
);

const stream: PipelineStreamSettings = {
ingest_id: ingestUuid,
source_id: sourceId,
pipeline_id: pipeline.pipeline_id!,
input_slot: input_slot,
alignment_ms: pipeline.alignment_ms,
audio_format: pipeline.audio_format,
audio_sampling_frequency: pipeline.audio_sampling_frequency,
bit_depth: pipeline.bit_depth,
convert_color_range: pipeline.convert_color_range,
encoder: pipeline.encoder,
encoder_device: pipeline.encoder_device,
format: pipeline.format,
max_network_latency_ms: pipeline.max_network_latency_ms,
width: pipeline.width,
height: pipeline.height,
frame_rate_d: pipeline.frame_rate_d,
frame_rate_n: pipeline.frame_rate_n,
format: pipeline.format,
encoder: pipeline.encoder,
encoder_device: pipeline.encoder_device,
gop_length: pipeline.gop_length,
height: pipeline.height,
max_network_latency_ms: pipeline.max_network_latency_ms,
pic_mode: pipeline.pic_mode,
speed_quality_balance: pipeline.speed_quality_balance,
video_kilobit_rate: pipeline.video_kilobit_rate,
width: pipeline.width,
ingest_id: ingestUuid,
source_id: sourceId,
input_slot,
bit_depth: pipeline.bit_depth,
speed_quality_balance: pipeline.speed_quality_balance,
convert_color_range: pipeline.convert_color_range,
audio_sampling_frequency: pipeline.audio_sampling_frequency,
audio_format: pipeline.audio_format,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid moving around the order of lines of code, is there a difference here? Can we revert this to old order (which I guess didn't make sense, but new order doesn't make it more clear either?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to move it around so that it matched the order in the PipelineStreamSettings type declaration, but I can change it back

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

audio_mapping: JSON.stringify(audioMapping),
interfaces: [
{
Expand All @@ -131,6 +135,7 @@ export async function createStream(
}
]
};

try {
Log().info(
`Connecting '${source.ingest_name}/${ingestUuid}}:${source.ingest_source_name}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}'`
Expand All @@ -147,6 +152,7 @@ export async function createStream(
Log().info(
`Stream '${result.stream_uuid}' from '${source.ingest_name}/${ingestUuid}' to '${pipeline.pipeline_name}/${pipeline.pipeline_id}' connected`
);

sourceToPipelineStreams.push({
source_id: source._id.toString(),
stream_uuid: result.stream_uuid,
Expand Down
40 changes: 40 additions & 0 deletions src/api/ateliereLive/websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import WebSocket from 'ws';

function createWebSocket(): Promise<WebSocket> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://${process.env.CONTROL_PANEL_WS}`);
ws.on('error', reject);
ws.on('open', () => {
// const send = ws.send.bind(ws);
// ws.send = (message) => {
// console.debug(`[websocket] sending message: ${message}`);
// send(message);
// };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

resolve(ws);
});
});
}

export async function createControlPanelWebSocket() {
const ws = await createWebSocket();
return {
createHtml: (input: number) => {
ws.send(`html create ${input} 1920 1080`);
},
createMediaplayer: (input: number) => {
ws.send(`media create ${input} ${process.env.MEDIAPLAYER_PLACEHOLDER}`);
},
closeHtml: (input: number) => {
ws.send(`html close ${input}`);
ws.send('html reset');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset not necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

},
closeMediaplayer: (input: number) => {
ws.send(`media close ${input}`);
ws.send('media reset');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset not necessary?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

},
close: () =>
setTimeout(() => {
ws.close();
}, 1000)
};
}
11 changes: 11 additions & 0 deletions src/api/manager/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export async function getMultiviewPresets(): Promise<MultiviewPreset[]> {
const db = await getDatabase();
return await db.collection<MultiviewPreset>('multiviews').find({}).toArray();
}

export async function getMultiviewPreset(
id: string
): Promise<WithId<MultiviewPreset>> {
Expand All @@ -26,6 +27,16 @@ export async function getMultiviewPreset(
.collection<MultiviewPreset>('multiviews')
.findOne({ _id: new ObjectId(id) })) as WithId<MultiviewPreset>;
}

export async function putMultiviewPreset(
newMultiviewPreset: MultiviewPreset
): Promise<void> {
const db = await getDatabase();
await db
.collection('multiviews')
.insertOne({ ...newMultiviewPreset, _id: new ObjectId() });
}

export async function putPreset(
id: string,
preset: PresetWithId
Expand Down
27 changes: 25 additions & 2 deletions src/api/manager/productions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,44 @@ export async function setProductionsIsActiveFalse(): Promise<
export async function putProduction(
id: string,
production: Production
): Promise<void> {
): Promise<Production> {
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
}
);

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<ObjectId> {
Expand Down
54 changes: 31 additions & 23 deletions src/api/manager/sources.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import inventory from './mocks/inventory.json';
import { Source } from '../../interfaces/Source';
import { ObjectId } from 'mongodb';
import { ObjectId, OptionalId, WithId } from 'mongodb';
import { getDatabase } from '../mongoClient/dbClient';

export function getMockedSources() {
Expand All @@ -9,37 +9,45 @@ export function getMockedSources() {

export async function postSource(data: Source): Promise<ObjectId> {
const db = await getDatabase();
return (await db.collection('inventory').insertOne(data))
.insertedId as ObjectId;
const insertData: OptionalId<Omit<Source, '_id'>> & { _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<Source>('inventory').find().toArray();
}

export async function getSourcesByIds(_ids: string[]) {
export async function getSourcesByIds(
_ids: string[]
): Promise<WithId<Source>[]> {
const db = await getDatabase().catch(() => {
throw "Can't connect to Database";
});
const objectIds = _ids.map((id: string) => {
return new ObjectId(id);
throw new Error("Can't connect to Database");
});
const objectIds = _ids.map((id: string) => new ObjectId(id));

return (
await db
.collection<Source>('inventory')
.find({
_id: {
$in: objectIds
}
})
.toArray()
).sort(
(a, b) =>
_ids.findIndex((id) => a._id.equals(id)) -
_ids.findIndex((id) => b._id.equals(id))
);
const sources = await db
.collection<Source>('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) {
Expand Down
Loading
Loading