| """
|
| 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=['-movflags', 'faststart']):
|
|
|
| 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()
|
|
|
|
|
|
|
|
|
|
|