@ascegu/teamily 1.0.28 → 1.0.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascegu/teamily",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
package/src/monitor.ts CHANGED
@@ -7,6 +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
11
 
11
12
  export type TeamilyMessageHandler = (message: TeamilyMessage) => Promise<void> | void;
12
13
  export type TeamilyConnectionState = "connecting" | "connected" | "disconnected" | "error";
@@ -305,11 +306,13 @@ export class TeamilyMonitor {
305
306
  const snapshotFile = new File([new Uint8Array(MINIMAL_JPEG)], "snapshot.jpg", {
306
307
  type: "image/jpeg",
307
308
  });
308
- // SDK expects a bare extension ("mp4"), not a MIME type ("video/mp4").
309
+ // Extract real duration/dimensions from the MP4 container so clients
310
+ // can play the video inline instead of showing a broken player.
311
+ const meta = parseVideoMeta(buffer);
309
312
  const videoExt = fileName.split(".").pop()?.toLowerCase() || "mp4";
310
313
  const created = await sdk.createVideoMessageByFile({
311
314
  videoPath: "",
312
- duration: 0,
315
+ duration: meta.duration,
313
316
  videoType: videoExt,
314
317
  snapshotPath: "",
315
318
  videoUUID: crypto.randomUUID(),
@@ -318,8 +321,8 @@ export class TeamilyMonitor {
318
321
  snapshotUUID: crypto.randomUUID(),
319
322
  snapshotSize: MINIMAL_JPEG.length,
320
323
  snapshotUrl: "",
321
- snapshotWidth: 1,
322
- snapshotHeight: 1,
324
+ snapshotWidth: meta.width || 1,
325
+ snapshotHeight: meta.height || 1,
323
326
  videoFile,
324
327
  snapshotFile,
325
328
  });
package/src/upload.ts CHANGED
@@ -117,3 +117,106 @@ export function guessContentType(filePath: string): string {
117
117
  };
118
118
  return map[ext] ?? "application/octet-stream";
119
119
  }
120
+
121
+ // ---- MP4 metadata extraction (no external dependencies) ----
122
+
123
+ export interface VideoMeta {
124
+ duration: number; // seconds
125
+ width: number;
126
+ height: number;
127
+ }
128
+
129
+ /**
130
+ * Parse MP4/MOV container to extract duration, width, and height.
131
+ * Returns zeroed metadata on parse failure (non-MP4, truncated, etc.).
132
+ */
133
+ export function parseVideoMeta(buf: Buffer): VideoMeta {
134
+ const fallback: VideoMeta = { duration: 0, width: 0, height: 0 };
135
+ try {
136
+ const moov = findBox(buf, 0, buf.length, "moov");
137
+ if (!moov) return fallback;
138
+
139
+ let duration = 0;
140
+ let width = 0;
141
+ let height = 0;
142
+
143
+ // mvhd → duration & timescale
144
+ const mvhd = findBox(buf, moov.dataStart, moov.end, "mvhd");
145
+ if (mvhd) {
146
+ const ver = buf[mvhd.dataStart];
147
+ if (ver === 0) {
148
+ const timescale = buf.readUInt32BE(mvhd.dataStart + 12);
149
+ const dur = buf.readUInt32BE(mvhd.dataStart + 16);
150
+ if (timescale > 0) duration = dur / timescale;
151
+ } else if (ver === 1) {
152
+ const timescale = buf.readUInt32BE(mvhd.dataStart + 20);
153
+ // 64-bit duration: read high and low 32-bit parts
154
+ const durHi = buf.readUInt32BE(mvhd.dataStart + 24);
155
+ const durLo = buf.readUInt32BE(mvhd.dataStart + 28);
156
+ const dur = durHi * 0x100000000 + durLo;
157
+ if (timescale > 0) duration = dur / timescale;
158
+ }
159
+ }
160
+
161
+ // trak → tkhd → width & height (use first video track)
162
+ let offset = moov.dataStart;
163
+ while (offset < moov.end) {
164
+ const trak = findBox(buf, offset, moov.end, "trak");
165
+ if (!trak) break;
166
+ const tkhd = findBox(buf, trak.dataStart, trak.end, "tkhd");
167
+ if (tkhd) {
168
+ const ver = buf[tkhd.dataStart];
169
+ // width/height are fixed-point 16.16 at the end of tkhd
170
+ const whOffset = ver === 0 ? tkhd.dataStart + 76 : tkhd.dataStart + 88;
171
+ if (whOffset + 8 <= tkhd.end) {
172
+ const w = buf.readUInt32BE(whOffset) >> 16;
173
+ const h = buf.readUInt32BE(whOffset + 4) >> 16;
174
+ if (w > 0 && h > 0) {
175
+ width = w;
176
+ height = h;
177
+ break; // first video track is enough
178
+ }
179
+ }
180
+ }
181
+ offset = trak.end;
182
+ }
183
+
184
+ return { duration: Math.round(duration), width, height };
185
+ } catch {
186
+ return fallback;
187
+ }
188
+ }
189
+
190
+ interface BoxInfo {
191
+ dataStart: number; // offset right after the 8-byte header
192
+ end: number; // first byte past this box
193
+ }
194
+
195
+ /** Find a box by type within [start, end) range. */
196
+ function findBox(
197
+ buf: Buffer,
198
+ start: number,
199
+ end: number,
200
+ type: string,
201
+ ): BoxInfo | null {
202
+ let offset = start;
203
+ while (offset + 8 <= end) {
204
+ let size = buf.readUInt32BE(offset);
205
+ const boxType = buf.toString("ascii", offset + 4, offset + 8);
206
+ let headerSize = 8;
207
+ if (size === 1 && offset + 16 <= end) {
208
+ // 64-bit extended size
209
+ const hi = buf.readUInt32BE(offset + 8);
210
+ const lo = buf.readUInt32BE(offset + 12);
211
+ size = hi * 0x100000000 + lo;
212
+ headerSize = 16;
213
+ }
214
+ if (size < headerSize) break; // invalid
215
+ const boxEnd = offset + size;
216
+ if (boxType === type) {
217
+ return { dataStart: offset + headerSize, end: Math.min(boxEnd, end) };
218
+ }
219
+ offset = boxEnd;
220
+ }
221
+ return null;
222
+ }