use unix domain socket for communication with mpv
[uwplayer.git] / application / player.c
1 /*
2  * Copyright 2022 Olaf Wintermann
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a 
5  * copy of this software and associated documentation files (the "Software"), 
6  * to deal in the Software without restriction, including without limitation 
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
8  * and/or sell copies of the Software, and to permit persons to whom the 
9  * Software is furnished to do so, subject to the following conditions:
10  * 
11  * The above copyright notice and this permission notice shall be included in 
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
17  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
19  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
20  * DEALINGS IN THE SOFTWARE.
21  */
22
23 #include "player.h"
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <sys/fcntl.h>
29 #include <spawn.h>
30 #include <sys/wait.h>
31 #include <signal.h>
32 #include <poll.h>
33 #include <fcntl.h>
34 #include <sys/stat.h>
35 #include <errno.h>
36 #include <sys/socket.h>
37 #include <sys/un.h>
38 #include <pthread.h>
39
40
41
42 extern char **environ;
43
44 #define STR_BUFSIZE 512
45 #define WID_ARG_BUFSIZE 24
46
47 #define PLAYER_POLL_TIMEOUT 500
48 #define PLAYER_IN_BUFSIZE   8192
49
50 static void* start_player(void *data);
51
52 static void player_io(Player *p);
53
54 void PlayerOpenFile(MainWindow *win) {
55     pthread_t tid;
56     if(pthread_create(&tid, NULL, start_player, win)) {
57         perror("pthread_create");
58     }
59 }
60
61 static int prepare_player(Player *player, char *log_arg, char *ipc_arg) {
62     // create tmp directory for IPC
63     char *player_tmp = NULL;
64     char buf[STR_BUFSIZE];
65     snprintf(buf, STR_BUFSIZE, "/tmp/uwplayer-%x", rand());
66     int mkdir_success = 0;
67     for(int t=0;t<5;t++) {
68         if(!mkdir(buf, S_IRWXU)) {
69             mkdir_success = 1;
70             break;
71         } else if(errno != EEXIST) {
72             break;
73         }
74     }
75     if(!mkdir_success) return 1;
76     player_tmp = strdup(buf);
77     player->tmp = player_tmp;
78     
79     // prepare log/ipc args and create log fifo
80     int err = 0;
81     
82     if(snprintf(log_arg, STR_BUFSIZE, "--log-file=%s/%s", player_tmp, "log.fifo") >= STR_BUFSIZE) {
83         err = 1;
84     }
85     if(snprintf(ipc_arg, STR_BUFSIZE, "--input-ipc-server=%s/%s", player_tmp, "ipc.socket") >= STR_BUFSIZE) {
86         err = 1;
87     }
88     
89     snprintf(buf, STR_BUFSIZE, "%s/log.fifo", player_tmp);
90     if(err || mkfifo(buf, S_IRUSR|S_IWUSR)) {
91         rmdir(player_tmp);
92         return 1;
93     }
94     
95     return 0;
96 }
97
98 static int start_player_process(Player *player, MainWindow *win) {
99     char log_arg[STR_BUFSIZE];
100     char ipc_arg[STR_BUFSIZE];
101     
102     if(prepare_player(player, log_arg, ipc_arg)) {
103         return 1;
104     }
105     
106     char *player_bin = "/usr/local/bin/mpv"; // TODO: get bin from settings
107     
108     // -wid parameter value for embedding the player in the player_widget
109     Window wid = XtWindow(win->player_widget);
110     char wid_arg[WID_ARG_BUFSIZE];
111     if(snprintf(wid_arg, WID_ARG_BUFSIZE, "%lu", wid) >= WID_ARG_BUFSIZE) {
112         return 1;
113     }
114     
115     // create player arg list
116     char *args[32];
117     args[0] = player_bin;
118     args[1] = "-wid";
119     args[2] = wid_arg;
120     args[3] = "--no-terminal";
121     args[4] = log_arg;
122     args[5] = ipc_arg;
123     args[6] = win->file;
124     args[7] = NULL;
125     
126     posix_spawn_file_actions_t actions;
127     posix_spawn_file_actions_init(&actions);
128     
129     //posix_spawn_file_actions_adddup2(&actions, pin[0], STDIN_FILENO);
130     //posix_spawn_file_actions_adddup2(&actions, pout[1], STDOUT_FILENO);
131     
132     // start player
133     pid_t player_pid;
134     if(posix_spawn(&player_pid, player_bin, &actions, NULL, args, environ)) {
135         perror("posix_spawn");
136         return 1;
137     }
138     posix_spawn_file_actions_destroy(&actions);
139     
140     player->process = player_pid;
141     player->isactive = TRUE;
142     
143     return 0;
144 }
145
146 static int wait_for_ipc(Player *player) {
147     char buf[STR_BUFSIZE];
148     snprintf(buf, STR_BUFSIZE, "%s/log.fifo", player->tmp); // cannot fail
149     
150     // open log
151     int fd_log = open(buf, O_RDONLY);
152     if(fd_log < 0) {
153         perror("Cannot open log");
154         return 1;
155     }
156     player->log = fd_log;
157     
158     // read log until IPC socket is created
159     char *scan_str = "Listening to IPC";
160     int scan_pos = 0;
161     int scan_len = strlen(scan_str);
162     int ipc_listen = 0;
163     ssize_t r;
164     while((r = read(fd_log, buf, STR_BUFSIZE)) > 0) {
165         for(int i=0;i<r;i++) {
166             char c = buf[i];
167             char *s_str = buf+i;
168             
169             if(scan_pos == scan_len) {
170                 ipc_listen = 1;
171                 break;
172             }
173             
174             if(scan_str[scan_pos] == c) {
175                 scan_pos++;
176             } else {
177                 scan_pos = 0;
178             }
179         }
180         if(ipc_listen) break;
181     }
182     
183     return 0;
184 }
185
186 static int connect_to_ipc(Player *player) {
187     // connect to IPC socket
188     int fd_ipc = socket(AF_UNIX, SOCK_STREAM, 0);
189     if(fd_ipc < 0) {
190         perror("Cannot create IPC socket");
191         return 1;
192     }
193     player->ipc = fd_ipc;
194     
195     char buf[STR_BUFSIZE];
196     snprintf(buf, STR_BUFSIZE, "%s/%s", player->tmp, "ipc.socket"); // cannot fail
197     
198     struct sockaddr_un ipc_addr;
199     memset(&ipc_addr, 0, sizeof(struct sockaddr_un));
200     ipc_addr.sun_family = AF_UNIX;
201     memcpy(ipc_addr.sun_path, buf, strlen(buf));
202     if(connect(fd_ipc, (struct sockaddr *)&ipc_addr, sizeof(ipc_addr)) == -1) {
203         perror("Cannot connect to IPC socket");
204         return 1;
205     }
206     
207     return 0;
208 }
209
210 static void* start_player(void *data) {
211     MainWindow *win = data;
212     
213     Player *player = malloc(sizeof(Player));
214     memset(player, 0, sizeof(Player));
215     
216     // start mpv
217     if(start_player_process(player, win)) {
218         PlayerDestroy(player);
219         return NULL;
220     } 
221     
222     // wait until IPC socket is ready
223     if(wait_for_ipc(player)) {
224         PlayerDestroy(player);
225         return NULL;
226     }
227      
228     if(connect_to_ipc(player)) {
229         PlayerDestroy(player);
230         return NULL;
231     }
232     
233     // set player in main window
234     if(win->player) {
235         PlayerDestroy(win->player);
236     }
237     win->player = player;
238     
239     // IO
240     player_io(player);
241     
242     return NULL;
243 }
244
245 static void player_io(Player *p) {
246     int flags = fcntl(p->log, F_GETFL, 0);
247     fcntl(p->log, F_SETFL, flags | O_NONBLOCK);
248     
249     struct pollfd fds[2];
250     fds[0].fd = p->log;
251     fds[0].events = POLLIN;
252     fds[0].revents = 0;
253     fds[1].fd = p->ipc;
254     fds[1].events = POLLIN;
255     fds[1].revents = 0;
256     
257     char buf[PLAYER_IN_BUFSIZE];
258     while(poll(fds, 2, PLAYER_POLL_TIMEOUT)) {
259         if(fds[0].revents == POLLIN) {
260             // clean up fifo
261             read(fds[0].fd, buf, PLAYER_IN_BUFSIZE);
262         }
263         
264         if(fds[1].revents == POLLIN) {
265             ssize_t r;
266             if((r = read(fds[1].fd, buf, PLAYER_IN_BUFSIZE)) <= 0) {
267                 break;
268             }
269             
270             write(1, buf, r);
271             
272         }
273         
274         char *cmd = "{ \"command\": [\"get_property\", \"playback-time\"] }\n";
275         write(p->ipc, cmd, strlen(cmd));
276     }
277 }
278
279 void PlayerDestroy(Player *p) {
280     if(p->log >= 0) {
281         close(p->log);
282     }
283     if(p->ipc >= 0) {
284         close(p->ipc);
285     }
286     
287     if(p->tmp) {
288         free(p->tmp);
289     }
290     
291     if(p->isactive) {
292         kill(p->process, SIGTERM);
293     }
294     
295     free(p);
296 }
297