Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close video encoder pipes and wait for it to finalize the video before exiting #1503

Merged
merged 2 commits into from Aug 13, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 69 additions & 13 deletions backend/baseclass.pm
Expand Up @@ -62,6 +62,7 @@ sub new {
$self->{serial_offset} = 0;
$self->{video_frame_data} = [];
$self->{video_frame_number} = 0;
$self->{video_encoders} = {};
$self->{external_video_encoder_image_data} = [];
$self->{min_image_similarity} = 10000;
$self->{min_video_similarity} = 10000;
Expand Down Expand Up @@ -325,6 +326,15 @@ sub check_select_rate {
return 0;
}

sub _invoke_video_encoder {
my ($self, $pipe_name, $display_name, @cmd) = @_;

my $pid = open($self->{$pipe_name}, '|-', @cmd);
my $pipe = $self->{$pipe_name};
$self->{video_encoders}->{$pid} = {name => $display_name, pipe => $pipe};
$pipe->blocking(0);
}

sub _start_external_video_encoder_if_configured {
my ($self) = @_;

Expand All @@ -336,8 +346,7 @@ sub _start_external_video_encoder_if_configured {
$cmd .= " '$output_file_path'" unless $cmd =~ s/%OUTPUT_FILE_NAME%/$output_file_path/;

bmwqemu::diag "Launching external video encoder: $cmd";
open($self->{external_video_encoder_cmd_pipe}, '|-', $cmd);
$self->{external_video_encoder_cmd_pipe}->blocking(0);
$self->_invoke_video_encoder(external_video_encoder_cmd_pipe => 'external video encoder', $cmd);
return 1;
}

Expand All @@ -351,8 +360,7 @@ sub start_encoder {
my $cwd = Cwd::getcwd;
my @cmd = (qw(nice -n 19), "$bmwqemu::scriptdir/videoencoder", "$cwd/video.ogv");
push(@cmd, '-n') if $bmwqemu::vars{NOVIDEO} || ($has_external_video_encoder_configured && !$bmwqemu::vars{EXTERNAL_VIDEO_ENCODER_ADDITIONALLY});
open($self->{encoder_pipe}, '|-', @cmd);
$self->{encoder_pipe}->blocking(0);
$self->_invoke_video_encoder(encoder_pipe => 'built-in video encoder', @cmd);

# open file for recording real time clock timestamps as subtitle
open($self->{vtt_caption_file}, '>', "$cwd/video_time.vtt");
Expand All @@ -361,6 +369,61 @@ sub start_encoder {
return;
}

sub _stop_video_encoder {
my ($self) = @_;

my $video_encoders = delete $self->{video_encoders};
return undef unless defined $video_encoders && keys %$video_encoders;

# pass remaining video frames to the video encoder
bmwqemu::diag 'Passing remaining frames to the video encoder';
my $timeout = 30;
my $video_data_for_internal_encoder = $self->{video_frame_data};
my $video_data_for_external_encoder = $self->{external_video_encoder_image_data};
my $select = IO::Select->new;
$select->add(my $internal_pipe = $self->{encoder_pipe}) if @$video_data_for_internal_encoder;
$select->add(my $external_pipe = $self->{external_video_encoder_cmd_pipe}) if @$video_data_for_external_encoder;
try {
while ($select->count) {
$! = 'timeout exceeded'; # can_write returns an empty array and leaves $! unchanged in case of a timeout
die "$!\n" unless my @ready = $select->can_write($timeout);
for my $fh (@ready) {
if ($fh == $internal_pipe) {
$self->_write_buffered_data_to_file_handle('Encoder', $video_data_for_internal_encoder, $fh);
$select->remove($fh) unless @$video_data_for_internal_encoder;
}
elsif ($fh == $external_pipe) {
$self->_write_buffered_data_to_file_handle('External encoder', $video_data_for_external_encoder, $fh);
$select->remove($fh) unless @$video_data_for_external_encoder;
}
}
}
}
catch {
bmwqemu::diag "Unable to pass remaining frames to video encoder: $_";
};

# give the video encoder processes time to finalize the video
# note: Closing the pipe should cause the video encoder to terminate. Not sending SIGTERM/SIGINT because the signal might be
# already sent by the worker or shell and ffmpeg will not continue finalizing the video after receiving a 2nd exit signal.
no autodie qw(close waitpid);
close $video_encoders->{$_}->{pipe} for keys %$video_encoders;
bmwqemu::diag 'Waiting for video encoder to finalize the video';
for (my $interval = 0.25; $timeout > 0; sleep($interval), $timeout -= $interval) {
for my $pid (keys %$video_encoders) {
my $ret = waitpid($pid, WNOHANG);
if ($ret == $pid || $ret == -1) {
bmwqemu::diag "The $video_encoders->{$pid}->{name} (pid $pid) terminated";
delete $video_encoders->{$pid};
}
}
last unless keys %$video_encoders;
}
return undef unless keys %$video_encoders;
bmwqemu::diag "Unable to terminate $video_encoders->{$_}->{name}, sending SIGKILL" for keys %$video_encoders;
kill KILL => (keys %$video_encoders);
}

# new api

sub start_vm {
Expand All @@ -377,16 +440,9 @@ sub stop_vm {
no autodie 'unlink';
unlink('backend.run');
$self->do_stop_vm();

bmwqemu::diag('flushing frames');
$self->{encoder_pipe}->blocking(1);
for my $fdata (@{$self->{video_frame_data}}) {
$self->{encoder_pipe}->print($fdata);
}
$self->{encoder_pipe}->close if $self->{encoder_pipe};
$self->{encoder_pipe} = undef;
$self->{started} = 0;
$self->{started} = 0;
}
$self->_stop_video_encoder();
$self->close_ssh_connections();
$self->close_pipes(); # does not return
return;
Expand Down