@editframe/api 0.8.0-beta.4 → 0.8.0-beta.7
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.
- package/dist/CHUNK_SIZE_BYTES.d.ts +1 -0
- package/dist/CHUNK_SIZE_BYTES.js +7 -0
- package/dist/client.d.ts +0 -2
- package/dist/client.js +2 -14
- package/dist/client.test.d.ts +1 -0
- package/dist/readableFromBuffers.d.ts +2 -0
- package/dist/resources/caption-file.d.ts +22 -3
- package/dist/resources/caption-file.js +28 -26
- package/dist/resources/caption-file.test.d.ts +1 -0
- package/dist/resources/image-file.d.ts +5 -3
- package/dist/resources/image-file.js +23 -27
- package/dist/resources/image-file.test.d.ts +1 -0
- package/dist/resources/isobmff-file.d.ts +2 -3
- package/dist/resources/isobmff-file.js +17 -23
- package/dist/resources/isobmff-file.test.d.ts +1 -0
- package/dist/resources/isobmff-track.d.ts +27 -28
- package/dist/resources/isobmff-track.js +18 -31
- package/dist/resources/isobmff-track.test.d.ts +1 -0
- package/dist/resources/renders.d.ts +4 -5
- package/dist/resources/renders.js +17 -21
- package/dist/resources/renders.test.d.ts +1 -0
- package/dist/resources/unprocessed-file.d.ts +14 -12
- package/dist/resources/unprocessed-file.js +36 -45
- package/dist/resources/unprocessed-file.test.d.ts +1 -0
- package/dist/resources/url-token.d.ts +0 -1
- package/dist/resources/url-token.test.d.ts +1 -0
- package/dist/streamChunker.d.ts +2 -0
- package/dist/streamChunker.js +17 -0
- package/dist/streamChunker.test.d.ts +1 -0
- package/dist/uploadChunks.d.ts +10 -0
- package/dist/uploadChunks.js +65 -0
- package/dist/uploadChunks.test.d.ts +1 -0
- package/package.json +4 -3
- package/src/resources/caption-file.test.ts +124 -0
- package/src/resources/caption-file.ts +49 -24
- package/src/resources/image-file.test.ts +138 -0
- package/src/resources/image-file.ts +28 -25
- package/src/resources/isobmff-file.test.ts +108 -0
- package/src/resources/isobmff-file.ts +19 -22
- package/src/resources/isobmff-track.test.ts +152 -0
- package/src/resources/isobmff-track.ts +22 -31
- package/src/resources/renders.test.ts +112 -0
- package/src/resources/renders.ts +19 -20
- package/src/resources/test-av-file.txt +1 -0
- package/src/resources/unprocessed-file.test.ts +312 -0
- package/src/resources/unprocessed-file.ts +41 -44
- package/src/resources/url-token.test.ts +46 -0
|
@@ -7,9 +7,12 @@ import type { Client } from "../client.ts";
|
|
|
7
7
|
|
|
8
8
|
const log = debug("ef:api:caption-file");
|
|
9
9
|
|
|
10
|
+
const MAX_CAPTION_SIZE = 1024 * 1024 * 2; // 2MB
|
|
11
|
+
|
|
10
12
|
export const CreateCaptionFilePayload = z.object({
|
|
11
13
|
id: z.string(),
|
|
12
14
|
filename: z.string(),
|
|
15
|
+
byte_size: z.number().int().max(MAX_CAPTION_SIZE),
|
|
13
16
|
});
|
|
14
17
|
|
|
15
18
|
export interface CreateCaptionFileResult {
|
|
@@ -17,56 +20,78 @@ export interface CreateCaptionFileResult {
|
|
|
17
20
|
id: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
const restrictSize = (size: number) => {
|
|
24
|
+
if (size > MAX_CAPTION_SIZE) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`File size ${size} bytes exceeds limit ${MAX_CAPTION_SIZE} bytes\n`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a caption file
|
|
33
|
+
* @param client - The authenticated client to use for the request
|
|
34
|
+
* @param payload - The payload to send to the server
|
|
35
|
+
* @returns The result of the request
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const result = await createCaptionFile(client, {
|
|
39
|
+
* id: "123",
|
|
40
|
+
* filename: "caption.srt",
|
|
41
|
+
* });
|
|
42
|
+
* console.log(result);
|
|
43
|
+
* ```
|
|
44
|
+
* @category CaptionFile
|
|
45
|
+
* @resource
|
|
46
|
+
* @beta
|
|
47
|
+
*/
|
|
20
48
|
export const createCaptionFile = async (
|
|
21
49
|
client: Client,
|
|
22
50
|
payload: z.infer<typeof CreateCaptionFilePayload>,
|
|
23
51
|
) => {
|
|
24
52
|
log("Creating caption file", payload);
|
|
25
|
-
|
|
53
|
+
restrictSize(payload.byte_size);
|
|
54
|
+
const response = await client.authenticatedFetch(
|
|
26
55
|
"/api/video2/caption_files",
|
|
27
56
|
{
|
|
28
57
|
method: "POST",
|
|
29
58
|
body: JSON.stringify(payload),
|
|
30
59
|
},
|
|
31
60
|
);
|
|
32
|
-
log("Caption file created",
|
|
61
|
+
log("Caption file created", response);
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return (await fileCreation.json()) as CreateCaptionFileResult;
|
|
37
|
-
}
|
|
38
|
-
default: {
|
|
39
|
-
console.error(
|
|
40
|
-
`Failed to create file ${fileCreation.status} ${fileCreation.statusText}`,
|
|
41
|
-
);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
63
|
+
if (response.ok) {
|
|
64
|
+
return (await response.json()) as CreateCaptionFileResult;
|
|
44
65
|
}
|
|
66
|
+
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Failed to create caption ${response.status} ${response.statusText}`,
|
|
69
|
+
);
|
|
45
70
|
};
|
|
46
71
|
|
|
47
72
|
export const uploadCaptionFile = async (
|
|
48
73
|
client: Client,
|
|
49
74
|
fileId: string,
|
|
50
75
|
fileStream: Readable,
|
|
76
|
+
fileSize: number,
|
|
51
77
|
) => {
|
|
52
78
|
log("Uploading caption file", fileId);
|
|
53
|
-
|
|
79
|
+
restrictSize(fileSize);
|
|
80
|
+
|
|
81
|
+
const response = await client.authenticatedFetch(
|
|
54
82
|
`/api/video2/caption_files/${fileId}/upload`,
|
|
55
83
|
{
|
|
56
84
|
method: "POST",
|
|
57
85
|
body: fileStream,
|
|
58
86
|
},
|
|
59
87
|
);
|
|
60
|
-
log("Caption file uploaded",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
default: {
|
|
66
|
-
console.error(
|
|
67
|
-
`Failed to upload caption ${fileIndex.status} ${fileIndex.statusText}`,
|
|
68
|
-
);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
88
|
+
log("Caption file uploaded", response);
|
|
89
|
+
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
return response.json();
|
|
71
92
|
}
|
|
93
|
+
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Failed to upload caption ${response.status} ${response.statusText}`,
|
|
96
|
+
);
|
|
72
97
|
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
|
|
2
|
+
import { http, HttpResponse } from "msw";
|
|
3
|
+
import { setupServer } from "msw/node";
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
|
+
|
|
6
|
+
import { Client } from "../client.ts";
|
|
7
|
+
import { createImageFile, uploadImageFile } from "./image-file.ts";
|
|
8
|
+
import { readableFromBuffers } from "../readableFromBuffers.ts";
|
|
9
|
+
|
|
10
|
+
const server = setupServer();
|
|
11
|
+
const client = new Client("ef_TEST_TOKEN", "http://localhost");
|
|
12
|
+
|
|
13
|
+
describe("ImageFile", () => {
|
|
14
|
+
beforeAll(() => server.listen());
|
|
15
|
+
afterEach(() => server.resetHandlers());
|
|
16
|
+
afterAll(() => server.close());
|
|
17
|
+
|
|
18
|
+
describe("createImageFile", () => {
|
|
19
|
+
test("Throws when file is too large", async () => {
|
|
20
|
+
await expect(
|
|
21
|
+
createImageFile(client, {
|
|
22
|
+
id: "test-id",
|
|
23
|
+
filename: "test",
|
|
24
|
+
byte_size: 1024 * 1024 * 17,
|
|
25
|
+
height: 100,
|
|
26
|
+
width: 100,
|
|
27
|
+
mime_type: "image/jpeg",
|
|
28
|
+
}),
|
|
29
|
+
).rejects.toThrowError(
|
|
30
|
+
new ZodError([
|
|
31
|
+
{
|
|
32
|
+
code: "too_big",
|
|
33
|
+
maximum: 16777216,
|
|
34
|
+
type: "number",
|
|
35
|
+
inclusive: true,
|
|
36
|
+
exact: false,
|
|
37
|
+
message: "Number must be less than or equal to 16777216",
|
|
38
|
+
path: ["byte_size"],
|
|
39
|
+
},
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("Throws when server returns an error", async () => {
|
|
45
|
+
server.use(
|
|
46
|
+
http.post("http://localhost/api/video2/image_files", () =>
|
|
47
|
+
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await expect(
|
|
52
|
+
createImageFile(client, {
|
|
53
|
+
id: "test-id",
|
|
54
|
+
filename: "test",
|
|
55
|
+
byte_size: 4,
|
|
56
|
+
height: 100,
|
|
57
|
+
width: 100,
|
|
58
|
+
mime_type: "image/jpeg",
|
|
59
|
+
}),
|
|
60
|
+
).rejects.toThrowError("Failed to create file 500 Internal Server Error");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("Returns json data from the http response", async () => {
|
|
64
|
+
server.use(
|
|
65
|
+
http.post("http://localhost/api/video2/image_files", () =>
|
|
66
|
+
HttpResponse.json(
|
|
67
|
+
{ id: "test-id" },
|
|
68
|
+
{ status: 200, statusText: "OK" },
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const response = await createImageFile(client, {
|
|
74
|
+
id: "test-id",
|
|
75
|
+
filename: "test",
|
|
76
|
+
byte_size: 4,
|
|
77
|
+
height: 100,
|
|
78
|
+
width: 100,
|
|
79
|
+
mime_type: "image/jpeg",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(response).toEqual({ id: "test-id" });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("uploadImageFile", () => {
|
|
87
|
+
test("Throws when file is too large", async () => {
|
|
88
|
+
await expect(
|
|
89
|
+
uploadImageFile(
|
|
90
|
+
client,
|
|
91
|
+
"test-file-id",
|
|
92
|
+
readableFromBuffers(Buffer.from("test")),
|
|
93
|
+
1024 * 1024 * 17,
|
|
94
|
+
),
|
|
95
|
+
).rejects.toThrowError(
|
|
96
|
+
"File size 17825792 bytes exceeds limit 16777216 bytes",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("Throws if upload fails", async () => {
|
|
101
|
+
server.use(
|
|
102
|
+
http.post(
|
|
103
|
+
"http://localhost/api/video2/image_files/test-file-id/upload",
|
|
104
|
+
() => HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
uploadImageFile(
|
|
110
|
+
client,
|
|
111
|
+
"test-file-id",
|
|
112
|
+
readableFromBuffers(Buffer.from("test")),
|
|
113
|
+
4,
|
|
114
|
+
),
|
|
115
|
+
).rejects.toThrowError(
|
|
116
|
+
"Failed to upload chunk 0 for /api/video2/image_files/test-file-id/upload 500 Internal Server Error",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Uploads file", async () => {
|
|
121
|
+
server.use(
|
|
122
|
+
http.post(
|
|
123
|
+
"http://localhost/api/video2/image_files/test-file-id/upload",
|
|
124
|
+
() => HttpResponse.json(null, { status: 201 }),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await expect(
|
|
129
|
+
uploadImageFile(
|
|
130
|
+
client,
|
|
131
|
+
"test-file-id",
|
|
132
|
+
readableFromBuffers(Buffer.from("test")),
|
|
133
|
+
4,
|
|
134
|
+
),
|
|
135
|
+
).resolves.toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -4,15 +4,19 @@ import { z } from "zod";
|
|
|
4
4
|
import debug from "debug";
|
|
5
5
|
|
|
6
6
|
import type { Client } from "../client.ts";
|
|
7
|
+
import { uploadChunks } from "../uploadChunks.ts";
|
|
7
8
|
|
|
8
9
|
const log = debug("ef:api:image-file");
|
|
9
10
|
|
|
11
|
+
const MAX_IMAGE_SIZE = 1024 * 1024 * 16; // 16MB
|
|
12
|
+
|
|
10
13
|
export const CreateImageFilePayload = z.object({
|
|
11
14
|
id: z.string(),
|
|
12
15
|
height: z.number().int(),
|
|
13
16
|
width: z.number().int(),
|
|
14
17
|
mime_type: z.enum(["image/jpeg", "image/png", "image/jpg", "image/webp"]),
|
|
15
18
|
filename: z.string(),
|
|
19
|
+
byte_size: z.number().int().max(MAX_IMAGE_SIZE),
|
|
16
20
|
});
|
|
17
21
|
|
|
18
22
|
export interface CreateImageFileResult {
|
|
@@ -25,44 +29,43 @@ export const createImageFile = async (
|
|
|
25
29
|
payload: z.infer<typeof CreateImageFilePayload>,
|
|
26
30
|
) => {
|
|
27
31
|
log("Creating image file", payload);
|
|
32
|
+
CreateImageFilePayload.parse(payload);
|
|
28
33
|
const response = await client.authenticatedFetch("/api/video2/image_files", {
|
|
29
34
|
method: "POST",
|
|
30
35
|
body: JSON.stringify(payload),
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
log("Image file created", response);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
default: {
|
|
39
|
-
console.error(
|
|
40
|
-
`Failed to create file ${response.status} ${response.statusText}`,
|
|
41
|
-
);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
39
|
+
|
|
40
|
+
if (response.ok) {
|
|
41
|
+
return (await response.json()) as CreateImageFileResult;
|
|
44
42
|
}
|
|
43
|
+
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Failed to create file ${response.status} ${response.statusText}`,
|
|
46
|
+
);
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
export const uploadImageFile = async (
|
|
48
50
|
client: Client,
|
|
49
51
|
fileId: string,
|
|
50
52
|
fileStream: Readable,
|
|
53
|
+
fileSize: number,
|
|
51
54
|
) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
switch (fileIndex.status) {
|
|
60
|
-
case 200: {
|
|
61
|
-
return fileIndex.json();
|
|
62
|
-
}
|
|
63
|
-
default: {
|
|
64
|
-
console.error("Failed to upload image");
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
55
|
+
log("Uploading image file", fileId);
|
|
56
|
+
if (fileSize > MAX_IMAGE_SIZE) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`File size ${fileSize} bytes exceeds limit ${MAX_IMAGE_SIZE} bytes`,
|
|
59
|
+
);
|
|
67
60
|
}
|
|
61
|
+
|
|
62
|
+
const result = await uploadChunks(client, {
|
|
63
|
+
url: `/api/video2/image_files/${fileId}/upload`,
|
|
64
|
+
fileSize,
|
|
65
|
+
fileStream,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
log("Image file upload complete");
|
|
69
|
+
|
|
70
|
+
return result;
|
|
68
71
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
|
|
2
|
+
import { http, HttpResponse } from "msw";
|
|
3
|
+
import { setupServer } from "msw/node";
|
|
4
|
+
|
|
5
|
+
import { Client } from "../client.ts";
|
|
6
|
+
import { createISOBMFFFile, uploadFragmentIndex } from "./isobmff-file.ts";
|
|
7
|
+
import { readableFromBuffers } from "../readableFromBuffers.ts";
|
|
8
|
+
|
|
9
|
+
const server = setupServer();
|
|
10
|
+
const client = new Client("ef_TEST_TOKEN", "http://localhost");
|
|
11
|
+
|
|
12
|
+
describe("ISOBMFFFile", () => {
|
|
13
|
+
beforeAll(() => server.listen());
|
|
14
|
+
afterEach(() => server.resetHandlers());
|
|
15
|
+
afterAll(() => server.close());
|
|
16
|
+
|
|
17
|
+
describe("createISOBMFFFile", () => {
|
|
18
|
+
test("Throws when server returns an error", async () => {
|
|
19
|
+
server.use(
|
|
20
|
+
http.post("http://localhost/api/video2/isobmff_files", () =>
|
|
21
|
+
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
await expect(
|
|
26
|
+
createISOBMFFFile(client, {
|
|
27
|
+
id: "test-id",
|
|
28
|
+
filename: "test",
|
|
29
|
+
}),
|
|
30
|
+
).rejects.toThrowError(
|
|
31
|
+
"Failed to create isobmff file 500 Internal Server Error",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("Returns json data from the http response", async () => {
|
|
36
|
+
server.use(
|
|
37
|
+
http.post("http://localhost/api/video2/isobmff_files", () =>
|
|
38
|
+
HttpResponse.json(
|
|
39
|
+
{ id: "test-id" },
|
|
40
|
+
{ status: 200, statusText: "OK" },
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const response = await createISOBMFFFile(client, {
|
|
46
|
+
id: "test-id",
|
|
47
|
+
filename: "test",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(response).toEqual({ id: "test-id" });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("uploadFragmentIndex", () => {
|
|
55
|
+
test("Throws when file size exceeds limit", async () => {
|
|
56
|
+
await expect(
|
|
57
|
+
uploadFragmentIndex(
|
|
58
|
+
client,
|
|
59
|
+
"test-id",
|
|
60
|
+
readableFromBuffers(Buffer.from("test")),
|
|
61
|
+
1024 * 1024 * 3,
|
|
62
|
+
),
|
|
63
|
+
).rejects.toThrowError("File size exceeds limit of 2097152 bytes");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("Throws when server returns an error", async () => {
|
|
67
|
+
server.use(
|
|
68
|
+
http.post(
|
|
69
|
+
"http://localhost/api/video2/isobmff_files/test-id/index/upload",
|
|
70
|
+
() => HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await expect(
|
|
75
|
+
uploadFragmentIndex(
|
|
76
|
+
client,
|
|
77
|
+
"test-id",
|
|
78
|
+
readableFromBuffers(Buffer.from("test")),
|
|
79
|
+
4,
|
|
80
|
+
),
|
|
81
|
+
).rejects.toThrowError(
|
|
82
|
+
"Failed to create fragment index 500 Internal Server Error",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("Returns json data from the http response", async () => {
|
|
87
|
+
server.use(
|
|
88
|
+
http.post(
|
|
89
|
+
"http://localhost/api/video2/isobmff_files/test-id/index/upload",
|
|
90
|
+
() =>
|
|
91
|
+
HttpResponse.json(
|
|
92
|
+
{ fragment_index_complete: true },
|
|
93
|
+
{ status: 200, statusText: "OK" },
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const response = await uploadFragmentIndex(
|
|
99
|
+
client,
|
|
100
|
+
"test-id",
|
|
101
|
+
readableFromBuffers(Buffer.from("test")),
|
|
102
|
+
4,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(response).toEqual({ fragment_index_complete: true });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -6,6 +6,7 @@ import debug from "debug";
|
|
|
6
6
|
import type { Client } from "../client.ts";
|
|
7
7
|
|
|
8
8
|
const log = debug("ef:api:isobmff-file");
|
|
9
|
+
const FILE_SIZE_LIMIT = 1024 * 1024 * 2; // 32MB
|
|
9
10
|
|
|
10
11
|
export const CreateISOBMFFFilePayload = z.object({
|
|
11
12
|
id: z.string(),
|
|
@@ -32,26 +33,26 @@ export const createISOBMFFFile = async (
|
|
|
32
33
|
|
|
33
34
|
log("ISOBMFF file created", response);
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return (await response.json()) as CreateISOBMFFFileResult;
|
|
38
|
-
}
|
|
39
|
-
default: {
|
|
40
|
-
console.error(
|
|
41
|
-
`Failed to create file ${response.status} ${response.statusText}`,
|
|
42
|
-
);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
return (await response.json()) as CreateISOBMFFFileResult;
|
|
45
38
|
}
|
|
39
|
+
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Failed to create isobmff file ${response.status} ${response.statusText}`,
|
|
42
|
+
);
|
|
46
43
|
};
|
|
47
44
|
|
|
48
45
|
export const uploadFragmentIndex = async (
|
|
49
46
|
client: Client,
|
|
50
47
|
fileId: string,
|
|
51
48
|
fileStream: Readable,
|
|
49
|
+
fileSize: number,
|
|
52
50
|
) => {
|
|
53
51
|
log("Uploading fragment index", fileId);
|
|
54
|
-
|
|
52
|
+
if (fileSize > FILE_SIZE_LIMIT) {
|
|
53
|
+
throw new Error(`File size exceeds limit of ${FILE_SIZE_LIMIT} bytes`);
|
|
54
|
+
}
|
|
55
|
+
const response = await client.authenticatedFetch(
|
|
55
56
|
`/api/video2/isobmff_files/${fileId}/index/upload`,
|
|
56
57
|
{
|
|
57
58
|
method: "POST",
|
|
@@ -59,16 +60,12 @@ export const uploadFragmentIndex = async (
|
|
|
59
60
|
},
|
|
60
61
|
);
|
|
61
62
|
|
|
62
|
-
log("Fragment index uploaded",
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return fileIndex.json();
|
|
66
|
-
}
|
|
67
|
-
default: {
|
|
68
|
-
console.error(
|
|
69
|
-
`Failed to create fragment index ${fileIndex.status} ${fileIndex.statusText}`,
|
|
70
|
-
);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
63
|
+
log("Fragment index uploaded", response);
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
return response.json();
|
|
73
66
|
}
|
|
67
|
+
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Failed to create fragment index ${response.status} ${response.statusText}`,
|
|
70
|
+
);
|
|
74
71
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
|
|
2
|
+
import { http, HttpResponse } from "msw";
|
|
3
|
+
import { setupServer } from "msw/node";
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
|
+
|
|
6
|
+
import { Client } from "../client.ts";
|
|
7
|
+
import { createISOBMFFTrack, uploadISOBMFFTrack } from "./isobmff-track.ts";
|
|
8
|
+
import { readableFromBuffers } from "../readableFromBuffers.ts";
|
|
9
|
+
|
|
10
|
+
const server = setupServer();
|
|
11
|
+
const client = new Client("ef_TEST_TOKEN", "http://localhost");
|
|
12
|
+
|
|
13
|
+
describe("ISOBMFF Track", () => {
|
|
14
|
+
beforeAll(() => server.listen());
|
|
15
|
+
afterEach(() => server.resetHandlers());
|
|
16
|
+
afterAll(() => server.close());
|
|
17
|
+
|
|
18
|
+
describe("createISOBMFFTrack", () => {
|
|
19
|
+
test("Throws when track is too large", async () => {
|
|
20
|
+
await expect(
|
|
21
|
+
createISOBMFFTrack(
|
|
22
|
+
client,
|
|
23
|
+
createTestTrack({ byte_size: 1024 * 1024 * 1025 }),
|
|
24
|
+
),
|
|
25
|
+
).rejects.toThrowError(
|
|
26
|
+
new ZodError([
|
|
27
|
+
{
|
|
28
|
+
code: "too_big",
|
|
29
|
+
maximum: 1073741824,
|
|
30
|
+
type: "number",
|
|
31
|
+
inclusive: true,
|
|
32
|
+
exact: false,
|
|
33
|
+
message: "Number must be less than or equal to 1073741824",
|
|
34
|
+
path: ["byte_size"],
|
|
35
|
+
},
|
|
36
|
+
]),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("Throws when server returns an error", async () => {
|
|
41
|
+
server.use(
|
|
42
|
+
http.post("http://localhost/api/video2/isobmff_tracks", () =>
|
|
43
|
+
HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await expect(
|
|
48
|
+
createISOBMFFTrack(client, createTestTrack()),
|
|
49
|
+
).rejects.toThrowError(
|
|
50
|
+
"Failed to create isobmff track 500 Internal Server Error",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("Returns json data from the http response", async () => {
|
|
55
|
+
server.use(
|
|
56
|
+
http.post("http://localhost/api/video2/isobmff_tracks", () =>
|
|
57
|
+
HttpResponse.json(
|
|
58
|
+
{ testResponse: "test" },
|
|
59
|
+
{ status: 200, statusText: "OK" },
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const response = await createISOBMFFTrack(
|
|
65
|
+
client,
|
|
66
|
+
createTestTrack({ byte_size: 1024 * 1024 * 5 }),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(response).toEqual({ testResponse: "test" });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("uploadISOBMFFTrack", () => {
|
|
74
|
+
test("Throws when server returns an error", async () => {
|
|
75
|
+
server.use(
|
|
76
|
+
http.post(
|
|
77
|
+
"http://localhost/api/video2/isobmff_tracks/test-file/1/upload",
|
|
78
|
+
() => HttpResponse.text("Internal Server Error", { status: 500 }),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
uploadISOBMFFTrack(
|
|
84
|
+
client,
|
|
85
|
+
"test-file",
|
|
86
|
+
1,
|
|
87
|
+
readableFromBuffers(Buffer.from("test")),
|
|
88
|
+
4,
|
|
89
|
+
),
|
|
90
|
+
).rejects.toThrowError(
|
|
91
|
+
"Failed to upload chunk 0 for /api/video2/isobmff_tracks/test-file/1/upload 500 Internal Server Error",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("Succeeds when server returns a success", async () => {
|
|
96
|
+
server.use(
|
|
97
|
+
http.post(
|
|
98
|
+
"http://localhost/api/video2/isobmff_tracks/test-file/1/upload",
|
|
99
|
+
() => HttpResponse.json({}, { status: 201 }),
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
uploadISOBMFFTrack(
|
|
105
|
+
client,
|
|
106
|
+
"test-file",
|
|
107
|
+
1,
|
|
108
|
+
readableFromBuffers(Buffer.from("test")),
|
|
109
|
+
4,
|
|
110
|
+
),
|
|
111
|
+
).resolves.toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function createTestTrack(
|
|
117
|
+
options: Partial<Parameters<typeof createISOBMFFTrack>[1]> = {},
|
|
118
|
+
) {
|
|
119
|
+
return Object.assign(
|
|
120
|
+
{
|
|
121
|
+
file_id: "test-id",
|
|
122
|
+
track_id: 1,
|
|
123
|
+
type: "audio",
|
|
124
|
+
probe_info: {
|
|
125
|
+
channels: 2,
|
|
126
|
+
sample_rate: "44100",
|
|
127
|
+
duration: 1000,
|
|
128
|
+
duration_ts: 1000,
|
|
129
|
+
start_time: 0,
|
|
130
|
+
start_pts: 0,
|
|
131
|
+
r_frame_rate: "100",
|
|
132
|
+
channel_layout: "stereo",
|
|
133
|
+
codec_tag_string: "mp3",
|
|
134
|
+
codec_long_name: "MP3",
|
|
135
|
+
codec_type: "audio",
|
|
136
|
+
codec_tag: "0x0000",
|
|
137
|
+
codec_name: "aac",
|
|
138
|
+
bits_per_sample: 16,
|
|
139
|
+
index: 0,
|
|
140
|
+
sample_fmt: "s16",
|
|
141
|
+
time_base: "100",
|
|
142
|
+
avg_frame_rate: "100",
|
|
143
|
+
disposition: {},
|
|
144
|
+
bit_rate: "100",
|
|
145
|
+
},
|
|
146
|
+
duration_ms: 1000,
|
|
147
|
+
codec_name: "mp3",
|
|
148
|
+
byte_size: 1024 * 1024 * 5,
|
|
149
|
+
} as const,
|
|
150
|
+
options,
|
|
151
|
+
);
|
|
152
|
+
}
|