/* * PipeWire audio output driver. * Copyright (C) 2021 Thomas Weißschuh * Copyright (C) 2021 Oschowa * Copyright (C) 2020 Andreas Kempf * * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * mpv 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 mpv. If not, see . */ #include #include #include #include #include #include #include "common/common.h" #include "common/msg.h" #include "options/m_config.h" #include "options/m_option.h" #include "ao.h" #include "audio/format.h" #include "internal.h" #include "osdep/timer.h" #if !PW_CHECK_VERSION(0, 3, 50) static inline int pw_stream_get_time_n(struct pw_stream *stream, struct pw_time *time, size_t size) { return pw_stream_get_time(stream, time); } #endif #if !PW_CHECK_VERSION(0, 3, 57) // Earlier versions segfault on zeroed hooks #define spa_hook_remove(hook) if ((hook)->link.prev) spa_hook_remove(hook) #endif #if !PW_CHECK_VERSION(1, 0, 4) static uint64_t pw_stream_get_nsec(struct pw_stream *stream) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return SPA_TIMESPEC_TO_NSEC(&ts); } #endif enum init_state { INIT_STATE_NONE, INIT_STATE_SUCCESS, INIT_STATE_ERROR, }; enum { VOLUME_MODE_CHANNEL, VOLUME_MODE_GLOBAL, }; struct priv { struct pw_thread_loop *loop; struct pw_stream *stream; struct pw_core *core; struct spa_hook stream_listener; struct spa_hook core_listener; enum init_state init_state; bool muted; float volume; struct { int buffer_msec; char *remote; int volume_mode; } options; struct { struct pw_registry *registry; struct spa_hook registry_listener; struct spa_list sinks; } hotplug; }; struct id_list { uint32_t id; struct spa_list node; }; static enum spa_audio_format af_fmt_to_pw(enum af_format format) { switch (format) { case AF_FORMAT_U8: return SPA_AUDIO_FORMAT_U8; case AF_FORMAT_S16: return SPA_AUDIO_FORMAT_S16; case AF_FORMAT_S32: return SPA_AUDIO_FORMAT_S32; case AF_FORMAT_FLOAT: return SPA_AUDIO_FORMAT_F32; case AF_FORMAT_DOUBLE: return SPA_AUDIO_FORMAT_F64; case AF_FORMAT_U8P: return SPA_AUDIO_FORMAT_U8P; case AF_FORMAT_S16P: return SPA_AUDIO_FORMAT_S16P; case AF_FORMAT_S32P: return SPA_AUDIO_FORMAT_S32P; case AF_FORMAT_FLOATP: return SPA_AUDIO_FORMAT_F32P; case AF_FORMAT_DOUBLEP: return SPA_AUDIO_FORMAT_F64P; default: return SPA_AUDIO_FORMAT_UNKNOWN; } } static enum spa_audio_iec958_codec af_fmt_to_codec(enum af_format format) { switch (format) { case AF_FORMAT_S_AAC: return SPA_AUDIO_IEC958_CODEC_MPEG2_AAC; case AF_FORMAT_S_AC3: return SPA_AUDIO_IEC958_CODEC_AC3; case AF_FORMAT_S_DTS: return SPA_AUDIO_IEC958_CODEC_DTS; case AF_FORMAT_S_DTSHD: return SPA_AUDIO_IEC958_CODEC_DTSHD; case AF_FORMAT_S_EAC3: return SPA_AUDIO_IEC958_CODEC_EAC3; case AF_FORMAT_S_MP3: return SPA_AUDIO_IEC958_CODEC_MPEG; case AF_FORMAT_S_TRUEHD: return SPA_AUDIO_IEC958_CODEC_TRUEHD; default: return SPA_AUDIO_IEC958_CODEC_UNKNOWN; } } static enum spa_audio_channel mp_speaker_id_to_spa(struct ao *ao, enum mp_speaker_id mp_speaker_id) { switch (mp_speaker_id) { case MP_SPEAKER_ID_FL: return SPA_AUDIO_CHANNEL_FL; case MP_SPEAKER_ID_FR: return SPA_AUDIO_CHANNEL_FR; case MP_SPEAKER_ID_FC: return SPA_AUDIO_CHANNEL_FC; case MP_SPEAKER_ID_LFE: return SPA_AUDIO_CHANNEL_LFE; case MP_SPEAKER_ID_BL: return SPA_AUDIO_CHANNEL_RL; case MP_SPEAKER_ID_BR: return SPA_AUDIO_CHANNEL_RR; case MP_SPEAKER_ID_FLC: return SPA_AUDIO_CHANNEL_FLC; case MP_SPEAKER_ID_FRC: return SPA_AUDIO_CHANNEL_FRC; case MP_SPEAKER_ID_BC: return SPA_AUDIO_CHANNEL_RC; case MP_SPEAKER_ID_SL: return SPA_AUDIO_CHANNEL_SL; case MP_SPEAKER_ID_SR: return SPA_AUDIO_CHANNEL_SR; case MP_SPEAKER_ID_TC: return SPA_AUDIO_CHANNEL_TC; case MP_SPEAKER_ID_TFL: return SPA_AUDIO_CHANNEL_TFL; case MP_SPEAKER_ID_TFC: return SPA_AUDIO_CHANNEL_TFC; case MP_SPEAKER_ID_TFR: return SPA_AUDIO_CHANNEL_TFR; case MP_SPEAKER_ID_TBL: return SPA_AUDIO_CHANNEL_TRL; case MP_SPEAKER_ID_TBC: return SPA_AUDIO_CHANNEL_TRC; case MP_SPEAKER_ID_TBR: return SPA_AUDIO_CHANNEL_TRR; case MP_SPEAKER_ID_DL: return SPA_AUDIO_CHANNEL_FL; case MP_SPEAKER_ID_DR: return SPA_AUDIO_CHANNEL_FR; case MP_SPEAKER_ID_WL: return SPA_AUDIO_CHANNEL_FL; case MP_SPEAKER_ID_WR: return SPA_AUDIO_CHANNEL_FR; case MP_SPEAKER_ID_SDL: return SPA_AUDIO_CHANNEL_SL; case MP_SPEAKER_ID_SDR: return SPA_AUDIO_CHANNEL_SL; case MP_SPEAKER_ID_LFE2: return SPA_AUDIO_CHANNEL_LFE2; case MP_SPEAKER_ID_TSL: return SPA_AUDIO_CHANNEL_TSL; case MP_SPEAKER_ID_TSR: return SPA_AUDIO_CHANNEL_TSR; case MP_SPEAKER_ID_BFC: return SPA_AUDIO_CHANNEL_BC; case MP_SPEAKER_ID_BFL: return SPA_AUDIO_CHANNEL_BLC; case MP_SPEAKER_ID_BFR: return SPA_AUDIO_CHANNEL_BRC; case MP_SPEAKER_ID_NA: return SPA_AUDIO_CHANNEL_NA; default: MP_WARN(ao, "Unhandled channel %d\n", mp_speaker_id); return SPA_AUDIO_CHANNEL_UNKNOWN; }; } static void on_process(void *userdata) { struct ao *ao = userdata; struct priv *p = ao->priv; struct pw_time time; struct pw_buffer *b; void *data[MP_NUM_CHANNELS]; if ((b = pw_stream_dequeue_buffer(p->stream)) == NULL) { MP_WARN(ao, "out of buffers: %s\n", mp_strerror(errno)); return; } struct spa_buffer *buf = b->buffer; int nframes = buf->datas[0].maxsize / ao->sstride; #if PW_CHECK_VERSION(0, 3, 49) if (b->requested != 0) nframes = MPMIN(b->requested, nframes); #endif for (int i = 0; i < buf->n_datas; i++) data[i] = buf->datas[i].data; pw_stream_get_time_n(p->stream, &time, sizeof(time)); if (time.rate.denom == 0) time.rate.denom = ao->samplerate; if (time.rate.num == 0) time.rate.num = 1; int64_t end_time = mp_time_ns(); end_time += MP_TIME_S_TO_NS(nframes) / ao->samplerate; end_time += MP_TIME_S_TO_NS(time.delay) * time.rate.num / time.rate.denom; end_time += MP_TIME_S_TO_NS(time.queued) / ao->samplerate; #if PW_CHECK_VERSION(0, 3, 50) end_time += MP_TIME_S_TO_NS(time.buffered) / ao->samplerate; #endif end_time -= pw_stream_get_nsec(p->stream) - time.now; int samples = ao_read_data_nonblocking(ao, data, nframes, end_time); b->size = samples; for (int i = 0; i < buf->n_datas; i++) { buf->datas[i].chunk->size = samples * ao->sstride; buf->datas[i].chunk->offset = 0; buf->datas[i].chunk->stride = ao->sstride; } pw_stream_queue_buffer(p->stream, b); MP_TRACE(ao, "queued %d of %d samples\n", samples, nframes); } static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) { struct ao *ao = userdata; struct priv *p = ao->priv; const struct spa_pod *params[1]; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); /* We want to know when our node is linked. * As there is no proper callback for this we use the Latency param for this */ if (id == SPA_PARAM_Latency) { p->init_state = INIT_STATE_SUCCESS; pw_thread_loop_signal(p->loop, false); } if (param == NULL || id != SPA_PARAM_Format) return; int buffer_size = ao->device_buffer * ao->sstride; params[0] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(ao->num_planes), SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int( buffer_size, 0, INT32_MAX), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ao->sstride)); if (!params[0]) { MP_ERR(ao, "Could not build parameter pod\n"); return; } if (pw_stream_update_params(p->stream, params, 1) < 0) { MP_ERR(ao, "Could not update stream parameters\n"); return; } } static void on_state_changed(void *userdata, enum pw_stream_state old, enum pw_stream_state state, const char *error) { struct ao *ao = userdata; struct priv *p = ao->priv; MP_DBG(ao, "Stream state changed: old_state=%s state=%s error=%s\n", pw_stream_state_as_string(old), pw_stream_state_as_string(state), error); if (state == PW_STREAM_STATE_ERROR) { MP_WARN(ao, "Stream in error state, trying to reload...\n"); p->init_state = INIT_STATE_ERROR; pw_thread_loop_signal(p->loop, false); ao_request_reload(ao); } if (state == PW_STREAM_STATE_UNCONNECTED && old != PW_STREAM_STATE_UNCONNECTED) { MP_WARN(ao, "Stream disconnected, trying to reload...\n"); ao_request_reload(ao); } } static float spa_volume_to_mp_volume(float vol) { return vol * 100; } static float mp_volume_to_spa_volume(float vol) { return vol / 100; } static float volume_avg(float* vols, uint32_t n) { float sum = 0.0; for (int i = 0; i < n; i++) sum += vols[i]; return sum / n; } static void on_control_info(void *userdata, uint32_t id, const struct pw_stream_control *control) { struct ao *ao = userdata; struct priv *p = ao->priv; switch (id) { case SPA_PROP_mute: if (control->n_values == 1) p->muted = control->values[0] >= 0.5; break; case SPA_PROP_channelVolumes: if (p->options.volume_mode != VOLUME_MODE_CHANNEL) break; if (control->n_values > 0) p->volume = volume_avg(control->values, control->n_values); break; case SPA_PROP_volume: if (p->options.volume_mode != VOLUME_MODE_GLOBAL) break; if (control->n_values > 0) p->volume = control->values[0]; break; } } static const struct pw_stream_events stream_events = { .version = PW_VERSION_STREAM_EVENTS, .param_changed = on_param_changed, .process = on_process, .state_changed = on_state_changed, .control_info = on_control_info, }; static void uninit(struct ao *ao) { struct priv *p = ao->priv; if (p->loop) pw_thread_loop_stop(p->loop); spa_hook_remove(&p->stream_listener); spa_zero(p->stream_listener); if (p->stream) pw_stream_destroy(p->stream); p->stream = NULL; if (p->core) pw_context_destroy(pw_core_get_context(p->core)); p->core = NULL; if (p->loop) pw_thread_loop_destroy(p->loop); p->loop = NULL; pw_deinit(); } struct registry_event_global_ctx { struct ao *ao; void (*sink_cb) (struct ao *ao, uint32_t id, const struct spa_dict *props, void *sink_cb_ctx); void *sink_cb_ctx; }; static bool is_sink_node(const char *type, const struct spa_dict *props) { if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return false; if (!props) return false; const char *class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); if (!class || strcmp(class, "Audio/Sink") != 0) return false; return true; } static void for_each_sink_registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { struct registry_event_global_ctx *ctx = data; if (!is_sink_node(type, props)) return; ctx->sink_cb(ctx->ao, id, props, ctx->sink_cb_ctx); } struct for_each_done_ctx { struct pw_thread_loop *loop; bool done; }; static const struct pw_registry_events for_each_sink_registry_events = { .version = PW_VERSION_REGISTRY_EVENTS, .global = for_each_sink_registry_event_global, }; static void for_each_sink_done(void *data, uint32_t it, int seq) { struct for_each_done_ctx *ctx = data; ctx->done = true; pw_thread_loop_signal(ctx->loop, false); } static const struct pw_core_events for_each_sink_core_events = { .version = PW_VERSION_CORE_EVENTS, .done = for_each_sink_done, }; static int for_each_sink(struct ao *ao, void (cb) (struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx), void *cb_ctx) { struct priv *priv = ao->priv; struct pw_registry *registry; struct spa_hook core_listener; struct for_each_done_ctx done_ctx = { .loop = priv->loop, .done = false, }; int ret = -1; pw_thread_loop_lock(priv->loop); spa_zero(core_listener); if (pw_core_add_listener(priv->core, &core_listener, &for_each_sink_core_events, &done_ctx) < 0) goto unlock_loop; registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); if (!registry) goto remove_core_listener; pw_core_sync(priv->core, 0, 0); struct spa_hook registry_listener; struct registry_event_global_ctx revents_ctx = { .ao = ao, .sink_cb = cb, .sink_cb_ctx = cb_ctx, }; spa_zero(registry_listener); if (pw_registry_add_listener(registry, ®istry_listener, &for_each_sink_registry_events, &revents_ctx) < 0) goto destroy_registry; while (!done_ctx.done) pw_thread_loop_wait(priv->loop); spa_hook_remove(®istry_listener); ret = 0; destroy_registry: pw_proxy_destroy((struct pw_proxy *)registry); remove_core_listener: spa_hook_remove(&core_listener); unlock_loop: pw_thread_loop_unlock(priv->loop); return ret; } static void have_sink(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) { bool *b = ctx; *b = true; } static bool session_has_sinks(struct ao *ao) { bool b = false; if (for_each_sink(ao, have_sink, &b) < 0) MP_WARN(ao, "Could not list devices, sink detection may be wrong\n"); return b; } static void on_error(void *data, uint32_t id, int seq, int res, const char *message) { struct ao *ao = data; MP_WARN(ao, "Error during playback: %s, %s\n", spa_strerror(res), message); } static void on_core_info(void *data, const struct pw_core_info *info) { struct ao *ao = data; MP_VERBOSE(ao, "Core user: %s\n", info->user_name); MP_VERBOSE(ao, "Core host: %s\n", info->host_name); MP_VERBOSE(ao, "Core version: %s\n", info->version); MP_VERBOSE(ao, "Core name: %s\n", info->name); } static const struct pw_core_events core_events = { .version = PW_VERSION_CORE_EVENTS, .error = on_error, .info = on_core_info, }; static int pipewire_init_boilerplate(struct ao *ao) { struct priv *p = ao->priv; struct pw_context *context; pw_init(NULL, NULL); MP_VERBOSE(ao, "Headers version: %s\n", pw_get_headers_version()); MP_VERBOSE(ao, "Library version: %s\n", pw_get_library_version()); p->loop = pw_thread_loop_new("mpv/ao/pipewire", NULL); if (p->loop == NULL) return -1; pw_thread_loop_lock(p->loop); if (pw_thread_loop_start(p->loop) < 0) goto error; context = pw_context_new( pw_thread_loop_get_loop(p->loop), pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL), 0); if (!context) goto error; p->core = pw_context_connect( context, pw_properties_new(PW_KEY_REMOTE_NAME, p->options.remote, NULL), 0); if (!p->core) { MP_MSG(ao, ao->probing ? MSGL_V : MSGL_ERR, "Could not connect to context '%s': %s\n", p->options.remote, mp_strerror(errno)); pw_context_destroy(context); goto error; } if (pw_core_add_listener(p->core, &p->core_listener, &core_events, ao) < 0) goto error; pw_thread_loop_unlock(p->loop); if (!session_has_sinks(ao)) { MP_VERBOSE(ao, "PipeWire does not have any audio sinks, skipping\n"); return -1; } return 0; error: pw_thread_loop_unlock(p->loop); return -1; } static void wait_for_init_done(struct ao *ao) { struct priv *p = ao->priv; struct timespec abstime; int r; r = pw_thread_loop_get_time(p->loop, &abstime, 50 * SPA_NSEC_PER_MSEC); if (r < 0) { MP_WARN(ao, "Could not get timeout for initialization: %s\n", spa_strerror(r)); return; } while (p->init_state == INIT_STATE_NONE) { r = pw_thread_loop_timed_wait_full(p->loop, &abstime); if (r < 0) { MP_WARN(ao, "Could not wait for initialization: %s\n", spa_strerror(r)); return; } } } static int init(struct ao *ao) { struct priv *p = ao->priv; uint8_t buffer[1024]; struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); const struct spa_pod *params[1]; struct pw_properties *props = pw_properties_new( PW_KEY_MEDIA_TYPE, "Audio", PW_KEY_MEDIA_CATEGORY, "Playback", PW_KEY_MEDIA_ROLE, ao->init_flags & AO_INIT_MEDIA_ROLE_MUSIC ? "Music" : "Movie", PW_KEY_NODE_NAME, ao->client_name, PW_KEY_NODE_DESCRIPTION, ao->client_name, PW_KEY_APP_NAME, ao->client_name, PW_KEY_APP_ID, ao->client_name, PW_KEY_APP_ICON_NAME, ao->client_name, PW_KEY_NODE_ALWAYS_PROCESS, "true", PW_KEY_TARGET_OBJECT, ao->device, NULL ); if (pipewire_init_boilerplate(ao) < 0) goto error_props; if (p->options.buffer_msec) { ao->device_buffer = p->options.buffer_msec * ao->samplerate / 1000; pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%d/%d", ao->device_buffer, ao->samplerate); } pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%d", ao->samplerate); if (af_fmt_is_spdif(ao->format)) { enum spa_audio_iec958_codec spa_codec = af_fmt_to_codec(ao->format); if (spa_codec == SPA_AUDIO_IEC958_CODEC_UNKNOWN) { MP_ERR(ao, "Unhandled codec %d\n", ao->format); goto error_props; } struct spa_audio_info_iec958 audio_info = { .codec = spa_codec, .rate = ao->samplerate, }; params[0] = spa_format_audio_iec958_build(&b, SPA_PARAM_EnumFormat, &audio_info); if (!params[0]) goto error_props; } else { enum spa_audio_format spa_format = af_fmt_to_pw(ao->format); if (spa_format == SPA_AUDIO_FORMAT_UNKNOWN) { MP_ERR(ao, "Unhandled format %d\n", ao->format); goto error_props; } struct spa_audio_info_raw audio_info = { .format = spa_format, .rate = ao->samplerate, .channels = ao->channels.num, }; for (int i = 0; i < ao->channels.num; i++) audio_info.position[i] = mp_speaker_id_to_spa(ao, ao->channels.speaker[i]); params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &audio_info); if (!params[0]) goto error_props; } if (af_fmt_is_planar(ao->format)) { ao->num_planes = ao->channels.num; ao->sstride = af_fmt_to_bytes(ao->format); } else { ao->num_planes = 1; ao->sstride = ao->channels.num * af_fmt_to_bytes(ao->format); } pw_thread_loop_lock(p->loop); p->stream = pw_stream_new(p->core, "audio-src", props); if (p->stream == NULL) { pw_thread_loop_unlock(p->loop); goto error; } pw_stream_add_listener(p->stream, &p->stream_listener, &stream_events, ao); enum pw_stream_flags flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_INACTIVE | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS; if (ao->init_flags & AO_INIT_EXCLUSIVE) flags |= PW_STREAM_FLAG_EXCLUSIVE; if (pw_stream_connect(p->stream, PW_DIRECTION_OUTPUT, PW_ID_ANY, flags, params, 1) < 0) { pw_thread_loop_unlock(p->loop); goto error; } wait_for_init_done(ao); pw_thread_loop_unlock(p->loop); if (p->init_state == INIT_STATE_ERROR) goto error; return 0; error_props: pw_properties_free(props); error: uninit(ao); return -1; } static void reset(struct ao *ao) { struct priv *p = ao->priv; pw_thread_loop_lock(p->loop); pw_stream_set_active(p->stream, false); pw_stream_flush(p->stream, false); pw_thread_loop_unlock(p->loop); } static void start(struct ao *ao) { struct priv *p = ao->priv; pw_thread_loop_lock(p->loop); pw_stream_set_active(p->stream, true); pw_thread_loop_unlock(p->loop); } static bool set_pause(struct ao *ao, bool paused) { struct priv *p = ao->priv; pw_thread_loop_lock(p->loop); pw_stream_set_active(p->stream, !paused); pw_thread_loop_unlock(p->loop); return true; } #define CONTROL_RET(r) (!r ? CONTROL_OK : CONTROL_ERROR) static int control(struct ao *ao, enum aocontrol cmd, void *arg) { struct priv *p = ao->priv; switch (cmd) { case AOCONTROL_GET_VOLUME: { float *vol = arg; *vol = spa_volume_to_mp_volume(p->volume); return CONTROL_OK; } case AOCONTROL_GET_MUTE: { bool *muted = arg; *muted = p->muted; return CONTROL_OK; } case AOCONTROL_SET_VOLUME: case AOCONTROL_SET_MUTE: case AOCONTROL_UPDATE_STREAM_TITLE: { int ret; pw_thread_loop_lock(p->loop); switch (cmd) { case AOCONTROL_SET_VOLUME: { float *vol = arg; uint8_t n = ao->channels.num; if (p->options.volume_mode == VOLUME_MODE_CHANNEL) { float values[MP_NUM_CHANNELS] = {0}; for (int i = 0; i < n; i++) values[i] = mp_volume_to_spa_volume(*vol); ret = CONTROL_RET(pw_stream_set_control( p->stream, SPA_PROP_channelVolumes, n, values, 0)); } else { float value = mp_volume_to_spa_volume(*vol); ret = CONTROL_RET(pw_stream_set_control( p->stream, SPA_PROP_volume, 1, &value, 0)); } break; } case AOCONTROL_SET_MUTE: { bool *muted = arg; float value = *muted ? 1.f : 0.f; ret = CONTROL_RET(pw_stream_set_control(p->stream, SPA_PROP_mute, 1, &value, 0)); break; } case AOCONTROL_UPDATE_STREAM_TITLE: { char *title = arg; struct spa_dict_item items[1]; items[0] = SPA_DICT_ITEM_INIT(PW_KEY_MEDIA_NAME, title); ret = CONTROL_RET(pw_stream_update_properties(p->stream, &SPA_DICT_INIT(items, MP_ARRAY_SIZE(items)))); break; } default: ret = CONTROL_NA; } pw_thread_loop_unlock(p->loop); return ret; } default: return CONTROL_UNKNOWN; } } static void add_device_to_list(struct ao *ao, uint32_t id, const struct spa_dict *props, void *ctx) { struct ao_device_list *list = ctx; const char *name = spa_dict_lookup(props, PW_KEY_NODE_NAME); if (!name) return; const char *description = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); ao_device_list_add(list, ao, &(struct ao_device_desc){name, description}); } static void hotplug_registry_global_cb(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { struct ao *ao = data; struct priv *priv = ao->priv; if (!is_sink_node(type, props)) return; pw_thread_loop_lock(priv->loop); struct id_list *item = talloc(ao, struct id_list); item->id = id; spa_list_init(&item->node); spa_list_append(&priv->hotplug.sinks, &item->node); pw_thread_loop_unlock(priv->loop); ao_hotplug_event(ao); } static void hotplug_registry_global_remove_cb(void *data, uint32_t id) { struct ao *ao = data; struct priv *priv = ao->priv; bool removed_sink = false; struct id_list *e; pw_thread_loop_lock(priv->loop); spa_list_for_each(e, &priv->hotplug.sinks, node) { if (e->id == id) { removed_sink = true; spa_list_remove(&e->node); talloc_free(e); break; } } pw_thread_loop_unlock(priv->loop); if (removed_sink) ao_hotplug_event(ao); } static const struct pw_registry_events hotplug_registry_events = { .version = PW_VERSION_REGISTRY_EVENTS, .global = hotplug_registry_global_cb, .global_remove = hotplug_registry_global_remove_cb, }; static int hotplug_init(struct ao *ao) { struct priv *priv = ao->priv; int res = pipewire_init_boilerplate(ao); if (res) goto error_no_unlock; pw_thread_loop_lock(priv->loop); spa_zero(priv->hotplug); spa_list_init(&priv->hotplug.sinks); priv->hotplug.registry = pw_core_get_registry(priv->core, PW_VERSION_REGISTRY, 0); if (!priv->hotplug.registry) goto error; if (pw_registry_add_listener(priv->hotplug.registry, &priv->hotplug.registry_listener, &hotplug_registry_events, ao) < 0) { pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); goto error; } pw_thread_loop_unlock(priv->loop); return res; error: pw_thread_loop_unlock(priv->loop); error_no_unlock: uninit(ao); return -1; } static void hotplug_uninit(struct ao *ao) { struct priv *priv = ao->priv; pw_thread_loop_lock(priv->loop); spa_hook_remove(&priv->hotplug.registry_listener); pw_proxy_destroy((struct pw_proxy *)priv->hotplug.registry); pw_thread_loop_unlock(priv->loop); uninit(ao); } static void list_devs(struct ao *ao, struct ao_device_list *list) { ao_device_list_add(list, ao, &(struct ao_device_desc){}); if (for_each_sink(ao, add_device_to_list, list) < 0) MP_WARN(ao, "Could not list devices, list may be incomplete\n"); } #define OPT_BASE_STRUCT struct priv const struct ao_driver audio_out_pipewire = { .description = "PipeWire audio output", .name = "pipewire", .init = init, .uninit = uninit, .reset = reset, .start = start, .set_pause = set_pause, .control = control, .hotplug_init = hotplug_init, .hotplug_uninit = hotplug_uninit, .list_devs = list_devs, .priv_size = sizeof(struct priv), .priv_defaults = &(const struct priv) { .loop = NULL, .stream = NULL, .init_state = INIT_STATE_NONE, .options.buffer_msec = 0, .options.volume_mode = VOLUME_MODE_CHANNEL, }, .options_prefix = "pipewire", .options = (const struct m_option[]) { {"buffer", OPT_CHOICE(options.buffer_msec, {"native", 0}), M_RANGE(1, 2000)}, {"remote", OPT_STRING(options.remote) }, {"volume-mode", OPT_CHOICE(options.volume_mode, {"channel", VOLUME_MODE_CHANNEL}, {"global", VOLUME_MODE_GLOBAL})}, {0} }, };