platypush/platypush/plugins/camera/model/writer/ffmpeg.py

177 lines
5.7 KiB
Python
Raw Normal View History

2020-09-19 00:50:22 +02:00
import subprocess
import threading
import time
from abc import ABC
from typing import Optional, Tuple
2020-09-19 00:50:22 +02:00
from PIL.Image import Image
from platypush.plugins.camera.model.camera import Camera
2020-09-19 00:50:22 +02:00
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):
2020-09-19 00:50:22 +02:00
super().__init__(*args, **kwargs)
self.input_file = input_file
self.input_format = input_format
self.output_format = output_format
self.output_file = output_file
2020-09-19 00:50:22 +02:00
self.width, self.height = self.camera.effective_resolution()
self.pix_fmt = pix_fmt
self.output_opts = output_opts or ()
2020-09-19 00:50:22 +02:00
self.logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args)))
2020-09-19 00:50:22 +02:00
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',
2020-09-19 00:50:22 +02:00
'-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 ()),
2020-09-19 00:50:22 +02:00
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)))
2020-09-19 00:50:22 +02:00
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')
2020-09-19 00:50:22 +02:00
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):
2021-04-06 21:10:48 +02:00
super().__init__(*args, output_file=output_file, pix_fmt='rgb24', **kwargs)
2020-09-19 00:50:22 +02:00
class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
"""
Stream camera frames using FFmpeg.
"""
def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs):
2021-04-06 21:10:48 +02:00
super().__init__(*args, pix_fmt='rgb24', output_format=output_format, output_opts=output_opts or (
'-tune', 'zerolatency', '-preset', 'superfast', '-trellis', '0',
'-fflags', 'nobuffer'), **kwargs)
2020-09-19 00:50:22 +02:00
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)))
2020-09-19 00:50:22 +02:00
break
if not data:
continue
if self.frame is None:
latency = time.time() - start_time
self.logger.info('FFmpeg stream latency: {} secs'.format(latency))
2020-09-19 00:50:22 +02:00
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)))
2020-09-19 00:50:22 +02:00
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)
2020-09-19 00:50:22 +02:00
class H265StreamWriter(FFmpegStreamWriter):
mimetype = 'video/h265'
def __init__(self, *args, **kwargs):
super().__init__(*args, output_format='h265', **kwargs)
# vim:sw=4:ts=4:et: