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 		frameBuf.parseBMP!BGR(frame);
47 	}
48 
49 	@disable this(this);
50 
51 	~this()
52 	{
53 		if (done)
54 			wait(pid);
55 		else
56 		{
57 			if (!tryWait(pid).terminated)
58 			{
59 				try
60 					kill(pid);
61 				catch (ProcessException e)
62 				{}
63 			}
64 
65 			version(Posix)
66 			{
67 				import core.sys.posix.signal : SIGKILL;
68 				if (!tryWait(pid).terminated)
69 				{
70 					try
71 						kill(pid, SIGKILL);
72 					catch (ProcessException e)
73 					{}
74 				}
75 			}
76 
77 			wait(pid);
78 		}
79 	}
80 
81 	private void initialize(File f, string fn, string[] ffmpegArgs)
82 	{
83 		auto pipes = pipe();
84 		output = pipes.readEnd();
85 		pid = spawnProcess([
86 			"ffmpeg",
87 			// Be quiet
88 			"-loglevel", "panic",
89 			// Specify input
90 			"-i", "-",
91 			// No audio
92 			"-an",
93 			// Specify output codec
94 			"-vcodec", "bmp",
95 			// Specify output format
96 			"-f", "image2pipe",
97 			// Additional arguments
98 			] ~ ffmpegArgs ~ [
99 			// Specify output
100 			fn
101 		], f, pipes.writeEnd);
102 
103 		frameBuf.length = Header.sizeof;
104 
105 		popFront();
106 	}
107 
108 private:
109 	import std.process;
110 
111 	Pid pid;
112 	File output;
113 	bool done;
114 
115 	alias BitmapHeader!3 Header;
116 	ubyte[] frameBuf;
117 	Image!BGR frame;
118 }
119 
120 struct VideoInputStream
121 {
122 	RefCounted!VideoInputStreamImpl impl;
123 	this(File f, string[] ffmpegArgs) { impl.initialize(f, "-", ffmpegArgs); }
124 	this(string fn, string[] ffmpegArgs) { impl.initialize(stdin, fn, ffmpegArgs); }
125 	@property ref Image!BGR front() return { return impl.front; }
126 	@property bool empty() { return impl.empty; }
127 	void popFront() { impl.popFront(); }
128 }
129 //alias RefCounted!VideoStreamImpl VideoStream;
130 deprecated alias VideoStream = VideoInputStream;
131 
132 VideoInputStream streamVideo(File f, string[] ffmpegArgs = null) { return VideoInputStream(f, ffmpegArgs); }
133 VideoInputStream streamVideo(string fn, string[] ffmpegArgs = null) { return VideoInputStream(fn, ffmpegArgs); }
134 
135 // ----------------------------------------------------------------------------
136 
137 struct VideoOutputStream
138 {
139 	void put(ref Image!BGR frame)
140 	{
141 		output.rawWrite(frame.toBMP);
142 	}
143 
144 	@disable this(this);
145 
146 	~this()
147 	{
148 		output.close();
149 		wait(pid);
150 	}
151 
152 	private this(File f, string fn, string[] ffmpegArgs, string[] inputArgs)
153 	{
154 		auto pipes = pipe();
155 		output = pipes.writeEnd;
156 		pid = spawnProcess([
157 			"ffmpeg",
158 			// Additional input arguments (such as -framerate)
159 			] ~ inputArgs ~ [
160 		//	// Be quiet
161 		//	"-loglevel", "panic",
162 			// Specify input format
163 			"-f", "image2pipe",
164 			// Specify input
165 			"-i", "-",
166 			// Additional arguments
167 			] ~ ffmpegArgs ~ [
168 			// Specify output
169 			fn
170 		], pipes.readEnd, f);
171 	}
172 
173 	this(File f, string[] ffmpegArgs = null, string[] inputArgs = null)
174 	{
175 		this(f, "-", ffmpegArgs, inputArgs);
176 	}
177 
178 	this(string fn, string[] ffmpegArgs = null, string[] inputArgs = null)
179 	{
180 		this(stdin, fn, ffmpegArgs, inputArgs);
181 	}
182 
183 private:
184 	import std.process;
185 
186 	Pid pid;
187 	File output;
188 	bool done;
189 
190 	alias BitmapHeader!3 Header;
191 	Image!BGR frame;
192 }
193 
194 // ----------------------------------------------------------------------------
195 
196 private:
197 
198 import std.stdio;
199 
200 bool readExactly(ref File f, ubyte[] buf)
201 {
202 	auto read = f.rawRead(buf);
203 	if (read.length==0) return false;
204 	enforce(read.length == buf.length, "Unexpected end of stream");
205 	return true;
206 }