| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Tvh::Htsp::Client; |
|
2
|
|
|
|
|
|
|
# Tvheadend HTSP client library written in perl |
|
3
|
|
|
|
|
|
|
# https://docs.tvheadend.org/documentation/development/htsp |
|
4
|
1
|
|
|
1
|
|
81810
|
use strict; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
29
|
|
|
5
|
1
|
|
|
1
|
|
8
|
use warnings; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
35
|
|
|
6
|
1
|
|
|
1
|
|
20
|
use v5.28; |
|
|
1
|
|
|
|
|
2
|
|
|
7
|
1
|
|
|
1
|
|
418
|
use namespace::autoclean; |
|
|
1
|
|
|
|
|
15763
|
|
|
|
1
|
|
|
|
|
5
|
|
|
8
|
1
|
|
|
1
|
|
564
|
use IO::Socket qw(AF_INET SOCK_STREAM); |
|
|
1
|
|
|
|
|
19587
|
|
|
|
1
|
|
|
|
|
3
|
|
|
9
|
1
|
|
|
1
|
|
789
|
use LooksLike; |
|
|
1
|
|
|
|
|
3374
|
|
|
|
1
|
|
|
|
|
43
|
|
|
10
|
1
|
|
|
1
|
|
7
|
use List::Util qw(min max); |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
137
|
|
|
11
|
|
|
|
|
|
|
our $VERSION = '0.06'; # set the version for version checking |
|
12
|
|
|
|
|
|
|
sub new { |
|
13
|
1
|
|
|
1
|
1
|
194245
|
my ($class, $args) = @_; |
|
14
|
|
|
|
|
|
|
my $self = { |
|
15
|
|
|
|
|
|
|
host => $args->{host} // 'localhost', |
|
16
|
|
|
|
|
|
|
port => $args->{port} // 9982, |
|
17
|
|
|
|
|
|
|
debug_info => $args->{debug_info} // 0, |
|
18
|
|
|
|
|
|
|
no_client => $args->{no_client} // 0, |
|
19
|
1
|
|
50
|
|
|
32
|
epgdb_v3 => $args->{epgdb_v3} // 0, |
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
20
|
|
|
|
|
|
|
db => [], |
|
21
|
|
|
|
|
|
|
}; |
|
22
|
1
|
50
|
|
|
|
8
|
$self->{template} = $self->{epgdb_v3} ? "w" : "N"; # unpack template for length of htsp messages to be deserialised |
|
23
|
1
|
50
|
|
|
|
4
|
unless ($self->{no_client}) { |
|
24
|
1
|
|
|
1
|
|
6
|
no warnings 'once'; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
4360
|
|
|
25
|
|
|
|
|
|
|
$self->{client} = IO::Socket->new( |
|
26
|
|
|
|
|
|
|
Domain => AF_INET, |
|
27
|
|
|
|
|
|
|
Type => SOCK_STREAM, |
|
28
|
|
|
|
|
|
|
proto => 'tcp', |
|
29
|
|
|
|
|
|
|
PeerHost => "$self->{host}", |
|
30
|
|
|
|
|
|
|
PeerPort => $self->{port}, |
|
31
|
0
|
|
0
|
|
|
0
|
) || die "Error ".(caller(0))[3].": can't open socket: $IO::Socket::errstr"; |
|
32
|
|
|
|
|
|
|
# my $peer_addr = $self->{client}->connected(); |
|
33
|
|
|
|
|
|
|
# if ($peer_addr) { say "Connected to $peer_addr"; } |
|
34
|
|
|
|
|
|
|
} |
|
35
|
1
|
|
|
|
|
6
|
return bless $self, $class; |
|
36
|
|
|
|
|
|
|
} |
|
37
|
|
|
|
|
|
|
sub DESTROY { |
|
38
|
|
|
|
|
|
|
# close IO::Socket |
|
39
|
1
|
|
|
1
|
|
848
|
my $self = shift; |
|
40
|
1
|
50
|
|
|
|
94
|
$self->{client}->close() if $self->{client}; |
|
41
|
|
|
|
|
|
|
} |
|
42
|
|
|
|
|
|
|
sub getChanUuidId { |
|
43
|
|
|
|
|
|
|
# Get all channel uuid and ID |
|
44
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
45
|
|
|
|
|
|
|
# Enable async to find channel uuid and ID |
|
46
|
0
|
|
|
|
|
0
|
$self->htsp_send({'method' => 'enableAsyncMetadata'}); |
|
47
|
|
|
|
|
|
|
# Process messages |
|
48
|
0
|
|
|
|
|
0
|
my $chan_uuid_id = {}; |
|
49
|
0
|
|
|
|
|
0
|
while (1) { |
|
50
|
0
|
|
|
|
|
0
|
my $reply = $self->htsp_recv(); |
|
51
|
0
|
0
|
|
|
|
0
|
if ($reply->{'method'}) { |
|
52
|
0
|
0
|
|
|
|
0
|
if ($reply->{'method'} eq 'channelAdd') { |
|
|
|
0
|
|
|
|
|
|
|
53
|
0
|
|
|
|
|
0
|
$chan_uuid_id->{$reply->{'channelIdStr'}} = $reply->{'channelId'}; |
|
54
|
|
|
|
|
|
|
} |
|
55
|
|
|
|
|
|
|
elsif ($reply->{'method'} eq 'initialSyncCompleted') { |
|
56
|
0
|
|
|
|
|
0
|
last; |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
} |
|
59
|
|
|
|
|
|
|
} |
|
60
|
0
|
|
|
|
|
0
|
return $chan_uuid_id; |
|
61
|
|
|
|
|
|
|
} |
|
62
|
|
|
|
|
|
|
sub getChanNamId { |
|
63
|
|
|
|
|
|
|
# Get 'channelName' and channelId' |
|
64
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
65
|
0
|
|
|
|
|
0
|
my $channel = shift; |
|
66
|
0
|
|
|
|
|
0
|
my ($chan_name, $chan_id); |
|
67
|
0
|
0
|
0
|
|
|
0
|
if (LooksLike::integer($channel) and $channel > 100_000_000) { |
|
68
|
|
|
|
|
|
|
# $channel > 100_000_000 must be a 'channelId' |
|
69
|
0
|
|
|
|
|
0
|
$chan_id = $channel; |
|
70
|
|
|
|
|
|
|
# get channel name |
|
71
|
0
|
|
|
|
|
0
|
my $reply = $self->htsp_send_recv({'method' => 'getChannel', 'channelId' => $channel}); |
|
72
|
0
|
0
|
0
|
|
|
0
|
if ($reply->{'channelName'} and $reply->{'channelId'} eq "$channel") { |
|
73
|
0
|
|
|
|
|
0
|
$chan_name = $reply->{'channelName'}; |
|
74
|
|
|
|
|
|
|
} |
|
75
|
0
|
|
|
|
|
0
|
else {die "Error ".(caller(0))[3].": channelId '$channel' not found -- ".join(', ', map {"'$_ => $reply->{$_}'"} (sort keys $reply->%*))} |
|
|
0
|
|
|
|
|
0
|
|
|
76
|
|
|
|
|
|
|
} |
|
77
|
|
|
|
|
|
|
else { |
|
78
|
|
|
|
|
|
|
# Enable async to find 'channelId' from 'channelName' or 'channelNumber' |
|
79
|
0
|
|
|
|
|
0
|
$self->htsp_send({'method' => 'enableAsyncMetadata'}); |
|
80
|
|
|
|
|
|
|
# Process messages |
|
81
|
0
|
|
|
|
|
0
|
my $chan_nam_id = {}; |
|
82
|
0
|
|
|
|
|
0
|
my $chan_num_id = {}; |
|
83
|
0
|
|
|
|
|
0
|
my $chan_num_nam = {}; |
|
84
|
0
|
|
|
|
|
0
|
while (1) { |
|
85
|
0
|
|
|
|
|
0
|
my $reply = $self->htsp_recv(); |
|
86
|
0
|
0
|
|
|
|
0
|
if ($reply->{'method'}) { |
|
87
|
0
|
0
|
|
|
|
0
|
if ($reply->{'method'} eq 'channelAdd') { |
|
|
|
0
|
|
|
|
|
|
|
88
|
0
|
|
|
|
|
0
|
$chan_nam_id->{$reply->{'channelName'}} = $reply->{'channelId'}; |
|
89
|
0
|
|
|
|
|
0
|
$chan_num_id->{$reply->{'channelNumber'}} = $reply->{'channelId'}; |
|
90
|
0
|
|
|
|
|
0
|
$chan_num_nam->{$reply->{'channelNumber'}} = $reply->{'channelName'}; |
|
91
|
|
|
|
|
|
|
} |
|
92
|
|
|
|
|
|
|
elsif ($reply->{'method'} eq 'initialSyncCompleted') { |
|
93
|
0
|
|
|
|
|
0
|
last; |
|
94
|
|
|
|
|
|
|
} |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
} |
|
97
|
0
|
0
|
|
|
|
0
|
if ($chan_nam_id->{$channel}) { |
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
# found 'channelName' |
|
99
|
0
|
|
|
|
|
0
|
$chan_name = $channel; |
|
100
|
0
|
|
|
|
|
0
|
$chan_id = $chan_nam_id->{$channel}; |
|
101
|
|
|
|
|
|
|
} |
|
102
|
|
|
|
|
|
|
elsif ($chan_num_id->{$channel}) { |
|
103
|
|
|
|
|
|
|
# found 'channelNumber' |
|
104
|
0
|
|
|
|
|
0
|
$chan_name = $chan_num_nam->{$channel}; |
|
105
|
0
|
|
|
|
|
0
|
$chan_id = $chan_num_id->{$channel}; |
|
106
|
|
|
|
|
|
|
} |
|
107
|
|
|
|
|
|
|
elsif (LooksLike::integer($channel)) { |
|
108
|
|
|
|
|
|
|
# 'channelNumber' not found |
|
109
|
0
|
|
|
|
|
0
|
my $chan_num_sorted = join(', ', map {"'$_ $chan_num_nam->{$_}'"} (sort {$a <=> $b} keys $chan_num_nam->%*)); |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
110
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": channelNumber '$channel' not found in [ $chan_num_sorted ]"); |
|
111
|
|
|
|
|
|
|
} |
|
112
|
|
|
|
|
|
|
else { |
|
113
|
|
|
|
|
|
|
# 'channelName' not found |
|
114
|
0
|
|
|
|
|
0
|
my $chan_nam_sorted = join(', ', map {"'$_ $chan_num_nam->{$_}'"} (sort {$a <=> $b} keys $chan_num_nam->%*)); |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
115
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": channelName '$channel' not found in [ $chan_nam_sorted ]"); |
|
116
|
|
|
|
|
|
|
} |
|
117
|
|
|
|
|
|
|
} |
|
118
|
0
|
|
|
|
|
0
|
return ($chan_name, $chan_id); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
sub htsp_send_recv { |
|
121
|
|
|
|
|
|
|
# send and receive HTSP message |
|
122
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
123
|
0
|
|
|
|
|
0
|
$self->htsp_send(shift); # serialise and send |
|
124
|
0
|
|
|
|
|
0
|
return $self->htsp_recv(); # receive and deserialise |
|
125
|
|
|
|
|
|
|
} |
|
126
|
|
|
|
|
|
|
sub htsp_send { |
|
127
|
|
|
|
|
|
|
# send HTSP message |
|
128
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
129
|
0
|
|
|
|
|
0
|
my $msg = shift; |
|
130
|
0
|
|
|
|
|
0
|
state $i = 0; |
|
131
|
0
|
|
|
|
|
0
|
$i++; |
|
132
|
0
|
|
|
|
|
0
|
$self->{msg} = $msg; |
|
133
|
0
|
|
|
|
|
0
|
my $htspmsg = $self->htsmsg_serialise($msg); # serialise |
|
134
|
0
|
0
|
|
|
|
0
|
say STDERR join(' ',(unpack("H*",$htspmsg) =~ /../g)) if $self->{debug_info}; # convert serialised message to H = hex string (high nybble first) |
|
135
|
0
|
0
|
|
|
|
0
|
say "$i sending '$msg->{'method'}' -- ".(caller(0))[3] if $self->{debug_info}; |
|
136
|
0
|
|
|
|
|
0
|
my $size = $self->{client}->send($htspmsg); |
|
137
|
0
|
0
|
|
|
|
0
|
say " sent data of length $size" if $self->{debug_info}; |
|
138
|
0
|
0
|
|
|
|
0
|
$self->htsmsg_deserialise(\$htspmsg) if $self->{debug_info}; # deserialise |
|
139
|
0
|
|
|
|
|
0
|
return $size; |
|
140
|
|
|
|
|
|
|
} |
|
141
|
|
|
|
|
|
|
sub htsp_recv { |
|
142
|
|
|
|
|
|
|
# receive HTSP message |
|
143
|
0
|
|
|
0
|
1
|
0
|
my $self = shift; |
|
144
|
0
|
|
|
|
|
0
|
my ($buffer, $length) = ('', 0); |
|
145
|
0
|
|
|
|
|
0
|
state $rest = ''; |
|
146
|
0
|
|
|
|
|
0
|
state $i = 0; |
|
147
|
0
|
|
|
|
|
0
|
$i++; |
|
148
|
0
|
|
|
|
|
0
|
$buffer = $rest; # take over the 'rest' = remaining buffer from the previous call |
|
149
|
0
|
0
|
|
|
|
0
|
$length = unpack("N",substr($buffer,0,4,'')) if ($buffer); # Total length of message = 4 byte integer "network" big-endian byte-order |
|
150
|
0
|
|
|
|
|
0
|
while (not $buffer) { |
|
151
|
|
|
|
|
|
|
# Determine beginning and length of the message |
|
152
|
0
|
|
0
|
|
|
0
|
$self->{client}->recv($buffer, 4096) // die "Error ".(caller(0))[3].": from socket: $IO::Socket::errstr"; # get bytes from HTSP server |
|
153
|
0
|
0
|
|
|
|
0
|
die "Error ".(caller(0))[3].": received '$buffer' of length ".length($buffer) unless $buffer; |
|
154
|
0
|
0
|
|
|
|
0
|
say "$i received data of length ".length($buffer)." -- ".(caller(0))[3] if $self->{debug_info}; |
|
155
|
0
|
0
|
|
|
|
0
|
say STDERR join(' ',(unpack("H*",$buffer) =~ /../g)) if $self->{debug_info}; # convert serialised message to H = hex string (high nybble first) |
|
156
|
0
|
|
0
|
|
|
0
|
while ($length == 0 and $buffer) { |
|
157
|
|
|
|
|
|
|
# enableAsyncMetadata first transmits a Null-Byte, we need to get rid of this |
|
158
|
0
|
|
|
|
|
0
|
$length = unpack("N",substr($buffer,0,4,'')); # Total length of message = 4 byte integer "network" big-endian byte-order |
|
159
|
0
|
0
|
|
|
|
0
|
say " message length $length" if $self->{debug_info}; |
|
160
|
|
|
|
|
|
|
} |
|
161
|
0
|
0
|
0
|
|
|
0
|
return({ response => 0 }) if ($length == 0 and not $buffer and $self->{msg}->{method} ne "enableAsyncMetadata"); |
|
|
|
|
0
|
|
|
|
|
|
162
|
|
|
|
|
|
|
} |
|
163
|
0
|
0
|
0
|
|
|
0
|
if ($length > 10_000_000 and $self->{debug_info}) { |
|
164
|
|
|
|
|
|
|
# a likely incorrect excessive length, because the bytes are not being interpreted correctly |
|
165
|
0
|
|
|
|
|
0
|
say STDERR join(' ',(unpack("H*",$buffer) =~ /../g)); # convert serialised message to H = hex string (high nybble first) |
|
166
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": message length '$length' > 10_000_000, not realistic"); |
|
167
|
|
|
|
|
|
|
} |
|
168
|
0
|
|
|
|
|
0
|
while ($length > length($buffer)) { |
|
169
|
|
|
|
|
|
|
# the message length requires more bytes |
|
170
|
0
|
0
|
|
|
|
0
|
say " message length $length > ".length($buffer)." length(\$buffer)" if $self->{debug_info}; |
|
171
|
0
|
|
0
|
|
|
0
|
$self->{client}->recv(my $buffer0, min($length-length($buffer),4096)) // die "Error ".(caller(0))[3].": from socket: $IO::Socket::errstr"; # get bytes from HTSP server |
|
172
|
0
|
0
|
|
|
|
0
|
die "Error ".(caller(0))[3].": received '$buffer0' of length ".length($buffer0) unless $buffer0; |
|
173
|
0
|
0
|
|
|
|
0
|
say " received data of length ".length($buffer0) if $self->{debug_info}; |
|
174
|
0
|
0
|
|
|
|
0
|
say STDERR join(' ',(unpack("H*",$buffer0) =~ /../g)) if $self->{debug_info}; # convert serialised message to H = hex string (high nybble first) |
|
175
|
0
|
|
|
|
|
0
|
$buffer .= $buffer0; # append the received bytes to the response '$buffer' |
|
176
|
|
|
|
|
|
|
} |
|
177
|
0
|
0
|
|
|
|
0
|
if ($length <= length($buffer)) { |
|
178
|
|
|
|
|
|
|
# the response contains additional bytes of the subsequent message |
|
179
|
0
|
0
|
|
|
|
0
|
say " message length $length <= ".length($buffer)." length(\$buffer)" if $self->{debug_info}; |
|
180
|
0
|
|
|
|
|
0
|
$rest = $buffer; # transfer response to '$rest' |
|
181
|
0
|
|
|
|
|
0
|
$buffer = substr($rest,0,$length,''); # shorten response '$buffer' to the required message '$length', keep the remainder in '$rest' for the subsequent call to 'htsp_recv' |
|
182
|
|
|
|
|
|
|
} |
|
183
|
0
|
0
|
|
|
|
0
|
say "$i buffer length -- ".length($buffer)." -- ".(caller(0))[3] if $self->{debug_info}; |
|
184
|
0
|
|
|
|
|
0
|
my $htspmsg = pack("N",$length).$buffer; # prepend the previously removed 4 bytes with the message length |
|
185
|
0
|
|
|
|
|
0
|
return $self->htsmsg_deserialise(\$htspmsg); # deserialise the message an return it |
|
186
|
|
|
|
|
|
|
} |
|
187
|
|
|
|
|
|
|
sub htsmsg_serialise { |
|
188
|
|
|
|
|
|
|
# serialise a HTSP message |
|
189
|
7
|
|
|
7
|
0
|
3324
|
my $self = shift; |
|
190
|
7
|
|
|
|
|
14
|
my $msg = shift; |
|
191
|
7
|
|
|
|
|
14
|
state $sub_message = 0; |
|
192
|
7
|
|
|
|
|
9
|
state $i=0; |
|
193
|
7
|
50
|
33
|
|
|
32
|
say ++$i." -- ".(caller(0))[3] if $self->{debug_info} and not $sub_message; |
|
194
|
7
|
|
|
|
|
14
|
my $htspmsg = ''; |
|
195
|
7
|
|
|
|
|
15
|
my ($ishash, @keys, @vals); |
|
196
|
7
|
100
|
|
|
|
41
|
if (ref $msg eq "HASH") { |
|
|
|
50
|
|
|
|
|
|
|
197
|
5
|
|
|
|
|
9
|
$ishash = 1; |
|
198
|
5
|
|
|
|
|
38
|
@keys = keys $msg->%*; |
|
199
|
5
|
|
|
|
|
61
|
@vals = values $msg->%*; |
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
elsif (ref $msg eq "ARRAY") { |
|
202
|
2
|
50
|
|
|
|
7
|
die ("Error ".(caller(0))[3].": root message must be of type 'Map' = a 'HASH' reference, not '".ref($msg)."'") unless $sub_message; |
|
203
|
2
|
|
|
|
|
4
|
$ishash = 0; |
|
204
|
2
|
|
|
|
|
9
|
@keys = keys $msg->@*; |
|
205
|
2
|
|
|
|
|
7
|
@vals = values $msg->@*; |
|
206
|
|
|
|
|
|
|
} |
|
207
|
|
|
|
|
|
|
else { |
|
208
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": message must be a 'HASH' or 'ARRAY' reference, not '".ref($msg)."'"); |
|
209
|
|
|
|
|
|
|
} |
|
210
|
7
|
|
|
|
|
26
|
for my $key (@keys) { |
|
211
|
25
|
|
|
|
|
50
|
my $val = shift @vals; |
|
212
|
25
|
50
|
|
|
|
76
|
say ' 'x($sub_message+1)."$key => $val" if $self->{debug_info}; |
|
213
|
25
|
100
|
|
|
|
105
|
if (ref $val eq "HASH") { |
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
# Map = 1 = Sub message of type map |
|
215
|
3
|
|
|
|
|
7
|
my $type = pack("C",1); # 1 byte integer |
|
216
|
3
|
100
|
|
|
|
11
|
my $namelength = $ishash ? pack("C",length($key)) : chr(0); # 1 byte integer |
|
217
|
3
|
100
|
|
|
|
8
|
my $name = $ishash ? $key : ''; # string |
|
218
|
3
|
|
|
|
|
6
|
$sub_message++; |
|
219
|
3
|
|
|
|
|
43
|
my $data = $self->htsmsg_serialise($val); |
|
220
|
3
|
|
|
|
|
8
|
$sub_message--; |
|
221
|
3
|
|
|
|
|
7
|
my $datalength = pack("N",length($data)); # 4 byte integer "network" big-endian byte-order |
|
222
|
3
|
|
|
|
|
11
|
$htspmsg .= $type.$namelength.$datalength.$name.$data; |
|
223
|
|
|
|
|
|
|
} |
|
224
|
|
|
|
|
|
|
elsif (ref $val eq "ARRAY") { |
|
225
|
|
|
|
|
|
|
# List = 5 = Sub message of type list |
|
226
|
2
|
|
|
|
|
6
|
my $type = pack("C",5); # 1 byte integer |
|
227
|
2
|
100
|
|
|
|
8
|
my $namelength = $ishash ? pack("C",length($key)) : chr(0); # 1 byte integer |
|
228
|
2
|
100
|
|
|
|
5
|
my $name = $ishash ? $key : ''; # string |
|
229
|
2
|
|
|
|
|
5
|
$sub_message++; |
|
230
|
2
|
|
|
|
|
10
|
my $data = $self->htsmsg_serialise($val); |
|
231
|
2
|
|
|
|
|
3
|
$sub_message--; |
|
232
|
2
|
|
|
|
|
6
|
my $datalength = pack("N",length($data)); # 4 byte integer "network" big-endian byte-order |
|
233
|
2
|
|
|
|
|
6
|
$htspmsg .= $type.$namelength.$datalength.$name.$data; |
|
234
|
|
|
|
|
|
|
} |
|
235
|
|
|
|
|
|
|
elsif (ref $val ne "") { |
|
236
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": field value cannot be a reference of type '".ref($val)."'"); |
|
237
|
|
|
|
|
|
|
} |
|
238
|
|
|
|
|
|
|
elsif (LooksLike::integer($val)) { |
|
239
|
|
|
|
|
|
|
# S64 = 2 = Signed 64bit integer |
|
240
|
7
|
|
|
|
|
173
|
my $type = pack("C",2); # 1 byte integer |
|
241
|
7
|
100
|
|
|
|
25
|
my $namelength = $ishash ? pack("C",length($key)) : chr(0); # 1 byte integer |
|
242
|
7
|
100
|
|
|
|
19
|
my $name = $ishash ? $key : ''; # string |
|
243
|
7
|
|
|
|
|
18
|
my $data = pack("q<",$val); # 64 bit = 8 byte signed integer little-endian byte-order -> q = signed quad (64-bit) value |
|
244
|
|
|
|
|
|
|
# Integers are encoded using a very simple variable length encoding. All leading bytes that are 0 is discarded. |
|
245
|
7
|
|
|
|
|
46
|
$data =~ s/\0{1,7}$//; # remove Null-Byte from the end, keep a first one |
|
246
|
7
|
|
|
|
|
21
|
my $datalength = pack("N",length($data)); # 4 byte integer "network" big-endian byte-order |
|
247
|
7
|
|
|
|
|
24
|
$htspmsg .= $type.$namelength.$datalength.$name.$data; |
|
248
|
|
|
|
|
|
|
} |
|
249
|
|
|
|
|
|
|
elsif (LooksLike::numeric($val)) { |
|
250
|
|
|
|
|
|
|
# Dbl = 6 = Double precision floating point |
|
251
|
0
|
|
|
|
|
0
|
my $type = pack("C",6); # 1 byte integer |
|
252
|
0
|
0
|
|
|
|
0
|
my $namelength = $ishash ? pack("C",length($key)) : chr(0); # 1 byte integer |
|
253
|
0
|
0
|
|
|
|
0
|
my $name = $ishash ? $key : ''; # string |
|
254
|
0
|
|
|
|
|
0
|
my $data = pack("d",$val); # d = A double-precision float in native format |
|
255
|
0
|
|
|
|
|
0
|
my $datalength = pack("N",length($data)); # 4 byte integer "network" big-endian byte-order |
|
256
|
0
|
|
|
|
|
0
|
$htspmsg .= $type.$namelength.$datalength.$name.$data; |
|
257
|
|
|
|
|
|
|
} |
|
258
|
|
|
|
|
|
|
else { |
|
259
|
|
|
|
|
|
|
# Str = 3 = UTF-8 encoded string |
|
260
|
13
|
|
|
|
|
724
|
my $type = pack("C",3); # 1 byte integer |
|
261
|
13
|
100
|
|
|
|
45
|
my $namelength = $ishash ? pack("C",length($key)) : chr(0); # 1 byte integer |
|
262
|
13
|
100
|
|
|
|
55
|
my $name = $ishash ? $key : ''; # string |
|
263
|
13
|
|
|
|
|
25
|
my $data = $val; # UTF-8 encoded string |
|
264
|
13
|
|
|
|
|
28
|
my $datalength = pack("N",length($data)); # 4 byte integer "network" big-endian byte-order |
|
265
|
13
|
|
|
|
|
46
|
$htspmsg .= $type.$namelength.$datalength.$name.$data; |
|
266
|
|
|
|
|
|
|
} |
|
267
|
|
|
|
|
|
|
} |
|
268
|
|
|
|
|
|
|
# say ' 'x($sub_message+1)."sub_message = ".$sub_message if $self->{debug_info}; |
|
269
|
|
|
|
|
|
|
# in case of root message prepend message length |
|
270
|
7
|
100
|
|
|
|
37
|
return $sub_message ? $htspmsg : pack("N",length($htspmsg)).$htspmsg; |
|
271
|
|
|
|
|
|
|
} |
|
272
|
|
|
|
|
|
|
sub htsmsg_deserialise { |
|
273
|
|
|
|
|
|
|
# deserialise a HTSP message |
|
274
|
|
|
|
|
|
|
# https://docs.tvheadend.org/documentation/development/htsp/htsmsg-binary-format |
|
275
|
4
|
|
|
4
|
1
|
1719
|
my $self = shift; |
|
276
|
4
|
|
|
|
|
5
|
my $htsmsg = shift; # reference to the message to be deserialised |
|
277
|
4
|
|
|
|
|
8
|
state $i=0; |
|
278
|
4
|
|
|
|
|
6
|
my $bd={}; # root message must be of type 'Map' = a 'HASH' reference |
|
279
|
|
|
|
|
|
|
# Root body |
|
280
|
|
|
|
|
|
|
# Length of Root body = 4 byte integer = Total length of message (not including this length field itself) |
|
281
|
4
|
|
|
|
|
11
|
my $length = $self->htsmsg_message_length($htsmsg); # Total length of message |
|
282
|
4
|
50
|
|
|
|
11
|
say ++$i." -- $length -- ".(caller(0))[3] if $self->{debug_info}; |
|
283
|
4
|
|
|
|
|
11
|
my $body = substr($$htsmsg,0,$length,''); # keep additional length in '$htsmsg' for any subsequent messages |
|
284
|
|
|
|
|
|
|
# Body = HTSMSG-Field * N = Fields in the root body |
|
285
|
4
|
|
|
|
|
8
|
while (length $body) { |
|
286
|
18
|
|
|
|
|
32
|
$self->htsmsg_field_deserialise (\$body, $bd); |
|
287
|
|
|
|
|
|
|
} |
|
288
|
4
|
50
|
|
|
|
10
|
push($self->{db}->@*, $bd) if $self->{debug_info}; |
|
289
|
4
|
|
|
|
|
10
|
return $bd; |
|
290
|
|
|
|
|
|
|
} |
|
291
|
|
|
|
|
|
|
sub htsmsg_field_deserialise { |
|
292
|
|
|
|
|
|
|
# deserialise a HTSP field |
|
293
|
43
|
|
|
43
|
0
|
56
|
my $self = shift; |
|
294
|
43
|
|
|
|
|
48
|
my $htsmsg = shift; |
|
295
|
43
|
|
|
|
|
49
|
my $bd = shift; |
|
296
|
43
|
|
|
|
|
42
|
state $indent=1; |
|
297
|
|
|
|
|
|
|
# process HTSMSG-Field |
|
298
|
|
|
|
|
|
|
# Type = 1 byte integer = Type of field by ID |
|
299
|
43
|
|
|
|
|
101
|
my $type = unpack("C",substr($$htsmsg,0,1,'')); |
|
300
|
|
|
|
|
|
|
# Namelength = 1 byte integer = Length of name of field. If a field is part of a list message this must be 0 |
|
301
|
43
|
|
|
|
|
71
|
my $namelength = unpack("C",substr($$htsmsg,0,1,'')); |
|
302
|
|
|
|
|
|
|
# Datalength = 4 byte integer = Length of field data |
|
303
|
43
|
|
|
|
|
78
|
my $datalength = $self->htsmsg_message_length($htsmsg); # determine data length |
|
304
|
|
|
|
|
|
|
# Name = N bytes = Field name, length as specified by Namelength |
|
305
|
43
|
|
|
|
|
67
|
my $name = substr($$htsmsg,0,$namelength,''); |
|
306
|
|
|
|
|
|
|
# Data = N bytes = Field payload |
|
307
|
43
|
|
|
|
|
73
|
my $data = substr($$htsmsg,0,$datalength,''); |
|
308
|
43
|
100
|
|
|
|
98
|
if ($type == 1) { |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
# Map = 1 = Sub message of type map |
|
310
|
3
|
50
|
|
|
|
11
|
say ' 'x$indent."$type $namelength $datalength '$name'" if $self->{debug_info}; |
|
311
|
3
|
|
|
|
|
5
|
my $ref; |
|
312
|
3
|
100
|
|
|
|
14
|
if (ref $bd eq "HASH") { |
|
|
|
50
|
|
|
|
|
|
|
313
|
1
|
50
|
|
|
|
7
|
$bd->{"$name"} = {} unless $bd->{"$name"}; |
|
314
|
1
|
|
|
|
|
5
|
$ref = $bd->{"$name"}; |
|
315
|
|
|
|
|
|
|
} |
|
316
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
317
|
2
|
|
|
|
|
5
|
push($bd->@*, {}); |
|
318
|
2
|
|
|
|
|
5
|
$ref = $bd->@[-1]; |
|
319
|
|
|
|
|
|
|
} |
|
320
|
3
|
|
|
|
|
7
|
while (length $data) { |
|
321
|
11
|
|
|
|
|
18
|
$indent++; |
|
322
|
11
|
|
|
|
|
151
|
$self->htsmsg_field_deserialise (\$data, $ref) ; |
|
323
|
11
|
|
|
|
|
26
|
$indent--; |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
} |
|
326
|
|
|
|
|
|
|
elsif ($type == 2) { |
|
327
|
|
|
|
|
|
|
# S64 = 2 = Signed 64bit integer |
|
328
|
|
|
|
|
|
|
# Integers are encoded using a very simple variable length encoding. All leading bytes that are 0 is discarded. |
|
329
|
10
|
|
|
|
|
44
|
while (length($data) < 8) {$data .= chr(0);} # fill up with Null-Bytes |
|
|
68
|
|
|
|
|
121
|
|
|
330
|
10
|
|
|
|
|
21
|
$data = unpack("q<",$data); # 64 bit = 8 byte signed integer little-endian byte-order -> q = signed quad (64-bit) value |
|
331
|
10
|
100
|
|
|
|
28
|
if (ref $bd eq "HASH") { |
|
|
|
50
|
|
|
|
|
|
|
332
|
7
|
|
|
|
|
18
|
$bd->{"$name"} = $data; |
|
333
|
|
|
|
|
|
|
} |
|
334
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
335
|
3
|
|
|
|
|
6
|
push($bd->@*, $data); |
|
336
|
|
|
|
|
|
|
} |
|
337
|
10
|
50
|
|
|
|
38
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
338
|
|
|
|
|
|
|
} |
|
339
|
|
|
|
|
|
|
elsif ($type == 3) { |
|
340
|
|
|
|
|
|
|
# Str = 3 = UTF-8 encoded string |
|
341
|
|
|
|
|
|
|
# utf8::decode($data); |
|
342
|
26
|
100
|
|
|
|
48
|
if (ref $bd eq "HASH") { |
|
|
|
50
|
|
|
|
|
|
|
343
|
18
|
|
|
|
|
50
|
$bd->{"$name"} = $data; |
|
344
|
|
|
|
|
|
|
} |
|
345
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
346
|
8
|
|
|
|
|
10
|
push($bd->@*, $data); |
|
347
|
|
|
|
|
|
|
} |
|
348
|
26
|
50
|
|
|
|
59
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
349
|
|
|
|
|
|
|
} |
|
350
|
|
|
|
|
|
|
elsif ($type == 4) { |
|
351
|
|
|
|
|
|
|
# Bin = 4 = Binary blob |
|
352
|
1
|
|
|
|
|
4
|
$data = unpack("H*",$data); # convert binary blob to H = hex string (high nybble first) |
|
353
|
1
|
50
|
|
|
|
3
|
if (ref $bd eq "HASH") { |
|
|
|
0
|
|
|
|
|
|
|
354
|
1
|
|
|
|
|
3
|
$bd->{"$name"} = $data; |
|
355
|
|
|
|
|
|
|
} |
|
356
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
357
|
0
|
|
|
|
|
0
|
push($bd->@*, $data); |
|
358
|
|
|
|
|
|
|
} |
|
359
|
1
|
50
|
|
|
|
4
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
360
|
|
|
|
|
|
|
} |
|
361
|
|
|
|
|
|
|
elsif ($type == 5) { |
|
362
|
|
|
|
|
|
|
# List = 5 = Sub message of type list |
|
363
|
3
|
50
|
|
|
|
9
|
say ' 'x$indent."$type $namelength $datalength '$name'" if $self->{debug_info}; |
|
364
|
3
|
|
|
|
|
5
|
my $ref; |
|
365
|
3
|
100
|
|
|
|
26
|
if (ref $bd eq "HASH") { |
|
|
|
50
|
|
|
|
|
|
|
366
|
2
|
50
|
|
|
|
10
|
$bd->{"$name"} = [] unless $bd->{"$name"}; |
|
367
|
2
|
|
|
|
|
4
|
$ref = $bd->{"$name"}; |
|
368
|
|
|
|
|
|
|
} |
|
369
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
370
|
1
|
|
|
|
|
26
|
push($bd->@*, []); |
|
371
|
1
|
|
|
|
|
4
|
$ref = $bd->@[-1]; |
|
372
|
|
|
|
|
|
|
} |
|
373
|
3
|
|
|
|
|
8
|
while (length $data) { |
|
374
|
14
|
|
|
|
|
17
|
$indent++; |
|
375
|
14
|
|
|
|
|
43
|
$self->htsmsg_field_deserialise(\$data, $ref); |
|
376
|
14
|
|
|
|
|
21
|
$indent--; |
|
377
|
|
|
|
|
|
|
} |
|
378
|
|
|
|
|
|
|
} |
|
379
|
|
|
|
|
|
|
elsif ($type == 7) { |
|
380
|
|
|
|
|
|
|
# Bool = 7 = Boolean |
|
381
|
0
|
0
|
|
|
|
0
|
$data = $datalength ? unpack("C",$data) : 0; # C = 1 byte unsigned char |
|
382
|
0
|
0
|
|
|
|
0
|
if (ref $bd eq "HASH") { |
|
|
|
0
|
|
|
|
|
|
|
383
|
0
|
|
|
|
|
0
|
$bd->{"$name"} = $data; |
|
384
|
|
|
|
|
|
|
} |
|
385
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
386
|
0
|
|
|
|
|
0
|
push($bd->@*, $data); |
|
387
|
|
|
|
|
|
|
} |
|
388
|
0
|
0
|
|
|
|
0
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
389
|
|
|
|
|
|
|
} |
|
390
|
|
|
|
|
|
|
elsif ($type == 8) { |
|
391
|
|
|
|
|
|
|
# UUID = 8 = 64 bit UUID in binary format |
|
392
|
0
|
|
|
|
|
0
|
$data = unpack("H*",$data); # 16 byte = 128 bit binary number -> H = hex string (high nybble first) |
|
393
|
0
|
0
|
|
|
|
0
|
if (ref $bd eq "HASH") { |
|
|
|
0
|
|
|
|
|
|
|
394
|
0
|
|
|
|
|
0
|
$bd->{"$name"} = $data; |
|
395
|
|
|
|
|
|
|
} |
|
396
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
397
|
0
|
|
|
|
|
0
|
push($bd->@*, $data); |
|
398
|
|
|
|
|
|
|
} |
|
399
|
0
|
0
|
|
|
|
0
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
400
|
|
|
|
|
|
|
} |
|
401
|
|
|
|
|
|
|
elsif ($type == 6) { |
|
402
|
|
|
|
|
|
|
# Dbl = 6 = Double precision floating point |
|
403
|
0
|
|
|
|
|
0
|
$data = unpack("d",$data); # d = A double-precision float in native format |
|
404
|
0
|
0
|
|
|
|
0
|
if (ref $bd eq "HASH") { |
|
|
|
0
|
|
|
|
|
|
|
405
|
0
|
|
|
|
|
0
|
$bd->{"$name"} = $data; |
|
406
|
|
|
|
|
|
|
} |
|
407
|
|
|
|
|
|
|
elsif (ref $bd eq "ARRAY") { |
|
408
|
0
|
|
|
|
|
0
|
push($bd->@*, $data); |
|
409
|
|
|
|
|
|
|
} |
|
410
|
0
|
0
|
|
|
|
0
|
say ' 'x$indent."$type $namelength $datalength '$name' '$data'" if $self->{debug_info}; |
|
411
|
|
|
|
|
|
|
} |
|
412
|
|
|
|
|
|
|
else { |
|
413
|
0
|
|
|
|
|
0
|
die ("Error ".(caller(0))[3].": encountered unknown field type ID '$type', must be one of 1..8"); |
|
414
|
|
|
|
|
|
|
} |
|
415
|
|
|
|
|
|
|
} |
|
416
|
|
|
|
|
|
|
sub htsmsg_message_length { |
|
417
|
|
|
|
|
|
|
# determine message or data length of a htsp message to be deserialised |
|
418
|
47
|
|
|
47
|
0
|
54
|
my $self = shift; |
|
419
|
47
|
|
|
|
|
58
|
my $htsmsg = shift; |
|
420
|
47
|
|
|
|
|
86
|
my $template = $self->{template}; |
|
421
|
|
|
|
|
|
|
# for database version 3 -> a variable-length integer discarding leading zero bytes |
|
422
|
|
|
|
|
|
|
# -> template = "w" a BER compressed integer, Bit eight (the high bit) is set on each byte except the last |
|
423
|
|
|
|
|
|
|
# -- or -- |
|
424
|
|
|
|
|
|
|
# for HTSP protocol and database version 2 -> 4 byte integer "network" big-endian byte-order |
|
425
|
|
|
|
|
|
|
# -> template = "N" |
|
426
|
47
|
|
|
|
|
74
|
my $length = unpack("$template", $$htsmsg); |
|
427
|
47
|
|
|
|
|
90
|
substr($$htsmsg,0,length(pack("$template", $length)),''); # remove number of bytes consumed |
|
428
|
47
|
|
|
|
|
94
|
return $length; |
|
429
|
|
|
|
|
|
|
# instead of template "w" we can do like this |
|
430
|
|
|
|
|
|
|
# my ($bits, $seven_bit_chunks) = (1,''); # initalise |
|
431
|
|
|
|
|
|
|
# while ($bits) { |
|
432
|
|
|
|
|
|
|
# the continuation bit = most significant bit, MSB is set to 1, indicating that more bytes follow |
|
433
|
|
|
|
|
|
|
# $bits = unpack("B8", substr($$htsmsg,0,1,'')); # get next byte as a string of 8 bits |
|
434
|
|
|
|
|
|
|
# $seven_bit_chunks .= substr($bits,1,7,''); # add next 7-bit chunk |
|
435
|
|
|
|
|
|
|
# } |
|
436
|
|
|
|
|
|
|
# return oct('0b'.$seven_bit_chunks); # convert bits to value |
|
437
|
|
|
|
|
|
|
} |
|
438
|
|
|
|
|
|
|
1; |
|
439
|
|
|
|
|
|
|
# __ end of package htsp_tvh_client __ |
|
440
|
|
|
|
|
|
|
__END__ |