Skip to content

Commit 0642b43

Browse files
authored
Merge pull request restsend#5 from yeoleobun/tts
fix(tts): timestamp gap between tts track, and clock rate for g722
2 parents bda0658 + c3157bc commit 0642b43

6 files changed

Lines changed: 98 additions & 96 deletions

File tree

src/media/tests/tts_track.rs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ async fn test_tts_track_end_of_stream() -> Result<()> {
537537

538538
#[tokio::test]
539539
async fn test_tts_track_base64() -> Result<()> {
540-
// Create a command channel
540+
// Create a command channel
541541
let (command_tx, command_rx) = mpsc::unbounded_channel();
542542

543543
// Create a TtsTrack with non-streaming mode
@@ -567,16 +567,27 @@ async fn test_tts_track_base64() -> Result<()> {
567567
command_tx.send(SynthesisCommand {
568568
text,
569569
base64: true,
570+
end_of_stream: true,
570571
..Default::default()
571572
})?;
572573

573-
let mut bytes_received = 0;
574+
let mut sample_received = 0;
574575
let timeout = tokio::time::sleep(Duration::from_millis(3000));
575576
tokio::pin!(timeout);
576577
loop {
577578
tokio::select! {
578-
_ = &mut timeout => {
579-
break;
579+
biased;
580+
packet = packet_rx.recv() => {
581+
match packet {
582+
Some(packet) => {
583+
if let Samples::PCM { samples } = &packet.samples {
584+
sample_received += samples.len();
585+
}
586+
}
587+
None => {
588+
break
589+
}
590+
}
580591
}
581592
event = event_rx.recv() => {
582593
match event {
@@ -589,20 +600,14 @@ async fn test_tts_track_base64() -> Result<()> {
589600
_ => {}
590601
}
591602
}
592-
packet = packet_rx.recv() => {
593-
match packet {
594-
Some(packet) => {
595-
if let Samples::PCM { samples } = &packet.samples {
596-
bytes_received += samples.len();
597-
}
598-
}
599-
None => {
600-
break
601-
}
602-
}
603+
_ = &mut timeout => {
604+
break;
603605
}
606+
607+
604608
}
605609
}
606-
assert!(bytes_received >= 16000, "Not enough bytes");
610+
611+
assert!(sample_received >= 8000, "Not enough bytes");
607612
Ok(())
608613
}

src/media/track/rtc.rs

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ use rustrtc::{
2121
track::SampleStreamTrack,
2222
},
2323
};
24-
use std::{sync::Arc, time::Duration};
24+
use std::{
25+
sync::Arc,
26+
time::{Duration, Instant},
27+
};
2528
use tokio::sync::Mutex;
2629
use tokio_util::sync::CancellationToken;
2730
use tracing::{debug, info};
@@ -61,10 +64,11 @@ pub struct RtcTrack {
6164
local_source: Option<Arc<SampleStreamSource>>,
6265
encoder: TrackCodec,
6366
ssrc: u32,
64-
payload_type: Arc<std::sync::atomic::AtomicU8>,
67+
payload_type: u8,
6568
pub peer_connection: Option<Arc<PeerConnection>>,
66-
next_rtp_timestamp: Arc<std::sync::atomic::AtomicU32>,
67-
next_rtp_sequence_number: Arc<std::sync::atomic::AtomicU16>,
69+
next_rtp_timestamp: u32,
70+
next_rtp_sequence_number: u16,
71+
last_packet_time: Option<Instant>,
6872
}
6973

7074
impl RtcTrack {
@@ -85,10 +89,11 @@ impl RtcTrack {
8589
local_source: None,
8690
encoder: TrackCodec::new(),
8791
ssrc: 0,
88-
payload_type: Arc::new(std::sync::atomic::AtomicU8::new(0)),
92+
payload_type: 0,
8993
peer_connection: None,
90-
next_rtp_timestamp: Arc::new(std::sync::atomic::AtomicU32::new(0)),
91-
next_rtp_sequence_number: Arc::new(std::sync::atomic::AtomicU16::new(0)),
94+
next_rtp_timestamp: 0,
95+
next_rtp_sequence_number: 0,
96+
last_packet_time: None,
9297
}
9398
}
9499

@@ -164,8 +169,7 @@ impl RtcTrack {
164169
.payload_type
165170
.unwrap_or_else(|| codec.payload_type());
166171

167-
self.payload_type
168-
.store(payload_type, std::sync::atomic::Ordering::SeqCst);
172+
self.payload_type = payload_type;
169173

170174
let params = RtpCodecParameters {
171175
clock_rate: codec.clock_rate(),
@@ -181,7 +185,7 @@ impl RtcTrack {
181185
peer_connection.clone(),
182186
self.track_id.clone(),
183187
self.processor_chain.clone(),
184-
self.payload_type.clone(),
188+
self.payload_type,
185189
);
186190

187191
if self.rtc_config.mode == TransportMode::Rtp {
@@ -195,7 +199,7 @@ impl RtcTrack {
195199
self.track_id.clone(),
196200
self.cancel_token.clone(),
197201
self.processor_chain.clone(),
198-
self.payload_type.clone(),
202+
self.payload_type,
199203
);
200204
}
201205
}
@@ -209,7 +213,7 @@ impl RtcTrack {
209213
pc: Arc<PeerConnection>,
210214
track_id: TrackId,
211215
processor_chain: ProcessorChain,
212-
default_payload_type: Arc<std::sync::atomic::AtomicU8>,
216+
default_payload_type: u8,
213217
) {
214218
let cancel_token = self.cancel_token.clone();
215219
let packet_sender = self.packet_sender.clone();
@@ -282,7 +286,7 @@ impl RtcTrack {
282286
track_id: TrackId,
283287
cancel_token: CancellationToken,
284288
processor_chain: ProcessorChain,
285-
default_payload_type: Arc<std::sync::atomic::AtomicU8>,
289+
default_payload_type: u8,
286290
) {
287291
let (tx, mut rx) =
288292
tokio::sync::mpsc::unbounded_channel::<rustrtc::media::frame::AudioFrame>();
@@ -303,7 +307,7 @@ impl RtcTrack {
303307
&track_id_proc,
304308
&packet_sender_proc,
305309
&mut processor_chain_proc,
306-
default_payload_type.clone(),
310+
default_payload_type,
307311
)
308312
.await;
309313
}
@@ -335,13 +339,11 @@ impl RtcTrack {
335339
track_id: &TrackId,
336340
packet_sender: &Arc<Mutex<Option<TrackPacketSender>>>,
337341
processor_chain: &mut ProcessorChain,
338-
default_payload_type: Arc<std::sync::atomic::AtomicU8>,
342+
default_payload_type: u8,
339343
) {
340344
let packet_sender = packet_sender.lock().await;
341345
if let Some(sender) = packet_sender.as_ref() {
342-
let payload_type = frame
343-
.payload_type
344-
.unwrap_or_else(|| default_payload_type.load(std::sync::atomic::Ordering::SeqCst));
346+
let payload_type = frame.payload_type.unwrap_or(default_payload_type);
345347
let src_codec = match CodecType::try_from(payload_type) {
346348
Ok(c) => c,
347349
Err(_) => {
@@ -401,8 +403,7 @@ impl RtcTrack {
401403
if let Some(codec) = codec {
402404
if codec != CodecType::TelephoneEvent {
403405
info!(track_id=%self.track_id, "Negotiated primary audio PT {} ({:?})", pt, codec);
404-
self.payload_type
405-
.store(pt, std::sync::atomic::Ordering::SeqCst);
406+
self.payload_type = pt;
406407
break;
407408
}
408409
}
@@ -512,22 +513,32 @@ impl Track for RtcTrack {
512513
let (_, encoded) = self.encoder.encode(payload_type, packet.clone());
513514
let target_codec = CodecType::try_from(payload_type)?;
514515
if !encoded.is_empty() {
515-
let target_samples =
516-
(samples.len() as u64 * target_codec.samplerate() as u64
517-
/ packet.sample_rate as u64) as u32;
518-
let sample_count_per_channel =
519-
target_samples / self.track_config.channels as u32;
520-
let rtp_timestamp = self.next_rtp_timestamp.fetch_add(
521-
sample_count_per_channel,
522-
std::sync::atomic::Ordering::SeqCst,
523-
);
524-
let sequence_number = self
525-
.next_rtp_sequence_number
526-
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
516+
let clock_rate = target_codec.clock_rate();
517+
518+
let now = Instant::now();
519+
if let Some(last_time) = self.last_packet_time {
520+
let elapsed = now.duration_since(last_time);
521+
if elapsed.as_millis() > 50 {
522+
let gap_increment =
523+
(elapsed.as_millis() as u32 * clock_rate) / 1000;
524+
self.next_rtp_timestamp += gap_increment;
525+
}
526+
}
527+
528+
self.last_packet_time = Some(now);
529+
530+
let timestamp_increment = (samples.len() as u64 * clock_rate as u64
531+
/ packet.sample_rate as u64
532+
/ self.track_config.channels as u64)
533+
as u32;
534+
let rtp_timestamp = self.next_rtp_timestamp;
535+
self.next_rtp_timestamp += timestamp_increment;
536+
let sequence_number = self.next_rtp_sequence_number;
537+
self.next_rtp_sequence_number += 1;
527538

528539
let frame = RtcAudioFrame {
529540
data: Bytes::from(encoded),
530-
clock_rate: target_codec.clock_rate(),
541+
clock_rate,
531542
payload_type: Some(payload_type),
532543
sequence_number: Some(sequence_number),
533544
rtp_timestamp,
@@ -540,28 +551,36 @@ impl Track for RtcTrack {
540551
payload_type,
541552
sequence_number,
542553
} => {
543-
let target_sample_rate = match *payload_type {
544-
0 | 8 | 18 => 8000,
545-
9 => 16000,
554+
let clock_rate = match *payload_type {
555+
0 | 8 | 9 | 18 => 8000,
546556
111 => 48000,
547557
_ => packet.sample_rate,
548558
};
549559

550-
// Estimate samples if we don't have them
560+
let now = Instant::now();
561+
if let Some(last_time) = self.last_packet_time {
562+
let elapsed = now.duration_since(last_time);
563+
if elapsed.as_millis() > 50 {
564+
let gap_increment = (elapsed.as_millis() as u32 * clock_rate) / 1000;
565+
self.next_rtp_timestamp += gap_increment;
566+
}
567+
}
568+
self.last_packet_time = Some(now);
569+
551570
let increment = match *payload_type {
552571
0 | 8 | 18 => payload.len() as u32,
553-
9 => (payload.len() * 2) as u32,
554-
111 => (target_sample_rate / 50) as u32, // Assume 20ms for Opus if unknown
555-
_ => (target_sample_rate / 50) as u32,
572+
9 => payload.len() as u32,
573+
111 => (clock_rate / 50) as u32,
574+
_ => (clock_rate / 50) as u32,
556575
};
557-
let rtp_timestamp = self
558-
.next_rtp_timestamp
559-
.fetch_add(increment, std::sync::atomic::Ordering::SeqCst);
576+
577+
let rtp_timestamp = self.next_rtp_timestamp;
578+
self.next_rtp_timestamp += increment;
560579
let sequence_number = *sequence_number;
561580

562581
let frame = RtcAudioFrame {
563582
data: Bytes::from(payload.clone()),
564-
clock_rate: target_sample_rate,
583+
clock_rate,
565584
payload_type: Some(*payload_type),
566585
sequence_number: Some(sequence_number),
567586
rtp_timestamp,
@@ -577,7 +596,7 @@ impl Track for RtcTrack {
577596

578597
impl RtcTrack {
579598
fn get_payload_type(&self) -> u8 {
580-
let pt = self.payload_type.load(std::sync::atomic::Ordering::SeqCst);
599+
let pt = self.payload_type;
581600
if pt != 0 {
582601
return pt;
583602
}

src/media/track/tts.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
use crate::{
22
event::{EventSender, SessionEvent},
3-
media::AudioFrame,
4-
media::Samples,
5-
media::{
6-
cache,
7-
processor::ProcessorChain,
8-
track::{Track, TrackConfig, TrackId, TrackPacketSender},
9-
},
3+
media::{AudioFrame, Samples, cache, processor::ProcessorChain, track::{Track, TrackConfig, TrackId, TrackPacketSender}},
104
synthesis::{
115
Subtitle, SynthesisClient, SynthesisCommand, SynthesisCommandReceiver,
126
SynthesisCommandSender, SynthesisEvent, bytes_size_to_duration,
@@ -18,6 +12,7 @@ use audio_codec::bytes_to_samples;
1812
use base64::{Engine, prelude::BASE64_STANDARD};
1913
use bytes::{Bytes, BytesMut};
2014
use futures::StreamExt;
15+
use unic_emoji::char::is_emoji;
2116
use std::{
2217
collections::{HashMap, VecDeque},
2318
sync::{
@@ -97,6 +92,12 @@ struct TtsTask {
9792
graceful: Arc<AtomicBool>,
9893
}
9994

95+
pub fn strip_emoji_chars(text: &str) -> String {
96+
text.chars()
97+
.filter(|&c| c.is_ascii() || !is_emoji(c))
98+
.collect()
99+
}
100+
100101
impl TtsTask {
101102
async fn run(mut self) -> Result<()> {
102103
let mut stream;
@@ -370,6 +371,8 @@ impl TtsTask {
370371

371372
let emit_entry = self.get_emit_entry_mut(assume_seq);
372373

374+
let text = strip_emoji_chars(text);
375+
373376
// if text is empty:
374377
// in streaming mode, skip it
375378
// in non streaming mode, set entry[seq] finished to true

src/synthesis/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub struct SynthesisCommand {
2828
}
2929
pub type SynthesisCommandSender = mpsc::UnboundedSender<SynthesisCommand>;
3030
pub type SynthesisCommandReceiver = mpsc::UnboundedReceiver<SynthesisCommand>;
31-
pub use self::tencent_cloud::strip_emoji_chars;
3231

3332
#[derive(Debug, Clone, Serialize, Hash, Eq, PartialEq)]
3433
pub enum SynthesisType {

src/synthesis/tencent_cloud.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use tokio_tungstenite::{
2020
MaybeTlsStream, WebSocketStream, connect_async, tungstenite::protocol::Message,
2121
};
2222
use tracing::{debug, warn};
23-
use unic_emoji::char::is_emoji;
2423
use urlencoding;
2524
use uuid::Uuid;
2625

@@ -98,16 +97,6 @@ impl From<&TencentSubtitle> for Subtitle {
9897
}
9998
}
10099

101-
// tencent cloud will crash if text contains emoji
102-
// Only remove non-ASCII emoji characters. Keep all ASCII (digits, letters, punctuation),
103-
// since some ASCII (e.g., '0'..'9', '#', '*') are marked with the Unicode Emoji property
104-
// due to keycap sequences but are safe and expected in text.
105-
pub fn strip_emoji_chars(text: &str) -> String {
106-
text.chars()
107-
.filter(|&c| c.is_ascii() || !is_emoji(c))
108-
.collect()
109-
}
110-
111100
// construct request url
112101
// for non-streaming client, text is Some
113102
// session_id is used for tencent cloud tts service, not the session_id of media session
@@ -316,8 +305,7 @@ impl SynthesisClient for RealTimeClient {
316305
option: Option<SynthesisOption>,
317306
) -> Result<()> {
318307
if let Some(tx) = &self.tx {
319-
let text = strip_emoji_chars(text);
320-
tx.send((text, cmd_seq, option))?;
308+
tx.send((text.to_string(), cmd_seq, option))?;
321309
} else {
322310
return Err(anyhow::anyhow!("TencentCloud TTS: missing client sender"));
323311
};
@@ -419,7 +407,6 @@ impl SynthesisClient for StreamingClient {
419407
_option: Option<SynthesisOption>,
420408
) -> Result<()> {
421409
if let Some(sink) = &mut self.sink {
422-
let text = strip_emoji_chars(text);
423410
let request = WebSocketRequest::synthesis_action(&self.session_id, &text);
424411
let data = serde_json::to_string(&request)?;
425412
sink.send(Message::Text(data.into())).await?;

0 commit comments

Comments
 (0)