-
Notifications
You must be signed in to change notification settings - Fork 0
/
ffmpeg_writer.py
433 lines (342 loc) · 14.6 KB
/
ffmpeg_writer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# The MIT License (MIT)
# [OSI Approved License]
# The MIT License (MIT)
# Copyright (c) 2015 Zulko
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
On the long term this will implement several methods to make videos
out of VideoClips
"""
import subprocess as sp
import os
import numpy as np
try:
from subprocess import DEVNULL # py3k
except ImportError:
DEVNULL = open(os.devnull, 'wb')
from moviepy.config import get_setting
from moviepy.tools import verbose_print
class FFMPEG_VideoWriter:
""" A class for FFMPEG-based video writing.
A class to write videos using ffmpeg. ffmpeg will write in a large
choice of formats.
Parameters
-----------
filename
Any filename like 'video.mp4' etc. but if you want to avoid
complications it is recommended to use the generic extension
'.avi' for all your videos.
size
Size (width,height) of the output video in pixels.
fps
Frames per second in the output video file.
codec
FFMPEG codec. It seems that in terms of quality the hierarchy is
'rawvideo' = 'png' > 'mpeg4' > 'libx264'
'png' manages the same lossless quality as 'rawvideo' but yields
smaller files. Type ``ffmpeg -codecs`` in a terminal to get a list
of accepted codecs.
Note for default 'libx264': by default the pixel format yuv420p
is used. If the video dimensions are not both even (e.g. 720x405)
another pixel format is used, and this can cause problem in some
video readers.
audiofile
Optional: The name of an audio file that will be incorporated
to the video.
preset
Sets the time that FFMPEG will take to compress the video. The slower,
the better the compression rate. Possibilities are: ultrafast,superfast,
veryfast, faster, fast, medium (default), slow, slower, veryslow,
placebo.
bitrate
Only relevant for codecs which accept a bitrate. "5000k" offers
nice results in general.
withmask
Boolean. Set to ``True`` if there is a mask in the video to be
encoded.
"""
def __init__(self, filename, size, fps, codec="libx264", audiofile=None,
preset="medium", bitrate=None, withmask=False,
logfile=None, threads=None, ffmpeg_params=None):
if logfile is None:
logfile = sp.PIPE
self.filename = filename
self.codec = codec
self.ext = self.filename.split(".")[-1]
# order is important
cmd = [
get_setting("FFMPEG_BINARY"),
'-y',
'-loglevel', 'error' if logfile == sp.PIPE else 'info',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', '%dx%d' % (size[0], size[1]),
'-pix_fmt', 'rgba' if withmask else 'rgb24',
'-r', '%.02f' % fps,
'-i', '-', '-an',
]
if audiofile is not None:
cmd.extend([
'-i', audiofile,
'-acodec', 'copy'
])
cmd.extend([
'-vcodec', codec,
'-preset', preset,
])
if ffmpeg_params is not None:
cmd.extend(ffmpeg_params)
if bitrate is not None:
cmd.extend([
'-b', bitrate
])
if threads is not None:
cmd.extend(["-threads", str(threads)])
if ((codec == 'libx264') and
(size[0] % 2 == 0) and
(size[1] % 2 == 0)):
cmd.extend([
'-pix_fmt', 'yuv420p'
])
cmd.extend([
filename
])
popen_params = {"stdout": DEVNULL,
"stderr": logfile,
"stdin": sp.PIPE}
# This was added so that no extra unwanted window opens on windows
# when the child process is created
if os.name == "nt":
popen_params["creationflags"] = 0x08000000
self.proc = sp.Popen(cmd, **popen_params)
def write_frame(self, img_array):
""" Writes one frame in the file."""
try:
self.proc.stdin.write(img_array.tostring())
except IOError as err:
ffmpeg_error = self.proc.stderr.read()
error = (str(err) + ("\n\nMoviePy error: FFMPEG encountered "
"the following error while writing file %s:"
"\n\n %s" % (self.filename, ffmpeg_error)))
if "Unknown encoder" in ffmpeg_error:
error = error+("\n\nThe video export "
"failed because FFMPEG didn't find the specified "
"codec for video encoding (%s). Please install "
"this codec or change the codec when calling "
"write_videofile. For instance:\n"
" >>> clip.write_videofile('myvid.webm', codec='libvpx')")%(self.codec)
elif "incorrect codec parameters ?" in ffmpeg_error:
error = error+("\n\nThe video export "
"failed, possibly because the codec specified for "
"the video (%s) is not compatible with the given "
"extension (%s). Please specify a valid 'codec' "
"argument in write_videofile. This would be 'libx264' "
"or 'mpeg4' for mp4, 'libtheora' for ogv, 'libvpx for webm. "
"Another possible reason is that the audio codec was not "
"compatible with the video codec. For instance the video "
"extensions 'ogv' and 'webm' only allow 'libvorbis' (default) as a"
"video codec."
)%(self.codec, self.ext)
elif "encoder setup failed" in ffmpeg_error:
error = error+("\n\nThe video export "
"failed, possibly because the bitrate you specified "
"was too high or too low for the video codec.")
elif "Invalid encoder type" in ffmpeg_error:
error = error + ("\n\nThe video export failed because the codec "
"or file extension you provided is not a video")
raise IOError(error)
def close(self):
self.proc.stdin.close()
if self.proc.stderr is not None:
self.proc.stderr.close()
self.proc.wait()
del self.proc
def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None,
preset="medium", withmask=False, write_logfile=False,
audiofile=None, verbose=True, threads=None, ffmpeg_params=None):
""" Write the clip to a videofile. See VideoClip.write_videofile for details
on the parameters.
"""
if write_logfile:
logfile = open(filename + ".log", 'w+')
else:
logfile = None
verbose_print(verbose, "[MoviePy] Writing video %s\n"%filename)
writer = FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec,
preset=preset, bitrate=bitrate, logfile=logfile,
audiofile=audiofile, threads=threads,
ffmpeg_params=ffmpeg_params)
nframes = int(clip.duration*fps)
for t,frame in clip.iter_frames(progress_bar=False, with_times=True,
fps=fps, dtype="uint8"):
if withmask:
mask = (255*clip.mask.get_frame(t))
if mask.dtype != "uint8":
mask = mask.astype("uint8")
frame = np.dstack([frame,mask])
writer.write_frame(frame)
writer.close()
if write_logfile:
logfile.close()
verbose_print(verbose, "[MoviePy] Done.\n")
def ffmpeg_write_image(filename, image, logfile=False):
""" Writes an image (HxWx3 or HxWx4 numpy array) to a file, using
ffmpeg. """
if image.dtype != 'uint8':
image = image.astype("uint8")
cmd = [ get_setting("FFMPEG_BINARY"), '-y',
'-s', "%dx%d"%(image.shape[:2][::-1]),
"-f", 'rawvideo',
'-pix_fmt', "rgba" if (image.shape[2] == 4) else "rgb24",
'-i','-', filename]
if logfile:
log_file = open(filename + ".log", 'w+')
else:
log_file = sp.PIPE
popen_params = {"stdout": DEVNULL,
"stderr": log_file,
"stdin": sp.PIPE}
if os.name == "nt":
popen_params["creationflags"] = 0x08000000
proc = sp.Popen(cmd, **popen_params)
out, err = proc.communicate(image.tostring())
if proc.returncode:
err = "\n".join(["[MoviePy] Running : %s\n" % cmd,
"WARNING: this command returned an error:",
err.decode('utf8')])
raise IOError(err)
del proc
def write_gif(clip, filename, fps=None, program= 'ffmpeg',
opt="OptimizeTransparency", fuzz=1, verbose=True, withmask=True,
loop=0, dispose=True, colors=None):
""" Write the VideoClip to a GIF file, without temporary files.
Converts a VideoClip into an animated GIF using ImageMagick
or ffmpeg.
Parameters
-----------
filename
Name of the resulting gif file.
fps
Number of frames per second (see note below). If it
isn't provided, then the function will look for the clip's
``fps`` attribute (VideoFileClip, for instance, have one).
program
Software to use for the conversion, either 'ImageMagick' or
'ffmpeg'.
opt
(ImageMagick only) optimalization to apply, either
'optimizeplus' or 'OptimizeTransparency'.
fuzz
(ImageMagick only) Compresses the GIF by considering that
the colors that are less than fuzz% different are in fact
the same.
Notes
-----
The gif will be playing the clip in real time (you can
only change the frame rate). If you want the gif to be played
slower than the clip you will use ::
>>> # slow down clip 50% and make it a gif
>>> myClip.speedx(0.5).write_gif('myClip.gif')
"""
#
# We use processes chained with pipes.
#
# if program == 'ffmpeg'
# frames --ffmpeg--> gif
#
# if program == 'ImageMagick' and optimize == (None, False)
# frames --ffmpeg--> bmp frames --ImageMagick--> gif
#
#
# if program == 'ImageMagick' and optimize != (None, False)
# frames -ffmpeg-> bmp frames -ImagMag-> gif -ImagMag-> better gif
#
if fps is None:
fps = clip.fps
DEVNULL = open(os.devnull, 'wb')
delay= 100.0/fps
if clip.mask is None:
withmask = False
cmd1 = [get_setting("FFMPEG_BINARY"), '-y', '-loglevel', 'error',
'-f', 'rawvideo',
'-vcodec','rawvideo', '-r', "%.02f"%fps,
'-s', "%dx%d"%(clip.w, clip.h),
'-pix_fmt', ('rgba' if withmask else 'rgb24'),
'-i', '-']
popen_params = {"stdout": DEVNULL,
"stderr": DEVNULL,
"stdin": DEVNULL}
if os.name == "nt":
popen_params["creationflags"] = 0x08000000
if program == "ffmpeg":
popen_params["stdin"] = sp.PIPE
popen_params["stdout"] = DEVNULL
proc1 = sp.Popen(cmd1+[ '-pix_fmt', ('rgba' if withmask else 'rgb24'),
'-r', "%.02f"%fps, filename], **popen_params)
else:
popen_params["stdin"] = sp.PIPE
popen_params["stdout"] = sp.PIPE
proc1 = sp.Popen(cmd1+ ['-f', 'image2pipe', '-vcodec', 'bmp', '-'],
**popen_params)
if program == 'ImageMagick':
cmd2 = [get_setting("IMAGEMAGICK_BINARY"), '-delay', "%.02f"%(delay),
"-dispose" ,"%d"%(2 if dispose else 1),
'-loop', '%d'%loop, '-', '-coalesce']
if (opt in [False, None]):
popen_params["stdin"] = proc1.stdout
popen_params["stdout"] = DEVNULL
proc2 = sp.Popen(cmd2+[filename], **popen_params)
else:
popen_params["stdin"] = proc1.stdout
popen_params["stdout"] = sp.PIPE
proc2 = sp.Popen(cmd2+['gif:-'], **popen_params)
if opt:
cmd3 = [get_setting("IMAGEMAGICK_BINARY"), '-', '-layers', opt,
'-fuzz', '%d'%fuzz+'%'
]+(["-colors", "%d"%colors] if colors is not None else [])+[
filename]
popen_params["stdin"] = proc2.stdout
popen_params["stdout"] = DEVNULL
proc3 = sp.Popen(cmd3, **popen_params)
# We send all the frames to the first process
verbose_print(verbose, "\n[MoviePy] >>>> Building file %s\n"%filename)
verbose_print(verbose, "[MoviePy] Generating GIF frames...\n")
try:
for t,frame in clip.iter_frames(fps=fps, progress_bar=False,
with_times=True, dtype="uint8"):
if withmask:
mask = 255 * clip.mask.get_frame(t)
frame = np.dstack([frame, mask]).astype('uint8')
proc1.stdin.write(frame.tostring())
except IOError as err:
error = ("[MoviePy] Error: creation of %s failed because "
"of the following error:\n\n%s.\n\n."%(filename, str(err)))
if program == "ImageMagick":
error = error + ("This can be due to the fact that "
"ImageMagick is not installed on your computer, or "
"(for Windows users) that you didn't specify the "
"path to the ImageMagick binary in file conf.py." )
raise IOError(error)
if program == 'ImageMagick':
verbose_print(verbose, "[MoviePy] Optimizing the GIF with ImageMagick...\n")
proc1.stdin.close()
proc1.wait()
if program == 'ImageMagick':
proc2.wait()
if opt:
proc3.wait()
verbose_print(verbose, "[MoviePy] >>>> File %s is ready !"%filename)