1 | /* |
2 | * Copyright © 2018 Benjamin Otte |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkmediafileprivate.h" |
23 | |
24 | #include "gtkdebug.h" |
25 | #include "gtkintl.h" |
26 | #include "gtkmodulesprivate.h" |
27 | #include "gtknomediafileprivate.h" |
28 | |
29 | /** |
30 | * GtkMediaFile: |
31 | * |
32 | * `GtkMediaFile` implements `GtkMediaStream` for files. |
33 | * |
34 | * This provides a simple way to play back video files with GTK. |
35 | * |
36 | * GTK provides a GIO extension point for `GtkMediaFile` implementations |
37 | * to allow for external implementations using various media frameworks. |
38 | * |
39 | * GTK itself includes implementations using GStreamer and ffmpeg. |
40 | */ |
41 | |
42 | typedef struct _GtkMediaFilePrivate GtkMediaFilePrivate; |
43 | |
44 | struct _GtkMediaFilePrivate |
45 | { |
46 | GFile *file; |
47 | GInputStream *input_stream; |
48 | }; |
49 | |
50 | enum { |
51 | PROP_0, |
52 | PROP_FILE, |
53 | PROP_INPUT_STREAM, |
54 | |
55 | N_PROPS, |
56 | }; |
57 | |
58 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
59 | |
60 | G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GtkMediaFile, gtk_media_file, GTK_TYPE_MEDIA_STREAM, |
61 | G_ADD_PRIVATE (GtkMediaFile)) |
62 | |
63 | #define GTK_MEDIA_FILE_WARN_NOT_IMPLEMENTED_METHOD(obj,method) \ |
64 | g_critical ("Media file of type '%s' does not implement GtkMediaFile::" # method, G_OBJECT_TYPE_NAME (obj)) |
65 | |
66 | static void |
67 | gtk_media_file_default_open (GtkMediaFile *self) |
68 | { |
69 | GTK_MEDIA_FILE_WARN_NOT_IMPLEMENTED_METHOD (self, open); |
70 | } |
71 | |
72 | static void |
73 | gtk_media_file_default_close (GtkMediaFile *self) |
74 | { |
75 | gtk_media_stream_stream_unprepared (self: GTK_MEDIA_STREAM (ptr: self)); |
76 | } |
77 | |
78 | static void |
79 | gtk_media_file_set_property (GObject *object, |
80 | guint prop_id, |
81 | const GValue *value, |
82 | GParamSpec *pspec) |
83 | |
84 | { |
85 | GtkMediaFile *self = GTK_MEDIA_FILE (ptr: object); |
86 | |
87 | switch (prop_id) |
88 | { |
89 | case PROP_FILE: |
90 | gtk_media_file_set_file (self, file: g_value_get_object (value)); |
91 | break; |
92 | |
93 | case PROP_INPUT_STREAM: |
94 | gtk_media_file_set_input_stream (self, stream: g_value_get_object (value)); |
95 | break; |
96 | |
97 | default: |
98 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
99 | break; |
100 | } |
101 | } |
102 | |
103 | static void |
104 | gtk_media_file_get_property (GObject *object, |
105 | guint prop_id, |
106 | GValue *value, |
107 | GParamSpec *pspec) |
108 | { |
109 | GtkMediaFile *self = GTK_MEDIA_FILE (ptr: object); |
110 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
111 | |
112 | switch (prop_id) |
113 | { |
114 | case PROP_FILE: |
115 | g_value_set_object (value, v_object: priv->file); |
116 | break; |
117 | |
118 | case PROP_INPUT_STREAM: |
119 | g_value_set_object (value, v_object: priv->input_stream); |
120 | break; |
121 | |
122 | default: |
123 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
124 | break; |
125 | } |
126 | } |
127 | |
128 | static void |
129 | gtk_media_file_dispose (GObject *object) |
130 | { |
131 | GtkMediaFile *self = GTK_MEDIA_FILE (ptr: object); |
132 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
133 | |
134 | g_clear_object (&priv->file); |
135 | g_clear_object (&priv->input_stream); |
136 | |
137 | G_OBJECT_CLASS (gtk_media_file_parent_class)->dispose (object); |
138 | } |
139 | |
140 | static void |
141 | gtk_media_file_class_init (GtkMediaFileClass *class) |
142 | { |
143 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
144 | |
145 | class->open = gtk_media_file_default_open; |
146 | class->close = gtk_media_file_default_close; |
147 | |
148 | gobject_class->set_property = gtk_media_file_set_property; |
149 | gobject_class->get_property = gtk_media_file_get_property; |
150 | gobject_class->dispose = gtk_media_file_dispose; |
151 | |
152 | /** |
153 | * GtkMediaFile:file: (attributes org.gtk.Property.get=gtk_media_file_get_file org.gtk.Property.set=gtk_media_file_set_file) |
154 | * |
155 | * The file being played back or %NULL if not playing a file. |
156 | */ |
157 | properties[PROP_FILE] = |
158 | g_param_spec_object (name: "file" , |
159 | P_("File" ), |
160 | P_("File being played back" ), |
161 | G_TYPE_FILE, |
162 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
163 | |
164 | /** |
165 | * GtkMediaFile:input-stream: (attributes org.gtk.Property.get=gtk_media_file_get_input_stream org.gtk.Property.set=gtk_media_file_set_input_stream) |
166 | * |
167 | * The stream being played back or %NULL if not playing a stream. |
168 | * |
169 | * This is %NULL when playing a file. |
170 | */ |
171 | properties[PROP_INPUT_STREAM] = |
172 | g_param_spec_object (name: "input-stream" , |
173 | P_("Input stream" ), |
174 | P_("Input stream being played back" ), |
175 | G_TYPE_INPUT_STREAM, |
176 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
177 | |
178 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
179 | } |
180 | |
181 | static void |
182 | gtk_media_file_init (GtkMediaFile *self) |
183 | { |
184 | } |
185 | |
186 | GIOExtension * |
187 | gtk_media_file_get_extension (void) |
188 | { |
189 | const char *extension_name; |
190 | GIOExtension *e; |
191 | GIOExtensionPoint *ep; |
192 | |
193 | GTK_NOTE (MODULES, g_print ("Looking up MediaFile extension\n" )); |
194 | |
195 | ep = g_io_extension_point_lookup (GTK_MEDIA_FILE_EXTENSION_POINT_NAME); |
196 | e = NULL; |
197 | |
198 | extension_name = g_getenv (variable: "GTK_MEDIA" ); |
199 | if (extension_name) |
200 | { |
201 | if (g_str_equal (v1: extension_name, v2: "help" )) |
202 | { |
203 | GList *l; |
204 | |
205 | g_print (format: "Supported arguments for GTK_MEDIA environment variable:\n" ); |
206 | |
207 | for (l = g_io_extension_point_get_extensions (extension_point: ep); l; l = l->next) |
208 | { |
209 | e = l->data; |
210 | |
211 | g_print (format: "%10s - %d\n" , g_io_extension_get_name (extension: e), g_io_extension_get_priority (extension: e)); |
212 | } |
213 | |
214 | e = NULL; |
215 | } |
216 | else |
217 | { |
218 | e = g_io_extension_point_get_extension_by_name (extension_point: ep, name: extension_name); |
219 | if (e == NULL) |
220 | { |
221 | g_warning ("Media extension \"%s\" from GTK_MEDIA environment variable not found." , extension_name); |
222 | } |
223 | } |
224 | } |
225 | |
226 | if (e == NULL) |
227 | { |
228 | GList *l = g_io_extension_point_get_extensions (extension_point: ep); |
229 | |
230 | if (l == NULL) |
231 | { |
232 | g_error ("GTK was run without any GtkMediaFile extension being present. This must not happen." ); |
233 | } |
234 | |
235 | e = l->data; |
236 | } |
237 | |
238 | return e; |
239 | } |
240 | |
241 | static GType |
242 | gtk_media_file_get_impl_type (void) |
243 | { |
244 | static GType impl_type = G_TYPE_NONE; |
245 | GIOExtension *e; |
246 | |
247 | if (G_LIKELY (impl_type != G_TYPE_NONE)) |
248 | return impl_type; |
249 | |
250 | e = gtk_media_file_get_extension (); |
251 | impl_type = g_io_extension_get_type (extension: e); |
252 | |
253 | GTK_NOTE (MODULES, g_print ("Using %s from \"%s\" extension\n" , g_type_name (impl_type), g_io_extension_get_name (e))); |
254 | |
255 | return impl_type; |
256 | } |
257 | |
258 | /** |
259 | * gtk_media_file_new: |
260 | * |
261 | * Creates a new empty media file. |
262 | * |
263 | * Returns: (type Gtk.MediaFile): a new `GtkMediaFile` |
264 | **/ |
265 | GtkMediaStream * |
266 | gtk_media_file_new (void) |
267 | { |
268 | return g_object_new (object_type: gtk_media_file_get_impl_type (), NULL); |
269 | } |
270 | |
271 | /** |
272 | * gtk_media_file_new_for_filename: |
273 | * @filename: (type filename): filename to open |
274 | * |
275 | * Creates a new media file for the given filename. |
276 | * |
277 | * This is a utility function that converts the given @filename |
278 | * to a `GFile` and calls [ctor@Gtk.MediaFile.new_for_file]. |
279 | * |
280 | * Returns: (type Gtk.MediaFile): a new `GtkMediaFile` playing @filename |
281 | */ |
282 | GtkMediaStream * |
283 | gtk_media_file_new_for_filename (const char *filename) |
284 | { |
285 | GtkMediaStream *result; |
286 | GFile *file; |
287 | |
288 | if (filename) |
289 | file = g_file_new_for_path (path: filename); |
290 | else |
291 | file = NULL; |
292 | |
293 | result = gtk_media_file_new_for_file (file); |
294 | |
295 | if (file) |
296 | g_object_unref (object: file); |
297 | |
298 | return result; |
299 | } |
300 | |
301 | /** |
302 | * gtk_media_file_new_for_resource: |
303 | * @resource_path: resource path to open |
304 | * |
305 | * Creates a new new media file for the given resource. |
306 | * |
307 | * This is a utility function that converts the given @resource |
308 | * to a `GFile` and calls [ctor@Gtk.MediaFile.new_for_file]. |
309 | * |
310 | * Returns: (type Gtk.MediaFile): a new `GtkMediaFile` playing @resource_path |
311 | */ |
312 | GtkMediaStream * |
313 | gtk_media_file_new_for_resource (const char *resource_path) |
314 | { |
315 | GtkMediaStream *result; |
316 | GFile *file; |
317 | |
318 | if (resource_path) |
319 | { |
320 | char *uri, *escaped; |
321 | |
322 | escaped = g_uri_escape_string (unescaped: resource_path, |
323 | G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE); |
324 | uri = g_strconcat (string1: "resource://" , escaped, NULL); |
325 | g_free (mem: escaped); |
326 | |
327 | file = g_file_new_for_uri (uri); |
328 | g_free (mem: uri); |
329 | } |
330 | else |
331 | { |
332 | file = NULL; |
333 | } |
334 | |
335 | result = gtk_media_file_new_for_file (file); |
336 | |
337 | if (file) |
338 | g_object_unref (object: file); |
339 | |
340 | return result; |
341 | } |
342 | |
343 | /** |
344 | * gtk_media_file_new_for_file: |
345 | * @file: The file to play |
346 | * |
347 | * Creates a new media file to play @file. |
348 | * |
349 | * Returns: (type Gtk.MediaFile): a new `GtkMediaFile` playing @file |
350 | */ |
351 | GtkMediaStream * |
352 | gtk_media_file_new_for_file (GFile *file) |
353 | { |
354 | g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL); |
355 | |
356 | return g_object_new (object_type: gtk_media_file_get_impl_type (), |
357 | first_property_name: "file" , file, |
358 | NULL); |
359 | } |
360 | |
361 | /** |
362 | * gtk_media_file_new_for_input_stream: |
363 | * @stream: The stream to play |
364 | * |
365 | * Creates a new media file to play @stream. |
366 | * |
367 | * If you want the resulting media to be seekable, |
368 | * the stream should implement the `GSeekable` interface. |
369 | * |
370 | * Returns: (type Gtk.MediaFile): a new `GtkMediaFile` |
371 | */ |
372 | GtkMediaStream * |
373 | gtk_media_file_new_for_input_stream (GInputStream *stream) |
374 | { |
375 | g_return_val_if_fail (stream == NULL || G_IS_INPUT_STREAM (stream), NULL); |
376 | |
377 | return g_object_new (object_type: gtk_media_file_get_impl_type (), |
378 | first_property_name: "input-stream" , stream, |
379 | NULL); |
380 | } |
381 | |
382 | static gboolean |
383 | gtk_media_file_is_open (GtkMediaFile *self) |
384 | { |
385 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
386 | |
387 | return priv->file || priv->input_stream; |
388 | } |
389 | |
390 | /** |
391 | * gtk_media_file_clear: |
392 | * @self: a `GtkMediaFile` |
393 | * |
394 | * Resets the media file to be empty. |
395 | */ |
396 | void |
397 | gtk_media_file_clear (GtkMediaFile *self) |
398 | { |
399 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
400 | |
401 | g_return_if_fail (GTK_IS_MEDIA_FILE (self)); |
402 | |
403 | if (!gtk_media_file_is_open (self)) |
404 | return; |
405 | |
406 | GTK_MEDIA_FILE_GET_CLASS (ptr: self)->close (self); |
407 | |
408 | if (priv->input_stream) |
409 | { |
410 | g_clear_object (&priv->input_stream); |
411 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_INPUT_STREAM]); |
412 | } |
413 | if (priv->file) |
414 | { |
415 | g_clear_object (&priv->file); |
416 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FILE]); |
417 | } |
418 | } |
419 | |
420 | /** |
421 | * gtk_media_file_set_filename: |
422 | * @self: a `GtkMediaFile` |
423 | * @filename: (type filename) (nullable): name of file to play |
424 | * |
425 | * Sets the `GtkMediaFile to play the given file. |
426 | * |
427 | * This is a utility function that converts the given @filename |
428 | * to a `GFile` and calls [method@Gtk.MediaFile.set_file]. |
429 | **/ |
430 | void |
431 | gtk_media_file_set_filename (GtkMediaFile *self, |
432 | const char *filename) |
433 | { |
434 | GFile *file; |
435 | |
436 | g_return_if_fail (GTK_IS_MEDIA_FILE (self)); |
437 | |
438 | if (filename) |
439 | file = g_file_new_for_path (path: filename); |
440 | else |
441 | file = NULL; |
442 | |
443 | gtk_media_file_set_file (self, file); |
444 | |
445 | if (file) |
446 | g_object_unref (object: file); |
447 | } |
448 | |
449 | /** |
450 | * gtk_media_file_set_resource: |
451 | * @self: a `GtkMediaFile` |
452 | * @resource_path: (nullable): path to resource to play |
453 | * |
454 | * Sets the `GtkMediaFile to play the given resource. |
455 | * |
456 | * This is a utility function that converts the given @resource_path |
457 | * to a `GFile` and calls [method@Gtk.MediaFile.set_file]. |
458 | */ |
459 | void |
460 | gtk_media_file_set_resource (GtkMediaFile *self, |
461 | const char *resource_path) |
462 | { |
463 | GFile *file; |
464 | |
465 | g_return_if_fail (GTK_IS_MEDIA_FILE (self)); |
466 | |
467 | if (resource_path) |
468 | { |
469 | char *uri, *escaped; |
470 | |
471 | escaped = g_uri_escape_string (unescaped: resource_path, |
472 | G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE); |
473 | uri = g_strconcat (string1: "resource://" , escaped, NULL); |
474 | g_free (mem: escaped); |
475 | |
476 | file = g_file_new_for_uri (uri); |
477 | g_free (mem: uri); |
478 | } |
479 | else |
480 | { |
481 | file = NULL; |
482 | } |
483 | |
484 | |
485 | gtk_media_file_set_file (self, file); |
486 | |
487 | if (file) |
488 | g_object_unref (object: file); |
489 | } |
490 | |
491 | /** |
492 | * gtk_media_file_set_file: (attributes org.gtk.Method.set_property=file) |
493 | * @self: a `GtkMediaFile` |
494 | * @file: (nullable): the file to play |
495 | * |
496 | * Sets the `GtkMediaFile` to play the given file. |
497 | * |
498 | * If any file is still playing, stop playing it. |
499 | */ |
500 | void |
501 | gtk_media_file_set_file (GtkMediaFile *self, |
502 | GFile *file) |
503 | { |
504 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
505 | |
506 | g_return_if_fail (GTK_IS_MEDIA_FILE (self)); |
507 | g_return_if_fail (file == NULL || G_IS_FILE (file)); |
508 | |
509 | if (file) |
510 | g_object_ref (file); |
511 | |
512 | g_object_freeze_notify (G_OBJECT (self)); |
513 | |
514 | gtk_media_file_clear (self); |
515 | |
516 | if (file) |
517 | { |
518 | priv->file = file; |
519 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FILE]); |
520 | |
521 | GTK_MEDIA_FILE_GET_CLASS (ptr: self)->open (self); |
522 | } |
523 | |
524 | g_object_thaw_notify (G_OBJECT (self)); |
525 | } |
526 | |
527 | /** |
528 | * gtk_media_file_get_file: (attributes org.gtk.Method.get_property=file) |
529 | * @self: a `GtkMediaFile` |
530 | * |
531 | * Returns the file that @self is currently playing from. |
532 | * |
533 | * When @self is not playing or not playing from a file, |
534 | * %NULL is returned. |
535 | * |
536 | * Returns: (nullable) (transfer none): The currently playing file |
537 | */ |
538 | GFile * |
539 | gtk_media_file_get_file (GtkMediaFile *self) |
540 | { |
541 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
542 | |
543 | g_return_val_if_fail (GTK_IS_MEDIA_FILE (self), NULL); |
544 | |
545 | return priv->file; |
546 | } |
547 | |
548 | /** |
549 | * gtk_media_file_set_input_stream: (attributes org.gtk.Method.set_property=input-stream) |
550 | * @self: a `GtkMediaFile` |
551 | * @stream: (nullable): the stream to play from |
552 | * |
553 | * Sets the `GtkMediaFile` to play the given stream. |
554 | * |
555 | * If anything is still playing, stop playing it. |
556 | * |
557 | * Full control about the @stream is assumed for the duration of |
558 | * playback. The stream will not be closed. |
559 | */ |
560 | void |
561 | gtk_media_file_set_input_stream (GtkMediaFile *self, |
562 | GInputStream *stream) |
563 | { |
564 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
565 | |
566 | g_return_if_fail (GTK_IS_MEDIA_FILE (self)); |
567 | g_return_if_fail (stream == NULL || G_IS_INPUT_STREAM (stream)); |
568 | |
569 | if (stream) |
570 | g_object_ref (stream); |
571 | |
572 | g_object_freeze_notify (G_OBJECT (self)); |
573 | |
574 | gtk_media_file_clear (self); |
575 | |
576 | if (stream) |
577 | { |
578 | priv->input_stream = stream; |
579 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_INPUT_STREAM]); |
580 | |
581 | GTK_MEDIA_FILE_GET_CLASS (ptr: self)->open (self); |
582 | } |
583 | |
584 | g_object_thaw_notify (G_OBJECT (self)); |
585 | } |
586 | |
587 | /** |
588 | * gtk_media_file_get_input_stream: (attributes org.gtk.Method.get_property=input-stream) |
589 | * @self: a `GtkMediaFile` |
590 | * |
591 | * Returns the stream that @self is currently playing from. |
592 | * |
593 | * When @self is not playing or not playing from a stream, |
594 | * %NULL is returned. |
595 | * |
596 | * Returns: (nullable) (transfer none): The currently playing stream |
597 | */ |
598 | GInputStream * |
599 | gtk_media_file_get_input_stream (GtkMediaFile *self) |
600 | { |
601 | GtkMediaFilePrivate *priv = gtk_media_file_get_instance_private (self); |
602 | |
603 | g_return_val_if_fail (GTK_IS_MEDIA_FILE (self), NULL); |
604 | |
605 | return priv->input_stream; |
606 | } |
607 | |
608 | void |
609 | gtk_media_file_extension_init (void) |
610 | { |
611 | GIOExtensionPoint *ep; |
612 | GIOModuleScope *scope; |
613 | char **paths; |
614 | int i; |
615 | |
616 | GTK_NOTE (MODULES, |
617 | g_print ("Registering extension point %s\n" , GTK_MEDIA_FILE_EXTENSION_POINT_NAME)); |
618 | |
619 | ep = g_io_extension_point_register (GTK_MEDIA_FILE_EXTENSION_POINT_NAME); |
620 | g_io_extension_point_set_required_type (extension_point: ep, GTK_TYPE_MEDIA_FILE); |
621 | |
622 | g_type_ensure (GTK_TYPE_NO_MEDIA_FILE); |
623 | |
624 | scope = g_io_module_scope_new (flags: G_IO_MODULE_SCOPE_BLOCK_DUPLICATES); |
625 | |
626 | paths = _gtk_get_module_path (type: "media" ); |
627 | for (i = 0; paths[i]; i++) |
628 | { |
629 | GTK_NOTE (MODULES, |
630 | g_print ("Scanning io modules in %s\n" , paths[i])); |
631 | g_io_modules_scan_all_in_directory_with_scope (dirname: paths[i], scope); |
632 | } |
633 | g_strfreev (str_array: paths); |
634 | |
635 | g_io_module_scope_free (scope); |
636 | |
637 | if (GTK_DEBUG_CHECK (MODULES)) |
638 | { |
639 | GList *list, *l; |
640 | |
641 | list = g_io_extension_point_get_extensions (extension_point: ep); |
642 | for (l = list; l; l = l->next) |
643 | { |
644 | GIOExtension *ext = l->data; |
645 | g_print (format: "extension: %s: type %s\n" , |
646 | g_io_extension_get_name (extension: ext), |
647 | g_type_name (type: g_io_extension_get_type (extension: ext))); |
648 | } |
649 | } |
650 | } |
651 | |