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