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