2020-09-19 00:50:22 +02:00
|
|
|
import subprocess
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
|
|
|
|
from abc import ABC
|
2020-09-27 12:51:29 +02:00
|
|
|
from typing import Optional, Tuple
|
2020-09-19 00:50:22 +02:00
|
|
|
|
|
|
|
from PIL.Image import Image
|
|
|
|
|
2020-09-27 12:51:29 +02:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2020-09-27 12:51:29 +02:00
|
|
|
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
|
2020-09-27 12:51:29 +02:00
|
|
|
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
|
2020-09-27 12:51:29 +02:00
|
|
|
self.output_opts = output_opts or ()
|
2020-09-19 00:50:22 +02:00
|
|
|
|
2020-09-27 12:51:29 +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):
|
2020-09-27 12:51:29 +02:00
|
|
|
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,
|
2020-09-27 12:51:29 +02:00
|
|
|
*(('-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:
|
2020-09-27 12:51:29 +02:00
|
|
|
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:
|
2020-09-27 12:51:29 +02:00
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2020-09-27 12:51:29 +02:00
|
|
|
def __init__(self, *args, output_file: str, **kwargs):
|
|
|
|
FileVideoWriter.__init__(self, *args, output_file=output_file, **kwargs)
|
|
|
|
FFmpegWriter.__init__(self, *args, pix_fmt='rgb24', output_file=self.output_file, **kwargs)
|
2020-09-19 00:50:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
|
|
|
"""
|
|
|
|
Stream camera frames using FFmpeg.
|
|
|
|
"""
|
|
|
|
|
2020-09-27 12:51:29 +02:00
|
|
|
def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs):
|
2020-09-19 00:50:22 +02:00
|
|
|
StreamWriter.__init__(self, *args, **kwargs)
|
2020-09-27 12:51:29 +02:00
|
|
|
FFmpegWriter.__init__(self, *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:
|
2020-09-27 12:51:29 +02:00
|
|
|
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
|
2020-09-27 12:51:29 +02:00
|
|
|
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:
|
2020-09-27 12:51:29 +02:00
|
|
|
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'
|
|
|
|
|
2020-09-27 12:51:29 +02:00
|
|
|
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:
|