Skip to content

Commit 390bb2e

Browse files
committed
feat: play all audio tracks from livekit room in media player
Instead of pairing one audio track to the selected video track, LivekitPlayer now plays every audio track in the room — so all participants are heard regardless of which video is displayed.
1 parent dd905a9 commit 390bb2e

File tree

1 file changed

+112
-70
lines changed

1 file changed

+112
-70
lines changed

Explorer/Assets/DCL/SDKComponents/MediaStream/LivekitPlayer.cs

Lines changed: 112 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using LiveKit.Rooms.VideoStreaming;
99
using RichTypes;
1010
using System;
11+
using System.Collections.Generic;
1112
using UnityEngine;
1213
using 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

Comments
 (0)