@editframe/api 0.16.4-beta.0 → 0.16.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
1
  import { http, HttpResponse } from "msw";
2
2
  import { setupServer } from "msw/node";
3
3
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
- import { ZodError } from "zod";
5
4
 
6
5
  import { Client } from "../client.js";
7
6
  import { webReadableFromBuffers } from "../readableFromBuffers.js";
@@ -58,19 +57,21 @@ describe("ImageFile", () => {
58
57
  width: 100,
59
58
  mime_type: "image/jpeg",
60
59
  }),
61
- ).rejects.toThrowError(
62
- new ZodError([
60
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
61
+ [ZodError: [
63
62
  {
64
- code: "too_big",
65
- maximum: 16777216,
66
- type: "number",
67
- inclusive: true,
68
- exact: false,
69
- message: "Number must be less than or equal to 16777216",
70
- path: ["byte_size"],
71
- },
72
- ]),
73
- );
63
+ "code": "too_big",
64
+ "maximum": 16777216,
65
+ "type": "number",
66
+ "inclusive": true,
67
+ "exact": false,
68
+ "message": "Number must be less than or equal to 16777216",
69
+ "path": [
70
+ "byte_size"
71
+ ]
72
+ }
73
+ ]]
74
+ `);
74
75
  });
75
76
 
76
77
  test("Throws when server returns an error", async () => {
@@ -1,7 +1,6 @@
1
1
  import { http, HttpResponse } from "msw";
2
2
  import { setupServer } from "msw/node";
3
3
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
- import { ZodError } from "zod";
5
4
 
6
5
  import { createTestTrack } from "TEST/createTestTrack.js";
7
6
  import { Client } from "../client.js";
@@ -29,19 +28,21 @@ describe("ISOBMFF Track", () => {
29
28
  client,
30
29
  createTestTrack({ byte_size: 1024 * 1024 * 1025 }),
31
30
  ),
32
- ).rejects.toThrowError(
33
- new ZodError([
31
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
32
+ [ZodError: [
34
33
  {
35
- code: "too_big",
36
- maximum: 1073741824,
37
- type: "number",
38
- inclusive: true,
39
- exact: false,
40
- message: "Number must be less than or equal to 1073741824",
41
- path: ["byte_size"],
42
- },
43
- ]),
44
- );
34
+ "code": "too_big",
35
+ "maximum": 1073741824,
36
+ "type": "number",
37
+ "inclusive": true,
38
+ "exact": false,
39
+ "message": "Number must be less than or equal to 1073741824",
40
+ "path": [
41
+ "byte_size"
42
+ ]
43
+ }
44
+ ]]
45
+ `);
45
46
  });
46
47
 
47
48
  test("Throws when server returns an error", async () => {
@@ -4,7 +4,12 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
4
 
5
5
  import { Client } from "../client.js";
6
6
  import { webReadableFromBuffers } from "../readableFromBuffers.js";
7
- import { createRender, lookupRenderByMd5, uploadRender } from "./renders.js";
7
+ import {
8
+ OutputConfiguration,
9
+ createRender,
10
+ lookupRenderByMd5,
11
+ uploadRender,
12
+ } from "./renders.js";
8
13
 
9
14
  const server = setupServer();
10
15
  const client = new Client("ef_TEST_TOKEN", "http://localhost");
@@ -130,6 +135,61 @@ describe("Renders", () => {
130
135
  });
131
136
  });
132
137
 
138
+ describe("OutputConfiguration", () => {
139
+ test("should create a valid output configuration from nullish values", () => {
140
+ const outputConfiguration = OutputConfiguration.parse();
141
+ expect(outputConfiguration.output).toEqual({
142
+ container: "mp4",
143
+ audio: {
144
+ codec: "aac",
145
+ },
146
+ video: {
147
+ codec: "h264",
148
+ },
149
+ });
150
+ });
151
+
152
+ test("should permit mp4 configuration", () => {
153
+ const outputConfiguration = OutputConfiguration.parse({
154
+ container: "mp4",
155
+ video: {
156
+ codec: "h264",
157
+ },
158
+ audio: {
159
+ codec: "aac",
160
+ },
161
+ });
162
+ expect(outputConfiguration.isVideo).toBe(true);
163
+ });
164
+
165
+ test("should permit png configuration", () => {
166
+ const outputConfiguration = OutputConfiguration.parse({
167
+ container: "png",
168
+ compression: 100,
169
+ transparency: true,
170
+ });
171
+ expect(outputConfiguration.isStill).toBe(true);
172
+ });
173
+
174
+ test("should permit webp configuration", () => {
175
+ const outputConfiguration = OutputConfiguration.parse({
176
+ container: "webp",
177
+ quality: 100,
178
+ compression: 6,
179
+ transparency: true,
180
+ });
181
+ expect(outputConfiguration.isStill).toBe(true);
182
+ });
183
+
184
+ test("should permit jpeg configuration", () => {
185
+ const outputConfiguration = OutputConfiguration.parse({
186
+ container: "jpeg",
187
+ quality: 100,
188
+ });
189
+ expect(outputConfiguration.isStill).toBe(true);
190
+ });
191
+ });
192
+
133
193
  const createTestRender = () =>
134
194
  ({
135
195
  md5: "test-md5",
@@ -7,6 +7,49 @@ import { assertTypesMatch } from "../utils/assertTypesMatch.ts";
7
7
 
8
8
  const log = debug("ef:api:renders");
9
9
 
10
+ const H264Configuration = z.object({
11
+ codec: z.literal("h264"),
12
+ });
13
+
14
+ const AACConfiguration = z.object({
15
+ codec: z.literal("aac"),
16
+ });
17
+
18
+ const MP4Configuration = z.object({
19
+ container: z.literal("mp4"),
20
+ video: H264Configuration,
21
+ audio: AACConfiguration,
22
+ });
23
+
24
+ const JpegConfiguration = z.object({
25
+ container: z.literal("jpeg"),
26
+ quality: z.number().int().min(1).max(100).default(80).optional(),
27
+ });
28
+
29
+ const PngConfiguration = z.object({
30
+ container: z.literal("png"),
31
+ compression: z.number().int().min(1).max(100).default(80).optional(),
32
+ transparency: z.boolean().default(false).optional(),
33
+ });
34
+
35
+ const WebpConfiguration = z.object({
36
+ container: z.literal("webp"),
37
+ quality: z.number().int().min(1).max(100).default(80).optional(),
38
+ compression: z.number().int().min(0).max(6).default(4).optional(),
39
+ transparency: z.boolean().default(false).optional(),
40
+ });
41
+
42
+ export const RenderOutputConfiguration = z.discriminatedUnion("container", [
43
+ MP4Configuration,
44
+ JpegConfiguration,
45
+ PngConfiguration,
46
+ WebpConfiguration,
47
+ ]);
48
+
49
+ export type RenderOutputConfiguration = z.infer<
50
+ typeof RenderOutputConfiguration
51
+ >;
52
+
10
53
  export const CreateRenderPayload = z.object({
11
54
  md5: z.string().optional(),
12
55
  fps: z.number().int().min(1).max(120).default(30).optional(),
@@ -23,8 +66,83 @@ export const CreateRenderPayload = z.object({
23
66
  metadata: z.record(z.string(), z.string()).optional(),
24
67
  duration_ms: z.number().int().optional(),
25
68
  strategy: z.enum(["v1"]).default("v1").optional(),
69
+ output: RenderOutputConfiguration.default({
70
+ container: "mp4",
71
+ video: {
72
+ codec: "h264",
73
+ },
74
+ audio: {
75
+ codec: "aac",
76
+ },
77
+ }).optional(),
26
78
  });
27
79
 
80
+ export const CreateRenderPayloadWithOutput = CreateRenderPayload.extend({
81
+ output: RenderOutputConfiguration,
82
+ });
83
+
84
+ export class OutputConfiguration {
85
+ static parse(input?: any) {
86
+ const output = RenderOutputConfiguration.parse(
87
+ input ?? {
88
+ container: "mp4",
89
+ video: {
90
+ codec: "h264",
91
+ },
92
+ audio: {
93
+ codec: "aac",
94
+ },
95
+ },
96
+ );
97
+ return new OutputConfiguration(output);
98
+ }
99
+
100
+ constructor(public readonly output: RenderOutputConfiguration) {}
101
+
102
+ get isStill() {
103
+ return (
104
+ this.output.container === "jpeg" ||
105
+ this.output.container === "png" ||
106
+ this.output.container === "webp"
107
+ );
108
+ }
109
+
110
+ get isVideo() {
111
+ return this.output.container === "mp4";
112
+ }
113
+
114
+ get fileExtension() {
115
+ return this.output.container;
116
+ }
117
+
118
+ get contentType() {
119
+ if (this.isStill) {
120
+ return `image/${this.fileExtension}`;
121
+ }
122
+ return `video/${this.fileExtension}`;
123
+ }
124
+
125
+ get container() {
126
+ return this.output.container;
127
+ }
128
+
129
+ get jpegConfig() {
130
+ return this.output.container === "jpeg" ? this.output : null;
131
+ }
132
+
133
+ get pngConfig() {
134
+ return this.output.container === "png" ? this.output : null;
135
+ }
136
+
137
+ get webpConfig() {
138
+ return this.output.container === "webp" ? this.output : null;
139
+ }
140
+
141
+ get mp4Config() {
142
+ return this.output.container === "mp4" ? this.output : null;
143
+ }
144
+ }
145
+
28
146
  export interface CreateRenderPayload {
29
147
  md5?: string;
30
148
  fps?: number;
@@ -35,6 +153,7 @@ export interface CreateRenderPayload {
35
153
  duration_ms?: number;
36
154
  metadata?: Record<string, string>;
37
155
  strategy?: "v1";
156
+ output?: z.infer<typeof RenderOutputConfiguration>;
38
157
  }
39
158
 
40
159
  assertTypesMatch<CreateRenderPayload, z.infer<typeof CreateRenderPayload>>(
@@ -65,6 +184,16 @@ export const createRender = async (
65
184
  // Manually applying defaults here is a hack
66
185
  payload.strategy ??= "v1";
67
186
  payload.work_slice_ms ??= 4_000;
187
+ payload.output ??= {
188
+ container: "mp4",
189
+ video: {
190
+ codec: "h264",
191
+ },
192
+ audio: {
193
+ codec: "aac",
194
+ },
195
+ };
196
+
68
197
  const response = await client.authenticatedFetch("/api/v1/renders", {
69
198
  method: "POST",
70
199
  body: JSON.stringify(payload),
@@ -1,7 +1,6 @@
1
1
  import { http, HttpResponse } from "msw";
2
2
  import { setupServer } from "msw/node";
3
3
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
- import { ZodError } from "zod";
5
4
 
6
5
  import { Client } from "../client.js";
7
6
  import { webReadableFromBuffers } from "../readableFromBuffers.js";
@@ -32,19 +31,21 @@ describe("Unprocessed File", () => {
32
31
  filename: "test-file",
33
32
  byte_size: 1024 * 1024 * 1025,
34
33
  }),
35
- ).rejects.toThrowError(
36
- new ZodError([
34
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
35
+ [ZodError: [
37
36
  {
38
- code: "too_big",
39
- maximum: 1073741824,
40
- type: "number",
41
- inclusive: true,
42
- exact: false,
43
- message: "Number must be less than or equal to 1073741824",
44
- path: ["byte_size"],
45
- },
46
- ]),
47
- );
37
+ "code": "too_big",
38
+ "maximum": 1073741824,
39
+ "type": "number",
40
+ "inclusive": true,
41
+ "exact": false,
42
+ "message": "Number must be less than or equal to 1073741824",
43
+ "path": [
44
+ "byte_size"
45
+ ]
46
+ }
47
+ ]]
48
+ `);
48
49
  });
49
50
 
50
51
  test("Throws when server returns an error", async () => {