9e8408bfee0455c1becbbbb97961d3dee148e9e4
[profile/ivi/rygel.git] / src / librygel-renderer-gst / rygel-playbin-player.vala
1 /*
2  * Copyright (C) 2008 OpenedHand Ltd.
3  * Copyright (C) 2009,2010,2011,2012 Nokia Corporation.
4  * Copyright (C) 2012 Openismus GmbH
5  * Copyright (C) 2012,2013 Intel Corporation.
6  * Copyright (C) 2013  Cable Television Laboratories, Inc.
7  *
8  * Author: Jorn Baayen <jorn@openedhand.com>
9  *         Zeeshan Ali (Khattak) <zeeshanak@gnome.org>
10  *                               <zeeshan.ali@nokia.com>
11  *         Jens Georg <jensg@openismus.com>
12  *         Neha Shanbhag <N.Shanbhag@cablelabs.com>
13  *         Sivakumar Mani <siva@orexel.com>
14  *
15  * Rygel is free software; you can redistribute it and/or modify
16  * it under the terms of the GNU Lesser General Public License as published by
17  * the Free Software Foundation; either version 2 of the License, or
18  * (at your option) any later version.
19  *
20  * Rygel is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU Lesser General Public License for more details.
24  *
25  * You should have received a copy of the GNU Lesser General Public License
26  * along with this program; if not, write to the Free Software Foundation,
27  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
28  */
29
30 using Gst;
31 using GUPnP;
32
33 /**
34  * Implementation of RygelMediaPlayer for GStreamer.
35  *
36  * This class is useful only when implementing Rygel plugins.
37  */
38 public class Rygel.Playbin.Player : GLib.Object, Rygel.MediaPlayer {
39     private const string TRANSFER_MODE_STREAMING = "Streaming";
40     private const string TRANSFER_MODE_INTERACTIVE = "Interactive";
41     private const string PROTOCOL_INFO_TEMPLATE = "http-get:%s:*:%s";
42
43     private const string[] protocols = { "http-get", "rtsp" };
44     private const string[] mime_types = {
45                                         "audio/mpeg",
46                                         "application/ogg",
47                                         "audio/x-vorbis",
48                                         "audio/x-vorbis+ogg",
49                                         "audio/ogg",
50                                         "audio/x-ms-wma",
51                                         "audio/x-ms-asf",
52                                         "audio/x-flac",
53                                         "audio/x-flac+ogg",
54                                         "audio/flac",
55                                         "audio/mp4",
56                                         "audio/3gpp",
57                                         "audio/vnd.dlna.adts",
58                                         "audio/x-mod",
59                                         "audio/x-wav",
60                                         "audio/x-ac3",
61                                         "audio/x-m4a",
62                                         "audio/l16;rate=44100;channels=2",
63                                         "audio/l16;rate=44100;channels=1",
64                                         "audio/l16;channels=2;rate=44100",
65                                         "audio/l16;channels=1;rate=44100",
66                                         "audio/l16;rate=44100",
67                                         "image/jpeg",
68                                         "image/png",
69                                         "video/x-theora",
70                                         "video/x-theora+ogg",
71                                         "video/x-oggm",
72                                         "video/ogg",
73                                         "video/x-dirac",
74                                         "video/x-wmv",
75                                         "video/x-wma",
76                                         "video/x-msvideo",
77                                         "video/x-3ivx",
78                                         "video/x-3ivx",
79                                         "video/x-matroska",
80                                         "video/x-mkv",
81                                         "video/mpeg",
82                                         "video/mp4",
83                                         "application/x-shockwave-flash",
84                                         "video/x-ms-asf",
85                                         "video/x-xvid",
86                                         "video/x-ms-wmv" };
87     private static Player player;
88
89     public dynamic Element playbin { get; private set; }
90
91     private string _playback_state = "NO_MEDIA_PRESENT";
92     public string playback_state {
93         owned get {
94             return this._playback_state;
95         }
96
97         set {
98             Gst.State state, pending;
99
100             this.playbin.get_state (out state, out pending, Gst.MSECOND);
101
102             debug ("Changing playback state to %s.", value);
103
104             switch (value) {
105                 case "STOPPED":
106                     if (state != State.NULL || pending != State.VOID_PENDING) {
107                         this._playback_state = "TRANSITIONING";
108                         this.playbin.set_state (State.NULL);
109                     } else {
110                         this._playback_state = value;
111                     }
112                 break;
113                 case "PAUSED_PLAYBACK":
114                     if (state != State.PAUSED || pending != State.VOID_PENDING) {
115                         this._playback_state = "TRANSITIONING";
116                         this.playbin.set_state (State.PAUSED);
117                     } else {
118                         this._playback_state = value;
119                     }
120                 break;
121                 case "PLAYING":
122                     if (this._new_playback_speed != this._playback_speed &&
123                         (state == State.PLAYING || state == State.PAUSED) &&
124                         pending == State.VOID_PENDING) {
125                         /* already playing, but play speed has changed */
126                         this._playback_state = "TRANSITIONING";
127                         this.seek (this.position);
128                     } else if (state != State.PLAYING ||
129                                pending != State.VOID_PENDING) {
130                         // This needs a check if GStreamer and DLNA agree on
131                         // the "liveness" of the source (s0/sn increase in
132                         // protocol info)
133                         this._playback_state = "TRANSITIONING";
134                         this.playbin.set_state (State.PLAYING);
135                     } else {
136                         this._playback_state = value;
137                     }
138                 break;
139                 case "EOS":
140                     this._playback_state = value;
141                 break;
142                 default:
143                 break;
144             }
145         }
146     }
147
148     private string[] _allowed_playback_speeds = {
149         "1/16", "1/8", "1/4", "1/2", "1", "2", "4", "8", "16", "32", "64"
150     };
151     public string[] allowed_playback_speeds {
152         owned get {
153             return this._allowed_playback_speeds;
154         }
155     }
156
157     /**
158      * Actual _playback_speed is updated when playbin seek succeeds.
159      * Until that point, the playback speed set via api is stored in
160      * _new_playback_speed.
161      **/
162     private string _new_playback_speed = "1";
163
164     private string _playback_speed = "1";
165     public string playback_speed {
166         owned get {
167             return this._playback_speed;
168         }
169
170         set {
171             this._new_playback_speed = value;
172             /* theoretically we should trigger a seek here if we were
173              * playing already, but playback state does get changed
174              * after this when "Play" is invoked... */
175         }
176     }
177
178     private string transfer_mode = null;
179
180     private bool uri_update_hint = false;
181     private string? _uri = null;
182     public string? uri {
183         owned get {
184             return _uri;
185         }
186
187         set {
188             this._uri = value;
189             this.playbin.set_state (State.READY);
190             this.playbin.uri = value;
191             if (value != "") {
192                 switch (this._playback_state) {
193                     case "NO_MEDIA_PRESENT":
194                         this._playback_state = "STOPPED";
195                         this.notify_property ("playback-state");
196                         break;
197                     case "STOPPED":
198                         break;
199                     case "PAUSED_PLAYBACK":
200                         this.playbin.set_state (State.PAUSED);
201                         break;
202                     case "EOS":
203                     case "PLAYING":
204                         this.playbin.set_state (State.PLAYING);
205                         break;
206                     default:
207                         break;
208                 }
209             } else {
210                 this._playback_state = "NO_MEDIA_PRESENT";
211                 this.notify_property ("playback-state");
212             }
213             debug ("URI set to %s.", value);
214         }
215     }
216
217     private string _mime_type = "";
218     public string? mime_type {
219         owned get {
220             return this._mime_type;
221         }
222
223         set {
224             this._mime_type = value;
225         }
226     }
227
228     private string _metadata = "";
229     public string? metadata {
230         owned get {
231             return this._metadata;
232         }
233
234         set {
235             this._metadata = value;
236         }
237     }
238
239     public bool can_seek {
240         get {
241             return this.transfer_mode != TRANSFER_MODE_INTERACTIVE &&
242                    ! this.mime_type.has_prefix ("image/");
243         }
244     }
245
246     public bool can_seek_bytes {
247         get {
248             return this.transfer_mode != TRANSFER_MODE_INTERACTIVE &&
249                    ! this.mime_type.has_prefix ("image/");
250         }
251     }
252
253     private string _content_features = "";
254     private ProtocolInfo protocol_info;
255     public string? content_features {
256         owned get {
257             return this._content_features;
258         }
259
260         set {
261             var pi_string = PROTOCOL_INFO_TEMPLATE.printf (this.mime_type,
262                                                            value);
263             try {
264                 this.protocol_info = new ProtocolInfo.from_string (pi_string);
265                 var flags = this.protocol_info.dlna_flags;
266                 if (DLNAFlags.INTERACTIVE_TRANSFER_MODE in flags) {
267                     this.transfer_mode = TRANSFER_MODE_INTERACTIVE;
268                 } else if (DLNAFlags.STREAMING_TRANSFER_MODE in flags) {
269                     this.transfer_mode = TRANSFER_MODE_STREAMING;
270                 } else {
271                     this.transfer_mode = null;
272                 }
273             } catch (Error error) {
274                 this.protocol_info = null;
275                 this.transfer_mode = null;
276             }
277             this._content_features = value;
278         }
279     }
280
281     public double volume {
282         get {
283             return this.playbin.volume;
284         }
285
286         set {
287             this.playbin.volume = value;
288             debug ("volume set to %f.", value);
289         }
290     }
291
292     public int64 duration {
293         get {
294             int64 dur=0;
295
296             if (this.playbin.source.query_duration (Format.TIME, out dur)) {
297                 return dur / Gst.USECOND;
298             } else {
299                 return 0;
300             }
301         }
302     }
303
304     public int64 size {
305         get {
306             int64 dur;
307
308             if (this.playbin.source.query_duration (Format.BYTES, out dur)) {
309                 return dur;
310             } else {
311                 return 0;
312             }
313         }
314     }
315
316     public int64 position {
317         get {
318             int64 pos;
319
320             if (this.playbin.source.query_position (Format.TIME, out pos)) {
321                 return pos / Gst.USECOND;
322             } else {
323                 return 0;
324             }
325         }
326     }
327
328     public int64 byte_position {
329        get {
330             int64 pos;
331
332             if (this.playbin.source.query_position (Format.BYTES, out pos)) {
333                 return pos;
334             } else {
335                 return 0;
336             }
337         }
338     }
339
340     private Player () {
341         this.playbin = ElementFactory.make ("playbin", null);
342         this.setup_playbin ();
343     }
344
345     [Deprecated (since="0.21.5")]
346     public Player.wrap (Gst.Element playbin)
347                         requires (playbin.get_type ().name () == "GstPlayBin") {
348         this.playbin = playbin;
349         this.setup_playbin ();
350     }
351
352     public static Player get_default () {
353         if (player == null) {
354             player = new Player ();
355         }
356
357         return player;
358     }
359
360     private bool seek_with_format (Format format, int64 target) {
361         bool seeked;
362
363         var speed = this.play_speed_to_double (this._new_playback_speed);
364         if (speed > 0) {
365             seeked = this.playbin.seek (speed,
366                                         format,
367                                         SeekFlags.FLUSH | SeekFlags.SKIP | SeekFlags.ACCURATE,
368                                         Gst.SeekType.SET,
369                                         target,
370                                         Gst.SeekType.NONE,
371                                         -1);
372         } else {
373             seeked = this.playbin.seek (speed,
374                                         format,
375                                         SeekFlags.FLUSH | SeekFlags.SKIP | SeekFlags.ACCURATE,
376                                         Gst.SeekType.SET,
377                                         0,
378                                         Gst.SeekType.SET,
379                                         target);
380         }
381         if (seeked) {
382             this._playback_speed = this._new_playback_speed;
383         }
384
385         return seeked;
386     }
387
388     public bool seek (int64 time) {
389         debug ("Seeking %lld usec, play speed %s", time, this._new_playback_speed);
390
391         // Playbin doesn't return false when seeking beyond the end of the
392         // file
393         if (time > this.duration) {
394             return false;
395         }
396
397         return this.seek_with_format (Format.TIME, time * Gst.USECOND);
398     }
399
400     public bool seek_bytes (int64 bytes) {
401         debug ("Seeking %lld bytes, play speed %s", bytes, this._new_playback_speed);
402
403         int64 size = this.size;
404         if (size > 0 && bytes > size) {
405             return false;
406         }
407
408         return this.seek_with_format (Format.BYTES, bytes);
409     }
410
411     public string[] get_protocols () {
412         return protocols;
413     }
414
415     public string[] get_mime_types () {
416         return mime_types;
417     }
418
419     private GLib.List<DLNAProfile> _supported_profiles;
420     public unowned GLib.List<DLNAProfile> supported_profiles {
421         get {
422             if (_supported_profiles == null) {
423                 // FIXME: Check available decoders in registry and register
424                 // profiles after that
425                 _supported_profiles = new GLib.List<DLNAProfile> ();
426
427                 // Image
428                 _supported_profiles.prepend (new DLNAProfile ("JPEG_SM",
429                                                               "image/jpeg"));
430                 _supported_profiles.prepend (new DLNAProfile ("JPEG_MED",
431                                                               "image/jpeg"));
432                 _supported_profiles.prepend (new DLNAProfile ("JPEG_LRG",
433                                                               "image/jpeg"));
434                 _supported_profiles.prepend (new DLNAProfile ("PNG_LRG",
435                                                               "image/png"));
436
437                 // Audio
438                 _supported_profiles.prepend (new DLNAProfile ("MP3",
439                                                               "audio/mpeg"));
440                 _supported_profiles.prepend (new DLNAProfile ("MP3X",
441                                                               "audio/mpeg"));
442                 _supported_profiles.prepend (new DLNAProfile
443                                         ("AAC_ADTS_320",
444                                          "audio/vnd.dlna.adts"));
445                 _supported_profiles.prepend (new DLNAProfile ("AAC_ISO_320",
446                                                               "audio/mp4"));
447                 _supported_profiles.prepend (new DLNAProfile ("AAC_ISO_320",
448                                                               "audio/3gpp"));
449                 _supported_profiles.prepend (new DLNAProfile
450                                         ("LPCM",
451                                          "audio/l16;rate=44100;channels=2"));
452                 _supported_profiles.prepend (new DLNAProfile
453                                         ("LPCM",
454                                          "audio/l16;rate=44100;channels=1"));
455                 _supported_profiles.prepend (new DLNAProfile ("WMABASE",
456                                                               "audio/x-ms-wma"));
457                 _supported_profiles.prepend (new DLNAProfile ("WMAFULL",
458                                                               "audio/x-ms-wma"));
459                 _supported_profiles.prepend (new DLNAProfile ("WMAPRO",
460                                                               "audio/x-ms-wma"));
461
462                 // Video
463                 _supported_profiles.prepend (new DLNAProfile
464                                         ("MPEG_TS_SD_EU_ISO",
465                                          "video/mpeg"));
466                 _supported_profiles.prepend (new DLNAProfile
467                                         ("MPEG_TS_SD_NA_ISO",
468                                          "video/mpeg"));
469                 _supported_profiles.prepend (new DLNAProfile
470                                         ("MPEG_TS_HD_NA_ISO",
471                                          "video/mpeg"));
472                 _supported_profiles.prepend (new DLNAProfile
473                                         ("AVC_MP4_BL_CIF15_AAC_520",
474                                          "video/mp4"));
475             }
476
477             return _supported_profiles;
478         }
479     }
480
481     private bool is_rendering_image () {
482         dynamic Element typefind;
483
484         typefind = (this.playbin as Gst.Bin).get_by_name ("typefind");
485         Caps caps = typefind.caps;
486         unowned Structure structure = caps.get_structure (0);
487
488         return structure.get_name () == "image/jpeg" ||
489                structure.get_name () == "image/png";
490     }
491
492     private void bus_handler (Gst.Bus bus,
493                               Message message) {
494         switch (message.type) {
495         case MessageType.DURATION_CHANGED:
496             if (this.playbin.query_duration (Format.TIME, null)) {
497                 this.notify_property ("duration");
498             }
499         break;
500         case MessageType.STATE_CHANGED:
501             if (message.src == this.playbin) {
502                 State old_state, new_state, pending;
503
504                 message.parse_state_changed (out old_state,
505                                              out new_state,
506                                              out pending);
507                 if (old_state == State.READY && new_state == State.PAUSED) {
508                     if (this.uri_update_hint) {
509                         this.uri_update_hint = false;
510                         string uri = this.playbin.current_uri;
511                         if (this._uri != uri && uri != "") {
512                             // uri changed externally
513                             this._uri = this.playbin.uri;
514                             this.notify_property ("uri");
515                             this.metadata = this.generate_basic_didl ();
516                         }
517                     }
518                 }
519
520                 if (pending == State.VOID_PENDING) {
521                     switch (new_state) {
522                         case State.PAUSED:
523                             this.playback_state = "PAUSED_PLAYBACK";
524                             break;
525                         case State.NULL:
526                             this.playback_state = "STOPPED";
527                             break;
528                         case State.PLAYING:
529                             this.playback_state = "PLAYING";
530                             break;
531                         default:
532                             break;
533                     }
534                 }
535
536                 if (old_state == State.PAUSED && new_state == State.PLAYING) {
537                     this.playback_state = "PLAYING";
538                 }
539             }
540             break;
541         case MessageType.EOS:
542             if (!this.is_rendering_image ()) {
543                 debug ("EOS");
544                 this.playback_state = "EOS";
545             } else {
546                 debug ("Content is image, ignoring EOS");
547             }
548
549             break;
550         case MessageType.ERROR:
551             Error error;
552             string debug_message;
553
554             message.parse_error (out error, out debug_message);
555
556             warning ("Error from GStreamer element %s: %s (%s)",
557                      this.playbin.name,
558                      error.message,
559                      debug_message);
560             warning ("Going to STOPPED state");
561
562             this.playback_state = "STOPPED";
563
564             break;
565         }
566     }
567
568     private void on_source_setup (Element pipeline, dynamic Element source) {
569         if (source.get_type ().name () == "GstSoupHTTPSrc" &&
570             this.transfer_mode != null) {
571             debug ("Setting transfer mode to %s", this.transfer_mode);
572
573             var structure = new Structure.empty ("HTTPHeaders");
574             structure.set_value ("transferMode.dlna.org", this.transfer_mode);
575
576             source.extra_headers = structure;
577         }
578     }
579
580     private void on_uri_notify (ParamSpec pspec) {
581         this.uri_update_hint = true;
582     }
583
584     /**
585      * Generate basic DIDLLite information.
586      *
587      * This is used when the URI gets changed externally. DLNA requires that a
588      * minimum DIDLLite is always present if the URI is not empty.
589      */
590     private string generate_basic_didl () {
591         var writer = new DIDLLiteWriter (null);
592         var item = writer.add_item ();
593         item.id = "1";
594         item.parent_id = "-1";
595         item.upnp_class = "object.item";
596         var resource = item.add_resource ();
597         resource.uri = this._uri;
598         var file = File.new_for_uri (this.uri);
599         item.title = file.get_basename ();
600
601         return writer.get_string ();
602     }
603
604     private void setup_playbin () {
605         // Needed to get "Stop" events from the playbin.
606         // We can do this because we have a bus watch
607         this.playbin.auto_flush_bus = false;
608         assert (this.playbin != null);
609
610         this.playbin.source_setup.connect (this.on_source_setup);
611         this.playbin.notify["uri"].connect (this.on_uri_notify);
612
613         // Bus handler
614         var bus = this.playbin.get_bus ();
615         bus.add_signal_watch ();
616         bus.message.connect (this.bus_handler);
617     }
618 }