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.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 struct VideoInputStream
132 {
133 	RefCounted!VideoInputStreamImpl impl;
134 	this(File f, string[] ffmpegArgs) { impl.initialize(f, "-", ffmpegArgs); }
135 	this(string fn, string[] ffmpegArgs) { impl.initialize(stdin, fn, ffmpegArgs); }
136 	@property ref Image!BGR front() return { return impl.front; }
137 	@property bool empty() { return impl.empty; }
138 	void popFront() { impl.popFront(); }
139 }
140 //alias RefCounted!VideoStreamImpl VideoStream;
141 deprecated alias VideoStream = VideoInputStream;
142 
143 VideoInputStream streamVideo(File f, string[] ffmpegArgs = null) { return VideoInputStream(f, ffmpegArgs); }
144 VideoInputStream streamVideo(string fn, string[] ffmpegArgs = null) { return VideoInputStream(fn, ffmpegArgs); }
145 
146 // ----------------------------------------------------------------------------
147 
148 struct VideoOutputStream
149 {
150 	void put(ref Image!BGR frame)
151 	{
152 		output.rawWrite(frame.toBMP);
153 	}
154 
155 	@disable this(this);
156 
157 	~this()
158 	{
159 		output.close();
160 		wait(pid);
161 	}
162 
163 	private this(File f, string fn, string[] ffmpegArgs, string[] inputArgs)
164 	{
165 		auto pipes = pipe();
166 		output = pipes.writeEnd;
167 		auto args = [
168 			"ffmpeg",
169 			// Additional input arguments (such as -framerate)
170 			] ~ inputArgs ~ [
171 		//	// Be quiet
172 		//	"-loglevel", "panic",
173 			// Specify input format
174 			"-f", "image2pipe",
175 			// Specify input
176 			"-i", "-",
177 			// Additional arguments
178 			] ~ ffmpegArgs ~ [
179 			// Specify output
180 			fn
181 		];
182 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
183 		pid = spawnProcess(args, pipes.readEnd, f);
184 	}
185 
186 	this(File f, string[] ffmpegArgs = null, string[] inputArgs = null)
187 	{
188 		this(f, "-", ffmpegArgs, inputArgs);
189 	}
190 
191 	this(string fn, string[] ffmpegArgs = null, string[] inputArgs = null)
192 	{
193 		this(stdin, fn, ffmpegArgs, inputArgs);
194 	}
195 
196 private:
197 	import std.process;
198 
199 	Pid pid;
200 	File output;
201 	bool done;
202 
203 	alias BitmapHeader!3 Header;
204 	Image!BGR frame;
205 }