diff --git a/packages/api/src/compile-schemas.js b/packages/api/src/compile-schemas.js index 7fab22088c..74425a271e 100644 --- a/packages/api/src/compile-schemas.js +++ b/packages/api/src/compile-schemas.js @@ -72,16 +72,26 @@ const data = _.merge({}, apiData, dbData); const indexPath = path.resolve(validatorDir, "index.js"); write(indexPath, indexStr); - const typeDefinition = `export type InputCreatorId = + const creatorIdTypeDefinition = `export type InputCreatorId = | { type: "unverified"; value: string; } | string;`; + const playbackPolicyTypeDefinition = "export type PlaybackPolicy1 = null;"; + const playbackPolicy2TypeDefinition = + "export type PlaybackPolicy2 = PlaybackPolicy | PlaybackPolicy1;"; + let typeStr = types.join("\n\n"); - const cleanedTypeStr = typeStr.split(typeDefinition).join(""); - typeStr = `${cleanedTypeStr.trim()}\n\n${typeDefinition}`; + const cleanedTypeStr = typeStr + .split(creatorIdTypeDefinition) + .join("") + .split(playbackPolicyTypeDefinition) + .join("") + .split(playbackPolicy2TypeDefinition) + .join(""); + typeStr = `${cleanedTypeStr.trim()}\n\n${creatorIdTypeDefinition}\n\n${playbackPolicyTypeDefinition}\n\n${playbackPolicy2TypeDefinition}`; const typePath = path.resolve(schemaDir, "types.d.ts"); write(typePath, typeStr); diff --git a/packages/api/src/controllers/asset.ts b/packages/api/src/controllers/asset.ts index edc1b16b1f..82fb5017cd 100644 --- a/packages/api/src/controllers/asset.ts +++ b/packages/api/src/controllers/asset.ts @@ -804,88 +804,85 @@ app.post( } ); -const uploadWithUrlHandler: RequestHandler = async (req, res) => { - let { url, encryption, c2pa, profiles, targetSegmentSizeSecs } = - req.body as NewAssetFromUrlPayload; - if (!url) { - return res.status(422).json({ - errors: [`Must provide a "url" field for the asset contents`], - }); - } - if (encryption) { - if (encryption.encryptedKey) { - if (!isValidBase64(encryption.encryptedKey)) { - return res.status(422).json({ - errors: [`"encryptedKey" must be valid base64`], - }); +app.post( + "/upload/url", + authorizer({}), + validatePost("new-asset-from-url-payload"), + async (req, res) => { + let { url, encryption, c2pa, profiles, targetSegmentSizeSecs } = + req.body as NewAssetFromUrlPayload; + if (!url) { + return res.status(422).json({ + errors: [`Must provide a "url" field for the asset contents`], + }); + } + if (encryption) { + if (encryption.encryptedKey) { + if (!isValidBase64(encryption.encryptedKey)) { + return res.status(422).json({ + errors: [`"encryptedKey" must be valid base64`], + }); + } } } - } - const id = uuid(); - const playbackId = await generateUniquePlaybackId(id); - const newAsset = await validateAssetPayload(req, id, playbackId, Date.now(), { - type: "url", - url, - encryption: assetEncryptionWithoutKey(encryption), - }); - const dupAsset = await db.asset.findDuplicateUrlUpload( - url, - req.user.id, - req.project?.id - ); - if (dupAsset) { - const [task] = await db.task.find({ outputAssetId: dupAsset.id }); - if (!task.length) { - console.error("Found asset with no task", dupAsset); - // proceed as a regular new asset - } else { - // return the existing asset and task, as if created now, with a slightly - // different status code (200, not 201). Should be transparent to clients. - res.status(200).json({ asset: dupAsset, task: { id: task[0].id } }); - return; + const id = uuid(); + const playbackId = await generateUniquePlaybackId(id); + const newAsset = await validateAssetPayload( + req, + id, + playbackId, + Date.now(), + { + type: "url", + url, + encryption: assetEncryptionWithoutKey(encryption), + } + ); + const dupAsset = await db.asset.findDuplicateUrlUpload( + url, + req.user.id, + req.project?.id + ); + if (dupAsset) { + const [task] = await db.task.find({ outputAssetId: dupAsset.id }); + if (!task.length) { + console.error("Found asset with no task", dupAsset); + // proceed as a regular new asset + } else { + // return the existing asset and task, as if created now, with a slightly + // different status code (200, not 201). Should be transparent to clients. + res.status(200).json({ asset: dupAsset, task: { id: task[0].id } }); + return; + } } - } - await ensureQueueCapacity(req.config, req.user.id); + await ensureQueueCapacity(req.config, req.user.id); - const asset = await createAsset(newAsset, req.queue); - const task = await req.taskScheduler.createAndScheduleTask( - "upload", - { - upload: { - url, - c2pa, - catalystPipelineStrategy: catalystPipelineStrategy(req), - encryption, - thumbnails: !(await isExperimentSubject( - "vod-thumbs-off", - req.user?.id - )), - ...(profiles ? { profiles } : null), // avoid serializing null profiles on the task, - targetSegmentSizeSecs, + const asset = await createAsset(newAsset, req.queue); + const task = await req.taskScheduler.createAndScheduleTask( + "upload", + { + upload: { + url, + c2pa, + catalystPipelineStrategy: catalystPipelineStrategy(req), + encryption, + thumbnails: !(await isExperimentSubject( + "vod-thumbs-off", + req.user?.id + )), + ...(profiles ? { profiles } : null), // avoid serializing null profiles on the task, + targetSegmentSizeSecs, + }, }, - }, - undefined, - asset - ); - - res.status(201); - res.json({ asset, task: { id: task.id } }); -}; + undefined, + asset + ); -app.post( - "/upload/url", - authorizer({}), - validatePost("new-asset-from-url-payload"), - uploadWithUrlHandler -); -// TODO: Remove this at some point. Registered only for backward compatibility. -app.post( - "/import", - authorizer({}), - validatePost("new-asset-payload"), - uploadWithUrlHandler + res.status(201); + res.json({ asset, task: { id: task.id } }); + } ); app.post( diff --git a/packages/api/src/schema/api-schema.yaml b/packages/api/src/schema/api-schema.yaml index fd512dbc97..f9dceb565e 100644 --- a/packages/api/src/schema/api-schema.yaml +++ b/packages/api/src/schema/api-schema.yaml @@ -176,6 +176,15 @@ components: name: type: string example: test_webhook + kind: + type: string + example: webhook + readOnly: true + deprecated: true + userId: + type: string + readOnly: true + deprecated: true createdAt: type: number readOnly: true @@ -448,6 +457,7 @@ components: kind: type: string example: stream + deprecated: true creatorId: $ref: "#/components/schemas/creator-id" userTags: @@ -544,18 +554,20 @@ components: example: Authorization: "Bearer 123" isMobile: - type: - - boolean - - integer - enum: - - false - - true - - 0 - - 1 - - 2 + oneOf: + - type: integer + enum: + - 0 + - 1 + - 2 + description: |- + 0: not mobile, 1: mobile screen share, 2: mobile camera. + - type: boolean + description: |- + If true, the stream will be pulled from a mobile source. description: |- - If true, the stream will be pulled from a mobile source. - default: false + Indicates whether the stream will be pulled from a mobile source. + default: 0 location: type: object description: |- @@ -643,7 +655,9 @@ components: Timestamp (in milliseconds) when the stream was last terminated userId: type: string + readOnly: true example: "we7818e7-610a-4057-8f6f-b785dc1e6f88" + deprecated: true renditions: type: object new-stream-payload: @@ -735,21 +749,25 @@ components: type: boolean description: Indicates whether the stream is currently live or not. is_healthy: - type: boolean - description: Indicates whether the stream is healthy or not. + oneOf: + - type: "null" + - type: "boolean" + description: "Indicates whether the stream is healthy or not." issues: - type: string + oneOf: + - type: "null" + - type: "string" description: | - Raw issues affecting the stream as described by Mist, if any. We - don't expose those to the end-user, showing only human_issues - instead. + "Raw issues affecting the stream as described by Mist, if any. We don't expose those to the end-user, showing only human_issues instead." human_issues: - type: array - description: | - A string array of human-readable errors describing issues affecting - the stream, if any. - items: - type: string + oneOf: + - type: "null" + - type: "array" + items: + type: "string" + description: + "A string array of human-readable errors describing issues affecting + the stream, if any." tracks: type: object description: | @@ -801,6 +819,15 @@ components: type: string readOnly: true example: de7818e7-610a-4057-8f6f-b785dc1e6f88 + kind: + type: string + example: stream + deprecated: true + userId: + type: string + readOnly: true + example: 66E2161C-7670-4D05-B71D-DA2D6979556F + deprecated: true name: type: string example: test_session @@ -885,8 +912,6 @@ components: items: $ref: "#/components/schemas/ffmpeg-profile" error: - required: - - errors type: object properties: errors: @@ -909,6 +934,11 @@ components: example: 09F8B46C-61A0-4254-9875-F71F4C605BC7 name: type: string + userId: + type: string + readOnly: true + example: 66E2161C-7670-4D05-B71D-DA2D6979556F + deprecated: true url: type: string writeOnly: true @@ -954,6 +984,11 @@ components: description: >- The playback ID to use with the Playback Info endpoint to retrieve playback URLs. + userId: + type: string + readOnly: true + example: 66E2161C-7670-4D05-B71D-DA2D6979556F + deprecated: true staticMp4: type: boolean writeOnly: true @@ -2085,6 +2120,11 @@ components: type: string description: Name of the signing key example: key1 + userId: + type: string + readOnly: true + example: 78df0075-b5f3-4683-a618-1086faca35dc + deprecated: true createdAt: readOnly: true type: number @@ -2211,7 +2251,9 @@ components: type: number example: 200000 playback-policy: - type: object + type: + - object + - "null" description: Whether the playback policy for a asset or stream is public or signed additionalProperties: false @@ -2244,17 +2286,12 @@ components: type: object description: | An individual metric about usage of a user. - required: - - UserID - - CreatorID - - DeliveryUsageMins - - TotalUsageMins - - StorageUsageMins properties: UserID: type: string description: The user ID associated with the metric example: 1bde4o2i6xycudoy + CreatorID: type: string description: The creator ID associated with the metric @@ -2397,7 +2434,7 @@ components: - source properties: live: - type: number + type: integer enum: - 0 - 1 @@ -2953,9 +2990,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: "#/components/schemas/multistream-target" + $ref: "#/components/schemas/multistream-target" x-speakeasy-name-override: data default: description: Error @@ -3063,6 +3098,12 @@ paths: To create a new webhook, you need to make an API call with the events you want to listen for and the URL that will be called when those events occur. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/webhook" responses: "200": description: Success @@ -3116,6 +3157,12 @@ paths: operationId: updateWebhook tags: - webhook + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/webhook" responses: "200": description: Success @@ -3397,6 +3444,9 @@ paths: $ref: "#/components/schemas/asset" task: type: object + required: + - id + additionalProperties: false properties: id: type: string @@ -3423,7 +3473,30 @@ paths: $ref: "#/components/schemas/new-asset-from-url-payload" responses: "200": - description: Success + description: Upload in progress + content: + application/json: + schema: + type: object + additionalProperties: false + required: + - asset + - task + properties: + asset: + $ref: "#/components/schemas/asset" + task: + type: object + required: + - id + additionalProperties: false + properties: + id: + type: string + example: 34d7618e-fd42-4798-acf5-19504616a11e + x-speakeasy-name-override: data + "201": + description: Upload started content: application/json: schema: @@ -3437,6 +3510,9 @@ paths: $ref: "#/components/schemas/asset" task: type: object + required: + - id + additionalProperties: false properties: id: type: string @@ -4178,9 +4254,6 @@ paths: description: | A simplified metric object about aggregate viewership of an asset. Either playbackId or dStorageUrl will be set. - required: - - viewCount - - playtimeMins properties: playbackId: $ref: >- @@ -4237,6 +4310,17 @@ paths: The creator ID to filter the query results schema: type: string + - name: "breakdownBy[]" + in: query + description: | + The list of fields to break down the query results. Currently the + only supported breakdown is by `creatorId`. + schema: + type: array + items: + type: string + enum: + - creatorId responses: "200": description: A Usage Metric object diff --git a/packages/api/src/schema/db-schema.yaml b/packages/api/src/schema/db-schema.yaml index a192dcc0b8..fd009ce097 100644 --- a/packages/api/src/schema/db-schema.yaml +++ b/packages/api/src/schema/db-schema.yaml @@ -586,13 +586,7 @@ components: webhook: table: webhook properties: - kind: - type: string - example: webhook - readOnly: true userId: - readOnly: true - type: string index: true events: index: true @@ -890,15 +884,11 @@ components: table: session properties: kind: - type: string - example: stream readOnly: true version: type: string description: Version of the session object userId: - type: string - example: 66E2161C-7670-4D05-B71D-DA2D6979556F index: true lastSeen: index: true @@ -924,9 +914,6 @@ components: table: multistream_target properties: userId: - type: string - example: 66E2161C-7670-4D05-B71D-DA2D6979556F - readOnly: true index: true asset: table: asset @@ -936,11 +923,7 @@ components: playbackId: index: true userId: - readOnly: true index: true - type: string - example: 66E2161C-7670-4D05-B71D-DA2D6979556F - description: owner of the asset source: oneOf: - properties: @@ -1078,20 +1061,6 @@ components: type: number description: Timestamp (in milliseconds) at which the stream started. example: 1587667174725 - webhook-patch-payload: - type: object - additionalProperties: false - properties: - name: - $ref: "#/components/schemas/webhook/properties/name" - url: - $ref: "#/components/schemas/webhook/properties/url" - events: - $ref: "#/components/schemas/webhook/properties/events" - sharedSecret: - $ref: "#/components/schemas/webhook/properties/sharedSecret" - streamId: - $ref: "#/components/schemas/webhook/properties/streamId" ipfs-file-info: properties: cid: @@ -1316,10 +1285,7 @@ components: id: index: true userId: - readOnly: true index: true - type: string - example: 78df0075-b5f3-4683-a618-1086faca35dc deleted: type: boolean default: false @@ -1394,3 +1360,17 @@ components: example: 09F8B46C-61A0-4254-9875-F71F4C605BC7 catalystPipelineStrategy: $ref: "#/components/schemas/task/properties/params/properties/upload/properties/catalystPipelineStrategy" + webhook-patch-payload: + type: object + additionalProperties: false + properties: + name: + $ref: "#/components/schemas/webhook/properties/name" + url: + $ref: "#/components/schemas/webhook/properties/url" + events: + $ref: "#/components/schemas/webhook/properties/events" + sharedSecret: + $ref: "#/components/schemas/webhook/properties/sharedSecret" + streamId: + $ref: "#/components/schemas/webhook/properties/streamId" diff --git a/packages/www/pages/app/stream/[id].tsx b/packages/www/pages/app/stream/[id].tsx index d3e5738eb6..44f58fd02b 100644 --- a/packages/www/pages/app/stream/[id].tsx +++ b/packages/www/pages/app/stream/[id].tsx @@ -258,8 +258,9 @@ const ID = () => { } else if ("errors" in info) { throw new Error(info.errors.toString()); } - setStream(info.stream); - setStreamOwner(info.user); + + setStream(info.stream as Stream); + setStreamOwner(info.user as User); } catch (err) { console.error(err); // todo: surface this }