1 /**
2  * Get frames from a video file by invoking ffmpeg.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.utils.graphics.ffmpeg;
15 
16 import std.exception;
17 import std.stdio;
18 import std.typecons;
19 
20 import ae.utils.graphics.bitmap;
21 import ae.utils.graphics.color;
22 import ae.utils.graphics.image;
23 import ae.sys.file : readExactly;
24 
25 private struct VideoInputStreamImpl
26 {
27 	@property ref Image!BGR front() return
28 	{
29 		return frame;
30 	}
31 
32 	@property bool empty() { return done; }
33 
34 	void popFront()
35 	{
36 		auto headerBuf = frameBuf[0..Header.sizeof];
37 		if (!output.readExactly(headerBuf))
38 		{
39 			done = true;
40 			return;
41 		}
42 
43 		auto pHeader = cast(Header*)headerBuf.ptr;
44 		frameBuf.length = pHeader.bfSize;
45 		auto dataBuf = frameBuf[Header.sizeof..$];
46 		enforce(output.readExactly(dataBuf), "Unexpected end of stream");
47 
48 		if (pHeader.bcBitCount == 32)
49 		{
50 			// discard alpha
51 			auto frameAlpha = frameBuf.viewBMP!BGRX();
52 			frameAlpha.colorMap!(c => BGR(c.b, c.g, c.r)).copy(frame);
53 		}
54 		else
55 			frameBuf.parseBMP!BGR(frame);
56 	}
57 
58 	@disable this(this);
59 
60 	~this()
61 	{
62 		if (done)
63 			wait(pid);
64 		else
65 		{
66 			if (!tryWait(pid).terminated)
67 			{
68 				try
69 					kill(pid);
70 				catch (ProcessException e)
71 				{}
72 			}
73 
74 			version(Posix)
75 			{
76 				import core.sys.posix.signal : SIGKILL;
77 				if (!tryWait(pid).terminated)
78 				{
79 					try
80 						kill(pid, SIGKILL);
81 					catch (ProcessException e)
82 					{}
83 				}
84 			}
85 
86 			wait(pid);
87 		}
88 	}
89 
90 	private void initialize(File f, string fn, string[] ffmpegArgs)
91 	{
92 		auto pipes = pipe();
93 		output = pipes.readEnd();
94 		auto args = [
95 			"ffmpeg",
96 			// Be quiet
97 			"-loglevel", "panic",
98 			// Specify input
99 			"-i", fn,
100 			// No audio
101 			"-an",
102 			// Specify output codec
103 			"-vcodec", "bmp",
104 			// Specify output format
105 			"-f", "image2pipe",
106 			// Additional arguments
107 			] ~ ffmpegArgs ~ [
108 			// Specify output
109 			"-"
110 		];
111 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
112 		pid = spawnProcess(args, f, pipes.writeEnd);
113 
114 		frameBuf.length = Header.sizeof;
115 
116 		popFront();
117 	}
118 
119 private:
120 	import std.process;
121 
122 	Pid pid;
123 	File output;
124 	bool done;
125 
126 	alias BitmapHeader!3 Header;
127 	ubyte[] frameBuf;
128 	Image!BGR frame;
129 }
130 
131 /// Represents a video stream as a D range of frames.
132 struct VideoInputStream
133 {
134 	private RefCounted!VideoInputStreamImpl impl;
135 	this(File f, string[] ffmpegArgs) { impl.initialize(f, "-", ffmpegArgs); } ///
136 	this(string fn, string[] ffmpegArgs) { impl.initialize(stdin, fn, ffmpegArgs); } ///
137 	@property ref Image!BGR front() return { return impl.front; } ///
138 	@property bool empty() { return impl.empty; } ///
139 	void popFront() { impl.popFront(); } ///
140 }
141 //alias RefCounted!VideoStreamImpl VideoStream;
142 deprecated alias VideoStream = VideoInputStream;
143 
144 /// Creates a `VideoInputStream` from the given file.
145 VideoInputStream streamVideo(File f, string[] ffmpegArgs = null) { return VideoInputStream(f, ffmpegArgs); }
146 VideoInputStream streamVideo(string fn, string[] ffmpegArgs = null) { return VideoInputStream(fn, ffmpegArgs); } /// ditto
147 
148 // ----------------------------------------------------------------------------
149 
150 /// Represents a video encoding process as a D output range of frames.
151 struct VideoOutputStream
152 {
153 	void put(I)(ref I frame)
154 	if (is(typeof(frame.toBMP())) || is(typeof(frame.toPNG(0))))
155 	{
156 		static if (is(typeof(frame.toBMP)))
157 			output.rawWrite(frame.toBMP);
158 		else
159 			output.rawWrite(frame.toPNG(0));
160 	} ///
161 
162 	@disable this(this);
163 
164 	~this()
165 	{
166 		output.close();
167 		wait(pid);
168 	}
169 
170 	private this(File f, string fn, string[] ffmpegArgs, string[] inputArgs)
171 	{
172 		auto pipes = pipe();
173 		output = pipes.writeEnd;
174 
175 		version (linux)
176 		{
177 			// Set the pipe size to the maximum allowed. Hint:
178 			// sudo sysctl fs.pipe-user-pages-soft=$((4*1024*1024))
179 			// sudo sysctl fs.pipe-max-size=$((256*1024*1024))
180 
181 			int maxSize = {
182 				import std.file : readText;
183 				import std.string : chomp;
184 				import std.conv : to;
185 				return "/proc/sys/fs/pipe-max-size".readText.chomp.to!int;
186 			}();
187 
188 			import core.sys.linux.fcntl : fcntl;
189 			enum F_SETPIPE_SZ = 1024 + 7;
190 			auto res = fcntl(pipes.readEnd.fileno, F_SETPIPE_SZ, maxSize);
191 			errnoEnforce(res >= 0, "Failed to set the pipe size");
192 		}
193 
194 		auto args = [
195 			"ffmpeg",
196 			// Additional input arguments (such as -framerate)
197 			] ~ inputArgs ~ [
198 		//	// Be quiet
199 		//	"-loglevel", "panic",
200 			// Specify input format
201 			"-f", "image2pipe",
202 			// Specify input
203 			"-i", "-",
204 			// Additional arguments
205 			] ~ ffmpegArgs ~ [
206 			// Specify output
207 			fn
208 		];
209 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
210 		pid = spawnProcess(args, pipes.readEnd, f);
211 	}
212 
213 	/// Begin encoding to the given file with the given parameters.
214 	this(File f, string[] ffmpegArgs = null, string[] inputArgs = null)
215 	{
216 		this(f, "-", ffmpegArgs, inputArgs);
217 	}
218 
219 	this(string fn, string[] ffmpegArgs = null, string[] inputArgs = null)
220 	{
221 		this(stdin, fn, ffmpegArgs, inputArgs);
222 	} /// ditto
223 
224 private:
225 	import std.process;
226 
227 	Pid pid;
228 	File output;
229 }