88using LiveKit . Rooms . VideoStreaming ;
99using RichTypes ;
1010using System ;
11+ using System . Collections . Generic ;
1112using UnityEngine ;
1213using UnityEngine . Pool ;
1314
@@ -26,10 +27,12 @@ public class LivekitPlayer : IDisposable
2627 } ) ;
2728
2829 private readonly IRoom room ;
29- private readonly LivekitAudioSource audioSource ;
30- private ( Weak < IVideoStream > video , Weak < AudioStream > audio ) ? currentStream ;
30+ private readonly List < ( LivekitAudioSource source , Weak < AudioStream > stream ) > audioSources = new ( ) ;
31+ private readonly List < StreamKey > tempStreamKeys = new ( ) ;
32+ private Weak < IVideoStream > ? currentVideoStream ;
3133 private PlayerState playerState ;
3234 private LivekitAddress ? playingAddress ;
35+ private Vector3 audioPosition ;
3336
3437 private bool disposed ;
3538
@@ -41,14 +44,14 @@ public class LivekitPlayer : IDisposable
4144
4245 public PlayerState State => playerState ;
4346
44- public bool IsVideoOpened => currentStream != null && currentStream . Value . video . Resource . Has ;
47+ public bool IsVideoOpened => currentVideoStream != null && currentVideoStream . Value . Resource . Has ;
4548
46- private bool isAudioOpened => currentStream != null && currentStream . Value . audio . Resource . Has ;
49+ private bool isAudioOpened => audioSources . Count > 0
50+ && audioSources . Exists ( static a => a . stream . Resource . Has ) ;
4751
4852 public LivekitPlayer ( IRoom streamingRoom )
4953 {
5054 room = streamingRoom ;
51- audioSource = OBJECT_POOL . Get ( ) ;
5255 }
5356
5457 public void EnsureVideoIsPlaying ( )
@@ -71,70 +74,91 @@ public void EnsureAudioIsPlaying()
7174 {
7275 if ( State != PlayerState . PLAYING ) return ;
7376 if ( playingAddress == null ) return ;
74- if ( isAudioOpened ) return ;
7577
76- // If a specific user stream died, fallback to current-stream (first available track)
77- if ( playingAddress . Value . IsUserStream ( out _ ) )
78+ // Check if any audio stream died — if so, refresh all audio
79+ bool anyDied = false ;
80+
81+ foreach ( var ( _, stream ) in audioSources )
7882 {
79- OpenMedia ( LivekitAddress . CurrentStream ( ) ) ;
80- return ;
83+ if ( ! stream . Resource . Has )
84+ {
85+ anyDied = true ;
86+ break ;
87+ }
8188 }
8289
83- OpenMedia ( playingAddress . Value ) ;
90+ if ( ! anyDied && audioSources . Count > 0 ) return ;
91+
92+ // Release dead sources and re-collect all audio
93+ ReleaseAllAudioSources ( ) ;
94+ OpenAllAudioStreams ( ) ;
8495 }
8596
8697 public void OpenMedia ( LivekitAddress livekitAddress )
8798 {
8899 CloseCurrentStream ( ) ;
89100
90- currentStream = livekitAddress . Match (
101+ currentVideoStream = livekitAddress . Match (
91102 this ,
92103 onUserStream : static ( self , userStream ) =>
93- {
94- var video = self . room . VideoStreams . ActiveStream ( new StreamKey ( userStream . Identity , userStream . Sid ) ) ;
95- var audio = self . FindPairedAudio ( userStream . Identity , userStream . Sid ) ;
104+ self . room . VideoStreams . ActiveStream ( new StreamKey ( userStream . Identity , userStream . Sid ) ) ,
105+ onCurrentStream : static self => self . FirstVideo ( )
106+ ) ;
96107
97- if ( audio . Resource . Has )
98- {
99- self . audioSource . Construct ( audio ) ;
100- self . audioSource . Play ( ) ;
101- }
108+ OpenAllAudioStreams ( ) ;
109+
110+ playerState = PlayerState . PLAYING ;
111+ playingAddress = livekitAddress ;
112+ }
113+
114+ private void OpenAllAudioStreams ( )
115+ {
116+ CollectAllAudioTracks ( tempStreamKeys ) ;
102117
103- return ( video , audio ) ;
104- } ,
105- onCurrentStream : static self =>
118+ foreach ( StreamKey key in tempStreamKeys )
119+ {
120+ Weak < AudioStream > audioStream = room . AudioStreams . ActiveStream ( key ) ;
121+
122+ if ( ! audioStream . Resource . Has )
123+ continue ;
124+
125+ LivekitAudioSource source = OBJECT_POOL . Get ( ) ;
126+ source . Construct ( audioStream ) ;
127+ source . SetVolume ( Volume ) ;
128+ source . transform . position = audioPosition ;
129+ source . Play ( ) ;
130+ audioSources . Add ( ( source , audioStream ) ) ;
131+ }
132+ }
133+
134+ private void CollectAllAudioTracks ( List < StreamKey > output )
135+ {
136+ output . Clear ( ) ;
137+
138+ lock ( room . Participants )
139+ {
140+ foreach ( ( string identity , _ ) in room . Participants . RemoteParticipantIdentities ( ) )
106141 {
107- var videoTrack = self . FirstVideo ( ) ;
108- var audioTrack = self . FirstAudio ( ) ;
142+ var participant = room . Participants . RemoteParticipant ( identity ) ;
109143
110- if ( audioTrack . Resource . Has )
111- {
112- self . audioSource . Construct ( audioTrack ) ;
113- self . audioSource . Play ( ) ;
114- }
144+ if ( participant == null )
145+ continue ;
115146
116- return ( videoTrack , audioTrack ) ;
147+ foreach ( ( string sid , TrackPublication track ) in participant . Tracks )
148+ if ( track . Kind == TrackKind . KindAudio )
149+ output . Add ( new StreamKey ( identity , sid ) ) ;
117150 }
118- ) ;
119-
120- playerState = PlayerState . PLAYING ;
121- playingAddress = livekitAddress ;
151+ }
122152 }
123153
124154 private Weak < IVideoStream > FirstVideo ( )
125155 {
126156 var result = FirstAvailableTrackSid ( TrackKind . KindVideo ) ;
127- if ( result . HasValue == false ) return Weak < IVideoStream > . Null ;
128- var value = result . Value ;
129- return room . VideoStreams . ActiveStream ( value ) ;
130- }
131157
132- private Weak < AudioStream > FirstAudio ( )
133- {
134- var result = FirstAvailableTrackSid ( TrackKind . KindAudio ) ;
135- if ( result . HasValue == false ) return Weak < AudioStream > . Null ;
136- var value = result . Value ;
137- return room . AudioStreams . ActiveStream ( value ) ;
158+ if ( result . HasValue == false )
159+ return Weak < IVideoStream > . Null ;
160+
161+ return room . VideoStreams . ActiveStream ( result . Value ) ;
138162 }
139163
140164 private StreamKey ? FirstAvailableTrackSid ( TrackKind kind )
@@ -158,6 +182,10 @@ private Weak<AudioStream> FirstAudio()
158182 return null ;
159183 }
160184
185+ /// <summary>
186+ /// Finds the audio track paired to a specific video track from the same participant.
187+ /// Available for future targeted audio scenarios.
188+ /// </summary>
161189 private Weak < AudioStream > FindPairedAudio ( string identity , string videoSid )
162190 {
163191 lock ( room . Participants )
@@ -197,28 +225,33 @@ private Weak<AudioStream> FindPairedAudio(string identity, string videoSid)
197225 return Weak < AudioStream > . Null ;
198226 }
199227
200- public void CloseCurrentStream ( )
228+ private void ReleaseAllAudioSources ( )
201229 {
202- // doesn't need to dispose the stream, because it's responsibility of the owning room
203- currentStream = null ;
204- playerState = PlayerState . STOPPED ;
205-
206- //audioSource is never null during regular execution, but the check is required as when closing the game in a scene
207- //with a running livekit stream, the audioSource (being a monobehaviour) might be already destroyed when disposing the player
208- if ( audioSource != null )
230+ foreach ( var ( source , _) in audioSources )
209231 {
210- audioSource . Stop ( ) ;
211- audioSource . Free ( ) ;
232+ // Source might already be destroyed when closing the game with a running livekit stream.
233+ if ( source != null )
234+ OBJECT_POOL . Release ( source ) ;
212235 }
236+
237+ audioSources . Clear ( ) ;
238+ }
239+
240+ public void CloseCurrentStream ( )
241+ {
242+ // Doesn't need to dispose the stream, because it's responsibility of the owning room.
243+ currentVideoStream = null ;
244+ playerState = PlayerState . STOPPED ;
245+ ReleaseAllAudioSources ( ) ;
213246 }
214247
215248 public Texture ? LastTexture ( )
216249 {
217250 if ( playerState is not PlayerState . PLAYING )
218251 return null ;
219252
220- return currentStream ? . video . Resource . Has ?? false
221- ? currentStream ? . video . Resource . Value . DecodeLastFrame ( )
253+ return currentVideoStream != null && currentVideoStream . Value . Resource . Has
254+ ? currentVideoStream . Value . Resource . Value . DecodeLastFrame ( )
222255 : null ;
223256 }
224257
@@ -231,35 +264,40 @@ public void Dispose()
231264 }
232265
233266 disposed = true ;
234-
235267 CloseCurrentStream ( ) ;
236- OBJECT_POOL . Release ( audioSource ) ;
237268 }
238269
239270 public void Play ( )
240271 {
241272 playerState = PlayerState . PLAYING ;
242- audioSource . Play ( ) ;
273+
274+ foreach ( var ( source , _) in audioSources )
275+ source . Play ( ) ;
243276 }
244277
245278 public void Pause ( )
246279 {
247280 playerState = PlayerState . PAUSED ;
248281
249- //it's actually no "pause" for a streaming source
250- audioSource . Stop ( ) ;
282+ // There is no "pause" for a streaming source.
283+ foreach ( var ( source , _) in audioSources )
284+ source . Stop ( ) ;
251285 }
252286
253287 public void Stop ( )
254288 {
255289 playerState = PlayerState . STOPPED ;
256- audioSource . Stop ( ) ;
290+
291+ foreach ( var ( source , _) in audioSources )
292+ source . Stop ( ) ;
257293 }
258294
259295 public void SetVolume ( float target )
260296 {
261297 Volume = target ;
262- audioSource . SetVolume ( target ) ;
298+
299+ foreach ( var ( source , _) in audioSources )
300+ source . SetVolume ( target ) ;
263301 }
264302
265303 public void CrossfadeVolume ( float targetVolume , float volumeDelta )
@@ -271,18 +309,22 @@ public void CrossfadeVolume(float targetVolume, float volumeDelta)
271309
272310 public void PlaceAudioAt ( Vector3 position )
273311 {
274- audioSource . transform . position = position ;
312+ audioPosition = position ;
313+
314+ foreach ( var ( source , _) in audioSources )
315+ source . transform . position = position ;
275316 }
276317
277318 /// <summary>
278- /// MUST be used in place, caller doesn't take ownership of the referene.
319+ /// MUST be used in place, caller doesn't take ownership of the reference.
320+ /// Returns the first available audio source for audio visualization purposes.
279321 /// </summary>
280322 public AudioSource ? ExposedAudioSource ( )
281323 {
282- // Could be cached in LivekitAudioSource in future
283- // Strongly NOT RECOMMENDED to cache it here (LivekitPlayer.cs)
284- // to avoid implementation coupling and possiblity of caching bugs
285- return audioSource . gameObject . GetComponent < AudioSource > ( ) ;
324+ if ( audioSources . Count == 0 )
325+ return null ;
326+
327+ return audioSources [ 0 ] . source . gameObject . GetComponent < AudioSource > ( ) ;
286328 }
287329 }
288330}
0 commit comments