| """ |
| FFMPEG_Writer - write set of frames to video file |
| |
| original from |
| https://github.com/Zulko/moviepy/blob/master/moviepy/video/io/ffmpeg_writer.py |
| |
| removed unnecessary dependencies |
| |
| The MIT License (MIT) |
| |
| Copyright (c) 2015 Zulko |
| Copyright (c) 2023 Janvarev Vladislav |
| """ |
|
|
| import os |
| import subprocess as sp |
|
|
| PIPE = -1 |
| STDOUT = -2 |
| DEVNULL = -3 |
|
|
| FFMPEG_BINARY = "ffmpeg" |
|
|
| 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. |
| |
| """ |
|
|
| def __init__(self, filename, size, fps, codec="libx265", crf=14, audiofile=None, |
| preset="medium", bitrate=None, |
| 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] |
| w = size[0] - 1 if size[0] % 2 != 0 else size[0] |
| h = size[1] - 1 if size[1] % 2 != 0 else size[1] |
|
|
|
|
| |
| cmd = [ |
| FFMPEG_BINARY, |
| '-hide_banner', |
| '-hwaccel', 'auto', |
| '-y', |
| '-loglevel', 'error' if logfile == sp.PIPE else 'info', |
| '-f', 'rawvideo', |
| '-vcodec', 'rawvideo', |
| '-s', '%dx%d' % (size[0], size[1]), |
| |
| '-pix_fmt', 'bgr24', |
| '-r', str(fps), |
| '-an', '-i', '-' |
| ] |
|
|
| if audiofile is not None: |
| cmd.extend([ |
| '-i', audiofile, |
| '-acodec', 'copy' |
| ]) |
|
|
| cmd.extend([ |
| '-vcodec', codec, |
| '-crf', str(crf) |
| |
| ]) |
| if ffmpeg_params is not None: |
| cmd.extend(ffmpeg_params) |
| if bitrate is not None: |
| cmd.extend([ |
| '-b', bitrate |
| ]) |
|
|
| |
| cmd.extend(['-vf', f'scale={w}:{h}' if w != size[0] or h != size[1] else 'colorspace=bt709:iall=bt601-6-625:fast=1']) |
|
|
| if threads is not None: |
| cmd.extend(["-threads", str(threads)]) |
|
|
| cmd.extend([ |
| '-pix_fmt', 'yuv420p', |
|
|
| ]) |
| cmd.extend([ |
| filename |
| ]) |
|
|
| test = str(cmd) |
| print(test) |
|
|
| popen_params = {"stdout": DEVNULL, |
| "stderr": logfile, |
| "stdin": sp.PIPE} |
|
|
| |
| |
| 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.tobytes()) |
| |
| |
| except IOError as err: |
| _, ffmpeg_error = self.proc.communicate() |
| error = (str(err) + ("\n\nroop unleashed error: FFMPEG encountered " |
| "the following error while writing file %s:" |
| "\n\n %s" % (self.filename, str(ffmpeg_error)))) |
|
|
| if b"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 b"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 b"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 b"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): |
| if self.proc: |
| self.proc.stdin.close() |
| if self.proc.stderr is not None: |
| self.proc.stderr.close() |
| self.proc.wait() |
|
|
| self.proc = None |
|
|
| |
|
|
| def __enter__(self): |
| return self |
|
|
| def __exit__(self, exc_type, exc_value, traceback): |
| self.close() |
|
|
|
|
|
|
| |
|
|