openvidu/openvidu-dashboard/dashboard-ce/typings/contract/schemas.ts

561 lines
14 KiB
TypeScript

import {
BrowserFamily,
ClientSdk,
ClosedBy,
Collection,
ConnectionType,
DisconnectReason,
EgressOutputKind,
EgressSourceType,
EgressStatus,
EgressStreamStatus,
EgressType,
EncryptionType,
IngressInputType,
IngressSessionStatus,
IngressStatus,
MigrationState,
OsFamily,
ParticipantKind,
ParticipantKindDetail,
ParticipantStatus,
RoomEndReason,
RoomStatus,
StatsDetail,
StatsDirection,
StoredEventType,
SubscriptionStatus,
TrackSource,
TrackStatus,
TrackType,
VideoQuality
} from '../src/index.js';
import {
anyObject,
arr,
bool,
type BsonSchema,
counts,
date,
double,
int,
long,
nullable,
obj,
oneOfValues,
patternCounts,
req,
str
} from './schema-builder.js';
const SCOPE_PATTERN = '^(global|node:[^:]+)$';
const ROLLUP_ID_PATTERN = '^(global|node:[^:]+):[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:00Z$';
const API_METHOD_PATTERN = '^[A-Za-z0-9]+_[A-Za-z0-9]+$';
const entityBase = {
schemaVersion: req(int('SCHEMA_VERSION of the writer that last wrote the document')),
expiresAt: req(nullable(date('closedAt + retention once closed; null while open')))
};
const internal = anyObject('Writer bookkeeping; never read by the dashboard');
const closedBy = oneOfValues(ClosedBy);
const bytes = obj({ up: req(long()), down: req(long()) });
const quality = obj({
scoreSum: req(double()),
scoreCount: req(int()),
minScore: double(),
poor: req(int('Samples with score below 2.06')),
lost: req(int('Samples with score below 1.25'))
});
const layerStats = obj({ layer: req(int()), bytes: req(long()), packets: req(int()), frames: req(int()) });
const finalStatsFields = {
bytes: req(long()),
packets: req(int()),
packetsLost: req(int()),
packetLossPct: req(double()),
nacks: req(int()),
plis: req(int()),
firs: req(int()),
rttMaxMs: req(int()),
jitterMaxMs: req(double()),
durationMs: req(long())
};
const rooms = obj({
_id: req(str('Room SID')),
name: req(str()),
nameLower: req(str()),
status: req(oneOfValues(RoomStatus)),
createdAt: req(date()),
endedAt: date(),
durationMs: long(),
endReason: oneOfValues(RoomEndReason),
closedBy,
nodeId: req(str()),
config: obj({
maxParticipants: int(),
emptyTimeoutMs: long(),
departureTimeoutMs: long(),
metadata: str('Only with store_metadata')
}),
participants: req(
obj({
sessions: req(int()),
unique: req(int()),
active: req(int('Excludes dependent kinds (EGRESS, AGENT)')),
peakActive: req(int('Excludes dependent kinds (EGRESS, AGENT)')),
aborted: req(int()),
byKind: req(counts(ParticipantKind))
})
),
tracks: req(obj({ published: req(int()), bySource: req(counts(TrackSource)) })),
egress: req(obj({ total: req(int()), active: req(int()), byType: req(counts(EgressType)) })),
ingress: req(obj({ sessions: req(int()), active: req(int()) })),
bytes: req(bytes),
quality: req(quality),
features: req(obj({ egress: req(bool()), ingress: req(bool()), agent: req(bool()), sip: req(bool()) })),
lastEventAt: req(date()),
lastStatsAt: date(),
internal,
...entityBase
});
const participants = obj({
_id: req(str('Participant SID')),
identity: req(str()),
identityLower: req(str()),
name: str(),
roomSid: req(str()),
roomName: req(str()),
kind: req(oneOfValues(ParticipantKind)),
kindDetails: arr(oneOfValues(ParticipantKindDetail)),
status: req(oneOfValues(ParticipantStatus)),
joinedAt: req(date()),
activeAt: date(),
leftAt: date(),
durationMs: long(),
disconnectReason: oneOfValues(DisconnectReason),
closedBy,
connectTimeMs: int(),
connectionType: oneOfValues(Object.values(ConnectionType).filter((type) => type !== ConnectionType.OTHER)),
region: str(),
nodeId: req(str()),
resumes: req(int()),
client: obj({
sdk: oneOfValues(ClientSdk),
version: str(),
os: str(),
osVersion: str(),
browser: str(),
browserVersion: str(),
deviceModel: str(),
network: str(),
address: str('Absent with anonymize_ip')
}),
permission: obj({
canSubscribe: bool(),
canPublish: bool(),
canPublishData: bool(),
canPublishSources: arr(oneOfValues(TrackSource)),
hidden: bool(),
canUpdateMetadata: bool(),
canSubscribeMetrics: bool()
}),
isPublisher: req(bool()),
metadata: str('Only with store_metadata'),
attributes: obj({}, { additionalProperties: str(), description: 'Only with store_metadata' }),
tracks: req(
obj({
published: req(int()),
bySource: req(counts(TrackSource)),
subscribed: req(int()),
subscribeFailures: req(int())
})
),
bytes: req(bytes),
quality: req(
obj({
scoreSum: req(double()),
scoreCount: req(int()),
minScore: double(),
poor: req(int()),
lost: req(int()),
lastScore: double(),
lastScoreAt: date()
})
),
lastEventAt: req(date()),
lastStatsAt: date(),
internal,
...entityBase
});
const tracks = obj({
_id: req(str('Track SID')),
roomSid: req(str()),
participantSid: req(str()),
identity: str(),
type: req(oneOfValues(TrackType)),
source: req(oneOfValues(TrackSource)),
mimeType: str(),
name: str(),
status: req(oneOfValues(TrackStatus)),
publishedAt: req(date()),
unpublishedAt: date(),
durationMs: long(),
closedBy,
muted: req(bool()),
width: int(),
height: int(),
simulcast: bool(),
codecs: arr(
obj({
mimeType: req(str()),
layers: arr(obj({ quality: req(oneOfValues(VideoQuality)), width: int(), height: int(), bitrate: int() }))
})
),
encryption: oneOfValues(EncryptionType),
subscribers: req(obj({ current: req(int()), total: req(int()), failed: req(int()) })),
publishStats: obj({ ...finalStatsFields, layers: req(arr(layerStats)) }),
internal,
...entityBase
});
const trackSubscriptions = obj({
_id: req(str('<trackSid>:<subscriberSid>')),
trackSid: req(str()),
roomSid: req(str()),
subscriberSid: req(str()),
subscriberIdentity: str(),
publisherSid: str(),
publisherIdentity: str(),
source: oneOfValues(TrackSource),
status: req(oneOfValues(SubscriptionStatus)),
error: str(),
subscribedAt: date(),
unsubscribedAt: date(),
durationMs: long(),
closedBy,
subscribeStats: obj(finalStatsFields),
...entityBase
});
const egresses = obj({
_id: req(str('Egress id')),
roomSid: str('Absent for web egress'),
roomName: str(),
roomNameLower: str('Lower-cased roomName, for the room name prefix search'),
type: req(oneOfValues(EgressType)),
sourceType: req(oneOfValues(EgressSourceType)),
status: req(oneOfValues(EgressStatus)),
closedBy,
startedAt: req(date()),
updatedAt: req(date()),
endedAt: date(),
durationMs: long(),
error: str(),
errorCode: int(),
request: req(
obj(
{
layout: str(),
customBaseUrl: str(),
url: str('Redacted'),
audioOnly: bool(),
videoOnly: bool(),
participantIdentity: str(),
screenShare: bool(),
trackId: str(),
audioTrackId: str(),
videoTrackId: str(),
preset: str(),
advanced: anyObject('Encoding options without credentials'),
outputs: arr(oneOfValues(EgressOutputKind))
},
{ description: 'Sanitized request: no credentials, no stream keys' }
)
),
outputs: req(
arr(
obj({
kind: req(oneOfValues(EgressOutputKind)),
filename: str(),
url: str('Redacted stream URL'),
playlistName: str(),
filenamePrefix: str(),
location: str(),
size: long(),
durationMs: long(),
status: oneOfValues(EgressStreamStatus),
error: str(),
startedAt: date(),
endedAt: date()
})
)
),
statusHistory: req(arr(obj({ status: req(oneOfValues(EgressStatus)), at: req(date()) }), { maxItems: 50 })),
retryCount: req(int()),
backupStorageUsed: req(bool()),
...entityBase
});
const ingresses = obj({
_id: req(str('Ingress id')),
name: str(),
nameLower: str(),
inputType: req(oneOfValues(IngressInputType)),
url: str('Without stream key or credentials'),
roomName: req(str()),
participantIdentity: req(str()),
participantName: str(),
participantMetadata: str('Only with store_metadata'),
reusable: req(bool()),
enabled: req(bool()),
enableTranscoding: bool(),
createdAt: req(date()),
deletedAt: date(),
status: req(oneOfValues(IngressStatus)),
lastError: str(),
sessions: req(int()),
totalDurationMs: req(long()),
lastSessionAt: date(),
internal,
...entityBase
});
const ingressSessions = obj({
_id: req(str('ingress.state.resource_id')),
ingressId: req(str()),
ingressName: str(),
inputType: req(oneOfValues(IngressInputType)),
roomSid: str(),
roomName: str(),
participantIdentity: str(),
status: req(oneOfValues(IngressSessionStatus)),
error: str(),
closedBy,
startedAt: req(date()),
endedAt: date(),
durationMs: long(),
video: obj({ mimeType: str(), averageBitrate: int(), width: int(), height: int(), framerate: double() }),
audio: obj({ mimeType: str(), averageBitrate: int(), channels: int(), sampleRate: int() }),
...entityBase
});
const events = obj({
_id: req(str('LiveKit event id (AE_…); AE_SYN_… for synthetic events')),
type: req(oneOfValues(StoredEventType)),
ts: req(date()),
roomSid: str(),
roomName: str(),
participantSid: str(),
identity: str(),
trackSid: str(),
egressId: str(),
ingressId: str(),
ingressSessionId: str(),
nodeId: req(str()),
synthetic: req(bool()),
payload: req(anyObject('Protocol event in protojson naming without lifted keys or secrets'))
});
const trackStats = obj({
_id: { bsonType: 'objectId' },
ts: req(date()),
meta: req(
obj({
roomSid: req(str()),
participantSid: req(str()),
trackSid: str('Track stats detail only'),
kind: req(oneOfValues(StatsDirection)),
nodeId: req(str())
})
),
score: req(double()),
minScore: req(double()),
medianScore: req(double()),
bytes: req(long()),
packets: req(int()),
packetsLost: req(int()),
rttMs: req(int()),
jitterMs: req(double()),
nacks: req(int()),
plis: req(int()),
firs: req(int()),
frames: req(int()),
layers: req(arr(layerStats)),
mimeType: str('Track stats detail only')
});
const rollupBase = {
_id: req({ bsonType: 'string', pattern: ROLLUP_ID_PATTERN }),
scope: req({ bsonType: 'string', pattern: SCOPE_PATTERN }),
bucket: req(date('Start of the UTC bucket'))
};
const rollupBytes = obj({ up: long(), down: long() });
const rollupQuality = obj({ scoreSum: double(), scoreCount: int(), poor: int(), lost: int() });
const apiCalls = obj({ total: int(), byMethod: patternCounts(API_METHOD_PATTERN) });
const hourlyFields = {
...rollupBase,
rooms: obj({
created: int(),
ended: int(),
endedByReason: counts(RoomEndReason),
durationMsSum: long(),
participantsSum: int()
}),
participants: obj({
joined: int(),
active: int('PARTICIPANT_ACTIVE events'),
aborted: int(),
left: int(),
byKind: counts(ParticipantKind),
bySdk: counts(ClientSdk),
byOs: counts(OsFamily),
byBrowser: counts(BrowserFamily),
byConnectionType: counts(ConnectionType),
disconnectReasons: counts(DisconnectReason),
connectTimeSumMs: long(),
connectTimeCount: int(),
minutes: double()
}),
tracks: obj({ published: int(), subscribed: int(), subscribeFailed: int() }),
egress: obj({
started: int(),
byType: counts(EgressType),
endedByStatus: counts(EgressStatus),
durationMs: long()
}),
ingress: obj({
sessionsStarted: int(),
sessionsEnded: int(),
byInputType: counts(IngressInputType, 'Sessions started by input type'),
durationMs: long()
}),
bytes: rollupBytes,
quality: rollupQuality,
apiCalls,
apiErrors: apiCalls
};
const analyticsHourly = obj(hourlyFields);
const analyticsDaily = obj({
...hourlyFields,
uniqueIdentities: int('Set by the server reconciler once the UTC day has closed')
});
const analytics5min = obj({
...rollupBase,
rooms: obj({ created: int(), ended: int() }),
participants: obj({
connected: int('Distinct participants with stats in the bucket'),
joined: int(),
active: int('PARTICIPANT_ACTIVE events'),
left: int(),
aborted: int()
}),
egress: obj({ started: int(), ended: int() }),
bytes: rollupBytes,
quality: rollupQuality,
internal
});
const nodes = obj({
_id: req(str('Node id')),
firstSeenAt: req(date()),
lastSeenAt: req(date()),
serverVersion: req(str()),
region: str(),
batchIntervalMs: { ...int('Configured batch interval of the node, in milliseconds'), minimum: 1000 }
});
const metaId = (id: string): BsonSchema => ({ bsonType: 'string', enum: [id] });
const reconcilerCounts = {
room: req(int()),
participant: req(int()),
egress: req(int()),
ingressSession: req(int())
};
const schemaMeta: BsonSchema = {
bsonType: 'object',
required: ['_id'],
oneOf: [
obj({ _id: req(metaId('schema')), version: req(int()), writerVersion: req(str()), updatedAt: req(date()) }),
obj({
_id: req(metaId('retention')),
eventsMs: req(long()),
statsMs: req(long()),
tracksMs: req(long()),
entitiesMs: req(long()),
rollups5MinMs: req(long()),
rollupsHourlyMs: req(long()),
rollupsDailyMs: req(nullable(long('null keeps documents forever'))),
updatedAt: req(date())
}),
obj({
_id: req(metaId('capabilities')),
timeseries: req(bool()),
transactions: req(bool()),
serverVersion: req(str()),
detectedAt: req(date())
}),
obj({ _id: req(metaId('config')), statsDetail: req(oneOfValues(StatsDetail)), recordedAt: req(date()) }),
obj({
_id: req(metaId('migration')),
state: req(oneOfValues(MigrationState)),
checkpoint: anyObject(),
processed: req(long()),
total: req(long()),
startedAt: date(),
finishedAt: date(),
error: str(),
writerVersion: req(str()),
fromSchemaVersion: req(int()),
counts: req(obj({ events: req(long()), stats: req(long()), synthetic: req(long()), skipped: req(long()) })),
nodeId: str(),
legacyRetainedUntil: date()
}),
obj({
_id: req(metaId('reconciler')),
lastRunAt: req(date()),
lastRunDurationMs: req(long()),
nodeId: req(str()),
candidates: req(obj(reconcilerCounts)),
closed: req(obj({ ...reconcilerCounts, track: req(int()), subscription: req(int()) })),
partial: req(bool()),
lastError: str()
}),
obj({ _id: req(metaId('dashboard')) }, { additionalProperties: true })
]
};
/** `$jsonSchema` validator of every collection. */
export const COLLECTION_SCHEMAS: Readonly<Record<Collection, BsonSchema>> = {
[Collection.ROOMS]: rooms,
[Collection.PARTICIPANTS]: participants,
[Collection.TRACKS]: tracks,
[Collection.TRACK_SUBSCRIPTIONS]: trackSubscriptions,
[Collection.EGRESSES]: egresses,
[Collection.INGRESSES]: ingresses,
[Collection.INGRESS_SESSIONS]: ingressSessions,
[Collection.EVENTS]: events,
[Collection.TRACK_STATS]: trackStats,
[Collection.ANALYTICS_5MIN]: analytics5min,
[Collection.ANALYTICS_HOURLY]: analyticsHourly,
[Collection.ANALYTICS_DAILY]: analyticsDaily,
[Collection.NODES]: nodes,
[Collection.SCHEMA_META]: schemaMeta
};