ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MPV/MPV.pm
Revision: 1.1
Committed: Sun Mar 19 19:36:40 2023 UTC (14 months ago) by root
Branch: MAIN
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MPV - remote control mpv (https://mpv.io)
4    
5     =head1 SYNOPSIS
6    
7     use AnyEvent::MPV;
8    
9     =head1 DESCRIPTION
10    
11     This module is an L<AnyEvent> user, you need to make sure that you use and
12     run a supported event loop.
13    
14     =cut
15    
16     package AnyEvent::MPV;
17    
18     use common::sense;
19    
20     use AnyEvent ();
21     use AnyEvent::Util ();
22    
23     our $JSON = eval { require JSON::XS; JSON::XS:: }
24     || do { require JSON::PP; JSON::PP:: };
25    
26     our $VERSION = '0.1';
27    
28     our $mpv_path; # last mpv path used
29     our $mpv_optionlist; # output of mpv --list-options
30    
31     sub new {
32     my ($class, %kv) = @_;
33    
34     bless {
35     mpv => "mpv",
36     args => [],
37     %kv,
38     }, $class
39     }
40    
41     # can be used to escape filenames
42     sub escape_binary {
43     shift;
44     local $_ = shift;
45     # we escape every "illegal" octet using U+10e5df HEX. this is later undone in cmd
46     s/([\x00-\x1f\x80-\xff])/sprintf "\x{10e5df}%02x", ord $1/ge;
47     $_
48     }
49    
50     sub start {
51     my ($self, @extra_args) = @_;
52    
53     return if $self->{fh};
54    
55     # cache optionlist for same "path"
56     ($mpv_path, $mpv_optionlist) = ($self->{mpv}, qx{\Q$self->{$mpv_path}\E --list-options})
57     if $self->{mpv} ne $mpv_path;
58    
59     my $options = $mpv_optionlist;
60    
61     my ($fh, $slave) = AnyEvent::Util::portable_socketpair
62     or die "socketpair: $!\n";
63    
64     fh_nonblocking $fh , 1;
65     fh_nonblocking $slave, 0;
66    
67     my $self->{pid} = fork;
68    
69     if ($self->{pid} eq 0) {
70     my $input_file = $options =~ /\s--input-ipc-client\s/ ? "input-ipc-client" : "input-file";
71    
72     exec $self->{mpv},
73     qw(--no-input-terminal --idle=yes --pause),
74     ($self->{trace} ? "--quiet" : "--really-quiet"),
75     "--$input_file=fd://" . (fileno $slave),
76     @{ $self->{args} },
77     @extra_args;
78     exit 1;
79     }
80    
81     $self->{fh} = $fh;
82    
83     my $trace = $self->{trace} || sub { };
84    
85     $trace = sub { warn "$_[0] $_[1]\n" } if $trace && !ref $trace;
86    
87     my $buf;
88     my $rw;
89     my $ww;
90     my $wbuf;
91     my $reqid;
92    
93     $self->{rw} = AE::io $fh, 0, sub {
94     if (sysread $fh, $buf, 8192, length $buf) {
95     while ($buf =~ s/^([^\n]+)\n//) {
96     $trace->("mpv>" => "$1");
97    
98     if ("{" eq substr $1, 0, 1) {
99     eval {
100     my $reply = JSON::XS->new->latin1->decode ($1);
101    
102     if (exists $reply->{event}) {
103     if (
104     $reply->{event} eq "client-message"
105     and $reply->{args}[0] eq "AnyEvent::MPV"
106     and $reply->{args}[1] eq "keyhack"
107     ) {
108     $self->on_key_event ($reply->{args}[2]);
109     } else {
110     $self->on_event ($reply->{event});
111     }
112     } elsif (exists $reply->{request_id}) {
113     my $cv = delete $self->{cmd_cv}{$reply->{request_id}};
114    
115     unless ($cv) {
116     warn "no cv found for request id <$reply->{request_id}>\n";
117     next;
118     }
119    
120     if (exists $reply->{data}) {
121     $cv->send ($reply->{data});
122     } elsif ($reply->{error} eq "success") { # success means error... eh.. no...
123     $cv->send;
124     } else {
125     $cv->croak ($reply->{error});
126     }
127    
128     } else {
129     warn "unexpected reply from mpv, pleasew report: <$1>\n";
130     }
131     };
132     warn $@ if $@;
133     } else {
134     $trace->("mpv>" => "$1");
135     }
136     }
137     } else {
138     $self->on_eof;
139     }
140     };
141    
142     $self->{_send} = sub {
143     $wbuf .= "$_[0]\n";
144    
145     $trace->(">mpv" => "$_[0]");
146    
147     $self->{ww} ||= AE::io $fh, 1, sub {
148     my $len = syswrite $fh, $wbuf;
149     substr $wbuf, 0, $len, "";
150     undef $self->{ww} unless length $wbuf;
151     };
152     };
153     }
154    
155     sub on_eof {
156     my ($self) = @_;
157    
158     $self->{on_eof}($self) if $self->{on_eof};
159     }
160    
161     sub on_event {
162     my ($self, $key) = @_;
163    
164     $self->{on_event}($self, $key) if $self->{on_event};
165     }
166    
167     sub on_key_event {
168     my ($self, $key) = @_;
169    
170     $self->{on_key_event}($self, $key) if $self->{on_key_event};
171     }
172    
173     sub cmd {
174     my ($self, @cmd) = @_;
175    
176     my $cv = AE::cv;
177    
178     my $reqid = ++$self->{reqid};
179     $self->{cmd_cv}{$reqid} = $cv;
180    
181     my $cmd = JSON::XS::encode_json { command => ref $cmd[0] ? $cmd[0] : \@cmd, request_id => $reqid*1 };
182    
183     # (un-)apply escape_binary hack
184     $cmd =~ s/\xf4\x8e\x97\x9f(..)/sprintf sprintf "\\x%02x", hex $1/ges; # f48e979f == 10e5df in utf-8
185    
186     $self->{_send}($cmd);
187    
188     $cv
189     }
190    
191     sub stop {
192     my ($self) = @_;
193    
194     if ($self->{pid}) {
195     delete $self->{rw};
196     delete $self->{ww};
197    
198     close delete $self->{fh}; # current mpv versions should cleanup on their own on close
199    
200     kill TERM => $self->{pid};
201    
202     delete $self->{pid};
203     }
204     }
205    
206     =head1 SEE ALSO
207    
208     L<AnyEvent>.
209    
210     =head1 AUTHOR
211    
212     Marc Lehmann <schmorp@schmorp.de>
213     http://home.schmorp.de/
214    
215     =cut
216    
217     1
218