@@ -1068,3 +1068,132 @@ func TestE2EE_H264RoundTrip(t *testing.T) {
10681068 require .NoError (t , err )
10691069 require .NotNil (t , dec )
10701070}
1071+
1072+ // TestDynacastRepublish exercises two behaviours around simulcast dynacast:
1073+ //
1074+ // 1. Dynacast: a SubscribedQualityUpdate from the server disables the simulcast
1075+ // layers no subscriber needs. A disabled layer's LocalTrack stops writing
1076+ // samples (LocalTrack.disabled gates the write worker).
1077+ // 2. Re-publish reset: when tracks are re-published (e.g. on a full reconnect,
1078+ // which reuses the same *LocalTrack objects via republishTracks), the
1079+ // per-layer disabled flag is reset so every layer resumes publishing until
1080+ // the new SFU issues its own dynacast update. Without the reset, a layer
1081+ // disabled before the reconnect would stay dark forever.
1082+ //
1083+ // The dynacast update is injected through handleSubscribedQualityUpdate, which
1084+ // is exactly the path Room.OnSubscribedQualityUpdate drives for a real server
1085+ // message, so the layer-disabling wiring is covered end-to-end. The re-publish
1086+ // is driven by a real full reconnect.
1087+ func TestDynacastRepublish (t * testing.T ) {
1088+ if apiKey == "" || apiSecret == "" {
1089+ t .Skip ("no LIVEKIT_KEYS; requires a running livekit-server" )
1090+ }
1091+
1092+ videoCodec := webrtc.RTPCodecCapability {MimeType : webrtc .MimeTypeVP8 , ClockRate : 90000 }
1093+ const videoName = "dynacast_video"
1094+
1095+ var subVideoRTP atomic.Int32
1096+ subCB := & RoomCallback {
1097+ ParticipantCallback : ParticipantCallback {
1098+ OnTrackSubscribed : func (track * webrtc.TrackRemote , publication * RemoteTrackPublication , _ * RemoteParticipant ) {
1099+ if publication .Name () != videoName {
1100+ return
1101+ }
1102+ go func () {
1103+ for {
1104+ if _ , _ , err := track .ReadRTP (); err != nil {
1105+ return
1106+ }
1107+ subVideoRTP .Add (1 )
1108+ }
1109+ }()
1110+ },
1111+ },
1112+ }
1113+ sub , err := createAgent (t .Name (), subCB , "subscriber-dynacast" )
1114+ require .NoError (t , err )
1115+ defer sub .Disconnect ()
1116+
1117+ // simTracks is ordered [LOW, MEDIUM, HIGH]; republishTracks reuses these
1118+ // same objects, so they remain the canonical layer handles across reconnect.
1119+ simTracks := newSimulcastSampleTracks (t , videoCodec , "SC_" + videoName )
1120+ require .Len (t , simTracks , 3 )
1121+ layerOf := map [livekit.VideoQuality ]* LocalTrack {
1122+ livekit .VideoQuality_LOW : simTracks [0 ],
1123+ livekit .VideoQuality_MEDIUM : simTracks [1 ],
1124+ livekit .VideoQuality_HIGH : simTracks [2 ],
1125+ }
1126+
1127+ // OnReconnected fires right after OnRestarted -> republishTracks (which resets
1128+ // the disabled flags) and before the new SFU issues its own dynacast update.
1129+ // Snapshot the flags here so the "re-enabled" assertion is race-free against a
1130+ // later server update that could re-disable unwatched layers.
1131+ var reconnected , allEnabledAfterRepublish atomic.Bool
1132+ pubCB := & RoomCallback {
1133+ OnReconnected : func () {
1134+ allEnabled := true
1135+ for _ , tr := range simTracks {
1136+ if tr .disabled .Load () {
1137+ allEnabled = false
1138+ }
1139+ }
1140+ allEnabledAfterRepublish .Store (allEnabled )
1141+ reconnected .Store (true )
1142+ },
1143+ }
1144+ pub , err := createAgent (t .Name (), pubCB , "publisher-dynacast" )
1145+ require .NoError (t , err )
1146+ defer pub .Disconnect ()
1147+
1148+ videoPub , err := pub .LocalParticipant .PublishSimulcastTrack (simTracks , & TrackPublicationOptions {Name : videoName })
1149+ require .NoError (t , err )
1150+ require .NotNil (t , videoPub )
1151+ require .NotEmpty (t , videoPub .SID ())
1152+
1153+ // end-to-end: the subscriber receives video RTP
1154+ require .Eventually (t , func () bool {
1155+ return subVideoRTP .Load () > 0
1156+ }, 15 * time .Second , 100 * time .Millisecond , "subscriber should receive simulcast video RTP" )
1157+
1158+ // all layers start enabled (not disabled)
1159+ for q , tr := range layerOf {
1160+ require .False (t , tr .disabled .Load (), "layer %s should start enabled" , q )
1161+ }
1162+
1163+ // (1) dynacast: server reports only HIGH is needed -> LOW and MEDIUM disabled.
1164+ // handleSubscribedQualityUpdate applies the layer flags synchronously.
1165+ pub .LocalParticipant .handleSubscribedQualityUpdate (& livekit.SubscribedQualityUpdate {
1166+ TrackSid : videoPub .SID (),
1167+ SubscribedCodecs : []* livekit.SubscribedCodec {
1168+ {
1169+ Codec : "vp8" ,
1170+ Qualities : []* livekit.SubscribedQuality {
1171+ {Quality : livekit .VideoQuality_LOW , Enabled : false },
1172+ {Quality : livekit .VideoQuality_MEDIUM , Enabled : false },
1173+ {Quality : livekit .VideoQuality_HIGH , Enabled : true },
1174+ },
1175+ },
1176+ },
1177+ })
1178+ require .True (t , layerOf [livekit .VideoQuality_LOW ].disabled .Load (), "LOW must be disabled by dynacast" )
1179+ require .True (t , layerOf [livekit .VideoQuality_MEDIUM ].disabled .Load (), "MEDIUM must be disabled by dynacast" )
1180+ require .False (t , layerOf [livekit .VideoQuality_HIGH ].disabled .Load (), "HIGH must stay enabled" )
1181+
1182+ // (2) re-publish reset via a full reconnect: OnRestarted -> republishTracks
1183+ // re-publishes these same tracks, which must clear the disabled flags.
1184+ reconnected .Store (false )
1185+ pub .Simulate (SimulateNodeFailure )
1186+ require .Eventually (t , func () bool {
1187+ return reconnected .Load ()
1188+ }, 20 * time .Second , 100 * time .Millisecond , "publisher should complete a full reconnect" )
1189+
1190+ // the fix under test: re-publishing the (previously dynacast-disabled) tracks
1191+ // reset every layer's disabled flag to false, captured at reconnect time.
1192+ require .True (t , allEnabledAfterRepublish .Load (), "re-published layers must reset disabled to false" )
1193+
1194+ // end-to-end: video RTP resumes after the re-publish
1195+ baseline := subVideoRTP .Load ()
1196+ require .Eventually (t , func () bool {
1197+ return subVideoRTP .Load () > baseline
1198+ }, 20 * time .Second , 100 * time .Millisecond , "subscriber should receive video RTP again after re-publish" )
1199+ }
0 commit comments