@@ -119,6 +119,10 @@ pub struct TrackPublishOptions {
119119 pub stream : String ,
120120 pub preconnect_buffer : bool ,
121121 pub packet_trailer_features : PacketTrailerFeatures ,
122+ /// RTP scalability mode (e.g. "L3T3_KEY"). When set, a single RTP
123+ /// encoding is produced and that mode is forwarded to libwebrtc to
124+ /// enable true SVC for VP9/AV1. Has no effect for VP8/H264.
125+ pub scalability_mode : Option < String > ,
122126}
123127
124128impl Default for TrackPublishOptions {
@@ -135,6 +139,7 @@ impl Default for TrackPublishOptions {
135139 stream : "" . to_string ( ) ,
136140 preconnect_buffer : false ,
137141 packet_trailer_features : PacketTrailerFeatures :: default ( ) ,
142+ scalability_mode : None ,
138143 }
139144 }
140145}
@@ -176,6 +181,16 @@ pub fn compute_video_encodings(
176181 } ,
177182 } ;
178183
184+ // SVC: when an explicit scalability_mode is set, emit a single encoding
185+ // and let libwebrtc produce the spatial/temporal layers internally.
186+ if let Some ( mode) = options. scalability_mode . clone ( ) {
187+ let mut encodings = into_rtp_encodings ( width, height, & [ initial_preset] ) ;
188+ if let Some ( first) = encodings. first_mut ( ) {
189+ first. scalability_mode = Some ( mode) ;
190+ }
191+ return encodings;
192+ }
193+
179194 if !options. simulcast {
180195 return into_rtp_encodings ( width, height, & [ initial_preset] ) ;
181196 }
@@ -310,6 +325,19 @@ pub fn video_quality_for_rid(rid: &str) -> Option<proto::VideoQuality> {
310325 }
311326}
312327
328+ /// Parse the number of spatial layers from an RTP scalability mode string.
329+ /// Standard modes start with `L<N>` where `<N>` is the spatial-layer count
330+ /// (e.g. "L3T3_KEY" -> 3, "L2T3" -> 2, "L1T3" -> 1).
331+ pub fn spatial_layers_from_scalability_mode ( mode : & str ) -> u32 {
332+ if let Some ( rest) = mode. strip_prefix ( 'L' ) {
333+ let digits: String = rest. chars ( ) . take_while ( |c| c. is_ascii_digit ( ) ) . collect ( ) ;
334+ if let Ok ( n) = digits. parse :: < u32 > ( ) {
335+ return n. max ( 1 ) ;
336+ }
337+ }
338+ 1
339+ }
340+
313341pub fn video_layers_from_encodings (
314342 width : u32 ,
315343 height : u32 ,
@@ -326,6 +354,39 @@ pub fn video_layers_from_encodings(
326354 } ] ;
327355 }
328356
357+ // SVC: a single RTP encoding carries multiple spatial layers internally.
358+ // Synthesise one VideoLayer per spatial layer so the SFU knows the track
359+ // has switchable quality tiers.
360+ if encodings. len ( ) == 1 {
361+ if let Some ( mode) = encodings[ 0 ] . scalability_mode . as_ref ( ) {
362+ let spatial = spatial_layers_from_scalability_mode ( mode) ;
363+ if spatial > 1 {
364+ let total_bitrate = encodings[ 0 ] . max_bitrate . unwrap_or ( 0 ) ;
365+ let mut layers = Vec :: with_capacity ( spatial as usize ) ;
366+ // Highest spatial layer is the source resolution; each lower
367+ // layer is half on each axis (the libwebrtc default for
368+ // L2/L3 scalability modes).
369+ for i in 0 ..spatial {
370+ let scale = 1u32 << ( spatial - 1 - i) ;
371+ let quality = match ( spatial - 1 - i, spatial) {
372+ ( 0 , _) => proto:: VideoQuality :: High ,
373+ ( 1 , _) => proto:: VideoQuality :: Medium ,
374+ _ => proto:: VideoQuality :: Low ,
375+ } ;
376+ layers. push ( proto:: VideoLayer {
377+ quality : quality as i32 ,
378+ width : width / scale,
379+ height : height / scale,
380+ bitrate : ( total_bitrate / spatial as u64 ) as u32 ,
381+ ssrc : 0 ,
382+ ..Default :: default ( )
383+ } ) ;
384+ }
385+ return layers;
386+ }
387+ }
388+ }
389+
329390 let mut layers = Vec :: with_capacity ( encodings. len ( ) ) ;
330391 for encoding in encodings {
331392 let scale = encoding. scale_resolution_down_by . unwrap_or ( 1.0 ) ;
0 commit comments