@uploadista/flow-videos-av-node 0.0.13 → 0.0.14

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.
@@ -1,25 +1,17 @@
1
- import { randomUUID } from "node:crypto";
2
- import { promises as fs } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
1
  import { UploadistaError } from "@uploadista/core/errors";
6
2
  import type {
7
3
  DescribeVideoMetadata,
8
4
  VideoPluginShape,
9
5
  } from "@uploadista/core/flow";
10
6
  import { Effect } from "effect";
11
- import { Decoder, Encoder, MediaInput, MediaOutput } from "node-av/api";
7
+ import { Decoder, Demuxer, Encoder, Muxer } from "node-av/api";
12
8
  import type { Packet } from "node-av/lib";
13
9
  import {
14
10
  audioCodecToAVName,
15
11
  codecToAVName,
16
12
  imageFormatToEncoder,
17
13
  } from "./utils/format-mappings";
18
- import {
19
- bytesToTempFile,
20
- cleanup,
21
- tempFileToBytes,
22
- } from "./utils/temp-file-manager";
14
+ import { createMemoryOutput } from "./utils/memory-io";
23
15
 
24
16
  /**
25
17
  * Creates a node-av based video processing plugin
@@ -29,60 +21,53 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
29
21
  describe: (input) =>
30
22
  Effect.tryPromise({
31
23
  try: async () => {
32
- const inputPath = await bytesToTempFile(input, "input");
33
-
34
- try {
35
- await using mediaInput = await MediaInput.open(inputPath);
24
+ // Convert Uint8Array to Buffer for node-av
25
+ const buffer = Buffer.from(input);
26
+ await using mediaInput = await Demuxer.open(buffer);
36
27
 
37
- const videoStream = mediaInput.video();
38
- const audioStream = mediaInput.audio();
39
-
40
- if (!videoStream) {
41
- throw new Error("No video stream found");
42
- }
28
+ const videoStream = mediaInput.video();
29
+ const audioStream = mediaInput.audio();
43
30
 
44
- const videoCodecParams = videoStream.codecpar;
31
+ if (!videoStream) {
32
+ throw new Error("No video stream found");
33
+ }
45
34
 
46
- // Calculate frame rate from rational number
47
- let frameRate = 0;
48
- if (videoStream.rFrameRate) {
49
- const { num, den } = videoStream.rFrameRate;
50
- frameRate = den ? num / den : num;
51
- }
35
+ const videoCodecParams = videoStream.codecpar;
52
36
 
53
- // Get aspect ratio
54
- let aspectRatio = "unknown";
55
- if (videoStream.sampleAspectRatio) {
56
- const { num, den } = videoStream.sampleAspectRatio;
57
- aspectRatio = `${num}:${den}`;
58
- }
37
+ // Calculate frame rate from rational number
38
+ let frameRate = 0;
39
+ if (videoStream.rFrameRate) {
40
+ const { num, den } = videoStream.rFrameRate;
41
+ frameRate = den ? num / den : num;
42
+ }
59
43
 
60
- // Get file size
61
- const stats = await fs.stat(inputPath);
62
-
63
- const metadata: DescribeVideoMetadata = {
64
- duration: mediaInput.duration || 0,
65
- width: videoCodecParams.width || 0,
66
- height: videoCodecParams.height || 0,
67
- codec: String(videoCodecParams.codecId) || "unknown",
68
- format: mediaInput.formatName || "unknown",
69
- bitrate: mediaInput.bitRate || 0,
70
- frameRate,
71
- aspectRatio,
72
- hasAudio: !!audioStream,
73
- audioCodec: audioStream?.codecpar.codecId
74
- ? String(audioStream.codecpar.codecId)
75
- : undefined,
76
- audioBitrate: audioStream?.codecpar.bitRate
77
- ? Number(audioStream.codecpar.bitRate)
78
- : undefined,
79
- size: stats.size,
80
- };
81
-
82
- return metadata;
83
- } finally {
84
- await cleanup([inputPath]);
44
+ // Get aspect ratio
45
+ let aspectRatio = "unknown";
46
+ if (videoStream.sampleAspectRatio) {
47
+ const { num, den } = videoStream.sampleAspectRatio;
48
+ aspectRatio = `${num}:${den}`;
85
49
  }
50
+
51
+ const metadata: DescribeVideoMetadata = {
52
+ duration: mediaInput.duration || 0,
53
+ width: videoCodecParams.width || 0,
54
+ height: videoCodecParams.height || 0,
55
+ codec: String(videoCodecParams.codecId) || "unknown",
56
+ format: mediaInput.formatName || "unknown",
57
+ bitrate: mediaInput.bitRate || 0,
58
+ frameRate,
59
+ aspectRatio,
60
+ hasAudio: !!audioStream,
61
+ audioCodec: audioStream?.codecpar.codecId
62
+ ? String(audioStream.codecpar.codecId)
63
+ : undefined,
64
+ audioBitrate: audioStream?.codecpar.bitRate
65
+ ? Number(audioStream.codecpar.bitRate)
66
+ : undefined,
67
+ size: input.byteLength,
68
+ };
69
+
70
+ return metadata;
86
71
  },
87
72
  catch: (error) =>
88
73
  UploadistaError.fromCode("VIDEO_METADATA_EXTRACTION_FAILED", {
@@ -94,101 +79,93 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
94
79
  transcode: (input, options) =>
95
80
  Effect.tryPromise({
96
81
  try: async () => {
97
- const inputPath = await bytesToTempFile(input, "input");
98
- const outputPath = join(
99
- tmpdir(),
100
- `uploadista-${randomUUID()}.${options.format}`,
101
- );
102
-
103
- try {
104
- await using mediaInput = await MediaInput.open(inputPath);
105
- await using mediaOutput = await MediaOutput.open(outputPath);
106
-
107
- const videoStream = mediaInput.video();
108
- if (!videoStream) {
109
- throw new Error("No video stream found");
110
- }
82
+ // Convert Uint8Array to Buffer for input
83
+ const inputBuffer = Buffer.from(input);
111
84
 
112
- using videoDecoder = await Decoder.create(videoStream);
85
+ // Create in-memory output
86
+ const { callbacks, getOutput } = createMemoryOutput();
113
87
 
114
- // Determine encoder codec
115
- const encoderCodec = options.codec
116
- ? codecToAVName[options.codec]
117
- : codecToAVName.h264;
88
+ await using mediaInput = await Demuxer.open(inputBuffer);
89
+ await using mediaOutput = await Muxer.open(callbacks, {
90
+ format: options.format,
91
+ });
118
92
 
119
- using videoEncoder = await Encoder.create(encoderCodec, {
120
- timeBase: videoStream.timeBase,
121
- ...(options.videoBitrate && { bitrate: options.videoBitrate }),
122
- });
93
+ const videoStream = mediaInput.video();
94
+ if (!videoStream) {
95
+ throw new Error("No video stream found");
96
+ }
123
97
 
124
- const videoOutputIndex = mediaOutput.addStream(videoEncoder);
98
+ using videoDecoder = await Decoder.create(videoStream);
125
99
 
126
- // Process video frames
127
- for await (using frame of videoDecoder.frames(
128
- mediaInput.packets(videoStream.index),
129
- )) {
130
- const packet = await videoEncoder.encode(frame);
131
- if (packet) {
132
- await mediaOutput.writePacket(packet, videoOutputIndex);
133
- packet.free();
134
- }
135
- }
100
+ // Determine encoder codec
101
+ const encoderCodec = options.codec
102
+ ? codecToAVName[options.codec]
103
+ : codecToAVName.h264;
136
104
 
137
- // Flush remaining packets
138
- await videoEncoder.flush();
139
- let transcodeVPacket: Packet | null = await videoEncoder.receive();
140
- while (transcodeVPacket !== null) {
141
- await mediaOutput.writePacket(transcodeVPacket, videoOutputIndex);
142
- transcodeVPacket.free();
143
- transcodeVPacket = await videoEncoder.receive();
105
+ using videoEncoder = await Encoder.create(encoderCodec, {
106
+ ...(options.videoBitrate && { bitrate: options.videoBitrate }),
107
+ });
108
+
109
+ const videoOutputIndex = mediaOutput.addStream(videoEncoder);
110
+
111
+ // Process video frames
112
+ for await (using frame of videoDecoder.frames(
113
+ mediaInput.packets(videoStream.index),
114
+ )) {
115
+ const packet = await videoEncoder.encode(frame);
116
+ if (packet) {
117
+ await mediaOutput.writePacket(packet, videoOutputIndex);
118
+ packet.free();
144
119
  }
120
+ }
145
121
 
146
- // Handle audio stream if present
147
- const audioStream = mediaInput.audio();
148
- if (audioStream) {
149
- using audioDecoder = await Decoder.create(audioStream);
122
+ // Flush remaining packets
123
+ await videoEncoder.flush();
124
+ let transcodeVPacket: Packet | null = await videoEncoder.receive();
125
+ while (transcodeVPacket !== null) {
126
+ await mediaOutput.writePacket(transcodeVPacket, videoOutputIndex);
127
+ transcodeVPacket.free();
128
+ transcodeVPacket = await videoEncoder.receive();
129
+ }
150
130
 
151
- const audioEncoderCodec = options.audioCodec
152
- ? audioCodecToAVName[options.audioCodec]
153
- : audioCodecToAVName.aac;
131
+ // Handle audio stream if present
132
+ const audioStream = mediaInput.audio();
133
+ if (audioStream) {
134
+ using audioDecoder = await Decoder.create(audioStream);
154
135
 
155
- using audioEncoder = await Encoder.create(audioEncoderCodec, {
156
- timeBase: audioStream.timeBase,
157
- ...(options.audioBitrate && { bitrate: options.audioBitrate }),
158
- });
136
+ const audioEncoderCodec = options.audioCodec
137
+ ? audioCodecToAVName[options.audioCodec]
138
+ : audioCodecToAVName.aac;
159
139
 
160
- const audioOutputIndex = mediaOutput.addStream(audioEncoder);
140
+ using audioEncoder = await Encoder.create(audioEncoderCodec, {
141
+ ...(options.audioBitrate && { bitrate: options.audioBitrate }),
142
+ });
161
143
 
162
- // Process audio frames
163
- for await (using frame of audioDecoder.frames(
164
- mediaInput.packets(audioStream.index),
165
- )) {
166
- const packet = await audioEncoder.encode(frame);
167
- if (packet) {
168
- await mediaOutput.writePacket(packet, audioOutputIndex);
169
- packet.free();
170
- }
171
- }
144
+ const audioOutputIndex = mediaOutput.addStream(audioEncoder);
172
145
 
173
- // Flush remaining packets
174
- await audioEncoder.flush();
175
- let transcodeAPacket: Packet | null =
176
- await audioEncoder.receive();
177
- while (transcodeAPacket !== null) {
178
- await mediaOutput.writePacket(
179
- transcodeAPacket,
180
- audioOutputIndex,
181
- );
182
- transcodeAPacket.free();
183
- transcodeAPacket = await audioEncoder.receive();
146
+ // Process audio frames
147
+ for await (using frame of audioDecoder.frames(
148
+ mediaInput.packets(audioStream.index),
149
+ )) {
150
+ const packet = await audioEncoder.encode(frame);
151
+ if (packet) {
152
+ await mediaOutput.writePacket(packet, audioOutputIndex);
153
+ packet.free();
184
154
  }
185
155
  }
186
156
 
187
- const output = await tempFileToBytes(outputPath);
188
- return output;
189
- } finally {
190
- await cleanup([inputPath, outputPath]);
157
+ // Flush remaining packets
158
+ await audioEncoder.flush();
159
+ let transcodeAPacket: Packet | null = await audioEncoder.receive();
160
+ while (transcodeAPacket !== null) {
161
+ await mediaOutput.writePacket(transcodeAPacket, audioOutputIndex);
162
+ transcodeAPacket.free();
163
+ transcodeAPacket = await audioEncoder.receive();
164
+ }
191
165
  }
166
+
167
+ // Return accumulated output
168
+ return getOutput();
192
169
  },
193
170
  catch: (error) =>
194
171
  UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
@@ -200,100 +177,94 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
200
177
  resize: (input, options) =>
201
178
  Effect.tryPromise({
202
179
  try: async () => {
203
- const inputPath = await bytesToTempFile(input, "input");
204
- const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);
180
+ // Convert Uint8Array to Buffer for input
181
+ const inputBuffer = Buffer.from(input);
205
182
 
206
- try {
207
- await using mediaInput = await MediaInput.open(inputPath);
208
- await using mediaOutput = await MediaOutput.open(outputPath);
183
+ // Create in-memory output
184
+ const { callbacks, getOutput } = createMemoryOutput();
209
185
 
210
- const videoStream = mediaInput.video();
211
- if (!videoStream) {
212
- throw new Error("No video stream found");
213
- }
186
+ await using mediaInput = await Demuxer.open(inputBuffer);
187
+ await using mediaOutput = await Muxer.open(callbacks, {
188
+ format: "mp4",
189
+ });
190
+
191
+ const videoStream = mediaInput.video();
192
+ if (!videoStream) {
193
+ throw new Error("No video stream found");
194
+ }
195
+
196
+ using videoDecoder = await Decoder.create(videoStream);
214
197
 
215
- using videoDecoder = await Decoder.create(videoStream);
198
+ // TODO: Implement proper resizing with FilterAPI
199
+ // Currently, resize functionality is limited because node-av's Encoder
200
+ // auto-initializes from the first frame it receives. To implement proper
201
+ // resizing, we would need to:
202
+ // 1. Use FilterAPI.create('scale', { width, height }) to create a scale filter
203
+ // 2. Pass decoded frames through the filter before encoding
204
+ // For now, this function will pass through frames without resizing.
216
205
 
217
- // TODO: Implement proper resizing with FilterAPI
218
- // Currently, resize functionality is limited because node-av's Encoder
219
- // auto-initializes from the first frame it receives. To implement proper
220
- // resizing, we would need to:
221
- // 1. Use FilterAPI.create('scale', { width, height }) to create a scale filter
222
- // 2. Pass decoded frames through the filter before encoding
223
- // For now, this function will pass through frames without resizing.
206
+ // Validate that resize parameters are provided
207
+ if (!options.width && !options.height) {
208
+ throw new Error("Either width or height must be specified");
209
+ }
224
210
 
225
- // Validate that resize parameters are provided
226
- if (!options.width && !options.height) {
227
- throw new Error("Either width or height must be specified");
211
+ using videoEncoder = await Encoder.create(codecToAVName.h264);
212
+
213
+ const videoOutputIndex = mediaOutput.addStream(videoEncoder);
214
+
215
+ // Process video frames with resizing
216
+ // Note: For production use, consider using FilterAPI for better quality scaling
217
+ for await (using frame of videoDecoder.frames(
218
+ mediaInput.packets(videoStream.index),
219
+ )) {
220
+ // TODO: Apply scale filter here for better quality
221
+ // For now, encoder will handle basic resizing
222
+ const packet = await videoEncoder.encode(frame);
223
+ if (packet) {
224
+ await mediaOutput.writePacket(packet, videoOutputIndex);
225
+ packet.free();
228
226
  }
227
+ }
229
228
 
230
- using videoEncoder = await Encoder.create(codecToAVName.h264, {
231
- timeBase: videoStream.timeBase,
232
- });
229
+ // Flush remaining packets
230
+ await videoEncoder.flush();
231
+ let vPacket: Packet | null = await videoEncoder.receive();
232
+ while (vPacket !== null) {
233
+ await mediaOutput.writePacket(vPacket, videoOutputIndex);
234
+ vPacket.free();
235
+ vPacket = await videoEncoder.receive();
236
+ }
237
+
238
+ // Copy audio stream if present
239
+ const audioStream = mediaInput.audio();
240
+ if (audioStream) {
241
+ using audioDecoder = await Decoder.create(audioStream);
242
+ using audioEncoder = await Encoder.create(audioCodecToAVName.aac);
233
243
 
234
- const videoOutputIndex = mediaOutput.addStream(videoEncoder);
244
+ const audioOutputIndex = mediaOutput.addStream(audioEncoder);
235
245
 
236
- // Process video frames with resizing
237
- // Note: For production use, consider using FilterAPI for better quality scaling
238
- for await (using frame of videoDecoder.frames(
239
- mediaInput.packets(videoStream.index),
246
+ for await (using frame of audioDecoder.frames(
247
+ mediaInput.packets(audioStream.index),
240
248
  )) {
241
- // TODO: Apply scale filter here for better quality
242
- // For now, encoder will handle basic resizing
243
- const packet = await videoEncoder.encode(frame);
249
+ const packet = await audioEncoder.encode(frame);
244
250
  if (packet) {
245
- await mediaOutput.writePacket(packet, videoOutputIndex);
251
+ await mediaOutput.writePacket(packet, audioOutputIndex);
246
252
  packet.free();
247
253
  }
248
254
  }
249
255
 
250
256
  // Flush remaining packets
251
- await videoEncoder.flush();
252
- let vPacket: Packet | null = await videoEncoder.receive();
253
- while (vPacket !== null) {
254
- await mediaOutput.writePacket(vPacket, videoOutputIndex);
255
- vPacket.free();
256
- vPacket = await videoEncoder.receive();
257
- }
258
-
259
- // Copy audio stream if present
260
- const audioStream = mediaInput.audio();
261
- if (audioStream) {
262
- using audioDecoder = await Decoder.create(audioStream);
263
- using audioEncoder = await Encoder.create(
264
- audioCodecToAVName.aac,
265
- {
266
- timeBase: audioStream.timeBase,
267
- },
268
- );
269
-
270
- const audioOutputIndex = mediaOutput.addStream(audioEncoder);
271
-
272
- for await (using frame of audioDecoder.frames(
273
- mediaInput.packets(audioStream.index),
274
- )) {
275
- const packet = await audioEncoder.encode(frame);
276
- if (packet) {
277
- await mediaOutput.writePacket(packet, audioOutputIndex);
278
- packet.free();
279
- }
280
- }
281
-
282
- // Flush remaining packets
283
- await audioEncoder.flush();
284
- let resizeAPacket: Packet | null = await audioEncoder.receive();
285
- while (resizeAPacket !== null) {
286
- await mediaOutput.writePacket(resizeAPacket, audioOutputIndex);
287
- resizeAPacket.free();
288
- resizeAPacket = await audioEncoder.receive();
289
- }
257
+ await audioEncoder.flush();
258
+ let resizeAPacket: Packet | null = await audioEncoder.receive();
259
+ while (resizeAPacket !== null) {
260
+ await mediaOutput.writePacket(resizeAPacket, audioOutputIndex);
261
+ resizeAPacket.free();
262
+ resizeAPacket = await audioEncoder.receive();
290
263
  }
291
-
292
- const output = await tempFileToBytes(outputPath);
293
- return output;
294
- } finally {
295
- await cleanup([inputPath, outputPath]);
296
264
  }
265
+
266
+ // Return accumulated output
267
+ return getOutput();
297
268
  },
298
269
  catch: (error) =>
299
270
  UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
@@ -305,50 +276,89 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
305
276
  trim: (input, options) =>
306
277
  Effect.tryPromise({
307
278
  try: async () => {
308
- const inputPath = await bytesToTempFile(input, "input");
309
- const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);
279
+ // Convert Uint8Array to Buffer for input
280
+ const inputBuffer = Buffer.from(input);
310
281
 
311
- try {
312
- await using mediaInput = await MediaInput.open(inputPath);
313
- await using mediaOutput = await MediaOutput.open(outputPath);
282
+ // Create in-memory output
283
+ const { callbacks, getOutput } = createMemoryOutput();
314
284
 
315
- const videoStream = mediaInput.video();
316
- if (!videoStream) {
317
- throw new Error("No video stream found");
318
- }
285
+ await using mediaInput = await Demuxer.open(inputBuffer);
286
+ await using mediaOutput = await Muxer.open(callbacks, {
287
+ format: "mp4",
288
+ });
289
+
290
+ const videoStream = mediaInput.video();
291
+ if (!videoStream) {
292
+ throw new Error("No video stream found");
293
+ }
294
+
295
+ // Calculate end time
296
+ let endTime: number;
297
+ if (options.duration !== undefined) {
298
+ endTime = options.startTime + options.duration;
299
+ } else if (options.endTime !== undefined) {
300
+ endTime = options.endTime;
301
+ } else {
302
+ endTime = mediaInput.duration || Number.POSITIVE_INFINITY;
303
+ }
319
304
 
320
- // Calculate end time
321
- let endTime: number;
322
- if (options.duration !== undefined) {
323
- endTime = options.startTime + options.duration;
324
- } else if (options.endTime !== undefined) {
325
- endTime = options.endTime;
326
- } else {
327
- endTime = mediaInput.duration || Number.POSITIVE_INFINITY;
305
+ using videoDecoder = await Decoder.create(videoStream);
306
+ using videoEncoder = await Encoder.create(codecToAVName.h264);
307
+
308
+ const videoOutputIndex = mediaOutput.addStream(videoEncoder);
309
+
310
+ // Process video frames within time range
311
+ for await (using frame of videoDecoder.frames(
312
+ mediaInput.packets(videoStream.index),
313
+ )) {
314
+ // Calculate frame timestamp
315
+ const pts = frame?.pts || 0n;
316
+ const timeBase = videoStream.timeBase
317
+ ? videoStream.timeBase.num / videoStream.timeBase.den
318
+ : 1;
319
+ const timestamp = Number(pts) * timeBase;
320
+
321
+ if (timestamp >= options.startTime && timestamp < endTime) {
322
+ const packet = await videoEncoder.encode(frame);
323
+ if (packet) {
324
+ await mediaOutput.writePacket(packet, videoOutputIndex);
325
+ packet.free();
326
+ }
328
327
  }
329
328
 
330
- using videoDecoder = await Decoder.create(videoStream);
331
- using videoEncoder = await Encoder.create(codecToAVName.h264, {
332
- timeBase: videoStream.timeBase,
333
- });
329
+ if (timestamp >= endTime) break;
330
+ }
331
+
332
+ // Flush remaining packets
333
+ await videoEncoder.flush();
334
+ let trimVPacket: Packet | null = await videoEncoder.receive();
335
+ while (trimVPacket !== null) {
336
+ await mediaOutput.writePacket(trimVPacket, videoOutputIndex);
337
+ trimVPacket.free();
338
+ trimVPacket = await videoEncoder.receive();
339
+ }
340
+
341
+ // Handle audio stream if present
342
+ const audioStream = mediaInput.audio();
343
+ if (audioStream) {
344
+ using audioDecoder = await Decoder.create(audioStream);
345
+ using audioEncoder = await Encoder.create(audioCodecToAVName.aac);
334
346
 
335
- const videoOutputIndex = mediaOutput.addStream(videoEncoder);
347
+ const audioOutputIndex = mediaOutput.addStream(audioEncoder);
336
348
 
337
- // Process video frames within time range
338
- for await (using frame of videoDecoder.frames(
339
- mediaInput.packets(videoStream.index),
349
+ for await (using frame of audioDecoder.frames(
350
+ mediaInput.packets(audioStream.index),
340
351
  )) {
341
- // Calculate frame timestamp
342
- const pts = frame.pts || 0n;
343
- const timeBase = videoStream.timeBase
344
- ? videoStream.timeBase.num / videoStream.timeBase.den
352
+ const pts = frame?.pts || 0n;
353
+ const timeBase = audioStream.timeBase
354
+ ? audioStream.timeBase.num / audioStream.timeBase.den
345
355
  : 1;
346
356
  const timestamp = Number(pts) * timeBase;
347
357
 
348
358
  if (timestamp >= options.startTime && timestamp < endTime) {
349
- const packet = await videoEncoder.encode(frame);
359
+ const packet = await audioEncoder.encode(frame);
350
360
  if (packet) {
351
- await mediaOutput.writePacket(packet, videoOutputIndex);
361
+ await mediaOutput.writePacket(packet, audioOutputIndex);
352
362
  packet.free();
353
363
  }
354
364
  }
@@ -357,62 +367,17 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
357
367
  }
358
368
 
359
369
  // Flush remaining packets
360
- await videoEncoder.flush();
361
- let trimVPacket: Packet | null = await videoEncoder.receive();
362
- while (trimVPacket !== null) {
363
- await mediaOutput.writePacket(trimVPacket, videoOutputIndex);
364
- trimVPacket.free();
365
- trimVPacket = await videoEncoder.receive();
370
+ await audioEncoder.flush();
371
+ let trimAPacket: Packet | null = await audioEncoder.receive();
372
+ while (trimAPacket !== null) {
373
+ await mediaOutput.writePacket(trimAPacket, audioOutputIndex);
374
+ trimAPacket.free();
375
+ trimAPacket = await audioEncoder.receive();
366
376
  }
367
-
368
- // Handle audio stream if present
369
- const audioStream = mediaInput.audio();
370
- if (audioStream) {
371
- using audioDecoder = await Decoder.create(audioStream);
372
- using audioEncoder = await Encoder.create(
373
- audioCodecToAVName.aac,
374
- {
375
- timeBase: audioStream.timeBase,
376
- },
377
- );
378
-
379
- const audioOutputIndex = mediaOutput.addStream(audioEncoder);
380
-
381
- for await (using frame of audioDecoder.frames(
382
- mediaInput.packets(audioStream.index),
383
- )) {
384
- const pts = frame.pts || 0n;
385
- const timeBase = audioStream.timeBase
386
- ? audioStream.timeBase.num / audioStream.timeBase.den
387
- : 1;
388
- const timestamp = Number(pts) * timeBase;
389
-
390
- if (timestamp >= options.startTime && timestamp < endTime) {
391
- const packet = await audioEncoder.encode(frame);
392
- if (packet) {
393
- await mediaOutput.writePacket(packet, audioOutputIndex);
394
- packet.free();
395
- }
396
- }
397
-
398
- if (timestamp >= endTime) break;
399
- }
400
-
401
- // Flush remaining packets
402
- await audioEncoder.flush();
403
- let trimAPacket: Packet | null = await audioEncoder.receive();
404
- while (trimAPacket !== null) {
405
- await mediaOutput.writePacket(trimAPacket, audioOutputIndex);
406
- trimAPacket.free();
407
- trimAPacket = await audioEncoder.receive();
408
- }
409
- }
410
-
411
- const output = await tempFileToBytes(outputPath);
412
- return output;
413
- } finally {
414
- await cleanup([inputPath, outputPath]);
415
377
  }
378
+
379
+ // Return accumulated output
380
+ return getOutput();
416
381
  },
417
382
  catch: (error) =>
418
383
  UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
@@ -424,66 +389,56 @@ export function createAVNodeVideoPlugin(): VideoPluginShape {
424
389
  extractFrame: (input, options) =>
425
390
  Effect.tryPromise({
426
391
  try: async () => {
427
- const inputPath = await bytesToTempFile(input, "input");
392
+ // Convert Uint8Array to Buffer for input
393
+ const inputBuffer = Buffer.from(input);
428
394
  const format = options.format || "jpeg";
429
- const outputPath = join(
430
- tmpdir(),
431
- `uploadista-${randomUUID()}.${format}`,
432
- );
433
-
434
- try {
435
- await using mediaInput = await MediaInput.open(inputPath);
436
-
437
- const videoStream = mediaInput.video();
438
- if (!videoStream) {
439
- throw new Error("No video stream found");
440
- }
441
-
442
- using decoder = await Decoder.create(videoStream);
443
395
 
444
- let frameFound = false;
445
- const targetTimestamp = options.timestamp;
396
+ await using mediaInput = await Demuxer.open(inputBuffer);
446
397
 
447
- for await (using frame of decoder.frames(
448
- mediaInput.packets(videoStream.index),
449
- )) {
450
- // Calculate frame timestamp
451
- const pts = frame.pts || 0n;
452
- const timeBase = videoStream.timeBase
453
- ? videoStream.timeBase.num / videoStream.timeBase.den
454
- : 1;
455
- const timestamp = Number(pts) * timeBase;
398
+ const videoStream = mediaInput.video();
399
+ if (!videoStream) {
400
+ throw new Error("No video stream found");
401
+ }
456
402
 
457
- // Look for frame at or after target timestamp
458
- if (timestamp >= targetTimestamp) {
459
- // Use an image encoder to save the frame
460
- const encoderCodec =
461
- imageFormatToEncoder[format] || imageFormatToEncoder.jpeg;
462
- using imageEncoder = await Encoder.create(encoderCodec, {
463
- timeBase: { num: 1, den: 1 },
464
- });
465
-
466
- // Encode the frame as image
467
- // The encoder will initialize from the first frame's properties
468
- const packet = await imageEncoder.encode(frame);
469
- if (packet?.data) {
470
- await fs.writeFile(outputPath, packet.data);
471
- packet.free();
472
- frameFound = true;
473
- break;
474
- }
403
+ using decoder = await Decoder.create(videoStream);
404
+
405
+ let frameData: Uint8Array | null = null;
406
+ const targetTimestamp = options.timestamp;
407
+
408
+ for await (using frame of decoder.frames(
409
+ mediaInput.packets(videoStream.index),
410
+ )) {
411
+ // Calculate frame timestamp
412
+ const pts = frame?.pts || 0n;
413
+ const timeBase = videoStream.timeBase
414
+ ? videoStream.timeBase.num / videoStream.timeBase.den
415
+ : 1;
416
+ const timestamp = Number(pts) * timeBase;
417
+
418
+ // Look for frame at or after target timestamp
419
+ if (timestamp >= targetTimestamp) {
420
+ // Use an image encoder to save the frame
421
+ const encoderCodec =
422
+ imageFormatToEncoder[format] || imageFormatToEncoder.jpeg;
423
+ using imageEncoder = await Encoder.create(encoderCodec);
424
+
425
+ // Encode the frame as image
426
+ // The encoder will initialize from the first frame's properties
427
+ const packet = await imageEncoder.encode(frame);
428
+ if (packet?.data) {
429
+ // Convert Buffer to Uint8Array
430
+ frameData = new Uint8Array(packet.data);
431
+ packet.free();
432
+ break;
475
433
  }
476
434
  }
435
+ }
477
436
 
478
- if (!frameFound) {
479
- throw new Error(`No frame found at timestamp ${targetTimestamp}`);
480
- }
481
-
482
- const output = await tempFileToBytes(outputPath);
483
- return output;
484
- } finally {
485
- await cleanup([inputPath, outputPath]);
437
+ if (!frameData) {
438
+ throw new Error(`No frame found at timestamp ${targetTimestamp}`);
486
439
  }
440
+
441
+ return frameData;
487
442
  },
488
443
  catch: (error) =>
489
444
  UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {