From 83eca866a87a76ab79608dc7318118625de73d04 Mon Sep 17 00:00:00 2001 From: Ayoub-glitsh Date: Fri, 24 Apr 2026 19:23:53 +0100 Subject: [PATCH 1/3] fix #807: add --packet-encoding option to roc-send The --rate flag only sets the input device sample rate, but the packet encoding defaults to L16_Stereo at 44100 Hz. When input rate differs from packet encoding rate, the sender creates a resampler even if the user doesn't want one. Add --packet-encoding option that lets users register a custom RTP encoding and select it for outgoing packets. This allows sending at 48kHz (or any rate) without resampling: roc-send --rate=48000 --packet-encoding=96:s16/48000/stereo ... The option accepts the format '://', e.g.: 96:s16/48000/stereo 97:s16/48000/mono The encoding is registered in the context encoding map and the sender is configured to use it as the packet encoding. --- src/tools/roc_send/cmdline.ggo | 3 +++ src/tools/roc_send/main.cpp | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/tools/roc_send/cmdline.ggo b/src/tools/roc_send/cmdline.ggo index 90e48d170..94fbde67e 100644 --- a/src/tools/roc_send/cmdline.ggo +++ b/src/tools/roc_send/cmdline.ggo @@ -50,6 +50,9 @@ section "Options" option "rate" - "Override input sample rate, Hz" int optional + option "packet-encoding" - "Register custom packet encoding (e.g. \"96:s16/48000/stereo\")" + typestr="ENCODING" string optional + option "latency-backend" - "Which latency to use in latency tuner" values="niq" default="niq" enum optional diff --git a/src/tools/roc_send/main.cpp b/src/tools/roc_send/main.cpp index ca3af5dd0..551c3ce96 100644 --- a/src/tools/roc_send/main.cpp +++ b/src/tools/roc_send/main.cpp @@ -20,6 +20,7 @@ #include "roc_node/context.h" #include "roc_node/sender.h" #include "roc_pipeline/sender_sink.h" +#include "roc_rtp/encoding.h" #include "roc_sndio/backend_dispatcher.h" #include "roc_sndio/backend_map.h" #include "roc_sndio/print_supported.h" @@ -268,6 +269,22 @@ int main(int argc, char** argv) { return 1; } + if (args.packet_encoding_given) { + rtp::Encoding enc; + if (!rtp::parse_encoding(args.packet_encoding_arg, enc)) { + roc_log(LogError, + "invalid --packet-encoding: bad format," + " expected \"://\"," + " e.g. \"96:s16/48000/stereo\""); + return 1; + } + if (!context.encoding_map().add_encoding(enc)) { + roc_log(LogError, "invalid --packet-encoding: failed to register encoding"); + return 1; + } + sender_config.payload_type = enc.payload_type; + } + sndio::BackendDispatcher backend_dispatcher(context.arena()); if (args.list_supported_given) { if (!address::print_supported(context.arena())) { From 61fe66044fe6d4f77fa3c75412bf2a02d83b6ae7 Mon Sep 17 00:00:00 2001 From: Ayoub-glitsh Date: Fri, 24 Apr 2026 19:51:32 +0100 Subject: [PATCH 2/3] test #764: add unit tests for LatencyTuner Tests cover: - Initialization: valid/invalid configs, intact vs tuning profiles - Bounds checking: latency at target, above max, below min, stalling suppression, no-metrics-yet behavior, intact profile ignores bounds - Scaling: no scaling before first interval, near-1.0 at target, above-1.0 when latency high, below-1.0 when latency low, clamped to scaling_tolerance, intact profile never produces scaling - E2E backend: no update without e2e metrics, terminates on out-of-bounds --- src/tests/roc_audio/test_latency_tuner.cpp | 361 +++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/tests/roc_audio/test_latency_tuner.cpp diff --git a/src/tests/roc_audio/test_latency_tuner.cpp b/src/tests/roc_audio/test_latency_tuner.cpp new file mode 100644 index 000000000..1c249974e --- /dev/null +++ b/src/tests/roc_audio/test_latency_tuner.cpp @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2024 Roc Streaming authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include "roc_audio/latency_tuner.h" +#include "roc_core/time.h" +#include "roc_packet/units.h" + +namespace roc { +namespace audio { + +namespace { + +enum { + SampleRate = 44100, + NumCh = 2, + ChMask = 0x3 +}; + +const SampleSpec sample_spec(SampleRate, + Sample_RawFormat, + ChanLayout_Surround, + ChanOrder_Smpte, + ChMask); + +// Build a fully-specified config with explicit values so tests are deterministic. +LatencyConfig make_config(core::nanoseconds_t target, + core::nanoseconds_t tolerance, + LatencyTunerProfile profile = LatencyTunerProfile_Gradual, + LatencyTunerBackend backend = LatencyTunerBackend_Niq) { + LatencyConfig config; + config.tuner_backend = backend; + config.tuner_profile = profile; + config.target_latency = target; + config.latency_tolerance = tolerance; + config.stale_tolerance = tolerance / 4; + config.scaling_interval = 5 * core::Millisecond; + config.scaling_tolerance = 0.005f; + return config; +} + +// Feed the tuner N times with the given niq_latency and advance stream by one +// scaling_interval worth of samples each time. +void feed_niq(LatencyTuner& tuner, + core::nanoseconds_t niq_latency, + size_t iterations, + core::nanoseconds_t step = 5 * core::Millisecond) { + LatencyMetrics lm; + lm.niq_latency = niq_latency; + packet::LinkMetrics link; + + const packet::stream_timestamp_t step_samples = + sample_spec.ns_2_stream_timestamp(step); + + for (size_t i = 0; i < iterations; i++) { + tuner.write_metrics(lm, link); + CHECK(tuner.update_stream()); + tuner.advance_stream(step_samples); + } +} + +} // namespace + +// --------------------------------------------------------------------------- +// Initialization +// --------------------------------------------------------------------------- + +TEST_GROUP(latency_tuner_init) {}; + +TEST(latency_tuner_init, intact_profile_no_target_needed) { + // Intact profile: no tuning, no bounds — target_latency may be zero. + LatencyConfig config; + config.tuner_backend = LatencyTunerBackend_Niq; + config.tuner_profile = LatencyTunerProfile_Intact; + + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); +} + +TEST(latency_tuner_init, gradual_profile_valid_config) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); +} + +TEST(latency_tuner_init, responsive_profile_valid_config) { + LatencyConfig config = make_config( + 200 * core::Millisecond, 50 * core::Millisecond, LatencyTunerProfile_Responsive); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); +} + +TEST(latency_tuner_init, invalid_negative_target_latency) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + config.target_latency = -1; + LatencyTuner tuner(config, sample_spec); + CHECK_FALSE(tuner.is_valid()); +} + +TEST(latency_tuner_init, invalid_negative_tolerance) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + config.latency_tolerance = -1; + LatencyTuner tuner(config, sample_spec); + CHECK_FALSE(tuner.is_valid()); +} + +// --------------------------------------------------------------------------- +// Bounds checking +// --------------------------------------------------------------------------- + +TEST_GROUP(latency_tuner_bounds) {}; + +TEST(latency_tuner_bounds, latency_at_target_stays_alive) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Feed exactly target latency — should never go out of bounds. + feed_niq(tuner, 200 * core::Millisecond, 100); +} + +TEST(latency_tuner_bounds, latency_above_max_terminates) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Feed latency well above target + tolerance (200 + 50 = 250ms). + LatencyMetrics lm; + lm.niq_latency = 400 * core::Millisecond; + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK_FALSE(tuner.update_stream()); +} + +TEST(latency_tuner_bounds, latency_below_min_terminates) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Feed latency well below target - tolerance (200 - 50 = 150ms). + LatencyMetrics lm; + lm.niq_latency = 10 * core::Millisecond; + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK_FALSE(tuner.update_stream()); +} + +TEST(latency_tuner_bounds, stalling_suppresses_low_latency_termination) { + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + // stale_tolerance = 50ms / 4 = 12.5ms + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Latency below min, but queue is stalling (burst drop scenario). + // Tuner should NOT terminate — it defers to watchdog. + LatencyMetrics lm; + lm.niq_latency = 10 * core::Millisecond; + lm.niq_stalling = 100 * core::Millisecond; // well above stale_tolerance + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK(tuner.update_stream()); // should survive +} + +TEST(latency_tuner_bounds, no_metrics_yet_does_not_terminate) { + // Before any metrics arrive, update_stream() should return true. + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + CHECK(tuner.update_stream()); +} + +TEST(latency_tuner_bounds, intact_profile_ignores_out_of_bounds) { + // Intact profile has no bounds checking — should never terminate. + LatencyConfig config; + config.tuner_backend = LatencyTunerBackend_Niq; + config.tuner_profile = LatencyTunerProfile_Intact; + + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + LatencyMetrics lm; + lm.niq_latency = 10000 * core::Millisecond; // absurdly high + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK(tuner.update_stream()); // intact profile never terminates +} + +// --------------------------------------------------------------------------- +// Scaling / freq coefficient +// --------------------------------------------------------------------------- + +TEST_GROUP(latency_tuner_scaling) {}; + +TEST(latency_tuner_scaling, no_scaling_before_first_interval) { + // fetch_scaling() returns 0 until the first scaling interval elapses. + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + LatencyMetrics lm; + lm.niq_latency = 200 * core::Millisecond; + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK(tuner.update_stream()); + // No advance_stream yet — scaling interval not elapsed. + DOUBLES_EQUAL(0.0, (double)tuner.fetch_scaling(), 1e-6); +} + +TEST(latency_tuner_scaling, scaling_near_one_when_at_target) { + // When latency == target, freq_coeff should stay very close to 1.0. + LatencyConfig config = + make_config(200 * core::Millisecond, 50 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + feed_niq(tuner, 200 * core::Millisecond, 200); + + const float scaling = tuner.fetch_scaling(); + if (scaling != 0) { + // Allow ±0.5% (scaling_tolerance). + CHECK(scaling >= 0.995f); + CHECK(scaling <= 1.005f); + } +} + +TEST(latency_tuner_scaling, scaling_above_one_when_latency_high) { + // When latency > target, freq_coeff should drift above 1.0 (speed up sender). + LatencyConfig config = + make_config(200 * core::Millisecond, 100 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Feed latency significantly above target for many intervals. + feed_niq(tuner, 280 * core::Millisecond, 500); + + const float scaling = tuner.fetch_scaling(); + if (scaling != 0) { + CHECK(scaling > 1.0f); + } +} + +TEST(latency_tuner_scaling, scaling_below_one_when_latency_low) { + // When latency < target, freq_coeff should drift below 1.0 (slow down sender). + LatencyConfig config = + make_config(200 * core::Millisecond, 100 * core::Millisecond); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + feed_niq(tuner, 120 * core::Millisecond, 500); + + const float scaling = tuner.fetch_scaling(); + if (scaling != 0) { + CHECK(scaling < 1.0f); + } +} + +TEST(latency_tuner_scaling, scaling_clamped_to_tolerance) { + // Even with extreme latency, freq_coeff must stay within ±scaling_tolerance. + LatencyConfig config = + make_config(200 * core::Millisecond, 500 * core::Millisecond); + config.scaling_tolerance = 0.005f; + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + feed_niq(tuner, 5000 * core::Millisecond, 1000); + + const float scaling = tuner.fetch_scaling(); + if (scaling != 0) { + CHECK(scaling <= 1.0f + config.scaling_tolerance); + CHECK(scaling >= 1.0f - config.scaling_tolerance); + } +} + +TEST(latency_tuner_scaling, intact_profile_never_produces_scaling) { + // Intact profile disables tuning — fetch_scaling() should always return 0. + LatencyConfig config; + config.tuner_backend = LatencyTunerBackend_Niq; + config.tuner_profile = LatencyTunerProfile_Intact; + + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + LatencyMetrics lm; + lm.niq_latency = 500 * core::Millisecond; + packet::LinkMetrics link; + + const packet::stream_timestamp_t step = + sample_spec.ns_2_stream_timestamp(5 * core::Millisecond); + + for (size_t i = 0; i < 200; i++) { + tuner.write_metrics(lm, link); + tuner.update_stream(); + tuner.advance_stream(step); + DOUBLES_EQUAL(0.0, (double)tuner.fetch_scaling(), 1e-6); + } +} + +// --------------------------------------------------------------------------- +// E2E backend +// --------------------------------------------------------------------------- + +TEST_GROUP(latency_tuner_e2e) {}; + +TEST(latency_tuner_e2e, no_update_without_e2e_metrics) { + // E2E backend: if no e2e_latency has been reported, update_stream() returns true + // (no data yet, not an error). + LatencyConfig config = make_config(200 * core::Millisecond, + 50 * core::Millisecond, + LatencyTunerProfile_Gradual, + LatencyTunerBackend_E2e); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + // Only provide niq metrics — e2e backend should ignore them. + LatencyMetrics lm; + lm.niq_latency = 400 * core::Millisecond; // would be out of bounds for niq + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK(tuner.update_stream()); // no e2e data yet, should not terminate +} + +TEST(latency_tuner_e2e, terminates_on_e2e_out_of_bounds) { + LatencyConfig config = make_config(200 * core::Millisecond, + 50 * core::Millisecond, + LatencyTunerProfile_Gradual, + LatencyTunerBackend_E2e); + LatencyTuner tuner(config, sample_spec); + CHECK(tuner.is_valid()); + + LatencyMetrics lm; + lm.e2e_latency = 400 * core::Millisecond; // above max (200+50=250ms) + packet::LinkMetrics link; + + tuner.write_metrics(lm, link); + CHECK_FALSE(tuner.update_stream()); +} + +} // namespace audio +} // namespace roc From 7cf60a90f7b2e6db72803519613f3b3ad85852d2 Mon Sep 17 00:00:00 2001 From: Ayoub-glitsh Date: Sat, 25 Apr 2026 15:11:18 +0100 Subject: [PATCH 3/3] fix #821: add packet_mtu and auto-derive packet_length from MTU When neither packet_length nor packet_mtu is configured, the sender now derives packet_length from a default MTU of 1200 bytes (WebRTC default), then uses min(5ms, mtu_derived_length). This fixes multitrack scenarios where the default 5ms packet length exceeds the MTU for high channel counts. Rules: - neither set: use min(DefaultPacketLength, bytes_2_ns(DefaultMtu - RtpHeader)) - only packet_mtu set: derive packet_length from it - only packet_length set: use it as-is (existing behavior) - both set: log a warning and use whichever leads to smaller packet size Changes: - pipeline/config.h: add DefaultPacketMtu, RtpHeaderSize constants; add packet_mtu field to SenderSinkConfig; update deduce_defaults() signature to accept encoding SampleSpec - pipeline/config.cpp: implement MTU resolution logic in deduce_defaults() - pipeline/sender_sink.cpp: pass encoding spec to deduce_defaults() - public_api/include/roc/config.h: add packet_mtu to roc_sender_config - public_api/src/adapters.cpp: wire packet_mtu from public API - tools/roc_send/cmdline.ggo: add --packet-mtu option - tools/roc_send/main.cpp: parse and apply --packet-mtu --- src/internal_modules/roc_pipeline/config.cpp | 68 ++++++++++++++++++- src/internal_modules/roc_pipeline/config.h | 23 ++++++- .../roc_pipeline/sender_sink.cpp | 11 ++- src/public_api/include/roc/config.h | 15 +++- src/public_api/src/adapters.cpp | 4 ++ src/tools/roc_send/cmdline.ggo | 3 + src/tools/roc_send/main.cpp | 11 +++ 7 files changed, 130 insertions(+), 5 deletions(-) diff --git a/src/internal_modules/roc_pipeline/config.cpp b/src/internal_modules/roc_pipeline/config.cpp index da37856d7..ef1310af5 100644 --- a/src/internal_modules/roc_pipeline/config.cpp +++ b/src/internal_modules/roc_pipeline/config.cpp @@ -7,6 +7,10 @@ */ #include "roc_pipeline/config.h" +#include "roc_audio/sample_spec.h" +#include "roc_core/log.h" +#include "roc_core/stddefs.h" +#include "roc_core/time.h" #include "roc_rtp/headers.h" namespace roc { @@ -15,7 +19,8 @@ namespace pipeline { SenderSinkConfig::SenderSinkConfig() : input_sample_spec(DefaultSampleSpec) , payload_type(rtp::PayloadType_L16_Stereo) - , packet_length(DefaultPacketLength) + , packet_length(0) + , packet_mtu(0) , enable_timing(false) , enable_auto_duration(false) , enable_auto_cts(false) @@ -23,7 +28,66 @@ SenderSinkConfig::SenderSinkConfig() , enable_interleaving(false) { } -void SenderSinkConfig::deduce_defaults() { +void SenderSinkConfig::deduce_defaults(const audio::SampleSpec& encoding_sample_spec) { + // Resolve packet_length from packet_mtu if needed. + if (packet_length == 0 && packet_mtu == 0) { + // Neither set: derive packet_length from DefaultPacketMtu, then use + // min(DefaultPacketLength, mtu_derived_length). + if (encoding_sample_spec.is_valid() && encoding_sample_spec.pcm_format() + != audio::PcmFormat_Invalid) { + const size_t payload_bytes = + DefaultPacketMtu > RtpHeaderSize ? DefaultPacketMtu - RtpHeaderSize : 0; + const core::nanoseconds_t mtu_length = + encoding_sample_spec.bytes_2_ns(payload_bytes); + if (mtu_length > 0) { + packet_length = std::min(DefaultPacketLength, mtu_length); + } else { + packet_length = DefaultPacketLength; + } + } else { + packet_length = DefaultPacketLength; + } + } else if (packet_length == 0 && packet_mtu != 0) { + // Only MTU set: derive packet_length from it. + if (encoding_sample_spec.is_valid() && encoding_sample_spec.pcm_format() + != audio::PcmFormat_Invalid) { + const size_t payload_bytes = + packet_mtu > RtpHeaderSize ? packet_mtu - RtpHeaderSize : 0; + const core::nanoseconds_t mtu_length = + encoding_sample_spec.bytes_2_ns(payload_bytes); + packet_length = mtu_length > 0 ? mtu_length : DefaultPacketLength; + } else { + packet_length = DefaultPacketLength; + } + } else if (packet_length != 0 && packet_mtu != 0) { + // Both set: warn and pick the one that leads to smaller packet size. + if (encoding_sample_spec.is_valid() && encoding_sample_spec.pcm_format() + != audio::PcmFormat_Invalid) { + const size_t payload_bytes = + packet_mtu > RtpHeaderSize ? packet_mtu - RtpHeaderSize : 0; + const core::nanoseconds_t mtu_length = + encoding_sample_spec.bytes_2_ns(payload_bytes); + if (mtu_length > 0 && mtu_length < packet_length) { + roc_log(LogInfo, + "sender config: both packet_length and packet_mtu are set;" + " using packet_mtu-derived length (%.3fms) since it is smaller" + " than packet_length (%.3fms)", + (double)mtu_length / core::Millisecond, + (double)packet_length / core::Millisecond); + packet_length = mtu_length; + } else { + roc_log(LogInfo, + "sender config: both packet_length and packet_mtu are set;" + " using packet_length (%.3fms) since it is smaller" + " than packet_mtu-derived length (%.3fms)", + (double)packet_length / core::Millisecond, + (double)mtu_length / core::Millisecond); + } + } + // else: can't compute mtu_length, keep packet_length as-is + } + // else: only packet_length set — use it as-is + latency.deduce_defaults(DefaultLatency, false); resampler.deduce_defaults(latency.tuner_backend, latency.tuner_profile); } diff --git a/src/internal_modules/roc_pipeline/config.h b/src/internal_modules/roc_pipeline/config.h index d5b8f77af..2bcbed16e 100644 --- a/src/internal_modules/roc_pipeline/config.h +++ b/src/internal_modules/roc_pipeline/config.h @@ -48,6 +48,16 @@ static const audio::SampleSpec DefaultSampleSpec(DefaultSampleRate, //! a lower length may be required depending on network MTU, e.g. for Internet. const core::nanoseconds_t DefaultPacketLength = 5 * core::Millisecond; +//! Default MTU (maximum transmission unit) in bytes. +//! @remarks +//! 1200 bytes is the default MTU used in WebRTC and is safe for most networks. +//! When neither packet_length nor packet_mtu is configured by the user, the +//! effective packet length is min(DefaultPacketLength, mtu_derived_length). +const size_t DefaultPacketMtu = 1200; + +//! RTP header size in bytes (fixed part, no CSRC). +const size_t RtpHeaderSize = 12; + //! Default latency. //! @remarks //! 200ms works well on majority Wi-Fi networks and is not too annoying. However, many @@ -66,8 +76,17 @@ struct SenderSinkConfig { unsigned payload_type; //! Packet length, in nanoseconds. + //! @remarks + //! If zero and packet_mtu is also zero, derived from DefaultPacketMtu. + //! If zero and packet_mtu is set, derived from packet_mtu. core::nanoseconds_t packet_length; + //! Maximum transmission unit (MTU) in bytes. + //! @remarks + //! If zero and packet_length is also zero, DefaultPacketMtu is used. + //! If zero and packet_length is set, packet_mtu is ignored. + size_t packet_mtu; + //! FEC writer parameters. fec::WriterConfig fec_writer; @@ -108,7 +127,9 @@ struct SenderSinkConfig { SenderSinkConfig(); //! Fill unset values with defaults. - void deduce_defaults(); + //! @p encoding_sample_spec is the packet encoding spec (rate + channels + format), + //! used to derive packet_length from packet_mtu when needed. + void deduce_defaults(const audio::SampleSpec& encoding_sample_spec); }; //! Parameters of sender slot. diff --git a/src/internal_modules/roc_pipeline/sender_sink.cpp b/src/internal_modules/roc_pipeline/sender_sink.cpp index 17babdd74..07b210cf5 100644 --- a/src/internal_modules/roc_pipeline/sender_sink.cpp +++ b/src/internal_modules/roc_pipeline/sender_sink.cpp @@ -8,6 +8,7 @@ #include "roc_pipeline/sender_sink.h" #include "roc_audio/resampler_map.h" +#include "roc_audio/sample_spec.h" #include "roc_core/log.h" #include "roc_core/panic.h" @@ -27,7 +28,15 @@ SenderSink::SenderSink(const SenderSinkConfig& sink_config, , arena_(arena) , frame_writer_(NULL) , valid_(false) { - sink_config_.deduce_defaults(); + // Resolve packet_length using encoding spec if available. + { + const rtp::Encoding* enc = encoding_map_.find_by_pt(sink_config_.payload_type); + if (enc) { + sink_config_.deduce_defaults(enc->sample_spec); + } else { + sink_config_.deduce_defaults(audio::SampleSpec()); + } + } audio::IFrameWriter* frm_writer = &fanout_; diff --git a/src/public_api/include/roc/config.h b/src/public_api/include/roc/config.h index 4335e7bfe..aa7ae0362 100644 --- a/src/public_api/include/roc/config.h +++ b/src/public_api/include/roc/config.h @@ -671,10 +671,23 @@ typedef struct roc_sender_config { * accumulated or the sender is flushed or closed. Larger number reduces * packet overhead but also does not allow smaller latency. * - * If zero, default value is used. + * If zero and \c packet_mtu is also zero, default value is used. + * If zero and \c packet_mtu is set, packet length is derived from MTU. + * If both are set, a warning is logged and the smaller resulting size is used. */ unsigned long long packet_length; + /** Maximum transmission unit (MTU) for outgoing packets, in bytes. + * + * Defines the maximum network packet size. The sender will choose a packet + * duration that fits within this MTU (accounting for RTP header overhead). + * + * If zero and \c packet_length is also zero, a default MTU of 1200 bytes is used. + * If zero and \c packet_length is set, MTU is ignored. + * If both are set, a warning is logged and the smaller resulting size is used. + */ + unsigned int packet_mtu; + /** Enable packet interleaving. * * If non-zero, the sender shuffles packets before sending them. This diff --git a/src/public_api/src/adapters.cpp b/src/public_api/src/adapters.cpp index 587f47492..51c891858 100644 --- a/src/public_api/src/adapters.cpp +++ b/src/public_api/src/adapters.cpp @@ -94,6 +94,10 @@ bool sender_config_from_user(node::Context& context, out.packet_length = (core::nanoseconds_t)in.packet_length; } + if (in.packet_mtu != 0) { + out.packet_mtu = (size_t)in.packet_mtu; + } + if (in.target_latency != 0) { out.latency.target_latency = (core::nanoseconds_t)in.target_latency; } diff --git a/src/tools/roc_send/cmdline.ggo b/src/tools/roc_send/cmdline.ggo index 94fbde67e..02087c4db 100644 --- a/src/tools/roc_send/cmdline.ggo +++ b/src/tools/roc_send/cmdline.ggo @@ -38,6 +38,9 @@ section "Options" option "packet-len" - "Outgoing packet length, TIME units" string optional + option "packet-mtu" - "Maximum network packet size, in SIZE units (e.g. 1200, 1480)" + typestr="SIZE" string optional + option "frame-len" - "Duration of the internal frames, TIME units" typestr="TIME" string optional diff --git a/src/tools/roc_send/main.cpp b/src/tools/roc_send/main.cpp index 551c3ce96..a726ecf55 100644 --- a/src/tools/roc_send/main.cpp +++ b/src/tools/roc_send/main.cpp @@ -114,6 +114,17 @@ int main(int argc, char** argv) { } } + if (args.packet_mtu_given) { + if (!core::parse_size(args.packet_mtu_arg, sender_config.packet_mtu)) { + roc_log(LogError, "invalid --packet-mtu: bad format"); + return 1; + } + if (sender_config.packet_mtu == 0) { + roc_log(LogError, "invalid --packet-mtu: should be > 0"); + return 1; + } + } + if (args.source_given) { address::EndpointUri source_endpoint(heap_arena); if (!address::parse_endpoint_uri(