[Merge] lp:~phablet-team/media-hub/introduce-audio-output-observer-interface into lp:media-hub

Thomas Voß thomas.voss at canonical.com
Wed Mar 4 14:08:56 UTC 2015


Review: Needs Fixing

A minor niggle inline.

Diff comments:

> === modified file 'src/core/media/CMakeLists.txt'
> --- src/core/media/CMakeLists.txt	2015-03-03 18:07:53 +0000
> +++ src/core/media/CMakeLists.txt	2015-03-03 18:07:53 +0000
> @@ -91,6 +91,10 @@
>      cover_art_resolver.cpp
>      engine.cpp
>  
> +    audio/pulse_audio_output_observer.cpp
> +    audio/ostream_reporter.cpp
> +    audio/output_observer.cpp
> +
>      power/battery_observer.cpp
>      power/state_controller.cpp
>  
> 
> === added directory 'src/core/media/audio'
> === added file 'src/core/media/audio/ostream_reporter.cpp'
> --- src/core/media/audio/ostream_reporter.cpp	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/ostream_reporter.cpp	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,54 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + */
> +
> +#include <core/media/audio/ostream_reporter.h>
> +
> +namespace audio = core::ubuntu::media::audio;
> +
> +audio::OStreamReporter::OStreamReporter(std::ostream &out) : out{out}
> +{
> +}
> +
> +void audio::OStreamReporter::connected_to_pulse_audio()
> +{
> +    out << "Connection to PulseAudio has been successfully established." << std::endl;
> +}
> +
> +void audio::OStreamReporter::query_for_default_sink_failed()
> +{
> +    out << "Query for default sink failed." << std::endl;
> +}
> +
> +void audio::OStreamReporter::query_for_default_sink_finished(const std::string& sink_name)
> +{
> +    out << "Default PulseAudio sync has been identified: " << sink_name << std::endl;
> +}
> +
> +void audio::OStreamReporter::query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports)
> +{
> +    out << "PulseAudio sink details for " << name << " with index " << index << " is available:" << std::endl;
> +    for (const auto& port : known_ports)
> +    {
> +        if (port.is_monitored)
> +            out << "  " << port.description << ": " << std::boolalpha << port.is_available << "\n";
> +    }
> +}
> +void audio::OStreamReporter::sink_event_with_index(std::uint32_t index)
> +{
> +    out << "PulseAudio event for sink with index " << index << " received." << std::endl;
> +}
> 
> === added file 'src/core/media/audio/ostream_reporter.h'
> --- src/core/media/audio/ostream_reporter.h	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/ostream_reporter.h	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,55 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + */
> +#ifndef CORE_UBUNTU_MEDIA_AUDIO_OSTREAM_REPORTER_H_
> +#define CORE_UBUNTU_MEDIA_AUDIO_OSTREAM_REPORTER_H_
> +
> +#include <core/media/audio/pulse_audio_output_observer.h>
> +
> +#include <iosfwd>
> +
> +namespace core
> +{
> +namespace ubuntu
> +{
> +namespace media
> +{
> +namespace audio
> +{
> +// A PulseAudioOutputObserver::Reporter implementation printing events to
> +// the configured output stream.
> +class OStreamReporter : public PulseAudioOutputObserver::Reporter
> +{
> +public:
> +    // Constructs a new reporter instance, outputting events to the given stream.
> +    OStreamReporter(std::ostream& out = std::cout);
> +
> +    void connected_to_pulse_audio() override;
> +    void query_for_default_sink_failed() override;
> +    void query_for_default_sink_finished(const std::string& sink_name) override;
> +    void query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports) override;
> +    void sink_event_with_index(std::uint32_t index) override;
> +
> +private:
> +    std::ostream& out;
> +};
> +}
> +}
> +}
> +}
> +
> +#endif // CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OSTREAM_REPORTER_H_
> 
> === added file 'src/core/media/audio/output_observer.cpp'
> --- src/core/media/audio/output_observer.cpp	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/output_observer.cpp	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,48 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + */
> +
> +#include <core/media/audio/output_observer.h>
> +
> +#include <core/media/audio/pulse_audio_output_observer.h>
> +#include <core/media/audio/ostream_reporter.h>
> +
> +#include <iostream>
> +
> +namespace audio = core::ubuntu::media::audio;
> +
> +std::ostream& audio::operator<<(std::ostream& out, audio::OutputState state)
> +{
> +    switch (state)
> +    {
> +    case audio::OutputState::Private:
> +        return out << "OutputState::Private";
> +    case audio::OutputState::Public:
> +        return out << "OutputState::Public";
> +    }
> +
> +    return out;
> +}
> +
> +audio::OutputObserver::Ptr audio::make_platform_default_output_observer()
> +{
> +    audio::PulseAudioOutputObserver::Configuration config;
> +    config.sink = "sink.primary";
> +    config.output_port_patterns = {std::regex{"output-wired_head.*|output-a2dp_headphones"}};
> +    config.reporter = std::make_shared<audio::OStreamReporter>();
> +    return std::make_shared<audio::PulseAudioOutputObserver>(config);
> +}
> 
> === added file 'src/core/media/audio/output_observer.h'
> --- src/core/media/audio/output_observer.h	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/output_observer.h	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,74 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + */
> +#ifndef CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_
> +#define CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_
> +
> +#include <core/property.h>
> +
> +#include <iosfwd>
> +#include <memory>
> +
> +namespace core
> +{
> +namespace ubuntu
> +{
> +namespace media
> +{
> +namespace audio
> +{
> +// All known states of an audio output.
> +enum class OutputState
> +{
> +    // The output is via a Private device (i.e. Headphones).
> +    Private,

As private/public as member names are not possible, could we name them {private, public}_output?

> +    // The output is via a Public device (i.e. Speaker).
> +    Public,
> +};
> +
> +// Models observation of audio outputs of a device.
> +// Right now, we are only interested in monitoring the
> +// state of external outputs to react accordingly if
> +// wired or bluetooth outputs are connected/disconnected.
> +class OutputObserver
> +{
> +public:
> +    // Save us some typing.
> +    typedef std::shared_ptr<OutputObserver> Ptr;
> +
> +    virtual ~OutputObserver() = default;
> +
> +    // Getable/observable property holding the state of external outputs.
> +    virtual const core::Property<OutputState>& external_output_state() const = 0;
> +
> +protected:
> +    OutputObserver() = default;
> +    OutputObserver(const OutputObserver&) = delete;
> +    OutputObserver& operator=(const OutputObserver&) = delete;
> +};
> +
> +// Pretty prints the given state to the given output stream.
> +std::ostream& operator<<(std::ostream&, OutputState);
> +
> +// Creats a platform default instance for observing audio outputs.
> +OutputObserver::Ptr make_platform_default_output_observer();
> +}
> +}
> +}
> +}
> +
> +#endif // CORE_UBUNTU_MEDIA_AUDIO_OUTPUT_OBSERVER_H_
> 
> === added file 'src/core/media/audio/pulse_audio_output_observer.cpp'
> --- src/core/media/audio/pulse_audio_output_observer.cpp	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/pulse_audio_output_observer.cpp	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,457 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + *              Ricardo Mendoza <ricardo.mendoza at canonical.com>
> + */
> +
> +#include <core/media/audio/pulse_audio_output_observer.h>
> +
> +#include <pulse/pulseaudio.h>
> +
> +#include <cstdint>
> +
> +#include <map>
> +#include <regex>
> +#include <string>
> +
> +namespace audio = core::ubuntu::media::audio;
> +
> +namespace
> +{
> +// We wrap calls to the pulseaudio client api into its
> +// own namespace and make sure that only managed types
> +// can be passed to calls to pulseaudio. In addition,
> +// we add guards to the function calls to ensure that
> +// they are conly called on the correct thread.
> +namespace pa
> +{
> +typedef std::shared_ptr<pa_threaded_mainloop> ThreadedMainLoopPtr;
> +ThreadedMainLoopPtr make_threaded_main_loop()
> +{
> +    return ThreadedMainLoopPtr
> +    {
> +        pa_threaded_mainloop_new(),
> +        [](pa_threaded_mainloop* ml)
> +        {
> +            pa_threaded_mainloop_stop(ml);
> +            pa_threaded_mainloop_free(ml);
> +        }
> +    };
> +}
> +
> +void start_main_loop(ThreadedMainLoopPtr ml)
> +{
> +    pa_threaded_mainloop_start(ml.get());
> +}
> +
> +typedef std::shared_ptr<pa_context> ContextPtr;
> +ContextPtr make_context(ThreadedMainLoopPtr main_loop)
> +{
> +    return ContextPtr
> +    {
> +        pa_context_new(pa_threaded_mainloop_get_api(main_loop.get()), "MediaHubPulseContext"),
> +        pa_context_unref
> +    };
> +}
> +
> +void set_state_callback(ContextPtr ctxt, pa_context_notify_cb_t cb, void* cookie)
> +{
> +    pa_context_set_state_callback(ctxt.get(), cb, cookie);
> +}
> +
> +void set_subscribe_callback(ContextPtr ctxt, pa_context_subscribe_cb_t cb, void* cookie)
> +{
> +    pa_context_set_subscribe_callback(ctxt.get(), cb, cookie);
> +}
> +
> +void throw_if_not_on_main_loop(ThreadedMainLoopPtr ml)
> +{
> +    if (not pa_threaded_mainloop_in_thread(ml.get())) throw std::logic_error
> +    {
> +        "Attempted to call into a pulseaudio object from another"
> +        "thread than the pulseaudio mainloop thread."
> +    };
> +}
> +
> +void throw_if_not_connected(ContextPtr ctxt)
> +{
> +    if (pa_context_get_state(ctxt.get()) != PA_CONTEXT_READY ) throw std::logic_error
> +    {
> +        "Attempted to issue a call against pulseaudio via a non-connected context."
> +    };
> +}
> +
> +void get_server_info_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_server_info_cb_t cb, void* cookie)
> +{
> +    throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
> +    pa_operation_unref(pa_context_get_server_info(ctxt.get(), cb, cookie));
> +}
> +
> +void subscribe_to_events(ContextPtr ctxt, ThreadedMainLoopPtr ml, pa_subscription_mask mask)
> +{
> +    throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
> +    pa_operation_unref(pa_context_subscribe(ctxt.get(), mask, nullptr, nullptr));
> +}
> +
> +void get_index_of_sink_by_name_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, const std::string& name, pa_sink_info_cb_t cb, void* cookie)
> +{
> +    throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
> +    pa_operation_unref(pa_context_get_sink_info_by_name(ctxt.get(), name.c_str(), cb, cookie));
> +}
> +
> +void get_sink_info_by_index_async(ContextPtr ctxt, ThreadedMainLoopPtr ml, std::int32_t index, pa_sink_info_cb_t cb, void* cookie)
> +{
> +    throw_if_not_on_main_loop(ml); throw_if_not_connected(ctxt);
> +    pa_operation_unref(pa_context_get_sink_info_by_index(ctxt.get(), index, cb, cookie));
> +}
> +
> +void connect_async(ContextPtr ctxt)
> +{
> +    pa_context_connect(ctxt.get(), nullptr, static_cast<pa_context_flags_t>(PA_CONTEXT_NOAUTOSPAWN | PA_CONTEXT_NOFAIL), nullptr);
> +}
> +
> +bool is_port_available_on_sink(const pa_sink_info* info, const std::regex& port_pattern)
> +{
> +    if (not info)
> +        return false;
> +
> +    for (std::uint32_t i = 0; i < info->n_ports; i++)
> +    {
> +        if (info->ports[i]->available == PA_PORT_AVAILABLE_NO ||
> +            info->ports[i]->available == PA_PORT_AVAILABLE_UNKNOWN)
> +            continue;
> +
> +        if (std::regex_match(std::string{info->ports[i]->name}, port_pattern))
> +            return true;
> +    }
> +
> +    return false;
> +}
> +}
> +}
> +
> +struct audio::PulseAudioOutputObserver::Private
> +{
> +    static void context_notification_cb(pa_context* ctxt, void* cookie)
> +    {
> +        if (auto thiz = static_cast<Private*>(cookie))
> +        {
> +            // Better safe than sorry: Check if we got signaled for the
> +            // context we are actually interested in.
> +            if (thiz->context.get() != ctxt)
> +                return;
> +
> +            switch (pa_context_get_state(ctxt))
> +            {
> +            case PA_CONTEXT_READY:
> +                thiz->on_context_ready();
> +                break;
> +            case PA_CONTEXT_FAILED:
> +                thiz->on_context_failed();
> +                break;
> +            default:
> +                break;
> +            }
> +        }
> +    }
> +
> +    static void context_subscription_cb(pa_context* ctxt, pa_subscription_event_type_t ev, uint32_t idx, void* cookie)
> +    {
> +        (void) idx;
> +
> +        if (auto thiz = static_cast<Private*>(cookie))
> +        {
> +            // Better safe than sorry: Check if we got signaled for the
> +            // context we are actually interested in.
> +            if (thiz->context.get() != ctxt)
> +                return;
> +
> +            if ((ev & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
> +                thiz->on_sink_event_with_index(idx);
> +        }
> +    }
> +
> +    static void query_for_active_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
> +    {
> +        if (eol)
> +            return;
> +
> +        if (auto thiz = static_cast<Private*>(cookie))
> +        {
> +            // Better safe than sorry: Check if we got signaled for the
> +            // context we are actually interested in.
> +            if (thiz->context.get() != ctxt)
> +                return;
> +
> +            thiz->on_query_for_active_sink_finished(si);
> +        }
> +    }
> +
> +    static void query_for_primary_sink_finished(pa_context* ctxt, const pa_sink_info* si, int eol, void* cookie)
> +    {
> +        if (eol)
> +            return;
> +
> +        if (auto thiz = static_cast<Private*>(cookie))
> +        {
> +            // Better safe than sorry: Check if we got signaled for the
> +            // context we are actually interested in.
> +            if (thiz->context.get() != ctxt)
> +                return;
> +
> +            thiz->on_query_for_primary_sink_finished(si);
> +        }
> +    }
> +
> +    static void query_for_server_info_finished(pa_context* ctxt, const pa_server_info* si, void* cookie)
> +    {
> +        if (not si)
> +            return;
> +
> +        if (auto thiz = static_cast<Private*>(cookie))
> +        {
> +            // Better safe than sorry: Check if we got signaled for the
> +            // context we are actually interested in.
> +            if (thiz->context.get() != ctxt)
> +                return;
> +
> +            thiz->on_query_for_server_info_finished(si);
> +        }
> +    }
> +
> +    Private(const audio::PulseAudioOutputObserver::Configuration& config)
> +        : config(config),
> +          main_loop{pa::make_threaded_main_loop()},
> +          context{pa::make_context(main_loop)}
> +    {
> +        for (const auto& pattern : config.output_port_patterns)
> +        {
> +            outputs.emplace_back(pattern, core::Property<media::audio::OutputState>{media::audio::OutputState::Public});
> +            std::get<1>(outputs.back()) | properties.external_output_state;
> +            std::get<1>(outputs.back()).changed().connect([](media::audio::OutputState state)
> +            {
> +                std::cout << "Connection state for port changed to: " << state << std::endl;
> +            });
> +        }
> +
> +        pa::set_state_callback(context, Private::context_notification_cb, this);
> +        pa::set_subscribe_callback(context, Private::context_subscription_cb, this);
> +
> +        pa::connect_async(context);
> +        pa::start_main_loop(main_loop);
> +    }
> +
> +    // The connection attempt has been successful and we are connected
> +    // to pulseaudio now.
> +    void on_context_ready()
> +    {
> +        config.reporter->connected_to_pulse_audio();
> +
> +        pa::subscribe_to_events(context, main_loop, PA_SUBSCRIPTION_MASK_SINK);
> +
> +        if (config.sink == "query.from.server")
> +        {
> +            pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
> +        }
> +        else
> +        {
> +            properties.sink = config.sink;
> +            // Get primary sink index (default)
> +            pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this);
> +            // Update active sink (could be == default)
> +            pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
> +        }
> +    }
> +
> +    // Either a connection attempt failed, or an existing connection
> +    // was unexpectedly terminated.
> +    void on_context_failed()
> +    {
> +        pa::connect_async(context);
> +    }
> +
> +    // Something changed on the sink with index idx.
> +    void on_sink_event_with_index(std::int32_t index)
> +    {
> +        config.reporter->sink_event_with_index(index);
> +         
> +        // Update server info (active sink)    
> +        pa::get_server_info_async(context, main_loop, Private::query_for_server_info_finished, this);
> +
> +    }
> +
> +    void on_query_for_active_sink_finished(const pa_sink_info* info)
> +    {
> +        // Update active sink if a change is registered.
> +        if (std::get<0>(active_sink) != info->index)
> +        {
> +            std::get<0>(active_sink) = info->index;
> +            std::get<1>(active_sink) = info->name;
> +            if (info->index != static_cast<std::uint32_t>(primary_sink_index))
> +                for (auto& element : outputs)
> +                    std::get<1>(element) = audio::OutputState::Public;
> +        }
> +    }
> +
> +    // Query for primary sink finished.
> +    void on_query_for_primary_sink_finished(const pa_sink_info* info)
> +    {
> +        for (auto& element : outputs)
> +        {
> +            std::cout << "Checking if port is available " << " -> " << std::boolalpha << pa::is_port_available_on_sink(info, std::get<0>(element)) << std::endl;
> +            audio::OutputState state = pa::is_port_available_on_sink(info, std::get<0>(element))
> +                    ? media::audio::OutputState::Private
> +                    : media::audio::OutputState::Public;
> +
> +            // Only issue state change if the change happened on the active index.
> +            if (std::get<0>(active_sink) != info->index)
> +                continue;
> +
> +            std::get<1>(element) = state;
> +
> +        }
> +
> +        std::set<Reporter::Port> known_ports;
> +        for (std::uint32_t i = 0; i < info->n_ports; i++)
> +        {
> +            bool is_monitored = false;
> +
> +            for (auto& element : outputs)
> +                is_monitored |= std::regex_match(info->ports[i]->name, std::get<0>(element));
> +
> +
> +            known_ports.insert(Reporter::Port
> +            {
> +                info->ports[i]->name,
> +                info->ports[i]->description,
> +                info->ports[i]->available == PA_PORT_AVAILABLE_YES,
> +                is_monitored
> +            });
> +        }
> +
> +        properties.known_ports = known_ports;
> +
> +        // Initialize sink of primary index (onboard)
> +        if (primary_sink_index == -1) 
> +            primary_sink_index = info->index;
> +
> +        config.reporter->query_for_sink_info_finished(info->name, info->index, known_ports);
> +    }
> +
> +    void on_query_for_server_info_finished(const pa_server_info* info)
> +    {
> +        // We bail out if we could not determine the default sink name.
> +        // In this case, we are not able to carry out audio output observation.
> +        if (not info->default_sink_name)
> +        {
> +            config.reporter->query_for_default_sink_failed();
> +            return;
> +        }
> +
> +        // Update active sink
> +        if (info->default_sink_name != std::get<1>(active_sink))
> +            pa::get_index_of_sink_by_name_async(context, main_loop, info->default_sink_name, Private::query_for_active_sink_finished, this);
> +
> +        // Update wired output for primary sink (onboard)
> +        pa::get_sink_info_by_index_async(context, main_loop, primary_sink_index, Private::query_for_primary_sink_finished, this);
> +
> +        if (properties.sink.get() != config.sink)
> +        {
> +            config.reporter->query_for_default_sink_finished(info->default_sink_name);
> +            properties.sink = config.sink = info->default_sink_name;
> +            pa::get_index_of_sink_by_name_async(context, main_loop, config.sink, Private::query_for_primary_sink_finished, this);
> +        }
> +    }
> +
> +    PulseAudioOutputObserver::Configuration config;    
> +    pa::ThreadedMainLoopPtr main_loop;
> +    pa::ContextPtr context;
> +    std::int32_t primary_sink_index;
> +    std::tuple<uint32_t, std::string> active_sink;
> +    std::vector<std::tuple<std::regex, core::Property<media::audio::OutputState>>> outputs;
> +
> +    struct
> +    {
> +        core::Property<std::string> sink;
> +        core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>> known_ports;
> +        core::Property<audio::OutputState> external_output_state{audio::OutputState::Public};
> +    } properties;
> +};
> +
> +bool audio::PulseAudioOutputObserver::Reporter::Port::operator==(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const
> +{
> +    return name == rhs.name;
> +}
> +
> +bool audio::PulseAudioOutputObserver::Reporter::Port::operator<(const audio::PulseAudioOutputObserver::Reporter::Port& rhs) const
> +{
> +    return name < rhs.name;
> +}
> +
> +audio::PulseAudioOutputObserver::Reporter::~Reporter()
> +{
> +}
> +
> +void audio::PulseAudioOutputObserver::Reporter::connected_to_pulse_audio()
> +{
> +}
> +
> +void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_failed()
> +{
> +}
> +
> +void audio::PulseAudioOutputObserver::Reporter::query_for_default_sink_finished(const std::string&)
> +{
> +}
> +
> +void audio::PulseAudioOutputObserver::Reporter::query_for_sink_info_finished(const std::string&, std::uint32_t, const std::set<Port>&)
> +{
> +}
> +
> +void audio::PulseAudioOutputObserver::Reporter::sink_event_with_index(std::uint32_t)
> +{
> +}
> +
> +// Constructs a new instance, or throws std::runtime_error
> +// if connection to pulseaudio fails.
> +audio::PulseAudioOutputObserver::PulseAudioOutputObserver(const Configuration& config) : d{new Private{config}}
> +{
> +    if (not d->config.reporter) throw std::runtime_error
> +    {
> +        "PulseAudioOutputObserver: Cannot construct for invalid reporter instance."
> +    };
> +}
> +
> +// We provide the name of the sink we are connecting to as a
> +// getable/observable property. This is specifically meant for
> +// consumption by test code.
> +const core::Property<std::string>& audio::PulseAudioOutputObserver::sink() const
> +{
> +    return d->properties.sink;
> +}
> +
> +// The set of ports that have been identified on the configured sink.
> +// Specifically meant for consumption by test code.
> +const core::Property<std::set<audio::PulseAudioOutputObserver::Reporter::Port>>& audio::PulseAudioOutputObserver::known_ports() const
> +{
> +    return d->properties.known_ports;
> +}
> +
> +// Getable/observable property holding the state of external outputs.
> +const core::Property<audio::OutputState>& audio::PulseAudioOutputObserver::external_output_state() const
> +{
> +    return d->properties.external_output_state;
> +}
> 
> === added file 'src/core/media/audio/pulse_audio_output_observer.h'
> --- src/core/media/audio/pulse_audio_output_observer.h	1970-01-01 00:00:00 +0000
> +++ src/core/media/audio/pulse_audio_output_observer.h	2015-03-03 18:07:53 +0000
> @@ -0,0 +1,128 @@
> +/*
> + * Copyright © 2014 Canonical Ltd.
> + *
> + * This program is free software: you can redistribute it and/or modify it
> + * under the terms of the GNU Lesser General Public License version 3,
> + * as published by the Free Software Foundation.
> + *
> + * This program is distributed in the hope that it will be useful,
> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
> + * GNU Lesser General Public License for more details.
> + *
> + * You should have received a copy of the GNU Lesser General Public License
> + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
> + *
> + * Authored by: Thomas Voß <thomas.voss at canonical.com>
> + *              Ricardo Mendoza <ricardo.mendoza at canonical.com>
> + */
> +#ifndef CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_
> +#define CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_
> +
> +#include <core/media/audio/output_observer.h>
> +
> +#include <iosfwd>
> +#include <memory>
> +#include <regex>
> +
> +namespace core
> +{
> +namespace ubuntu
> +{
> +namespace media
> +{
> +namespace audio
> +{
> +// Implements the audio::OutputObserver interface
> +// relying on pulse to query the connected ports
> +// of the primary card of the system.
> +class PulseAudioOutputObserver : public OutputObserver
> +{
> +public:
> +    // Save us some typing.
> +    typedef std::shared_ptr<PulseAudioOutputObserver> Ptr;
> +
> +    // Reporter is responsible for surfacing events from the implementation
> +    // that help in resolving/tracking down issues. Default implementation is empty.
> +    struct Reporter
> +    {
> +        // To save us some typing.
> +        typedef std::shared_ptr<Reporter> Ptr;
> +
> +        // Simple type to help in reporting.
> +        struct Port
> +        {
> +            // Returns true iff the name of both ports are equal.
> +            bool operator==(const Port& rhs) const;
> +            // Returns true iff the name of the ports differ.
> +            bool operator<(const Port& rhs) const;
> +
> +            std::string name; // The name of the port.
> +            std::string description; // Human-readable description of the port.
> +            bool is_available; // True if the port is available.
> +            bool is_monitored; // True if the port is monitored by the observer.
> +        };
> +
> +        virtual ~Reporter();
> +        // connected_to_pulse_audio is called when a connection with pulse has been established.
> +        virtual void connected_to_pulse_audio();
> +        // query_for_default_sink_failed is called when no default sink was returned.
> +        virtual void query_for_default_sink_failed();
> +        // query_for_default_sink_finished is called when the default sink query against pulse
> +        // has finished, reporting the name of the sink to observers.
> +        virtual void query_for_default_sink_finished(const std::string& sink_name);
> +        // query_for_sink_info_finished is called when a query for information about a specific sink
> +        // has finished, reporting the name, index of the sink as well as the set of ports known to the sink.
> +        virtual void query_for_sink_info_finished(const std::string& name, std::uint32_t index, const std::set<Port>& known_ports);
> +        // sink_event_with_index is called when something happened on a sink, reporing the index of the
> +        // sink.
> +        virtual void sink_event_with_index(std::uint32_t index);
> +    };
> +
> +    // Construction time arguments go here
> +    struct Configuration
> +    {
> +        // Name of the sink that we should consider.
> +        std::string sink
> +        {
> +            // A special value that requests the implementation to
> +            // query pulseaudio for the default configured sink.
> +            "query.from.server"
> +        };
> +        // Output port name patterns that should be observed on the configured sink.
> +        // All patterns have to be valid regular expressions.
> +        std::vector<std::regex> output_port_patterns
> +        {
> +            // Any port is considered with this special value.
> +            std::regex{".+"}
> +        };
> +        // The Reporter instance that the implementation reports
> +        // events to. Must not be null.
> +        Reporter::Ptr reporter{std::make_shared<Reporter>()};
> +    };
> +
> +    // Constructs a new instance, throws:
> +    //   * std::runtime_error if connection to pulseaudio fails.
> +    //   * std::runtime_error if reporter instance is null.
> +    PulseAudioOutputObserver(const Configuration&);
> +
> +    // We provide the name of the sink we are connecting to as a
> +    // getable/observable property. This is specifically meant for
> +    // consumption by test code.
> +    const core::Property<std::string>& sink() const;
> +    // The set of ports that have been identified on the configured sink.
> +    // Specifically meant for consumption by test code.
> +    const core::Property<std::set<Reporter::Port>>& known_ports() const;
> +    // Getable/observable property holding the state of external outputs.
> +    const core::Property<OutputState>& external_output_state() const override;
> +
> +private:
> +    struct Private;
> +    std::shared_ptr<Private> d;
> +};
> +}
> +}
> +}
> +}
> +
> +#endif // CORE_UBUNTU_MEDIA_AUDIO_PULSE_AUDIO_OUTPUT_OBSERVER_H_
> 
> === modified file 'src/core/media/gstreamer/playbin.cpp'
> --- src/core/media/gstreamer/playbin.cpp	2015-03-03 18:07:53 +0000
> +++ src/core/media/gstreamer/playbin.cpp	2015-03-03 18:07:53 +0000
> @@ -406,6 +406,9 @@
>      };
>  
>      auto ret = gst_element_set_state(pipeline, new_state);
> +
> +    std::cout << __PRETTY_FUNCTION__ << ": requested state change." << std::endl;
> +
>      bool result = false; GstState current, pending;
>      switch(ret)
>      {
> @@ -419,7 +422,7 @@
>                      pipeline,
>                      &current,
>                      &pending,
> -                    state_change_timeout.count());    
> +                    state_change_timeout.count());
>          break;
>      }
>  
> 
> === modified file 'src/core/media/service_implementation.cpp'
> --- src/core/media/service_implementation.cpp	2015-03-03 18:07:53 +0000
> +++ src/core/media/service_implementation.cpp	2015-03-03 18:07:53 +0000
> @@ -22,6 +22,7 @@
>  
>  #include "service_implementation.h"
>  
> +#include "audio/output_observer.h"
>  #include "client_death_observer.h"
>  #include "call-monitor/call_monitor.h"
>  #include "player_configuration.h"
> @@ -57,349 +58,9 @@
>            display_state_lock(power_state_controller->display_state_lock()),
>            client_death_observer(media::platform_default_client_death_observer()),
>            recorder_observer(media::make_platform_default_recorder_observer()),
> -          pulse_mainloop_api(nullptr),
> -          pulse_context(nullptr),
> -          headphones_connected(false),
> -          a2dp_connected(false),
> -          primary_idx(-1),
> +          audio_output_observer(media::audio::make_platform_default_output_observer()),
>            call_monitor(new CallMonitor)
>      {
> -        // Spawn pulse watchdog
> -        pulse_mainloop = nullptr;
> -        pulse_worker = std::move(std::thread([this]()
> -        {
> -            std::unique_lock<std::mutex> lk(pulse_mutex);
> -            pcv.wait(lk,
> -                [this]{
> -                    if (pulse_mainloop != nullptr || pulse_context != nullptr)
> -                    {
> -                        // We come from instance death, destroy and create.
> -                        if (pulse_context != nullptr)
> -                        {
> -                            pa_threaded_mainloop_lock(pulse_mainloop);
> -                            pa_operation *o;
> -                            o = pa_context_drain(pulse_context,
> -                                [](pa_context *context, void *userdata)
> -                                {
> -                                    (void) context;
> -
> -                                    Private *p = reinterpret_cast<Private*>(userdata);
> -                                    pa_threaded_mainloop_signal(p->mainloop(), 0);
> -                                }, this);
> -
> -                            if (o)
> -                            {
> -                                while (pa_operation_get_state(o) == PA_OPERATION_RUNNING)
> -                                    pa_threaded_mainloop_wait(pulse_mainloop);
> -
> -                                pa_operation_unref(o);
> -                            }
> -
> -                            pa_context_set_state_callback(pulse_context, NULL, NULL);
> -                            pa_context_set_subscribe_callback(pulse_context, NULL, NULL);
> -                            pa_context_disconnect(pulse_context);
> -                            pa_context_unref(pulse_context);
> -                            pulse_context = nullptr;
> -                            pa_threaded_mainloop_unlock(pulse_mainloop);
> -                        }
> -                    }
> -
> -                    if (pulse_mainloop == nullptr)
> -                    {
> -                        pulse_mainloop = pa_threaded_mainloop_new();
> -
> -                        if (pa_threaded_mainloop_start(pulse_mainloop) != 0)
> -                        {
> -                            std::cerr << "Unable to start pulseaudio mainloop, audio output detection will not function" << std::endl;
> -                            pa_threaded_mainloop_free(pulse_mainloop);
> -                            pulse_mainloop = nullptr;
> -                        }
> -                    }
> -
> -                    do {
> -                        create_pulse_context();
> -                    } while (pulse_context == nullptr);
> -
> -                    // Wait for next instance death.
> -                    return false;
> -                });
> -        }));
> -        
> -        recorder_observer->recording_state().changed().connect([this](media::RecordingState state)
> -        {
> -            media_recording_state_changed(state);
> -        });
> -    }
> -
> -    ~Private()
> -    {
> -        release_pulse_context();
> -
> -        if (pulse_mainloop != nullptr)
> -        {
> -            pa_threaded_mainloop_stop(pulse_mainloop);
> -            pa_threaded_mainloop_free(pulse_mainloop);
> -            pulse_mainloop = nullptr;
> -        }
> -
> -        if (pulse_worker.joinable())
> -            pulse_worker.join();
> -    }
> -
> -    void media_recording_state_changed(media::RecordingState state)
> -    {
> -        if (state == media::RecordingState::started)
> -        {
> -            display_state_lock->request_acquire(media::power::DisplayState::on);
> -            pause_playback();
> -        }
> -        else if (state == media::RecordingState::stopped)
> -        {
> -            display_state_lock->request_release(media::power::DisplayState::on);
> -        }
> -    }
> -
> -    pa_threaded_mainloop *mainloop()
> -    {
> -        return pulse_mainloop;
> -    }
> -
> -    bool is_port_available(pa_card_port_info **ports, uint32_t n_ports, const char *name)
> -    {
> -        bool ret = false;
> -
> -        if (ports != nullptr && n_ports > 0 && name != nullptr)
> -        {
> -            for (uint32_t i=0; i<n_ports; i++)
> -            {
> -                if (strstr(ports[i]->name, name) != nullptr && ports[i]->available != PA_PORT_AVAILABLE_NO)
> -                {
> -                    ret = true;
> -                    break;
> -                }
> -            }
> -        }
> -
> -        return ret;
> -    }
> -
> -    void update_wired_output()
> -    {
> -        const pa_operation *o = pa_context_get_card_info_by_index(pulse_context, primary_idx,
> -                [](pa_context *context, const pa_card_info *info, int eol, void *userdata)
> -                {
> -                    (void) context;
> -                    (void) eol;
> -
> -                    if (info == nullptr || userdata == nullptr)
> -                        return;
> -
> -                    Private *p = reinterpret_cast<Private*>(userdata);
> -                    if (p->is_port_available(info->ports, info->n_ports, "output-wired"))
> -                    {
> -                        if (!p->headphones_connected)
> -                            std::cout << "Wired headphones connected" << std::endl;
> -                        p->headphones_connected = true;
> -                    }
> -                    else if (p->headphones_connected == true)
> -                    {
> -                        std::cout << "Wired headphones disconnected" << std::endl;
> -                        p->headphones_connected = false;
> -                        p->pause_playback_if_necessary(std::get<0>(p->active_sink));
> -                    }
> -                }, this);
> -        (void) o;
> -    }
> -
> -    void pause_playback_if_necessary(int index)
> -    {
> -        // Catch uninitialized case (active index == -1)
> -        if (std::get<0>(active_sink) == -1)
> -            return;
> -
> -        if (headphones_connected)
> -            return;
> -
> -        // No headphones/fallback on primary sink? Pause.
> -        if (index == primary_idx)
> -            pause_playback();
> -    }
> -
> -    void set_active_sink(const char *name)
> -    {
> -        const pa_operation *o = pa_context_get_sink_info_by_name(pulse_context, name,
> -                [](pa_context *context, const pa_sink_info *i, int eol, void *userdata)
> -                {
> -                    (void) context;
> -
> -                    if (eol)
> -                        return;
> -
> -                    Private *p = reinterpret_cast<Private*>(userdata); 
> -                    std::tuple<uint32_t, uint32_t, std::string> new_sink(std::make_tuple(i->index, i->card, i->name));
> -
> -                    printf("pulsesink: active_sink=('%s',%d,%d) -> ('%s',%d,%d)\n",
> -                        std::get<2>(p->active_sink).c_str(), std::get<0>(p->active_sink),
> -                        std::get<1>(p->active_sink), i->name, i->index, i->card);
> -
> -                    p->pause_playback_if_necessary(i->index);
> -                    p->active_sink = new_sink;
> -                }, this);
> -
> -        (void) o;
> -    }
> -
> -    void update_active_sink()
> -    {
> -        const pa_operation *o = pa_context_get_server_info(pulse_context,
> -                [](pa_context *context, const pa_server_info *i, void *userdata)
> -                {
> -                    (void) context;
> -
> -                    Private *p = reinterpret_cast<Private*>(userdata);
> -                    if (i->default_sink_name != std::get<2>(p->active_sink))
> -                        p->set_active_sink(i->default_sink_name);
> -                    p->update_wired_output();
> -                }, this);
> -
> -        (void) o;
> -    }
> -
> -    void create_pulse_context()
> -    {
> -        if (pulse_context != nullptr)
> -            return;
> -
> -        active_sink = std::make_tuple(-1, -1, "");
> -
> -        bool keep_going = true, ok = true;
> -
> -        pulse_mainloop_api = pa_threaded_mainloop_get_api(pulse_mainloop);
> -        pa_threaded_mainloop_lock(pulse_mainloop);
> -
> -        pulse_context = pa_context_new(pulse_mainloop_api, "MediaHubPulseContext");
> -        pa_context_set_state_callback(pulse_context,
> -                [](pa_context *context, void *userdata)
> -                {
> -                    (void) context;
> -                    Private *p = reinterpret_cast<Private*>(userdata);
> -                    // Signals the pa_threaded_mainloop_wait below to proceed
> -                    pa_threaded_mainloop_signal(p->mainloop(), 0);
> -                }, this);
> -
> -        if (pulse_context == nullptr)
> -        {
> -            std::cerr << "Unable to create new pulseaudio context" << std::endl;
> -            pa_threaded_mainloop_unlock(pulse_mainloop);
> -            return;
> -        }
> -
> -        pa_context_connect(pulse_context, nullptr, pa_context_flags_t((int) PA_CONTEXT_NOAUTOSPAWN | (int) PA_CONTEXT_NOFAIL), nullptr); 
> -        pa_threaded_mainloop_wait(pulse_mainloop);
> -
> -        while (keep_going)
> -        {
> -            switch (pa_context_get_state(pulse_context))
> -            {
> -                case PA_CONTEXT_CONNECTING: // Wait for service to be available
> -                case PA_CONTEXT_AUTHORIZING:
> -                case PA_CONTEXT_SETTING_NAME:
> -                    break;
> -
> -                case PA_CONTEXT_READY:
> -                    std::cout << "Pulseaudio connection established." << std::endl;
> -                    keep_going = false;
> -                    break;
> -
> -                case PA_CONTEXT_FAILED:
> -                case PA_CONTEXT_TERMINATED:
> -                    keep_going = false;
> -                    ok = false;
> -                    break;
> -
> -                default:
> -                    std::cerr << "Pulseaudio connection failure: " << pa_strerror(pa_context_errno(pulse_context));
> -                    keep_going = false;
> -                    ok = false;
> -            }
> -
> -            if (keep_going)
> -                pa_threaded_mainloop_wait(pulse_mainloop);
> -        }
> -
> -        if (ok)
> -        {
> -            pa_context_set_state_callback(pulse_context,
> -                    [](pa_context *context, void *userdata)
> -                    {
> -                        (void) context;
> -                        (void) userdata;
> -                        Private *p = reinterpret_cast<Private*>(userdata);
> -                        std::unique_lock<std::mutex> lk(p->pulse_mutex);
> -                        switch (pa_context_get_state(context))
> -                        {
> -                            case PA_CONTEXT_FAILED:
> -                            case PA_CONTEXT_TERMINATED:
> -                                p->pcv.notify_all();
> -                                break;
> -                            default:
> -                                break;
> -                        }
> -                    }, this);
> -
> -            //FIXME: Get index for "sink.primary", the default onboard card on Touch.
> -            pa_context_get_sink_info_by_name(pulse_context, "sink.primary",
> -                    [](pa_context *context, const pa_sink_info *i, int eol, void *userdata)
> -                    {
> -                        (void) context;
> -
> -                        if (eol)
> -                            return;
> -
> -                        Private *p = reinterpret_cast<Private*>(userdata);
> -                        p->primary_idx = i->index;
> -                        p->update_wired_output();
> -                    }, this);
> -
> -            update_active_sink();
> -
> -            pa_context_set_subscribe_callback(pulse_context,
> -                    [](pa_context *context, pa_subscription_event_type_t t, uint32_t idx, void *userdata)
> -                    {
> -                        (void) context;
> -                        (void) idx;
> -
> -                        if (userdata == nullptr)
> -                            return;
> -
> -                        Private *p = reinterpret_cast<Private*>(userdata);
> -                        if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK)
> -                        {
> -                            p->update_active_sink(); 
> -                        }
> -                    }, this);
> -            pa_context_subscribe(pulse_context, PA_SUBSCRIPTION_MASK_SINK, nullptr, this);
> -        }
> -        else
> -        {
> -            std::cerr << "Connection to pulseaudio failed or was dropped." << std::endl;
> -            pa_context_unref(pulse_context);
> -            pulse_context = nullptr;
> -        }
> -
> -        pa_threaded_mainloop_unlock(pulse_mainloop);
> -    }
> -
> -    void release_pulse_context()
> -    {
> -        if (pulse_context != nullptr)
> -        {
> -            pa_threaded_mainloop_lock(pulse_mainloop);
> -            pa_context_disconnect(pulse_context);
> -            pa_context_unref(pulse_context);
> -            pa_threaded_mainloop_unlock(pulse_mainloop);
> -            pulse_context = nullptr;
> -        }
>      }
>  
>      media::ServiceImplementation::Configuration configuration;
> @@ -411,22 +72,8 @@
>      media::power::StateController::Lock<media::power::DisplayState>::Ptr display_state_lock;
>      media::ClientDeathObserver::Ptr client_death_observer;
>      media::RecorderObserver::Ptr recorder_observer;
> -    // Pulse-specific
> -    pa_mainloop_api *pulse_mainloop_api;
> -    pa_threaded_mainloop *pulse_mainloop;
> -    pa_context *pulse_context;
> -    std::thread pulse_worker;
> -    std::mutex pulse_mutex;
> -    std::condition_variable pcv;
> -    bool headphones_connected;
> -    bool a2dp_connected;
> -    std::tuple<int, int, std::string> active_sink;
> -    int primary_idx;
> +    media::audio::OutputObserver::Ptr audio_output_observer;
>  
> -    // Gets signaled when both the headphone jack is removed or an A2DP device is
> -    // disconnected and playback needs pausing. Also gets signaled when recording
> -    // begins.
> -    core::Signal<void> pause_playback;
>      std::unique_ptr<CallMonitor> call_monitor;
>      std::list<media::Player::PlayerKey> paused_sessions;
>  };
> @@ -456,10 +103,18 @@
>              resume_multimedia_session();
>      });
>  
> -    d->pause_playback.connect([this]()
> +    d->audio_output_observer->external_output_state().changed().connect([this](audio::OutputState state)
>      {
> -        std::cout << "Got pause_playback signal, pausing all multimedia sessions" << std::endl;
> -        pause_all_multimedia_sessions();
> +        switch (state)
> +        {
> +        case audio::OutputState::Private:
> +            std::cout << "AudioOutputObserver reports that output is not Private." << std::endl;
> +            break;
> +        case audio::OutputState::Public:
> +            std::cout << "AudioOutputObserver reports that output is now Public." << std::endl;
> +            pause_all_multimedia_sessions();
> +            break;
> +        }
>      });
>  
>      d->call_monitor->on_change([this](CallMonitor::State state) {
> @@ -475,6 +130,19 @@
>              break;
>          }
>      });
> +
> +    d->recorder_observer->recording_state().changed().connect([this](RecordingState state)
> +    {
> +        if (state == media::RecordingState::started)
> +        {
> +            d->display_state_lock->request_acquire(media::power::DisplayState::on);
> +            pause_all_multimedia_sessions();
> +        }
> +        else if (state == media::RecordingState::stopped)
> +        {
> +            d->display_state_lock->request_release(media::power::DisplayState::on);
> +        }
> +    });
>  }
>  
>  media::ServiceImplementation::~ServiceImplementation()
> 


-- 
https://code.launchpad.net/~phablet-team/media-hub/introduce-audio-output-observer-interface/+merge/242914
Your team Ubuntu Phablet Team is subscribed to branch lp:media-hub.



More information about the Ubuntu-reviews mailing list