/** * Server info callback */ static void pulse_server_info(pa_context *c, const pa_server_info *i, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); blog(LOG_INFO, "Server name: '%s %s'", i->server_name, i->server_version); if (data->device && strcmp("default", data->device) == 0) { if (data->input) { bfree(data->device); data->device = bstrdup(i->default_source_name); blog(LOG_DEBUG, "Default input device: '%s'", data->device); } else { char *monitor = bzalloc(strlen(i->default_sink_name) + 9); strcat(monitor, i->default_sink_name); strcat(monitor, ".monitor"); bfree(data->device); data->device = bstrdup(monitor); blog(LOG_DEBUG, "Default output device: '%s'", data->device); bfree(monitor); } } pulse_signal(0); }
/** * Destroy the plugin object and free all memory */ static void pulse_destroy(void *vptr) { PULSE_DATA(vptr); if (!data) return; if (data->stream) pulse_stop_recording(data); pulse_unref(); if (data->device) bfree(data->device); bfree(data); }
/* * Server info callback, this is called from pa_mainloop_dispatch * TODO: how to free the server info struct ? */ static void pulse_get_server_info_cb(pa_context *c, const pa_server_info *i, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); const pa_sample_spec *spec = &i->sample_spec; data->format = spec->format; data->samples_per_sec = spec->rate; blog(LOG_DEBUG, "pulse-input: Default format: %s, %u Hz, %u channels", pa_sample_format_to_string(spec->format), spec->rate, spec->channels); }
/** * Callback for pulse which gets executed when new audio data is available * * @warning The function may be called even after disconnecting the stream */ static void pulse_stream_read(pa_stream *p, size_t nbytes, void *userdata) { UNUSED_PARAMETER(p); UNUSED_PARAMETER(nbytes); PULSE_DATA(userdata); const void *frames; size_t bytes; int64_t latency; if (!data->stream) goto exit; pa_stream_peek(data->stream, &frames, &bytes); // check if we got data if (!bytes) goto exit; if (!frames) { blog(LOG_ERROR, "pulse-input: Got audio hole of %u bytes", (unsigned int) bytes); pa_stream_drop(data->stream); goto exit; } if (pulse_get_stream_latency(data->stream, &latency) < 0) { blog(LOG_ERROR, "pulse-input: Failed to get timing info !"); pa_stream_drop(data->stream); goto exit; } struct source_audio out; out.speakers = data->speakers; out.samples_per_sec = data->samples_per_sec; out.format = pulse_to_obs_audio_format(data->format); out.data[0] = (uint8_t *) frames; out.frames = bytes / data->bytes_per_frame; out.timestamp = os_gettime_ns() - (latency * 1000ULL); obs_source_output_audio(data->source, &out); data->packets++; data->frames += out.frames; pa_stream_drop(data->stream); exit: pulse_signal(0); }
/* * Destroy the plugin object and free all memory */ static void pulse_destroy(void *vptr) { PULSE_DATA(vptr); if (!data) return; pulse_stop_recording(data); pulse_unref(); if (data->device) bfree(data->device); bfree(data); blog(LOG_DEBUG, "pulse-input: Input destroyed"); }
/** * Callback for pulse which gets executed when new audio data is available * * @warning The function may be called even after disconnecting the stream */ static void pulse_stream_read(pa_stream *p, size_t nbytes, void *userdata) { UNUSED_PARAMETER(p); UNUSED_PARAMETER(nbytes); PULSE_DATA(userdata); const void *frames; size_t bytes; if (!data->stream) goto exit; pa_stream_peek(data->stream, &frames, &bytes); // check if we got data if (!bytes) goto exit; if (!frames) { blog(LOG_ERROR, "Got audio hole of %u bytes", (unsigned int) bytes); pa_stream_drop(data->stream); goto exit; } struct obs_source_audio out; out.speakers = data->speakers; out.samples_per_sec = data->samples_per_sec; out.format = pulse_to_obs_audio_format(data->format); out.data[0] = (uint8_t *) frames; out.frames = bytes / data->bytes_per_frame; out.timestamp = get_sample_time(out.frames, out.samples_per_sec); if (!data->first_ts) data->first_ts = out.timestamp + STARTUP_TIMEOUT_NS; if (out.timestamp > data->first_ts) obs_source_output_audio(data->source, &out); data->packets++; data->frames += out.frames; pa_stream_drop(data->stream); exit: pulse_signal(0); }
/** * Source info callback * * We use the default stream settings for recording here unless pulse is * configured to something obs can't deal with. */ static void pulse_source_info(pa_context *c, const pa_source_info *i, int eol, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); // An error occured if (eol < 0) { data->format = PA_SAMPLE_INVALID; goto skip; } // Terminating call for multi instance callbacks if (eol > 0) goto skip; blog(LOG_INFO, "Audio format: %s, %"PRIu32" Hz" ", %"PRIu8" channels", pa_sample_format_to_string(i->sample_spec.format), i->sample_spec.rate, i->sample_spec.channels); pa_sample_format_t format = i->sample_spec.format; if (pulse_to_obs_audio_format(format) == AUDIO_FORMAT_UNKNOWN) { format = PA_SAMPLE_S16LE; blog(LOG_INFO, "Sample format %s not supported by OBS," "using %s instead for recording", pa_sample_format_to_string(i->sample_spec.format), pa_sample_format_to_string(format)); } uint8_t channels = i->sample_spec.channels; if (pulse_channels_to_obs_speakers(channels) == SPEAKERS_UNKNOWN) { channels = 2; blog(LOG_INFO, "%c channels not supported by OBS," "using %c instead for recording", i->sample_spec.channels, channels); } data->format = format; data->samples_per_sec = i->sample_spec.rate; data->channels = channels; skip: pulse_signal(0); }
/* * Server info callback */ static void pulse_server_info(pa_context *c, const pa_server_info *i, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); data->format = i->sample_spec.format; data->samples_per_sec = i->sample_spec.rate; data->channels = i->sample_spec.channels; blog(LOG_DEBUG, "pulse-input: Default format: %s, %u Hz, %u channels", pa_sample_format_to_string(i->sample_spec.format), i->sample_spec.rate, i->sample_spec.channels); pulse_signal(0); }
/** * Update the input settings */ static void pulse_update(void *vptr, obs_data_t *settings) { PULSE_DATA(vptr); bool restart = false; const char *new_device; new_device = obs_data_get_string(settings, "device_id"); if (!data->device || strcmp(data->device, new_device) != 0) { if (data->device) bfree(data->device); data->device = bstrdup(new_device); restart = true; } if (!restart) return; if (data->stream) pulse_stop_recording(data); pulse_start_recording(data); }
/** * Server info callback */ static void pulse_server_info(pa_context *c, const pa_server_info *i, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); blog(LOG_INFO, "pulse-input: Server name: '%s %s'", i->server_name, i->server_version); data->format = i->sample_spec.format; data->samples_per_sec = i->sample_spec.rate; data->channels = i->sample_spec.channels; blog(LOG_INFO, "pulse-input: " "Audio format: %s, %u Hz, %u channels", pa_sample_format_to_string(i->sample_spec.format), i->sample_spec.rate, i->sample_spec.channels); pulse_signal(0); }
/* * Destroy the plugin object and free all memory */ static void pulse_destroy(void *vptr) { PULSE_DATA(vptr); if (!data) return; if (data->thread) { void *ret; os_event_signal(data->event); pthread_join(data->thread, &ret); } os_event_destroy(data->event); pa_proplist_free(data->props); blog(LOG_DEBUG, "pulse-input: Input destroyed"); bfree(data); }
/* * Worker thread to get audio data * * Will run until signaled */ static void *pulse_thread(void *vptr) { PULSE_DATA(vptr); if (pulse_connect(data) < 0) return NULL; if (pulse_get_server_info(data) < 0) return NULL; if (pulse_connect_stream(data) < 0) return NULL; if (pulse_skip(data) < 0) return NULL; blog(LOG_DEBUG, "pulse-input: Start recording"); const void *frames; size_t bytes; uint64_t pa_time; int64_t pa_latency; struct source_audio out; out.speakers = data->speakers; out.samples_per_sec = data->samples_per_sec; out.format = pulse_to_obs_audio_format(data->format); while (os_event_try(data->event) == EAGAIN) { pulse_iterate(data); pa_stream_peek(data->stream, &frames, &bytes); // check if we got data if (!bytes) continue; if (!frames) { blog(LOG_DEBUG, "pulse-input: Got audio hole of %u bytes", (unsigned int) bytes); pa_stream_drop(data->stream); continue; } if (pa_stream_get_time(data->stream, &pa_time) < 0) { blog(LOG_ERROR, "pulse-input: Failed to get timing info !"); pa_stream_drop(data->stream); continue; } pulse_get_stream_latency(data->stream, &pa_latency); out.data[0] = (uint8_t *) frames; out.frames = frames_to_bytes(data, bytes); out.timestamp = (pa_time - pa_latency) * 1000; obs_source_output_audio(data->source, &out); pa_stream_drop(data->stream); } pulse_diconnect_stream(data); pulse_disconnect(data); return NULL; }