Setting the file. One moment.
Dither Test · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page ⋯
scripts/11 files
scripts/dither.test.mjs
scripts/ dither.test.mjs
JavaScript · 176 lines · 5 KB
from
"node:url"
;
8
9 const SCRIPT = fileURLToPath ( new URL ( "./dither.mjs" , import . meta .url));
10 const HAS_FFMPEG =
11 spawnSync ( "ffmpeg" , [ "-version" ], { stdio: "ignore" }).status === 0 &&
12 spawnSync ( "ffprobe" , [ "-version" ], { stdio: "ignore" }).status === 0 ;
13
14 function fixture () {
15 const dir = mkdtempSync ( join ( tmpdir (), "media-use-dither-" ));
16 return { dir, cleanup : () => rmSync (dir, { recursive: true , force: true }) };
17 }
18
19 function ffmpeg ( args ) {
20 execFileSync ( "ffmpeg" , [ "-y" , "-hide_banner" , "-loglevel" , "error" , ... args]);
21 }
22
23 function run ( args ) {
24 return spawnSync (process.execPath, [ SCRIPT , ... args, "--json" ], { encoding: "utf8" });
25 }
26
27 test ( "processes an image into the requested ordered palette" , { skip: ! HAS_FFMPEG }, ( t ) => {
28 const { dir , cleanup } = fixture ();
29 t. after (cleanup);
30 const input = join (dir, "source.png" );
31 const output = join (dir, "dithered.png" );
32 ffmpeg ([ "-f" , "lavfi" , "-i" , "testsrc2=size=16x12:rate=1" , "-frames:v" , "1" , input]);
33
34 const result = run ([
35 "--input" ,
36 input,
37 "--out" ,
38 output,
39 "--algorithm" ,
40 "floyd-steinberg" ,
41 "--palette" ,
42 "#000000,#ffffff" ,
43 "--point-size" ,
44 "3" ,
45 ]);
46 assert. equal (result.status, 0 , result.stderr || result.stdout);
47 assert. equal ( JSON . parse (result.stdout).type, "image" );
48
49 const rgb = execFileSync ( "ffmpeg" , [
50 "-hide_banner" ,
51 "-loglevel" ,
52 "error" ,
53 "-i" ,
54 output,
55 "-frames:v" ,
56 "1" ,
57 "-f" ,
58 "rawvideo" ,
59 "-pix_fmt" ,
60 "rgb24" ,
61 "-" ,
62 ]);
63 for ( let index = 0 ; index < rgb. length ; index += 3 ) {
64 const value = rgb[index];
65 assert. ok (value === 0 || value === 255 );
66 assert. equal (rgb[index + 1 ], value);
67 assert. equal (rgb[index + 2 ], value);
68 }
69 });
70
71 test ( "processes moving MP4 frames, audio, and BT.709 metadata" , { skip: ! HAS_FFMPEG }, ( t ) => {
72 const { dir , cleanup } = fixture ();
73 t. after (cleanup);
74 const input = join (dir, "source.mp4" );
75 const output = join (dir, "dithered.mp4" );
76 ffmpeg ([
77 "-f" ,
78 "lavfi" ,
79 "-i" ,
80 "testsrc2=size=16x12:rate=3:duration=2" ,
81 "-f" ,
82 "lavfi" ,
83 "-i" ,
84 "sine=frequency=440:duration=3" ,
85 "-c:v" ,
86 "libx264" ,
87 "-pix_fmt" ,
88 "yuv420p" ,
89 "-c:a" ,
90 "aac" ,
91 input,
92 ]);
93
94 const result = run ([
95 "--input" ,
96 input,
97 "--out" ,
98 output,
99 "--algorithm" ,
100 "atkinson" ,
101 "--palette" ,
102 "#0f380f,#306230,#8bac0f,#9bbc0f" ,
103 ]);
104 assert. equal (result.status, 0 , result.stderr || result.stdout);
105 const probe = JSON . parse (
106 execFileSync (
107 "ffprobe" ,
108 [ "-v" , "error" , "-print_format" , "json" , "-show_streams" , "-show_format" , "--" , output],
109 {
110 encoding: "utf8" ,
111 },
112 ),
113 );
114 const video = probe.streams. find (( stream ) => stream.codec_type === "video" );
115 const audio = probe.streams. find (( stream ) => stream.codec_type === "audio" );
116 assert. equal (video.width, 16 );
117 assert. equal (video.height, 12 );
118 assert. equal (video.nb_frames, "6" );
119 assert. equal (video.color_space, "bt709" );
120 assert. equal (video.color_transfer, "bt709" );
121 assert. equal (video.color_primaries, "bt709" );
122 assert. equal (video.color_range, "tv" );
123 assert. equal (audio.codec_name, "aac" );
124 assert. ok ( Number (probe.format.duration) < 2.5 , "audio must not outlive processed video" );
125 const keyframes = execFileSync (
126 "ffprobe" ,
127 [
128 "-v" ,
129 "error" ,
130 "-select_streams" ,
131 "v:0" ,
132 "-skip_frame" ,
133 "nokey" ,
134 "-show_entries" ,
135 "frame=best_effort_timestamp_time" ,
136 "-of" ,
137 "csv=p=0" ,
138 "--" ,
139 output,
140 ],
141 { encoding: "utf8" },
142 )
143 . trim ()
144 . split ( / \s + / )
145 . map (( value ) => Number. parseFloat (value));
146 assert. deepEqual (keyframes, [ 0 , 1 ]);
147 });
148
149 test ( "rejects tagged PQ or HLG instead of silently producing SDR" , { skip: ! HAS_FFMPEG }, ( t ) => {
150 const { dir , cleanup } = fixture ();
151 t. after (cleanup);
152 const input = join (dir, "hlg.mp4" );
153 ffmpeg ([
154 "-f" ,
155 "lavfi" ,
156 "-i" ,
157 "color=c=white:size=16x12:rate=1:duration=1" ,
158 "-c:v" ,
159 "libx264" ,
160 "-pix_fmt" ,
161 "yuv420p10le" ,
162 "-x264-params" ,
163 "colorprim=bt2020:transfer=arib-std-b67:colormatrix=bt2020nc" ,
164 "-color_primaries:v" ,
165 "bt2020" ,
166 "-color_trc:v" ,
167 "arib-std-b67" ,
168 "-colorspace:v" ,
169 "bt2020nc" ,
170 input,
171 ]);
172
173 const result = run ([ "--input" , input, "--out" , join (dir, "wrong.mp4" )]);
174 assert. equal (result.status, 1 );
175 assert. match ( JSON . parse (result.stdout).error, /HDR arib-std-b67 input is not supported/ );
176 });