1 /*
2     Copyright © 2022, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module creator.io.videoexport;
8 import std.process;
9 import std.string;
10 import std.array;
11 import std.uni;
12 import std.stdio : writeln;
13 import i18n;
14 
15 private {
16     bool hasffmpeg;
17     VideoCodec[] codecs;
18 
19     bool parseEncoders(string output) {
20         enum LIST_START_TAG = " -------";
21         enum TAG_NAME_START = 7;
22 
23         // First we want to find where the encoding list starts
24         // FFMPEG conveniently puts " -------" at the start of the list
25         import std.algorithm.searching : countUntil;
26         ptrdiff_t start = output.countUntil(LIST_START_TAG);
27 
28         // To prevent crashes due to ffmpeg changing the format they present the data in
29         // We check whether we could even find our little tag.
30         // Otherwise we'll just assume ffmpeg isn't there for now.
31         if (start == -1) return false;
32 
33         // Then we get every line with the starting and ending whitespace from
34         // out earlier operation stripped out.
35         // This prevents us from getting empty entries.
36         string[] lines = splitLines(strip(output[start+LIST_START_TAG.length..$]));
37         foreach(line; lines) {
38             string sline = strip(line);
39             bool isVideoFormat = sline[2] == 'V';
40 
41             if (isVideoFormat) {
42                 VideoCodec codec;
43                 sline = sline[TAG_NAME_START..$];
44 
45                 // Fetch the tag
46                 int i = 0;
47                 while(i < sline.length && !isWhite(sline[i])) codec.tag ~= sline[i++];
48                 
49                 // This line was for some reason invalidly formatted, we're skipping it.
50                 if (i >= sline.length) continue;
51 
52                 // Fetch the name, it'll be the remaining text with all the leading
53                 // and ending whitespace stripped out
54                 codec.name = strip(sline[i..$]);
55                 codecs ~= codec;
56             }
57         }
58 
59         codecs = VideoCodec("auto", _("Automatic selection"))~codecs;
60 
61         return true;
62     }
63 
64     string[] incBuildFFmpegCommand(VideoExportSettings settings) {
65         import std.conv : text;
66         import std.format : format;
67         string[] out_;
68 
69         if (settings.codec == "auto") {
70             out_ = [
71                 // Command
72                 "ffmpeg", 
73 
74                 // Auto replace files
75                 "-y",
76 
77                 // Accept RGBA encoded image data
78                 "-f", "rawvideo",
79                 "-vcodec", "rawvideo",
80                 "-pix_fmt", "rgba",
81 
82                 // Video resolution 
83                 "-s", "%sx%s".format(settings.width, settings.height),
84 
85                 // Framerate
86                 "-r", settings.framerate.text,
87                 
88                 // Piped from stdin
89                 "-i", "-", 
90 
91                 // Amount of frames to export (should be auto calculated)
92                 "-vframes", settings.frames.text,
93 
94                 // No audio
95                 "-an",
96             
97                 // Output file
98                 settings.file
99             ];
100         } else {
101             out_ = [
102                 // Command
103                 "ffmpeg", 
104 
105                 // Auto replace files
106                 "-y",
107 
108                 // Accept RGBA encoded image data
109                 "-f", "rawvideo",
110                 "-vcodec", "rawvideo",
111                 "-pix_fmt", "rgba",
112 
113                 // Video resolution 
114                 "-s", "%sx%s".format(settings.width, settings.height),
115                 
116                 // Framerate
117                 "-r", settings.framerate.text,
118                 
119                 // Piped from stdin
120                 "-i", "-", 
121 
122                 // Amount of frames to export (should be auto calculated)
123                 "-vframes", settings.frames.text,
124 
125                 // No audio
126                 "-an",
127                 
128                 // Video codec
129                 "-vcodec", settings.codec,
130                 
131                 // Output file
132                 settings.file
133             ];
134         }
135 
136         // Adds additional user specified options if any
137         string ffoptions = strip(settings.ffmpegOptions);
138         if (ffoptions.length > 0) {
139             out_ = out_[0..$-1]~ffoptions.split(" ")~out_[$-1]; 
140         }
141         
142         return out_;
143     }
144 }
145 
146 class VideoEncodingContext {
147 private:
148     VideoExportSettings settings;
149     string[] ffmpegLaunchOptions;
150     ProcessPipes ffmpegPipes;
151     
152     bool isAlive;
153     string errors_;
154     int encoded;
155 
156 
157 public:
158     this(VideoExportSettings settings) {
159         this.settings = settings;
160         this.ffmpegLaunchOptions = incBuildFFmpegCommand(settings);
161         try {
162             this.ffmpegPipes = pipeProcess(this.ffmpegLaunchOptions);
163             isAlive = true;
164         } catch (Exception ex) {
165             errors_ ~= ex.msg;
166             isAlive = false;
167         }
168     }
169 
170     /**
171         Gets whatever errors FFMPEG reported
172     */
173     string errors() {
174         return errors_;
175     }
176 
177     /**
178         Gets whether ffmpeg is in a good state
179     */
180     bool checkState() {
181         return isAlive && !ffmpegPipes.stdout.eof && !ffmpegPipes.stdin.error;
182     }
183 
184     /**
185         Encodes a frame
186     */
187     void encodeFrame(ubyte[] rgbadata) {
188         try {
189             this.ffmpegPipes.stdin.rawWrite(rgbadata);
190             encoded++;
191         } catch(Exception ex) {
192             errors_ ~= ex.msg;
193             isAlive = false;
194         }
195     }
196 
197     /**
198         Closes pipes
199     */
200     void end() {
201         this.ffmpegPipes.stdin.close();
202     }
203 
204     /**
205         Encoding progress
206     */
207     float progress() {
208         return cast(float)encoded/cast(float)settings.frames;
209     }
210 }
211 
212 struct VideoCodec {
213     string tag;
214     string name;
215 }
216 
217 struct VideoExportSettings {
218     string codec;
219     int framerate;
220     string file;
221 
222     int frames;
223 
224     float width;
225     float height;
226 
227     string ffmpegOptions;
228 }
229 
230 /**
231 
232 */
233 VideoEncodingContext incVideoExport(VideoExportSettings settings) {
234     return new VideoEncodingContext(settings);
235 }
236 
237 /**
238     Whether we can export video
239 */
240 bool incVideoCanExport() {
241     return hasffmpeg && codecs.length > 0;
242 }
243 
244 /**
245     Gets the list of supported encoders
246 */
247 ref VideoCodec[] incVideoCodecs() {
248     return codecs;
249 }
250 
251 
252 /**
253     Initlializes video export
254 */
255 void incInitVideoExport() {
256     try {
257         auto output = execute(["ffmpeg", "-codecs"]);
258         if (output.status == 0) {
259             hasffmpeg = parseEncoders(output.output);
260         }
261     } catch (Exception ex) {
262         hasffmpeg = false;
263     }
264 }
265