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; |