diff --git a/packages/api/src/controllers/stream.test.ts b/packages/api/src/controllers/stream.test.ts index 36f230c14d..c1911e5699 100644 --- a/packages/api/src/controllers/stream.test.ts +++ b/packages/api/src/controllers/stream.test.ts @@ -478,7 +478,7 @@ describe("controllers/stream", () => { }); }); - it("should create a stream, delete it, and error when attempting additional detele or replace", async () => { + it("should create a stream, delete it, and error when attempting additional delete or replace", async () => { const res = await client.post("/stream", { ...postMockStream }); expect(res.status).toBe(201); const stream = await res.json(); @@ -506,6 +506,26 @@ describe("controllers/stream", () => { } }); + it("should create a stream and add a multistream target for it", async () => { + const res = await client.post("/stream", { ...postMockStream }); + expect(res.status).toBe(201); + const stream = await res.json(); + expect(stream.id).toBeDefined(); + + const document = await server.store.get(`stream/${stream.id}`); + expect(server.db.stream.addDefaultFields(document)).toEqual(stream); + + const res2 = await client.post( + `/stream/${stream.id}/create-multistream-target`, + { + profile: "source", + videoOnly: false, + spec: { name: "target-name", url: "rtmp://test/test" }, + } + ); + expect(res2.status).toBe(204); + }); + describe("set active and heartbeat", () => { const callSetActive = async ( streamId: string, diff --git a/packages/api/src/controllers/stream.ts b/packages/api/src/controllers/stream.ts index bbe0098bed..9c2be47627 100644 --- a/packages/api/src/controllers/stream.ts +++ b/packages/api/src/controllers/stream.ts @@ -1327,6 +1327,86 @@ app.patch( } ); +app.post( + "/:id/create-multistream-target", + authorizer({}), + validatePost("target-add-payload"), + async (req, res) => { + const payload = req.body; + + const stream = await db.stream.get(req.params.id); + + if (!stream || stream.deleted) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } + + if (stream.userId !== req.user.id) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } + + let multistream: DBStream["multistream"] = { + targets: [...(stream.multistream?.targets ?? []), payload], + }; + + multistream = await validateMultistreamOpts( + req.user.id, + stream.profiles, + multistream + ); + + let patch: StreamPatchPayload & Partial = { + multistream, + }; + + await db.stream.update(stream.id, patch); + + await triggerCatalystStreamUpdated(req, stream.playbackId); + + res.status(204); + res.end(); + } +); + +app.delete("/:id/multistream/:targetId", authorizer({}), async (req, res) => { + const { id, targetId } = req.params; + + const stream = await db.stream.get(id); + + if (!stream || stream.deleted) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } + + if (stream.userId !== req.user.id) { + res.status(404); + return res.json({ errors: ["stream not found"] }); + } + + let multistream: DBStream["multistream"] = stream.multistream ?? { + targets: [], + }; + + multistream.targets = multistream.targets.filter((t) => t.id !== targetId); + multistream = await validateMultistreamOpts( + req.user.id, + stream.profiles, + multistream + ); + + let patch: StreamPatchPayload & Partial = { + multistream, + }; + + await db.stream.update(stream.id, patch); + + await triggerCatalystStreamUpdated(req, stream.playbackId); + + res.status(204); + res.end(); +}); + app.patch( "/:id", authorizer({}), diff --git a/packages/api/src/schema/api-schema.yaml b/packages/api/src/schema/api-schema.yaml index b33aa44026..8723bff159 100644 --- a/packages/api/src/schema/api-schema.yaml +++ b/packages/api/src/schema/api-schema.yaml @@ -273,6 +273,44 @@ components: sessionId: type: string description: Session ID of the stream to clip + target: + type: object + required: + - profile + additionalProperties: false + properties: + profile: + type: string + description: | + Name of transcoding profile that should be sent. Use + "source" for pushing source stream data + minLength: 1 + maxLength: 500 + example: 720p + videoOnly: + type: boolean + description: | + If true, the stream audio will be muted and only silent + video will be pushed to the target. + default: false + id: + type: string + description: ID of multistream target object where to push this stream + spec: + type: object + writeOnly: true + description: | + Inline multistream target object. Will automatically + create the target resource to be used by the created + stream. + required: + - url + additionalProperties: false + properties: + name: + type: string + url: + $ref: "#/components/schemas/multistream-target/properties/url" stream: type: object required: @@ -394,44 +432,7 @@ components: References to targets where this stream will be simultaneously streamed to items: - type: object - required: - - profile - additionalProperties: false - properties: - profile: - type: string - description: | - Name of transcoding profile that should be sent. Use - "source" for pushing source stream data - minLength: 1 - maxLength: 500 - example: 720p - videoOnly: - type: boolean - description: | - If true, the stream audio will be muted and only silent - video will be pushed to the target. - default: false - id: - type: string - description: - ID of multistream target object where to push this stream - spec: - type: object - writeOnly: true - description: | - Inline multistream target object. Will automatically - create the target resource to be used by the created - stream. - required: - - url - additionalProperties: false - properties: - name: - type: string - url: - $ref: "#/components/schemas/multistream-target/properties/url" + $ref: "#/components/schemas/target" suspended: type: boolean description: If currently suspended @@ -504,6 +505,10 @@ components: $ref: "#/components/schemas/playback-policy" profiles: $ref: "#/components/schemas/stream/properties/profiles" + target-add-payload: + type: object + additionalProperties: false + $ref: "#/components/schemas/target" stream-health-payload: type: object description: | @@ -3010,6 +3015,56 @@ paths: application/json: schema: $ref: "#/components/schemas/error" + "/stream/{id}/create-multistream-target": + post: + summary: Add a multistream target + parameters: + - in: path + name: id + schema: + type: string + description: ID of the parent stream + required: true + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/target-add-payload" + responses: + "204": + description: Success (No content) + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/error" + "/stream/{id}/multistream/{targetId}": + delete: + summary: Remove a multistream target + parameters: + - in: path + name: id + schema: + type: string + description: ID of the parent stream + required: true + - in: path + name: targetId + schema: + type: string + description: ID of the multistream target + required: true + responses: + "204": + description: Success (No content) + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/error" "/session/{id}/clips": get: summary: Retrieve clips of a session