Skip to content

Commit

Permalink
Merge pull request #281 from dfpc-coe/video-path-api
Browse files Browse the repository at this point in the history
Video Path API
  • Loading branch information
ingalls authored Aug 22, 2024
2 parents 8afacb1 + 9a71208 commit 8d27fa3
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 165 deletions.
121 changes: 104 additions & 17 deletions api/lib/control/video-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Err from '@openaddresses/batch-error';
import Config from '../config.js';
import { Type, Static } from '@sinclair/typebox';
import { VideoLeaseResponse } from '../types.js';
import fetch from '../fetch.js';

export const VideoConfig = Type.Object({
Expand All @@ -18,12 +19,15 @@ export const VideoConfig = Type.Object({
export const PathConfig = Type.Object({
name: Type.String(),
confName: Type.String(),
source: Type.Object({
id: Type.String(),
type: Type.String(),
}),
source: Type.Union([
Type.Object({
id: Type.String(),
type: Type.String(),
}),
Type.Null()
]),
ready: Type.Boolean(),
readyTime: Type.String(),
readyTime: Type.Union([Type.String(), Type.Null()]),
tracks: Type.Array(Type.String()),
bytesReceived: Type.Integer(),
bytesSent: Type.Integer(),
Expand Down Expand Up @@ -53,17 +57,18 @@ export default class VideoServiceControl {
this.config = config;
}

async configuration(): Promise<Static<typeof Configuration>> {
let video;

const headers = new Headers();
async settings(): Promise<{
configured: boolean;
url?: string;
username?: string;
password?: string;
}> {
let video, user, pass;

try {
video = await this.config.models.Setting.from('media::url');
const user = await this.config.models.Setting.from('media::username');
const pass = await this.config.models.Setting.from('media::password');

headers.append('Authorization', `Basic ${Buffer.from(user.value + ':' + pass.value).toString('base64')}`);
user = await this.config.models.Setting.from('media::username');
pass = await this.config.models.Setting.from('media::password');
} catch (err) {
if (err.message.includes('Not Found')) {
return {
Expand All @@ -74,23 +79,105 @@ export default class VideoServiceControl {
}
}

const url = new URL('/v3/config/global/get', video.value);
return {
configured: true,
url: video.value,
username: user.value,
password: pass.value,
}
}

headers(username: string, password: string): Headers {
const headers = new Headers();
headers.append('Authorization', `Basic ${Buffer.from(username + ':' + password).toString('base64')}`);
return headers;
}

async configuration(): Promise<Static<typeof Configuration>> {
const video = await this.settings();
const headers = this.headers(video.username, video.password);

if (!video.configured) return video;

const url = new URL('/v3/config/global/get', video.url);
url.port = '9997';

const res = await fetch(url, { headers })
if (!res.ok) throw new Err(500, null, await res.text())
const body = await res.typed(VideoConfig);

const urlPaths = new URL('/v3/paths/list', video.value);
const urlPaths = new URL('/v3/paths/list', video.url);
urlPaths.port = '9997';

const resPaths = await fetch(urlPaths, { headers })
if (!resPaths.ok) throw new Err(500, null, await resPaths.text())

const paths = await resPaths.typed(PathsConfig);

return {
configured: true,
url: video.value,
configured: video.configured,
url: video.url,
config: body,
paths: paths.items,
};
}

async generate(opts: {
name: string;
expiration: string;
path: string;
username: string;
}): Promise<Static<typeof VideoLeaseResponse>> {
const video = await this.settings();

if (!video.configured) throw new Err(400, null, 'Media Integration is not configured');

const headers = this.headers(video.username, video.password);

const lease = await this.config.models.VideoLease.generate({
name: opts.name,
expiration: opts.expiration,
path: opts.path,
username: opts.username
});

const url = new URL(`/v3/config/paths/add/${lease.path}`, video.url);
url.port = '9997';

headers.append('Content-Type', 'application/json');

const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
name: opts.path
}),
})

if (!res.ok) throw new Err(500, null, await res.text())

return lease;
}

async delete(leaseid: string): Promise<void> {
const video = await this.settings();

if (!video.configured) throw new Err(400, null, 'Media Integration is not configured');

const headers = this.headers(video.username, video.password);

const lease = await this.config.models.VideoLease.from(leaseid);

await this.config.models.VideoLease.delete(leaseid);

const url = new URL(`/v3/config/paths/delete/${lease.path}`, video.url);
url.port = '9997';

await fetch(url, {
method: 'DELETE',
headers,
})

return;
}
}
22 changes: 14 additions & 8 deletions api/routes/video-lease.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import { sql } from 'drizzle-orm';
import { Token } from '../lib/schema.js';
import { randomUUID } from 'node:crypto';
import { StandardResponse, VideoLeaseResponse } from '../lib/types.js';
import ECSVideoControl from '../lib/control/video-service.js';
import * as Default from '../lib/limits.js';

export default async function router(schema: Schema, config: Config) {
const videoControl = new ECSVideoControl(config);

await schema.get('/video/lease', {
name: 'List Leases',
group: 'VideoLease',
description: 'List all vide',
description: 'List all video leases',
query: Type.Object({
limit: Default.Limit,
page: Default.Page,
order: Default.Order,
sort: Type.Optional(Type.String({default: 'created', enum: Object.keys(Token) })),
sort: Type.Optional(Type.String({ default: 'created', enum: Object.keys(Token) })),
filter: Default.Filter
}),
res: Type.Object({
Expand Down Expand Up @@ -64,7 +67,7 @@ export default async function router(schema: Schema, config: Config) {
throw new Err(400, null, 'Only Administrators can request a lease > 16 hours')
}

const lease = await config.models.VideoLease.generate({
const lease = await videoControl.generate({
name: req.body.name,
expiration: moment().add(req.body.duration, 'seconds').toISOString(),
path: randomUUID(),
Expand All @@ -90,12 +93,15 @@ export default async function router(schema: Schema, config: Config) {
const user = await Auth.as_user(config, req);

if (user.access === AuthUserAccess.ADMIN) {
await config.models.VideoLease.delete(req.params.lease);
await videoControl.delete(req.params.lease);
} else {
await config.models.VideoLease.delete(sql`
username = ${user.email}
AND id = ${req.params.lease}
`);
const lease = await config.models.VideoLease.from(req.params.lease);

if (lease.username === user.email) {
await videoControl.delete(req.params.lease);
} else {
throw new Err(400, null, 'You can only delete a least you created');
}
}

return res.json({
Expand Down
Loading

0 comments on commit 8d27fa3

Please sign in to comment.