Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions data/locale/en-US.ini
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
Service.Name="MOQ"
Service.Server="Relay URL"
Service.Namespace="Namespace"
Service.EnableAudio="Enable audio"
Service.EnableAudio.Desc="Publish an audio track alongside video. When disabled, the stream is video-only."
Service.SkipTlsVerify="Skip TLS certificate verification"
Service.SkipTlsVerify.Desc="Accept the relay's TLS certificate without verifying it."
Service.DraftVersion="MoQ draft version"
Service.DraftVersion.Auto="Automatic"
Service.DraftVersion.Desc="\"Automatic\" negotiates the highest draft supported by both the plugin and the relay. Pick a draft number to force it."
Output.Name="MOQ Output"
Error.NoEncoder="No video encoder is assigned to the MOQ output."
Error.NoAudioEncoder="No audio encoder is assigned to the MOQ output."
Error.NoURL="No relay URL is configured. Set it in Settings -> Stream."
Error.NoKey="No namespace is configured. Set it in Settings -> Stream."
Error.UnsupportedVersion="This MoQ draft version is not supported by this build. Use \"Automatic\" or another draft number. Set it in Settings -> Stream."
Error.Connect="Could not connect to the MOQ relay. Check the relay URL and that the relay is reachable."
119 changes: 98 additions & 21 deletions src/moq-output.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
#include "moq-output.h"
#include "codec-signaling.h"
#include "moq-service.h"

#include <util/platform.h>
#include <obs.hpp>
#include <obs-avc.h>
#include <obs-hevc.h>

#define VIDEO_TIMESCALE 1000000u
#define MOQ_HANDSHAKE_TIMEOUT_US 5000000ull

#ifndef OBS_OUTPUT_NO_INTERLEAVE
#define OBS_OUTPUT_NO_INTERLEAVE (1 << 7)
#endif

MOQOutput::MOQOutput(obs_data_t *settings, obs_output_t *output) : output(output)
{
blog(LOG_INFO, "[obs-moq] output created");
Expand Down Expand Up @@ -70,6 +77,7 @@ bool MOQOutput::LoadVideoEncoderSettings()

video_init_data = BuildInitData(codec, extra, extra_size);
video_codec = BuildCodecString(codec, video_init_data);
video_codec_name = codec ? codec : "";
return true;
}

Expand Down Expand Up @@ -99,6 +107,24 @@ bool MOQOutput::LoadAudioEncoderSettings()
return true;
}

bool MOQOutput::LoadEndpointSettings(obs_service_t *service)
{
OBSDataAutoRelease resolved = obs_service_defaults(obs_service_get_type(service));
if (!resolved) {
obs_output_signal_stop(output, OBS_OUTPUT_ERROR);
return false;
}

OBSDataAutoRelease settings = obs_service_get_settings(service);
obs_data_apply(resolved, settings);

endpoint_conf.skip_tls_verify = obs_data_get_bool(resolved, kSettingSkipTlsVerify);
endpoint_conf.draft_version = (moq_version_t)obs_data_get_int(resolved, kSettingDraftVersion);
endpoint_conf.enable_audio = obs_data_get_bool(resolved, kSettingEnableAudio);

return true;
}

bool MOQOutput::ResolveServiceConfig()
{
url.clear();
Expand All @@ -110,6 +136,9 @@ bool MOQOutput::ResolveServiceConfig()
return false;
}

if (!LoadEndpointSettings(service))
return false;

const char *server = obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_SERVER_URL);
if (server && *server) {
url = server;
Expand Down Expand Up @@ -177,6 +206,7 @@ moq_media_track_t *MOQOutput::CreateVideoTrackFromPacket(moq_media_sender_t *cur

video_init_data = std::move(init);
video_codec = BuildCodecString(codec, video_init_data);
video_codec_name = codec ? codec : "";

moq_media_track_t *new_track = CreateVideoTrack(cur_sender);
if (!new_track) {
Expand Down Expand Up @@ -251,22 +281,28 @@ bool MOQOutput::Connect()
moq_endpoint_cfg_init_sized(&ecfg, sizeof(ecfg));
ecfg.url.data = (const uint8_t *)url.c_str();
ecfg.url.len = url.size();
//todo: dynamically resolve from config
ecfg.insecure_skip_verify = true;
ecfg.insecure_skip_verify = endpoint_conf.skip_tls_verify;
ecfg.handshake_timeout_us = MOQ_HANDSHAKE_TIMEOUT_US;

//todo: revisit if we want to set the version policy to exact or leave it auto
static const moq_version_t kVersions[] = {MOQ_VERSION_DRAFT_16};
ecfg.versions.policy = MOQ_VERSION_POLICY_EXACT;
ecfg.versions.versions = kVersions;
ecfg.versions.version_count = 1;
ecfg.versions.struct_size = sizeof(ecfg.versions);
if (endpoint_conf.draft_version) {
ecfg.versions.policy = MOQ_VERSION_POLICY_EXACT;
ecfg.versions.versions = &endpoint_conf.draft_version;
ecfg.versions.version_count = 1;
} else {
ecfg.versions.policy = MOQ_VERSION_POLICY_AUTO;
}

moq_media_sender_cfg_t scfg;
moq_media_sender_cfg_init_live_sized(&scfg, sizeof(scfg));
scfg.endpoint = &ecfg;
scfg.namespace_ = namespace_val;
scfg.publish_tracks = true;
scfg.drop_without_demand = true;
// The catalog only needs republishing when tracks change (MSF-01 §5). Leaving
// this 0 resolves to libmoq's 1-second default, which republishes the catalog
// as a new group every second. Same effect as openmoq/moq5#28.
scfg.catalog_refresh_interval_us = UINT64_MAX;

moq_media_sender_callbacks_init_sized(&scfg.callbacks, sizeof(scfg.callbacks));
scfg.callbacks.ctx = this;
Expand All @@ -275,9 +311,13 @@ bool MOQOutput::Connect()
scfg.callbacks.on_track_closed = &MOQOutput::OnTrackClosed;

moq_media_sender_t *media_sender = nullptr;
if (moq_media_sender_create(&scfg, &media_sender) != MOQ_OK) {
blog(LOG_WARNING, "[obs-moq] moq_media_sender_create failed");
obs_output_set_last_error(output, obs_module_text("Error.Connect"));
moq_result_t create_result = moq_media_sender_create(&scfg, &media_sender);
if (create_result != MOQ_OK) {
blog(LOG_WARNING, "[obs-moq] moq_media_sender_create failed: %d", (int)create_result);
const char *error = (create_result == MOQ_ERR_UNSUPPORTED && endpoint_conf.draft_version)
? "Error.UnsupportedVersion"
: "Error.Connect";
obs_output_set_last_error(output, obs_module_text(error));
obs_output_signal_stop(output, OBS_OUTPUT_CONNECT_FAILED);
return false;
}
Expand All @@ -293,12 +333,15 @@ bool MOQOutput::Connect()
}
}

moq_media_track_t *new_audio_track = CreateAudioTrack(media_sender);
if (!new_audio_track) {
blog(LOG_WARNING, "[obs-moq] failed to create audio track");
moq_media_sender_destroy(media_sender);
obs_output_signal_stop(output, OBS_OUTPUT_ERROR);
return false;
moq_media_track_t *new_audio_track = nullptr;
if (endpoint_conf.enable_audio) {
new_audio_track = CreateAudioTrack(media_sender);
if (!new_audio_track) {
blog(LOG_WARNING, "[obs-moq] failed to create audio track");
moq_media_sender_destroy(media_sender);
obs_output_signal_stop(output, OBS_OUTPUT_ERROR);
return false;
}
}

{
Expand Down Expand Up @@ -380,9 +423,38 @@ void MOQOutput::SendPacket(struct encoder_packet *packet, moq_media_track_t **tr
bool ends_group)
{

// OBS emits AVC/HEVC in Annex-B (start-code delimited), but init_data declares
// an avcC/hvcC record, whose lengthSizeMinusOne says NALs carry 4-byte length
// prefixes. Players that configure their decoder from that description (MSE,
// or WebCodecs with `description` set) fail on Annex-B bytes; players running
// WebCodecs in Annex-B mode ignore the description and accept either. Reframe
// so the payload matches what we advertise. AV1 carries OBUs, not NALs, so it
// is passed through untouched.
struct encoder_packet reframed;
bool did_reframe = false;
const uint8_t *payload_data = packet->data;
size_t payload_size = packet->size;
if (packet->type == OBS_ENCODER_VIDEO) {
if (video_codec_name == "h264") {
obs_parse_avc_packet(&reframed, packet);
did_reframe = true;
} else if (video_codec_name == "hevc") {
obs_parse_hevc_packet(&reframed, packet);
did_reframe = true;
}
if (did_reframe) {
payload_data = reframed.data;
payload_size = reframed.size;
}
}

moq_rcbuf_t *payload = nullptr;
// moq_rcbuf_create will copy the data into a new rcbuf, and increment the refcount. We will need to decref it after sending, or if we don't send it.
if (moq_rcbuf_create(moq_alloc_default(), packet->data, packet->size, &payload) != MOQ_OK) {
moq_result_t alloc_result = moq_rcbuf_create(moq_alloc_default(), payload_data, payload_size, &payload);
if (did_reframe) {
obs_encoder_packet_release(&reframed);
}
if (alloc_result != MOQ_OK) {
blog(LOG_WARNING, "[obs-moq] rcbuf alloc failed");
return;
}
Expand Down Expand Up @@ -443,7 +515,7 @@ void MOQOutput::Data(struct encoder_packet *packet)
if (packet->type == OBS_ENCODER_VIDEO) {
SendPacket(packet, &video_track, packet->keyframe, packet->keyframe, false);
}
if (packet->type == OBS_ENCODER_AUDIO) {
if (packet->type == OBS_ENCODER_AUDIO && endpoint_conf.enable_audio) {
SendPacket(packet, &audio_track, true, true, true);
}
}
Expand All @@ -457,8 +529,13 @@ void MOQOutput::StartThread()
return;
}

if (!LoadVideoEncoderSettings() || !LoadAudioEncoderSettings()) {
blog(LOG_WARNING, "[obs-moq] failed to configure video or audio track");
if (!LoadVideoEncoderSettings()) {
blog(LOG_WARNING, "[obs-moq] failed to configure video track");
return;
}

if (endpoint_conf.enable_audio && !LoadAudioEncoderSettings()) {
blog(LOG_WARNING, "[obs-moq] failed to configure audio track");
return;
}

Expand All @@ -472,7 +549,7 @@ void register_moq_output()
{
struct obs_output_info info = {};
info.id = "moq_output";
info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE;
info.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_SERVICE | OBS_OUTPUT_NO_INTERLEAVE;
info.protocols = "MOQ";
info.encoded_video_codecs = "h264;hevc;av1";
info.encoded_audio_codecs = "aac;opus";
Expand Down
9 changes: 9 additions & 0 deletions src/moq-output.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ struct audio_config {
uint64_t bitrate;
};

struct endpoint_config {
bool skip_tls_verify = false;
bool enable_audio = true;
moq_version_t draft_version = (moq_version_t)0; // 0 : negotiate automatically
};

class MOQOutput {
public:
MOQOutput(obs_data_t *settings, obs_output_t *output);
Expand All @@ -50,6 +56,7 @@ class MOQOutput {
void SendPacket(struct encoder_packet *packet, moq_media_track_t **track, bool is_sync, bool starts_group,
bool ends_group);
bool ResolveServiceConfig();
bool LoadEndpointSettings(obs_service_t *service);
bool Connect();

static void OnReady(void *ctx, moq_media_sender_t *sender);
Expand All @@ -72,9 +79,11 @@ class MOQOutput {

video_config video_conf;
audio_config audio_conf;
endpoint_config endpoint_conf;

std::vector<uint8_t> video_init_data;
std::string video_codec;
std::string video_codec_name; // raw OBS encoder codec ("h264"/"hevc"/"av1")

std::vector<uint8_t> audio_init_data;
std::string audio_codec;
Expand Down
33 changes: 33 additions & 0 deletions src/moq-service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <cstring>
#include <obs.hpp>

#include <moq/session.h>

const char *audio_codecs[] = {"aac", "opus", nullptr};
const char *video_codecs[] = {"h264", "hevc", "av1", nullptr};

Expand All @@ -19,13 +21,37 @@ void MOQService::Update(obs_data_t *settings)
blog(LOG_DEBUG, "[obs-moq] service updated: server='%s' namespace='%s'", server.c_str(), moq_namespace.c_str());
}

void MOQService::Defaults(obs_data_t *settings)
{
obs_data_set_default_bool(settings, kSettingSkipTlsVerify, true);
obs_data_set_default_int(settings, kSettingDraftVersion, 0);
obs_data_set_default_bool(settings, kSettingEnableAudio, true);
}

obs_properties_t *MOQService::Properties()
{
obs_properties_t *ppts = obs_properties_create();

obs_properties_add_text(ppts, "server", obs_module_text("Service.Server"), OBS_TEXT_DEFAULT);
obs_properties_add_text(ppts, "key", obs_module_text("Service.Namespace"), OBS_TEXT_DEFAULT);

obs_property_t *enable_audio =
obs_properties_add_bool(ppts, kSettingEnableAudio, obs_module_text("Service.EnableAudio"));
obs_property_set_long_description(enable_audio, obs_module_text("Service.EnableAudio.Desc"));

obs_property_t *skip_tls =
obs_properties_add_bool(ppts, kSettingSkipTlsVerify, obs_module_text("Service.SkipTlsVerify"));
obs_property_set_long_description(skip_tls, obs_module_text("Service.SkipTlsVerify.Desc"));

// Mirrors the drafts libmoq supports, it exposes no API to enumerate them.
obs_property_t *draft = obs_properties_add_list(ppts, kSettingDraftVersion,
obs_module_text("Service.DraftVersion"), OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_INT);
obs_property_list_add_int(draft, obs_module_text("Service.DraftVersion.Auto"), 0);
obs_property_list_add_int(draft, "16", MOQ_VERSION_DRAFT_16);
obs_property_list_add_int(draft, "18", MOQ_VERSION_DRAFT_18);
obs_property_set_long_description(draft, obs_module_text("Service.DraftVersion.Desc"));

return ppts;
}

Expand Down Expand Up @@ -62,6 +88,10 @@ bool MOQService::Initialize(obs_output_t *output)
if (strcmp(enc_id, "obs_x264") == 0)
obs_data_set_string(overrides, "tune", "zerolatency");

static constexpr char kVTEncoderIdPrefix[] = "com.apple.videotoolbox.videoencoder.";
if (strncmp(enc_id, kVTEncoderIdPrefix, sizeof(kVTEncoderIdPrefix) - 1) == 0)
obs_data_set_bool(overrides, "low_latency", true);

obs_encoder_update(venc, overrides);

return true;
Expand Down Expand Up @@ -101,6 +131,9 @@ void register_moq_service()
info.update = [](void *priv, obs_data_t *settings) {
static_cast<MOQService *>(priv)->Update(settings);
};
info.get_defaults = [](obs_data_t *settings) {
MOQService::Defaults(settings);
};
info.get_properties = [](void *) -> obs_properties_t * {
return MOQService::Properties();
};
Expand Down
7 changes: 5 additions & 2 deletions src/moq-service.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@
#include <obs-module.h>
#include <string>

inline constexpr const char *kSettingSkipTlsVerify = "skip_tls_verify";
inline constexpr const char *kSettingDraftVersion = "draft_version";
inline constexpr const char *kSettingEnableAudio = "enable_audio";

class MOQService {
public:
std::string server;
std::string moq_namespace;
// todo: add a setting or find a way to set this
bool skip_verify = true;

MOQService(obs_data_t *settings, obs_service_t *service);

void Update(obs_data_t *settings);
static void Defaults(obs_data_t *settings);
static void ApplyEncoderSettings(obs_data_t *video_settings, obs_data_t *audio_settings);
static bool Initialize(obs_output_t *output);
static obs_properties_t *Properties();
Expand Down
Loading