File Coverage

blib/lib/Roku/ECP.pm
Criterion Covered Total %
statement 21 150 14.0
branch 0 36 0.0
condition 0 3 0.0
subroutine 7 26 26.9
pod 15 15 100.0
total 43 230 18.7


line stmt bran cond sub pod time code
1             # Roku::ECP
2             # Package implementing Roku External Control Guide:
3             # http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide
4             package Roku::ECP;
5 1     1   28767 use strict;
  1         3  
  1         71  
6 1     1   8 use warnings;
  1         3  
  1         56  
7 1     1   1186 use Encode; # To encode chars as UTF8
  1         18171  
  1         166  
8 1     1   1566 use URI;
  1         11014  
  1         75  
9 1     1   15 use URI::Escape; # To encode chars in URLs
  1         3  
  1         124  
10 1     1   8316 use LWP::UserAgent;
  1         50545  
  1         101  
11              
12             our $VERSION = "1.0.0";
13             our $USER_AGENT = __PACKAGE__ . "/" . $VERSION;
14             # User agent, for HTTP requests.
15              
16             =head1 NAME
17              
18             Roku::ECP - External Control Protocol for Roku
19              
20             =head1 SYNOPSIS
21              
22             use Roku::ECP;
23              
24             my $r = new Roku::ECP
25             hostname => "my-settop-box.dom.ain";
26              
27             my @apps = $r->apps();
28              
29             # Key and string input functions:
30             $r->keydown(Roku::ECP::Home);
31             $r->keyup(Roku::ECP::Down);
32             $r->keypress(Roku::ECP::Info,
33             Roku::ECP::Search,
34             Roku::ECP::Select);
35             $r->keydown_str("x");
36             $r->keyup_str("x");
37             $r->keydown_str("Hello world");
38              
39             $r->launch($app_id);
40             $r->launch($app_id, "12345abcd");
41             $r->launch($app_id, "12345abcd", "movie");
42              
43             my $icon = $r->geticonbyid("12345");
44             my $icon = $r->geticonbyname("My Roku Channel");
45              
46             $r->acceleration($x, $y, $z);
47             $r->orientation($x, $y, $z);
48             $r->rotation($x, $y, $z);
49             $r->magnetic($x, $y, $z);
50              
51             =head1 DESCRIPTION
52              
53             Roku::ECP implements the Roku External Control Guide, which permits
54             callers to query and control a Roku over the network.
55              
56             =head1 KEY NAMES
57              
58             The C<&key>* functions L, L, and L take
59             symbolic key names. They are:
60              
61             =over 4
62              
63             =item C
64              
65             =item C
66              
67             =item C
68              
69             =item C
70              
71             =item C
72              
73             =item C
74              
75             =item C
76              
77             =item C
78              
79             =item C
80              
81             =item C
82              
83             =item C
84              
85             =item C
86              
87             =item C
88              
89             =item C
90              
91             =item C
92              
93             =back
94              
95             =cut
96              
97             # These get fed to the /keypress (and friends) REST requests.
98             use constant {
99 1         1451 KEY_Home => "home",
100             KEY_Rev => "rev",
101             KEY_Fwd => "fwd",
102             KEY_Play => "play",
103             KEY_Select => "select",
104             KEY_Left => "left",
105             KEY_Right => "right",
106             KEY_Down => "down",
107             KEY_Up => "up",
108             KEY_Back => "back",
109             KEY_InstantReplay => "instantreplay",
110             KEY_Info => "info",
111             KEY_Backspace => "backspace",
112             KEY_Search => "search",
113             KEY_Enter => "enter",
114 1     1   9 };
  1         1  
115              
116             =head1 METHODS
117              
118             =cut
119              
120             # XXX - SSDP to discover devices wolud be nice. But I think that
121             # requires IO::Socket::Multicast, and also me learning how to use it.
122             # So for now, just keep your receipts so you know how many Rokus you
123             # have.
124              
125             =head2 C
126              
127             my $r = new Roku::ECP([I => I, ...])
128             my $r = Roku::ECP->new
129              
130             Create a new object with which to communicate with a Roku. For example:
131              
132             my $r = new Roku::ECP hostname => "my-settop-box.dom.ain";
133             my $r = new Roku::ECP addr => "192.168.1.10",
134             port => 1234;
135              
136             Possible Is:
137              
138             =over 4
139              
140             =item hostname
141              
142             Name of the Roku.
143              
144             =item addr
145              
146             IP(v4) address of the Roku.
147              
148             =item port
149              
150             TCP port on which to communicate with the Roku.
151              
152             =back
153              
154             Only one of C and C needs to be specified. If both are
155             given, the address takes precedence.
156              
157             =cut
158              
159             sub new
160             {
161 0     0 1   my $class = shift;
162 0           my %args = @_;
163 0           my $retval = {
164             port => 8060,
165             };
166              
167 0 0         $retval->{"hostname"} = $args{"hostname"} if defined $args{"hostname"};
168 0 0         $retval->{"addr"} = $args{"addr"} if defined $args{"addr"};
169 0 0 0       if (!defined($args{"hostname"}) &&
170             !defined($args{"addr"}))
171             {
172 0           warn __PACKAGE__ . "::new: Must specify at least one of hostname or addr.";
173 0           return undef;
174             }
175              
176 0 0         $retval->{"port"} = $args{"port"} if defined $args{"port"};
177              
178             # Construct base URL for subsequent requests.
179 0 0         $retval->{"url_base"} = "http://" .
180             (defined($retval->{'addr'}) ? $retval->{'addr'} : $retval->{'hostname'}) .
181             ":$retval->{'port'}";
182              
183             # Construct a LWP::UserAgent to use for REST calls. Might as
184             # well cache it if we're going to be making multiple calls.
185             # There might be some benefit in caching the connection as
186             # well.
187 0           $retval->{'ua'} = new LWP::UserAgent
188             agent => $USER_AGENT;
189              
190 0           bless $retval, $class;
191 0           return $retval;
192             }
193              
194             # _rest_request
195             # Wrapper around REST calls.
196             # $self->_rest_request(method, path,
197             # arg0 => value0,
198             # arg1 => value1,
199             # ...
200             # )
201             # Where:
202             # "method" is either "GET" or "POST'.
203             # "path" is a URL path, e.g., "/query/apps" or "/launch". This comes
204             # after the base URL, which was defined in the constructor.
205             # The remaining argument pairs are passed along
206             sub _rest_request
207             {
208 0     0     my $self = shift;
209 0           my $method = shift; # "GET" or "POST"
210 0           my $path = shift; # A URL path, like "/query/apps" or "/launch"
211              
212 0           my $result;
213              
214             # Construct the URL
215 0           my $url = new URI $self->{'url_base'} . $path;
216 0           $url->query_form(@_); # Add the remaining arguments as query
217             # parameters ("?a=foo&b=bar")
218              
219             # Call the right method for the request type.
220 0 0         if ($method eq "GET")
    0          
221             {
222 0           $result = $self->{'ua'}->get($url);
223             } elsif ($method eq "POST") {
224 0           $result = $self->{'ua'}->post($url);
225             } else {
226             # XXX - Complain and die
227             }
228 0 0         if ($result->code !~ /^2..$/)
229             {
230             return {
231 0           status => undef, # Unhappy
232             error => $result->code(),
233             message => $result->message(),
234             };
235             }
236              
237             return {
238 0           status => 1, # We're happy
239             "Content-Type" => $result->header("Content-Type"),
240             data => $result->decoded_content(),
241             };
242             }
243              
244             =head2 C
245              
246             my @apps = $r->apps();
247             # $apps[0] ==
248             # {
249             # id => '12345', # Can include underscores
250             # type => 'appl', # 'appl'|'menu'
251             # name => "Channel Name",
252             # version => '1.2.3',
253             # }
254              
255             Returns a list of ref-to-hash entries listing the channels installed
256             on the Roku.
257              
258             =cut
259              
260             sub apps
261             {
262 0     0 1   my $self = shift;
263 0           my @retval = ();
264 0           my $result = $self->_rest_request("GET", "/query/apps");
265 0 0         if (!$result->{'status'})
266             {
267 0           warn "Error: query/apps got status $result->{error}: $result->{message}";
268 0           return undef;
269             }
270 0           my $text = $result->{'data'};
271              
272             # Yeah, ideally it'd be nice to have a full-fledged XML parser
273             # but I can't be bothered until it actually becomes a problem.
274             # We expect lines of the form
275             # Some Channel
276 0           while ($text =~ m{
277            
278             id=\"(\w+)\" \s+
279             type=\"(\w+)\" \s+
280             version=\"([^\"]+)\"
281             >([^<]*)
282             }sgx)
283             {
284 0           my $app_id = $1;
285 0           my $app_type = $2;
286 0           my $app_version = $3;
287 0           my $app_name = $4;
288              
289 0           push @retval, {
290             id => $app_id,
291             type => $app_type,
292             version => $app_version,
293             name => $app_name,
294             };
295             }
296              
297 0           $self->{'apps'} = [@retval]; # Cache a copy
298 0           return @retval;
299             }
300              
301             =head2 C
302              
303             $r->launch($app_id);
304             $r->launch($app_id, $contentid);
305             $r->launch($app_id, $contentid, $mediatype)
306              
307             Launch an app on the Roku, optionally giving it an argument saying
308             what to do.
309              
310             The app ID can be obtained from C>.
311              
312             The optional C<$contentid> and C<$mediatype> arguments can be used to
313             implement deep linking, if the channel supports it. For instance,
314             C<$contentid> might be the ID number of a movie that the channel will
315             then automatically start playing. Likewise, C<$mediatype> can be used
316             to tell the channel what sort of entity C<$contentid> refers to.
317              
318             =cut
319              
320             # Deep linking
321             # Channel Store - ID 11:
322             # Opens the channel store to the channel whose ID is given by
323             # $contentid. Note that this is an integer, not the alphanumeric
324             # string code you find in listings of private channels.
325             # YouTube - ID 837
326             # $contentid is the YouTube identifier of the video to launch,
327             # the same identifier you get in
328             # https://youtube.com/watch?v=VVVVVVVVVVV
329             # URLs.
330             # Pandora - ID 28
331             # $contentid is the ID of a Pandora channel to play. It can take
332             # some digging to find these, but they're in Pandora URLs.
333              
334             sub launch
335             {
336 0     0 1   my $self = shift;
337 0           my $app = shift;
338 0           my $contentid = shift;
339 0           my $mediatype = shift;
340              
341             # XXX - Perhaps check whether $app is an ID or a name, and if
342             # the latter, try to look it up? How can we identify channel
343             # IDs?
344             # AFAICT channel IDs are of the form
345             # ^\d+(_[\da-f]{4})?$
346             # That is, a decimal number, optionally followed by an
347             # underscore and a four-hex-digit extension.
348              
349 0           my @query_args = ();
350 0 0         if (defined($contentid))
351             {
352 0           push @query_args, "contentID" => $contentid;
353             }
354 0 0         if (defined($mediatype))
355             {
356 0           push @query_args, "mediaType" => $mediatype;
357             }
358              
359 0           my $result = $self->_rest_request("POST", "/launch/$app", @query_args);
360 0 0         if (!$result->{'status'})
361             {
362             # Something went wrong;
363 0           warn "Error: launch/$app got status $result->{error}: $result->{message}";
364 0           return undef;
365             }
366 0           return 1; # Happy
367             }
368              
369             =head2 C
370              
371             my $icon = $r->geticonbyid("12345_67");
372             print ICONFILE $icon->{data} if $icon->{status};
373              
374             Fetches an app's icon. Most users will want to use C
375             instead.
376              
377             Takes the ID of an app (usually a number, but sometimes not).
378             Returns an anonymous hash describing the app's icon:
379              
380             =over 4
381              
382             =item status
383              
384             True if the icon was successfully fetched; false otherwise.
385              
386             =item error
387              
388             If C is false, then C gives the HTTP error code (e.g.,
389             404).
390              
391             =item message
392              
393             If C is false, then C gives the HTTP error message
394             (e.g., "not found").
395              
396             =item Content-Type
397              
398             The MIME type of the image. Usually C or C.
399              
400             =item data
401              
402             The binary data of the icon.
403              
404             =back
405              
406             =cut
407              
408             sub geticonbyid
409             {
410 0     0 1   my $self = shift;
411 0           my $app_id = shift;
412             ;
413 0           my $result = $self->_rest_request("GET", "/query/icon/$app_id");
414 0           return $result;
415             }
416              
417             =head2 C
418              
419             my $icon = $r->geticonbyid("My Roku Channel");
420             print ICONFILE $icon->{data} if $icon->{status};
421              
422             Fetches an app's icon.
423              
424             Takes the name of an app (a string).
425              
426             Returns an anonymous hash describing the app's icon, in the same
427             format as C.
428              
429             =cut
430              
431             sub geticonbyname
432             {
433 0     0 1   my $self = shift;
434 0           my $appname = shift;
435              
436             # Call 'apps' if necessary, to get a list of apps installed on
437             # the Roku.
438 0 0         if (!defined($self->{'apps'}))
439             {
440             # Fetch list of apps, since we don't have it yet
441 0           $self->apps;
442             }
443              
444             # Look up the app name in the id table
445 0           my $id = undef;
446 0           foreach my $app (@{$self->{'apps'}})
  0            
447             {
448 0 0         next unless $app->{'name'} eq $appname;
449 0           $id = $app->{'id'};
450 0           last;
451             }
452 0 0         return undef if !defined($id); # Name not found
453              
454             # Call geticonbyid to do the hard work.
455 0           return $self->geticonbyid($id);
456             }
457              
458             =head2 Keypress functions
459              
460             These functions use predefined key names. See L.
461              
462             All of these functions take any number of arguments, and send all of
463             the keys to the Roku in sequence.
464              
465             These functions all return 1 if successful, or undef otherwise. In
466             case of error, the return status does not say which parts of the
467             request were successful; the undef just means that something went
468             wrong.
469              
470             =cut
471              
472             # _key
473             # This is an internal helper function for the keydown/keyup/keypress
474             # functions. It takes key names (from the KEY_* constants, above) and
475             # issues a series of REST requests to send each key in turn to the
476             # Roku.
477             #
478             # Returns 1 on success, or undef on failure. If it fails, the return
479             # status doesn't say which keys succeeded; it just means that not all
480             # of them succeeded.
481             sub _key
482             {
483 0     0     my $self = shift;
484 0           my $url = shift; # The REST URL
485              
486 0           foreach my $key (@_)
487             {
488 0           my $result = $self->_rest_request("POST", "$url/$key");
489              
490 0 0         if (!$result->{'status'})
491             {
492 0           warn "Error: $url/$key got status $result->{error}: $result->{message}";
493 0           return undef;
494             }
495             }
496 0           return 1; # Happy
497             }
498              
499             # _key_str
500             # This is an internal helper function similar to _key, but for letters
501             # and such, rather than the buttons on the remote.
502             #
503             # It takes each string argument in turn, breaks it up into individual
504             # characters, and uses _key to send each letter in turn. For instance,
505             # the string "xyz" gets broken down into three requests: "Lit_x",
506             # "Lit_y", and "Lit_z".
507             #
508             # And yes, you may pronounce it "keister" if you want.
509             sub _key_str
510             {
511 0     0     my $self = shift;
512 0           my $url = shift; # The REST URL
513              
514 0           my $result;
515 0           foreach my $str (@_)
516             {
517             # Break this string up into individual characters
518 0           foreach my $c ($str =~ m{.}sg)
519             {
520             # Send the character as a /key*/Lit_* REST
521             # request.
522             # Assume that the string is UTF-8, coded, so
523             # $c might be several non-ASCII bytes. We use
524             # uri_escape_utf8 to escape this properly, so
525             # that a Euro symbol gets sent as
526             # "Lit_%E2%82%AC"
527 0           $result = $self->_key($url,
528             "Lit_" .
529             uri_escape_utf8($c));
530 0 0         return undef if !$result;
531             }
532             }
533 0           return 1;
534             }
535              
536             =head3 C
537              
538             my $status = $r->keypress(key, [key,...]);
539              
540             Sends a keypress event to the Roku. This is equivalent to releasing a key
541             on the remote, then releasing it.
542              
543             =cut
544              
545             sub keypress
546             {
547 0     0 1   my $self = shift;
548              
549 0           return $self->_key("/keypress", @_);
550             }
551              
552             =head3 C
553              
554             my $status = $r->keypress_str($string, [$string...]);
555              
556             Takes a string, breaks it up into individual characters, and sends
557             each one in turn to the Roku.
558              
559             =cut
560              
561             sub keypress_str
562             {
563 0     0 1   my $self = shift;
564              
565 0           return $self->_key_str("/keypress", @_);
566             }
567              
568             =head3 C
569              
570             my $status = $r->keydown(key, [key...]);
571              
572             Sends a keydown event to the Roku. This is equivalent to pressing a
573             key on the remote. Most people will want to use C>
574             instead.
575              
576             =cut
577              
578             sub keydown
579             {
580 0     0 1   my $self = shift;
581              
582 0           return $self->_key("/keydown", @_);
583             }
584              
585             =head3 C
586              
587             my $status = $r->keydown_str($string, [$string...]);
588              
589             Takes a string, breaks it up into individual characters, and sends
590             each one in turn to the Roku. Most people will want to use
591             C> instead.
592              
593             =cut
594              
595             sub keydown_str
596             {
597 0     0 1   my $self = shift;
598              
599 0           print "inside keydown_str(@_)\n";
600 0           return $self->_key_str("/keydown", @_);
601             }
602              
603             =head3 C
604              
605             my $status = $r->keyup(key, [key,...]);
606              
607             Sends a keyup event to the Roku. This is equivalent to releasing a key
608             on the remote. Most people will want to use C> instead.
609              
610             =cut
611              
612             sub keyup
613             {
614 0     0 1   my $self = shift;
615              
616 0           return $self->_key("/keyup", @_);
617             }
618              
619             =head3 C
620              
621             my $status = $r->keyup_str($string, [$string...]);
622              
623             Takes a string, breaks it up into individual characters, and sends
624             each one in turn to the Roku. Most people will want to use
625             C> instead.
626              
627             =cut
628              
629             sub keyup_str
630             {
631 0     0 1   my $self = shift;
632              
633 0           return $self->_key_str("/keyup", @_);
634             }
635              
636             =head2 Vector input methods
637              
638             The following methods send three-dimensional vectors to the
639             currently-running application. They each take three arguments: C<$x>,
640             C<$y>, C<$z>.
641              
642             These functions use one of two coordinate systems: relative to the
643             remote, or relative to the Earth. See the L in
644             the Roku documentation for details.
645              
646             These functions all return 1 if successful, or undef if not.
647              
648             =cut
649              
650             # _input
651             # Internal helper function for the user-visible input functions. Those
652             # are just implemented with _input.
653             sub _input
654             {
655 0     0     my $self = shift;
656 0           my $type = shift; # Input type
657 0           my $x = shift;
658 0           my $y = shift;
659 0           my $z = shift;
660              
661 0           my $result = $self->_rest_request("POST", "/input",
662             "$type.x" => $x,
663             "$type.x" => $y,
664             "$type.x" => $z);
665 0 0         if (!$result->{'status'})
666             {
667             # Something went wrong;
668 0           warn "Error: input/$type got status $result->{error}: $result->{message}";
669 0           return undef;
670             }
671 0           return 1; # Happy
672             }
673              
674             =head3 C
675              
676             my $status = $r->acceleration($x, $y, $z);
677              
678             Send an acceleration event to the currently-running application,
679             indicating motion in space.
680              
681             =cut
682              
683             sub acceleration
684             {
685 0     0 1   my $self = shift;
686 0           my $x = shift;
687 0           my $y = shift;
688 0           my $z = shift;
689              
690 0           return $self->_input("acceleration", $x, $y, $z);
691             }
692              
693             =head3 C
694              
695             my $status = $r->orientation($x, $y, $z);
696              
697             Send an orientation event to the currently-running application,
698             indicating tilting or displacement from lying flat.
699              
700             =cut
701              
702             sub orientation
703             {
704 0     0 1   my $self = shift;
705 0           my $x = shift;
706 0           my $y = shift;
707 0           my $z = shift;
708              
709 0           return $self->_input("orientation", $x, $y, $z);
710             }
711              
712             =head3 C
713              
714             my $status = $r->rotation($x, $y, $z);
715              
716             Send a rotation event to the currently-running application, indicating
717             rotation around an axis.
718              
719             =cut
720              
721             sub rotation
722             {
723 0     0 1   my $self = shift;
724 0           my $x = shift;
725 0           my $y = shift;
726 0           my $z = shift;
727              
728 0           return $self->_input("rotation", $x, $y, $z);
729             }
730              
731             =head3 C
732              
733             my $status = $r->magnetic($x, $y, $z);
734              
735             Send a magnetometer event to the currently-running application,
736             indicating the strength of the local magnetic field.
737              
738             =cut
739              
740             sub magnetic
741             {
742 0     0 1   my $self = shift;
743 0           my $x = shift;
744 0           my $y = shift;
745 0           my $z = shift;
746              
747 0           return $self->_input("magnetic", $x, $y, $z);
748             }
749              
750             # XXX - /input allegedly also supports touch and multi-touch, but I
751             # can't tell from the documentation how to send those.
752              
753             =head1 SEE ALSO
754              
755             =over 4
756              
757             =item External Control Guide
758              
759             http://sdkdocs.roku.com/display/sdkdoc/External+Control+Guide
760              
761             =back
762              
763             =head1 AUTHOR
764              
765             Andrew Arensburger, Earensb+pause@ooblick.comE
766              
767             =head1 COPYRIGHT AND LICENSE
768              
769             Copyright (C) 2014 by Andrew Arensburger
770              
771             This library is free software; you can redistribute it and/or modify
772             it under the same terms as Perl itself, either Perl version 5.14.2 or,
773             at your option, any later version of Perl 5 you may have available.
774              
775             =cut
776              
777             1;