fix(sync): stamp upstream media from source timing
This commit is contained in:
parent
e155a9e140
commit
d7ed1e0a4d
@ -50,6 +50,7 @@ pub struct CameraCapture {
|
|||||||
pipeline: gst::Pipeline,
|
pipeline: gst::Pipeline,
|
||||||
sink: gst_app::AppSink,
|
sink: gst_app::AppSink,
|
||||||
preview_tap_running: Option<Arc<AtomicBool>>,
|
preview_tap_running: Option<Arc<AtomicBool>>,
|
||||||
|
pts_rebaser: crate::live_capture_clock::SourcePtsRebaser,
|
||||||
}
|
}
|
||||||
|
|
||||||
include!("camera/capture_pipeline.rs");
|
include!("camera/capture_pipeline.rs");
|
||||||
|
|||||||
@ -1,21 +1,3 @@
|
|||||||
#[cfg(any(coverage, test))]
|
|
||||||
fn shared_capture_pts_us() -> u64 {
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
static CAPTURE_ORIGIN: OnceLock<Instant> = OnceLock::new();
|
|
||||||
CAPTURE_ORIGIN
|
|
||||||
.get_or_init(Instant::now)
|
|
||||||
.elapsed()
|
|
||||||
.as_micros()
|
|
||||||
.min(u64::MAX as u128) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(coverage, test)))]
|
|
||||||
fn shared_capture_pts_us() -> u64 {
|
|
||||||
crate::live_capture_clock::capture_pts_us()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CameraCapture {
|
impl CameraCapture {
|
||||||
pub fn new(device_fragment: Option<&str>, cfg: Option<CameraConfig>) -> anyhow::Result<Self> {
|
pub fn new(device_fragment: Option<&str>, cfg: Option<CameraConfig>) -> anyhow::Result<Self> {
|
||||||
gst::init().ok();
|
gst::init().ok();
|
||||||
@ -245,6 +227,7 @@ impl CameraCapture {
|
|||||||
pipeline,
|
pipeline,
|
||||||
sink,
|
sink,
|
||||||
preview_tap_running,
|
preview_tap_running,
|
||||||
|
pts_rebaser: crate::live_capture_clock::SourcePtsRebaser::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,15 +235,35 @@ impl CameraCapture {
|
|||||||
let sample = self.sink.pull_sample().ok()?;
|
let sample = self.sink.pull_sample().ok()?;
|
||||||
let buf = sample.buffer()?;
|
let buf = sample.buffer()?;
|
||||||
let map = buf.map_readable().ok()?;
|
let map = buf.map_readable().ok()?;
|
||||||
let pts = shared_capture_pts_us();
|
let source_pts_us = buf.pts().map(|ts| ts.nseconds() / 1_000);
|
||||||
static FIRST_CAMERA_PACKET: AtomicBool = AtomicBool::new(false);
|
let timing = self.pts_rebaser.rebase_or_now(source_pts_us, 1);
|
||||||
if !FIRST_CAMERA_PACKET.swap(true, Ordering::Relaxed) {
|
let pts = timing.packet_pts_us;
|
||||||
|
static CAMERA_PACKET_COUNT: std::sync::atomic::AtomicU64 =
|
||||||
|
std::sync::atomic::AtomicU64::new(0);
|
||||||
|
let packet_index = CAMERA_PACKET_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if packet_index == 0 {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
bytes = map.as_slice().len(),
|
bytes = map.as_slice().len(),
|
||||||
pts_us = pts,
|
pts_us = pts,
|
||||||
"📸 upstream webcam frames flowing"
|
"📸 upstream webcam frames flowing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if crate::live_capture_clock::upstream_timing_trace_enabled()
|
||||||
|
&& (packet_index < 10 || packet_index.is_multiple_of(300))
|
||||||
|
{
|
||||||
|
tracing::info!(
|
||||||
|
packet_index,
|
||||||
|
source_pts_us = timing.source_pts_us.unwrap_or_default(),
|
||||||
|
source_base_us = timing.source_base_us.unwrap_or_default(),
|
||||||
|
capture_base_us = timing.capture_base_us.unwrap_or_default(),
|
||||||
|
capture_now_us = timing.capture_now_us,
|
||||||
|
packet_pts_us = timing.packet_pts_us,
|
||||||
|
pull_path_delay_us = timing.capture_now_us as i128 - timing.packet_pts_us as i128,
|
||||||
|
used_source_pts = timing.used_source_pts,
|
||||||
|
bytes = map.as_slice().len(),
|
||||||
|
"📸 upstream webcam timing sample"
|
||||||
|
);
|
||||||
|
}
|
||||||
Some(VideoPacket {
|
Some(VideoPacket {
|
||||||
id: 2,
|
id: 2,
|
||||||
pts,
|
pts,
|
||||||
|
|||||||
@ -94,6 +94,7 @@ impl CameraCapture {
|
|||||||
pipeline,
|
pipeline,
|
||||||
sink,
|
sink,
|
||||||
preview_tap_running: None,
|
preview_tap_running: None,
|
||||||
|
pts_rebaser: crate::live_capture_clock::SourcePtsRebaser::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,24 +20,6 @@ use tracing::{debug, warn};
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use tracing::{error, info, trace};
|
use tracing::{error, info, trace};
|
||||||
|
|
||||||
#[cfg(any(coverage, test))]
|
|
||||||
fn shared_capture_pts_us() -> u64 {
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
static CAPTURE_ORIGIN: OnceLock<Instant> = OnceLock::new();
|
|
||||||
CAPTURE_ORIGIN
|
|
||||||
.get_or_init(Instant::now)
|
|
||||||
.elapsed()
|
|
||||||
.as_micros()
|
|
||||||
.min(u64::MAX as u128) as u64
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(coverage, test)))]
|
|
||||||
fn shared_capture_pts_us() -> u64 {
|
|
||||||
crate::live_capture_clock::capture_pts_us()
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIC_GAIN_ENV: &str = "LESAVKA_MIC_GAIN";
|
const MIC_GAIN_ENV: &str = "LESAVKA_MIC_GAIN";
|
||||||
const MIC_GAIN_CONTROL_ENV: &str = "LESAVKA_MIC_GAIN_CONTROL";
|
const MIC_GAIN_CONTROL_ENV: &str = "LESAVKA_MIC_GAIN_CONTROL";
|
||||||
const MIC_LEVEL_TAP_ENV: &str = "LESAVKA_UPLINK_MIC_LEVEL";
|
const MIC_LEVEL_TAP_ENV: &str = "LESAVKA_UPLINK_MIC_LEVEL";
|
||||||
@ -47,6 +29,7 @@ pub struct MicrophoneCapture {
|
|||||||
pipeline: gst::Pipeline,
|
pipeline: gst::Pipeline,
|
||||||
sink: gst_app::AppSink,
|
sink: gst_app::AppSink,
|
||||||
level_tap_running: Option<Arc<AtomicBool>>,
|
level_tap_running: Option<Arc<AtomicBool>>,
|
||||||
|
pts_rebaser: crate::live_capture_clock::SourcePtsRebaser,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MicrophoneCapture {
|
impl MicrophoneCapture {
|
||||||
@ -131,6 +114,7 @@ impl MicrophoneCapture {
|
|||||||
pipeline,
|
pipeline,
|
||||||
sink,
|
sink,
|
||||||
level_tap_running,
|
level_tap_running,
|
||||||
|
pts_rebaser: crate::live_capture_clock::SourcePtsRebaser::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,11 +124,30 @@ impl MicrophoneCapture {
|
|||||||
Ok(sample) => {
|
Ok(sample) => {
|
||||||
let buf = sample.buffer().unwrap();
|
let buf = sample.buffer().unwrap();
|
||||||
let map = buf.map_readable().unwrap();
|
let map = buf.map_readable().unwrap();
|
||||||
let pts = shared_capture_pts_us();
|
let source_pts_us = buf.pts().map(|ts| ts.nseconds() / 1_000);
|
||||||
|
let timing = self.pts_rebaser.rebase_or_now(source_pts_us, 1);
|
||||||
|
let pts = timing.packet_pts_us;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
{
|
{
|
||||||
static CNT: AtomicU64 = AtomicU64::new(0);
|
static CNT: AtomicU64 = AtomicU64::new(0);
|
||||||
let n = CNT.fetch_add(1, Ordering::Relaxed);
|
let n = CNT.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if crate::live_capture_clock::upstream_timing_trace_enabled()
|
||||||
|
&& (n < 10 || n.is_multiple_of(300))
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
packet_index = n,
|
||||||
|
source_pts_us = timing.source_pts_us.unwrap_or_default(),
|
||||||
|
source_base_us = timing.source_base_us.unwrap_or_default(),
|
||||||
|
capture_base_us = timing.capture_base_us.unwrap_or_default(),
|
||||||
|
capture_now_us = timing.capture_now_us,
|
||||||
|
packet_pts_us = timing.packet_pts_us,
|
||||||
|
pull_path_delay_us =
|
||||||
|
timing.capture_now_us as i128 - timing.packet_pts_us as i128,
|
||||||
|
used_source_pts = timing.used_source_pts,
|
||||||
|
bytes = map.len(),
|
||||||
|
"🎤 upstream microphone timing sample"
|
||||||
|
);
|
||||||
|
}
|
||||||
if n < 10 || n.is_multiple_of(300) {
|
if n < 10 || n.is_multiple_of(300) {
|
||||||
trace!("🎤⇧ cli pkt#{n} {} bytes", map.len());
|
trace!("🎤⇧ cli pkt#{n} {} bytes", map.len());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
static CAPTURE_ORIGIN: OnceLock<Instant> = OnceLock::new();
|
static CAPTURE_ORIGIN: OnceLock<Instant> = OnceLock::new();
|
||||||
@ -32,9 +32,104 @@ pub fn packet_age(pts_us: u64) -> Duration {
|
|||||||
Duration::from_micros(capture_pts_us().saturating_sub(pts_us))
|
Duration::from_micros(capture_pts_us().saturating_sub(pts_us))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decide whether extra upstream timing instrumentation should be emitted.
|
||||||
|
///
|
||||||
|
/// Inputs: none.
|
||||||
|
/// Outputs: `true` when detailed capture/rebase timing logs are enabled.
|
||||||
|
/// Why: A/V sync work needs bursts of deep timing visibility without leaving
|
||||||
|
/// noisy logs on during normal live operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn upstream_timing_trace_enabled() -> bool {
|
||||||
|
std::env::var("LESAVKA_UPSTREAM_TIMING_TRACE")
|
||||||
|
.ok()
|
||||||
|
.map(|value| {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
!(trimmed.eq_ignore_ascii_case("0")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("false")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("no")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("off"))
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct SourcePtsRebaserState {
|
||||||
|
source_base_us: Option<u64>,
|
||||||
|
capture_base_us: Option<u64>,
|
||||||
|
last_packet_pts_us: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebase source-buffer timestamps onto the shared client capture clock.
|
||||||
|
///
|
||||||
|
/// Inputs: optional source PTS values from one live capture pipeline.
|
||||||
|
/// Outputs: packet timestamps that share the same client clock origin across
|
||||||
|
/// camera and microphone while still advancing based on source timing rather
|
||||||
|
/// than late appsink pull time.
|
||||||
|
/// Why: camera and microphone encode paths can add different queue/encode
|
||||||
|
/// delays before appsink pull, so stamping at pull time bakes skew into the
|
||||||
|
/// packets before the server ever sees them.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct SourcePtsRebaser {
|
||||||
|
state: Mutex<SourcePtsRebaserState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of one client-side timestamp rebasing decision.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RebasedSourcePts {
|
||||||
|
pub packet_pts_us: u64,
|
||||||
|
pub capture_now_us: u64,
|
||||||
|
pub source_pts_us: Option<u64>,
|
||||||
|
pub source_base_us: Option<u64>,
|
||||||
|
pub capture_base_us: Option<u64>,
|
||||||
|
pub used_source_pts: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourcePtsRebaser {
|
||||||
|
/// Translate one source-buffer timestamp onto the shared capture clock.
|
||||||
|
///
|
||||||
|
/// Inputs: the buffer PTS if available plus the minimum monotonic step.
|
||||||
|
/// Outputs: a rebased packet timestamp and the values used to derive it.
|
||||||
|
/// Why: source PTS should drive packet timing when available, but packets
|
||||||
|
/// must still remain monotonic even if buffers repeat or arrive oddly.
|
||||||
|
#[must_use]
|
||||||
|
pub fn rebase_or_now(&self, source_pts_us: Option<u64>, min_step_us: u64) -> RebasedSourcePts {
|
||||||
|
let capture_now_us = capture_pts_us();
|
||||||
|
let mut state = self
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.expect("source pts rebaser mutex poisoned");
|
||||||
|
let mut packet_pts_us = capture_now_us;
|
||||||
|
let mut used_source_pts = false;
|
||||||
|
|
||||||
|
if let Some(source_pts_us) = source_pts_us {
|
||||||
|
let source_base_us = *state.source_base_us.get_or_insert(source_pts_us);
|
||||||
|
let capture_base_us = *state.capture_base_us.get_or_insert(capture_now_us);
|
||||||
|
packet_pts_us =
|
||||||
|
capture_base_us.saturating_add(source_pts_us.saturating_sub(source_base_us));
|
||||||
|
used_source_pts = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(last_packet_pts_us) = state.last_packet_pts_us
|
||||||
|
&& packet_pts_us <= last_packet_pts_us
|
||||||
|
{
|
||||||
|
packet_pts_us = last_packet_pts_us.saturating_add(min_step_us.max(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.last_packet_pts_us = Some(packet_pts_us);
|
||||||
|
RebasedSourcePts {
|
||||||
|
packet_pts_us,
|
||||||
|
capture_now_us,
|
||||||
|
source_pts_us,
|
||||||
|
source_base_us: state.source_base_us,
|
||||||
|
capture_base_us: state.capture_base_us,
|
||||||
|
used_source_pts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{capture_pts_us, packet_age};
|
use super::{SourcePtsRebaser, capture_pts_us, packet_age, upstream_timing_trace_enabled};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -53,4 +148,56 @@ mod tests {
|
|||||||
assert!(age >= Duration::from_millis(1));
|
assert!(age >= Duration::from_millis(1));
|
||||||
assert!(age < Duration::from_secs(1));
|
assert!(age < Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_pts_rebaser_preserves_source_delta_on_shared_capture_clock() {
|
||||||
|
let rebased = SourcePtsRebaser::default();
|
||||||
|
let first = rebased.rebase_or_now(Some(1_000_000), 1);
|
||||||
|
let second = rebased.rebase_or_now(Some(1_033_333), 1);
|
||||||
|
|
||||||
|
assert!(first.used_source_pts);
|
||||||
|
assert_eq!(
|
||||||
|
second.packet_pts_us.saturating_sub(first.packet_pts_us),
|
||||||
|
33_333
|
||||||
|
);
|
||||||
|
assert_eq!(first.source_base_us, Some(1_000_000));
|
||||||
|
assert_eq!(second.source_base_us, Some(1_000_000));
|
||||||
|
assert_eq!(first.capture_base_us, second.capture_base_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_pts_rebaser_stays_monotonic_when_source_pts_repeat() {
|
||||||
|
let rebased = SourcePtsRebaser::default();
|
||||||
|
let first = rebased.rebase_or_now(Some(50_000), 1);
|
||||||
|
let second = rebased.rebase_or_now(Some(50_000), 1);
|
||||||
|
|
||||||
|
assert_eq!(second.packet_pts_us, first.packet_pts_us + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_pts_rebaser_falls_back_to_capture_clock_without_source_pts() {
|
||||||
|
let rebased = SourcePtsRebaser::default();
|
||||||
|
let first = rebased.rebase_or_now(None, 1);
|
||||||
|
std::thread::sleep(Duration::from_millis(2));
|
||||||
|
let second = rebased.rebase_or_now(None, 1);
|
||||||
|
|
||||||
|
assert!(!first.used_source_pts);
|
||||||
|
assert!(!second.used_source_pts);
|
||||||
|
assert!(second.packet_pts_us > first.packet_pts_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upstream_timing_trace_flag_defaults_off_and_accepts_true_values() {
|
||||||
|
temp_env::with_var_unset("LESAVKA_UPSTREAM_TIMING_TRACE", || {
|
||||||
|
assert!(!upstream_timing_trace_enabled());
|
||||||
|
});
|
||||||
|
|
||||||
|
temp_env::with_var("LESAVKA_UPSTREAM_TIMING_TRACE", Some("1"), || {
|
||||||
|
assert!(upstream_timing_trace_enabled());
|
||||||
|
});
|
||||||
|
|
||||||
|
temp_env::with_var("LESAVKA_UPSTREAM_TIMING_TRACE", Some("false"), || {
|
||||||
|
assert!(!upstream_timing_trace_enabled());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,9 +32,24 @@ struct UpstreamClockState {
|
|||||||
microphone_base_remote_pts_us: Option<u64>,
|
microphone_base_remote_pts_us: Option<u64>,
|
||||||
last_video_local_pts_us: Option<u64>,
|
last_video_local_pts_us: Option<u64>,
|
||||||
last_audio_local_pts_us: Option<u64>,
|
last_audio_local_pts_us: Option<u64>,
|
||||||
|
camera_packet_count: u64,
|
||||||
|
microphone_packet_count: u64,
|
||||||
startup_anchor_logged: bool,
|
startup_anchor_logged: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upstream_timing_trace_enabled() -> bool {
|
||||||
|
std::env::var("LESAVKA_UPSTREAM_TIMING_TRACE")
|
||||||
|
.ok()
|
||||||
|
.map(|value| {
|
||||||
|
let trimmed = value.trim();
|
||||||
|
!(trimmed.eq_ignore_ascii_case("0")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("false")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("no")
|
||||||
|
|| trimmed.eq_ignore_ascii_case("off"))
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Coordinate upstream stream ownership and keep audio/video on one timeline.
|
/// Coordinate upstream stream ownership and keep audio/video on one timeline.
|
||||||
///
|
///
|
||||||
/// Inputs: stream-open/close events plus remote packet timestamps.
|
/// Inputs: stream-open/close events plus remote packet timestamps.
|
||||||
@ -118,6 +133,8 @@ impl UpstreamMediaRuntime {
|
|||||||
state.microphone_base_remote_pts_us = None;
|
state.microphone_base_remote_pts_us = None;
|
||||||
state.last_video_local_pts_us = None;
|
state.last_video_local_pts_us = None;
|
||||||
state.last_audio_local_pts_us = None;
|
state.last_audio_local_pts_us = None;
|
||||||
|
state.camera_packet_count = 0;
|
||||||
|
state.microphone_packet_count = 0;
|
||||||
state.startup_anchor_logged = false;
|
state.startup_anchor_logged = false;
|
||||||
}
|
}
|
||||||
match kind {
|
match kind {
|
||||||
@ -185,6 +202,8 @@ impl UpstreamMediaRuntime {
|
|||||||
state.microphone_base_remote_pts_us = None;
|
state.microphone_base_remote_pts_us = None;
|
||||||
state.last_video_local_pts_us = None;
|
state.last_video_local_pts_us = None;
|
||||||
state.last_audio_local_pts_us = None;
|
state.last_audio_local_pts_us = None;
|
||||||
|
state.camera_packet_count = 0;
|
||||||
|
state.microphone_packet_count = 0;
|
||||||
state.startup_anchor_logged = false;
|
state.startup_anchor_logged = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,6 +230,16 @@ impl UpstreamMediaRuntime {
|
|||||||
.lock()
|
.lock()
|
||||||
.expect("upstream media state mutex poisoned");
|
.expect("upstream media state mutex poisoned");
|
||||||
let session_id = state.session_id;
|
let session_id = state.session_id;
|
||||||
|
let packet_count = match kind {
|
||||||
|
UpstreamMediaKind::Camera => {
|
||||||
|
state.camera_packet_count = state.camera_packet_count.saturating_add(1);
|
||||||
|
state.camera_packet_count
|
||||||
|
}
|
||||||
|
UpstreamMediaKind::Microphone => {
|
||||||
|
state.microphone_packet_count = state.microphone_packet_count.saturating_add(1);
|
||||||
|
state.microphone_packet_count
|
||||||
|
}
|
||||||
|
};
|
||||||
let base_slot = match kind {
|
let base_slot = match kind {
|
||||||
UpstreamMediaKind::Camera => &mut state.camera_base_remote_pts_us,
|
UpstreamMediaKind::Camera => &mut state.camera_base_remote_pts_us,
|
||||||
UpstreamMediaKind::Microphone => &mut state.microphone_base_remote_pts_us,
|
UpstreamMediaKind::Microphone => &mut state.microphone_base_remote_pts_us,
|
||||||
@ -245,6 +274,21 @@ impl UpstreamMediaRuntime {
|
|||||||
local_pts_us = last_pts_us.saturating_add(min_step_us.max(1));
|
local_pts_us = last_pts_us.saturating_add(min_step_us.max(1));
|
||||||
}
|
}
|
||||||
*last_slot = Some(local_pts_us);
|
*last_slot = Some(local_pts_us);
|
||||||
|
if upstream_timing_trace_enabled()
|
||||||
|
&& (packet_count <= 10 || packet_count.is_multiple_of(300))
|
||||||
|
{
|
||||||
|
let remote_elapsed_us = remote_pts_us.saturating_sub(first_remote_for_kind);
|
||||||
|
info!(
|
||||||
|
session_id,
|
||||||
|
?kind,
|
||||||
|
packet_count,
|
||||||
|
remote_pts_us,
|
||||||
|
first_remote_for_kind,
|
||||||
|
remote_elapsed_us,
|
||||||
|
local_pts_us,
|
||||||
|
"upstream media rebase sample"
|
||||||
|
);
|
||||||
|
}
|
||||||
local_pts_us
|
local_pts_us
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user