2018-07-22 09:06:10 +00:00
|
|
|
# https://ffmpeg.org/ffplay.html
|
|
|
|
import shlex
|
|
|
|
import subprocess
|
|
|
|
from abc import ABC, abstractmethod
|
2018-07-22 11:56:10 +00:00
|
|
|
from typing import TYPE_CHECKING, Type, List, Union
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-25 12:01:58 +00:00
|
|
|
from ovgenpy.config import register_config
|
2018-07-22 09:06:10 +00:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2018-07-22 10:17:31 +00:00
|
|
|
import numpy as np
|
2018-07-22 09:06:10 +00:00
|
|
|
from ovgenpy.ovgenpy import Config
|
|
|
|
|
|
|
|
|
2018-07-22 10:17:31 +00:00
|
|
|
RGB_DEPTH = 3
|
2018-07-22 09:06:10 +00:00
|
|
|
|
|
|
|
|
2018-07-24 11:28:53 +00:00
|
|
|
class IOutputConfig:
|
2018-07-22 09:06:10 +00:00
|
|
|
cls: 'Type[Output]'
|
|
|
|
|
|
|
|
def __call__(self, ovgen_cfg: 'Config'):
|
|
|
|
return self.cls(ovgen_cfg, cfg=self)
|
|
|
|
|
|
|
|
|
|
|
|
class Output(ABC):
|
2018-07-24 11:28:53 +00:00
|
|
|
def __init__(self, ovgen_cfg: 'Config', cfg: IOutputConfig):
|
2018-07-22 09:06:10 +00:00
|
|
|
self.ovgen_cfg = ovgen_cfg
|
|
|
|
self.cfg = cfg
|
|
|
|
|
|
|
|
@abstractmethod
|
2018-07-22 10:17:31 +00:00
|
|
|
def write_frame(self, frame: 'np.ndarray') -> None:
|
|
|
|
""" Output a Numpy ndarray. """
|
2018-07-22 09:06:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Glue logic
|
|
|
|
|
2018-07-24 11:28:53 +00:00
|
|
|
def register_output(config_t: Type[IOutputConfig]):
|
2018-07-22 09:06:10 +00:00
|
|
|
def inner(output_t: Type[Output]):
|
|
|
|
config_t.cls = output_t
|
|
|
|
return output_t
|
|
|
|
|
|
|
|
return inner
|
|
|
|
|
|
|
|
|
2018-07-24 03:25:53 +00:00
|
|
|
# FFmpeg input format
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
class _FFmpegCommand:
|
|
|
|
def __init__(self, templates: List[str], ovgen_cfg: 'Config'):
|
|
|
|
self.templates = templates
|
|
|
|
self.ovgen_cfg = ovgen_cfg
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
self.templates += ffmpeg_input_video(ovgen_cfg) # video
|
2018-07-25 12:25:53 +00:00
|
|
|
if self.ovgen_cfg.master_audio:
|
|
|
|
self.templates += ffmpeg_input_audio(audio_path=ovgen_cfg.master_audio) # audio
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 11:56:10 +00:00
|
|
|
def add_output(self, cfg: 'Union[FFmpegOutputConfig, FFplayOutputConfig]') -> None:
|
2018-07-22 10:51:38 +00:00
|
|
|
self.templates.append(cfg.video_template) # video
|
2018-07-25 12:25:53 +00:00
|
|
|
if self.ovgen_cfg.master_audio:
|
2018-07-22 10:51:38 +00:00
|
|
|
self.templates.append(cfg.audio_template) # audio
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 11:56:10 +00:00
|
|
|
def popen(self, process_args=None, **kwargs) -> subprocess.Popen:
|
|
|
|
if process_args is None:
|
|
|
|
process_args = []
|
|
|
|
|
2018-07-24 03:25:53 +00:00
|
|
|
return subprocess.Popen(self._generate_args() + process_args,
|
|
|
|
stdin=subprocess.PIPE, **kwargs)
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
def _generate_args(self) -> List[str]:
|
|
|
|
return [arg
|
|
|
|
for template in self.templates
|
|
|
|
for arg in shlex.split(template)]
|
2018-07-22 09:06:10 +00:00
|
|
|
|
|
|
|
|
2018-07-24 03:25:53 +00:00
|
|
|
assert RGB_DEPTH == 3
|
|
|
|
def ffmpeg_input_video(cfg: 'Config') -> List[str]:
|
|
|
|
fps = cfg.fps
|
|
|
|
width = cfg.render.width
|
|
|
|
height = cfg.render.height
|
|
|
|
|
|
|
|
return [f'-f rawvideo -pixel_format rgb24 -video_size {width}x{height}',
|
|
|
|
f'-framerate {fps}',
|
|
|
|
'-i -']
|
|
|
|
|
|
|
|
|
|
|
|
def ffmpeg_input_audio(audio_path: str) -> List[str]:
|
|
|
|
return ['-i', audio_path]
|
|
|
|
|
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
class ProcessOutput(Output):
|
|
|
|
def open(self, popen: subprocess.Popen):
|
|
|
|
self._popen = popen
|
2018-07-22 09:06:10 +00:00
|
|
|
self._stream = self._popen.stdin
|
|
|
|
# Python documentation discourages accessing popen.stdin. It's wrong.
|
|
|
|
# https://stackoverflow.com/a/9886747
|
|
|
|
|
2018-07-22 10:17:31 +00:00
|
|
|
def write_frame(self, frame: bytes) -> None:
|
2018-07-22 09:06:10 +00:00
|
|
|
self._stream.write(frame)
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self._stream.close()
|
|
|
|
self._popen.wait()
|
|
|
|
|
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
# FFmpegOutput
|
2018-07-25 12:01:58 +00:00
|
|
|
@register_config
|
2018-07-24 11:28:53 +00:00
|
|
|
class FFmpegOutputConfig(IOutputConfig):
|
2018-07-22 10:51:38 +00:00
|
|
|
path: str
|
2018-07-24 03:25:53 +00:00
|
|
|
video_template: str = '-c:v libx264 -crf 18 -bf 2 -flags +cgop -pix_fmt yuv420p -movflags faststart'
|
|
|
|
audio_template: str = '-c:a aac -b:a 384k'
|
2018-07-22 10:51:38 +00:00
|
|
|
|
|
|
|
|
2018-07-24 03:25:53 +00:00
|
|
|
FFMPEG = 'ffmpeg'
|
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
@register_output(FFmpegOutputConfig)
|
|
|
|
class FFmpegOutput(ProcessOutput):
|
|
|
|
def __init__(self, ovgen_cfg: 'Config', cfg: FFmpegOutputConfig):
|
|
|
|
super().__init__(ovgen_cfg, cfg)
|
|
|
|
|
|
|
|
ffmpeg = _FFmpegCommand([FFMPEG, '-y'], ovgen_cfg)
|
|
|
|
ffmpeg.add_output(cfg)
|
2018-07-22 11:56:10 +00:00
|
|
|
self.open(ffmpeg.popen([cfg.path]))
|
2018-07-22 10:51:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
# FFplayOutput
|
2018-07-25 12:01:58 +00:00
|
|
|
@register_config
|
2018-07-24 11:28:53 +00:00
|
|
|
class FFplayOutputConfig(IOutputConfig):
|
2018-07-22 11:56:10 +00:00
|
|
|
video_template: str = '-c:v copy'
|
|
|
|
audio_template: str = '-c:a copy'
|
2018-07-22 09:06:10 +00:00
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
|
2018-07-24 03:25:53 +00:00
|
|
|
FFPLAY = 'ffplay'
|
|
|
|
|
2018-07-22 09:06:10 +00:00
|
|
|
@register_output(FFplayOutputConfig)
|
2018-07-22 10:51:38 +00:00
|
|
|
class FFplayOutput(ProcessOutput):
|
|
|
|
def __init__(self, ovgen_cfg: 'Config', cfg: FFplayOutputConfig):
|
|
|
|
super().__init__(ovgen_cfg, cfg)
|
|
|
|
|
2018-07-22 11:56:10 +00:00
|
|
|
ffmpeg = _FFmpegCommand([FFMPEG], ovgen_cfg)
|
|
|
|
ffmpeg.add_output(cfg)
|
|
|
|
ffmpeg.templates.append('-f nut -')
|
|
|
|
|
|
|
|
p1 = ffmpeg.popen(stdout=subprocess.PIPE)
|
2018-07-23 09:43:05 +00:00
|
|
|
|
|
|
|
ffplay = shlex.split('ffplay -autoexit -')
|
|
|
|
self.p2 = subprocess.Popen(ffplay, stdin=p1.stdout)
|
|
|
|
|
2018-07-22 11:56:10 +00:00
|
|
|
p1.stdout.close()
|
|
|
|
self.open(p1)
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
ProcessOutput.close(self)
|
|
|
|
self.p2.wait()
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: MPVOutput?
|
2018-07-22 09:06:10 +00:00
|
|
|
|
|
|
|
|
2018-07-22 10:51:38 +00:00
|
|
|
# ImageOutput
|
2018-07-25 12:01:58 +00:00
|
|
|
@register_config
|
2018-07-24 11:28:53 +00:00
|
|
|
class ImageOutputConfig(IOutputConfig):
|
2018-07-22 09:06:10 +00:00
|
|
|
path_prefix: str
|
|
|
|
|
|
|
|
|
|
|
|
@register_output(ImageOutputConfig)
|
|
|
|
class ImageOutput(Output):
|
|
|
|
pass
|