@uploadista/flow-videos-av-node 0.0.13-beta.3 → 0.0.13-beta.5
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/.turbo/turbo-build.log +14 -8
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.mjs +3 -451
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/tsdown.config.ts +11 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @uploadista/flow-videos-av-node@0.0.13-beta.
|
|
3
|
+
> @uploadista/flow-videos-av-node@0.0.13-beta.4 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/videos/av-node
|
|
4
4
|
> tsdown
|
|
5
5
|
|
|
6
6
|
[34mℹ[39m tsdown [2mv0.16.0[22m powered by rolldown [2mv1.0.0-beta.46[22m
|
|
7
|
+
[34mℹ[39m Using tsdown config: [4m/Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/flow/videos/av-node/tsdown.config.ts[24m
|
|
7
8
|
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
10
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m Cleaning
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mindex.
|
|
12
|
-
[34mℹ[39m [
|
|
13
|
-
[34mℹ[39m [2mdist/[
|
|
14
|
-
[34mℹ[39m [
|
|
15
|
-
[34mℹ[39m
|
|
16
|
-
[
|
|
11
|
+
[34mℹ[39m Cleaning 7 files
|
|
12
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[1mindex.cjs[22m [2m7.60 kB[22m [2m│ gzip: 2.23 kB[22m
|
|
13
|
+
[34mℹ[39m [33m[CJS][39m 1 files, total: 7.60 kB
|
|
14
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m 7.22 kB[22m [2m│ gzip: 2.28 kB[22m
|
|
15
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.mjs.map [2m33.46 kB[22m [2m│ gzip: 6.44 kB[22m
|
|
16
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mindex.d.mts.map [2m 0.88 kB[22m [2m│ gzip: 0.47 kB[22m
|
|
17
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 3.39 kB[22m [2m│ gzip: 1.04 kB[22m
|
|
18
|
+
[34mℹ[39m [34m[ESM][39m 4 files, total: 44.95 kB
|
|
19
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22mindex.d.cts.map [2m0.88 kB[22m [2m│ gzip: 0.47 kB[22m
|
|
20
|
+
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32m[1mindex.d.cts[22m[39m [2m3.39 kB[22m [2m│ gzip: 1.04 kB[22m
|
|
21
|
+
[34mℹ[39m [33m[CJS][39m 2 files, total: 4.28 kB
|
|
22
|
+
[32m✔[39m Build complete in [32m7892ms[39m
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
let e=require(`node-av/constants`),t=require(`node:crypto`),n=require(`node:fs`),r=require(`node:os`),i=require(`node:path`),a=require(`@uploadista/core/errors`),o=require(`effect`),s=require(`node-av/api`),c=require(`@uploadista/core/flow`);async function l(){try{return await import(`node-av`),{available:!0,version:`3.x`}}catch(e){return{available:!1,error:e instanceof Error?e.message:String(e)}}}const u={mp4:`video/mp4`,webm:`video/webm`,mov:`video/quicktime`,avi:`video/x-msvideo`},d={mp4:`mp4`,webm:`webm`,mov:`mov`,avi:`avi`},f={h264:e.FF_ENCODER_LIBX264,h265:e.FF_ENCODER_LIBX265,vp9:e.FF_ENCODER_LIBVPX_VP9,av1:e.FF_ENCODER_LIBAOM_AV1},p={aac:e.FF_ENCODER_AAC,mp3:e.FF_ENCODER_LIBMP3LAME,opus:e.FF_ENCODER_LIBOPUS,vorbis:e.FF_ENCODER_LIBVORBIS},m={jpeg:e.FF_ENCODER_MJPEG,mjpeg:e.FF_ENCODER_MJPEG,png:e.FF_ENCODER_PNG};async function h(e,a){let o=(0,i.join)((0,r.tmpdir)(),`uploadista-${(0,t.randomUUID)()}.${a}`);return await n.promises.writeFile(o,e),o}async function g(e){let t=await n.promises.readFile(e);return new Uint8Array(t)}async function _(e){await Promise.all(e.map(e=>n.promises.unlink(e).catch(()=>{})))}function v(){return{describe:e=>o.Effect.tryPromise({try:async()=>{let t=await h(e,`input`);try{await using e=await s.MediaInput.open(t);let r=e.video(),i=e.audio();if(!r)throw Error(`No video stream found`);let a=r.codecpar,o=0;if(r.rFrameRate){let{num:e,den:t}=r.rFrameRate;o=t?e/t:e}let c=`unknown`;if(r.sampleAspectRatio){let{num:e,den:t}=r.sampleAspectRatio;c=`${e}:${t}`}let l=await n.promises.stat(t);return{duration:e.duration||0,width:a.width||0,height:a.height||0,codec:String(a.codecId)||`unknown`,format:e.formatName||`unknown`,bitrate:e.bitRate||0,frameRate:o,aspectRatio:c,hasAudio:!!i,audioCodec:i?.codecpar.codecId?String(i.codecpar.codecId):void 0,audioBitrate:i?.codecpar.bitRate?Number(i.codecpar.bitRate):void 0,size:l.size}}finally{await _([t])}},catch:e=>a.UploadistaError.fromCode(`VIDEO_METADATA_EXTRACTION_FAILED`,{body:`Failed to extract video metadata: ${e instanceof Error?e.message:String(e)}`,cause:e})}),transcode:(e,n)=>o.Effect.tryPromise({try:async()=>{let a=await h(e,`input`),o=(0,i.join)((0,r.tmpdir)(),`uploadista-${(0,t.randomUUID)()}.${n.format}`);try{await using e=await s.MediaInput.open(a),t=await s.MediaOutput.open(o);let r=e.video();if(!r)throw Error(`No video stream found`);using i=await s.Decoder.create(r);let c=n.codec?f[n.codec]:f.h264;using l=await s.Encoder.create(c,{timeBase:r.timeBase,...n.videoBitrate&&{bitrate:n.videoBitrate}});let u=t.addStream(l);for await(using n of i.frames(e.packets(r.index))){let e=await l.encode(n);e&&(await t.writePacket(e,u),e.free())}await l.flush();let d=await l.receive();for(;d!==null;)await t.writePacket(d,u),d.free(),d=await l.receive();let m=e.audio();if(m){using r=await s.Decoder.create(m);let i=n.audioCodec?p[n.audioCodec]:p.aac;using a=await s.Encoder.create(i,{timeBase:m.timeBase,...n.audioBitrate&&{bitrate:n.audioBitrate}});let o=t.addStream(a);for await(using n of r.frames(e.packets(m.index))){let e=await a.encode(n);e&&(await t.writePacket(e,o),e.free())}await a.flush();let c=await a.receive();for(;c!==null;)await t.writePacket(c,o),c.free(),c=await a.receive()}return await g(o)}finally{await _([a,o])}},catch:e=>a.UploadistaError.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Transcode failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),resize:(e,n)=>o.Effect.tryPromise({try:async()=>{let a=await h(e,`input`),o=(0,i.join)((0,r.tmpdir)(),`uploadista-${(0,t.randomUUID)()}.mp4`);try{await using e=await s.MediaInput.open(a),t=await s.MediaOutput.open(o);let r=e.video();if(!r)throw Error(`No video stream found`);using i=await s.Decoder.create(r);if(!n.width&&!n.height)throw Error(`Either width or height must be specified`);using c=await s.Encoder.create(f.h264,{timeBase:r.timeBase});let l=t.addStream(c);for await(using n of i.frames(e.packets(r.index))){let e=await c.encode(n);e&&(await t.writePacket(e,l),e.free())}await c.flush();let u=await c.receive();for(;u!==null;)await t.writePacket(u,l),u.free(),u=await c.receive();let d=e.audio();if(d){using n=await s.Decoder.create(d),r=await s.Encoder.create(p.aac,{timeBase:d.timeBase});let i=t.addStream(r);for await(using a of n.frames(e.packets(d.index))){let e=await r.encode(a);e&&(await t.writePacket(e,i),e.free())}await r.flush();let a=await r.receive();for(;a!==null;)await t.writePacket(a,i),a.free(),a=await r.receive()}return await g(o)}finally{await _([a,o])}},catch:e=>a.UploadistaError.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Resize failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),trim:(e,n)=>o.Effect.tryPromise({try:async()=>{let a=await h(e,`input`),o=(0,i.join)((0,r.tmpdir)(),`uploadista-${(0,t.randomUUID)()}.mp4`);try{await using e=await s.MediaInput.open(a),t=await s.MediaOutput.open(o);let r=e.video();if(!r)throw Error(`No video stream found`);let i;i=n.duration===void 0?n.endTime===void 0?e.duration||1/0:n.endTime:n.startTime+n.duration;using c=await s.Decoder.create(r),l=await s.Encoder.create(f.h264,{timeBase:r.timeBase});let u=t.addStream(l);for await(using a of c.frames(e.packets(r.index))){let e=a.pts||0n,o=r.timeBase?r.timeBase.num/r.timeBase.den:1,s=Number(e)*o;if(s>=n.startTime&&s<i){let e=await l.encode(a);e&&(await t.writePacket(e,u),e.free())}if(s>=i)break}await l.flush();let d=await l.receive();for(;d!==null;)await t.writePacket(d,u),d.free(),d=await l.receive();let m=e.audio();if(m){using r=await s.Decoder.create(m),a=await s.Encoder.create(p.aac,{timeBase:m.timeBase});let o=t.addStream(a);for await(using s of r.frames(e.packets(m.index))){let e=s.pts||0n,r=m.timeBase?m.timeBase.num/m.timeBase.den:1,c=Number(e)*r;if(c>=n.startTime&&c<i){let e=await a.encode(s);e&&(await t.writePacket(e,o),e.free())}if(c>=i)break}await a.flush();let c=await a.receive();for(;c!==null;)await t.writePacket(c,o),c.free(),c=await a.receive()}return await g(o)}finally{await _([a,o])}},catch:e=>a.UploadistaError.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Trim failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),extractFrame:(e,c)=>o.Effect.tryPromise({try:async()=>{let a=await h(e,`input`),o=c.format||`jpeg`,l=(0,i.join)((0,r.tmpdir)(),`uploadista-${(0,t.randomUUID)()}.${o}`);try{await using e=await s.MediaInput.open(a);let t=e.video();if(!t)throw Error(`No video stream found`);using r=await s.Decoder.create(t);let i=!1,u=c.timestamp;for await(using a of r.frames(e.packets(t.index))){let e=a.pts||0n,r=t.timeBase?t.timeBase.num/t.timeBase.den:1;if(Number(e)*r>=u){let e=m[o]||m.jpeg;using t=await s.Encoder.create(e,{timeBase:{num:1,den:1}});let r=await t.encode(a);if(r?.data){await n.promises.writeFile(l,r.data),r.free(),i=!0;break}}}if(!i)throw Error(`No frame found at timestamp ${u}`);return await g(l)}finally{await _([a,l])}},catch:e=>a.UploadistaError.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Frame extraction failed: ${e instanceof Error?e.message:String(e)}`,cause:e})})}}const y=o.Layer.succeed(c.VideoPlugin,v()),b=o.Layer.effectDiscard(o.Effect.gen(function*(){let e=yield*o.Effect.promise(()=>l());e.available?console.log(`✓ node-av ${e.version} detected`):console.warn(`⚠️ node-av is not installed or not available.`,`
|
|
2
|
+
Video processing operations will fail.`,`
|
|
3
|
+
Install node-av: npm install node-av`)})).pipe(o.Layer.provideMerge(y));exports.AVNodeVideoPlugin=y,exports.AVNodeVideoPluginWithCheck=b,exports.audioCodecToAVName=p,exports.checkAVAvailable=l,exports.codecToAVName=f,exports.createAVNodeVideoPlugin=v,exports.formatToExtension=d,exports.formatToMimeType=u,exports.imageFormatToEncoder=m;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { TranscodeVideoParams, VideoPlugin, VideoPluginShape } from "@uploadista/core/flow";
|
|
2
|
+
import { FFEncoderCodec } from "node-av/constants";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/utils/av-check.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Result of node-av availability check
|
|
8
|
+
*/
|
|
9
|
+
type AVCheckResult = {
|
|
10
|
+
available: boolean;
|
|
11
|
+
version?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Checks if node-av is available and can access FFmpeg binaries
|
|
16
|
+
* @returns Promise with availability status and version info
|
|
17
|
+
*/
|
|
18
|
+
declare function checkAVAvailable(): Promise<AVCheckResult>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/utils/format-mappings.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* Maps video format to MIME type
|
|
23
|
+
*/
|
|
24
|
+
declare const formatToMimeType: Record<TranscodeVideoParams["format"], string>;
|
|
25
|
+
/**
|
|
26
|
+
* Maps video format to file extension
|
|
27
|
+
*/
|
|
28
|
+
declare const formatToExtension: Record<TranscodeVideoParams["format"], string>;
|
|
29
|
+
/**
|
|
30
|
+
* Maps codec parameter to node-av codec constant
|
|
31
|
+
*/
|
|
32
|
+
declare const codecToAVName: Record<NonNullable<TranscodeVideoParams["codec"]>, FFEncoderCodec>;
|
|
33
|
+
/**
|
|
34
|
+
* Maps audio codec parameter to node-av audio codec constant
|
|
35
|
+
*/
|
|
36
|
+
declare const audioCodecToAVName: Record<NonNullable<TranscodeVideoParams["audioCodec"]>, FFEncoderCodec>;
|
|
37
|
+
/**
|
|
38
|
+
* Maps image format to encoder constant
|
|
39
|
+
*/
|
|
40
|
+
declare const imageFormatToEncoder: Record<string, FFEncoderCodec>;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/video-plugin.d.ts
|
|
43
|
+
/**
|
|
44
|
+
* Creates a node-av based video processing plugin
|
|
45
|
+
*/
|
|
46
|
+
declare function createAVNodeVideoPlugin(): VideoPluginShape;
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/video-plugin-layer.d.ts
|
|
49
|
+
/**
|
|
50
|
+
* Effect Layer for the node-av video plugin
|
|
51
|
+
*
|
|
52
|
+
* This layer provides video processing capabilities using node-av (FFmpeg bindings).
|
|
53
|
+
* Note: node-av includes prebuilt FFmpeg binaries, so no system installation is required.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { AVNodeVideoPlugin } from "@uploadista/flow-videos-av-node";
|
|
58
|
+
* import { Effect } from "effect";
|
|
59
|
+
*
|
|
60
|
+
* const program = Effect.gen(function* () {
|
|
61
|
+
* const videoPlugin = yield* VideoPlugin;
|
|
62
|
+
* const metadata = yield* videoPlugin.describe(videoBytes);
|
|
63
|
+
* return metadata;
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Run with node-av plugin layer
|
|
67
|
+
* const result = await Effect.runPromise(
|
|
68
|
+
* program.pipe(Effect.provide(AVNodeVideoPluginLive))
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare const AVNodeVideoPlugin: Layer.Layer<VideoPlugin, never, never>;
|
|
73
|
+
/**
|
|
74
|
+
* Effect Layer for the node-av video plugin with availability check
|
|
75
|
+
*
|
|
76
|
+
* This layer checks if node-av is properly installed and logs status information.
|
|
77
|
+
* The plugin will still be created, but operations will fail if node-av is not available.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* import { AVNodeVideoPluginWithCheck } from "@uploadista/flow-videos-av-node";
|
|
82
|
+
* import { Effect } from "effect";
|
|
83
|
+
*
|
|
84
|
+
* const program = Effect.gen(function* () {
|
|
85
|
+
* const videoPlugin = yield* VideoPlugin;
|
|
86
|
+
* const metadata = yield* videoPlugin.describe(videoBytes);
|
|
87
|
+
* return metadata;
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* // Run with node-av plugin layer (with check)
|
|
91
|
+
* const result = await Effect.runPromise(
|
|
92
|
+
* program.pipe(Effect.provide(AVNodeVideoPluginWithCheck))
|
|
93
|
+
* );
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare const AVNodeVideoPluginWithCheck: Layer.Layer<VideoPlugin, never, never>;
|
|
97
|
+
//#endregion
|
|
98
|
+
export { AVCheckResult, AVNodeVideoPlugin, AVNodeVideoPluginWithCheck, audioCodecToAVName, checkAVAvailable, codecToAVName, createAVNodeVideoPlugin, formatToExtension, formatToMimeType, imageFormatToEncoder };
|
|
99
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/utils/av-check.ts","../src/utils/format-mappings.ts","../src/video-plugin.ts","../src/video-plugin-layer.ts"],"sourcesContent":[],"mappings":";;;;;;;;KAGY,aAAA;;EAAA,OAAA,CAAA,EAAA,MAAa;EAUH,KAAA,CAAA,EAAA,MAAA;;;;ACKtB;AAWA;AAWa,iBD3BS,gBAAA,CAAA,CCmCrB,EDnCyC,OCmCzC,CDnCiD,aCmCjD,CAAA;;;;;;AD7CW,cCeC,gBDfY,ECeM,MDfN,CCea,oBDfb,CAAA,QAAA,CAAA,EAAA,MAAA,CAAA;AAUzB;;;cCgBa,mBAAmB,OAAO;AAXvC;AAWA;AAWA;AACc,cADD,aACC,EADc,MACd,CAAZ,WAAY,CAAA,oBAAA,CAAA,OAAA,CAAA,CAAA,EACZ,cADY,CAAA;;;;AADoB,cAarB,kBAbqB,EAaD,MAbC,CAchC,WAdgC,CAcpB,oBAdoB,CAAA,YAAA,CAAA,CAAA,EAehC,cAfgC,CAAA;AAalC;;;AAEE,cAWW,oBAXX,EAWiC,MAXjC,CAAA,MAAA,EAWgD,cAXhD,CAAA;;;;;;iBC7Bc,uBAAA,CAAA,GAA2B;;;;;;AFvB3C;AAUA;;;;ACKA;AAWA;AAWA;;;;;;AAaA;;;;;;AAaA;cEtCa,mBAAiB,KAAA,CAAA,MAAA;;;ADF9B;;;;ACEA;AA4BA;;;;;;;;;;;;;;;;cAAa,4BAA0B,KAAA,CAAA,MAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,452 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { UploadistaError } from "@uploadista/core/errors";
|
|
7
|
-
import { Effect, Layer } from "effect";
|
|
8
|
-
import { Decoder, Encoder, MediaInput, MediaOutput } from "node-av/api";
|
|
9
|
-
import { VideoPlugin } from "@uploadista/core/flow";
|
|
10
|
-
|
|
11
|
-
//#region src/utils/av-check.ts
|
|
12
|
-
/**
|
|
13
|
-
* Checks if node-av is available and can access FFmpeg binaries
|
|
14
|
-
* @returns Promise with availability status and version info
|
|
15
|
-
*/
|
|
16
|
-
async function checkAVAvailable() {
|
|
17
|
-
try {
|
|
18
|
-
await import("node-av");
|
|
19
|
-
return {
|
|
20
|
-
available: true,
|
|
21
|
-
version: "3.x"
|
|
22
|
-
};
|
|
23
|
-
} catch (error) {
|
|
24
|
-
return {
|
|
25
|
-
available: false,
|
|
26
|
-
error: error instanceof Error ? error.message : String(error)
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/utils/format-mappings.ts
|
|
33
|
-
/**
|
|
34
|
-
* Maps video format to MIME type
|
|
35
|
-
*/
|
|
36
|
-
const formatToMimeType = {
|
|
37
|
-
mp4: "video/mp4",
|
|
38
|
-
webm: "video/webm",
|
|
39
|
-
mov: "video/quicktime",
|
|
40
|
-
avi: "video/x-msvideo"
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* Maps video format to file extension
|
|
44
|
-
*/
|
|
45
|
-
const formatToExtension = {
|
|
46
|
-
mp4: "mp4",
|
|
47
|
-
webm: "webm",
|
|
48
|
-
mov: "mov",
|
|
49
|
-
avi: "avi"
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Maps codec parameter to node-av codec constant
|
|
53
|
-
*/
|
|
54
|
-
const codecToAVName = {
|
|
55
|
-
h264: FF_ENCODER_LIBX264,
|
|
56
|
-
h265: FF_ENCODER_LIBX265,
|
|
57
|
-
vp9: FF_ENCODER_LIBVPX_VP9,
|
|
58
|
-
av1: FF_ENCODER_LIBAOM_AV1
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Maps audio codec parameter to node-av audio codec constant
|
|
62
|
-
*/
|
|
63
|
-
const audioCodecToAVName = {
|
|
64
|
-
aac: FF_ENCODER_AAC,
|
|
65
|
-
mp3: FF_ENCODER_LIBMP3LAME,
|
|
66
|
-
opus: FF_ENCODER_LIBOPUS,
|
|
67
|
-
vorbis: FF_ENCODER_LIBVORBIS
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Maps image format to encoder constant
|
|
71
|
-
*/
|
|
72
|
-
const imageFormatToEncoder = {
|
|
73
|
-
jpeg: FF_ENCODER_MJPEG,
|
|
74
|
-
mjpeg: FF_ENCODER_MJPEG,
|
|
75
|
-
png: FF_ENCODER_PNG
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
//#endregion
|
|
79
|
-
//#region src/utils/temp-file-manager.ts
|
|
80
|
-
/**
|
|
81
|
-
* Writes a Uint8Array to a temporary file
|
|
82
|
-
* @param bytes - The bytes to write
|
|
83
|
-
* @param extension - The file extension (without dot)
|
|
84
|
-
* @returns The path to the temporary file
|
|
85
|
-
*/
|
|
86
|
-
async function bytesToTempFile(bytes, extension) {
|
|
87
|
-
const tempPath = join(tmpdir(), `uploadista-${randomUUID()}.${extension}`);
|
|
88
|
-
await promises.writeFile(tempPath, bytes);
|
|
89
|
-
return tempPath;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Reads a temporary file into a Uint8Array
|
|
93
|
-
* @param path - The path to the file
|
|
94
|
-
* @returns The file contents as Uint8Array
|
|
95
|
-
*/
|
|
96
|
-
async function tempFileToBytes(path) {
|
|
97
|
-
const buffer = await promises.readFile(path);
|
|
98
|
-
return new Uint8Array(buffer);
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Cleans up temporary files, suppressing errors
|
|
102
|
-
* @param paths - The paths to delete
|
|
103
|
-
*/
|
|
104
|
-
async function cleanup(paths) {
|
|
105
|
-
await Promise.all(paths.map((p) => promises.unlink(p).catch(() => {})));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/video-plugin.ts
|
|
110
|
-
/**
|
|
111
|
-
* Creates a node-av based video processing plugin
|
|
112
|
-
*/
|
|
113
|
-
function createAVNodeVideoPlugin() {
|
|
114
|
-
return {
|
|
115
|
-
describe: (input) => Effect.tryPromise({
|
|
116
|
-
try: async () => {
|
|
117
|
-
const inputPath = await bytesToTempFile(input, "input");
|
|
118
|
-
try {
|
|
119
|
-
await using mediaInput = await MediaInput.open(inputPath);
|
|
120
|
-
const videoStream = mediaInput.video();
|
|
121
|
-
const audioStream = mediaInput.audio();
|
|
122
|
-
if (!videoStream) throw new Error("No video stream found");
|
|
123
|
-
const videoCodecParams = videoStream.codecpar;
|
|
124
|
-
let frameRate = 0;
|
|
125
|
-
if (videoStream.rFrameRate) {
|
|
126
|
-
const { num, den } = videoStream.rFrameRate;
|
|
127
|
-
frameRate = den ? num / den : num;
|
|
128
|
-
}
|
|
129
|
-
let aspectRatio = "unknown";
|
|
130
|
-
if (videoStream.sampleAspectRatio) {
|
|
131
|
-
const { num, den } = videoStream.sampleAspectRatio;
|
|
132
|
-
aspectRatio = `${num}:${den}`;
|
|
133
|
-
}
|
|
134
|
-
const stats = await promises.stat(inputPath);
|
|
135
|
-
return {
|
|
136
|
-
duration: mediaInput.duration || 0,
|
|
137
|
-
width: videoCodecParams.width || 0,
|
|
138
|
-
height: videoCodecParams.height || 0,
|
|
139
|
-
codec: String(videoCodecParams.codecId) || "unknown",
|
|
140
|
-
format: mediaInput.formatName || "unknown",
|
|
141
|
-
bitrate: mediaInput.bitRate || 0,
|
|
142
|
-
frameRate,
|
|
143
|
-
aspectRatio,
|
|
144
|
-
hasAudio: !!audioStream,
|
|
145
|
-
audioCodec: audioStream?.codecpar.codecId ? String(audioStream.codecpar.codecId) : void 0,
|
|
146
|
-
audioBitrate: audioStream?.codecpar.bitRate ? Number(audioStream.codecpar.bitRate) : void 0,
|
|
147
|
-
size: stats.size
|
|
148
|
-
};
|
|
149
|
-
} finally {
|
|
150
|
-
await cleanup([inputPath]);
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
catch: (error) => UploadistaError.fromCode("VIDEO_METADATA_EXTRACTION_FAILED", {
|
|
154
|
-
body: `Failed to extract video metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
-
cause: error
|
|
156
|
-
})
|
|
157
|
-
}),
|
|
158
|
-
transcode: (input, options) => Effect.tryPromise({
|
|
159
|
-
try: async () => {
|
|
160
|
-
const inputPath = await bytesToTempFile(input, "input");
|
|
161
|
-
const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.${options.format}`);
|
|
162
|
-
try {
|
|
163
|
-
await using mediaInput = await MediaInput.open(inputPath);
|
|
164
|
-
await using mediaOutput = await MediaOutput.open(outputPath);
|
|
165
|
-
const videoStream = mediaInput.video();
|
|
166
|
-
if (!videoStream) throw new Error("No video stream found");
|
|
167
|
-
using videoDecoder = await Decoder.create(videoStream);
|
|
168
|
-
const encoderCodec = options.codec ? codecToAVName[options.codec] : codecToAVName.h264;
|
|
169
|
-
using videoEncoder = await Encoder.create(encoderCodec, {
|
|
170
|
-
timeBase: videoStream.timeBase,
|
|
171
|
-
...options.videoBitrate && { bitrate: options.videoBitrate }
|
|
172
|
-
});
|
|
173
|
-
const videoOutputIndex = mediaOutput.addStream(videoEncoder);
|
|
174
|
-
for await (using frame of videoDecoder.frames(mediaInput.packets(videoStream.index))) {
|
|
175
|
-
const packet = await videoEncoder.encode(frame);
|
|
176
|
-
if (packet) {
|
|
177
|
-
await mediaOutput.writePacket(packet, videoOutputIndex);
|
|
178
|
-
packet.free();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
await videoEncoder.flush();
|
|
182
|
-
let transcodeVPacket = await videoEncoder.receive();
|
|
183
|
-
while (transcodeVPacket !== null) {
|
|
184
|
-
await mediaOutput.writePacket(transcodeVPacket, videoOutputIndex);
|
|
185
|
-
transcodeVPacket.free();
|
|
186
|
-
transcodeVPacket = await videoEncoder.receive();
|
|
187
|
-
}
|
|
188
|
-
const audioStream = mediaInput.audio();
|
|
189
|
-
if (audioStream) {
|
|
190
|
-
using audioDecoder = await Decoder.create(audioStream);
|
|
191
|
-
const audioEncoderCodec = options.audioCodec ? audioCodecToAVName[options.audioCodec] : audioCodecToAVName.aac;
|
|
192
|
-
using audioEncoder = await Encoder.create(audioEncoderCodec, {
|
|
193
|
-
timeBase: audioStream.timeBase,
|
|
194
|
-
...options.audioBitrate && { bitrate: options.audioBitrate }
|
|
195
|
-
});
|
|
196
|
-
const audioOutputIndex = mediaOutput.addStream(audioEncoder);
|
|
197
|
-
for await (using frame of audioDecoder.frames(mediaInput.packets(audioStream.index))) {
|
|
198
|
-
const packet = await audioEncoder.encode(frame);
|
|
199
|
-
if (packet) {
|
|
200
|
-
await mediaOutput.writePacket(packet, audioOutputIndex);
|
|
201
|
-
packet.free();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
await audioEncoder.flush();
|
|
205
|
-
let transcodeAPacket = await audioEncoder.receive();
|
|
206
|
-
while (transcodeAPacket !== null) {
|
|
207
|
-
await mediaOutput.writePacket(transcodeAPacket, audioOutputIndex);
|
|
208
|
-
transcodeAPacket.free();
|
|
209
|
-
transcodeAPacket = await audioEncoder.receive();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return await tempFileToBytes(outputPath);
|
|
213
|
-
} finally {
|
|
214
|
-
await cleanup([inputPath, outputPath]);
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
catch: (error) => UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
|
|
218
|
-
body: `Transcode failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
219
|
-
cause: error
|
|
220
|
-
})
|
|
221
|
-
}),
|
|
222
|
-
resize: (input, options) => Effect.tryPromise({
|
|
223
|
-
try: async () => {
|
|
224
|
-
const inputPath = await bytesToTempFile(input, "input");
|
|
225
|
-
const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);
|
|
226
|
-
try {
|
|
227
|
-
await using mediaInput = await MediaInput.open(inputPath);
|
|
228
|
-
await using mediaOutput = await MediaOutput.open(outputPath);
|
|
229
|
-
const videoStream = mediaInput.video();
|
|
230
|
-
if (!videoStream) throw new Error("No video stream found");
|
|
231
|
-
using videoDecoder = await Decoder.create(videoStream);
|
|
232
|
-
if (!options.width && !options.height) throw new Error("Either width or height must be specified");
|
|
233
|
-
using videoEncoder = await Encoder.create(codecToAVName.h264, { timeBase: videoStream.timeBase });
|
|
234
|
-
const videoOutputIndex = mediaOutput.addStream(videoEncoder);
|
|
235
|
-
for await (using frame of videoDecoder.frames(mediaInput.packets(videoStream.index))) {
|
|
236
|
-
const packet = await videoEncoder.encode(frame);
|
|
237
|
-
if (packet) {
|
|
238
|
-
await mediaOutput.writePacket(packet, videoOutputIndex);
|
|
239
|
-
packet.free();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
await videoEncoder.flush();
|
|
243
|
-
let vPacket = await videoEncoder.receive();
|
|
244
|
-
while (vPacket !== null) {
|
|
245
|
-
await mediaOutput.writePacket(vPacket, videoOutputIndex);
|
|
246
|
-
vPacket.free();
|
|
247
|
-
vPacket = await videoEncoder.receive();
|
|
248
|
-
}
|
|
249
|
-
const audioStream = mediaInput.audio();
|
|
250
|
-
if (audioStream) {
|
|
251
|
-
using audioDecoder = await Decoder.create(audioStream);
|
|
252
|
-
using audioEncoder = await Encoder.create(audioCodecToAVName.aac, { timeBase: audioStream.timeBase });
|
|
253
|
-
const audioOutputIndex = mediaOutput.addStream(audioEncoder);
|
|
254
|
-
for await (using frame of audioDecoder.frames(mediaInput.packets(audioStream.index))) {
|
|
255
|
-
const packet = await audioEncoder.encode(frame);
|
|
256
|
-
if (packet) {
|
|
257
|
-
await mediaOutput.writePacket(packet, audioOutputIndex);
|
|
258
|
-
packet.free();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
await audioEncoder.flush();
|
|
262
|
-
let resizeAPacket = await audioEncoder.receive();
|
|
263
|
-
while (resizeAPacket !== null) {
|
|
264
|
-
await mediaOutput.writePacket(resizeAPacket, audioOutputIndex);
|
|
265
|
-
resizeAPacket.free();
|
|
266
|
-
resizeAPacket = await audioEncoder.receive();
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return await tempFileToBytes(outputPath);
|
|
270
|
-
} finally {
|
|
271
|
-
await cleanup([inputPath, outputPath]);
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
catch: (error) => UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
|
|
275
|
-
body: `Resize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
276
|
-
cause: error
|
|
277
|
-
})
|
|
278
|
-
}),
|
|
279
|
-
trim: (input, options) => Effect.tryPromise({
|
|
280
|
-
try: async () => {
|
|
281
|
-
const inputPath = await bytesToTempFile(input, "input");
|
|
282
|
-
const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);
|
|
283
|
-
try {
|
|
284
|
-
await using mediaInput = await MediaInput.open(inputPath);
|
|
285
|
-
await using mediaOutput = await MediaOutput.open(outputPath);
|
|
286
|
-
const videoStream = mediaInput.video();
|
|
287
|
-
if (!videoStream) throw new Error("No video stream found");
|
|
288
|
-
let endTime;
|
|
289
|
-
if (options.duration !== void 0) endTime = options.startTime + options.duration;
|
|
290
|
-
else if (options.endTime !== void 0) endTime = options.endTime;
|
|
291
|
-
else endTime = mediaInput.duration || Number.POSITIVE_INFINITY;
|
|
292
|
-
using videoDecoder = await Decoder.create(videoStream);
|
|
293
|
-
using videoEncoder = await Encoder.create(codecToAVName.h264, { timeBase: videoStream.timeBase });
|
|
294
|
-
const videoOutputIndex = mediaOutput.addStream(videoEncoder);
|
|
295
|
-
for await (using frame of videoDecoder.frames(mediaInput.packets(videoStream.index))) {
|
|
296
|
-
const pts = frame.pts || 0n;
|
|
297
|
-
const timeBase = videoStream.timeBase ? videoStream.timeBase.num / videoStream.timeBase.den : 1;
|
|
298
|
-
const timestamp = Number(pts) * timeBase;
|
|
299
|
-
if (timestamp >= options.startTime && timestamp < endTime) {
|
|
300
|
-
const packet = await videoEncoder.encode(frame);
|
|
301
|
-
if (packet) {
|
|
302
|
-
await mediaOutput.writePacket(packet, videoOutputIndex);
|
|
303
|
-
packet.free();
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (timestamp >= endTime) break;
|
|
307
|
-
}
|
|
308
|
-
await videoEncoder.flush();
|
|
309
|
-
let trimVPacket = await videoEncoder.receive();
|
|
310
|
-
while (trimVPacket !== null) {
|
|
311
|
-
await mediaOutput.writePacket(trimVPacket, videoOutputIndex);
|
|
312
|
-
trimVPacket.free();
|
|
313
|
-
trimVPacket = await videoEncoder.receive();
|
|
314
|
-
}
|
|
315
|
-
const audioStream = mediaInput.audio();
|
|
316
|
-
if (audioStream) {
|
|
317
|
-
using audioDecoder = await Decoder.create(audioStream);
|
|
318
|
-
using audioEncoder = await Encoder.create(audioCodecToAVName.aac, { timeBase: audioStream.timeBase });
|
|
319
|
-
const audioOutputIndex = mediaOutput.addStream(audioEncoder);
|
|
320
|
-
for await (using frame of audioDecoder.frames(mediaInput.packets(audioStream.index))) {
|
|
321
|
-
const pts = frame.pts || 0n;
|
|
322
|
-
const timeBase = audioStream.timeBase ? audioStream.timeBase.num / audioStream.timeBase.den : 1;
|
|
323
|
-
const timestamp = Number(pts) * timeBase;
|
|
324
|
-
if (timestamp >= options.startTime && timestamp < endTime) {
|
|
325
|
-
const packet = await audioEncoder.encode(frame);
|
|
326
|
-
if (packet) {
|
|
327
|
-
await mediaOutput.writePacket(packet, audioOutputIndex);
|
|
328
|
-
packet.free();
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (timestamp >= endTime) break;
|
|
332
|
-
}
|
|
333
|
-
await audioEncoder.flush();
|
|
334
|
-
let trimAPacket = await audioEncoder.receive();
|
|
335
|
-
while (trimAPacket !== null) {
|
|
336
|
-
await mediaOutput.writePacket(trimAPacket, audioOutputIndex);
|
|
337
|
-
trimAPacket.free();
|
|
338
|
-
trimAPacket = await audioEncoder.receive();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return await tempFileToBytes(outputPath);
|
|
342
|
-
} finally {
|
|
343
|
-
await cleanup([inputPath, outputPath]);
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
catch: (error) => UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
|
|
347
|
-
body: `Trim failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
348
|
-
cause: error
|
|
349
|
-
})
|
|
350
|
-
}),
|
|
351
|
-
extractFrame: (input, options) => Effect.tryPromise({
|
|
352
|
-
try: async () => {
|
|
353
|
-
const inputPath = await bytesToTempFile(input, "input");
|
|
354
|
-
const format = options.format || "jpeg";
|
|
355
|
-
const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.${format}`);
|
|
356
|
-
try {
|
|
357
|
-
await using mediaInput = await MediaInput.open(inputPath);
|
|
358
|
-
const videoStream = mediaInput.video();
|
|
359
|
-
if (!videoStream) throw new Error("No video stream found");
|
|
360
|
-
using decoder = await Decoder.create(videoStream);
|
|
361
|
-
let frameFound = false;
|
|
362
|
-
const targetTimestamp = options.timestamp;
|
|
363
|
-
for await (using frame of decoder.frames(mediaInput.packets(videoStream.index))) {
|
|
364
|
-
const pts = frame.pts || 0n;
|
|
365
|
-
const timeBase = videoStream.timeBase ? videoStream.timeBase.num / videoStream.timeBase.den : 1;
|
|
366
|
-
if (Number(pts) * timeBase >= targetTimestamp) {
|
|
367
|
-
const encoderCodec = imageFormatToEncoder[format] || imageFormatToEncoder.jpeg;
|
|
368
|
-
using imageEncoder = await Encoder.create(encoderCodec, { timeBase: {
|
|
369
|
-
num: 1,
|
|
370
|
-
den: 1
|
|
371
|
-
} });
|
|
372
|
-
const packet = await imageEncoder.encode(frame);
|
|
373
|
-
if (packet?.data) {
|
|
374
|
-
await promises.writeFile(outputPath, packet.data);
|
|
375
|
-
packet.free();
|
|
376
|
-
frameFound = true;
|
|
377
|
-
break;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (!frameFound) throw new Error(`No frame found at timestamp ${targetTimestamp}`);
|
|
382
|
-
return await tempFileToBytes(outputPath);
|
|
383
|
-
} finally {
|
|
384
|
-
await cleanup([inputPath, outputPath]);
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
catch: (error) => UploadistaError.fromCode("VIDEO_PROCESSING_FAILED", {
|
|
388
|
-
body: `Frame extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
389
|
-
cause: error
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
//#endregion
|
|
396
|
-
//#region src/video-plugin-layer.ts
|
|
397
|
-
/**
|
|
398
|
-
* Effect Layer for the node-av video plugin
|
|
399
|
-
*
|
|
400
|
-
* This layer provides video processing capabilities using node-av (FFmpeg bindings).
|
|
401
|
-
* Note: node-av includes prebuilt FFmpeg binaries, so no system installation is required.
|
|
402
|
-
*
|
|
403
|
-
* @example
|
|
404
|
-
* ```typescript
|
|
405
|
-
* import { AVNodeVideoPlugin } from "@uploadista/flow-videos-av-node";
|
|
406
|
-
* import { Effect } from "effect";
|
|
407
|
-
*
|
|
408
|
-
* const program = Effect.gen(function* () {
|
|
409
|
-
* const videoPlugin = yield* VideoPlugin;
|
|
410
|
-
* const metadata = yield* videoPlugin.describe(videoBytes);
|
|
411
|
-
* return metadata;
|
|
412
|
-
* });
|
|
413
|
-
*
|
|
414
|
-
* // Run with node-av plugin layer
|
|
415
|
-
* const result = await Effect.runPromise(
|
|
416
|
-
* program.pipe(Effect.provide(AVNodeVideoPluginLive))
|
|
417
|
-
* );
|
|
418
|
-
* ```
|
|
419
|
-
*/
|
|
420
|
-
const AVNodeVideoPlugin = Layer.succeed(VideoPlugin, createAVNodeVideoPlugin());
|
|
421
|
-
/**
|
|
422
|
-
* Effect Layer for the node-av video plugin with availability check
|
|
423
|
-
*
|
|
424
|
-
* This layer checks if node-av is properly installed and logs status information.
|
|
425
|
-
* The plugin will still be created, but operations will fail if node-av is not available.
|
|
426
|
-
*
|
|
427
|
-
* @example
|
|
428
|
-
* ```typescript
|
|
429
|
-
* import { AVNodeVideoPluginWithCheck } from "@uploadista/flow-videos-av-node";
|
|
430
|
-
* import { Effect } from "effect";
|
|
431
|
-
*
|
|
432
|
-
* const program = Effect.gen(function* () {
|
|
433
|
-
* const videoPlugin = yield* VideoPlugin;
|
|
434
|
-
* const metadata = yield* videoPlugin.describe(videoBytes);
|
|
435
|
-
* return metadata;
|
|
436
|
-
* });
|
|
437
|
-
*
|
|
438
|
-
* // Run with node-av plugin layer (with check)
|
|
439
|
-
* const result = await Effect.runPromise(
|
|
440
|
-
* program.pipe(Effect.provide(AVNodeVideoPluginWithCheck))
|
|
441
|
-
* );
|
|
442
|
-
* ```
|
|
443
|
-
*/
|
|
444
|
-
const AVNodeVideoPluginWithCheck = Layer.effectDiscard(Effect.gen(function* () {
|
|
445
|
-
const result = yield* Effect.promise(() => checkAVAvailable());
|
|
446
|
-
if (!result.available) console.warn("⚠️ node-av is not installed or not available.", "\nVideo processing operations will fail.", "\nInstall node-av: npm install node-av");
|
|
447
|
-
else console.log(`✓ node-av ${result.version} detected`);
|
|
448
|
-
})).pipe(Layer.provideMerge(AVNodeVideoPlugin));
|
|
449
|
-
|
|
450
|
-
//#endregion
|
|
451
|
-
export { AVNodeVideoPlugin, AVNodeVideoPluginWithCheck, audioCodecToAVName, checkAVAvailable, codecToAVName, createAVNodeVideoPlugin, formatToExtension, formatToMimeType, imageFormatToEncoder };
|
|
1
|
+
import{FF_ENCODER_AAC as e,FF_ENCODER_LIBAOM_AV1 as t,FF_ENCODER_LIBMP3LAME as n,FF_ENCODER_LIBOPUS as r,FF_ENCODER_LIBVORBIS as i,FF_ENCODER_LIBVPX_VP9 as a,FF_ENCODER_LIBX264 as o,FF_ENCODER_LIBX265 as s,FF_ENCODER_MJPEG as c,FF_ENCODER_PNG as l}from"node-av/constants";import{randomUUID as u}from"node:crypto";import{promises as d}from"node:fs";import{tmpdir as f}from"node:os";import{join as p}from"node:path";import{UploadistaError as m}from"@uploadista/core/errors";import{Effect as h,Layer as g}from"effect";import{Decoder as _,Encoder as v,MediaInput as y,MediaOutput as b}from"node-av/api";import{VideoPlugin as x}from"@uploadista/core/flow";async function S(){try{return await import(`node-av`),{available:!0,version:`3.x`}}catch(e){return{available:!1,error:e instanceof Error?e.message:String(e)}}}const C={mp4:`video/mp4`,webm:`video/webm`,mov:`video/quicktime`,avi:`video/x-msvideo`},w={mp4:`mp4`,webm:`webm`,mov:`mov`,avi:`avi`},T={h264:o,h265:s,vp9:a,av1:t},E={aac:e,mp3:n,opus:r,vorbis:i},D={jpeg:c,mjpeg:c,png:l};async function O(e,t){let n=p(f(),`uploadista-${u()}.${t}`);return await d.writeFile(n,e),n}async function k(e){let t=await d.readFile(e);return new Uint8Array(t)}async function A(e){await Promise.all(e.map(e=>d.unlink(e).catch(()=>{})))}function j(){return{describe:e=>h.tryPromise({try:async()=>{let t=await O(e,`input`);try{await using e=await y.open(t);let n=e.video(),r=e.audio();if(!n)throw Error(`No video stream found`);let i=n.codecpar,a=0;if(n.rFrameRate){let{num:e,den:t}=n.rFrameRate;a=t?e/t:e}let o=`unknown`;if(n.sampleAspectRatio){let{num:e,den:t}=n.sampleAspectRatio;o=`${e}:${t}`}let s=await d.stat(t);return{duration:e.duration||0,width:i.width||0,height:i.height||0,codec:String(i.codecId)||`unknown`,format:e.formatName||`unknown`,bitrate:e.bitRate||0,frameRate:a,aspectRatio:o,hasAudio:!!r,audioCodec:r?.codecpar.codecId?String(r.codecpar.codecId):void 0,audioBitrate:r?.codecpar.bitRate?Number(r.codecpar.bitRate):void 0,size:s.size}}finally{await A([t])}},catch:e=>m.fromCode(`VIDEO_METADATA_EXTRACTION_FAILED`,{body:`Failed to extract video metadata: ${e instanceof Error?e.message:String(e)}`,cause:e})}),transcode:(e,t)=>h.tryPromise({try:async()=>{let n=await O(e,`input`),r=p(f(),`uploadista-${u()}.${t.format}`);try{await using e=await y.open(n),i=await b.open(r);let a=e.video();if(!a)throw Error(`No video stream found`);using o=await _.create(a);let s=t.codec?T[t.codec]:T.h264;using c=await v.create(s,{timeBase:a.timeBase,...t.videoBitrate&&{bitrate:t.videoBitrate}});let l=i.addStream(c);for await(using t of o.frames(e.packets(a.index))){let e=await c.encode(t);e&&(await i.writePacket(e,l),e.free())}await c.flush();let u=await c.receive();for(;u!==null;)await i.writePacket(u,l),u.free(),u=await c.receive();let d=e.audio();if(d){using n=await _.create(d);let r=t.audioCodec?E[t.audioCodec]:E.aac;using a=await v.create(r,{timeBase:d.timeBase,...t.audioBitrate&&{bitrate:t.audioBitrate}});let o=i.addStream(a);for await(using t of n.frames(e.packets(d.index))){let e=await a.encode(t);e&&(await i.writePacket(e,o),e.free())}await a.flush();let s=await a.receive();for(;s!==null;)await i.writePacket(s,o),s.free(),s=await a.receive()}return await k(r)}finally{await A([n,r])}},catch:e=>m.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Transcode failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),resize:(e,t)=>h.tryPromise({try:async()=>{let n=await O(e,`input`),r=p(f(),`uploadista-${u()}.mp4`);try{await using e=await y.open(n),i=await b.open(r);let a=e.video();if(!a)throw Error(`No video stream found`);using o=await _.create(a);if(!t.width&&!t.height)throw Error(`Either width or height must be specified`);using s=await v.create(T.h264,{timeBase:a.timeBase});let c=i.addStream(s);for await(using t of o.frames(e.packets(a.index))){let e=await s.encode(t);e&&(await i.writePacket(e,c),e.free())}await s.flush();let l=await s.receive();for(;l!==null;)await i.writePacket(l,c),l.free(),l=await s.receive();let u=e.audio();if(u){using t=await _.create(u),n=await v.create(E.aac,{timeBase:u.timeBase});let r=i.addStream(n);for await(using a of t.frames(e.packets(u.index))){let e=await n.encode(a);e&&(await i.writePacket(e,r),e.free())}await n.flush();let a=await n.receive();for(;a!==null;)await i.writePacket(a,r),a.free(),a=await n.receive()}return await k(r)}finally{await A([n,r])}},catch:e=>m.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Resize failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),trim:(e,t)=>h.tryPromise({try:async()=>{let n=await O(e,`input`),r=p(f(),`uploadista-${u()}.mp4`);try{await using e=await y.open(n),i=await b.open(r);let a=e.video();if(!a)throw Error(`No video stream found`);let o;o=t.duration===void 0?t.endTime===void 0?e.duration||1/0:t.endTime:t.startTime+t.duration;using s=await _.create(a),c=await v.create(T.h264,{timeBase:a.timeBase});let l=i.addStream(c);for await(using n of s.frames(e.packets(a.index))){let e=n.pts||0n,r=a.timeBase?a.timeBase.num/a.timeBase.den:1,s=Number(e)*r;if(s>=t.startTime&&s<o){let e=await c.encode(n);e&&(await i.writePacket(e,l),e.free())}if(s>=o)break}await c.flush();let u=await c.receive();for(;u!==null;)await i.writePacket(u,l),u.free(),u=await c.receive();let d=e.audio();if(d){using n=await _.create(d),r=await v.create(E.aac,{timeBase:d.timeBase});let a=i.addStream(r);for await(using s of n.frames(e.packets(d.index))){let e=s.pts||0n,n=d.timeBase?d.timeBase.num/d.timeBase.den:1,c=Number(e)*n;if(c>=t.startTime&&c<o){let e=await r.encode(s);e&&(await i.writePacket(e,a),e.free())}if(c>=o)break}await r.flush();let s=await r.receive();for(;s!==null;)await i.writePacket(s,a),s.free(),s=await r.receive()}return await k(r)}finally{await A([n,r])}},catch:e=>m.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Trim failed: ${e instanceof Error?e.message:String(e)}`,cause:e})}),extractFrame:(e,t)=>h.tryPromise({try:async()=>{let n=await O(e,`input`),r=t.format||`jpeg`,i=p(f(),`uploadista-${u()}.${r}`);try{await using e=await y.open(n);let a=e.video();if(!a)throw Error(`No video stream found`);using o=await _.create(a);let s=!1,c=t.timestamp;for await(using t of o.frames(e.packets(a.index))){let e=t.pts||0n,n=a.timeBase?a.timeBase.num/a.timeBase.den:1;if(Number(e)*n>=c){let e=D[r]||D.jpeg;using n=await v.create(e,{timeBase:{num:1,den:1}});let a=await n.encode(t);if(a?.data){await d.writeFile(i,a.data),a.free(),s=!0;break}}}if(!s)throw Error(`No frame found at timestamp ${c}`);return await k(i)}finally{await A([n,i])}},catch:e=>m.fromCode(`VIDEO_PROCESSING_FAILED`,{body:`Frame extraction failed: ${e instanceof Error?e.message:String(e)}`,cause:e})})}}const M=g.succeed(x,j()),N=g.effectDiscard(h.gen(function*(){let e=yield*h.promise(()=>S());e.available?console.log(`✓ node-av ${e.version} detected`):console.warn(`⚠️ node-av is not installed or not available.`,`
|
|
2
|
+
Video processing operations will fail.`,`
|
|
3
|
+
Install node-av: npm install node-av`)})).pipe(g.provideMerge(M));export{M as AVNodeVideoPlugin,N as AVNodeVideoPluginWithCheck,E as audioCodecToAVName,S as checkAVAvailable,T as codecToAVName,j as createAVNodeVideoPlugin,w as formatToExtension,C as formatToMimeType,D as imageFormatToEncoder};
|
|
452
4
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["formatToMimeType: Record<TranscodeVideoParams[\"format\"], string>","formatToExtension: Record<TranscodeVideoParams[\"format\"], string>","codecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"codec\"]>,\n FFEncoderCodec\n>","audioCodecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"audioCodec\"]>,\n FFEncoderCodec\n>","imageFormatToEncoder: Record<string, FFEncoderCodec>","fs","fs","transcodeVPacket: Packet | null","transcodeAPacket: Packet | null","vPacket: Packet | null","resizeAPacket: Packet | null","endTime: number","trimVPacket: Packet | null","trimAPacket: Packet | null"],"sources":["../src/utils/av-check.ts","../src/utils/format-mappings.ts","../src/utils/temp-file-manager.ts","../src/video-plugin.ts","../src/video-plugin-layer.ts"],"sourcesContent":["/**\n * Result of node-av availability check\n */\nexport type AVCheckResult = {\n available: boolean;\n version?: string;\n error?: string;\n};\n\n/**\n * Checks if node-av is available and can access FFmpeg binaries\n * @returns Promise with availability status and version info\n */\nexport async function checkAVAvailable(): Promise<AVCheckResult> {\n try {\n // Try to import node-av to verify it's available\n await import(\"node-av\");\n\n // node-av includes FFmpeg binaries, so if import succeeds, it's available\n return {\n available: true,\n version: \"3.x\", // node-av version is in package.json\n };\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n","import type { TranscodeVideoParams } from \"@uploadista/core/flow\";\nimport type { FFEncoderCodec } from \"node-av/constants\";\nimport {\n FF_ENCODER_AAC,\n FF_ENCODER_LIBAOM_AV1,\n FF_ENCODER_LIBMP3LAME,\n FF_ENCODER_LIBOPUS,\n FF_ENCODER_LIBVORBIS,\n FF_ENCODER_LIBVPX_VP9,\n FF_ENCODER_LIBX264,\n FF_ENCODER_LIBX265,\n FF_ENCODER_MJPEG,\n FF_ENCODER_PNG,\n} from \"node-av/constants\";\n\n/**\n * Maps video format to MIME type\n */\nexport const formatToMimeType: Record<TranscodeVideoParams[\"format\"], string> =\n {\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n avi: \"video/x-msvideo\",\n };\n\n/**\n * Maps video format to file extension\n */\nexport const formatToExtension: Record<TranscodeVideoParams[\"format\"], string> =\n {\n mp4: \"mp4\",\n webm: \"webm\",\n mov: \"mov\",\n avi: \"avi\",\n };\n\n/**\n * Maps codec parameter to node-av codec constant\n */\nexport const codecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"codec\"]>,\n FFEncoderCodec\n> = {\n h264: FF_ENCODER_LIBX264,\n h265: FF_ENCODER_LIBX265,\n vp9: FF_ENCODER_LIBVPX_VP9,\n av1: FF_ENCODER_LIBAOM_AV1,\n};\n\n/**\n * Maps audio codec parameter to node-av audio codec constant\n */\nexport const audioCodecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"audioCodec\"]>,\n FFEncoderCodec\n> = {\n aac: FF_ENCODER_AAC,\n mp3: FF_ENCODER_LIBMP3LAME,\n opus: FF_ENCODER_LIBOPUS,\n vorbis: FF_ENCODER_LIBVORBIS,\n};\n\n/**\n * Maps image format to encoder constant\n */\nexport const imageFormatToEncoder: Record<string, FFEncoderCodec> = {\n jpeg: FF_ENCODER_MJPEG,\n mjpeg: FF_ENCODER_MJPEG,\n png: FF_ENCODER_PNG,\n};\n","import { randomUUID } from \"node:crypto\";\nimport { promises as fs } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Writes a Uint8Array to a temporary file\n * @param bytes - The bytes to write\n * @param extension - The file extension (without dot)\n * @returns The path to the temporary file\n */\nexport async function bytesToTempFile(\n bytes: Uint8Array,\n extension: string,\n): Promise<string> {\n const tempPath = join(tmpdir(), `uploadista-${randomUUID()}.${extension}`);\n await fs.writeFile(tempPath, bytes);\n return tempPath;\n}\n\n/**\n * Reads a temporary file into a Uint8Array\n * @param path - The path to the file\n * @returns The file contents as Uint8Array\n */\nexport async function tempFileToBytes(path: string): Promise<Uint8Array> {\n const buffer = await fs.readFile(path);\n return new Uint8Array(buffer);\n}\n\n/**\n * Cleans up temporary files, suppressing errors\n * @param paths - The paths to delete\n */\nexport async function cleanup(paths: string[]): Promise<void> {\n await Promise.all(\n paths.map((p) =>\n fs.unlink(p).catch(() => {\n // Suppress errors during cleanup\n }),\n ),\n );\n}\n","import { randomUUID } from \"node:crypto\";\nimport { promises as fs } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport type {\n DescribeVideoMetadata,\n VideoPluginShape,\n} from \"@uploadista/core/flow\";\nimport { Effect } from \"effect\";\nimport { Decoder, Encoder, MediaInput, MediaOutput } from \"node-av/api\";\nimport type { Packet } from \"node-av/lib\";\nimport {\n audioCodecToAVName,\n codecToAVName,\n imageFormatToEncoder,\n} from \"./utils/format-mappings\";\nimport {\n bytesToTempFile,\n cleanup,\n tempFileToBytes,\n} from \"./utils/temp-file-manager\";\n\n/**\n * Creates a node-av based video processing plugin\n */\nexport function createAVNodeVideoPlugin(): VideoPluginShape {\n return {\n describe: (input) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n\n const videoStream = mediaInput.video();\n const audioStream = mediaInput.audio();\n\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n const videoCodecParams = videoStream.codecpar;\n\n // Calculate frame rate from rational number\n let frameRate = 0;\n if (videoStream.rFrameRate) {\n const { num, den } = videoStream.rFrameRate;\n frameRate = den ? num / den : num;\n }\n\n // Get aspect ratio\n let aspectRatio = \"unknown\";\n if (videoStream.sampleAspectRatio) {\n const { num, den } = videoStream.sampleAspectRatio;\n aspectRatio = `${num}:${den}`;\n }\n\n // Get file size\n const stats = await fs.stat(inputPath);\n\n const metadata: DescribeVideoMetadata = {\n duration: mediaInput.duration || 0,\n width: videoCodecParams.width || 0,\n height: videoCodecParams.height || 0,\n codec: String(videoCodecParams.codecId) || \"unknown\",\n format: mediaInput.formatName || \"unknown\",\n bitrate: mediaInput.bitRate || 0,\n frameRate,\n aspectRatio,\n hasAudio: !!audioStream,\n audioCodec: audioStream?.codecpar.codecId\n ? String(audioStream.codecpar.codecId)\n : undefined,\n audioBitrate: audioStream?.codecpar.bitRate\n ? Number(audioStream.codecpar.bitRate)\n : undefined,\n size: stats.size,\n };\n\n return metadata;\n } finally {\n await cleanup([inputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_METADATA_EXTRACTION_FAILED\", {\n body: `Failed to extract video metadata: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n transcode: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(\n tmpdir(),\n `uploadista-${randomUUID()}.${options.format}`,\n );\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n\n // Determine encoder codec\n const encoderCodec = options.codec\n ? codecToAVName[options.codec]\n : codecToAVName.h264;\n\n using videoEncoder = await Encoder.create(encoderCodec, {\n timeBase: videoStream.timeBase,\n ...(options.videoBitrate && { bitrate: options.videoBitrate }),\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let transcodeVPacket: Packet | null = await videoEncoder.receive();\n while (transcodeVPacket !== null) {\n await mediaOutput.writePacket(transcodeVPacket, videoOutputIndex);\n transcodeVPacket.free();\n transcodeVPacket = await videoEncoder.receive();\n }\n\n // Handle audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n\n const audioEncoderCodec = options.audioCodec\n ? audioCodecToAVName[options.audioCodec]\n : audioCodecToAVName.aac;\n\n using audioEncoder = await Encoder.create(audioEncoderCodec, {\n timeBase: audioStream.timeBase,\n ...(options.audioBitrate && { bitrate: options.audioBitrate }),\n });\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n // Process audio frames\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let transcodeAPacket: Packet | null =\n await audioEncoder.receive();\n while (transcodeAPacket !== null) {\n await mediaOutput.writePacket(\n transcodeAPacket,\n audioOutputIndex,\n );\n transcodeAPacket.free();\n transcodeAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Transcode failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n resize: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n\n // TODO: Implement proper resizing with FilterAPI\n // Currently, resize functionality is limited because node-av's Encoder\n // auto-initializes from the first frame it receives. To implement proper\n // resizing, we would need to:\n // 1. Use FilterAPI.create('scale', { width, height }) to create a scale filter\n // 2. Pass decoded frames through the filter before encoding\n // For now, this function will pass through frames without resizing.\n\n // Validate that resize parameters are provided\n if (!options.width && !options.height) {\n throw new Error(\"Either width or height must be specified\");\n }\n\n using videoEncoder = await Encoder.create(codecToAVName.h264, {\n timeBase: videoStream.timeBase,\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames with resizing\n // Note: For production use, consider using FilterAPI for better quality scaling\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // TODO: Apply scale filter here for better quality\n // For now, encoder will handle basic resizing\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let vPacket: Packet | null = await videoEncoder.receive();\n while (vPacket !== null) {\n await mediaOutput.writePacket(vPacket, videoOutputIndex);\n vPacket.free();\n vPacket = await videoEncoder.receive();\n }\n\n // Copy audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n using audioEncoder = await Encoder.create(\n audioCodecToAVName.aac,\n {\n timeBase: audioStream.timeBase,\n },\n );\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let resizeAPacket: Packet | null = await audioEncoder.receive();\n while (resizeAPacket !== null) {\n await mediaOutput.writePacket(resizeAPacket, audioOutputIndex);\n resizeAPacket.free();\n resizeAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Resize failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n trim: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n // Calculate end time\n let endTime: number;\n if (options.duration !== undefined) {\n endTime = options.startTime + options.duration;\n } else if (options.endTime !== undefined) {\n endTime = options.endTime;\n } else {\n endTime = mediaInput.duration || Number.POSITIVE_INFINITY;\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n using videoEncoder = await Encoder.create(codecToAVName.h264, {\n timeBase: videoStream.timeBase,\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames within time range\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // Calculate frame timestamp\n const pts = frame.pts || 0n;\n const timeBase = videoStream.timeBase\n ? videoStream.timeBase.num / videoStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n if (timestamp >= options.startTime && timestamp < endTime) {\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n if (timestamp >= endTime) break;\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let trimVPacket: Packet | null = await videoEncoder.receive();\n while (trimVPacket !== null) {\n await mediaOutput.writePacket(trimVPacket, videoOutputIndex);\n trimVPacket.free();\n trimVPacket = await videoEncoder.receive();\n }\n\n // Handle audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n using audioEncoder = await Encoder.create(\n audioCodecToAVName.aac,\n {\n timeBase: audioStream.timeBase,\n },\n );\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const pts = frame.pts || 0n;\n const timeBase = audioStream.timeBase\n ? audioStream.timeBase.num / audioStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n if (timestamp >= options.startTime && timestamp < endTime) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n if (timestamp >= endTime) break;\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let trimAPacket: Packet | null = await audioEncoder.receive();\n while (trimAPacket !== null) {\n await mediaOutput.writePacket(trimAPacket, audioOutputIndex);\n trimAPacket.free();\n trimAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Trim failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n extractFrame: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const format = options.format || \"jpeg\";\n const outputPath = join(\n tmpdir(),\n `uploadista-${randomUUID()}.${format}`,\n );\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using decoder = await Decoder.create(videoStream);\n\n let frameFound = false;\n const targetTimestamp = options.timestamp;\n\n for await (using frame of decoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // Calculate frame timestamp\n const pts = frame.pts || 0n;\n const timeBase = videoStream.timeBase\n ? videoStream.timeBase.num / videoStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n // Look for frame at or after target timestamp\n if (timestamp >= targetTimestamp) {\n // Use an image encoder to save the frame\n const encoderCodec =\n imageFormatToEncoder[format] || imageFormatToEncoder.jpeg;\n using imageEncoder = await Encoder.create(encoderCodec, {\n timeBase: { num: 1, den: 1 },\n });\n\n // Encode the frame as image\n // The encoder will initialize from the first frame's properties\n const packet = await imageEncoder.encode(frame);\n if (packet?.data) {\n await fs.writeFile(outputPath, packet.data);\n packet.free();\n frameFound = true;\n break;\n }\n }\n }\n\n if (!frameFound) {\n throw new Error(`No frame found at timestamp ${targetTimestamp}`);\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Frame extraction failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n };\n}\n","import { VideoPlugin } from \"@uploadista/core/flow\";\nimport { Effect, Layer } from \"effect\";\nimport { checkAVAvailable } from \"./utils/av-check\";\nimport { createAVNodeVideoPlugin } from \"./video-plugin\";\n\n/**\n * Effect Layer for the node-av video plugin\n *\n * This layer provides video processing capabilities using node-av (FFmpeg bindings).\n * Note: node-av includes prebuilt FFmpeg binaries, so no system installation is required.\n *\n * @example\n * ```typescript\n * import { AVNodeVideoPlugin } from \"@uploadista/flow-videos-av-node\";\n * import { Effect } from \"effect\";\n *\n * const program = Effect.gen(function* () {\n * const videoPlugin = yield* VideoPlugin;\n * const metadata = yield* videoPlugin.describe(videoBytes);\n * return metadata;\n * });\n *\n * // Run with node-av plugin layer\n * const result = await Effect.runPromise(\n * program.pipe(Effect.provide(AVNodeVideoPluginLive))\n * );\n * ```\n */\nexport const AVNodeVideoPlugin = Layer.succeed(\n VideoPlugin,\n createAVNodeVideoPlugin(),\n);\n\n/**\n * Effect Layer for the node-av video plugin with availability check\n *\n * This layer checks if node-av is properly installed and logs status information.\n * The plugin will still be created, but operations will fail if node-av is not available.\n *\n * @example\n * ```typescript\n * import { AVNodeVideoPluginWithCheck } from \"@uploadista/flow-videos-av-node\";\n * import { Effect } from \"effect\";\n *\n * const program = Effect.gen(function* () {\n * const videoPlugin = yield* VideoPlugin;\n * const metadata = yield* videoPlugin.describe(videoBytes);\n * return metadata;\n * });\n *\n * // Run with node-av plugin layer (with check)\n * const result = await Effect.runPromise(\n * program.pipe(Effect.provide(AVNodeVideoPluginWithCheck))\n * );\n * ```\n */\nexport const AVNodeVideoPluginWithCheck = Layer.effectDiscard(\n Effect.gen(function* () {\n const result = yield* Effect.promise(() => checkAVAvailable());\n\n if (!result.available) {\n console.warn(\n \"⚠️ node-av is not installed or not available.\",\n \"\\nVideo processing operations will fail.\",\n \"\\nInstall node-av: npm install node-av\",\n );\n } else {\n console.log(`✓ node-av ${result.version} detected`);\n }\n }),\n).pipe(Layer.provideMerge(AVNodeVideoPlugin));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["formatToMimeType: Record<TranscodeVideoParams[\"format\"], string>","formatToExtension: Record<TranscodeVideoParams[\"format\"], string>","codecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"codec\"]>,\n FFEncoderCodec\n>","audioCodecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"audioCodec\"]>,\n FFEncoderCodec\n>","imageFormatToEncoder: Record<string, FFEncoderCodec>","fs","fs","transcodeVPacket: Packet | null","transcodeAPacket: Packet | null","vPacket: Packet | null","resizeAPacket: Packet | null","endTime: number","trimVPacket: Packet | null","trimAPacket: Packet | null"],"sources":["../src/utils/av-check.ts","../src/utils/format-mappings.ts","../src/utils/temp-file-manager.ts","../src/video-plugin.ts","../src/video-plugin-layer.ts"],"sourcesContent":["/**\n * Result of node-av availability check\n */\nexport type AVCheckResult = {\n available: boolean;\n version?: string;\n error?: string;\n};\n\n/**\n * Checks if node-av is available and can access FFmpeg binaries\n * @returns Promise with availability status and version info\n */\nexport async function checkAVAvailable(): Promise<AVCheckResult> {\n try {\n // Try to import node-av to verify it's available\n await import(\"node-av\");\n\n // node-av includes FFmpeg binaries, so if import succeeds, it's available\n return {\n available: true,\n version: \"3.x\", // node-av version is in package.json\n };\n } catch (error) {\n return {\n available: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n","import type { TranscodeVideoParams } from \"@uploadista/core/flow\";\nimport type { FFEncoderCodec } from \"node-av/constants\";\nimport {\n FF_ENCODER_AAC,\n FF_ENCODER_LIBAOM_AV1,\n FF_ENCODER_LIBMP3LAME,\n FF_ENCODER_LIBOPUS,\n FF_ENCODER_LIBVORBIS,\n FF_ENCODER_LIBVPX_VP9,\n FF_ENCODER_LIBX264,\n FF_ENCODER_LIBX265,\n FF_ENCODER_MJPEG,\n FF_ENCODER_PNG,\n} from \"node-av/constants\";\n\n/**\n * Maps video format to MIME type\n */\nexport const formatToMimeType: Record<TranscodeVideoParams[\"format\"], string> =\n {\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n avi: \"video/x-msvideo\",\n };\n\n/**\n * Maps video format to file extension\n */\nexport const formatToExtension: Record<TranscodeVideoParams[\"format\"], string> =\n {\n mp4: \"mp4\",\n webm: \"webm\",\n mov: \"mov\",\n avi: \"avi\",\n };\n\n/**\n * Maps codec parameter to node-av codec constant\n */\nexport const codecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"codec\"]>,\n FFEncoderCodec\n> = {\n h264: FF_ENCODER_LIBX264,\n h265: FF_ENCODER_LIBX265,\n vp9: FF_ENCODER_LIBVPX_VP9,\n av1: FF_ENCODER_LIBAOM_AV1,\n};\n\n/**\n * Maps audio codec parameter to node-av audio codec constant\n */\nexport const audioCodecToAVName: Record<\n NonNullable<TranscodeVideoParams[\"audioCodec\"]>,\n FFEncoderCodec\n> = {\n aac: FF_ENCODER_AAC,\n mp3: FF_ENCODER_LIBMP3LAME,\n opus: FF_ENCODER_LIBOPUS,\n vorbis: FF_ENCODER_LIBVORBIS,\n};\n\n/**\n * Maps image format to encoder constant\n */\nexport const imageFormatToEncoder: Record<string, FFEncoderCodec> = {\n jpeg: FF_ENCODER_MJPEG,\n mjpeg: FF_ENCODER_MJPEG,\n png: FF_ENCODER_PNG,\n};\n","import { randomUUID } from \"node:crypto\";\nimport { promises as fs } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Writes a Uint8Array to a temporary file\n * @param bytes - The bytes to write\n * @param extension - The file extension (without dot)\n * @returns The path to the temporary file\n */\nexport async function bytesToTempFile(\n bytes: Uint8Array,\n extension: string,\n): Promise<string> {\n const tempPath = join(tmpdir(), `uploadista-${randomUUID()}.${extension}`);\n await fs.writeFile(tempPath, bytes);\n return tempPath;\n}\n\n/**\n * Reads a temporary file into a Uint8Array\n * @param path - The path to the file\n * @returns The file contents as Uint8Array\n */\nexport async function tempFileToBytes(path: string): Promise<Uint8Array> {\n const buffer = await fs.readFile(path);\n return new Uint8Array(buffer);\n}\n\n/**\n * Cleans up temporary files, suppressing errors\n * @param paths - The paths to delete\n */\nexport async function cleanup(paths: string[]): Promise<void> {\n await Promise.all(\n paths.map((p) =>\n fs.unlink(p).catch(() => {\n // Suppress errors during cleanup\n }),\n ),\n );\n}\n","import { randomUUID } from \"node:crypto\";\nimport { promises as fs } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { UploadistaError } from \"@uploadista/core/errors\";\nimport type {\n DescribeVideoMetadata,\n VideoPluginShape,\n} from \"@uploadista/core/flow\";\nimport { Effect } from \"effect\";\nimport { Decoder, Encoder, MediaInput, MediaOutput } from \"node-av/api\";\nimport type { Packet } from \"node-av/lib\";\nimport {\n audioCodecToAVName,\n codecToAVName,\n imageFormatToEncoder,\n} from \"./utils/format-mappings\";\nimport {\n bytesToTempFile,\n cleanup,\n tempFileToBytes,\n} from \"./utils/temp-file-manager\";\n\n/**\n * Creates a node-av based video processing plugin\n */\nexport function createAVNodeVideoPlugin(): VideoPluginShape {\n return {\n describe: (input) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n\n const videoStream = mediaInput.video();\n const audioStream = mediaInput.audio();\n\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n const videoCodecParams = videoStream.codecpar;\n\n // Calculate frame rate from rational number\n let frameRate = 0;\n if (videoStream.rFrameRate) {\n const { num, den } = videoStream.rFrameRate;\n frameRate = den ? num / den : num;\n }\n\n // Get aspect ratio\n let aspectRatio = \"unknown\";\n if (videoStream.sampleAspectRatio) {\n const { num, den } = videoStream.sampleAspectRatio;\n aspectRatio = `${num}:${den}`;\n }\n\n // Get file size\n const stats = await fs.stat(inputPath);\n\n const metadata: DescribeVideoMetadata = {\n duration: mediaInput.duration || 0,\n width: videoCodecParams.width || 0,\n height: videoCodecParams.height || 0,\n codec: String(videoCodecParams.codecId) || \"unknown\",\n format: mediaInput.formatName || \"unknown\",\n bitrate: mediaInput.bitRate || 0,\n frameRate,\n aspectRatio,\n hasAudio: !!audioStream,\n audioCodec: audioStream?.codecpar.codecId\n ? String(audioStream.codecpar.codecId)\n : undefined,\n audioBitrate: audioStream?.codecpar.bitRate\n ? Number(audioStream.codecpar.bitRate)\n : undefined,\n size: stats.size,\n };\n\n return metadata;\n } finally {\n await cleanup([inputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_METADATA_EXTRACTION_FAILED\", {\n body: `Failed to extract video metadata: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n transcode: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(\n tmpdir(),\n `uploadista-${randomUUID()}.${options.format}`,\n );\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n\n // Determine encoder codec\n const encoderCodec = options.codec\n ? codecToAVName[options.codec]\n : codecToAVName.h264;\n\n using videoEncoder = await Encoder.create(encoderCodec, {\n timeBase: videoStream.timeBase,\n ...(options.videoBitrate && { bitrate: options.videoBitrate }),\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let transcodeVPacket: Packet | null = await videoEncoder.receive();\n while (transcodeVPacket !== null) {\n await mediaOutput.writePacket(transcodeVPacket, videoOutputIndex);\n transcodeVPacket.free();\n transcodeVPacket = await videoEncoder.receive();\n }\n\n // Handle audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n\n const audioEncoderCodec = options.audioCodec\n ? audioCodecToAVName[options.audioCodec]\n : audioCodecToAVName.aac;\n\n using audioEncoder = await Encoder.create(audioEncoderCodec, {\n timeBase: audioStream.timeBase,\n ...(options.audioBitrate && { bitrate: options.audioBitrate }),\n });\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n // Process audio frames\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let transcodeAPacket: Packet | null =\n await audioEncoder.receive();\n while (transcodeAPacket !== null) {\n await mediaOutput.writePacket(\n transcodeAPacket,\n audioOutputIndex,\n );\n transcodeAPacket.free();\n transcodeAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Transcode failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n resize: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n\n // TODO: Implement proper resizing with FilterAPI\n // Currently, resize functionality is limited because node-av's Encoder\n // auto-initializes from the first frame it receives. To implement proper\n // resizing, we would need to:\n // 1. Use FilterAPI.create('scale', { width, height }) to create a scale filter\n // 2. Pass decoded frames through the filter before encoding\n // For now, this function will pass through frames without resizing.\n\n // Validate that resize parameters are provided\n if (!options.width && !options.height) {\n throw new Error(\"Either width or height must be specified\");\n }\n\n using videoEncoder = await Encoder.create(codecToAVName.h264, {\n timeBase: videoStream.timeBase,\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames with resizing\n // Note: For production use, consider using FilterAPI for better quality scaling\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // TODO: Apply scale filter here for better quality\n // For now, encoder will handle basic resizing\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let vPacket: Packet | null = await videoEncoder.receive();\n while (vPacket !== null) {\n await mediaOutput.writePacket(vPacket, videoOutputIndex);\n vPacket.free();\n vPacket = await videoEncoder.receive();\n }\n\n // Copy audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n using audioEncoder = await Encoder.create(\n audioCodecToAVName.aac,\n {\n timeBase: audioStream.timeBase,\n },\n );\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let resizeAPacket: Packet | null = await audioEncoder.receive();\n while (resizeAPacket !== null) {\n await mediaOutput.writePacket(resizeAPacket, audioOutputIndex);\n resizeAPacket.free();\n resizeAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Resize failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n trim: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const outputPath = join(tmpdir(), `uploadista-${randomUUID()}.mp4`);\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n await using mediaOutput = await MediaOutput.open(outputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n // Calculate end time\n let endTime: number;\n if (options.duration !== undefined) {\n endTime = options.startTime + options.duration;\n } else if (options.endTime !== undefined) {\n endTime = options.endTime;\n } else {\n endTime = mediaInput.duration || Number.POSITIVE_INFINITY;\n }\n\n using videoDecoder = await Decoder.create(videoStream);\n using videoEncoder = await Encoder.create(codecToAVName.h264, {\n timeBase: videoStream.timeBase,\n });\n\n const videoOutputIndex = mediaOutput.addStream(videoEncoder);\n\n // Process video frames within time range\n for await (using frame of videoDecoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // Calculate frame timestamp\n const pts = frame.pts || 0n;\n const timeBase = videoStream.timeBase\n ? videoStream.timeBase.num / videoStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n if (timestamp >= options.startTime && timestamp < endTime) {\n const packet = await videoEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, videoOutputIndex);\n packet.free();\n }\n }\n\n if (timestamp >= endTime) break;\n }\n\n // Flush remaining packets\n await videoEncoder.flush();\n let trimVPacket: Packet | null = await videoEncoder.receive();\n while (trimVPacket !== null) {\n await mediaOutput.writePacket(trimVPacket, videoOutputIndex);\n trimVPacket.free();\n trimVPacket = await videoEncoder.receive();\n }\n\n // Handle audio stream if present\n const audioStream = mediaInput.audio();\n if (audioStream) {\n using audioDecoder = await Decoder.create(audioStream);\n using audioEncoder = await Encoder.create(\n audioCodecToAVName.aac,\n {\n timeBase: audioStream.timeBase,\n },\n );\n\n const audioOutputIndex = mediaOutput.addStream(audioEncoder);\n\n for await (using frame of audioDecoder.frames(\n mediaInput.packets(audioStream.index),\n )) {\n const pts = frame.pts || 0n;\n const timeBase = audioStream.timeBase\n ? audioStream.timeBase.num / audioStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n if (timestamp >= options.startTime && timestamp < endTime) {\n const packet = await audioEncoder.encode(frame);\n if (packet) {\n await mediaOutput.writePacket(packet, audioOutputIndex);\n packet.free();\n }\n }\n\n if (timestamp >= endTime) break;\n }\n\n // Flush remaining packets\n await audioEncoder.flush();\n let trimAPacket: Packet | null = await audioEncoder.receive();\n while (trimAPacket !== null) {\n await mediaOutput.writePacket(trimAPacket, audioOutputIndex);\n trimAPacket.free();\n trimAPacket = await audioEncoder.receive();\n }\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Trim failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n\n extractFrame: (input, options) =>\n Effect.tryPromise({\n try: async () => {\n const inputPath = await bytesToTempFile(input, \"input\");\n const format = options.format || \"jpeg\";\n const outputPath = join(\n tmpdir(),\n `uploadista-${randomUUID()}.${format}`,\n );\n\n try {\n await using mediaInput = await MediaInput.open(inputPath);\n\n const videoStream = mediaInput.video();\n if (!videoStream) {\n throw new Error(\"No video stream found\");\n }\n\n using decoder = await Decoder.create(videoStream);\n\n let frameFound = false;\n const targetTimestamp = options.timestamp;\n\n for await (using frame of decoder.frames(\n mediaInput.packets(videoStream.index),\n )) {\n // Calculate frame timestamp\n const pts = frame.pts || 0n;\n const timeBase = videoStream.timeBase\n ? videoStream.timeBase.num / videoStream.timeBase.den\n : 1;\n const timestamp = Number(pts) * timeBase;\n\n // Look for frame at or after target timestamp\n if (timestamp >= targetTimestamp) {\n // Use an image encoder to save the frame\n const encoderCodec =\n imageFormatToEncoder[format] || imageFormatToEncoder.jpeg;\n using imageEncoder = await Encoder.create(encoderCodec, {\n timeBase: { num: 1, den: 1 },\n });\n\n // Encode the frame as image\n // The encoder will initialize from the first frame's properties\n const packet = await imageEncoder.encode(frame);\n if (packet?.data) {\n await fs.writeFile(outputPath, packet.data);\n packet.free();\n frameFound = true;\n break;\n }\n }\n }\n\n if (!frameFound) {\n throw new Error(`No frame found at timestamp ${targetTimestamp}`);\n }\n\n const output = await tempFileToBytes(outputPath);\n return output;\n } finally {\n await cleanup([inputPath, outputPath]);\n }\n },\n catch: (error) =>\n UploadistaError.fromCode(\"VIDEO_PROCESSING_FAILED\", {\n body: `Frame extraction failed: ${error instanceof Error ? error.message : String(error)}`,\n cause: error,\n }),\n }),\n };\n}\n","import { VideoPlugin } from \"@uploadista/core/flow\";\nimport { Effect, Layer } from \"effect\";\nimport { checkAVAvailable } from \"./utils/av-check\";\nimport { createAVNodeVideoPlugin } from \"./video-plugin\";\n\n/**\n * Effect Layer for the node-av video plugin\n *\n * This layer provides video processing capabilities using node-av (FFmpeg bindings).\n * Note: node-av includes prebuilt FFmpeg binaries, so no system installation is required.\n *\n * @example\n * ```typescript\n * import { AVNodeVideoPlugin } from \"@uploadista/flow-videos-av-node\";\n * import { Effect } from \"effect\";\n *\n * const program = Effect.gen(function* () {\n * const videoPlugin = yield* VideoPlugin;\n * const metadata = yield* videoPlugin.describe(videoBytes);\n * return metadata;\n * });\n *\n * // Run with node-av plugin layer\n * const result = await Effect.runPromise(\n * program.pipe(Effect.provide(AVNodeVideoPluginLive))\n * );\n * ```\n */\nexport const AVNodeVideoPlugin = Layer.succeed(\n VideoPlugin,\n createAVNodeVideoPlugin(),\n);\n\n/**\n * Effect Layer for the node-av video plugin with availability check\n *\n * This layer checks if node-av is properly installed and logs status information.\n * The plugin will still be created, but operations will fail if node-av is not available.\n *\n * @example\n * ```typescript\n * import { AVNodeVideoPluginWithCheck } from \"@uploadista/flow-videos-av-node\";\n * import { Effect } from \"effect\";\n *\n * const program = Effect.gen(function* () {\n * const videoPlugin = yield* VideoPlugin;\n * const metadata = yield* videoPlugin.describe(videoBytes);\n * return metadata;\n * });\n *\n * // Run with node-av plugin layer (with check)\n * const result = await Effect.runPromise(\n * program.pipe(Effect.provide(AVNodeVideoPluginWithCheck))\n * );\n * ```\n */\nexport const AVNodeVideoPluginWithCheck = Layer.effectDiscard(\n Effect.gen(function* () {\n const result = yield* Effect.promise(() => checkAVAvailable());\n\n if (!result.available) {\n console.warn(\n \"⚠️ node-av is not installed or not available.\",\n \"\\nVideo processing operations will fail.\",\n \"\\nInstall node-av: npm install node-av\",\n );\n } else {\n console.log(`✓ node-av ${result.version} detected`);\n }\n }),\n).pipe(Layer.provideMerge(AVNodeVideoPlugin));\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/flow-videos-av-node",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.13-beta.
|
|
4
|
+
"version": "0.0.13-beta.5",
|
|
5
5
|
"description": "FFmpeg video processing plugin for Uploadista Flow with av-node",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"node-av": "^3.1.3",
|
|
18
18
|
"effect": "3.19.2",
|
|
19
19
|
"zod": "4.1.12",
|
|
20
|
-
"@uploadista/core": "0.0.13-beta.
|
|
20
|
+
"@uploadista/core": "0.0.13-beta.5"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/fluent-ffmpeg": "^2.1.28",
|
|
24
24
|
"@types/node": "24.10.0",
|
|
25
25
|
"tsdown": "0.16.0",
|
|
26
|
-
"@uploadista/typescript-config": "0.0.13-beta.
|
|
26
|
+
"@uploadista/typescript-config": "0.0.13-beta.5"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsdown",
|