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 }