1 module caLib_util.video; 2 3 import std.process : execute, executeShell; 4 import std.file : mkdirRecurse, rmdir, remove, exists, write, rename, getcwd, thisExePath, isFile; 5 import std.path : buildNormalizedPath; 6 import std.exception : enforce; 7 import std.conv : to; 8 import std.string : split; 9 import caLib_util.image : Image; 10 import caLib_util.misc : findInPATH; 11 import caLib_util.build : arch, os; 12 import caLib_util.tempdir : makePrivateTempDir; 13 14 import std.stdio : writeln; 15 import std.random : uniform; 16 17 18 19 class Video 20 { 21 22 private: 23 24 string creationDir; 25 uint nVideos; 26 uint nFrames; 27 28 string path; 29 uint framerate; 30 31 uint width; 32 uint height; 33 34 public: 35 36 this(string path, uint framerate) 37 { 38 this.path = path; 39 40 creationDir = makePrivateTempDir(); 41 nVideos = 0; 42 nFrames = 0; 43 44 this.framerate = framerate; 45 46 width = -1; 47 height = -1; 48 } 49 50 void addFrame(Image frame) 51 { 52 if(width == -1 && height == -1) 53 { 54 width = frame.getWidth(); 55 height = frame.getHeight(); 56 } 57 58 try 59 { 60 frame.saveToFile(creationDir ~ "/" ~ to!string(nFrames) ~ ".png"); 61 nFrames ++; 62 63 if(nFrames == 10) 64 mergeFrames(); 65 } 66 catch(Exception e) 67 { 68 writeln(e.msg); 69 addFrame(frame); 70 } 71 } 72 73 void saveToFile() 74 { 75 mergeFrames(); 76 mergeVideos(); 77 rename(creationDir ~ "/0.mp4", getcwd() ~ "/" ~ path); 78 } 79 80 private: 81 82 void mergeFrames() 83 { 84 auto ret = execute([ 85 encoderPath, 86 "-r", to!string(framerate), 87 "-f", "image2", 88 "-s", to!string(width) ~ "x" ~ to!string(height), 89 "-i", creationDir ~ "/%d.png", 90 "-vcodec", "libx264", "-crf", "25", "-pix_fmt", "yuv420p", 91 creationDir ~ "/" ~ to!string(nVideos) ~ ".mp4", 92 ]); 93 94 enforce(ret.status == 0, "An error occured when creating video"); 95 96 nFrames = 0; 97 nVideos ++; 98 99 if(nVideos == 2) 100 mergeVideos(); 101 } 102 103 void mergeVideos() 104 { 105 string buffer = ""; 106 foreach(i; 0 .. nVideos) 107 { 108 buffer = buffer ~ "\nfile " ~ to!string(i) ~ ".mp4"; 109 } 110 write(creationDir ~ "/files.txt", buffer); 111 112 auto ret = execute([ 113 encoderPath, 114 "-f", "concat", "-safe", "0", 115 "-i", creationDir ~ "/files.txt", 116 "-c", "copy", 117 creationDir ~ "/a.mp4", 118 ]); 119 120 enforce(ret.status == 0, "An error occured when creating video"); 121 122 foreach(i; 0 .. nVideos) 123 { 124 remove(creationDir ~ "/" ~ to!string(i) ~ ".mp4"); 125 } 126 127 rename(creationDir ~ "/a.mp4", 128 creationDir ~ "/0.mp4"); 129 130 nVideos = 1; 131 } 132 } 133 134 135 136 private string decoderPath = null; 137 private string encoderPath = null; 138 139 static this() 140 { 141 enum decoderName = [ 142 "Windows" : "ffmpeg.exe", 143 "Linux" : "ffmpeg", 144 ].get(os, null); 145 146 enum encoderName = [ 147 "Windows" : "ffmpeg.exe", 148 "Linux" : "ffmpeg", 149 ].get(os, null); 150 151 static assert(decoderName != null && encoderName != null, 152 "can't compile video.d becuase codec usage is not yet" 153 ~ " implemented for " ~ os); 154 155 string path; 156 157 // look for decoder 158 159 // look in path 160 path = findInPATH(decoderName); 161 if(exists(path) && isFile(path)) 162 decoderPath = path; 163 164 // look in the working directory 165 path = buildNormalizedPath(getcwd(), decoderName); 166 if(exists(path) && isFile(path)) 167 decoderPath = path; 168 169 // look in the same directory as the executable 170 path = buildNormalizedPath(thisExePath(), "..", decoderName); 171 if(exists(path) && isFile(path)) 172 decoderPath = path; 173 174 175 // look for encoder 176 177 // look in path 178 path = findInPATH(encoderName); 179 if(exists(path) && isFile(path)) 180 encoderPath = path; 181 182 // look in the working directory 183 path = buildNormalizedPath(getcwd(), encoderName); 184 if(exists(path) && isFile(path)) 185 encoderPath = path; 186 187 // look in the same directory as the executable 188 path = buildNormalizedPath(thisExePath(), "..", encoderName); 189 if(exists(path) && isFile(path)) 190 encoderPath = path; 191 192 enforce(decoderPath != null && encoderPath != null, 193 encoderName ~ " and/or " ~ decoderName ~ ", wich is " 194 ~ "essential for creating video could not be found"); 195 } 196 197 198