Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify-api #27

Merged
merged 6 commits into from
Mar 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,5 @@ dist
.DS_Store

.tmp
packages/*/lib
packages/*/lib
packages/*/output
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun check
75 changes: 32 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,54 @@ The docker image is available at [ghcr.io/sv2dev/media-optimizer](https://github

## Usage

```bash
docker run -p 3000:3000 -v $(pwd)/output:/output ghcr.io/sv2dev/media-optimizer:0.0.3
```

## API

### POST /images/process
### Running the server

#### Request
The [`@m4k/server`](./packages/server) can be deployed as a docker container.

```
URL: /images/process?<Options|options=OptionsJSON[]>
Method: POST
Headers:
- [X-Options: <OptionsJSON[]>]
Body: Binary file
```bash
docker run -p 3000:3000 -v $(pwd)/output:/output ghcr.io/sv2dev/m4k:0.1.2
```

#### Response
Alternatively, you can install the server as a local package and run it:

```
Status: 200 OK
Headers:
- Content-Type: multipart/mixed; boundary=<boundary>
Body: multipart/mixed body with status updates and the processed files (if no output option was provided)
```bash
bun add @m4k/server
```

### POST /videos/process
Note: This server implementation is using Bun as runtime, so it can only be run on a system that has Bun installed.

#### Request
Then, run the server:

```
URL: /videos/process?<Options|options=OptionsJSON[]>
Method: POST
Headers:
- [X-Options: <OptionsJSON[]>]
Body: Binary file
```bash
bun run @m4k/server
```

#### Response
Or import the server as a module and embed it in your project:

```
Status: 200 OK
Headers:
- Content-Type: multipart/mixed; boundary=<boundary>
Body: multipart/mixed body with status updates and the processed files (if no output option was provided)
```ts
import { serveOpts } from "@m4k/server";

Bun.serve(serveOpts);
```

### Options
### Using the client

The options can be provided as query parameters or as `X-Options` header.
Each provided option object will result in a processed output file, which allows you to process the same input file multiple times and create different output files (currently only enabled for images).
The client is available as a standalone package [@m4k/client](./packages/client). You can use it to connect to the m4k server.

#### Examples
Example, using the client on Bun:

Using curl with headers, redirecting output to a file (within the container):
```ts
import { ProcessedFile, processVideo } from "@m4k/client";

```
curl -X POST http://localhost:3000/videos/process \
-H 'X-Options: {"format": "mp4", "videoCodec": "libsvtav1", "output": "/output/output.mp4"}' \
--data-binary @/path/to/input.mov
for await (const value of processVideo(
"http://localhost:3000",
Bun.file("fixtures/video.mp4"),
{ format: "mp4", videoCodec: "libx265", output: "output/video.mp4" }
)) {
if (value instanceof ProcessedFile) {
// handle the processed file
} else {
// handle the status update
}
}
```
7 changes: 5 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"": {
"name": "m4k-workspaces",
"devDependencies": {
"husky": "^9.1.7",
"typescript": "^5.0.0",
},
},
"packages/client": {
"name": "@m4k/client",
"version": "0.1.1",
"version": "0.1.2",
"dependencies": {
"@m4k/common": "workspace:^",
},
Expand Down Expand Up @@ -44,7 +45,7 @@
},
"packages/server": {
"name": "@m4k/server",
"version": "0.1.1",
"version": "0.1.2",
"dependencies": {
"m4k": "workspace:^",
},
Expand Down Expand Up @@ -142,6 +143,8 @@

"detect-libc": ["[email protected]", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],

"husky": ["[email protected]", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],

"is-arrayish": ["[email protected]", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],

"m4k": ["m4k@workspace:packages/core"],
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
services:
video-optimizer:
m4k:
build: .
ports:
- 3000:3000
volumes:
- ./output:/output
Binary file added fixtures/audio.mp3
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
"scripts": {
"dev": "bun --filter @m4k/server dev",
"test": "bun --filter '*' test",
"check": "bun run build -f && bun run test",
"build": "bun --filter '*' build",
"build:docker": "bun --filter @m4k/server build:docker",
"typecheck": "bun --filter '*' typecheck"
},
"devDependencies": {
"husky": "^9.1.7",
"typescript": "^5.0.0"
},
"workspaces": [
Expand Down
41 changes: 30 additions & 11 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# m4k - media kit client

A client for the media kit server `@m4k/server`.
It provides queueing and progress reporting via async iterables.
It provides queueing and progress reporting via `AsyncIterable`s.

## Usage

Converting images:
### Processing audio

```ts
import { optimizeImage, ConvertedFile } from "@m4k/client";
import { processAudio, ProcessedFile } from "@m4k/client";
// host is the URL of the media kit server
// input is a ReadableStream or Blob
for await (const value of optimizeImage(host, input, opts)) {
if (value instanceof ConvertedFile) {
// input is the binary file content as an AsyncIterable or Blob
for await (const value of processAudio(host, input, opts)) {
if (value instanceof ProcessedFile) {
// do something with the file
} else if ("position" in value) {
// do something with the queue position
Expand All @@ -24,14 +24,33 @@ for await (const value of optimizeImage(host, input, opts)) {
}
```

Converting videos:
### Processing images

```ts
import { optimizeVideo, ConvertedFile } from "@m4k/client";
import { processImage, ProcessedFile } from "@m4k/client";
// host is the URL of the media kit server
// input is a file path
for await (const value of optimizeVideo(host, input, opts)) {
if (value instanceof ConvertedFile) {
// input is the binary file content as an AsyncIterable or Blob
for await (const value of processImage(host, input, opts)) {
if (value instanceof ProcessedFile) {
// do something with the file
} else if ("position" in value) {
// do something with the queue position
} else if ("progress" in value) {
// do something with the progress
} else {
// handle value.error
}
}
```

### Processing videos

```ts
import { processVideo, ProcessedFile } from "@m4k/client";
// host is the URL of the media kit server
// input is the binary file content as an AsyncIterable or Blob
for await (const value of processVideo(host, input, opts)) {
if (value instanceof ProcessedFile) {
// do something with the file
} else if ("position" in value) {
// do something with the queue position
Expand Down
65 changes: 58 additions & 7 deletions packages/client/src/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
import { ConvertedFile } from "@m4k/common";
import { ProcessedFile } from "@m4k/common";
import serveOpts from "@m4k/server";
import { describe, expect, it } from "bun:test";
import { optimizeImage, setFetch } from "./client";
setFetch(async (x) => serveOpts.fetch.call(serveOpts as any, new Request(x), null as any));
import { processAudio, processImage, processVideo, setFetch } from "./client";

describe("optimizeImage()", () => {
setFetch(async (x) =>
serveOpts.fetch.call(serveOpts as any, new Request(x), null as any)
);

describe("processAudio()", () => {
it("should query the server to process an audio file", async () => {
const audio = Bun.file("../../fixtures/audio.mp3");
const files: { name: string; type: string; size: number }[] = [];

for await (const x of processAudio("http://localhost:3000", audio, [
{ format: "mp3", name: "audio.mp3" },
])) {
if (x instanceof ProcessedFile) {
const chunks = await Array.fromAsync(x.stream);
files.push({
name: x.name,
type: x.type,
size: chunks.reduce((sum, chunk) => sum + chunk.length, 0),
});
}
}

expect(files).toEqual([
{ name: "audio.mp3", type: "audio/mpeg", size: 52288 },
]);
});
});

describe("processImage()", () => {
it("should query the server to process an image", async () => {
const img = Bun.file("../../fixtures/image.jpeg");
const files: ({ name: string, type: string, size: number })[] = [];
const files: { name: string; type: string; size: number }[] = [];

for await (const x of optimizeImage("http://localhost:3000", img, [
for await (const x of processImage("http://localhost:3000", img, [
{ format: "webp", name: "image.webp" },
{ format: "avif", name: "image.avif" },
])) {
if (x instanceof ConvertedFile) {
if (x instanceof ProcessedFile) {
const chunks = await Array.fromAsync(x.stream);
files.push({
name: x.name,
Expand All @@ -29,3 +56,27 @@ describe("optimizeImage()", () => {
]);
});
});

describe("processVideo()", () => {
it("should query the server to process a video", async () => {
const video = Bun.file("../../fixtures/video.mp4");
const files: { name: string; type: string; size: number }[] = [];

for await (const x of processVideo("http://localhost:3000", video, [
{ format: "mp4", name: "video.mp4" },
])) {
if (x instanceof ProcessedFile) {
const chunks = await Array.fromAsync(x.stream);
files.push({
name: x.name,
type: x.type,
size: chunks.reduce((sum, chunk) => sum + chunk.length, 0),
});
}
}

expect(files).toEqual([
{ name: "video.mp4", type: "video/mp4", size: expect.any(Number) },
]);
});
});
Loading