ai-cli 0.0.12 → 0.1.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.
@@ -0,0 +1,231 @@
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
+ }