kopia lustrzana https://github.com/corrscope/corrscope
Merge pull request #52 from nyanpasu64/terminate-ffmpeg
Properly terminate FFmpeg output processespull/357/head
commit
dd1f2b7b1d
|
@ -49,7 +49,7 @@ def register_output(config_t: Type[IOutputConfig]):
|
||||||
|
|
||||||
# FFmpeg command line generation
|
# FFmpeg command line generation
|
||||||
|
|
||||||
class _FFmpegCommand:
|
class _FFmpegProcess:
|
||||||
def __init__(self, templates: List[str], ovgen_cfg: 'Config'):
|
def __init__(self, templates: List[str], ovgen_cfg: 'Config'):
|
||||||
self.templates = templates
|
self.templates = templates
|
||||||
self.ovgen_cfg = ovgen_cfg
|
self.ovgen_cfg = ovgen_cfg
|
||||||
|
@ -122,9 +122,19 @@ class PipeOutput(Output):
|
||||||
return retval # final value
|
return retval # final value
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
if exc_type is not None:
|
if exc_type is not None:
|
||||||
|
e = None
|
||||||
for popen in self._pipeline:
|
for popen in self._pipeline:
|
||||||
popen.terminate()
|
popen.terminate()
|
||||||
|
# https://stackoverflow.com/a/49038779/2683842
|
||||||
|
try:
|
||||||
|
popen.wait(1) # timeout=seconds
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
popen.kill()
|
||||||
|
|
||||||
|
if e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
# FFmpegOutput
|
# FFmpegOutput
|
||||||
|
@ -135,8 +145,7 @@ class FFmpegOutputConfig(IOutputConfig):
|
||||||
path: Optional[str]
|
path: Optional[str]
|
||||||
args: str = ''
|
args: str = ''
|
||||||
|
|
||||||
# Do not use `-movflags faststart`, I get corrupted mp4 files (missing MOOV)
|
video_template: str = '-c:v libx264 -crf 18 -preset superfast -movflags faststart'
|
||||||
video_template: str = '-c:v libx264 -crf 18 -preset superfast'
|
|
||||||
audio_template: str = '-c:a aac -b:a 384k'
|
audio_template: str = '-c:a aac -b:a 384k'
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,7 +156,7 @@ class FFmpegOutput(PipeOutput):
|
||||||
def __init__(self, ovgen_cfg: 'Config', cfg: FFmpegOutputConfig):
|
def __init__(self, ovgen_cfg: 'Config', cfg: FFmpegOutputConfig):
|
||||||
super().__init__(ovgen_cfg, cfg)
|
super().__init__(ovgen_cfg, cfg)
|
||||||
|
|
||||||
ffmpeg = _FFmpegCommand([FFMPEG, '-y'], ovgen_cfg)
|
ffmpeg = _FFmpegProcess([FFMPEG, '-y'], ovgen_cfg)
|
||||||
ffmpeg.add_output(cfg)
|
ffmpeg.add_output(cfg)
|
||||||
ffmpeg.templates.append(cfg.args)
|
ffmpeg.templates.append(cfg.args)
|
||||||
|
|
||||||
|
@ -174,7 +183,7 @@ class FFplayOutput(PipeOutput):
|
||||||
def __init__(self, ovgen_cfg: 'Config', cfg: FFplayOutputConfig):
|
def __init__(self, ovgen_cfg: 'Config', cfg: FFplayOutputConfig):
|
||||||
super().__init__(ovgen_cfg, cfg)
|
super().__init__(ovgen_cfg, cfg)
|
||||||
|
|
||||||
ffmpeg = _FFmpegCommand([FFMPEG], ovgen_cfg)
|
ffmpeg = _FFmpegProcess([FFMPEG], ovgen_cfg)
|
||||||
ffmpeg.add_output(cfg)
|
ffmpeg.add_output(cfg)
|
||||||
ffmpeg.templates.append('-f nut')
|
ffmpeg.templates.append('-f nut')
|
||||||
|
|
||||||
|
@ -184,6 +193,8 @@ class FFplayOutput(PipeOutput):
|
||||||
p2 = subprocess.Popen(ffplay, stdin=p1.stdout)
|
p2 = subprocess.Popen(ffplay, stdin=p1.stdout)
|
||||||
|
|
||||||
p1.stdout.close()
|
p1.stdout.close()
|
||||||
|
# assert p2.stdin is None # True unless Popen is being mocked (test_output).
|
||||||
|
|
||||||
self.open(p1, p2)
|
self.open(p1, p2)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -232,8 +232,13 @@ class Ovgen:
|
||||||
for output in self.outputs:
|
for output in self.outputs:
|
||||||
output.write_frame(frame)
|
output.write_frame(frame)
|
||||||
|
|
||||||
|
if self.raise_on_teardown:
|
||||||
|
raise self.raise_on_teardown
|
||||||
|
|
||||||
if PRINT_TIMESTAMP:
|
if PRINT_TIMESTAMP:
|
||||||
# noinspection PyUnboundLocalVariable
|
# noinspection PyUnboundLocalVariable
|
||||||
dtime = time.perf_counter() - begin
|
dtime = time.perf_counter() - begin
|
||||||
render_fps = (end_frame - begin_frame) / dtime
|
render_fps = (end_frame - begin_frame) / dtime
|
||||||
print(f'FPS = {render_fps}')
|
print(f'FPS = {render_fps}')
|
||||||
|
|
||||||
|
raise_on_teardown: Exception = None
|
||||||
|
|
|
@ -10,11 +10,18 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def Popen(mocker: 'pytest_mock.MockFixture'):
|
def Popen(mocker: 'pytest_mock.MockFixture'):
|
||||||
|
real_Popen = subprocess.Popen
|
||||||
|
|
||||||
|
def popen_factory(*args, **kwargs):
|
||||||
|
popen = mocker.create_autospec(real_Popen)
|
||||||
|
|
||||||
|
popen.stdin = mocker.mock_open()(os.devnull, "wb")
|
||||||
|
popen.stdout = mocker.mock_open()(os.devnull, "rb")
|
||||||
|
assert popen.stdin != popen.stdout
|
||||||
|
|
||||||
|
popen.wait.return_value = 0
|
||||||
|
return popen
|
||||||
|
|
||||||
Popen = mocker.patch.object(subprocess, 'Popen', autospec=True)
|
Popen = mocker.patch.object(subprocess, 'Popen', autospec=True)
|
||||||
popen = Popen.return_value
|
Popen.side_effect = popen_factory
|
||||||
|
|
||||||
popen.stdin = open(os.devnull, "wb")
|
|
||||||
popen.stdout = open(os.devnull, "rb")
|
|
||||||
popen.wait.return_value = 0
|
|
||||||
|
|
||||||
yield Popen
|
yield Popen
|
||||||
|
|
|
@ -39,9 +39,24 @@ def test_output():
|
||||||
assert not Path('-').exists()
|
assert not Path('-').exists()
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure ovgen closes pipe to output upon completion.
|
||||||
|
@pytest.mark.usefixtures('Popen')
|
||||||
|
def test_close_output(Popen):
|
||||||
|
""" FFplayOutput unit test: Ensure ffmpeg and ffplay are terminated when Python
|
||||||
|
exceptions occur.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ffplay_cfg = FFplayOutputConfig()
|
||||||
|
output: FFplayOutput
|
||||||
|
with ffplay_cfg(CFG) as output:
|
||||||
|
pass
|
||||||
|
|
||||||
|
output._pipeline[0].stdin.close.assert_called()
|
||||||
|
for popen in output._pipeline:
|
||||||
|
popen.wait.assert_called() # Does wait() need to be called?
|
||||||
|
|
||||||
|
|
||||||
# Ensure ovgen terminates FFplay upon exceptions.
|
# Ensure ovgen terminates FFplay upon exceptions.
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('Popen')
|
@pytest.mark.usefixtures('Popen')
|
||||||
def test_terminate_ffplay(Popen):
|
def test_terminate_ffplay(Popen):
|
||||||
""" FFplayOutput unit test: Ensure ffmpeg and ffplay are terminated when Python
|
""" FFplayOutput unit test: Ensure ffmpeg and ffplay are terminated when Python
|
||||||
|
@ -61,8 +76,8 @@ def test_terminate_ffplay(Popen):
|
||||||
|
|
||||||
@pytest.mark.usefixtures('Popen')
|
@pytest.mark.usefixtures('Popen')
|
||||||
def test_ovgen_terminate_ffplay(Popen, mocker: 'pytest_mock.MockFixture'):
|
def test_ovgen_terminate_ffplay(Popen, mocker: 'pytest_mock.MockFixture'):
|
||||||
""" Integration test: Ensure ffmpeg and ffplay are terminated when Python exceptions
|
""" Integration test: Ensure ovgenpy calls terminate() on ffmpeg and ffplay when
|
||||||
occur. """
|
Python exceptions occur. """
|
||||||
|
|
||||||
cfg = default_config(
|
cfg = default_config(
|
||||||
channels=[ChannelConfig('tests/sine440.wav')],
|
channels=[ChannelConfig('tests/sine440.wav')],
|
||||||
|
@ -83,6 +98,25 @@ def test_ovgen_terminate_ffplay(Popen, mocker: 'pytest_mock.MockFixture'):
|
||||||
popen.terminate.assert_called()
|
popen.terminate.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip('Launches ffmpeg and ffplay processes, creating a ffplay window')
|
||||||
|
def test_ovgen_terminate_works():
|
||||||
|
""" Ensure that ffmpeg/ffplay terminate quickly after Python exceptions, when
|
||||||
|
`popen.terminate()` is called. """
|
||||||
|
|
||||||
|
cfg = default_config(
|
||||||
|
channels=[ChannelConfig('tests/sine440.wav')],
|
||||||
|
master_audio='tests/sine440.wav',
|
||||||
|
outputs=[FFplayOutputConfig()],
|
||||||
|
end_time=0.5, # Reduce test duration
|
||||||
|
)
|
||||||
|
ovgen = Ovgen(cfg, '.')
|
||||||
|
ovgen.raise_on_teardown = DummyException
|
||||||
|
|
||||||
|
with pytest.raises(DummyException):
|
||||||
|
# Raises `subprocess.TimeoutExpired` if popen.terminate() doesn't work.
|
||||||
|
ovgen.play()
|
||||||
|
|
||||||
|
|
||||||
# TODO integration test without audio
|
# TODO integration test without audio
|
||||||
|
|
||||||
# TODO integration test on ???
|
# TODO integration test on ???
|
||||||
|
|
Ładowanie…
Reference in New Issue