File Coverage

consoles/video_stream.pm
Criterion Covered Total %
statement 177 216 81.9
total 177 216 81.9


line stmt code
1   # Copyright 2021 Marek Marczykowski-Górecki
2   # Copyright 2021 SUSE LLC
3   # SPDX-License-Identifier: GPL-2.0-or-later
4    
5    
6   use Mojo::Base 'consoles::video_base', -signatures;
7 2  
  2  
  2  
8   use List::Util 'max';
9 2 use Time::HiRes qw(usleep);
  2  
  2  
10 2 use Fcntl;
  2  
  2  
11 2  
  2  
  2  
12   use Try::Tiny;
13 2 use bmwqemu;
  2  
  2  
14 2  
  2  
  2  
15   # speed limit: 30 keys per second
16   use constant STREAM_TYPING_LIMIT_DEFAULT => 30;
17 2  
  2  
  2  
18   use constant DV_TIMINGS_CHECK_INTERVAL => 3;
19 2  
  2  
  2  
20   use constant STALL_THRESHOLD => 4;
21 2  
  2  
  2  
22   return $self;
23 1 }
  1  
  1  
24 1  
25   my $ret = 0;
26   if ($self->{ffmpeg}) {
27 7 kill(TERM => $self->{ffmpegpid});
  7  
  7  
28 7 close($self->{ffmpeg});
29 7 $self->{ffmpeg} = undef;
30 6 $ret = waitpid($self->{ffmpegpid}, 0);
31 6 $self->{ffmpegpid} = undef;
32 6 }
33 6 return $ret;
34 6 }
35    
36 7 my $ret = $self->disable_video;
37   if ($self->{input_pipe}) {
38   close($self->{input_pipe});
39 3 waitpid($self->{inputpid}, 0);
  3  
  3  
40 3 }
41 3 return $ret;
42 2 }
43 2  
44   my @cmd = ("v4l2-ctl", "--device", $device, "--concise");
45 3 push(@cmd, split(/ /, $cmd));
46   my $pipe;
47   my $pid = open($pipe, '-|', @cmd) or return undef;
48 0 $pipe->read(my $str, 50);
  0  
  0  
  0  
49 0 my $ret = waitpid($pid, 0);
50 0 if ($ret > 0 && $? == 0) {
51 0 # remove header and whitespaces
52 0 $str =~ s/DV timings://;
53 0 $str =~ s/^\s+|\s+$//g;
54 0 return $str;
55 0 }
56   return undef;
57 0 }
58 0  
59 0 $self->{_last_update_received} = 0;
60    
61 0 if ($args->{url} =~ m/^\/dev\/video/) {
62   if ($args->{edid}) {
63   my $ret = _v4l2_ctl($args->{url}, "--set-edid $args->{edid}");
64 10 die "Failed to set EDID" unless defined $ret;
  10  
  10  
  10  
65 10 }
66    
67 10 my $timings = _v4l2_ctl($args->{url}, '--get-dv-timings');
68 5 if ($timings) {
69 1 if ($timings ne "0x0pnan") {
70 1 $self->{dv_timings} = $timings;
71   } else {
72   $self->{dv_timings} = '';
73 5 }
74 5 $self->{dv_timings_supported} = 1;
75 4 $self->{dv_timings_last_check} = time;
76 3 bmwqemu::diag "Current DV timings: $timings";
77   } else {
78 1 $self->{dv_timings_supported} = 0;
79   bmwqemu::diag "DV timings not supported";
80 4 }
81 4 } else {
82 4 # applies to v4l only
83   $self->{dv_timings_supported} = 0;
84 1 }
85 1  
86   bmwqemu::diag "Starting to receive video stream at $args->{url}";
87   $self->connect_remote_video($args->{url});
88    
89 5 $self->connect_remote_input($args->{input_cmd}) if $args->{input_cmd};
90   }
91    
92 10 my @cmd = ('ffmpeg', '-loglevel', 'fatal', '-i', $url);
93 10 push(@cmd, ('-vcodec', 'ppm', '-f', 'rawvideo', '-r', '2', '-'));
94   return \@cmd;
95 10 }
96    
97   if ($self->{dv_timings_supported}) {
98 0 if (!_v4l2_ctl($url, '--set-dv-bt-timings query')) {
  0  
  0  
  0  
99 0 bmwqemu::diag("No video signal");
100 0 $self->{dv_timings} = '';
101 0 return;
102   }
103   $self->{dv_timings} = _v4l2_ctl($url, '--get-dv-timings');
104 11 }
  11  
  11  
  11  
105 11  
106 5 my $cmd = $self->_get_ffmpeg_cmd($url);
107 1 my $ffmpeg;
108 1 $self->{ffmpegpid} = open($ffmpeg, '-|', @$cmd)
109 1 or die "Failed to start ffmpeg for video stream at $url";
110   # make the pipe size large enough to hold full frame and a bit
111 4 fcntl($ffmpeg, Fcntl::F_SETPIPE_SZ, 4194304);
112   $self->{ffmpeg} = $ffmpeg;
113   $ffmpeg->blocking(0);
114 10  
115 10 $self->{_last_update_received} = time;
116 10  
117   return 1;
118   }
119 10  
120 10 $self->{mouse} = {x => -1, y => -1};
121 10  
122   bmwqemu::diag "Connecting input device";
123 10  
124   my $input_pipe;
125 10 $self->{inputpid} = open($input_pipe, '|' . $cmd)
126   or die "Failed to start input_cmd($cmd)";
127   $self->{input_pipe} = $input_pipe;
128 2 $self->{input_pipe}->autoflush(1);
  2  
  2  
  2  
129 2  
130   return $input_pipe;
131 2 }
132    
133 2  
134 2 my $ffmpeg = $self->{ffmpeg};
135   $ffmpeg or die 'ffmpeg is not running. Probably your backend instance could not start or died.';
136 2 $ffmpeg->blocking(0);
137 2 my $ret = $ffmpeg->read(my $header, 20);
138   $ffmpeg->blocking(1);
139 2  
140   return undef unless $ret;
141    
142   die "ffmpeg closed: $ret\n${\Dumper $self}" unless $ret > 0;
143 8  
  8  
  8  
144 8 # support P6 only
145 8 if (!($header =~ m/^(P6\n(\d+) (\d+)\n(\d+)\n)/)) {
146 8 die "Invalid PPM header: $header";
147 8 }
148 8 my $header_len = length($1);
149   my $width = $2;
150 8 my $height = $3;
151   my $bytes_per_pixel = ($4 < 256) ? 1 : 2;
152 4 my $frame_len = $width * $height * 3 * $bytes_per_pixel;
  0  
153   my $remaining_len = $header_len + $frame_len - $ret;
154   $ret = $ffmpeg->read(my $frame_data, $remaining_len);
155 4 die "Incomplete frame (got $ret instead of $remaining_len)" if $ret != $remaining_len;
156 0 my $img = tinycv::from_ppm($header . $frame_data);
157   $self->{_framebuffer} = $img;
158 4 $self->{width} = $width;
159 4 $self->{height} = $height;
160 4 $self->{_last_update_received} = time;
161 4 return $img;
162 4 }
163 4  
164 4 if ($self->{dv_timings_supported}) {
165 4 # periodically check if DV timings needs update due to resolution change
166 4 if (time - $self->{dv_timings_last_check} >= DV_TIMINGS_CHECK_INTERVAL) {
167 4 my $current_timings = _v4l2_ctl($self->{args}->{url}, '--query-dv-timings');
168 4 if ($current_timings && $current_timings ne $self->{dv_timings}) {
169 4 bmwqemu::diag "Updating DV timings, new: $current_timings";
170 4 # yes, there is need to update DV timings, restart ffmpeg,
171 4 # connect_remote_video will update the timings
172   $self->disable_video;
173   $self->connect_remote_video($self->{args}->{url});
174 4 } elsif ($self->{dv_timings} && !$current_timings) {
  4  
  4  
175 4 bmwqemu::diag "video disconnected";
176   $self->disable_video;
177 2 $self->{dv_timings} = '';
178 2 }
179 2 $self->{dv_timings_last_check} = time;
180 1 }
181   }
182    
183 1 # no video connected, don't read anything
184 1 return 0 unless $self->{ffmpeg};
185    
186 0 my $have_recieved_update = 0;
187 0 while ($self->_receive_frame()) {
188 0 $have_recieved_update = 1;
189   }
190 2 return $have_recieved_update;
191   }
192    
193   $self->update_framebuffer();
194   return unless $self->{_framebuffer};
195 4 return $self->{_framebuffer};
196   }
197 4  
198 4 if (!$self->update_framebuffer()) {
199 4 # check if it isn't stalled, perhaps we missed resolution change?
200   my $time_since_last_update = time - $self->{_last_update_received};
201 4 if ($self->{ffmpeg} && $time_since_last_update > STALL_THRESHOLD) {
202   # reconnect, it will refresh the device settings too
203   $self->disable_video;
204 3 $self->connect_remote_video($self->{args}->{url});
  3  
  3  
205 3 }
206 3 }
207 2 }
208    
209   return unless $self->{input_pipe};
210 0 $self->{input_pipe}->write($key . "\n")
  0  
  0  
211 0 or die "failed to send '$key' input event";
212   }
213 0  
214 0 return $self->{mouse};
215   }
216 0  
217 0 return unless $self->{input_pipe};
218   $self->{input_pipe}->write("mouse_move $x $y\n");
219   $self->{input_pipe}->flush;
220   # let the event be processed before further commands
221   $self->backend->run_capture_loop(.1);
222 12 }
  12  
  12  
  12  
  12  
223 12  
224 12 return unless $self->{input_pipe};
225   my $button = $args->{button};
226   my $bstate = $args->{bstate};
227   # careful: the bits order is different than in VNC
228 0 my $mask = {left => $bstate, right => $bstate << 1, middle => $bstate << 2}->{$button} // 0;
  0  
  0  
229 0 bmwqemu::diag "pointer_event $mask $self->{mouse}->{x}, $self->{mouse}->{y}";
230   $self->{input_pipe}->write("mouse_button $mask\n");
231   $self->{input_pipe}->flush;
232 4 return {};
  4  
  4  
  4  
  4  
233 4 }
234 4  
235 4 1;