class OutputOptions(StreamOptions): @staticmethod def __convert_overwrite(overwrite): if isinstance(overwrite, bool) and not overwrite: raise InterruptedSetOption return overwrite @staticmethod def __video_filter(_filter: str): check_type(_filter, str) if _filter.endswith(","): _filter = _filter[:-1] return _filter def add_video_filter(self, _filter): check_type(_filter, str) if self.is_set(OutputOptions.video_filter): self.video_filter += f",{_filter}" else: self.video_filter = _filter video_filter = option("vf", type_filter(str)) audio_filter = option("af", type_filter(str)) frame_rate = option("r", type_filter((float, int))) overwrite = option("y", __convert_overwrite)
class InputOptions(InputOptionsBase, StreamOptions): @staticmethod def format_video_size(size): check_type(size, tuple) width, height = size return f"{width}x{height}" video_size = option('video_size', format_video_size) frame_rate = option('r', min_value_filter(0))
class Stream(Options): def __init__(self, codec=None, muxer=None): super().__init__() if codec is None: self.codec = Codec() else: self.codec = codec if muxer is None: self.muxer = Muxer() else: self.muxer = muxer self.__path = None @property def path(self): return self.__path @path.setter def path(self, path): self.__path = path def __repr__(self): _str = "" for k, v in self.build().items(): _str += f"\n\t\"{k}\": " if isinstance(v, Options): _str += f"{v.__class__.__name__}" if v: _str += f"({v.__str__()})" else: _str += "(None)" else: _str += f"{v}" if not _str: _str = "None" return f"{self.__class__.__name__}: \"{self.path}\"{_str}\n" def build(self): args = [] options = self.dict() for k in sorted(options): v = options[k] if isinstance(v, Options): v = convert_kwargs_to_cmd_line_args(v.dict()) args.extend(v) else: args.append(f'-{k}') if v is not None: args.append(f'{v}') return args codec = option("codecs", type_filter(Codec)) muxer = option("muxers", type_filter(Muxer))
class LibX265(LibX): codec = option( LibX.codec, in_list_filter(LibXCodec.HEVC,), LibXCodec.HEVC ) x265_params = option( "x265-params", set_filter=set_opt, default_value=Params(), doc="Set x265 options. See x265 --help for a list of options." )
class VideoEncoding(Video, Encoding): def __init__(self): for name, opt in self.options(): if not getattr(Encoding, name, False): continue if opt.name.endswith(":v"): continue setattr(self.__class__, name, voption(opt)) super().__init__() gop_size = option("g", min_value_filter(0), doc="GOP size (default: 12)") keyint_min = option("keyint_min", type_filter(int), doc="Set minimum interval between IDR-frames.") refs = option("refs", type_filter(int), doc="Reference frames to consider for motion compensation") brd_scale = option("brd_scale", in_range_filter(0, 3), doc="Downscale frames for dynamic B-frame decision.") chroma_offset = option("chromaoffset", type_filter(int), doc="Set chroma qp offset from luma.") mv0_threshold = option("mv0_threshold", min_value_filter(0)) b_sensitivity = option("b_sensitivity", min_value_filter(1), doc="Adjust sensitivity of b_frame_strategy 1.") timecode_frame_start = option( "timecode_frame_start", min_value_filter(-1), doc="GOP timecode frame start number, in non-drop-frame format")
class LibX264(LibX): codec = option( LibX.codec, in_list_filter(LibXCodec.AVC,), LibXCodec.AVC ) x264opts = option( "x264opts", set_filter=set_opt, default_value=Params(), doc="Set any x264 option. see x264 --fullhelp" ) nal_hrd = option( "nal-hrd", in_list_filter(NalHRD), doc="Set signal HRD information (requires vbv-bufsize to be set)" )
class OutputStream(Stream, OutputOptions): def __init__(self, path=None, codec=None, muxer=None): if not codec: codec = EncodeVideo() if not muxer: muxer = Muxer() super().__init__(codec, muxer) self.path = path # self.maps = [] # def multi_output(self, outputs): # for output in outputs: # TODO: support multi IO stream. https://trac.ffmpeg.org/wiki/Creating%20multiple%20outputs # def map(self, stream, output): # self.maps.append((stream, output)) def build(self): cmd = super().build() cmd.append(self.path) return cmd @staticmethod def filter_encoder(encoder): if not isinstance(encoder, (EncodeVideo, str)): raise TypeError(f"Must be string or EncodeVideo.") if isinstance(encoder, str): encoder = get_encoder(encoder) return encoder @staticmethod def filter_muxer(muxer): if not isinstance(muxer, (str, Muxer)): raise TypeError(f"Must be string or Muxer.") if isinstance(muxer, str): muxer = get_demuxer(muxer) return muxer codec = option(Stream.codec.name, filter_encoder) muxer = option(Stream.muxer.name, filter_muxer)
class Segment(Muxer): format = option( Muxer.format, in_list_filter([FormatMuxer.SEGMENT]), FormatMuxer.SEGMENT ) segment_time = option("segment_time", min_value_filter(0)) segment_format_options = option("segment_format_options", type_filter(str)) segment_format = option("segment_format", in_list_filter(FormatMuxer.MP4)) strftime = option("strftime", in_list_filter([1, 0, '1', '0'])) reset_timestamps = option("reset_timestamps", in_list_filter((1, 0))) segment_atclocktime = option("segment_atclocktime", in_list_filter((1, 0)))
class Nvenc(LibX): codec = option(LibX.codec, set_filter=in_list_filter(NvencCodec)) rc = option( "rc", set_filter=in_list_filter(RateControl), doc="Override the preset rate-control (from -1 to INT_MAX) (default -1)" ) preset = option("preset", set_filter=in_list_filter(Preset), doc="Set the encoding level restriction.") gpu = option("gpu", max_value_filter(number_of_gpus())) constant_quality = option("cq", in_range_filter(0, 51)) strict_gop = option("strict_gop", type_filter(bool)) zerolatency = option("zerolatency", type_filter(bool)) cbr = option("cbr", type_filter(bool)) two_pass = option("2pass", type_filter(bool))
class MP4(Muxer): format = option( Muxer.format, in_list_filter([FormatMuxer.MOV, FormatMuxer.MP4, FormatMuxer.MPEGTS]), FormatMuxer.MP4, doc='MP4 muxers') movflags = option("movflags", set_flags(MOVFlags), Flags(limit_list=MOVFlags), doc="MOV muxers flags") moov_size = option("moov_size", min_value_filter(0)) frag_duration = option("frag_duration", min_value_filter(0)) frag_size = option("frag_size", min_value_filter(0)) min_frag_duration = option("min_frag_duration", min_value_filter(0)) write_tmcd = option("write_tmcd") write_prft = option("write_prft")
class RawVideo(Demuxer): """ Raw video demuxer. Need framerate(=25), pixel_format(=yuv420p) and video_size to decode raw video data. """ format = option(Demuxer.format, set_filter=in_list_filter(FormatDemuxer.RAW_VIDEO, ), default_value=FormatDemuxer.RAW_VIDEO, doc='Raw video demuxer.') framerate = option("framerate", set_filter=min_value_filter(1), doc="Set input video frame rate. Default value is 25.") pixel_format = option( "pixel_format", set_filter=in_list_filter(PixelFormat), doc="Set the input video pixel format. Default value is `yuv420p`.") video_size = option( "video_size", set_filter=set_video_size, doc="Set the input video size. This value must be specified explicitly." )
class V4L2(Muxer, Demuxer): format = option( Boxer.format, in_list_filter(Format.V4L2,), Format.V4L2 )
class Muxer(Boxer): format = option(Boxer.format, in_list_filter(FormatMuxer), doc='Muxer base') fflags = option("fflags", in_list_filter(FFlagsMuxer))
class Demuxer(Boxer): format = option(Boxer.format, in_list_filter(FormatDemuxer), doc="Demuxer base") probesize = option('probesize', in_range_filter(32, 5000000)) analyzeduration = option('analyzeduration', in_range_filter(0, 5000000)) fflags = option("fflags", in_list_filter(FFlagsDemuxer))
class Boxer(Options): format = option("f", in_list_filter(FormatMuxer), doc='Muxer/demuxer base') fflags = option("fflags", in_list_filter(FFlags))
def aoption(opt): if not isinstance(opt, Option): raise TypeError("opt must be Option") return option(f"{opt.name}:a", opt.set_filter, opt.default_value, opt.doc)
class HEVCDemuxer(Demuxer): format = option(Demuxer.format, in_list_filter(FormatDemuxer.HEVC, ), FormatMuxer.HEVC)
class FFmpeg(Options): """ FFmpeg command builder Parameters ---------- input_stream: InputStream Input stream setup Attributes ---------- input_stream: InputStream Input stream setup output_streams: list of OutputStream Output stream setup """ def __init__(self, input_stream, *output_streams): super().__init__() if not isinstance(input_stream, InputStream): raise TypeError(f"Required input_stream's type is `InputStream`.") self.input_stream = input_stream self.output_streams = [] if output_streams: for output_stream in output_streams: self.add_output(output_stream) # Default setting self.hide_banner = None def __repr__(self): _str = f"{self.__class__.__name__}" _str += f"\n{self.input_stream!r}" for output_stream in self.output_streams: _str += f"\n{output_stream!r}" return _str @property def output_stream(self) -> OutputStream: if self.output_streams.__len__() < 1: raise ValueError("Output empty!") return self.output_streams[0] @property def source(self): return self.input_stream.path @source.setter def source(self, path): self.input_stream.path = path def build(self): cmd = [FFMPEG_CMD] cmd += convert_kwargs_to_cmd_line_args(self.dict()) cmd += self.input_stream.build() for output_stream in self.output_streams: if hasattr(output_stream.codec, 'add_params'): output_stream.codec.add_params("log-level", self.loglevel) cmd += output_stream.build() return cmd def add_output(self, output_stream): check_type(output_stream, OutputStream) for output in self.output_streams: if output_stream.path == output.path: raise ValueError("Stream existed!") self.output_streams.append(output_stream) def run(self, stdin=None, stdout=None): """ Create ffmpegpy subprocess with current settings call .build() to show current settings. """ if self.output_streams.__len__() <= 0: raise RuntimeError("Not found any output stream.") args = self.build() for output_stream in self.output_streams: if output_stream.path == PIPE_LINE: stdout = Subprocess.PIPE if stdout is None else stdout stdin = Subprocess.PIPE if stdin is None else stdin return Subprocess(args, stdout=stdout, stdin=stdin) hide_banner = option("hide_banner", is_not_params_filter) loglevel = option("loglevel", in_list_filter(get_attr_values(LogLevel)))
class LibX(VideoEncoding): """ LibX of VideoLan. Only using for encoding video. CMD: ffmpegpy -hide_banner -h encoder=libx264 Supported pixel formats: yuv420p yuvj420p yuv422p yuvj422p yuv444p yuvj444p nv12 nv16 nv21 yuv420p10le yuv422p10le yuv444p10le nv20le gray gray10le """ codec = option( VideoEncoding.codec, in_list_filter(LibXCodec) ) preset = option( "preset", in_list_filter(Preset), doc="Use a preset to select encoding settings (default: medium)" ) tune = option( "tune", in_list_filter(Tune), doc="Tune the settings for a particular type of source or situation" ) profile = option( VideoEncoding.profile, in_list_filter(Profile), doc="Force the limits of an H.264 profile" ) level = option( VideoEncoding.level, in_list_filter(LEVEL), doc="Specify level (as defined by Annex A)" ) crf = option( "crf", in_range_filter(0, 51), doc="Constant Rate Factor. Select the quality for constant quality mode (0 (lossless) -> 51) (default 23)" ) crf_max = option( "crf_max", in_range_filter(0, 51), doc="In CRF mode, prevents VBV from lowering quality beyond this point. " "(from 0 to 51) (default -1: not set)" ) quantization_parameter = option("qp", doc="Constant quantization parameter rate control method") quantizer_min = option("qmin", doc="Minimum quantizer scale.") quantizer_max = option("qmax", doc="Maximum quantizer scale.") quantizer_diff = option("qdiff", doc="Maximum difference between quantizer scales.") quantizer_blur = option("qblur", doc="Quantizer curve blur") quantizer_compression = option("qcomp", doc="Quantizer curve compression factor") refs = option("refs", in_range_filter(1, 16)) rc_lookahead = option("rc-lookahead", type_filter(int))
class StreamOptions(Options): @staticmethod def convert_time(times): if isinstance(times, str): if len(times) > 8: raise ValueError("Time format: %H:%M:%s. Ex: 09:35:12") times = REGEX_TIME_FMT.search(times) if not bool(times): raise ValueError("Time format: %H:%M:%s. Ex: 09:35:12") hours, minutes, seconds = times.groups() else: hours, minutes, seconds = times hours = int(hours) if hours > 23: raise ValueError( f"Hour must in 24-hour military time. Got {hours}") minutes = int(minutes) if minutes > 60: raise ValueError(f"Minute must be standard. Got {minutes}") seconds = int(seconds) if seconds > 60: raise ValueError(f"Seconds must be standard. Got {seconds}") return f"{hours:02}:{minutes:02}:{seconds:02}" an = option("an", is_not_params_filter) dn = option("dn", is_not_params_filter) sn = option("sn", is_not_params_filter) vn = option("vn", is_not_params_filter) re = option('re', is_not_params_filter) video_sync = option('vsync', in_list_filter(VSync)) audio_sync = option('async', min_value_filter(1)) seek = option("ss", convert_time) to = option("to", convert_time) duration = option("t", min_value_filter(0)) pix_fmt = option("pix_fmt", in_list_filter(PixelFormat))
class H264Demuxer(Demuxer): format = option(Demuxer.format, in_list_filter(FormatDemuxer.H264, ), FormatMuxer.H264)
class NvencH264(Nvenc): codec = option(Nvenc.codec, in_list_filter(NvencCodec.H264, ), NvencCodec.H264) profile = option(Nvenc.profile, in_list_filter(ProfileAVC))
class InputStream(Stream, InputOptions, HWAccel): def __init__(self, path, codec=None, demuxer=None): if codec is None: codec = DecodeVideo() if demuxer is None: demuxer = Demuxer() super().__init__(codec, demuxer) self.path = path def build(self): cmd = super().build() cmd.append("-i") cmd.append(self.path) return cmd @property def path(self): return self.__path @path.setter def path(self, path): if isinstance(path, int) and sys.platform == "linux": path = f"{LINUX_DEVICE}{path}" if path.startswith(LINUX_DEVICE): self.muxer = V4L2() if self.is_existed(InputStream.codec): del self.codec elif isinstance(self.muxer, V4L2): self.muxer = Demuxer() if path.startswith("rtsp"): self.rtsp_transport = RTSPTransport.TCP elif self.is_existed(InputStream.rtsp_transport): del self.rtsp_transport self.__path = path def filter_decoder(self, decoder): if not isinstance(decoder, (DecodeVideo, str)): raise TypeError(f"Must be string or DecodeVideo.") if isinstance(decoder, str): decoder = get_decoder(decoder) self.hwaccel_device = HWAccelType.CUDA self.an = None elif self.is_existed(InputStream.hwaccel_device): del self.hwaccel_device return decoder @staticmethod def filter_demuxer(demuxer): if not isinstance(demuxer, (str, Demuxer)): raise TypeError(f"Must be string or Demuxer.") if isinstance(demuxer, str): demuxer = get_demuxer(demuxer) return demuxer codec = option(Stream.codec.name, filter_decoder) muxer = option(Stream.muxer.name, filter_demuxer)
class NvencH265(Nvenc): codec = option(Nvenc.codec, in_list_filter(NvencCodec.H265, ), NvencCodec.H265) profile = option(Nvenc.profile, in_list_filter(ProfileHEVC))
class MP4Demuxer(Demuxer): format = option(Demuxer.format, in_list_filter(FormatDemuxer.MP4, ), FormatDemuxer.MP4, doc='MP4 demuxer')