using System;
using StreamVideo.Core;
using StreamVideo.Libs.Auth;
using UnityEngine;
public class VideoClient : MonoBehaviour
{
async void Start()
{
_client = StreamVideoClient.CreateDefaultClient();
try
{
var authCredentials = new AuthCredentials("api-key", "user-id", "user-token");
await _client.ConnectUserAsync(authCredentials);
// After the await ConnectUserAsync task completes, the client is connected
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
private IStreamVideoClient _client;
}Quickstart
This section will give you a quick overview on every integration step. Please refer to Core Concepts sections for more in-depth information and more code examples.
Client setup
- Create the client using
StreamVideoClient.CreateDefaultClient(); - Connect a user to Stream server with
await _client.ConnectUserAsync(authCredentials)
Full example:
Here are credentials to try out the app with:
| Property | Value |
|---|---|
| API Key | undefined |
| Token | undefined |
| User ID | undefined |
| Call ID | undefined |
For testing you can join the call on our web-app: Join Call
Read more on client authorization and obtaining auth tokens in the Client & Authentication section.
Creating and Joining Calls
Most commonly you will either want to join another call or create a new call and join it immediately.
You can use the _client.JoinCallAsync method for both scenarios. If you're joining another call, set the create argument to false. If you're creating a new call for others to join, set the create argument to true.
Besides create, JoinCallAsync takes two required arguments that control whether the call's members are alerted. Pass false for both unless you specifically want that behaviour:
| Argument | Description |
|---|---|
ring | When true, the call members are rung — they receive an incoming-call notification and can answer with AcceptAsync or decline with RejectAsync. Only meaningful when you are creating the call; use false when joining a call that is already running. |
notify | When true, the call members receive a regular (non-ringing) push notification that the call has started. Use this for "a call is happening" alerts that shouldn't make the device ring. |
ring: true sends an incoming-call notification to every member of the call. Keep it false while you're getting started, and only enable it once you have an incoming-call flow that handles AcceptAsync / RejectAsync.
Create and Join the Call
var callType = StreamCallType.Default; // Call type affects default permissions
var callId = "my-call-id";
// Passing create: true will create the call if it doesn't exist
var streamCall = await _client.JoinCallAsync(callType, callId, create: true, ring: false, notify: false);Join another call
var callType = StreamCallType.Default; // Call type affects default permissions
var callId = "my-call-id";
// Passing create: false means the call must already exist, otherwise the join attempt will fail
var streamCall = await _client.JoinCallAsync(callType, callId, create: false, ring: false, notify: false);Capture Audio from a Microphone
The AudioDeviceManager manages all interactions with camera devices. Below are several fundamental operations; for a comprehensive list, please visit our Camera & Microphone documentation section.
List available microphone devices:
var microphones = _client.AudioDeviceManager.EnumerateDevices();
foreach (var mic in microphones)
{
Debug.Log(mic.Name);
}Select active device:
var firstMicrophone = microphones.First();
// Select microphone device to capture audio input. `enable` argument determines whether audio capturing should start
_client.AudioDeviceManager.SelectDevice(firstMicrophone, enable: true);The enable argument determines whether audio capture should start for this device.
You can start/stop audio capturing with:
// Start audio capturing
_client.AudioDeviceManager.Enable();
// Stop audio capturing
_client.AudioDeviceManager.Disable();Android & iOS platforms
On Android and iOS, the user must grant permission to access microphone devices. You can read more about requesting permissions in the Camera & Microphone docs section.
Capture Video from a Web Camera
The VideoDeviceManager manages all interactions with camera devices. Below are several fundamental operations; for a comprehensive list, please visit our Camera & Microphone documentation section.
List available camera devices:
var cameras = _client.VideoDeviceManager.EnumerateDevices();
foreach (var cam in cameras)
{
Debug.Log(cam.Name);
}Select active device:
var firstCamera = cameras.First();
// Select camera device to capture video input. `enable` argument determines whether video capturing should start
_client.VideoDeviceManager.SelectDevice(firstCamera, enable: true);The enable argument determines whether video capture should start immediately for this device.
You can start/stop video capturing with:
// Start video capturing
_client.VideoDeviceManager.Enable();
// Stop video capturing
_client.VideoDeviceManager.Disable();Android & iOS platforms
On Android and iOS, the user must grant permission to access camera devices. You can read more about requesting permissions in the Camera & Microphone docs section.
Handling participants
Once you obtain a reference to a call object of type IStreamCall you can iterate through the current participants and subscribe to events to get notified whenever a participant joins or leaves.
var callType = StreamCallType.Default; // Call type affects default permissions
var callId = "my-call-id";
var streamCall = await _client.JoinCallAsync(callType, callId, create: false, ring: false, notify: false);
// Subscribe to events to get notified that streamCall.Participants collection changed
streamCall.ParticipantJoined += OnParticipantJoined;
streamCall.ParticipantLeft += OnParticipantLeft;
// Iterate through current participants, participant is an object of type IStreamVideoCallParticipant
foreach (var participant in streamCall.Participants)
{
// Handle participant logic. For example: create a view for each participant
}Handling participant tracks
You must subscribe to participant tracks and bind them to Unity components in order to receive audio and video from other participants. Without calling SetAudioSourceTarget on each participant's StreamAudioTrack, you will not hear their audio. Without calling SetRenderTarget on their StreamVideoTrack, you will not see their video. This is the most common integration mistake.
Data streamed by participants is organized in form of "tracks". There are two type of tracks:
StreamAudioTrack- Audio Track, contains audio sent by a participantStreamVideoTrack- Video Track, contains video image sent by a participant
Each participant has its own tracks that can change during the call — for example, a participant may turn on/off their camera or microphone. Subscribe to participant.TrackAdded and it will be called for every track, including the ones participants were already publishing before you joined.
Incoming tracks are driven by SFU subscriptions, which are requested after you join. Tracks published before you joined therefore arrive over the same path as any other track and are delivered through TrackAdded as well. Don't also iterate participant.GetTracks() to bind them — you would handle every track twice.
If a participant turns off their camera or microphone during the call, the track you've already received will be paused. Once re-enabled, the track resumes automatically. Note that TrackAdded may be called multiple times for the same participant — for example, if the network connection drops and the SDK reconnects under the hood, new track instances will be provided. Your handler should account for this by updating the existing bindings rather than assuming tracks are received only once.
By default, the SDK automatically subscribes to video tracks for up to 5 participants. Beyond this limit, incoming video is not requested automatically. You must manually enable video for additional participants using participant.SetIncomingVideoEnabled(true). Audio is always auto-subscribed with no limit. See Video Optimization for details.
Here's an example of how you could process participant tracks:
var callType = StreamCallType.Default; // Call type affects default permissions
var callId = "my-call-id";
// JoinCall to get a IStreamCall object. You can also call _client.GetOrCreateCallAsync or _client.GetCallAsync
var streamCall = await _client.JoinCallAsync(callType, callId, create: false, ring: false, notify: false);
// Subscribe for participants change
streamCall.ParticipantJoined += OnParticipantJoined;
streamCall.ParticipantLeft += OnParticipantLeft;
// Process current participants
foreach (var participant in streamCall.Participants)
{
// Subscribe to TrackAdded - it will be called for every track, including
// tracks that were already being published before you joined the call
participant.TrackAdded += OnParticipantTrackAdded;
}And here's the corresponding OnParticipantTrackAdded method:
private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IStreamTrack track)
{
switch (track)
{
case StreamAudioTrack streamAudioTrack:
// This assumes that this gameObject contains the AudioSource component but it's not a requirement. You can obtain the AudioSource reference in your preferred way
var audioSource = GetComponent<AudioSource>();
// This AudioSource will receive audio from the participant
streamAudioTrack.SetAudioSourceTarget(audioSource);
break;
case StreamVideoTrack streamVideoTrack:
// This assumes that this gameObject contains the RawImage component but it's not a requirement. You can obtain the RawImage reference in your preferred way
var rawImage = GetComponent<RawImage>();
// This RawImage will receive video from the participant
streamVideoTrack.SetRenderTarget(rawImage);
break;
}
}Complete example of handling participants and their tracks
Video Manager
An example of script that creates an instance of IStreamVideoClient, connects a user to the Stream server and exposes a JoinCallAsync method that allows to create and join calls.
Once the call is joined, this script will create a view object for every participant and subscribe to ParticipantJoined and the ParticipantLeft events in order to create or destroy participant view objects.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using StreamVideo.Core;
using StreamVideo.Core.StatefulModels;
using StreamVideo.Libs.Auth;
using UnityEngine;
public class VideoManager : MonoBehaviour
{
public async Task JoinCallAsync(string callId, StreamCallType callType, bool create, bool ring, bool notify)
{
var streamCall = await _client.JoinCallAsync(callType, callId, create, ring, notify);
// Subscribe to events to get notified that streamCall.Participants collection changed
streamCall.ParticipantJoined += OnParticipantJoined;
streamCall.ParticipantLeft += OnParticipantLeft;
// Iterate through current participants
foreach (var participant in streamCall.Participants)
{
// Handle participant logic. For example: create a view for each participant
CreateParticipantView(participant);
}
}
private void OnParticipantLeft(string sessionid, string userid)
{
// Try find view for this participant and destroy it because he left the call
var viewInstance = _participantViews.FirstOrDefault(v => v.SessionId == sessionid);
if (viewInstance != null)
{
// If the participant view was found -> destroy it
Destroy(viewInstance.gameObject);
}
}
private void OnParticipantJoined(IStreamVideoCallParticipant participant)
{
// Create view whenever new participant joins during the call
CreateParticipantView(participant);
}
private void CreateParticipantView(IStreamVideoCallParticipant participant)
{
// Create new prefab instance for the view. In this example we'll add it as a child of this gameObject
var viewInstance = Instantiate(_participantViewPrefab, transform);
// Add to list so we can easily destroy it when a participant leaves the call
_participantViews.Add(viewInstance);
// Call ParticipantView.Init in order to process the participant tracks and subscribe to events
viewInstance.Init(participant);
}
// Start() is called automatically by UnityEngine
protected async void Start()
{
_client = StreamVideoClient.CreateDefaultClient();
try
{
var authCredentials = new AuthCredentials("api-key", "user-id", "user-token");
await _client.ConnectUserAsync(authCredentials);
// After we awaited the ConnectUserAsync the client is connected
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
[SerializeField]
private ParticipantView _participantViewPrefab;
private IStreamVideoClient _client;
private readonly List<ParticipantView> _participantViews = new List<ParticipantView>();
}Participant View
An example of script that you'd attach to a prefab gameObject that you'd spawn and initialize by calling the Init method per each participant in the call.
This script, once initialized with a participant object, subscribes to the TrackAdded event, which is called for every track — including tracks the participant was already publishing before you joined.
The OnParticipantTrackAdded method handles both audio and video tracks and binds them to an AudioSource and the RawImage respectively in order to handle received data.
It also exposes a SessionId property, which VideoManager uses to find the right view to destroy when a participant leaves.
using System;
using StreamVideo.Core.StatefulModels;
using StreamVideo.Core.StatefulModels.Tracks;
using UnityEngine;
using UnityEngine.UI;
public class ParticipantView : MonoBehaviour
{
// This is how we'll identify that this view belongs to a specific participant
public string SessionId => _participant.SessionId;
// Call this method to setup view for a participant
public void Init(IStreamVideoCallParticipant participant)
{
if (_participant != null)
{
Debug.LogError("Participant view already initialized.");
return;
}
_participant = participant ?? throw new ArgumentNullException(nameof(participant));
// Subscribe to TrackAdded - it will be called for every track, including
// tracks that were already being published before you joined the call
_participant.TrackAdded += OnParticipantTrackAdded;
_name.text = _participant.Name;
}
public void OnDestroy()
{
// It's a good practice to unsubscribe from events when the object is destroyed
if (_participant != null)
{
_participant.TrackAdded -= OnParticipantTrackAdded;
}
}
[SerializeField]
private Text _name; // This will show participant name
[SerializeField]
private RawImage _video; // This will show participant video
[SerializeField]
private AudioSource _audioSource; // This will play participant audio
private IStreamVideoCallParticipant _participant;
private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IStreamTrack track)
{
switch (track)
{
case StreamAudioTrack streamAudioTrack:
// Set AudioSource component to be the target of the audio track
streamAudioTrack.SetAudioSourceTarget(_audioSource);
break;
case StreamVideoTrack streamVideoTrack:
// Set RawImage component to be the target of the video track
streamVideoTrack.SetRenderTarget(_video);
break;
}
}
}You can study the Sample Project in order to get the full image of how you could approach the integration process. See Example Project for how to import it into your own project.