@ascegu/teamily 1.0.30 → 1.0.32
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/package.json +1 -1
- package/src/monitor.ts +6 -3
- package/src/upload.ts +64 -25
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -308,7 +308,10 @@ export class TeamilyMonitor {
|
|
|
308
308
|
|
|
309
309
|
// Try to extract a real first-frame snapshot via ffmpeg; fall back to
|
|
310
310
|
// a minimal 1x1 JPEG placeholder if ffmpeg is unavailable.
|
|
311
|
-
const
|
|
311
|
+
const snapshot = await extractVideoSnapshot(buffer);
|
|
312
|
+
const snapshotBuf = snapshot?.buffer ?? Buffer.from(MINIMAL_JPEG);
|
|
313
|
+
const snapWidth = snapshot?.width || meta.width || 1;
|
|
314
|
+
const snapHeight = snapshot?.height || meta.height || 1;
|
|
312
315
|
const snapshotFile = new File([new Uint8Array(snapshotBuf)], "snapshot.jpg", {
|
|
313
316
|
type: "image/jpeg",
|
|
314
317
|
});
|
|
@@ -325,8 +328,8 @@ export class TeamilyMonitor {
|
|
|
325
328
|
snapshotUUID: crypto.randomUUID(),
|
|
326
329
|
snapshotSize: snapshotBuf.length,
|
|
327
330
|
snapshotUrl: "",
|
|
328
|
-
snapshotWidth:
|
|
329
|
-
snapshotHeight:
|
|
331
|
+
snapshotWidth: snapWidth,
|
|
332
|
+
snapshotHeight: snapHeight,
|
|
330
333
|
videoFile,
|
|
331
334
|
snapshotFile,
|
|
332
335
|
});
|
package/src/upload.ts
CHANGED
|
@@ -125,39 +125,53 @@ export function guessContentType(filePath: string): string {
|
|
|
125
125
|
* Returns null if ffmpeg is unavailable or extraction fails.
|
|
126
126
|
* The thumbnail is scaled down to fit within maxDim (default 320px).
|
|
127
127
|
*/
|
|
128
|
+
export interface SnapshotResult {
|
|
129
|
+
buffer: Buffer;
|
|
130
|
+
width: number;
|
|
131
|
+
height: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
128
134
|
export async function extractVideoSnapshot(
|
|
129
135
|
videoBuf: Buffer,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const { tmpdir } = await import("node:os");
|
|
137
|
-
const execFileAsync = promisify(execFile);
|
|
136
|
+
): Promise<SnapshotResult | null> {
|
|
137
|
+
const { execFile } = await import("node:child_process");
|
|
138
|
+
const { promisify } = await import("node:util");
|
|
139
|
+
const { writeFile, readFile, unlink, mkdtemp } = await import("node:fs/promises");
|
|
140
|
+
const { tmpdir } = await import("node:os");
|
|
141
|
+
const execFileAsync = promisify(execFile);
|
|
138
142
|
|
|
139
|
-
|
|
143
|
+
let dir: string | undefined;
|
|
144
|
+
try {
|
|
145
|
+
dir = await mkdtemp(path.join(tmpdir(), "oc-snap-"));
|
|
140
146
|
const inPath = path.join(dir, "input.mp4");
|
|
141
147
|
const outPath = path.join(dir, "snapshot.jpg");
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await unlink(outPath).catch(() => {});
|
|
156
|
-
const { rmdir } = await import("node:fs/promises");
|
|
157
|
-
await rmdir(dir).catch(() => {});
|
|
149
|
+
await writeFile(inPath, videoBuf);
|
|
150
|
+
await execFileAsync("ffmpeg", [
|
|
151
|
+
"-y",
|
|
152
|
+
"-i", inPath,
|
|
153
|
+
"-frames:v", "1",
|
|
154
|
+
outPath,
|
|
155
|
+
], { timeout: 15_000 });
|
|
156
|
+
|
|
157
|
+
const snap = await readFile(outPath);
|
|
158
|
+
if (snap.length === 0) {
|
|
159
|
+
console.warn("[teamily] ffmpeg produced empty snapshot");
|
|
160
|
+
return null;
|
|
158
161
|
}
|
|
159
|
-
|
|
162
|
+
const dims = parseJpegDimensions(snap);
|
|
163
|
+
console.info(
|
|
164
|
+
`[teamily] extracted video snapshot: ${snap.length} bytes, ${dims.width}x${dims.height}`,
|
|
165
|
+
);
|
|
166
|
+
return { buffer: snap, width: dims.width, height: dims.height };
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.warn("[teamily] ffmpeg snapshot extraction failed:", err);
|
|
160
169
|
return null;
|
|
170
|
+
} finally {
|
|
171
|
+
if (dir) {
|
|
172
|
+
const { rm } = await import("node:fs/promises");
|
|
173
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
174
|
+
}
|
|
161
175
|
}
|
|
162
176
|
}
|
|
163
177
|
|
|
@@ -263,3 +277,28 @@ function findBox(
|
|
|
263
277
|
}
|
|
264
278
|
return null;
|
|
265
279
|
}
|
|
280
|
+
|
|
281
|
+
/** Parse width/height from a JPEG buffer by scanning for SOF markers. */
|
|
282
|
+
function parseJpegDimensions(buf: Buffer): { width: number; height: number } {
|
|
283
|
+
let i = 0;
|
|
284
|
+
while (i + 1 < buf.length) {
|
|
285
|
+
if (buf[i] !== 0xff) { i++; continue; }
|
|
286
|
+
const marker = buf[i + 1];
|
|
287
|
+
// SOF0–SOF3 (0xC0–0xC3): baseline, progressive, etc.
|
|
288
|
+
if (marker >= 0xc0 && marker <= 0xc3 && i + 9 < buf.length) {
|
|
289
|
+
const height = buf.readUInt16BE(i + 5);
|
|
290
|
+
const width = buf.readUInt16BE(i + 7);
|
|
291
|
+
return { width, height };
|
|
292
|
+
}
|
|
293
|
+
// Skip marker segment (length follows the 2-byte marker)
|
|
294
|
+
if (marker === 0xd8 || marker === 0xd9) {
|
|
295
|
+
i += 2; // SOI/EOI have no payload
|
|
296
|
+
} else if (i + 3 < buf.length) {
|
|
297
|
+
const len = buf.readUInt16BE(i + 2);
|
|
298
|
+
i += 2 + len;
|
|
299
|
+
} else {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { width: 0, height: 0 };
|
|
304
|
+
}
|