@ascegu/teamily 1.0.29 → 1.0.30
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 +11 -7
- package/src/upload.ts +43 -0
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
TeamilyAudioContent,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
import { CONTENT_TYPES, SESSION_TYPES, isGroupSession } from "./types.js";
|
|
10
|
-
import { parseVideoMeta } from "./upload.js";
|
|
10
|
+
import { extractVideoSnapshot, parseVideoMeta } from "./upload.js";
|
|
11
11
|
|
|
12
12
|
export type TeamilyMessageHandler = (message: TeamilyMessage) => Promise<void> | void;
|
|
13
13
|
export type TeamilyConnectionState = "connecting" | "connected" | "disconnected" | "error";
|
|
@@ -301,14 +301,18 @@ export class TeamilyMonitor {
|
|
|
301
301
|
): Promise<string> {
|
|
302
302
|
const sdk = this.requireSdk();
|
|
303
303
|
const videoFile = new File([new Uint8Array(buffer)], fileName, { type: contentType });
|
|
304
|
-
|
|
305
|
-
// Use a minimal valid 1x1 JPEG so the SDK upload succeeds.
|
|
306
|
-
const snapshotFile = new File([new Uint8Array(MINIMAL_JPEG)], "snapshot.jpg", {
|
|
307
|
-
type: "image/jpeg",
|
|
308
|
-
});
|
|
304
|
+
|
|
309
305
|
// Extract real duration/dimensions from the MP4 container so clients
|
|
310
306
|
// can play the video inline instead of showing a broken player.
|
|
311
307
|
const meta = parseVideoMeta(buffer);
|
|
308
|
+
|
|
309
|
+
// Try to extract a real first-frame snapshot via ffmpeg; fall back to
|
|
310
|
+
// a minimal 1x1 JPEG placeholder if ffmpeg is unavailable.
|
|
311
|
+
const snapshotBuf = (await extractVideoSnapshot(buffer)) ?? Buffer.from(MINIMAL_JPEG);
|
|
312
|
+
const snapshotFile = new File([new Uint8Array(snapshotBuf)], "snapshot.jpg", {
|
|
313
|
+
type: "image/jpeg",
|
|
314
|
+
});
|
|
315
|
+
|
|
312
316
|
const videoExt = fileName.split(".").pop()?.toLowerCase() || "mp4";
|
|
313
317
|
const created = await sdk.createVideoMessageByFile({
|
|
314
318
|
videoPath: "",
|
|
@@ -319,7 +323,7 @@ export class TeamilyMonitor {
|
|
|
319
323
|
videoUrl: "",
|
|
320
324
|
videoSize: buffer.length,
|
|
321
325
|
snapshotUUID: crypto.randomUUID(),
|
|
322
|
-
snapshotSize:
|
|
326
|
+
snapshotSize: snapshotBuf.length,
|
|
323
327
|
snapshotUrl: "",
|
|
324
328
|
snapshotWidth: meta.width || 1,
|
|
325
329
|
snapshotHeight: meta.height || 1,
|
package/src/upload.ts
CHANGED
|
@@ -118,6 +118,49 @@ export function guessContentType(filePath: string): string {
|
|
|
118
118
|
return map[ext] ?? "application/octet-stream";
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// ---- Video snapshot extraction via ffmpeg ----
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract the first video frame as a JPEG thumbnail using ffmpeg.
|
|
125
|
+
* Returns null if ffmpeg is unavailable or extraction fails.
|
|
126
|
+
* The thumbnail is scaled down to fit within maxDim (default 320px).
|
|
127
|
+
*/
|
|
128
|
+
export async function extractVideoSnapshot(
|
|
129
|
+
videoBuf: Buffer,
|
|
130
|
+
maxDim = 320,
|
|
131
|
+
): Promise<Buffer | null> {
|
|
132
|
+
try {
|
|
133
|
+
const { execFile } = await import("node:child_process");
|
|
134
|
+
const { promisify } = await import("node:util");
|
|
135
|
+
const { writeFile, readFile, unlink, mkdtemp } = await import("node:fs/promises");
|
|
136
|
+
const { tmpdir } = await import("node:os");
|
|
137
|
+
const execFileAsync = promisify(execFile);
|
|
138
|
+
|
|
139
|
+
const dir = await mkdtemp(path.join(tmpdir(), "oc-snap-"));
|
|
140
|
+
const inPath = path.join(dir, "input.mp4");
|
|
141
|
+
const outPath = path.join(dir, "snapshot.jpg");
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await writeFile(inPath, videoBuf);
|
|
145
|
+
await execFileAsync("ffmpeg", [
|
|
146
|
+
"-i", inPath,
|
|
147
|
+
"-vframes", "1",
|
|
148
|
+
"-vf", `scale='min(${maxDim},iw)':'-1'`,
|
|
149
|
+
"-q:v", "6",
|
|
150
|
+
"-y", outPath,
|
|
151
|
+
], { timeout: 10_000 });
|
|
152
|
+
return await readFile(outPath);
|
|
153
|
+
} finally {
|
|
154
|
+
await unlink(inPath).catch(() => {});
|
|
155
|
+
await unlink(outPath).catch(() => {});
|
|
156
|
+
const { rmdir } = await import("node:fs/promises");
|
|
157
|
+
await rmdir(dir).catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
121
164
|
// ---- MP4 metadata extraction (no external dependencies) ----
|
|
122
165
|
|
|
123
166
|
export interface VideoMeta {
|