app/vidispine/shape/VidispineShape.ts (287 lines of code) (raw):

import VidispineShapeTI from "./VidispineShape-ti"; import CustomDataTI from "../field-group/CustomData-ti"; import VidispineFileTI from "./VidispineFile-ti"; import { VidispineFileChecker } from "./VidispineFile"; import { createCheckers, VError } from "ts-interface-checker"; import { DataPair } from "../field-group/VidispineFieldGroup"; import { VidispineFile } from "./VidispineFile"; import omit from "lodash.omit"; interface TimeBase { numerator: number; denominator: number; } interface SampleBasedTime { samples: number; timeBase: TimeBase; } interface VidispineShapeMediaInfo { //these first two are only applicable to video streams Format_Settings_GOP?: string; Bit_Rate_Mode?: string; property: DataPair[]; } interface WidthHeight { width: number; height: number; } interface HorizVert { horizontal: number; vertical: number; } interface VidispineContainerComponent { id: string; duration?: SampleBasedTime; format: string; firstSMPTETimecode?: string; startTimecode?: number; startTimestamp?: SampleBasedTime; roundedTimeBase?: number; dropFrame?: boolean; timeCodeTimeBase?: TimeBase; mediaInfo?: VidispineShapeMediaInfo; file?: VidispineFile[]; metadata?: DataPair[]; } interface VidispineAudioComponent { id: string; channelCount: number; channelLayout: number; sampleFormat: string; frameSize?: number; mediaInfo?: VidispineShapeMediaInfo; file: VidispineFile[]; metadata?: DataPair[]; codec?: string; timeBase?: TimeBase; itemTrack?: string; essenceStreamId?: number; bitrate?: number; numberOfPackets?: number; extradata?: string; pid?: number; duration?: SampleBasedTime; startTimestamp?: SampleBasedTime; } interface VidispineVideoComponent { id: string; resolution: WidthHeight; pixelFormat?: string; maxBFrames?: number; pixelAspectRation?: HorizVert; fieldOrder?: string; codecTimeBase?: TimeBase; averageFrameRate?: TimeBase; realBaseFrameRate?: TimeBase; displayWidth?: TimeBase; displayHeight?: TimeBase; max_packet_size?: number; ticks_per_frame?: number; bitDepth?: number; bitsPerPixel?: number; mediaInfo?: VidispineShapeMediaInfo; file: VidispineFile[]; metadata?: DataPair[]; codec: string; timeBase?: TimeBase; itemTrack?: string; essenceStreamId?: number; bitrate?: number; numberOfPackets?: number; extradata?: string; pid?: number; duration?: SampleBasedTime; profile?: number; level?: number; startTimestamp?: SampleBasedTime; } interface VidispineBinaryComponent { file: VidispineFile[]; id: string; metadata?: DataPair[]; length?: number; } interface VidispineShapeIF { id: string; created: string; essenceVersion: number; tag: string[]; mimeType: string[]; containerComponent?: VidispineContainerComponent; audioComponent?: VidispineAudioComponent[]; videoComponent?: VidispineVideoComponent[]; binaryComponent?: VidispineBinaryComponent[]; } const { VidispineShapeIF, VidispineVideoComponent, VidispineAudioComponent, VidispineContainerComponent, VidispineBinaryComponent, } = createCheckers(VidispineShapeTI, CustomDataTI, VidispineFileTI); /** * Helper class that defines a number of useful methods for accessing the VidispineShape data * The most efficient way to get one of these, given a VidispineShapeIF object, is simply to cast it * (because VidispineShape implements VidispineShapeIF): * const shape = myshapedata as VidispineShape; * * Alternatively, you can use a constructor to validate the data on the way in: * const shape = new VidispineShape(uncheckedObject as any); */ class VidispineShape implements VidispineShapeIF { id: string; created: string; essenceVersion: number; tag: string[]; mimeType: string[]; containerComponent?: VidispineContainerComponent; audioComponent?: VidispineAudioComponent[]; videoComponent?: VidispineVideoComponent[]; binaryComponent?: VidispineBinaryComponent[]; /** * construct from an untyped object (parsed from json). * raises an exception if the data does not validate against the object spec * @param sourceObject untyped parsed json to build from * @param check if false then skip the data check (i.e. data has already been checked) */ constructor(sourceObject: any, check = true) { const containerComponent = check && sourceObject.hasOwnProperty("containerComponent") ? this.validateContainerComponent(sourceObject.containerComponent) : sourceObject.containerComponent; const audioComponents = check && sourceObject.hasOwnProperty("audioComponent") ? this.validateAudioComponentList(sourceObject.audioComponent) : sourceObject.audioComponent; const videoComponents = check && sourceObject.hasOwnProperty("videoComponent") ? this.validateVideoComponentList(sourceObject.videoComponent) : sourceObject.videoComponent; const binaryComponents = check && sourceObject.hasOwnProperty("binaryComponent") ? this.validateBinaryComponentList(sourceObject.binaryComponent) : sourceObject.binaryComponent; const everythingElse = omit(sourceObject, [ "containerComponent", "audioComponent", "videoComponent", "binaryComponent", ]); if (check) VidispineShapeIF.check(everythingElse); this.id = everythingElse.id; this.created = everythingElse.created; this.essenceVersion = everythingElse.essenceVersion; this.tag = everythingElse.tag; this.mimeType = everythingElse.mimeType; this.containerComponent = containerComponent; this.audioComponent = audioComponents; this.videoComponent = videoComponents; this.binaryComponent = binaryComponents; } validateFiles(sourceFileList: any[], parentPath: string): VidispineFile[] { if (sourceFileList === undefined || sourceFileList === null) { throw new VError(parentPath + ".file", "file list was not present"); } return <VidispineFile[]>sourceFileList.filter((f: any) => { try { VidispineFileChecker.check(f); return true; } catch (e) { const displayFileId = f.hasOwnProperty("id") ? f.id : "no-id-present"; console.warn(`File with id ${displayFileId} was not valid: ${e}`); return false; } }); } /** * try to validate the given untyped object as a ContainerComponent. * files are validated individually and a non-confirming file will be omitted from the returned object. * this can mean a valid shape with zero files * if validation fails, throws a VError * @param sourceComponent untyped object to validate */ validateContainerComponent( sourceComponent: any ): VidispineContainerComponent { if (sourceComponent === undefined || sourceComponent === null) { throw new VError("containerComponent", "no container component present"); } const files = sourceComponent.hasOwnProperty("file") ? this.validateFiles(sourceComponent.file, "containerComponent") : undefined; const everythingElse = omit(sourceComponent, "file"); VidispineContainerComponent.check(everythingElse); return <VidispineContainerComponent>( Object.assign(everythingElse, { file: files }) ); } validateAudioComponentList( sourceComponent: any[] ): VidispineAudioComponent[] { let i = 0; return sourceComponent .filter((component) => { try { //the object does not verify if the "file" key is absent so replace it with an empty one const everythingElse = Object.assign(omit(component, "file"), { file: [], }); VidispineAudioComponent.check(everythingElse); return true; } catch (err) { console.warn(`Audio component ${i} did not validate: ${err}`); return false; } }) .map((component) => { const files = component.hasOwnProperty("file") ? this.validateFiles(component.file, `audioComponent.${i}`) : undefined; i += 1; return <VidispineAudioComponent>( Object.assign(component, { file: files }) ); }); } validateVideoComponentList( sourceComponent: any[] ): VidispineVideoComponent[] { let i = 0; return sourceComponent .filter((component) => { try { //the object does not verify if the "file" key is absent so replace it with an empty one const everythingElse = Object.assign(omit(component, "file"), { file: [], }); VidispineVideoComponent.check(everythingElse); return true; } catch (err) { console.warn(`Video component ${i} did not validate: ${err}`); return false; } }) .map((component) => { const files = component.hasOwnProperty("file") ? this.validateFiles(component.file, `audioComponent.${i}`) : undefined; i += 1; return <VidispineVideoComponent>( Object.assign(component, { file: files }) ); }); } validateBinaryComponentList( sourceComponent: any[] ): VidispineBinaryComponent[] { let i = 0; return sourceComponent .filter((component) => { try { //the object does not verify if the "file" key is absent so replace it with an empty one const everythingElse = Object.assign(omit(component, "file"), { file: [], }); VidispineBinaryComponent.check(everythingElse); return true; } catch (err) { console.warn(`Binary component ${i} did not validate: ${err}`); return false; } }) .map((component) => { const files = component.hasOwnProperty("file") ? this.validateFiles(component.file, `binaryComponent.${i}`) : undefined; i += 1; return <VidispineBinaryComponent>( Object.assign(component, { file: files }) ); }); } /** * returns the first appropriate access URL for the given protocol * @param forProto the protocol you want, e.g. "file" or "http". "https" will match "http". * @returns either a URL string or undefined */ getDefaultUri(forProto: string): string | undefined { if (this.containerComponent && this.containerComponent.file) { for (let entry of this.containerComponent.file) { for (let uri of entry.uri) { if (uri.startsWith(forProto)) return uri; } } } return undefined; } } export type { VidispineShapeIF }; export { VidispineShape, VidispineShapeIF as VidispineShapeChecker };