ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MPV/MPV.pm
Revision: 1.4
Committed: Sun Mar 19 20:56:59 2023 UTC (13 months, 4 weeks ago) by root
Branch: MAIN
Changes since 1.3: +16 -2 lines
Log Message:
*** empty log message ***

File Contents

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