ai-cli 0.1.1 → 0.2.1

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/src/lib/kitty.ts DELETED
@@ -1,55 +0,0 @@
1
- const SUPPORTED_TERMS = new Set(["xterm-kitty"]);
2
-
3
- const SUPPORTED_TERM_PROGRAMS = new Set([
4
- "kitty",
5
- "ghostty",
6
- "wezterm",
7
- "warpterminal",
8
- ]);
9
-
10
- export function supportsKittyGraphics(): boolean {
11
- if (process.env.AI_CLI_PREVIEW === "0") return false;
12
- if (process.env.AI_CLI_PREVIEW === "1") return true;
13
-
14
- const term = process.env.TERM ?? "";
15
- if (SUPPORTED_TERMS.has(term)) return true;
16
-
17
- const termProgram = (process.env.TERM_PROGRAM ?? "").toLowerCase();
18
- if (SUPPORTED_TERM_PROGRAMS.has(termProgram)) return true;
19
-
20
- const lcTerminal = (process.env.LC_TERMINAL ?? "").toLowerCase();
21
- if (lcTerminal === "iterm2") return true;
22
-
23
- return false;
24
- }
25
-
26
- import { decodeIDR } from "./h264-wasm.js";
27
- import { extractKeyframe } from "./mp4.js";
28
- import { encodePNG } from "./png.js";
29
-
30
- const CHUNK_SIZE = 4096;
31
-
32
- export async function displayVideoFrame(buf: Buffer): Promise<void> {
33
- try {
34
- const kf = extractKeyframe(new Uint8Array(buf));
35
- if (!kf) return;
36
- const frame = await decodeIDR(kf.sps, kf.pps, kf.sliceData);
37
- if (!frame) return;
38
- const png = encodePNG(frame.yuv, frame.width, frame.height);
39
- displayImage(png);
40
- } catch {
41
- // Preview is best-effort; skip silently on any failure
42
- }
43
- }
44
-
45
- export function displayImage(buf: Buffer): void {
46
- const encoded = buf.toString("base64");
47
- for (let i = 0; i < encoded.length; i += CHUNK_SIZE) {
48
- const chunk = encoded.slice(i, i + CHUNK_SIZE);
49
- const isLast = i + CHUNK_SIZE >= encoded.length;
50
- const control =
51
- i === 0 ? `a=T,f=100,m=${isLast ? 0 : 1}` : `m=${isLast ? 0 : 1}`;
52
- process.stderr.write(`\x1b_G${control};${chunk}\x1b\\`);
53
- }
54
- process.stderr.write("\n");
55
- }
@@ -1,197 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- import {
4
- resolveModels,
5
- fetchGatewayModels,
6
- FALLBACK_TEXT_MODELS,
7
- FALLBACK_IMAGE_MODELS,
8
- FALLBACK_VIDEO_MODELS,
9
- } from "./models.js";
10
-
11
- describe("resolveModels", () => {
12
- test("returns default when no user model", () => {
13
- expect(resolveModels("text")[0]).toContain("/");
14
- expect(resolveModels("image")[0]).toContain("/");
15
- expect(resolveModels("video")[0]).toContain("/");
16
- });
17
-
18
- test("returns fully-qualified model as-is", () => {
19
- expect(resolveModels("text", "openai/gpt-4")).toEqual(["openai/gpt-4"]);
20
- expect(resolveModels("image", "openai/gpt-image-1")).toEqual([
21
- "openai/gpt-image-1",
22
- ]);
23
- });
24
-
25
- test("expands short image model names", () => {
26
- expect(resolveModels("image", "gpt-image-1")).toEqual([
27
- "openai/gpt-image-1",
28
- ]);
29
- expect(resolveModels("image", "flux-2-pro")).toEqual(["bfl/flux-2-pro"]);
30
- });
31
-
32
- test("expands short video model names", () => {
33
- expect(resolveModels("video", "seedance-2.0")).toEqual([
34
- "bytedance/seedance-2.0",
35
- ]);
36
- });
37
-
38
- test("expands short text model names", () => {
39
- expect(resolveModels("text", "gpt-5.5")).toEqual(["openai/gpt-5.5"]);
40
- expect(resolveModels("text", "o3")).toEqual(["openai/o3"]);
41
- });
42
-
43
- test("returns unknown short names as-is for text", () => {
44
- expect(resolveModels("text", "gpt-image-1")).toEqual(["gpt-image-1"]);
45
- expect(resolveModels("text", "my-model")).toEqual(["my-model"]);
46
- });
47
-
48
- test("returns unknown short names as-is for image/video", () => {
49
- expect(resolveModels("image", "nonexistent-model")).toEqual([
50
- "nonexistent-model",
51
- ]);
52
- });
53
- });
54
-
55
- describe("resolveModels multi", () => {
56
- test("returns default when no user model", () => {
57
- const result = resolveModels("text");
58
- expect(result).toHaveLength(1);
59
- expect(result[0]).toContain("/");
60
- });
61
-
62
- test("splits comma-separated models", () => {
63
- const result = resolveModels("image", "openai/gpt-image-1,bfl/flux-2-pro");
64
- expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
65
- });
66
-
67
- test("trims whitespace around model names", () => {
68
- const result = resolveModels(
69
- "image",
70
- "openai/gpt-image-1 , bfl/flux-2-pro"
71
- );
72
- expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
73
- });
74
-
75
- test("expands short names in comma list", () => {
76
- const result = resolveModels("image", "gpt-image-1,flux-2-pro");
77
- expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
78
- });
79
-
80
- test("filters empty segments from trailing comma", () => {
81
- const result = resolveModels("image", "openai/gpt-image-1,");
82
- expect(result).toEqual(["openai/gpt-image-1"]);
83
- });
84
-
85
- test("falls back to default when all segments are empty", () => {
86
- const result = resolveModels("image", ",,,");
87
- expect(result).toHaveLength(1);
88
- expect(result[0]).toContain("/");
89
- });
90
- });
91
-
92
- describe("fetchGatewayModels", () => {
93
- test("partitions models by modelType", async () => {
94
- const { gateway } = await import("ai");
95
- const original = gateway.getAvailableModels;
96
- gateway.getAvailableModels = mock(() =>
97
- Promise.resolve({
98
- models: [
99
- { id: "openai/gpt-5", name: "GPT 5", modelType: "language" },
100
- {
101
- id: "openai/gpt-image-2",
102
- name: "GPT Image 2",
103
- description: "Image gen",
104
- modelType: "image",
105
- },
106
- { id: "google/veo-3.0", name: "Veo 3", modelType: "video" },
107
- {
108
- id: "openai/text-embedding-3",
109
- name: "Embedding",
110
- modelType: "embedding",
111
- },
112
- ],
113
- })
114
- ) as unknown as typeof gateway.getAvailableModels;
115
-
116
- try {
117
- const result = await fetchGatewayModels();
118
-
119
- expect(result.text).toEqual([
120
- { id: "openai/gpt-5", name: "GPT 5", description: undefined },
121
- ]);
122
- expect(result.image).toEqual([
123
- {
124
- id: "openai/gpt-image-2",
125
- name: "GPT Image 2",
126
- description: "Image gen",
127
- },
128
- ]);
129
- expect(result.video).toEqual([
130
- { id: "google/veo-3.0", name: "Veo 3", description: undefined },
131
- ]);
132
- } finally {
133
- gateway.getAvailableModels = original;
134
- }
135
- });
136
-
137
- test("falls back to static lists on gateway error", async () => {
138
- const { gateway } = await import("ai");
139
- const original = gateway.getAvailableModels;
140
- gateway.getAvailableModels = mock(() =>
141
- Promise.reject(new Error("network error"))
142
- ) as typeof gateway.getAvailableModels;
143
-
144
- try {
145
- const result = await fetchGatewayModels();
146
-
147
- expect(result.text.map((m) => m.id)).toEqual(FALLBACK_TEXT_MODELS);
148
- expect(result.image.map((m) => m.id)).toEqual(FALLBACK_IMAGE_MODELS);
149
- expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
150
- } finally {
151
- gateway.getAvailableModels = original;
152
- }
153
- });
154
-
155
- test("uses fallbacks when gateway returns no image/video models", async () => {
156
- const { gateway } = await import("ai");
157
- const original = gateway.getAvailableModels;
158
- gateway.getAvailableModels = mock(() =>
159
- Promise.resolve({
160
- models: [{ id: "openai/gpt-5", name: "GPT 5", modelType: "language" }],
161
- })
162
- ) as unknown as typeof gateway.getAvailableModels;
163
-
164
- try {
165
- const result = await fetchGatewayModels();
166
-
167
- expect(result.text).toHaveLength(1);
168
- expect(result.text[0].id).toBe("openai/gpt-5");
169
- expect(result.image.map((m) => m.id)).toEqual(FALLBACK_IMAGE_MODELS);
170
- expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
171
- } finally {
172
- gateway.getAvailableModels = original;
173
- }
174
- });
175
-
176
- test("uses text fallbacks when gateway returns no text models", async () => {
177
- const { gateway } = await import("ai");
178
- const original = gateway.getAvailableModels;
179
- gateway.getAvailableModels = mock(() =>
180
- Promise.resolve({
181
- models: [
182
- { id: "openai/gpt-image-2", name: "GPT Image 2", modelType: "image" },
183
- ],
184
- })
185
- ) as unknown as typeof gateway.getAvailableModels;
186
-
187
- try {
188
- const result = await fetchGatewayModels();
189
-
190
- expect(result.text.map((m) => m.id)).toEqual(FALLBACK_TEXT_MODELS);
191
- expect(result.image).toHaveLength(1);
192
- expect(result.video.map((m) => m.id)).toEqual(FALLBACK_VIDEO_MODELS);
193
- } finally {
194
- gateway.getAvailableModels = original;
195
- }
196
- });
197
- });
package/src/lib/models.ts DELETED
@@ -1,163 +0,0 @@
1
- import { gateway } from "ai";
2
-
3
- export type Modality = "text" | "image" | "video";
4
-
5
- const DEFAULTS: Record<Modality, string> = {
6
- text: process.env.AI_CLI_TEXT_MODEL ?? "openai/gpt-5.5",
7
- image: process.env.AI_CLI_IMAGE_MODEL ?? "openai/gpt-image-2",
8
- video: process.env.AI_CLI_VIDEO_MODEL ?? "bytedance/seedance-2.0",
9
- };
10
-
11
- export const FALLBACK_TEXT_MODELS = [
12
- "anthropic/claude-sonnet-4",
13
- "google/gemini-2.5-pro",
14
- "meta/llama-4-maverick",
15
- "openai/gpt-4.1",
16
- "openai/gpt-4.1-mini",
17
- "openai/gpt-4.1-nano",
18
- "openai/gpt-5.5",
19
- "openai/o3",
20
- "openai/o4-mini",
21
- "xai/grok-3",
22
- ];
23
-
24
- export const FALLBACK_IMAGE_MODELS = [
25
- "bfl/flux-2-flex",
26
- "bfl/flux-2-klein-4b",
27
- "bfl/flux-2-klein-9b",
28
- "bfl/flux-2-max",
29
- "bfl/flux-2-pro",
30
- "bfl/flux-kontext-max",
31
- "bfl/flux-kontext-pro",
32
- "bfl/flux-pro-1.0-fill",
33
- "bfl/flux-pro-1.1",
34
- "bfl/flux-pro-1.1-ultra",
35
- "bytedance/seedream-4.0",
36
- "bytedance/seedream-4.5",
37
- "bytedance/seedream-5.0-lite",
38
- "google/imagen-4.0-fast-generate-001",
39
- "google/imagen-4.0-generate-001",
40
- "google/imagen-4.0-ultra-generate-001",
41
- "openai/gpt-image-1",
42
- "openai/gpt-image-1-mini",
43
- "openai/gpt-image-1.5",
44
- "openai/gpt-image-2",
45
- "prodia/flux-fast-schnell",
46
- "recraft/recraft-v2",
47
- "recraft/recraft-v3",
48
- "recraft/recraft-v4",
49
- "recraft/recraft-v4-pro",
50
- "xai/grok-imagine-image",
51
- "xai/grok-imagine-image-pro",
52
- ];
53
-
54
- export const FALLBACK_VIDEO_MODELS = [
55
- "alibaba/wan-v2.5-t2v-preview",
56
- "alibaba/wan-v2.6-i2v",
57
- "alibaba/wan-v2.6-i2v-flash",
58
- "alibaba/wan-v2.6-r2v",
59
- "alibaba/wan-v2.6-r2v-flash",
60
- "alibaba/wan-v2.6-t2v",
61
- "bytedance/seedance-2.0",
62
- "bytedance/seedance-2.0-fast",
63
- "bytedance/seedance-v1.0-lite-i2v",
64
- "bytedance/seedance-v1.0-lite-t2v",
65
- "bytedance/seedance-v1.0-pro",
66
- "bytedance/seedance-v1.0-pro-fast",
67
- "bytedance/seedance-v1.5-pro",
68
- "google/veo-3.0-fast-generate-001",
69
- "google/veo-3.0-generate-001",
70
- "google/veo-3.1-fast-generate-001",
71
- "google/veo-3.1-generate-001",
72
- "klingai/kling-v2.5-turbo-i2v",
73
- "klingai/kling-v2.5-turbo-t2v",
74
- "klingai/kling-v2.6-i2v",
75
- "klingai/kling-v2.6-motion-control",
76
- "klingai/kling-v2.6-t2v",
77
- "klingai/kling-v3.0-i2v",
78
- "klingai/kling-v3.0-t2v",
79
- "xai/grok-imagine-video",
80
- ];
81
-
82
- export interface ModelEntry {
83
- id: string;
84
- name?: string;
85
- description?: string;
86
- }
87
-
88
- export interface GatewayModels {
89
- text: ModelEntry[];
90
- image: ModelEntry[];
91
- video: ModelEntry[];
92
- }
93
-
94
- const MODEL_TYPE_TO_MODALITY: Record<string, Modality> = {
95
- language: "text",
96
- image: "image",
97
- video: "video",
98
- };
99
-
100
- export async function fetchGatewayModels(): Promise<GatewayModels> {
101
- const result: GatewayModels = { text: [], image: [], video: [] };
102
-
103
- try {
104
- const { models } = await gateway.getAvailableModels();
105
- for (const m of models) {
106
- const modality =
107
- MODEL_TYPE_TO_MODALITY[(m as { modelType?: string }).modelType ?? ""];
108
- if (!modality) continue;
109
- result[modality].push({
110
- id: m.id,
111
- name: m.name,
112
- description: m.description ?? undefined,
113
- });
114
- }
115
- } catch {
116
- result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
117
- result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
118
- result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
119
- }
120
-
121
- if (result.text.length === 0) {
122
- result.text = FALLBACK_TEXT_MODELS.map((id) => ({ id }));
123
- }
124
- if (result.image.length === 0) {
125
- result.image = FALLBACK_IMAGE_MODELS.map((id) => ({ id }));
126
- }
127
- if (result.video.length === 0) {
128
- result.video = FALLBACK_VIDEO_MODELS.map((id) => ({ id }));
129
- }
130
-
131
- return result;
132
- }
133
-
134
- function expandModelId(input: string, modality: Modality): string {
135
- if (input.includes("/")) return input;
136
-
137
- const knownLists: string[][] = [];
138
- if (modality === "text") knownLists.push(FALLBACK_TEXT_MODELS);
139
- else if (modality === "image") knownLists.push(FALLBACK_IMAGE_MODELS);
140
- else if (modality === "video") knownLists.push(FALLBACK_VIDEO_MODELS);
141
-
142
- for (const list of knownLists) {
143
- for (const fullId of list) {
144
- const name = fullId.slice(fullId.indexOf("/") + 1);
145
- if (name === input) return fullId;
146
- }
147
- }
148
-
149
- return input;
150
- }
151
-
152
- export function resolveModels(
153
- modality: Modality,
154
- userModel?: string
155
- ): string[] {
156
- if (!userModel) return [DEFAULTS[modality]];
157
- const models = userModel
158
- .split(",")
159
- .map((m) => m.trim())
160
- .filter(Boolean)
161
- .map((m) => expandModelId(m, modality));
162
- return models.length > 0 ? models : [DEFAULTS[modality]];
163
- }
@@ -1,231 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import { extractKeyframe } from "./mp4.js";
4
-
5
- describe("extractKeyframe", () => {
6
- test("returns null for empty buffer", () => {
7
- expect(extractKeyframe(new Uint8Array(0))).toBeNull();
8
- });
9
-
10
- test("returns null for buffer too small", () => {
11
- expect(extractKeyframe(new Uint8Array(4))).toBeNull();
12
- });
13
-
14
- test("returns null when no moov box found", () => {
15
- // Valid-looking box header but wrong type
16
- const buf = new Uint8Array(16);
17
- const view = new DataView(buf.buffer);
18
- view.setUint32(0, 16);
19
- buf[4] = 0x66;
20
- buf[5] = 0x74;
21
- buf[6] = 0x79;
22
- buf[7] = 0x70; // ftyp
23
- expect(extractKeyframe(buf)).toBeNull();
24
- });
25
-
26
- test("returns null for minimal moov with no video track", () => {
27
- // Construct a minimal moov box with no trak inside
28
- const moovPayload = new Uint8Array(0);
29
- const moovSize = 8 + moovPayload.length;
30
- const buf = new Uint8Array(moovSize);
31
- const view = new DataView(buf.buffer);
32
- view.setUint32(0, moovSize);
33
- buf[4] = 0x6d;
34
- buf[5] = 0x6f;
35
- buf[6] = 0x6f;
36
- buf[7] = 0x76; // moov
37
- expect(extractKeyframe(buf)).toBeNull();
38
- });
39
-
40
- test("builds a minimal MP4 structure and extracts keyframe data", () => {
41
- // This test creates a valid-ish MP4 structure to exercise the parsing path.
42
- // The actual H.264 data is minimal/dummy, so we just verify the parser
43
- // successfully extracts SPS, PPS, and slice data.
44
- const mp4 = buildMinimalMP4();
45
- const result = extractKeyframe(mp4);
46
-
47
- // If the MP4 was well-formed enough, we should get non-null result
48
- if (result) {
49
- expect(result.sps.length).toBeGreaterThan(0);
50
- expect(result.pps.length).toBeGreaterThan(0);
51
- expect(result.sliceData.length).toBeGreaterThan(0);
52
- }
53
- });
54
- });
55
-
56
- function writeBox(type: string, payload: Uint8Array): Uint8Array {
57
- const size = 8 + payload.length;
58
- const buf = new Uint8Array(size);
59
- const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
60
- view.setUint32(0, size);
61
- for (let i = 0; i < 4; i++) buf[4 + i] = type.charCodeAt(i);
62
- buf.set(payload, 8);
63
- return buf;
64
- }
65
-
66
- function concatArrays(...arrs: Uint8Array[]): Uint8Array {
67
- const total = arrs.reduce((s, a) => s + a.length, 0);
68
- const result = new Uint8Array(total);
69
- let off = 0;
70
- for (const a of arrs) {
71
- result.set(a, off);
72
- off += a.length;
73
- }
74
- return result;
75
- }
76
-
77
- function buildMinimalMP4(): Uint8Array {
78
- // SPS NAL unit (minimal baseline profile, 16x16 resolution)
79
- const sps = new Uint8Array([
80
- 0x67, 0x42, 0x00, 0x0a, 0xe9, 0x40, 0x40, 0x04, 0x00, 0x00, 0x00, 0x04,
81
- 0x00, 0x00, 0x00, 0xc8, 0x40,
82
- ]);
83
- const pps = new Uint8Array([0x68, 0xce, 0x38, 0x80]);
84
-
85
- // avcC box
86
- const avcCPayload = new Uint8Array([
87
- 1, // version
88
- 0x42, // profile
89
- 0x00, // compat
90
- 0x0a, // level
91
- 0xff, // nal length size = 4
92
- 0xe1, // num SPS = 1
93
- // SPS length + data
94
- (sps.length >> 8) & 0xff,
95
- sps.length & 0xff,
96
- ...sps,
97
- 1, // num PPS = 1
98
- (pps.length >> 8) & 0xff,
99
- pps.length & 0xff,
100
- ...pps,
101
- ]);
102
- const avcC = writeBox("avcC", avcCPayload);
103
-
104
- // avc1 box: 78 bytes fixed header + avcC child
105
- const avc1Fixed = new Uint8Array(78);
106
- // data_ref_index at offset 6 = 1
107
- avc1Fixed[6] = 0;
108
- avc1Fixed[7] = 1;
109
- // width at offset 24
110
- avc1Fixed[24] = 0;
111
- avc1Fixed[25] = 16;
112
- // height at offset 26
113
- avc1Fixed[26] = 0;
114
- avc1Fixed[27] = 16;
115
- const avc1Payload = concatArrays(avc1Fixed, avcC);
116
- const avc1 = writeBox("avc1", avc1Payload);
117
-
118
- // stsd box: version(4) + entry_count(4) + avc1
119
- const stsdInner = new Uint8Array(8);
120
- const stsdView = new DataView(stsdInner.buffer);
121
- stsdView.setUint32(4, 1); // entry count
122
- const stsd = writeBox("stsd", concatArrays(stsdInner, avc1));
123
-
124
- // stsz (1 sample, size 10)
125
- const stszPayload = new Uint8Array(12);
126
- const stszView = new DataView(stszPayload.buffer);
127
- stszView.setUint32(4, 0); // sample_size = variable
128
- stszView.setUint32(8, 1); // sample_count = 1
129
- const stszEntry = new Uint8Array(4);
130
- new DataView(stszEntry.buffer).setUint32(0, 10);
131
- const stsz = writeBox("stsz", concatArrays(stszPayload, stszEntry));
132
-
133
- // stsc (1 entry: chunk 1, 1 sample/chunk, sdi 1)
134
- const stscPayload = new Uint8Array(16);
135
- const stscView = new DataView(stscPayload.buffer);
136
- stscView.setUint32(4, 1); // entry count
137
- stscView.setUint32(8, 1); // first chunk
138
- stscView.setUint32(12, 1); // samples per chunk
139
- const stscExtra = new Uint8Array(4);
140
- new DataView(stscExtra.buffer).setUint32(0, 1);
141
- const stsc = writeBox("stsc", concatArrays(stscPayload, stscExtra));
142
-
143
- // stco (1 chunk offset, will be filled in later)
144
- const stcoPayload = new Uint8Array(8);
145
- const stcoView = new DataView(stcoPayload.buffer);
146
- stcoView.setUint32(4, 1); // entry count
147
- // offset will be set after we know the layout
148
- const stcoOffsetPos = 8; // position within stco payload for the offset value
149
- const stco = writeBox("stco", concatArrays(stcoPayload, new Uint8Array(4)));
150
-
151
- // stss (1 sync sample: sample 1)
152
- const stssPayload = new Uint8Array(8);
153
- const stssView = new DataView(stssPayload.buffer);
154
- stssView.setUint32(4, 1); // entry count
155
- const stssSample = new Uint8Array(4);
156
- new DataView(stssSample.buffer).setUint32(0, 1);
157
- const stss = writeBox("stss", concatArrays(stssPayload, stssSample));
158
-
159
- const stbl = writeBox("stbl", concatArrays(stsd, stsz, stsc, stco, stss));
160
- const minf = writeBox("minf", stbl);
161
-
162
- // hdlr box (video handler)
163
- const hdlrPayload = new Uint8Array(20);
164
- // handler_type at offset 8..11 = "vide"
165
- hdlrPayload[8] = 0x76;
166
- hdlrPayload[9] = 0x69;
167
- hdlrPayload[10] = 0x64;
168
- hdlrPayload[11] = 0x65;
169
- const hdlr = writeBox("hdlr", hdlrPayload);
170
-
171
- const mdia = writeBox("mdia", concatArrays(hdlr, minf));
172
- const trak = writeBox("trak", mdia);
173
- const moov = writeBox("moov", trak);
174
-
175
- // mdat with a dummy IDR NAL unit (length-prefixed)
176
- const idrNal = new Uint8Array([0x65, 0x88, 0x80, 0x40, 0x00, 0x00]);
177
- const mdatPayload = new Uint8Array(4 + idrNal.length);
178
- new DataView(mdatPayload.buffer).setUint32(0, idrNal.length);
179
- mdatPayload.set(idrNal, 4);
180
-
181
- // ftyp box
182
- const ftyp = writeBox(
183
- "ftyp",
184
- new Uint8Array([
185
- 0x69,
186
- 0x73,
187
- 0x6f,
188
- 0x6d, // brand: isom
189
- 0x00,
190
- 0x00,
191
- 0x02,
192
- 0x00, // version
193
- ])
194
- );
195
-
196
- const mdat = writeBox("mdat", mdatPayload);
197
-
198
- // Calculate chunk offset
199
- const mdatOffset = ftyp.length + moov.length + 8; // 8 for mdat header
200
- const fullMp4 = concatArrays(ftyp, moov, mdat);
201
-
202
- // Patch the stco chunk offset
203
- // We need to find the stco entry in the final buffer and patch it
204
- const stcoMarker = findStcoOffset(fullMp4);
205
- if (stcoMarker >= 0) {
206
- const view = new DataView(
207
- fullMp4.buffer,
208
- fullMp4.byteOffset,
209
- fullMp4.byteLength
210
- );
211
- view.setUint32(stcoMarker, mdatOffset);
212
- }
213
-
214
- return fullMp4;
215
- }
216
-
217
- function findStcoOffset(buf: Uint8Array): number {
218
- // Find the stco box and return the position of its first chunk offset entry
219
- for (let i = 0; i < buf.length - 12; i++) {
220
- if (
221
- buf[i] === 0x73 &&
222
- buf[i + 1] === 0x74 &&
223
- buf[i + 2] === 0x63 &&
224
- buf[i + 3] === 0x6f
225
- ) {
226
- // Found "stco" - the chunk offset entry is at i + 4 (version/flags) + 4 (entry_count) + 4
227
- return i + 4 + 8;
228
- }
229
- }
230
- return -1;
231
- }