File Coverage

blib/lib/Tvh/Htsp/Client.pm
Criterion Covered Total %
statement 143 263 54.3
branch 66 180 36.6
condition 6 37 16.2
subroutine 14 19 73.6
pod 7 10 70.0
total 236 509 46.3


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__