1# Copyright 2009-2011 Steven Robertson, Christoph Reiter
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8import collections
9import subprocess
10
11from gi.repository import GLib, Gst
12
13from quodlibet import _
14from quodlibet.util.string import decode
15from quodlibet.util import is_linux, is_windows
16from quodlibet.player import PlayerError
17
18
19def pulse_is_running():
20    """Returns whether pulseaudio is running"""
21
22    # If we have a pulsesink we can get the server presence through
23    # setting the ready state
24    element = Gst.ElementFactory.make("pulsesink", None)
25    if element is not None:
26        element.set_state(Gst.State.READY)
27        res = element.get_state(0)[0]
28        element.set_state(Gst.State.NULL)
29        return res != Gst.StateChangeReturn.FAILURE
30
31    # NOTE: Don't check with 'pulseaudio --check' because it can't guarantee
32    #       Gstreamer works with PA (e.g., when 'pulsesink' not installed).
33    return False
34
35
36def link_many(elements):
37    last = None
38    for element in elements:
39        if last:
40            if not Gst.Element.link(last, element):
41                return False
42        last = element
43    return True
44
45
46def unlink_many(elements):
47    last = None
48    for element in elements:
49        if last:
50            if not Gst.Element.unlink(last, element):
51                return False
52        last = element
53    return True
54
55
56def iter_to_list(func):
57    objects = []
58
59    iter_ = func()
60    while 1:
61        status, value = iter_.next()
62        if status == Gst.IteratorResult.OK:
63            objects.append(value)
64        else:
65            break
66    return objects
67
68
69def find_audio_sink():
70    """Get the best audio sink available.
71
72    Returns (element, description) or raises PlayerError.
73    """
74
75    if is_windows():
76        sinks = [
77            "directsoundsink",
78        ]
79    elif is_linux() and pulse_is_running():
80        sinks = [
81            "pulsesink",
82        ]
83    else:
84        sinks = [
85            "autoaudiosink",  # plugins-good
86            "pulsesink",  # plugins-good
87            "alsasink",  # plugins-base
88        ]
89
90    for name in sinks:
91        element = Gst.ElementFactory.make(name, None)
92        if element is not None:
93            return (element, name)
94    else:
95        details = " (%s)" % ", ".join(sinks) if sinks else ""
96        raise PlayerError(_("No GStreamer audio sink found") + details)
97
98
99def GStreamerSink(pipeline_desc):
100    """Returns a list of unlinked gstreamer elements ending with an audio sink
101    and a textual description of the pipeline.
102
103    `pipeline_desc` can be gst-launch syntax for multiple elements
104    with or without an audiosink.
105
106    In case of an error, raises PlayerError
107    """
108
109    pipe = None
110    if pipeline_desc:
111        try:
112            pipe = [Gst.parse_launch(e) for e in pipeline_desc.split('!')]
113        except GLib.GError as e:
114            message = e.message
115            raise PlayerError(_("Invalid GStreamer output pipeline"), message)
116
117    if pipe:
118        # In case the last element is linkable with a fakesink
119        # it is not an audiosink, so we append the default one
120        fake = Gst.ElementFactory.make('fakesink', None)
121        if link_many([pipe[-1], fake]):
122            unlink_many([pipe[-1], fake])
123            default_elm, default_desc = find_audio_sink()
124            pipe += [default_elm]
125            pipeline_desc += " ! " + default_desc
126    else:
127        elm, pipeline_desc = find_audio_sink()
128        pipe = [elm]
129
130    return pipe, pipeline_desc
131
132
133class TagListWrapper(collections.Mapping):
134    def __init__(self, taglist, merge=False):
135        self._list = taglist
136        self._merge = merge
137
138    def __len__(self):
139        return self._list.n_tags()
140
141    def __iter__(self):
142        for i in range(len(self)):
143            yield self._list.nth_tag_name(i)
144
145    def __getitem__(self, key):
146        if not Gst.tag_exists(key):
147            raise KeyError
148
149        values = []
150        index = 0
151        while 1:
152            value = self._list.get_value_index(key, index)
153            if value is None:
154                break
155            values.append(value)
156            index += 1
157
158        if not values:
159            raise KeyError
160
161        if self._merge:
162            try:
163                return " - ".join(values)
164            except TypeError:
165                return values[0]
166
167        return values
168
169
170def parse_gstreamer_taglist(tags):
171    """Takes a GStreamer taglist and returns a dict containing only
172    numeric and unicode values and str keys."""
173
174    merged = {}
175    for key in tags.keys():
176        value = tags[key]
177        # extended-comment sometimes contains a single vorbiscomment or
178        # a list of them ["key=value", "key=value"]
179        if key == "extended-comment":
180            if not isinstance(value, list):
181                value = [value]
182            for val in value:
183                if not isinstance(val, str):
184                    continue
185                split = val.split("=", 1)
186                sub_key = split[0]
187                val = split[-1]
188                if sub_key in merged:
189                    sub_val = merged[sub_key]
190                    if not isinstance(sub_val, str):
191                        continue
192                    if val not in sub_val.split("\n"):
193                        merged[sub_key] += "\n" + val
194                else:
195                    merged[sub_key] = val
196        elif isinstance(value, Gst.DateTime):
197            value = value.to_iso8601_string()
198            merged[key] = value
199        else:
200            if isinstance(value, (int, float)):
201                merged[key] = value
202                continue
203
204            if isinstance(value, bytes):
205                value = decode(value)
206
207            if not isinstance(value, str):
208                value = str(value)
209
210            if key in merged:
211                merged[key] += "\n" + value
212            else:
213                merged[key] = value
214
215    return merged
216
217
218def bin_debug(elements, depth=0, lines=None):
219    """Takes a list of gst.Element that are part of a prerolled pipeline, and
220    recursively gets the children and all caps between the elements.
221
222    Returns a list of text lines suitable for printing.
223    """
224
225    from quodlibet.util.dprint import Colorise
226
227    if lines is None:
228        lines = []
229    else:
230        lines.append(" " * (depth - 1) + "\\")
231
232    for i, elm in enumerate(elements):
233        for pad in iter_to_list(elm.iterate_sink_pads):
234            caps = pad.get_current_caps()
235            if caps:
236                lines.append("%s| %s" % (" " * depth, caps.to_string()))
237        name = elm.get_name()
238        cls = Colorise.blue(type(elm).__name__.split(".", 1)[-1])
239        lines.append("%s|-%s (%s)" % (" " * depth, cls, name))
240
241        if isinstance(elm, Gst.Bin):
242            children = reversed(iter_to_list(elm.iterate_sorted))
243            bin_debug(children, depth + 1, lines)
244
245    return lines
246