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 <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.graphics.ffmpeg;
15 
16 import std.exception;
17 import std.typecons;
18 
19 import ae.utils.graphics.bitmap;
20 import ae.utils.graphics.color;
21 import ae.utils.graphics.image;
22 
23 private struct VideoInputStreamImpl
24 {
25 	@property ref Image!BGR front() return
26 	{
27 		return frame;
28 	}
29 
30 	@property bool empty() { return done; }
31 
32 	void popFront()
33 	{
34 		auto headerBuf = frameBuf[0..Header.sizeof];
35 		if (!output.readExactly(headerBuf))
36 		{
37 			done = true;
38 			return;
39 		}
40 
41 		auto pHeader = cast(Header*)headerBuf.ptr;
42 		frameBuf.length = pHeader.bfSize;
43 		auto dataBuf = frameBuf[Header.sizeof..$];
44 		enforce(output.readExactly(dataBuf), "Unexpected end of stream");
45 
46 		if (pHeader.bcBitCount == 32)
47 		{
48 			// discard alpha
49 			auto frameAlpha = frameBuf.viewBMP!BGRX();
50 			frameAlpha.colorMap!(c => BGR(c.b, c.g, c.r)).copy(frame);
51 		}
52 		else
53 			frameBuf.parseBMP!BGR(frame);
54 	}
55 
56 	@disable this(this);
57 
58 	~this()
59 	{
60 		if (done)
61 			wait(pid);
62 		else
63 		{
64 			if (!tryWait(pid).terminated)
65 			{
66 				try
67 					kill(pid);
68 				catch (ProcessException e)
69 				{}
70 			}
71 
72 			version(Posix)
73 			{
74 				import core.sys.posix.signal : SIGKILL;
75 				if (!tryWait(pid).terminated)
76 				{
77 					try
78 						kill(pid, SIGKILL);
79 					catch (ProcessException e)
80 					{}
81 				}
82 			}
83 
84 			wait(pid);
85 		}
86 	}
87 
88 	private void initialize(File f, string fn, string[] ffmpegArgs)
89 	{
90 		auto pipes = pipe();
91 		output = pipes.readEnd();
92 		auto args = [
93 			"ffmpeg",
94 			// Be quiet
95 			"-loglevel", "panic",
96 			// Specify input
97 			"-i", fn,
98 			// No audio
99 			"-an",
100 			// Specify output codec
101 			"-vcodec", "bmp",
102 			// Specify output format
103 			"-f", "image2pipe",
104 			// Additional arguments
105 			] ~ ffmpegArgs ~ [
106 			// Specify output
107 			"-"
108 		];
109 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
110 		pid = spawnProcess(args, f, pipes.writeEnd);
111 
112 		frameBuf.length = Header.sizeof;
113 
114 		popFront();
115 	}
116 
117 private:
118 	import std.process;
119 
120 	Pid pid;
121 	File output;
122 	bool done;
123 
124 	alias BitmapHeader!3 Header;
125 	ubyte[] frameBuf;
126 	Image!BGR frame;
127 }
128 
129 struct VideoInputStream
130 {
131 	RefCounted!VideoInputStreamImpl impl;
132 	this(File f, string[] ffmpegArgs) { impl.initialize(f, "-", ffmpegArgs); }
133 	this(string fn, string[] ffmpegArgs) { impl.initialize(stdin, fn, ffmpegArgs); }
134 	@property ref Image!BGR front() return { return impl.front; }
135 	@property bool empty() { return impl.empty; }
136 	void popFront() { impl.popFront(); }
137 }
138 //alias RefCounted!VideoStreamImpl VideoStream;
139 deprecated alias VideoStream = VideoInputStream;
140 
141 VideoInputStream streamVideo(File f, string[] ffmpegArgs = null) { return VideoInputStream(f, ffmpegArgs); }
142 VideoInputStream streamVideo(string fn, string[] ffmpegArgs = null) { return VideoInputStream(fn, ffmpegArgs); }
143 
144 // ----------------------------------------------------------------------------
145 
146 struct VideoOutputStream
147 {
148 	void put(ref Image!BGR frame)
149 	{
150 		output.rawWrite(frame.toBMP);
151 	}
152 
153 	@disable this(this);
154 
155 	~this()
156 	{
157 		output.close();
158 		wait(pid);
159 	}
160 
161 	private this(File f, string fn, string[] ffmpegArgs, string[] inputArgs)
162 	{
163 		auto pipes = pipe();
164 		output = pipes.writeEnd;
165 		auto args = [
166 			"ffmpeg",
167 			// Additional input arguments (such as -framerate)
168 			] ~ inputArgs ~ [
169 		//	// Be quiet
170 		//	"-loglevel", "panic",
171 			// Specify input format
172 			"-f", "image2pipe",
173 			// Specify input
174 			"-i", "-",
175 			// Additional arguments
176 			] ~ ffmpegArgs ~ [
177 			// Specify output
178 			fn
179 		];
180 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
181 		pid = spawnProcess(args, pipes.readEnd, f);
182 	}
183 
184 	this(File f, string[] ffmpegArgs = null, string[] inputArgs = null)
185 	{
186 		this(f, "-", ffmpegArgs, inputArgs);
187 	}
188 
189 	this(string fn, string[] ffmpegArgs = null, string[] inputArgs = null)
190 	{
191 		this(stdin, fn, ffmpegArgs, inputArgs);
192 	}
193 
194 private:
195 	import std.process;
196 
197 	Pid pid;
198 	File output;
199 	bool done;
200 
201 	alias BitmapHeader!3 Header;
202 	Image!BGR frame;
203 }
204 
205 // ----------------------------------------------------------------------------
206 
207 private:
208 
209 import std.stdio;
210 
211 bool readExactly(ref File f, ubyte[] buf)
212 {
213 	auto read = f.rawRead(buf);
214 	if (read.length==0) return false;
215 	enforce(read.length == buf.length, "Unexpected end of stream");
216 	return true;
217 }