import subprocess import threading import time from abc import ABC from typing import Optional, Tuple from PIL.Image import Image from platypush.plugins.camera.model.camera import Camera from platypush.plugins.camera.model.writer import VideoWriter, FileVideoWriter, StreamWriter class FFmpegWriter(VideoWriter, ABC): """ Generic FFmpeg encoder for camera frames. """ def __init__(self, *args, input_file: str = '-', input_format: str = 'rawvideo', output_file: str = '-', output_format: Optional[str] = None, pix_fmt: Optional[str] = None, output_opts: Optional[Tuple] = None, **kwargs): super().__init__(*args, **kwargs) self.input_file = input_file self.input_format = input_format self.output_format = output_format self.output_file = output_file self.width, self.height = self.camera.effective_resolution() self.pix_fmt = pix_fmt self.output_opts = output_opts or () self.logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args))) self.ffmpeg = subprocess.Popen(self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) @property def ffmpeg_args(self): return [self.camera.info.ffmpeg_bin, '-y', '-f', self.input_format, *(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()), '-s', '{}x{}'.format(self.width, self.height), '-r', str(self.camera.info.fps), '-i', self.input_file, *(('-f', self.output_format) if self.output_format else ()), *self.output_opts, *(('-vcodec', self.camera.info.output_codec) if self.camera.info.output_codec else ()), self.output_file] def is_closed(self): return self.closed or not self.ffmpeg or self.ffmpeg.poll() is not None def write(self, image: Image): if self.is_closed(): return try: self.ffmpeg.stdin.write(image.convert('RGB').tobytes()) except Exception as e: self.logger.warning('FFmpeg send error: {}'.format(str(e))) self.close() def close(self): if not self.is_closed(): if self.ffmpeg and self.ffmpeg.stdin: try: self.ffmpeg.stdin.close() except (IOError, OSError): pass if self.ffmpeg: self.ffmpeg.terminate() try: self.ffmpeg.wait(timeout=5.0) except subprocess.TimeoutExpired: self.logger.warning('FFmpeg has not returned - killing it') self.ffmpeg.kill() if self.ffmpeg and self.ffmpeg.stdout: try: self.ffmpeg.stdout.close() except (IOError, OSError): pass self.ffmpeg = None super().close() class FFmpegFileWriter(FileVideoWriter, FFmpegWriter): """ Write camera frames to a file using FFmpeg. """ def __init__(self, *args, output_file: str, **kwargs): super().__init__(*args, output_file=output_file, pix_fmt='rgb24', **kwargs) class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC): """ Stream camera frames using FFmpeg. """ def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs): super().__init__(*args, pix_fmt='rgb24', output_format=output_format, output_opts=output_opts or ( '-tune', 'zerolatency', '-preset', 'superfast', '-trellis', '0', '-fflags', 'nobuffer'), **kwargs) self._reader = threading.Thread(target=self._reader_thread) self._reader.start() def encode(self, image: Image) -> bytes: return image.convert('RGB').tobytes() def _reader_thread(self): start_time = time.time() while not self.is_closed(): try: data = self.ffmpeg.stdout.read(1 << 15) except Exception as e: self.logger.warning('FFmpeg reader error: {}'.format(str(e))) break if not data: continue if self.frame is None: latency = time.time() - start_time self.logger.info('FFmpeg stream latency: {} secs'.format(latency)) with self.ready: self.frame = data self.frame_time = time.time() self.ready.notify_all() self._sock_send(self.frame) def write(self, image: Image): if self.is_closed(): return data = self.encode(image) try: self.ffmpeg.stdin.write(data) except Exception as e: self.logger.warning('FFmpeg send error: {}'.format(str(e))) self.close() def close(self): super().close() if self._reader and self._reader.is_alive() and threading.get_ident() != self._reader.ident: self._reader.join(timeout=5.0) self._reader = None class MKVStreamWriter(FFmpegStreamWriter): mimetype = 'video/webm' def __init__(self, *args, **kwargs): super().__init__(*args, output_format='matroska', **kwargs) class H264StreamWriter(FFmpegStreamWriter): mimetype = 'video/h264' def __init__(self, camera: Camera, *args, **kwargs): if not camera.info.output_codec: camera.info.output_codec = 'libxvid' super().__init__(camera, *args, output_format='h264', **kwargs) class H265StreamWriter(FFmpegStreamWriter): mimetype = 'video/h265' def __init__(self, *args, **kwargs): super().__init__(*args, output_format='h265', **kwargs) # vim:sw=4:ts=4:et: