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(ref Image!BGR frame)
154 	{
155 		output.rawWrite(frame.toBMP);
156 	} ///
157 
158 	@disable this(this);
159 
160 	~this()
161 	{
162 		output.close();
163 		wait(pid);
164 	}
165 
166 	private this(File f, string fn, string[] ffmpegArgs, string[] inputArgs)
167 	{
168 		auto pipes = pipe();
169 		output = pipes.writeEnd;
170 		auto args = [
171 			"ffmpeg",
172 			// Additional input arguments (such as -framerate)
173 			] ~ inputArgs ~ [
174 		//	// Be quiet
175 		//	"-loglevel", "panic",
176 			// Specify input format
177 			"-f", "image2pipe",
178 			// Specify input
179 			"-i", "-",
180 			// Additional arguments
181 			] ~ ffmpegArgs ~ [
182 			// Specify output
183 			fn
184 		];
185 		debug(FFMPEG) stderr.writeln(args.escapeShellCommand);
186 		pid = spawnProcess(args, pipes.readEnd, f);
187 	}
188 
189 	/// Begin encoding to the given file with the given parameters.
190 	this(File f, string[] ffmpegArgs = null, string[] inputArgs = null)
191 	{
192 		this(f, "-", ffmpegArgs, inputArgs);
193 	}
194 
195 	this(string fn, string[] ffmpegArgs = null, string[] inputArgs = null)
196 	{
197 		this(stdin, fn, ffmpegArgs, inputArgs);
198 	} /// ditto
199 
200 private:
201 	import std.process;
202 
203 	Pid pid;
204 	File output;
205 	bool done;
206 
207 	alias BitmapHeader!3 Header;
208 	Image!BGR frame;
209 }