473f4227d5a6de91ea3af40837b0aa5c96363bbd
[isatis.git] / isatis-plugin / plugin.c
1 #include <npapi.h>
2 #include <npupp.h>
3
4 #include <string.h>
5 #include <unistd.h>
6 #include <errno.h>
7 #include <poll.h>
8 #include <fcntl.h>
9 #include <sys/wait.h>
10
11 #ifdef DEBUG
12 #include <stdio.h>
13 #define DBG(x) printf x
14 #else
15 #define DBG(x)
16 #endif
17
18 #define SUPPORTED_MIME_TYPES \
19         "application/ogg:ogg:OGG Multimedia;" \
20         "audio/ogg:ogg:OGG Audio;" \
21         "video/mpeg:mpg, mpeg, mpe:MPEG Video;" \
22         "video/quicktime:mov:QuickTime Video;" \
23         "video/x-ms-asf:asf:Advanced Systems Format;" \
24         "video/x-ms-wmv:wmv:Windows Media Video;"
25
26 struct plugin_private
27 {
28         int data_fd;
29         pid_t player_pid;
30         int has_stream;
31         char *uri;
32         unsigned long xid;
33 };
34
35 #define GET_PLUGIN_PRIVATE(i, x)           \
36         do {                                     \
37     if ((i) == NULL)                       \
38       return NPERR_INVALID_INSTANCE_ERROR; \
39                                            \
40     x = (i)->pdata;                        \
41         } while (0)
42
43 static NPNetscapeFuncs host_funcs;
44
45 static NPError NPP_SpawnPlayer(NPP instance)
46 {
47         int r;
48         int pipe_fds[2];
49         struct plugin_private *priv;
50
51         DBG(("NPP_SpawnPlayer(%p)\n", instance));
52
53         GET_PLUGIN_PRIVATE(instance, priv);
54
55         if (pipe(pipe_fds) < 0)
56         {
57                 DBG(("  pipe() failed: %s\n", strerror(errno)));
58                 return NPERR_GENERIC_ERROR;
59         }
60
61         r = fork();
62
63         switch (r)
64         {
65                 case 0:
66                         /* Child process */
67                         {
68                                 char xid_buf[32];
69                                 char uri_buf[32];
70                                 char *uri;
71
72                                 /* Close the input side of the pipe in the child process */
73                                 close(pipe_fds[1]);
74
75                                 snprintf(xid_buf, sizeof(xid_buf), "0x%lx", priv->xid);
76
77                                 if (priv->uri)
78                                         uri = priv->uri;
79                                 else
80                                 {
81                                         snprintf(uri_buf, sizeof(uri_buf), "fd://%d", pipe_fds[0]);
82
83                                         uri = uri_buf;
84                                 }
85
86                                 execl(BINDIR "/isatis-player",
87                                                 "isatis-player",
88                                                 "--xid",
89                                                 xid_buf,
90                                                 uri,
91                                                 NULL);
92
93                                 DBG(("exec() failed\n"));
94                                 _exit(1);
95                         }
96
97                 default:
98                         /* Parent process */
99                         break;
100
101                 case -1:
102                         /* Error */
103                         DBG(("  fork() failed: %s\n", strerror(errno)));
104                         return NPERR_GENERIC_ERROR;
105         }
106
107         DBG(("  child pid is %d\n", r));
108
109         /* Close the output side of the pipe in the parent process */
110         close(pipe_fds[0]);
111
112         fcntl(pipe_fds[1], F_SETFL, O_NONBLOCK);
113
114         priv->player_pid = r;
115         priv->data_fd = pipe_fds[1];
116
117         return NPERR_NO_ERROR;
118 }
119
120 NPError NPP_New(NPMIMEType type, NPP instance, uint16 mode, int16 argc, char *argn[], char *argv[], NPSavedData *saved)
121 {
122         struct plugin_private *priv;
123
124         DBG(("NPP_New(%p, \"%s\", %d)\n", instance, type, mode));
125
126         if (instance == NULL)
127                 return NPERR_INVALID_INSTANCE_ERROR;
128
129         instance->pdata = malloc(sizeof(struct plugin_private));
130
131         GET_PLUGIN_PRIVATE(instance, priv);
132
133         priv->data_fd = -1;
134         priv->player_pid = 0;
135         priv->has_stream = 0;
136         priv->uri = NULL;
137         priv->xid = 0;
138
139         return NPERR_NO_ERROR;
140 }
141
142 NPError NPP_Destroy(NPP instance, NPSavedData **save)
143 {
144         struct plugin_private *priv;
145
146         DBG(("NPP_Destroy(%p)\n", instance));
147
148         GET_PLUGIN_PRIVATE(instance, priv);
149
150         if (priv)
151         {
152                 if (priv->uri)
153                         free(priv->uri);
154
155                 if (priv->data_fd != -1)
156                         close(priv->data_fd);
157
158                 if (priv->player_pid)
159                 {
160                         int i;
161
162                         kill(priv->player_pid, SIGTERM);
163
164                         for (i=0; i<20; i++)
165                         {
166                                 /* We're done here if waitpid() fails (the child was already reaped by someone) or succeeds
167                                  * (the child exited and we just reaped it) we're done */
168                                 if (waitpid(priv->player_pid, NULL, WNOHANG) != 0)
169                                         break;
170
171                                 /* Sleep 10ms per iteration for maximal total delay of 200ms if the child refuses to exit */
172                                 usleep(10000);
173                         }
174                 }
175
176                 free(priv);
177         }
178
179         return NPERR_NO_ERROR;
180 }
181
182 NPError NPP_SetWindow(NPP instance, NPWindow *window)
183 {
184         struct plugin_private *priv;
185
186         DBG(("NPP_SetWindow(%p, %p)\n", instance, window));
187
188         GET_PLUGIN_PRIVATE(instance, priv);
189
190         DBG(("  window = %p pos = %d,%d dim = %dx%d\n", window->window, window->x, window->y, window->width, window->height));
191
192         /* Ignore repeated NPP_SetWindow() calls, we only want the XID of the window and that does not change */
193         if (priv->player_pid > 0)
194                 return NPERR_NO_ERROR;
195
196         priv->xid = (unsigned long)window->window;
197
198         /* Delay child startup until we get the stream we're going to play */
199         if (priv->has_stream)
200                 return NPP_SpawnPlayer(instance);
201
202         return NPERR_NO_ERROR;
203 }
204
205 NPError NPP_NewStream(NPP instance, NPMIMEType type, NPStream *stream, NPBool seekable, uint16 *stype)
206 {
207         struct plugin_private *priv;
208
209         DBG(("NPP_NewStream(%p, \"%s\", %p, %s, %d)\n", instance, type, stream, seekable ? "true" : "false", *stype));
210
211         GET_PLUGIN_PRIVATE(instance, priv);
212
213         if (priv->has_stream)
214         {
215                 DBG(("  browser is trying to send extra streams\n"));
216                 return NPERR_GENERIC_ERROR;
217         }
218
219         priv->has_stream = 1;
220
221         DBG(("  stream->url = %s\n", stream->url));
222
223         /* Shortcut file:// URIs to direct file access */
224         if (!strncmp("file://", stream->url, 7))
225         {
226                 priv->uri = strdup(stream->url);
227
228                 CallNPN_DestroyStreamProc(host_funcs.destroystream, instance, stream, NPRES_DONE);
229         }
230
231         *stype = NP_NORMAL;
232
233         /* If we already have the XID we can launch the player process now */
234         if (priv->xid)
235                 return NPP_SpawnPlayer(instance);
236
237         return NPERR_NO_ERROR;
238 }
239
240 NPError NPP_DestroyStream(NPP instance, NPStream *stream, NPReason reason)
241 {
242         struct plugin_private *priv;
243
244         DBG(("NPP_DestroyStream(%p, %p, %d)\n", instance, stream, reason));
245
246         GET_PLUGIN_PRIVATE(instance, priv);
247
248         /* Unfortunately we cannot pass the reason why the stream was closed to the player, oh well... */
249
250         if (priv->data_fd != -1)
251         {
252                 close(priv->data_fd);
253                 priv->data_fd = -1;
254         }
255
256         return NPERR_NO_ERROR;
257 }
258
259 int32_t NPP_WriteReady(NPP instance, NPStream *stream)
260 {
261         struct plugin_private *priv;
262         struct pollfd fds;
263
264         /* DBG(("NPP_WriteReady(%p, %p)\n", instance, stream)); */
265
266         GET_PLUGIN_PRIVATE(instance, priv);
267
268         fds.fd = priv->data_fd;
269         fds.events = POLLOUT | POLLERR | POLLHUP;
270
271         switch (poll(&fds, 1, 0))
272         {
273                 case 0:
274                         return 0;
275
276                 case 1:
277                         /* Note that this function isn't documented as being allowed to return -1 for failures;
278                          * thus we return success even if the child has died and fail in NPP_Write() instead */
279                         return 1024;
280
281                 default:
282                         DBG(("poll() failed: %s\n", strerror(errno)));
283                         break;
284         }
285
286         return 0;
287 }
288
289 int32_t NPP_Write(NPP instance, NPStream *stream, int32_t offset, int32_t len, void *buffer)
290 {
291         struct plugin_private *priv;
292
293         /* DBG(("NPP_Write(%p, %p, %d, %d, %p)\n", instance, stream, offset, len, buffer)); */
294
295         GET_PLUGIN_PRIVATE(instance, priv);
296
297         int r = write(priv->data_fd, buffer, len);
298
299         /* XXX What about EAGAIN? It might happen if PIPE_BUF is ridiculously small for some reason XXX */
300
301         if (r < 0)
302                 DBG(("write() failed: %s\n", strerror(errno)));
303
304         return r;
305 }
306
307 NPError NPP_GetValue(NPP instance, NPPVariable variable, void *value)
308 {
309         DBG(("NPP_GetValue(%p, %d)\n", instance, variable));
310
311         /* For simplicity's sake (and because the docs are very unclear on this) we handle both instanced
312          * and non-instanced value queries here */
313
314         switch (variable)
315         {
316                 case NPPVpluginNameString:
317                         *((char **)value) = "Isatis Multimedia Player";
318                         return NPERR_NO_ERROR;
319
320                 case NPPVpluginDescriptionString:
321                         *((char **)value) = "Isatis Multimedia Player Plugin " VERSION;
322                         return NPERR_NO_ERROR;
323
324                 case NPPVpluginNeedsXEmbed:
325                         *((NPBool *)value) = TRUE;
326                         return NPERR_NO_ERROR;
327
328                 default:
329                         return NPERR_INVALID_PARAM;
330         }
331
332         return NPERR_GENERIC_ERROR;
333 }
334
335 NPError NP_Initialize(NPNetscapeFuncs *host_vtable, NPPluginFuncs *plugin_vtable)
336 {
337         NPError err;
338         NPBool hasxembed = FALSE;
339
340         DBG(("NP_Initialize\n"));
341
342         if (host_vtable == NULL || plugin_vtable == NULL)
343                 return NPERR_INVALID_FUNCTABLE_ERROR;
344
345         if ((host_vtable->version >> 8) > NP_VERSION_MAJOR)
346                 return NPERR_INCOMPATIBLE_VERSION_ERROR;
347
348         if (host_vtable->size < sizeof(NPNetscapeFuncs) || plugin_vtable->size < sizeof(NPPluginFuncs))
349                 return NPERR_INVALID_FUNCTABLE_ERROR;
350
351         /* XXX Safe? XXX */
352         memset(plugin_vtable, 0, sizeof(NPPluginFuncs));
353
354         plugin_vtable->size          = sizeof(NPPluginFuncs);
355         plugin_vtable->version       = (NP_VERSION_MAJOR << 8) | NP_VERSION_MINOR;
356         plugin_vtable->newp          = NewNPP_NewProc(NPP_New);
357         plugin_vtable->destroy       = NewNPP_DestroyProc(NPP_Destroy);
358         plugin_vtable->setwindow     = NewNPP_SetWindowProc(NPP_SetWindow);
359         plugin_vtable->newstream     = NewNPP_NewStreamProc(NPP_NewStream);
360         plugin_vtable->destroystream = NewNPP_DestroyStreamProc(NPP_DestroyStream);
361         plugin_vtable->writeready    = NewNPP_WriteReadyProc(NPP_WriteReady);
362         plugin_vtable->write         = NewNPP_WriteProc(NPP_Write);
363         plugin_vtable->getvalue      = NewNPP_GetValueProc(NPP_GetValue);
364
365         memcpy(&host_funcs, host_vtable, sizeof(host_funcs));
366
367         /* Not sure if missing XEmbed should be fatal error; GTK seems quite unhappy without it, even though
368          * technically things should work (albeit with focus issues and such) */
369         err = CallNPN_GetValueProc(host_vtable->getvalue, NULL, NPNVSupportsXEmbedBool, &hasxembed);
370         if (err != NPERR_NO_ERROR || !hasxembed)
371         {
372                 DBG(("No XEmbed support\n"));
373                 return NPERR_INCOMPATIBLE_VERSION_ERROR;
374         }
375
376         return NPERR_NO_ERROR;
377 }
378
379 NPError NP_Shutdown(void)
380 {
381         DBG(("NP_Shutdown\n"));
382
383         return NPERR_NO_ERROR;
384 }
385
386 char *NP_GetMIMEDescription(void)
387 {
388         DBG(("NP_GetMIMEDescription\n"));
389
390         return SUPPORTED_MIME_TYPES;
391 }
392
393 NPError NP_GetValue(void *future, NPPVariable variable, void *value)
394 {
395         return NPP_GetValue(NULL, variable, value);
396 }