class HLS(Output): """ m3u8 muxer """ format: str = param(name='f', init=False, default='hls') # Add empty `param()` call to prevent # "Non-default argument(s) follows default argument(s)" # dataclass error. hls_init_time: int = param() hls_base_url: str = param() hls_segment_filename: str = 'file%03d.ts'
class Scale2Ref(VideoFilter): """ Filter that scales one stream to fit another one. """ # define filter name filter = 'scale2ref' # configure input and output edges count input_count = 2 output_count = 2 # add some parameters that compute dimensions # based on input stream characteristics width: str = param(name='w') height: str = param(name='h')
class VideoCodec(codecs.VideoCodec): force_key_frames: str = param() constant_rate_factor: int = param(name='crf') preset: str = param() max_rate: int = param(name='maxrate') buf_size: int = param(name='bufsize') profile: str = param(stream_suffix=True) gop: int = param(name='g') rate: float = param(name='r')
class MediaInfo(BaseWrapper): command = 'mediainfo' input_file: str = param(name='') def handle_stderr(self, line: str) -> str: if 'error' in line: raise RuntimeError(f"Mediainfo error: {line}") return super().handle_stderr(line)
class X11Grab(encoding.Input): """ X-server grabbing input. """ # `skip=True` removes parameter from argument list # (it is added manually in `as_pairs`). # This field overwrites `default` from `encoding.Input`. input_file: str = param(name='i', default=':0.0', skip=True) # `init=False` excludes parameter from `__init__`. # Field is initialized with value passed in `default` # parameter. Exactly as in dataclasses. format: str = param(name='f', default='x11grab', init=False) size: str = param(name='video_size') fps: float = param(name='framerate') def as_pairs(self) -> List[Tuple[Optional[str], Optional[str]]]: return super().as_pairs() + [('i', self.input_file)]
class FFMPEG(ffmpeg.FFMPEG): realtime: bool = param(name='re')
class FFMPEG(encoding.FFMPEG): no_banner: bool = param(default=False, name='hide_banner')
class MyFilter(filters.VideoFilter): my_flag: bool = param(init=False, default=True)
class StubFilter(AudioFilter): filter = 'stub' p: int = param()
class AAC(AudioCodec): codec = 'aac' bitrate: int = param(name='b', stream_suffix=True) def transform(self, *metadata: Meta) -> Meta: return replace(ensure_audio(*metadata), bitrate=self.bitrate)
class Python(BaseWrapper): command = 'python' module: str = param(name='m')
class AudioCodec(codecs.AudioCodec): rate: float = param(name='ar') channels: int = param(name='ac')
class FFMPEG(BaseWrapper): """ ffmpeg command line basic wrapper. >>> from fffw.encoding.codecs import VideoCodec, AudioCodec >>> from fffw.encoding.filters import Scale >>> from fffw.encoding.outputs import output_file >>> ff = FFMPEG('/tmp/input.mp4', overwrite=True) >>> c = VideoCodec('libx264', bitrate=4_000_000) >>> ff.video | Scale(1280, 720) > c VideoCodec(codec='libx264', bitrate=4000000) >>> ff.overwrite = True >>> ff > output_file('/tmp/output.mp4', c, ... AudioCodec('libfdk_aac', bitrate=192_000)) >>> ff.get_cmd() 'ffmpeg -y -i /tmp/input.mp4\ -filter_complex "[0:v]scale=w=1280:h=720[vout0]"\ -map "[vout0]" -c:v libx264 -b:v 4000000 -map 0:a -c:a libfdk_aac -b:a 192000\ /tmp/output.mp4' >>> """ command = 'ffmpeg' stderr_markers = ['[error]', '[fatal]'] input: Union[str, Input] = param(skip=True) output: Union[str, Output] = param(skip=True) loglevel: str = param() """ Loglevel: i.e. `level+info`.""" overwrite: bool = param(name='y') """ Overwrite output files without manual confirmation.""" init_hardware: str = param(name='init_hw_device') """ Initializes hardware acceleration device.""" filter_hardware: str = param(name='filter_hw_device') """ Sets a device for filter graph by it's name set with `init_hardware`.""" def __post_init__(self) -> None: """ Fills internal shared structures for input and output files, and initializes filter graph. """ self.__inputs = InputList() if self.input: if not isinstance(self.input, Input): self.__inputs.append(Input(input_file=self.input)) else: self.__inputs.append(self.input) self.__outputs = OutputList() if self.output: if not isinstance(self.output, Output): self.__outputs.append(Output(output_file=self.output)) else: self.__outputs.append(self.output) self.__filter_complex = FilterComplex(self.__inputs, self.__outputs) # calling super() to freeze params. super().__post_init__() def __lt__(self, other: Input) -> Input: """ Adds new source file. >>> ff = FFMPEG() >>> src = ff < Input(input_file='/tmp/input.mp4') >>> """ if not isinstance(other, Input): return NotImplemented return self.add_input(other) def __gt__(self, other: Output) -> Output: """ Adds new output file. >>> from fffw.encoding.inputs import * >>> from fffw.encoding.outputs import * >>> ff = FFMPEG(input=input_file('input.mp4')) >>> dest = ff > output_file('/tmp/output.mp4') >>> """ if not isinstance(other, Output): return NotImplemented return self.add_output(other) @property def inputs(self) -> Tuple[Input, ...]: """ :return: a copy of ffmpeg input list. """ return tuple(self.__inputs) @property def outputs(self) -> Tuple[Output, ...]: """ :return: a copy of ffmpeg output list. """ return tuple(self.__outputs) @property def filter_device(self) -> meta.Device: """ Returns filter hardware device metadata.""" hardware, init = self.init_hardware.split("=") name = init.split(':', 1)[0] if self.filter_hardware != name: raise ValueError(self.filter_hardware) return meta.Device(hardware, name) @property def video(self) -> Stream: """ :returns: first video stream not yet connected to filter graph or codec. >>> from fffw.encoding.filters import Scale >>> ff = FFMPEG('/tmp/input.mp4') >>> ff.video | Scale(1280, 720) Scale(width=1280, height=720) >>> """ return self._get_free_source(VIDEO) @property def audio(self) -> Stream: """ :returns: first audio stream not yet connected to filter graph or codec. >>> from fffw.encoding.codecs import AudioCodec >>> ff = FFMPEG('/tmp/input.mp4') >>> ac = AudioCodec('aac') >>> ff.audio > ac AudioCodec(codec='aac', bitrate=0) >>> """ return self._get_free_source(AUDIO) def _get_free_source(self, kind: StreamType) -> Stream: """ :param kind: stream type :return: first stream of this kind not connected to destination """ for stream in self.__inputs.streams: if stream.kind != kind or stream.connected: continue return stream else: raise RuntimeError("no free streams") def _add_codec(self, c: Codec) -> Optional[Codec]: """ Connect codec to filter graph output or input stream. :param c: codec to connect to free source :returns: None of codec already connected to filter graph or codec itself if it was connected successfully to input stream. """ if c.connected: return None node = self._get_free_source(c.kind) node.connect_dest(c) return c def get_args(self) -> List[bytes]: """ :returns: command line arguments for ffmpeg. This includes: - ffmpeg executable name - ffmpeg parameters - input list args - filter_graph definition - output list args """ with base.Namer(): fc = str(self.__filter_complex) fc_args = ['-filter_complex', fc] if fc else [] # Namer context is used to generate unique output stream names return (super().get_args() + self.__inputs.get_args() + ensure_binary(fc_args) + self.__outputs.get_args()) def add_input(self, input_file: Input) -> Input: """ Adds new source to ffmpeg. >>> ff = FFMPEG() >>> ff.add_input(Input(input_file="/tmp/input.mp4")) >>> """ if not isinstance(input_file, Input): raise ValueError('Illegal input file type') self.__inputs.append(input_file) return input_file def add_output(self, output: Output) -> Output: """ Adds output file to ffmpeg and connect it's codecs to free sources. >>> ff = FFMPEG() >>> ff.add_output(Output(output_file='/tmp/output.mp4')) >>> """ self.__outputs.append(output) for codec in output.codecs: self._add_codec(codec) return output def handle_stderr(self, line: str) -> str: """ Handle ffmpeg output. Capture only lines containing one of `stderr_markers`. :param line: ffmpeg output line :returns: line to be appended to whole ffmpeg output. """ if not self.stderr_markers: # if no markers are defined, handle each line return super().handle_stderr(line) # capture only lines containing markers for marker in self.stderr_markers: if marker in line: return super().handle_stderr(line) return '' def check_buffering(self) -> None: """ Checks that ffmpeg command will not cause frame buffering and out of memory errors. Each input file must be read simultaneously be all codecs in outputs, or some streams will be buffered until requested by output codecs. """ chains = [] for output in self.__outputs: for codec in output.codecs: streams = codec.check_buffering() if streams is None: # Streams can't be computed because of missing metadata. raise ValueError(streams) chains.append(streams) for chunk in zip(*chains): # Check that every codec reads same input stream if len(set(chunk)) > 1: # some codec read different file at this step, so one of input # stream will be buffered until this file is read by another # codec. raise BufferError(chunk)
class Output(BaseWrapper): # noinspection PyUnresolvedReferences """ Base class for ffmpeg output. :arg codecs: list of codecs used in output. :arg format: output file format. :arg output_file: output file name. """ codecs: List[Codec] = param(default=list, skip=True) no_video: Optional[bool] = param(name='vn') no_audio: Optional[bool] = param(name='an') format: str = param(name="f") output_file: str = param(name="", skip=True) def __lt__(self, other: base.InputType) -> "Output": """ Connects a source or a filter to a first free codec. If there is no free codecs, new codec stub is created. """ codec = self.get_free_codec(other.kind) other.connect_dest(codec) return self @property def video(self) -> Codec: """ :returns: first video codec not connected to source. If no free codecs left, new one codec stub is appended to output. """ return self.get_free_codec(VIDEO) @property def audio(self) -> Codec: """ :returns: first audio codec not connected to source. If no free codecs left, new one codec stub is appended to output. """ return self.get_free_codec(AUDIO) def get_free_codec(self, kind: StreamType, create: bool = True) -> Codec: """ Finds first codec not connected to filter graph or to an input, or creates a new unnamed codec stub if no free codecs left. :param kind: desired codec stream type :param create: create new codec stub :return: first free codec or a new codec stub. """ try: codec = next( filter(lambda c: not c.connected and c.kind == kind, self.codecs)) except StopIteration: if not create: raise KeyError(kind) codec = Codec() codec.kind = kind self.codecs.append(codec) return codec def get_args(self) -> List[bytes]: """ :returns: codec args and output file parameters for ffmpeg """ args = [] # Check if we need to disable audio or video for output file because no # corresponding codecs are found. # Skipping `-an` / `-vn` parameters is still supported by manually # setting `no_audio` / `no_video` parameters to `False`. for codec in self.codecs: if codec.kind == VIDEO: self.no_video = False if codec.kind == AUDIO: self.no_audio = False args.extend(codec.get_args()) if self.no_video is None: self.no_video = True if self.no_audio is None: self.no_audio = True args.extend(super().get_args()) args.append(ensure_binary(self.output_file)) return args
class Codec(mixins.StreamValidationMixin, base.Dest, BaseWrapper): # noinspection PyUnresolvedReferences """ Base class for output codecs. :arg codec: ffmpeg codec name. :arg bitrate: output bitrate in bps. """ index = cast(int, base.Once('index')) """ Index of current codec in ffmpeg output streams.""" codec: str = param(name='c', stream_suffix=True) bitrate: int = param(default=0, name='b', stream_suffix=True) def __post_init__(self) -> None: if self.codec is None: self.codec = self.__class__.codec super().__post_init__() @property def map(self) -> Optional[str]: """ :returns: `-map` argument value depending of a node or a source connected to codec. """ if self.edge is None: raise RuntimeError("Codec not connected to source") source = self.edge.input # Source input has name generated from input file index, stream # specifier and stream index. Node has no attribute index, so we use # Dest name ("[vout0]") as map argument. See also `Node.get_filter_args` return getattr(source, 'name', self.name) @property def connected(self) -> bool: """ :return: True if codec is already connected to a node or a source. """ return bool(self.edge) def get_args(self) -> List[bytes]: args = ['-map', self.map] return ensure_binary(args) + super().get_args() def clone(self, count: int = 1) -> List["Codec"]: """ Creates multiple copies of self to reuse it as output node for multiple sources. Any connected input node is being split and connected to a corresponding copy of current filter. """ if count == 1: return [self] raise RuntimeError("Trying to clone codec, is this intended?") def check_buffering(self) -> Optional[List[str]]: """ Check that scenes read from input stream are ordered with ascending timestamps. :returns: A list of streams needed for this codec or None if metadata for codec can't be computed. """ meta = self.get_meta_data() if not meta: return None prev = meta.scenes[0] for scene in meta.scenes[1:]: if prev.stream == scene.stream and prev.end > scene.start: # Previous scene in same stream is located after current, so # current decoded scene will be buffered until previous scene is # decoded. raise BufferError(prev, scene) prev = scene return meta.streams
class FdkAAC(codecs.AudioCodec): codec = 'libfdk_aac' bitrate: int = param(name='b', stream_suffix=True) def transform(self, metadata: Meta) -> Meta: return replace(metadata, bitrate=self.bitrate)