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