The producer's presentation ticks start at zero per session and sit in its clock domain; handed to the mixer they read as ever-growing drift, and OBS kept adding buffering and restarting the source's audio for lagging. OBS's own network sources stamp with the receive clock; so does this one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
536 lines
18 KiB
C
536 lines
18 KiB
C
/* The OBS source. Owns no transport, no reassembly, no discovery: all of that
|
|
* is ratatoskr-core behind ratatoskr.h. What this file owns is the OBS
|
|
* lifecycle — properties, activation, and handing what the core delivers to
|
|
* OBS in the shape OBS wants.
|
|
*
|
|
* Video goes through a private child `ffmpeg_source` reading a raw H.264
|
|
* byte stream from a loopback UDP port the core relays into; OBS's own
|
|
* decoder does the decoding and this source draws the child. Audio is PCM
|
|
* and goes straight to obs_source_output_audio. That is the arrangement the
|
|
* previous plugin proved in the field; what it hand-rolled underneath it is
|
|
* what the core replaces.
|
|
*/
|
|
|
|
#include <obs-module.h>
|
|
#include <util/platform.h>
|
|
#include <util/threading.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "ratatoskr.h"
|
|
|
|
OBS_DECLARE_MODULE()
|
|
OBS_MODULE_USE_DEFAULT_LOCALE("ratatoskr", "en-US")
|
|
|
|
#define SETTING_ODIN "odin"
|
|
#define SETTING_RECEIVER_ID "receiver_id"
|
|
#define SETTING_STREAM "stream_id"
|
|
#define SETTING_VIDEO_SOURCE "video_source_id"
|
|
#define SETTING_AUDIO_SOURCE "audio_source_id"
|
|
#define SETTING_VIDEO_CODEC "video_codec"
|
|
#define SETTING_AUDIO_CODEC "audio_codec"
|
|
#define SETTING_BITRATE "video_bitrate_kbps"
|
|
#define SETTING_LATENCY "latency_budget_ms"
|
|
#define SETTING_STATUS "status"
|
|
|
|
/* Odin's canonical rendezvous endpoint on the mesh, the one every GameCult
|
|
* consumer uses. Idunn's route keeps it stable across releases; the daemon's
|
|
* own port behind it moves. */
|
|
#define DEFAULT_ODIN "rudp://10.77.0.1:17871"
|
|
#define NONE_ID ""
|
|
|
|
struct ratatoskr_source {
|
|
obs_source_t *source;
|
|
|
|
/* settings as last applied */
|
|
char *odin;
|
|
char *receiver_id;
|
|
char *stream_id;
|
|
char *video_source_id;
|
|
char *audio_source_id;
|
|
char *video_codec;
|
|
char *audio_codec;
|
|
unsigned int bitrate_kbps;
|
|
unsigned int latency_ms;
|
|
|
|
/* what Odin last said */
|
|
RatatoskrCatalog *catalog;
|
|
pthread_mutex_t catalog_mutex;
|
|
|
|
/* the live subscription */
|
|
RatatoskrHandle *receiver;
|
|
obs_source_t *video_child;
|
|
uint16_t relay_port;
|
|
uint32_t audio_sample_rate;
|
|
uint32_t audio_channels;
|
|
size_t catalog_index;
|
|
|
|
pthread_t pump;
|
|
volatile bool pump_running;
|
|
bool active;
|
|
|
|
uint8_t *buffer;
|
|
size_t buffer_capacity;
|
|
};
|
|
|
|
static void log_last_error(const char *what)
|
|
{
|
|
char message[512];
|
|
ratatoskr_last_error(message, sizeof(message));
|
|
blog(LOG_WARNING, "[ratatoskr] %s: %s", what, message);
|
|
}
|
|
|
|
static void set_string(char **slot, const char *value)
|
|
{
|
|
bfree(*slot);
|
|
*slot = bstrdup(value ? value : "");
|
|
}
|
|
|
|
/* ---- catalog ---- */
|
|
|
|
static void refresh_catalog(struct ratatoskr_source *ctx)
|
|
{
|
|
char state_dir[1024];
|
|
const char *config = obs_module_config_path("");
|
|
snprintf(state_dir, sizeof(state_dir), "%s", config ? config : ".");
|
|
bfree((void *)config);
|
|
os_mkdirs(state_dir);
|
|
|
|
RatatoskrCatalog *fresh = NULL;
|
|
int rc = ratatoskr_catalog_pull(ctx->odin, ctx->receiver_id, state_dir, &fresh);
|
|
if (rc != RATATOSKR_OK) {
|
|
log_last_error("catalog pull from Odin failed");
|
|
return;
|
|
}
|
|
pthread_mutex_lock(&ctx->catalog_mutex);
|
|
RatatoskrCatalog *old = ctx->catalog;
|
|
ctx->catalog = fresh;
|
|
pthread_mutex_unlock(&ctx->catalog_mutex);
|
|
ratatoskr_catalog_close(old);
|
|
blog(LOG_INFO, "[ratatoskr] Odin advertises %zu stream(s), %zu malformed", ratatoskr_catalog_count(fresh),
|
|
ratatoskr_catalog_rejected(fresh));
|
|
}
|
|
|
|
static bool find_stream(struct ratatoskr_source *ctx, const char *stream_id, size_t *out_index,
|
|
RatatoskrStreamInfo *out_info)
|
|
{
|
|
if (!ctx->catalog || !stream_id || !*stream_id)
|
|
return false;
|
|
size_t count = ratatoskr_catalog_count(ctx->catalog);
|
|
for (size_t i = 0; i < count; i++) {
|
|
RatatoskrStreamInfo info;
|
|
if (ratatoskr_catalog_stream(ctx->catalog, i, &info) != RATATOSKR_OK)
|
|
continue;
|
|
if (strcmp(info.stream_id, stream_id) == 0) {
|
|
*out_index = i;
|
|
*out_info = info;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ---- media ---- */
|
|
|
|
static void release_video_child(struct ratatoskr_source *ctx)
|
|
{
|
|
if (!ctx->video_child)
|
|
return;
|
|
obs_source_remove_active_child(ctx->source, ctx->video_child);
|
|
obs_source_release(ctx->video_child);
|
|
ctx->video_child = NULL;
|
|
}
|
|
|
|
static void create_video_child(struct ratatoskr_source *ctx)
|
|
{
|
|
char url[64];
|
|
snprintf(url, sizeof(url), "udp://127.0.0.1:%u", (unsigned)ctx->relay_port);
|
|
obs_data_t *settings = obs_data_create();
|
|
obs_data_set_bool(settings, "is_local_file", false);
|
|
obs_data_set_string(settings, "input", url);
|
|
obs_data_set_string(settings, "input_format", "h264");
|
|
obs_data_set_string(settings, "ffmpeg_options",
|
|
"fflags=nobuffer flags=low_delay probesize=32768 analyzeduration=0");
|
|
obs_data_set_bool(settings, "restart_on_activate", true);
|
|
obs_data_set_bool(settings, "clear_on_media_end", false);
|
|
obs_data_set_bool(settings, "close_when_inactive", false);
|
|
obs_data_set_bool(settings, "hw_decode", true);
|
|
ctx->video_child = obs_source_create_private("ffmpeg_source", "Ratatoskr video", settings);
|
|
obs_data_release(settings);
|
|
if (!ctx->video_child) {
|
|
blog(LOG_WARNING, "[ratatoskr] could not create the ffmpeg_source child for %s", url);
|
|
return;
|
|
}
|
|
obs_source_add_active_child(ctx->source, ctx->video_child);
|
|
}
|
|
|
|
/* Live audio is stamped with the receive clock, as OBS's own network sources
|
|
* do. The producer's presentation ticks start at zero per session and sit in
|
|
* its clock domain; handing them to the mixer read as ever-growing drift and
|
|
* OBS kept adding buffering, then restarted the source's audio for lagging. */
|
|
static void output_audio(struct ratatoskr_source *ctx, const uint8_t *pcm, size_t len, int64_t pts_ns)
|
|
{
|
|
UNUSED_PARAMETER(pts_ns);
|
|
if (ctx->audio_channels == 0 || ctx->audio_sample_rate == 0)
|
|
return;
|
|
size_t frame_bytes = sizeof(float) * ctx->audio_channels;
|
|
if (len < frame_bytes)
|
|
return;
|
|
struct obs_source_audio audio = {0};
|
|
audio.data[0] = pcm;
|
|
audio.frames = (uint32_t)(len / frame_bytes);
|
|
audio.samples_per_sec = ctx->audio_sample_rate;
|
|
audio.format = AUDIO_FORMAT_FLOAT;
|
|
audio.speakers = ctx->audio_channels == 1 ? SPEAKERS_MONO : SPEAKERS_STEREO;
|
|
audio.timestamp = os_gettime_ns();
|
|
obs_source_output_audio(ctx->source, &audio);
|
|
}
|
|
|
|
static void *pump_loop(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
os_set_thread_name("ratatoskr-pump");
|
|
while (ctx->pump_running) {
|
|
int waiting = ratatoskr_receiver_poll(ctx->receiver);
|
|
if (waiting < 0) {
|
|
log_last_error("poll failed");
|
|
os_sleep_ms(50);
|
|
continue;
|
|
}
|
|
for (;;) {
|
|
size_t len = 0;
|
|
int kind = -1;
|
|
int64_t pts_ns = 0;
|
|
int rc = ratatoskr_receiver_next_payload(ctx->receiver, ctx->buffer, ctx->buffer_capacity, &len,
|
|
&kind, &pts_ns);
|
|
if (rc == RATATOSKR_ERR_BUFFER_TOO_SMALL) {
|
|
ctx->buffer_capacity = len + len / 2;
|
|
ctx->buffer = brealloc(ctx->buffer, ctx->buffer_capacity);
|
|
continue;
|
|
}
|
|
if (rc != RATATOSKR_OK)
|
|
break;
|
|
/* video already went to the relay; the child decodes it */
|
|
if (kind == RATATOSKR_KIND_AUDIO)
|
|
output_audio(ctx, ctx->buffer, len, pts_ns);
|
|
}
|
|
os_sleep_ms(2);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void stop_stream(struct ratatoskr_source *ctx)
|
|
{
|
|
if (ctx->pump_running) {
|
|
ctx->pump_running = false;
|
|
pthread_join(ctx->pump, NULL);
|
|
}
|
|
if (ctx->catalog && ctx->receiver) {
|
|
pthread_mutex_lock(&ctx->catalog_mutex);
|
|
if (ratatoskr_request_stop(ctx->catalog, ctx->catalog_index) != RATATOSKR_OK)
|
|
log_last_error("stop request failed");
|
|
pthread_mutex_unlock(&ctx->catalog_mutex);
|
|
}
|
|
release_video_child(ctx);
|
|
if (ctx->receiver) {
|
|
ratatoskr_receiver_close(ctx->receiver);
|
|
ctx->receiver = NULL;
|
|
}
|
|
}
|
|
|
|
static void start_stream(struct ratatoskr_source *ctx)
|
|
{
|
|
stop_stream(ctx);
|
|
if (!ctx->catalog)
|
|
refresh_catalog(ctx);
|
|
|
|
size_t index;
|
|
RatatoskrStreamInfo info;
|
|
pthread_mutex_lock(&ctx->catalog_mutex);
|
|
bool found = find_stream(ctx, ctx->stream_id, &index, &info);
|
|
pthread_mutex_unlock(&ctx->catalog_mutex);
|
|
if (!found) {
|
|
blog(LOG_WARNING, "[ratatoskr] stream %s is not advertised through %s", ctx->stream_id, ctx->odin);
|
|
return;
|
|
}
|
|
bool wants_video = ctx->video_source_id[0] != 0;
|
|
bool wants_audio = ctx->audio_source_id[0] != 0;
|
|
if (!wants_video && !wants_audio) {
|
|
blog(LOG_INFO, "[ratatoskr] neither video nor audio selected for %s", ctx->stream_id);
|
|
return;
|
|
}
|
|
|
|
ctx->catalog_index = index;
|
|
ctx->audio_sample_rate = info.audio_sample_rate;
|
|
ctx->audio_channels = info.audio_channels;
|
|
ctx->relay_port = wants_video ? ratatoskr_free_udp_port() : 0;
|
|
|
|
/* Ask first, then dial: the request spins the producer up and the dial
|
|
* is repeated until it answers. Nothing here listens. */
|
|
pthread_mutex_lock(&ctx->catalog_mutex);
|
|
int rc = ratatoskr_request_start(ctx->catalog, index, wants_video ? ctx->video_source_id : NONE_ID,
|
|
wants_audio ? ctx->audio_source_id : NONE_ID, ctx->video_codec, ctx->audio_codec,
|
|
ctx->bitrate_kbps, ctx->latency_ms);
|
|
pthread_mutex_unlock(&ctx->catalog_mutex);
|
|
if (rc != RATATOSKR_OK) {
|
|
log_last_error("start request failed");
|
|
return;
|
|
}
|
|
blog(LOG_INFO, "[ratatoskr] asked %s for %s (video=%s audio=%s %s/%s %u kbps, %u ms); dialling %s",
|
|
info.producer_id, ctx->stream_id, wants_video ? ctx->video_source_id : "-",
|
|
wants_audio ? ctx->audio_source_id : "-", ctx->video_codec, ctx->audio_codec, ctx->bitrate_kbps,
|
|
ctx->latency_ms, info.media_endpoint);
|
|
|
|
char relay[64] = {0};
|
|
if (wants_video)
|
|
snprintf(relay, sizeof(relay), "127.0.0.1:%u", (unsigned)ctx->relay_port);
|
|
rc = ratatoskr_receiver_open(info.media_endpoint, ctx->receiver_id, info.media_connection_id,
|
|
wants_video ? relay : NULL, &ctx->receiver);
|
|
if (rc != RATATOSKR_OK) {
|
|
log_last_error("receiver open failed");
|
|
stop_stream(ctx);
|
|
return;
|
|
}
|
|
if (wants_video)
|
|
create_video_child(ctx);
|
|
|
|
ctx->pump_running = true;
|
|
if (pthread_create(&ctx->pump, NULL, pump_loop, ctx) != 0) {
|
|
ctx->pump_running = false;
|
|
blog(LOG_ERROR, "[ratatoskr] could not start the pump thread");
|
|
stop_stream(ctx);
|
|
}
|
|
}
|
|
|
|
/* ---- obs_source_info ---- */
|
|
|
|
static const char *ratatoskr_get_name(void *unused)
|
|
{
|
|
UNUSED_PARAMETER(unused);
|
|
return obs_module_text("RatatoskrSource");
|
|
}
|
|
|
|
static void ratatoskr_get_defaults(obs_data_t *settings)
|
|
{
|
|
obs_data_set_default_string(settings, SETTING_ODIN, DEFAULT_ODIN);
|
|
obs_data_set_default_string(settings, SETTING_RECEIVER_ID, "obs");
|
|
obs_data_set_default_string(settings, SETTING_VIDEO_CODEC, "h264");
|
|
obs_data_set_default_string(settings, SETTING_AUDIO_CODEC, "pcm-f32le-interleaved");
|
|
obs_data_set_default_int(settings, SETTING_BITRATE, 0);
|
|
obs_data_set_default_int(settings, SETTING_LATENCY, 0);
|
|
}
|
|
|
|
static void apply_settings(struct ratatoskr_source *ctx, obs_data_t *settings)
|
|
{
|
|
set_string(&ctx->odin, obs_data_get_string(settings, SETTING_ODIN));
|
|
set_string(&ctx->receiver_id, obs_data_get_string(settings, SETTING_RECEIVER_ID));
|
|
set_string(&ctx->stream_id, obs_data_get_string(settings, SETTING_STREAM));
|
|
set_string(&ctx->video_source_id, obs_data_get_string(settings, SETTING_VIDEO_SOURCE));
|
|
set_string(&ctx->audio_source_id, obs_data_get_string(settings, SETTING_AUDIO_SOURCE));
|
|
set_string(&ctx->video_codec, obs_data_get_string(settings, SETTING_VIDEO_CODEC));
|
|
set_string(&ctx->audio_codec, obs_data_get_string(settings, SETTING_AUDIO_CODEC));
|
|
ctx->bitrate_kbps = (unsigned int)obs_data_get_int(settings, SETTING_BITRATE);
|
|
ctx->latency_ms = (unsigned int)obs_data_get_int(settings, SETTING_LATENCY);
|
|
}
|
|
|
|
static void *ratatoskr_create(obs_data_t *settings, obs_source_t *source)
|
|
{
|
|
struct ratatoskr_source *ctx = bzalloc(sizeof(*ctx));
|
|
ctx->source = source;
|
|
pthread_mutex_init(&ctx->catalog_mutex, NULL);
|
|
ctx->buffer_capacity = 256 * 1024;
|
|
ctx->buffer = bmalloc(ctx->buffer_capacity);
|
|
apply_settings(ctx, settings);
|
|
return ctx;
|
|
}
|
|
|
|
static void ratatoskr_destroy(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
stop_stream(ctx);
|
|
ratatoskr_catalog_close(ctx->catalog);
|
|
pthread_mutex_destroy(&ctx->catalog_mutex);
|
|
bfree(ctx->buffer);
|
|
bfree(ctx->odin);
|
|
bfree(ctx->receiver_id);
|
|
bfree(ctx->stream_id);
|
|
bfree(ctx->video_source_id);
|
|
bfree(ctx->audio_source_id);
|
|
bfree(ctx->video_codec);
|
|
bfree(ctx->audio_codec);
|
|
bfree(ctx);
|
|
}
|
|
|
|
static void ratatoskr_update(void *data, obs_data_t *settings)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
apply_settings(ctx, settings);
|
|
if (ctx->active)
|
|
start_stream(ctx);
|
|
}
|
|
|
|
static void ratatoskr_activate(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
ctx->active = true;
|
|
start_stream(ctx);
|
|
}
|
|
|
|
static void ratatoskr_deactivate(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
ctx->active = false;
|
|
stop_stream(ctx);
|
|
}
|
|
|
|
static uint32_t ratatoskr_get_width(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
return ctx->video_child ? obs_source_get_width(ctx->video_child) : 0;
|
|
}
|
|
|
|
static uint32_t ratatoskr_get_height(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
return ctx->video_child ? obs_source_get_height(ctx->video_child) : 0;
|
|
}
|
|
|
|
static void ratatoskr_video_render(void *data, gs_effect_t *effect)
|
|
{
|
|
UNUSED_PARAMETER(effect);
|
|
struct ratatoskr_source *ctx = data;
|
|
if (ctx->video_child)
|
|
obs_source_video_render(ctx->video_child);
|
|
}
|
|
|
|
/* ---- properties ---- */
|
|
|
|
static void fill_list(obs_property_t *list, size_t count, const char *const *ids, const char *const *labels)
|
|
{
|
|
obs_property_list_clear(list);
|
|
obs_property_list_add_string(list, obs_module_text("Ratatoskr.None"), NONE_ID);
|
|
for (size_t i = 0; i < count; i++)
|
|
obs_property_list_add_string(list, labels ? labels[i] : ids[i], ids[i]);
|
|
}
|
|
|
|
static void fill_from_catalog(struct ratatoskr_source *ctx, obs_properties_t *props, const char *stream_id)
|
|
{
|
|
obs_property_t *streams = obs_properties_get(props, SETTING_STREAM);
|
|
obs_property_t *video = obs_properties_get(props, SETTING_VIDEO_SOURCE);
|
|
obs_property_t *audio = obs_properties_get(props, SETTING_AUDIO_SOURCE);
|
|
obs_property_t *vcodec = obs_properties_get(props, SETTING_VIDEO_CODEC);
|
|
obs_property_t *acodec = obs_properties_get(props, SETTING_AUDIO_CODEC);
|
|
|
|
pthread_mutex_lock(&ctx->catalog_mutex);
|
|
obs_property_list_clear(streams);
|
|
size_t count = ctx->catalog ? ratatoskr_catalog_count(ctx->catalog) : 0;
|
|
RatatoskrStreamInfo selected;
|
|
bool have_selected = false;
|
|
for (size_t i = 0; i < count; i++) {
|
|
RatatoskrStreamInfo info;
|
|
if (ratatoskr_catalog_stream(ctx->catalog, i, &info) != RATATOSKR_OK)
|
|
continue;
|
|
char label[512];
|
|
snprintf(label, sizeof(label), "%s — %s (%s)", info.producer_id, info.label, info.state);
|
|
obs_property_list_add_string(streams, label, info.stream_id);
|
|
if (stream_id && strcmp(info.stream_id, stream_id) == 0) {
|
|
selected = info;
|
|
have_selected = true;
|
|
}
|
|
}
|
|
if (have_selected) {
|
|
fill_list(video, selected.video_source_count, selected.video_source_ids, selected.video_source_labels);
|
|
fill_list(audio, selected.audio_source_count, selected.audio_source_ids, selected.audio_source_labels);
|
|
obs_property_list_clear(vcodec);
|
|
for (size_t i = 0; i < selected.video_codec_count; i++)
|
|
obs_property_list_add_string(vcodec, selected.video_codecs[i], selected.video_codecs[i]);
|
|
obs_property_list_clear(acodec);
|
|
for (size_t i = 0; i < selected.audio_codec_count; i++)
|
|
obs_property_list_add_string(acodec, selected.audio_codecs[i], selected.audio_codecs[i]);
|
|
} else {
|
|
fill_list(video, 0, NULL, NULL);
|
|
fill_list(audio, 0, NULL, NULL);
|
|
obs_property_list_clear(vcodec);
|
|
obs_property_list_clear(acodec);
|
|
}
|
|
pthread_mutex_unlock(&ctx->catalog_mutex);
|
|
}
|
|
|
|
static bool on_stream_changed(void *data, obs_properties_t *props, obs_property_t *property, obs_data_t *settings)
|
|
{
|
|
UNUSED_PARAMETER(property);
|
|
struct ratatoskr_source *ctx = data;
|
|
fill_from_catalog(ctx, props, obs_data_get_string(settings, SETTING_STREAM));
|
|
return true;
|
|
}
|
|
|
|
static bool on_refresh(obs_properties_t *props, obs_property_t *property, void *data)
|
|
{
|
|
UNUSED_PARAMETER(property);
|
|
struct ratatoskr_source *ctx = data;
|
|
obs_data_t *settings = obs_source_get_settings(ctx->source);
|
|
set_string(&ctx->odin, obs_data_get_string(settings, SETTING_ODIN));
|
|
set_string(&ctx->receiver_id, obs_data_get_string(settings, SETTING_RECEIVER_ID));
|
|
refresh_catalog(ctx);
|
|
fill_from_catalog(ctx, props, obs_data_get_string(settings, SETTING_STREAM));
|
|
obs_data_release(settings);
|
|
return true;
|
|
}
|
|
|
|
static obs_properties_t *ratatoskr_get_properties(void *data)
|
|
{
|
|
struct ratatoskr_source *ctx = data;
|
|
obs_properties_t *props = obs_properties_create();
|
|
|
|
obs_properties_add_text(props, SETTING_ODIN, obs_module_text("Ratatoskr.Odin"), OBS_TEXT_DEFAULT);
|
|
obs_properties_add_text(props, SETTING_RECEIVER_ID, obs_module_text("Ratatoskr.ReceiverId"), OBS_TEXT_DEFAULT);
|
|
obs_properties_add_button(props, "refresh", obs_module_text("Ratatoskr.Refresh"), on_refresh);
|
|
|
|
obs_property_t *streams = obs_properties_add_list(props, SETTING_STREAM, obs_module_text("Ratatoskr.Stream"),
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_properties_add_list(props, SETTING_VIDEO_SOURCE, obs_module_text("Ratatoskr.VideoSource"),
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_properties_add_list(props, SETTING_AUDIO_SOURCE, obs_module_text("Ratatoskr.AudioSource"),
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_properties_add_list(props, SETTING_VIDEO_CODEC, obs_module_text("Ratatoskr.VideoCodec"),
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_properties_add_list(props, SETTING_AUDIO_CODEC, obs_module_text("Ratatoskr.AudioCodec"),
|
|
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
|
|
obs_properties_add_int(props, SETTING_BITRATE, obs_module_text("Ratatoskr.VideoBitrateKbps"), 0, 100000, 500);
|
|
obs_properties_add_int(props, SETTING_LATENCY, obs_module_text("Ratatoskr.LatencyBudgetMs"), 0, 2000, 25);
|
|
|
|
if (ctx) {
|
|
if (!ctx->catalog)
|
|
refresh_catalog(ctx);
|
|
obs_property_set_modified_callback2(streams, on_stream_changed, ctx);
|
|
fill_from_catalog(ctx, props, ctx->stream_id);
|
|
}
|
|
return props;
|
|
}
|
|
|
|
static struct obs_source_info ratatoskr_source_info = {
|
|
.id = "ratatoskr_media_stream",
|
|
.type = OBS_SOURCE_TYPE_INPUT,
|
|
.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_DO_NOT_DUPLICATE,
|
|
.get_name = ratatoskr_get_name,
|
|
.create = ratatoskr_create,
|
|
.destroy = ratatoskr_destroy,
|
|
.update = ratatoskr_update,
|
|
.get_defaults = ratatoskr_get_defaults,
|
|
.get_properties = ratatoskr_get_properties,
|
|
.activate = ratatoskr_activate,
|
|
.deactivate = ratatoskr_deactivate,
|
|
.get_width = ratatoskr_get_width,
|
|
.get_height = ratatoskr_get_height,
|
|
.video_render = ratatoskr_video_render,
|
|
.icon_type = OBS_ICON_TYPE_MEDIA,
|
|
};
|
|
|
|
bool obs_module_load(void)
|
|
{
|
|
obs_register_source(&ratatoskr_source_info);
|
|
blog(LOG_INFO, "[ratatoskr] loaded");
|
|
return true;
|
|
}
|