forked from platypush/platypush
Added FFmpeg camera plugin [relates to #150]
This commit is contained in:
parent
b21193dc74
commit
df1e03f0af
13 changed files with 360 additions and 205 deletions
|
@ -0,0 +1 @@
|
||||||
|
camera
|
|
@ -1,116 +0,0 @@
|
||||||
@import 'common/vars';
|
|
||||||
|
|
||||||
.camera {
|
|
||||||
min-height: 90%;
|
|
||||||
margin-top: 4%;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.camera-container {
|
|
||||||
position: relative;
|
|
||||||
background: black;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
|
|
||||||
.frame, .no-frame {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.frame {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-frame {
|
|
||||||
display: flex;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
color: white;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.url {
|
|
||||||
width: 640px;
|
|
||||||
display: flex;
|
|
||||||
margin: 1em;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 500px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.params {
|
|
||||||
margin-top: 1em;
|
|
||||||
padding: 1em;
|
|
||||||
width: 640px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: $default-border-3;
|
|
||||||
border-radius: 1em;
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
label {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: right;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0 0 0 -1em;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background: $default-bg-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $hover-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -71,8 +71,10 @@ class CameraPlugin(Plugin, ABC):
|
||||||
capture_timeout: Optional[float] = 20.0, scale_x: Optional[float] = None,
|
capture_timeout: Optional[float] = 20.0, scale_x: Optional[float] = None,
|
||||||
scale_y: Optional[float] = None, rotate: Optional[float] = None, grayscale: Optional[bool] = None,
|
scale_y: Optional[float] = None, rotate: Optional[float] = None, grayscale: Optional[bool] = None,
|
||||||
color_transform: Optional[Union[int, str]] = None, fps: float = 16, horizontal_flip: bool = False,
|
color_transform: Optional[Union[int, str]] = None, fps: float = 16, horizontal_flip: bool = False,
|
||||||
vertical_flip: bool = False, video_type: Optional[str] = None, stream_format: str = 'mjpeg',
|
vertical_flip: bool = False, input_format: Optional[str] = None, output_format: Optional[str] = None,
|
||||||
listen_port: Optional[int] = 5000, bind_address: str = '0.0.0.0', **kwargs):
|
stream_format: str = 'mjpeg', listen_port: Optional[int] = 5000, bind_address: str = '0.0.0.0',
|
||||||
|
ffmpeg_bin: str = 'ffmpeg', input_codec: Optional[str] = None, output_codec: Optional[str] = None,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
:param device: Identifier of the default capturing device.
|
:param device: Identifier of the default capturing device.
|
||||||
:param resolution: Default resolution, as a tuple of two integers.
|
:param resolution: Default resolution, as a tuple of two integers.
|
||||||
|
@ -96,9 +98,14 @@ class CameraPlugin(Plugin, ABC):
|
||||||
:param fps: Frames per second (default: 25).
|
:param fps: Frames per second (default: 25).
|
||||||
:param horizontal_flip: If set, the images will be flipped on the horizontal axis.
|
:param horizontal_flip: If set, the images will be flipped on the horizontal axis.
|
||||||
:param vertical_flip: If set, the images will be flipped on the vertical axis.
|
:param vertical_flip: If set, the images will be flipped on the vertical axis.
|
||||||
:param video_type: Plugin-specific format/type for the output videos.
|
|
||||||
:param listen_port: Default port to be used for streaming over TCP (default: 5000).
|
:param listen_port: Default port to be used for streaming over TCP (default: 5000).
|
||||||
:param bind_address: Default bind address for TCP streaming (default: 0.0.0.0, accept any connections).
|
:param bind_address: Default bind address for TCP streaming (default: 0.0.0.0, accept any connections).
|
||||||
|
:param input_codec: Specify the ffmpeg video codec (``-vcodec``) used for the input.
|
||||||
|
:param output_codec: Specify the ffmpeg video codec (``-vcodec``) to be used for encoding the output. For some
|
||||||
|
ffmpeg output formats (e.g. ``h264`` and ``rtp``) this may default to ``libxvid``.
|
||||||
|
:param input_format: Plugin-specific format/type for the input stream.
|
||||||
|
:param output_format: Plugin-specific format/type for the output videos.
|
||||||
|
:param ffmpeg_bin: Path to the ffmpeg binary (default: ``ffmpeg``).
|
||||||
:param stream_format: Default format for the output when streamed to a network device. Available:
|
:param stream_format: Default format for the output when streamed to a network device. Available:
|
||||||
|
|
||||||
- ``MJPEG`` (default)
|
- ``MJPEG`` (default)
|
||||||
|
@ -110,26 +117,29 @@ class CameraPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
_default_frames_dir = os.path.join(Config.get('workdir'), get_plugin_name_by_class(self), 'frames')
|
self.workdir = os.path.join(Config.get('workdir'), get_plugin_name_by_class(self))
|
||||||
|
pathlib.Path(self.workdir).mkdir(mode=0o644, exist_ok=True, parents=True)
|
||||||
|
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
self.camera_info = self._camera_info_class(device, color_transform=color_transform, warmup_frames=warmup_frames,
|
self.camera_info = self._camera_info_class(device, color_transform=color_transform, warmup_frames=warmup_frames,
|
||||||
warmup_seconds=warmup_seconds, rotate=rotate, scale_x=scale_x,
|
warmup_seconds=warmup_seconds, rotate=rotate, scale_x=scale_x,
|
||||||
scale_y=scale_y, capture_timeout=capture_timeout,
|
scale_y=scale_y, capture_timeout=capture_timeout, fps=fps,
|
||||||
video_type=video_type, fps=fps, stream_format=stream_format,
|
input_format=input_format, output_format=output_format,
|
||||||
resolution=resolution, grayscale=grayscale, listen_port=listen_port,
|
stream_format=stream_format, resolution=resolution,
|
||||||
|
grayscale=grayscale, listen_port=listen_port,
|
||||||
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
|
horizontal_flip=horizontal_flip, vertical_flip=vertical_flip,
|
||||||
bind_address=bind_address, frames_dir=os.path.abspath(
|
ffmpeg_bin=ffmpeg_bin, input_codec=input_codec,
|
||||||
os.path.expanduser(frames_dir or _default_frames_dir)))
|
output_codec=output_codec, bind_address=bind_address,
|
||||||
|
frames_dir=os.path.abspath(
|
||||||
|
os.path.expanduser(frames_dir or
|
||||||
|
os.path.join(self.workdir, 'frames'))))
|
||||||
|
|
||||||
self._devices: Dict[Union[int, str], Camera] = {}
|
self._devices: Dict[Union[int, str], Camera] = {}
|
||||||
self._streams: Dict[Union[int, str], Camera] = {}
|
self._streams: Dict[Union[int, str], Camera] = {}
|
||||||
|
|
||||||
def _merge_info(self, **info) -> CameraInfo:
|
def _merge_info(self, **info) -> CameraInfo:
|
||||||
merged_info = self.camera_info.clone()
|
merged_info = self.camera_info.clone()
|
||||||
for k, v in info.items():
|
merged_info.set(**info)
|
||||||
if hasattr(merged_info, k):
|
|
||||||
setattr(merged_info, k, v)
|
|
||||||
|
|
||||||
return merged_info
|
return merged_info
|
||||||
|
|
||||||
def open_device(self, device: Optional[Union[int, str]] = None, stream: bool = False, **params) -> Camera:
|
def open_device(self, device: Optional[Union[int, str]] = None, stream: bool = False, **params) -> Camera:
|
||||||
|
@ -179,12 +189,12 @@ class CameraPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
name = camera.info.device
|
name = camera.info.device
|
||||||
self.stop_preview(camera)
|
self.stop_preview(camera)
|
||||||
camera.start_event.clear()
|
self.release_device(camera)
|
||||||
|
|
||||||
|
camera.start_event.clear()
|
||||||
if wait_capture:
|
if wait_capture:
|
||||||
self.wait_capture(camera)
|
self.wait_capture(camera)
|
||||||
|
|
||||||
self.release_device(camera)
|
|
||||||
if name in self._devices:
|
if name in self._devices:
|
||||||
del self._devices[name]
|
del self._devices[name]
|
||||||
|
|
||||||
|
@ -356,7 +366,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
if duration and camera.info.warmup_seconds:
|
if duration and camera.info.warmup_seconds:
|
||||||
duration = duration + camera.info.warmup_seconds
|
duration = duration + camera.info.warmup_seconds
|
||||||
if video_file:
|
if video_file:
|
||||||
camera.file_writer = self._video_writer_class(camera=camera, video_file=video_file, plugin=self)
|
camera.file_writer = self._video_writer_class(camera=camera, plugin=self, output_file=video_file)
|
||||||
|
|
||||||
frame_queue = Queue()
|
frame_queue = Queue()
|
||||||
frame_processor = threading.Thread(target=self.frame_processor,
|
frame_processor = threading.Thread(target=self.frame_processor,
|
||||||
|
@ -373,6 +383,10 @@ class CameraPlugin(Plugin, ABC):
|
||||||
frame_capture_start = time.time()
|
frame_capture_start = time.time()
|
||||||
try:
|
try:
|
||||||
frame = self.capture_frame(camera, **kwargs)
|
frame = self.capture_frame(camera, **kwargs)
|
||||||
|
if not frame:
|
||||||
|
self.logger.warning('Invalid frame received, terminating the capture session')
|
||||||
|
break
|
||||||
|
|
||||||
frame_queue.put(frame)
|
frame_queue.put(frame)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
self.logger.warning(str(e))
|
self.logger.warning(str(e))
|
||||||
|
@ -478,12 +492,13 @@ class CameraPlugin(Plugin, ABC):
|
||||||
return image_file
|
return image_file
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def take_picture(self, image_file: str, **camera) -> str:
|
def take_picture(self, image_file: str, preview: bool = False, **camera) -> str:
|
||||||
"""
|
"""
|
||||||
Alias for :meth:`.capture_image`.
|
Alias for :meth:`.capture_image`.
|
||||||
|
|
||||||
:param image_file: Path where the output image will be stored.
|
:param image_file: Path where the output image will be stored.
|
||||||
:param camera: Camera parameters override - see constructors parameters.
|
:param camera: Camera parameters override - see constructors parameters.
|
||||||
|
:param preview: Show a preview of the camera frames.
|
||||||
:return: The local path to the saved image.
|
:return: The local path to the saved image.
|
||||||
"""
|
"""
|
||||||
return self.capture_image(image_file, **camera)
|
return self.capture_image(image_file, **camera)
|
||||||
|
@ -599,11 +614,11 @@ class CameraPlugin(Plugin, ABC):
|
||||||
assert not camera.stream_event.is_set() and camera.info.device not in self._streams, \
|
assert not camera.stream_event.is_set() and camera.info.device not in self._streams, \
|
||||||
'A streaming session is already running for device {}'.format(camera.info.device)
|
'A streaming session is already running for device {}'.format(camera.info.device)
|
||||||
|
|
||||||
|
self._streams[camera.info.device] = camera
|
||||||
|
camera.stream_event.set()
|
||||||
|
|
||||||
camera.stream_thread = threading.Thread(target=self.streaming_thread, kwargs=dict(
|
camera.stream_thread = threading.Thread(target=self.streaming_thread, kwargs=dict(
|
||||||
camera=camera, duration=duration, stream_format=stream_format))
|
camera=camera, duration=duration, stream_format=stream_format))
|
||||||
self._streams[camera.info.device] = camera
|
|
||||||
|
|
||||||
camera.stream_event.set()
|
|
||||||
camera.stream_thread.start()
|
camera.stream_thread.start()
|
||||||
return self.status(camera.info.device)
|
return self.status(camera.info.device)
|
||||||
|
|
||||||
|
@ -624,7 +639,7 @@ class CameraPlugin(Plugin, ABC):
|
||||||
|
|
||||||
def _stop_streaming(self, camera: Camera):
|
def _stop_streaming(self, camera: Camera):
|
||||||
camera.stream_event.clear()
|
camera.stream_event.clear()
|
||||||
if camera.stream_thread.is_alive():
|
if camera.stream_thread and camera.stream_thread.is_alive():
|
||||||
camera.stream_thread.join(timeout=5.0)
|
camera.stream_thread.join(timeout=5.0)
|
||||||
|
|
||||||
if camera.info.device in self._streams:
|
if camera.info.device in self._streams:
|
||||||
|
|
83
platypush/plugins/camera/ffmpeg/__init__.py
Normal file
83
platypush/plugins/camera/ffmpeg/__init__.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
from PIL.Image import Image as ImageType
|
||||||
|
|
||||||
|
from platypush.plugins.camera import CameraPlugin
|
||||||
|
from platypush.plugins.camera.ffmpeg.model import FFmpegCamera, FFmpegCameraInfo
|
||||||
|
|
||||||
|
|
||||||
|
class CameraFfmpegPlugin(CameraPlugin):
|
||||||
|
"""
|
||||||
|
Plugin to interact with a camera over FFmpeg.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **ffmpeg** package installed on the system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_camera_class = FFmpegCamera
|
||||||
|
_camera_info_class = FFmpegCameraInfo
|
||||||
|
|
||||||
|
def __init__(self, device: Optional[str] = None, input_format: str = 'v4l2', ffmpeg_args: Tuple[str] = (), **opts):
|
||||||
|
"""
|
||||||
|
:param device: Path to the camera device (e.g. ``/dev/video0``).
|
||||||
|
:param input_format: FFmpeg input format for the the camera device (default: ``v4l2``).
|
||||||
|
:param ffmpeg_args: Extra options to be passed to the FFmpeg executable.
|
||||||
|
:param opts: Camera options - see constructor of :class:`platypush.plugins.camera.CameraPlugin`.
|
||||||
|
"""
|
||||||
|
super().__init__(device=device, input_format=input_format, **opts)
|
||||||
|
self.camera_info.ffmpeg_args = ffmpeg_args or ()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_warmup_seconds(camera: FFmpegCamera) -> float:
|
||||||
|
if camera.info.warmup_seconds:
|
||||||
|
return camera.info.warmup_seconds
|
||||||
|
if camera.info.warmup_frames and camera.info.fps:
|
||||||
|
return camera.info.warmup_frames / camera.info.fps
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def prepare_device(self, camera: FFmpegCamera) -> subprocess.Popen:
|
||||||
|
warmup_seconds = self._get_warmup_seconds(camera)
|
||||||
|
ffmpeg = [camera.info.ffmpeg_bin, '-y', '-f', camera.info.input_format, '-i', camera.info.device, '-s',
|
||||||
|
'{}x{}'.format(*camera.info.resolution), '-ss', str(warmup_seconds),
|
||||||
|
*(('-r', str(camera.info.fps)) if camera.info.fps else ()),
|
||||||
|
'-pix_fmt', 'rgb24', '-f', 'rawvideo', *camera.info.ffmpeg_args, '-']
|
||||||
|
|
||||||
|
self.logger.info('Running FFmpeg: {}'.format(' '.join(ffmpeg)))
|
||||||
|
proc = subprocess.Popen(ffmpeg, stdout=subprocess.PIPE)
|
||||||
|
# Start in suspended mode
|
||||||
|
proc.send_signal(signal.SIGSTOP)
|
||||||
|
return proc
|
||||||
|
|
||||||
|
def start_camera(self, camera: FFmpegCamera, preview: bool = False, *args, **kwargs):
|
||||||
|
super().start_camera(*args, camera=camera, preview=preview, **kwargs)
|
||||||
|
if camera.object:
|
||||||
|
camera.object.send_signal(signal.SIGCONT)
|
||||||
|
|
||||||
|
def release_device(self, camera: FFmpegCamera):
|
||||||
|
if camera.object:
|
||||||
|
camera.object.terminate()
|
||||||
|
if camera.object.stdout:
|
||||||
|
camera.object.stdout.close()
|
||||||
|
camera.object = None
|
||||||
|
|
||||||
|
def wait_capture(self, camera: FFmpegCamera) -> None:
|
||||||
|
if camera.object and camera.object.poll() is None:
|
||||||
|
try:
|
||||||
|
camera.object.wait(timeout=camera.info.capture_timeout)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Error on FFmpeg capture wait: {}'.format(str(e)))
|
||||||
|
|
||||||
|
def capture_frame(self, camera: FFmpegCamera, *args, **kwargs) -> Optional[ImageType]:
|
||||||
|
raw_size = camera.info.resolution[0] * camera.info.resolution[1] * 3
|
||||||
|
data = camera.object.stdout.read(raw_size)
|
||||||
|
if len(data) < raw_size:
|
||||||
|
return
|
||||||
|
return Image.frombytes('RGB', camera.info.resolution, data)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
24
platypush/plugins/camera/ffmpeg/model.py
Normal file
24
platypush/plugins/camera/ffmpeg/model.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from subprocess import Popen
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from platypush.plugins.camera import CameraInfo, Camera
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FFmpegCameraInfo(CameraInfo):
|
||||||
|
ffmpeg_args: Tuple[str] = ()
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'ffmpeg_args': list(self.ffmpeg_args or ()),
|
||||||
|
**super().to_dict()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegCamera(Camera):
|
||||||
|
info: FFmpegCameraInfo
|
||||||
|
object: Popen
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -12,48 +12,57 @@ from platypush.plugins.camera.model.writer.preview import PreviewWriter
|
||||||
@dataclass
|
@dataclass
|
||||||
class CameraInfo:
|
class CameraInfo:
|
||||||
device: Optional[Union[int, str]]
|
device: Optional[Union[int, str]]
|
||||||
resolution: Optional[Tuple[int, int]] = None
|
bind_address: Optional[str] = None
|
||||||
|
capture_timeout: float = 20.0
|
||||||
color_transform: Optional[str] = None
|
color_transform: Optional[str] = None
|
||||||
|
ffmpeg_bin: Optional[str] = None
|
||||||
|
fps: Optional[float] = None
|
||||||
frames_dir: Optional[str] = None
|
frames_dir: Optional[str] = None
|
||||||
rotate: Optional[float] = None
|
grayscale: Optional[bool] = None
|
||||||
horizontal_flip: bool = False
|
horizontal_flip: bool = False
|
||||||
vertical_flip: bool = False
|
input_codec: Optional[str] = None
|
||||||
|
input_format: Optional[str] = None
|
||||||
|
listen_port: Optional[int] = None
|
||||||
|
output_codec: Optional[str] = None
|
||||||
|
output_format: Optional[str] = None
|
||||||
|
resolution: Optional[Tuple[int, int]] = None
|
||||||
|
rotate: Optional[float] = None
|
||||||
scale_x: Optional[float] = None
|
scale_x: Optional[float] = None
|
||||||
scale_y: Optional[float] = None
|
scale_y: Optional[float] = None
|
||||||
|
stream_format: Optional[str] = None
|
||||||
|
vertical_flip: bool = False
|
||||||
warmup_frames: int = 0
|
warmup_frames: int = 0
|
||||||
warmup_seconds: float = 0.
|
warmup_seconds: float = 0.
|
||||||
capture_timeout: float = 20.0
|
|
||||||
fps: Optional[float] = None
|
|
||||||
grayscale: Optional[bool] = None
|
|
||||||
video_type: Optional[str] = None
|
|
||||||
stream_format: str = 'mjpeg'
|
|
||||||
listen_port: Optional[int] = None
|
|
||||||
bind_address: Optional[str] = None
|
|
||||||
|
|
||||||
def set(self, **kwargs):
|
def set(self, **kwargs):
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
setattr(self, k, v)
|
if hasattr(self, k):
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
'device': self.device,
|
'bind_address': self.bind_address,
|
||||||
|
'capture_timeout': self.capture_timeout,
|
||||||
'color_transform': self.color_transform,
|
'color_transform': self.color_transform,
|
||||||
|
'device': self.device,
|
||||||
|
'ffmpeg_bin': self.ffmpeg_bin,
|
||||||
|
'fps': self.fps,
|
||||||
'frames_dir': self.frames_dir,
|
'frames_dir': self.frames_dir,
|
||||||
'rotate': self.rotate,
|
'grayscale': self.grayscale,
|
||||||
'horizontal_flip': self.horizontal_flip,
|
'horizontal_flip': self.horizontal_flip,
|
||||||
'vertical_flip': self.vertical_flip,
|
'input_codec': self.input_codec,
|
||||||
|
'input_format': self.input_format,
|
||||||
|
'listen_port': self.listen_port,
|
||||||
|
'output_codec': self.output_codec,
|
||||||
|
'output_format': self.output_format,
|
||||||
|
'resolution': list(self.resolution or ()),
|
||||||
|
'rotate': self.rotate,
|
||||||
'scale_x': self.scale_x,
|
'scale_x': self.scale_x,
|
||||||
'scale_y': self.scale_y,
|
'scale_y': self.scale_y,
|
||||||
|
'stream_format': self.stream_format,
|
||||||
|
'vertical_flip': self.vertical_flip,
|
||||||
'warmup_frames': self.warmup_frames,
|
'warmup_frames': self.warmup_frames,
|
||||||
'warmup_seconds': self.warmup_seconds,
|
'warmup_seconds': self.warmup_seconds,
|
||||||
'capture_timeout': self.capture_timeout,
|
|
||||||
'fps': self.fps,
|
|
||||||
'grayscale': self.grayscale,
|
|
||||||
'resolution': list(self.resolution or ()),
|
|
||||||
'video_type': self.video_type,
|
|
||||||
'stream_format': self.stream_format,
|
|
||||||
'listen_port': self.listen_port,
|
|
||||||
'bind_address': self.bind_address,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
|
|
|
@ -9,8 +9,6 @@ from typing import Optional, IO
|
||||||
|
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
|
|
||||||
logger = logging.getLogger('video-writer')
|
|
||||||
|
|
||||||
|
|
||||||
class VideoWriter(ABC):
|
class VideoWriter(ABC):
|
||||||
"""
|
"""
|
||||||
|
@ -21,6 +19,8 @@ class VideoWriter(ABC):
|
||||||
|
|
||||||
def __init__(self, camera, plugin, *_, **__):
|
def __init__(self, camera, plugin, *_, **__):
|
||||||
from platypush.plugins.camera import Camera, CameraPlugin
|
from platypush.plugins.camera import Camera, CameraPlugin
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.camera: Camera = camera
|
self.camera: Camera = camera
|
||||||
self.plugin: CameraPlugin = plugin
|
self.plugin: CameraPlugin = plugin
|
||||||
self.closed = False
|
self.closed = False
|
||||||
|
@ -60,9 +60,9 @@ class FileVideoWriter(VideoWriter, ABC):
|
||||||
"""
|
"""
|
||||||
Abstract class to handle frames-to-video file operations.
|
Abstract class to handle frames-to-video file operations.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, video_file: str, **kwargs):
|
def __init__(self, *args, output_file: str, **kwargs):
|
||||||
VideoWriter.__init__(self, *args, **kwargs)
|
VideoWriter.__init__(self, *args, **kwargs)
|
||||||
self.video_file = os.path.abspath(os.path.expanduser(video_file))
|
self.output_file = os.path.abspath(os.path.expanduser(output_file))
|
||||||
|
|
||||||
|
|
||||||
class StreamWriter(VideoWriter, ABC):
|
class StreamWriter(VideoWriter, ABC):
|
||||||
|
@ -98,7 +98,7 @@ class StreamWriter(VideoWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.sock.write(data)
|
self.sock.write(data)
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
logger.warning('Client connection closed')
|
self.logger.info('Client connection closed')
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
@ -12,13 +12,13 @@ class CvFileWriter(FileVideoWriter):
|
||||||
import cv2
|
import cv2
|
||||||
super(CvFileWriter, self).__init__(*args, **kwargs)
|
super(CvFileWriter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
video_type = cv2.VideoWriter_fourcc(*(self.camera.info.video_type or 'xvid').upper())
|
video_type = cv2.VideoWriter_fourcc(*(self.camera.info.output_format or 'xvid').upper())
|
||||||
resolution = (
|
resolution = (
|
||||||
int(self.camera.info.resolution[0] * (self.camera.info.scale_x or 1.)),
|
int(self.camera.info.resolution[0] * (self.camera.info.scale_x or 1.)),
|
||||||
int(self.camera.info.resolution[1] * (self.camera.info.scale_y or 1.)),
|
int(self.camera.info.resolution[1] * (self.camera.info.scale_y or 1.)),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.writer = cv2.VideoWriter(self.video_file, video_type, self.camera.info.fps, resolution, False)
|
self.writer = cv2.VideoWriter(self.output_file, video_type, self.camera.info.fps, resolution, False)
|
||||||
|
|
||||||
def write(self, img):
|
def write(self, img):
|
||||||
if not self.writer:
|
if not self.writer:
|
||||||
|
|
|
@ -1,54 +1,48 @@
|
||||||
import logging
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Optional, List
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from PIL.Image import Image
|
from PIL.Image import Image
|
||||||
|
|
||||||
|
from platypush.plugins.camera.model.camera import Camera
|
||||||
from platypush.plugins.camera.model.writer import VideoWriter, FileVideoWriter, StreamWriter
|
from platypush.plugins.camera.model.writer import VideoWriter, FileVideoWriter, StreamWriter
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('ffmpeg-writer')
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegWriter(VideoWriter, ABC):
|
class FFmpegWriter(VideoWriter, ABC):
|
||||||
"""
|
"""
|
||||||
Generic FFmpeg encoder for camera frames.
|
Generic FFmpeg encoder for camera frames.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, input_file: str = '-', input_format: str = 'rawvideo', input_codec: Optional[str] = None,
|
def __init__(self, *args, input_file: str = '-', input_format: str = 'rawvideo', output_file: str = '-',
|
||||||
output_file: str = '-', output_format: Optional[str] = None, output_codec: Optional[str] = None,
|
output_format: Optional[str] = None, pix_fmt: Optional[str] = None,
|
||||||
pix_fmt: Optional[str] = None, output_opts: Optional[List[str]] = None, **kwargs):
|
output_opts: Optional[Tuple] = None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.input_file = input_file
|
self.input_file = input_file
|
||||||
self.input_format = input_format
|
self.input_format = input_format
|
||||||
self.input_codec = input_codec
|
|
||||||
self.output_file = output_file
|
|
||||||
self.output_format = output_format
|
self.output_format = output_format
|
||||||
self.output_codec = output_codec
|
self.output_file = output_file
|
||||||
self.width, self.height = self.camera.effective_resolution()
|
self.width, self.height = self.camera.effective_resolution()
|
||||||
self.pix_fmt = pix_fmt
|
self.pix_fmt = pix_fmt
|
||||||
self.output_opts = output_opts or []
|
self.output_opts = output_opts or ()
|
||||||
|
|
||||||
logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args)))
|
self.logger.info('Starting FFmpeg. Command: {}'.format(' '.join(self.ffmpeg_args)))
|
||||||
self.ffmpeg = subprocess.Popen(self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
self.ffmpeg = subprocess.Popen(self.ffmpeg_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ffmpeg_args(self):
|
def ffmpeg_args(self):
|
||||||
return ['ffmpeg', '-y',
|
return [self.camera.info.ffmpeg_bin, '-y',
|
||||||
'-f', self.input_format,
|
'-f', self.input_format,
|
||||||
*(('-vcodec', self.input_codec) if self.input_codec else ()),
|
|
||||||
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
|
*(('-pix_fmt', self.pix_fmt) if self.pix_fmt else ()),
|
||||||
'-s', '{}x{}'.format(self.width, self.height),
|
'-s', '{}x{}'.format(self.width, self.height),
|
||||||
'-r', str(self.camera.info.fps),
|
'-r', str(self.camera.info.fps),
|
||||||
'-i', self.input_file,
|
'-i', self.input_file,
|
||||||
*(('-f', self.output_format) if self.output_format else ()),
|
*(('-f', self.output_format) if self.output_format else ()),
|
||||||
*self.output_opts,
|
*self.output_opts,
|
||||||
*(('-vcodec', self.output_codec) if self.output_codec else ()),
|
*(('-vcodec', self.camera.info.output_codec) if self.camera.info.output_codec else ()),
|
||||||
self.output_file]
|
self.output_file]
|
||||||
|
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
|
@ -61,7 +55,7 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdin.write(image.convert('RGB').tobytes())
|
self.ffmpeg.stdin.write(image.convert('RGB').tobytes())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('FFmpeg send error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
@ -77,7 +71,7 @@ class FFmpegWriter(VideoWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.wait(timeout=5.0)
|
self.ffmpeg.wait(timeout=5.0)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
logger.warning('FFmpeg has not returned - killing it')
|
self.logger.warning('FFmpeg has not returned - killing it')
|
||||||
self.ffmpeg.kill()
|
self.ffmpeg.kill()
|
||||||
|
|
||||||
if self.ffmpeg and self.ffmpeg.stdout:
|
if self.ffmpeg and self.ffmpeg.stdout:
|
||||||
|
@ -95,9 +89,9 @@ class FFmpegFileWriter(FileVideoWriter, FFmpegWriter):
|
||||||
Write camera frames to a file using FFmpeg.
|
Write camera frames to a file using FFmpeg.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, video_file: str, **kwargs):
|
def __init__(self, *args, output_file: str, **kwargs):
|
||||||
FileVideoWriter.__init__(self, *args, video_file=video_file, **kwargs)
|
FileVideoWriter.__init__(self, *args, output_file=output_file, **kwargs)
|
||||||
FFmpegWriter.__init__(self, *args, pix_fmt='rgb24', output_file=self.video_file, **kwargs)
|
FFmpegWriter.__init__(self, *args, pix_fmt='rgb24', output_file=self.output_file, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
|
@ -105,12 +99,11 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
Stream camera frames using FFmpeg.
|
Stream camera frames using FFmpeg.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, output_format: str, **kwargs):
|
def __init__(self, *args, output_format: str, output_opts: Optional[Tuple] = None, **kwargs):
|
||||||
StreamWriter.__init__(self, *args, **kwargs)
|
StreamWriter.__init__(self, *args, **kwargs)
|
||||||
FFmpegWriter.__init__(self, *args, pix_fmt='rgb24', output_format=output_format,
|
FFmpegWriter.__init__(self, *args, pix_fmt='rgb24', output_format=output_format, output_opts=output_opts or (
|
||||||
output_opts=[
|
'-tune', 'zerolatency', '-preset', 'superfast', '-trellis', '0',
|
||||||
'-tune', '-zerolatency', '-preset', 'superfast', '-trellis', '0',
|
'-fflags', 'nobuffer'), **kwargs)
|
||||||
'-fflags', 'nobuffer'], **kwargs)
|
|
||||||
self._reader = threading.Thread(target=self._reader_thread)
|
self._reader = threading.Thread(target=self._reader_thread)
|
||||||
self._reader.start()
|
self._reader.start()
|
||||||
|
|
||||||
|
@ -124,7 +117,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
try:
|
try:
|
||||||
data = self.ffmpeg.stdout.read(1 << 15)
|
data = self.ffmpeg.stdout.read(1 << 15)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('FFmpeg reader error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg reader error: {}'.format(str(e)))
|
||||||
break
|
break
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
@ -132,7 +125,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
|
|
||||||
if self.frame is None:
|
if self.frame is None:
|
||||||
latency = time.time() - start_time
|
latency = time.time() - start_time
|
||||||
logger.info('FFmpeg stream latency: {} secs'.format(latency))
|
self.logger.info('FFmpeg stream latency: {} secs'.format(latency))
|
||||||
|
|
||||||
with self.ready:
|
with self.ready:
|
||||||
self.frame = data
|
self.frame = data
|
||||||
|
@ -149,7 +142,7 @@ class FFmpegStreamWriter(StreamWriter, FFmpegWriter, ABC):
|
||||||
try:
|
try:
|
||||||
self.ffmpeg.stdin.write(data)
|
self.ffmpeg.stdin.write(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('FFmpeg send error: {}'.format(str(e)))
|
self.logger.warning('FFmpeg send error: {}'.format(str(e)))
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
@ -169,8 +162,10 @@ class MKVStreamWriter(FFmpegStreamWriter):
|
||||||
class H264StreamWriter(FFmpegStreamWriter):
|
class H264StreamWriter(FFmpegStreamWriter):
|
||||||
mimetype = 'video/h264'
|
mimetype = 'video/h264'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, camera: Camera, *args, **kwargs):
|
||||||
super().__init__(*args, output_format='h264', **kwargs)
|
if not camera.info.output_codec:
|
||||||
|
camera.info.output_codec = 'libxvid'
|
||||||
|
super().__init__(camera, *args, output_format='h264', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class H265StreamWriter(FFmpegStreamWriter):
|
class H265StreamWriter(FFmpegStreamWriter):
|
||||||
|
|
|
@ -4,7 +4,7 @@ from abc import ABC
|
||||||
|
|
||||||
from platypush.plugins.camera.model.writer import VideoWriter
|
from platypush.plugins.camera.model.writer import VideoWriter
|
||||||
|
|
||||||
logger = logging.getLogger('cam-preview')
|
logger = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
|
||||||
class PreviewWriter(VideoWriter, ABC):
|
class PreviewWriter(VideoWriter, ABC):
|
||||||
|
|
|
@ -5,8 +5,6 @@ import threading
|
||||||
from platypush.plugins.camera.model.writer.image import MJPEGStreamWriter
|
from platypush.plugins.camera.model.writer.image import MJPEGStreamWriter
|
||||||
from platypush.plugins.camera.model.writer.preview import PreviewWriter
|
from platypush.plugins.camera.model.writer.preview import PreviewWriter
|
||||||
|
|
||||||
logger = logging.getLogger('cam-preview')
|
|
||||||
|
|
||||||
|
|
||||||
class FFplayPreviewWriter(PreviewWriter, MJPEGStreamWriter):
|
class FFplayPreviewWriter(PreviewWriter, MJPEGStreamWriter):
|
||||||
"""
|
"""
|
||||||
|
@ -14,6 +12,7 @@ class FFplayPreviewWriter(PreviewWriter, MJPEGStreamWriter):
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.ffplay = subprocess.Popen(['ffplay', '-'], stdin=subprocess.PIPE)
|
self.ffplay = subprocess.Popen(['ffplay', '-'], stdin=subprocess.PIPE)
|
||||||
self._preview_thread = threading.Thread(target=self._ffplay_thread)
|
self._preview_thread = threading.Thread(target=self._ffplay_thread)
|
||||||
self._preview_thread.start()
|
self._preview_thread.start()
|
||||||
|
@ -28,7 +27,7 @@ class FFplayPreviewWriter(PreviewWriter, MJPEGStreamWriter):
|
||||||
try:
|
try:
|
||||||
self.ffplay.stdin.write(self.frame)
|
self.ffplay.stdin.write(self.frame)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('ffplay write error: {}'.format(str(e)))
|
self.logger.warning('ffplay write error: {}'.format(str(e)))
|
||||||
self.close()
|
self.close()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import logging
|
|
||||||
from multiprocessing import Process, Queue, Event
|
from multiprocessing import Process, Queue, Event
|
||||||
|
|
||||||
from platypush.plugins.camera.model.writer import VideoWriter
|
from platypush.plugins.camera.model.writer import VideoWriter
|
||||||
from platypush.plugins.camera.model.writer.preview import PreviewWriter
|
from platypush.plugins.camera.model.writer.preview import PreviewWriter
|
||||||
|
|
||||||
logger = logging.getLogger('cam-preview')
|
|
||||||
|
|
||||||
|
|
||||||
class WxPreviewWriter(PreviewWriter, Process):
|
class WxPreviewWriter(PreviewWriter, Process):
|
||||||
"""
|
"""
|
||||||
|
@ -47,7 +44,7 @@ class WxPreviewWriter(PreviewWriter, Process):
|
||||||
try:
|
try:
|
||||||
self.bitmap_queue.put(image)
|
self.bitmap_queue.put(image)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('Could not add an image to the preview queue: {}'.format(str(e)))
|
self.logger.warning('Could not add an image to the preview queue: {}'.format(str(e)))
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
148
platypush/plugins/ffmpeg.py
Normal file
148
platypush/plugins/ffmpeg.py
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from typing import Callable, Optional, List, Tuple
|
||||||
|
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
class FfmpegPlugin(Plugin):
|
||||||
|
"""
|
||||||
|
Generic FFmpeg plugin to interact with media files and devices.
|
||||||
|
|
||||||
|
Requires:
|
||||||
|
|
||||||
|
* **ffmpeg-python** (``pip install ffmpeg-python``)
|
||||||
|
* The **ffmpeg** package installed on the system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ffmpeg_cmd: str = 'ffmpeg', ffprobe_cmd: str = 'ffprobe', **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.ffmpeg_cmd = ffmpeg_cmd
|
||||||
|
self.ffprobe_cmd = ffprobe_cmd
|
||||||
|
self._threads = {}
|
||||||
|
self._next_thread_id = 1
|
||||||
|
self._thread_lock = threading.RLock()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def info(self, filename: str, **kwargs) -> dict:
|
||||||
|
"""
|
||||||
|
Get the information of a media file.
|
||||||
|
|
||||||
|
:param filename: Path to the media file.
|
||||||
|
:return: Media file information. Example:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
||||||
|
"profile": "High 4:2:2",
|
||||||
|
"codec_type": "video",
|
||||||
|
"codec_time_base": "1/60",
|
||||||
|
"codec_tag_string": "[0][0][0][0]",
|
||||||
|
"codec_tag": "0x0000",
|
||||||
|
"width": 640,
|
||||||
|
"height": 480,
|
||||||
|
"coded_width": 640,
|
||||||
|
"coded_height": 480,
|
||||||
|
"closed_captions": 0,
|
||||||
|
"has_b_frames": 2,
|
||||||
|
"pix_fmt": "yuv422p",
|
||||||
|
"level": 30,
|
||||||
|
"chroma_location": "left",
|
||||||
|
"field_order": "progressive",
|
||||||
|
"refs": 1,
|
||||||
|
"is_avc": "true",
|
||||||
|
"nal_length_size": "4",
|
||||||
|
"r_frame_rate": "30/1",
|
||||||
|
"avg_frame_rate": "30/1",
|
||||||
|
"time_base": "1/1000",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"bits_per_raw_sample": "8",
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"ENCODER": "Lavc58.91.100 libx264"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"filename": "./output.mkv",
|
||||||
|
"nb_streams": 1,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"format_name": "matroska,webm",
|
||||||
|
"format_long_name": "Matroska / WebM",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"size": "786432",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"ENCODER": "Lavf58.45.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
# noinspection PyPackageRequirements
|
||||||
|
import ffmpeg
|
||||||
|
filename = os.path.abspath(os.path.expanduser(filename))
|
||||||
|
info = ffmpeg.probe(filename, cmd=self.ffprobe_cmd, **kwargs)
|
||||||
|
return info
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _poll_thread(proc: subprocess.Popen, packet_size: int, on_packet: Callable[[bytes], None],
|
||||||
|
on_open: Optional[Callable[[], None]] = None,
|
||||||
|
on_close: Optional[Callable[[], None]] = None):
|
||||||
|
try:
|
||||||
|
if on_open:
|
||||||
|
on_open()
|
||||||
|
|
||||||
|
while proc.poll() is None:
|
||||||
|
data = proc.stdout.read(packet_size)
|
||||||
|
on_packet(data)
|
||||||
|
finally:
|
||||||
|
if on_close:
|
||||||
|
on_close()
|
||||||
|
|
||||||
|
@action
|
||||||
|
def start(self, pipeline: List[dict], pipe_stdin: bool = False, pipe_stdout: bool = False,
|
||||||
|
pipe_stderr: bool = False, quiet: bool = False, overwrite_output: bool = False,
|
||||||
|
on_packet: Callable[[bytes], None] = None, packet_size: int = 4096):
|
||||||
|
# noinspection PyPackageRequirements
|
||||||
|
import ffmpeg
|
||||||
|
stream = ffmpeg
|
||||||
|
|
||||||
|
for step in pipeline:
|
||||||
|
args = step.pop('args') if 'args' in step else []
|
||||||
|
stream = getattr(stream, step.pop('method'))(*args, **step)
|
||||||
|
|
||||||
|
self.logger.info('Executing {cmd} {args}'.format(cmd=self.ffmpeg_cmd, args=stream.get_args()))
|
||||||
|
proc = stream.run_async(cmd=self.ffmpeg_cmd, pipe_stdin=pipe_stdin, pipe_stdout=pipe_stdout,
|
||||||
|
pipe_stderr=pipe_stderr, quiet=quiet, overwrite_output=overwrite_output)
|
||||||
|
|
||||||
|
if on_packet:
|
||||||
|
with self._thread_lock:
|
||||||
|
self._threads[self._next_thread_id] = threading.Thread(target=self._poll_thread, kwargs=dict(
|
||||||
|
proc=proc, on_packet=on_packet, packet_size=packet_size))
|
||||||
|
self._threads[self._next_thread_id].start()
|
||||||
|
self._next_thread_id += 1
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
Loading…
Reference in a new issue