implement basic autoplay
[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 #include "main.h"
25
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <sys/fcntl.h>
30 #include <spawn.h>
31 #include <sys/wait.h>
32 #include <signal.h>
33 #include <poll.h>
34 #include <fcntl.h>
35 #include <sys/stat.h>
36 #include <errno.h>
37 #include <sys/socket.h>
38 #include <sys/un.h>
39 #include <pthread.h>
40
41 #include "json.h"
42 #include "utils.h"
43 #include "settings.h"
44
45 extern char **environ;
46
47 #define STR_BUFSIZE 512
48 #define WID_ARG_BUFSIZE 24
49
50 #define PLAYER_POLL_TIMEOUT 500
51 #define PLAYER_IN_BUFSIZE   8192
52
53 static void json_print(JSONValue *value, char *name, int indent);
54
55 static void* start_player(void *data);
56
57 static void player_io(Player *p);
58
59 static void handle_json_rpc_msg(Player *player, JSONValue *v);
60 static void handle_json_rpc_reqid(Player *player, JSONValue *v, int reqid);
61 static void handle_json_rpc_event(Player *player, JSONValue *v, JSONValue *event);
62
63 void PlayerOpenFile(MainWindow *win) {
64     pthread_t tid;
65     if(pthread_create(&tid, NULL, start_player, win)) {
66         perror("pthread_create");
67     }
68 }
69
70 static int prepare_player(Player *player, char *log_arg, char *ipc_arg) {
71     // create tmp directory for IPC
72     char *player_tmp = NULL;
73     char buf[STR_BUFSIZE];
74     snprintf(buf, STR_BUFSIZE, "/tmp/uwplayer-%x", rand());
75     int mkdir_success = 0;
76     for(int t=0;t<5;t++) {
77         if(!mkdir(buf, S_IRWXU)) {
78             mkdir_success = 1;
79             break;
80         } else if(errno != EEXIST) {
81             break;
82         }
83     }
84     if(!mkdir_success) return 1;
85     player_tmp = strdup(buf);
86     player->tmp = player_tmp;
87     
88     // prepare log/ipc args and create log fifo
89     int err = 0;
90     
91     if(snprintf(log_arg, STR_BUFSIZE, "--log-file=%s/%s", player_tmp, "log.fifo") >= STR_BUFSIZE) {
92         err = 1;
93     }
94     if(snprintf(ipc_arg, STR_BUFSIZE, "--input-ipc-server=%s/%s", player_tmp, "ipc.socket") >= STR_BUFSIZE) {
95         err = 1;
96     }
97     
98     snprintf(buf, STR_BUFSIZE, "%s/log.fifo", player_tmp);
99     if(err || mkfifo(buf, S_IRUSR|S_IWUSR)) {
100         rmdir(player_tmp);
101         return 1;
102     }
103     
104     return 0;
105 }
106
107 static void* wait_for_process(void *data) {
108     Player *player = data;
109     int status = 0;
110     waitpid(player->process, &status, 0);
111
112     player->isactive = FALSE;
113     player->status = status;
114     SetPlayerWindow(0);
115     
116     return NULL;
117 }
118
119 static int start_player_process(Player *player, MainWindow *win) {
120     char log_arg[STR_BUFSIZE];
121     char ipc_arg[STR_BUFSIZE];
122     
123     if(prepare_player(player, log_arg, ipc_arg)) {
124         return 1;
125     }
126     
127     char *player_bin = SettingsGetPlayerBin();
128     if(!player_bin) {
129         fprintf(stderr, "No mpv binary available\n");
130         return 1;
131     }
132     
133     // -wid parameter value for embedding the player in the player_widget
134     Window wid = XtWindow(win->player_widget);
135     char wid_arg[WID_ARG_BUFSIZE];
136     if(snprintf(wid_arg, WID_ARG_BUFSIZE, "%lu", wid) >= WID_ARG_BUFSIZE) {
137         return 1;
138     }
139     
140     // create player arg list
141     char *args[32];
142     args[0] = player_bin;
143     args[1] = "-wid";
144     args[2] = wid_arg;
145     args[3] = "--no-terminal";
146     args[4] = log_arg;
147     args[5] = ipc_arg;
148     args[6] = win->file;
149     args[7] = NULL;
150     
151     posix_spawn_file_actions_t actions;
152     posix_spawn_file_actions_init(&actions);
153     
154     //posix_spawn_file_actions_adddup2(&actions, pin[0], STDIN_FILENO);
155     //posix_spawn_file_actions_adddup2(&actions, pout[1], STDOUT_FILENO);
156     
157     // start player
158     pid_t player_pid;
159     if(posix_spawn(&player_pid, player_bin, &actions, NULL, args, environ)) {
160         perror("posix_spawn");
161         return 1;
162     }
163     posix_spawn_file_actions_destroy(&actions);
164     
165     player->process = player_pid;
166     player->isactive = TRUE;
167     
168     pthread_t tid;
169     if(pthread_create(&tid, NULL, wait_for_process, player)) {
170         perror("pthread_create");
171     }
172     
173     return 0;
174 }
175
176 static int wait_for_ipc(Player *player) {
177     char buf[STR_BUFSIZE];
178     snprintf(buf, STR_BUFSIZE, "%s/log.fifo", player->tmp); // cannot fail
179     
180     // open log
181     int fd_log = open(buf, O_RDONLY);
182     if(fd_log < 0) {
183         perror("Cannot open log");
184         return 1;
185     }
186     player->log = fd_log;
187     
188     // read log until IPC socket is created
189     char *scan_str = "Listening to IPC";
190     int scan_pos = 0;
191     int scan_len = strlen(scan_str);
192     int ipc_listen = 0;
193     ssize_t r;
194     while((r = read(fd_log, buf, STR_BUFSIZE)) > 0) {
195         for(int i=0;i<r;i++) {
196             char c = buf[i];
197             char *s_str = buf+i;
198             
199             if(scan_pos == scan_len) {
200                 ipc_listen = 1;
201                 break;
202             }
203             
204             if(scan_str[scan_pos] == c) {
205                 scan_pos++;
206             } else {
207                 scan_pos = 0;
208             }
209         }
210         if(ipc_listen) break;
211     }
212     
213     return 0;
214 }
215
216 static int connect_to_ipc(Player *player) {
217     // connect to IPC socket
218     int fd_ipc = socket(AF_UNIX, SOCK_STREAM, 0);
219     if(fd_ipc < 0) {
220         perror("Cannot create IPC socket");
221         return 1;
222     }
223     player->ipc = fd_ipc;
224     
225     char buf[STR_BUFSIZE];
226     snprintf(buf, STR_BUFSIZE, "%s/%s", player->tmp, "ipc.socket"); // cannot fail
227     
228     struct sockaddr_un ipc_addr;
229     memset(&ipc_addr, 0, sizeof(struct sockaddr_un));
230     ipc_addr.sun_family = AF_UNIX;
231     memcpy(ipc_addr.sun_path, buf, strlen(buf));
232     if(connect(fd_ipc, (struct sockaddr *)&ipc_addr, sizeof(ipc_addr)) == -1) {
233         perror("Cannot connect to IPC socket");
234         return 1;
235     }
236     
237     return 0;
238 }
239
240 static void* start_player(void *data) {
241     MainWindow *win = data;
242     
243     Player *player = malloc(sizeof(Player));
244     memset(player, 0, sizeof(Player));
245     
246     // start mpv
247     if(start_player_process(player, win)) {
248         PlayerDestroy(player);
249         return NULL;
250     } 
251     
252     // wait until IPC socket is ready
253     if(wait_for_ipc(player)) {
254         PlayerDestroy(player);
255         return NULL;
256     }
257     close(player->log);
258      
259     if(connect_to_ipc(player)) {
260         PlayerDestroy(player);
261         return NULL;
262     }
263     
264     // set player in main window
265     if(win->player) {
266         PlayerDestroy(win->player);
267     }
268     win->player = player;
269     
270     // IO
271     player_io(player);
272     
273     return NULL;
274 }
275
276 static void player_io(Player *p) {
277     struct pollfd fds[2];
278     fds[0].fd = p->ipc;
279     fds[0].events = POLLIN;
280     fds[0].revents = 0;
281     JSONParser *js = json_parser_new();
282     
283     char buf[PLAYER_IN_BUFSIZE];
284     while(p->isactive && poll(fds, 2, PLAYER_POLL_TIMEOUT)) {
285         if(fds[0].revents == POLLIN) {
286             ssize_t r;
287             if((r = read(fds[0].fd, buf, PLAYER_IN_BUFSIZE)) <= 0) {
288                 break;
289             }
290             //fwrite(buf, 1, r, stdout);
291             fflush(stdout);
292             json_parser_fill(js, buf, r);
293             
294             JSONValue *value;
295             int ret;
296             while((ret = json_read_value(js, &value)) == 1) {
297                 handle_json_rpc_msg(p, value);
298                 json_value_free(value);
299             }
300             
301             if(ret == -1) {
302                 fprintf(stderr, "JSON-RPC error\n");
303                 break;
304             }
305         }
306         
307         char *cmd = "{ \"command\": [\"get_property\", \"playback-time\"], request_id=\"" REQ_ID_PLAYBACK_TIME "\" }\n";
308         //write(p->ipc, cmd, strlen(cmd));
309     }
310     
311     
312     printf("PlayerEnd: %s\n", strerror(errno));
313     fflush(stdout);
314 }
315
316
317 static void handle_json_rpc_msg(Player *player, JSONValue *v) {
318     if(v->type != JSON_OBJECT) return;
319       
320     JSONValue *request_id_v = json_obj_get(&v->value.object, "request_id");
321     JSONValue *event = NULL;
322     if(request_id_v && request_id_v->type == JSON_STRING) {
323         int request_id = 0;
324         if(request_id_v->value.string.length == 2) {
325             request_id = 10 * (request_id_v->value.string.string[0] - '0') + (request_id_v->value.string.string[1] - '0');
326             handle_json_rpc_reqid(player, v, request_id);
327             return;
328         }
329     } else if ((event = json_obj_get(&v->value.object, "event")) != NULL) {
330         handle_json_rpc_event(player, v, event);
331     }
332     
333     //json_print(v, NULL, 0);
334 }
335
336 static Boolean player_widget_set_size(XtPointer data) {
337     Player *player = data;
338     MainWindow *win = GetMainWindow();
339         
340     Dimension win_width, win_height;
341     XtVaGetValues(win->window, XmNwidth, &win_width, XmNheight, &win_height, NULL);
342     Dimension player_width, player_height;
343     XtVaGetValues(win->player_widget, XmNwidth, &player_width, XmNheight, &player_height, NULL);
344
345     Dimension new_width = player->width + win_width - player_width;
346     Dimension new_height = player->height + win_height - player_height;
347     
348     // set window size
349     XtVaSetValues(win->window, XmNwidth, new_width, XmNheight, new_height, NULL);
350     
351     // set window aspect ratio
352     XSizeHints hints;
353     hints.flags = PAspect;
354     hints.min_aspect.x = new_width;
355     hints.min_aspect.y = new_height;
356     hints.max_aspect.x = new_width;
357     hints.max_aspect.y = new_height;
358     XSetWMNormalHints(XtDisplay(win->window), XtWindow(win->window), &hints);
359     
360     return 0;
361 }
362
363
364
365 static void player_set_size(Player *player, int width, int height) {
366     if(width >= 0) {
367         player->width = width;
368     }
369     if(height >= 0) {
370         player->height = height;
371     }
372     if(player->width > 0 && player->height > 0) {
373         AppExecProc(player_widget_set_size, player);
374     }
375 }
376
377 static void handle_json_rpc_reqid(Player *player, JSONValue *v, int reqid) {
378     JSONValue *data = json_obj_get(&v->value.object, "data");
379     if(!data) return;
380     
381     switch(reqid) {
382         case REQ_ID_PLAYBACK_TIME_INT: {
383             if(data->type == JSON_NUMBER) {
384                 player->playback_time = data->value.number.value;
385             }
386             break;
387         }
388         case REQ_ID_WIDTH_INT: {
389             if(data->type == JSON_INTEGER) {
390                 player_set_size(player, data->value.integer.value, -1);
391             }
392             break;
393         }
394         case REQ_ID_HEIGHT_INT: {
395             if(data->type == JSON_INTEGER) {
396                 player_set_size(player, -1, data->value.integer.value);
397             }
398             break;
399         }
400     }
401 }
402
403 static Boolean get_player_window(XtPointer data) {
404     Player *p = data;
405     MainWindow *win = GetMainWindow();
406     
407     Widget player_wid = win->player_widget;
408     Window root, parent;
409     Window *child;
410     unsigned int nchild;
411     XQueryTree(XtDisplay(player_wid), XtWindow(player_wid), &root, &parent, &child, &nchild);
412     if(nchild > 0) {
413         p->window = child[0];
414         XFree(child);
415         
416         SetPlayerWindow(p->window);
417         XSelectInput(XtDisplay(win->player_widget), p->window, PointerMotionMask);
418     }
419     
420     return 0;
421 }
422
423 #define CURSOR_AUTOHIDE_THRESHOLD_SEC  4
424
425 static Boolean hide_cursor(XtPointer data) {
426     MainWindow *win = data;
427     WindowHidePlayerCursor(win);
428     return 0;
429 }
430
431 static void check_hide_cursor(Player *p) {
432     MainWindow *win = GetMainWindow();
433     if(win->cursorhidden) return;
434     
435     if(p->playback_time - win->motion_playback_time > CURSOR_AUTOHIDE_THRESHOLD_SEC) {
436         AppExecProc(hide_cursor, win);
437     }
438 }
439
440 static void handle_json_rpc_event(Player *p, JSONValue *v, JSONValue *event) {
441     if(!json_strcmp(event, "property-change")) {
442         JSONValue *name = json_obj_get(&v->value.object, "name");
443         JSONValue *data = json_obj_get(&v->value.object, "data");
444         if(!json_strcmp(name, "playback-time")) {
445             if(data && data->type == JSON_NUMBER) {
446                 p->playback_time = data->value.number.value;
447                 //printf("playback-time: %f\n", p->playback_time);
448                 check_hide_cursor(p);
449             }
450         } else if(!json_strcmp(name, "eof-reached")) {
451             if(data && data->type == JSON_LITERAL && data->value.literal.literal == JSON_TRUE) {
452                 PlayerEOF(p);
453             }
454         } else if(!json_strcmp(name, "osd-height")) {
455             if(data->type == JSON_NUMBER) {
456                 p->osd_height = data->value.number.value;
457             }
458         }
459     } else if(!p->isstarted && !json_strcmp(event, "playback-restart")) {
460         char *cmd = "{ \"command\": [\"observe_property\", 1, \"playback-time\"] }\n"
461                     "{ \"command\": [\"observe_property\", 1, \"eof-reached\"] }\n"
462                     "{ \"command\": [\"observe_property\", 1, \"osd-height\"] }\n"
463                     "{ \"command\": [\"get_property\", \"width\"], request_id=\"" REQ_ID_WIDTH "\" }\n"
464                     "{ \"command\": [\"get_property\", \"height\"], request_id=\"" REQ_ID_HEIGHT "\" }\n"
465                     "{ \"command\": [\"set_property\", \"keep-open\", true] }\n";
466         write(p->ipc, cmd, strlen(cmd));
467         p->isstarted = TRUE;
468         
469         AppExecProc(get_player_window, p);
470     }
471 }
472
473 void PlayerDestroy(Player *p) {
474     if(p->log >= 0) {
475         close(p->log);
476     }
477     if(p->ipc >= 0) {
478         close(p->ipc);
479     }
480     
481     if(p->tmp) {
482         free(p->tmp);
483     }
484     
485     if(p->isactive) {
486         kill(p->process, SIGTERM);
487     }
488     
489     SetPlayerWindow(0);
490     free(p);
491 }
492
493
494 static void json_print(JSONValue *value, char *name, int indent) {
495     if(name) {
496         printf("%*s%s: ", indent*4, "", name);
497     } else {
498         printf("%*s", indent*4, "");
499     }
500     
501     
502     switch(value->type) {
503         case JSON_OBJECT: {
504             printf("{\n");
505             
506             for(int i=0;i<value->value.object.size;i++) {
507                 JSONObjValue val = value->value.object.values[i];
508                 json_print(val.value, val.name, indent+1);
509                 if(i+1 < value->value.object.size) {
510                     printf(",\n");
511                 } else {
512                     printf("\n");
513                 }
514             }
515             
516             printf("%*s}", indent*4, "");
517             break;
518         }
519         case JSON_ARRAY: {
520             printf("[\n");
521             
522             for(int i=0;i<value->value.object.size;i++) {
523                 JSONValue *v = value->value.array.array[i];
524                 json_print(v, NULL, indent+1);
525                 if(i+1 < value->value.array.size) {
526                     printf(",\n");
527                 } else {
528                     printf("\n");
529                 }
530             }
531             
532             printf("%*s]", indent*4, "");
533             break;
534         }
535         case JSON_STRING: {
536             printf("\"%s\"", value->value.string.string);
537             break;
538         }
539         case JSON_INTEGER: {
540             printf("%i", (int)value->value.integer.value);
541             break;
542         }
543         case JSON_NUMBER: {
544             printf("%f", value->value.number.value);
545             break;
546         }
547         case JSON_LITERAL: {
548             char *lit = "NULL";
549             switch(value->value.literal.literal) {
550                 case JSON_NULL: break;
551                 case JSON_TRUE: lit = "true"; break;
552                 case JSON_FALSE: lit = "false"; break;
553             }
554             printf("%s\n", lit);
555             break;
556         }
557     }
558     
559     if(indent == 0) {
560         putchar('\n');
561     }
562 }
563
564 static Boolean open_next_file(XtPointer data) {
565     char *file = data;
566     MainWindow *win = GetMainWindow();
567     if(win->file) {
568         free(file);
569     }
570     win->file = file;
571     PlayerOpenFile(win);
572     return 0;
573 }
574
575 void PlayerEOF(Player *p) {
576     MainWindow *win = GetMainWindow();
577     if(win->repeatTrack) {
578         char *cmd = "{ \"command\": [\"set_property\", \"playback-time\", 0] }\n";
579         write(p->ipc, cmd, strlen(cmd));
580     } else if(win->autoplayFolder) {
581         char *next_file = util_find_next_file(win->file);
582         if(next_file) {
583             AppExecProc(open_next_file, next_file);
584         }
585     }
586 }
587
588 void PlayerHandleInput(MainWindow *win, Player *p, XmDrawingAreaCallbackStruct *cb) {
589     
590 }