Follow this step-by-step tutorial to implement the Watch Together video chat sample application.
While the client-side application will take care of most of the functionality, in order to make this sample application work, you will need to get an access token from the Cluster Authentication Server (CAS).
To better understand the Watch Together architecture have a look at this guide - Watch Together overview
An Access Token is needed in order to allow a client to connect to a Session.
Note: It is important that the client application does not request an Access Token directly from the backend. By doing that you risk exposing the API_TOKEN and API_SECRET.
You can get your API_KEY and API_SECRET in your private area, here.
Note: Every Streaming Token corresponds to one specific Session only. To allow two different clients to connect to the same Session, the clients need to use the same Access Token.
Going to production
To go to production you will need to implement your own authentication server. Using the server the Access Token will be shared to your various clients (Web, Android, and iOS). With this valid Access Token you will be able to use the service.
For that you will need:
API_KEY, and API_SECRET - can be retrieved in your private area once you login
If you are using android.enableJetifier=true to automatically convert third-party libraries to use AndroidX, please add this line android.jetifier.blacklist=wsdk_v2.0.4.aar to your gradle.properties file.
If this line is not there the WT library will not compile
# AndroidX package structure to make it clearer which packages are bundled with the# Android operating system, and which are packaged with your app"s APK# https://developer.android.com/topic/libraries/support-library/androidx-rnandroid.useAndroidX=true# Automatically convert third-party libraries to use AndroidXandroid.enableJetifier=trueandroid.jetifier.blacklist=sdk-release.aar
Adding dependencies
Modify the build.gradle file (in the app folder) with SDK dependencies, once modified it should look as follows:
If the Sync and Rebuild was executed successfully, the Watch Together classes and functions should be available in the project
Granting access to camera and microphone
Using the device Audio and Video requires permissions to be granted from the user and some code that will allow the application to work well under different scenarios.
Add the following permissions to the AndroidManifest.xml file under the app/src/main folder in the project above the application tag.
The permissions should be validated before every connect of a client to a session.
Sample application UI
The sample application has a UI element that needs to be handled to allow the proper presentation of the streams and stream properties.
Use a view container in the application’s resource layout file to display videos. The sample application demonstrates how to manage videos with RecyclerView container
For rendering videos, the SDK provides the VideoRenderer class for use in the application’s layout
Initialize Session object with SessionBuilder class
// Host class of Session object should implement or have interface SessionListenermSession =new Session.SessionBuilder(this) // this - SessionListener object .setReconnectListener(this) // this - ReconnectListener object.setConnectionListener(this) // this - ConnectionListener object.build(this); // this - Android contextmSession.setDisplayName(mDisplayName); // optional mSession.setEnableStats(isEnableStats); // isEnableStats - enable stats received
// Host class of Session object should implement or have interface SessionListenermSession = Session.SessionBuilder(this) // this - SessionListener object .setReconnectListener(this) // this - ReconnectListener object .setConnectionListener(this) // this - ConnectionListener object .build(this) // this - Android contextmSession?.setDisplayName(mDisplayName) // optional mSession?.isEnableStats = isEnableStats // isEnableStats - enable stats received
Start camera preview - can be called without the Session’s url and the Access Token
mSession.startCameraPreview();
mSession?.startCameraPreview()
Connecting to the Session using the Session object requires a Session’s URL and a valid Streaming Token to be available before connecting. The connect() function establishes a connection to the Session with the audio and video tracks inside MediaStream object
// String parameter sessionId should be passed in the function connect.mSession.connect(mToken); // mToken - authorization token
// String parameter sessionId should be passed in the function connect.mSession?.connect(mToken) // mToken - authorization token
It is possible connect as different types of participants:
FULL_PARTICIPANT - publishes Audio and Video to the session and Subscribes to all other participants Audio and Video in the Session
VIEWER - Subscribes to all other participants Audio and Video in the Session
AV_BROADCASTER - Publishes Audio and Video to the Session
A_BROADCASTER - Publishes only Audio to the Session
In the case you would like to connect as a Viewer do the following:
// String parameter sessionId should be passed in the function connectAsViewer.mSession.connect(mToken, mParticipantType); // mParticipantType - participant's connection type
// String parameter sessionId should be passed in the function connectAsViewer.mSession?.connect(mToken, mParticipantType) // mParticipantType - participant's connection type
Managing Session logic
To manage the session's logic we provided several callbacks that will allow you to customize the interactions as you need.
The SessionListener interface, which the MainActivity implements, will allow you to control the flow of logic of the Session you are managing
publicclassMainActivityextendsAppCompatActivityimplementsSessionListener {... @OverridepublicvoidonConnected(@NonNullList<?extendsParticipant> list) {// client has been connected to the session with the unique sessionId and participants already in the Session } @OverridepublicvoidonDisconnected() { // client has been disconnected from the session. All connections will be closed.// clear ui and data finish()// Close MainActivity page } @OverridepublicvoidonError(SessionError error) {// You can implement error handling here. Check API rferences for possible errors. } @OverridepublicvoidonLocalParticipantJoined(@NullableParticipant participant) {// Camera preview started, local stream created, update UI if (mParticipantAdapter !=null) {mParticipantAdapter.addLocalParticipant(participant); } } @OverridepublicvoidonRemoteParticipantJoined(@NullableParticipant participant) {// remote participant joined to the session, update ui.// create addParticipant method in the ParticipantsAdapter as in the sample codeif (mParticipantAdapter !=null) {mParticipantAdapter.addRemoteParticipant(participant); } } @OverridepublicvoidonUpdateParticipant(@NullableString participantId, @NullableParticipant participant) {// participant update the session, update uiif (mParticipantAdapter !=null) {mParticipantAdapter.updateParticipant(participantId, participant); } } @OverridepublicvoidonRemoteParticipantLeft(@NullableString participantId) {// remote participant left the session, update uiif (mParticipantAdapter !=null) {mParticipantAdapter.removeParticipant(participantId); } } @OverridepublicvoidonParticipantMediaStateChanged(@NullableString participantId,MediaType mediaType,MediaState mediaState) {// Method is triggered when remote participant disabled/enabled (mediastate) it's audio/video (mediaType).// On UI it can be reflected with this callbackif (mParticipantAdapter !=null) {mParticipantAdapter.updateParticipantMedia(participantId, mediaType, mediaState); } } @OverridepublicvoidonMessageReceived(@NullableString participantId, @NullableString message) {// The method is receiving messages from remote participants into the current roomToast.makeText(this,"participant=$participant, message=$message",Toast.LENGTH_LONG).show() } @OverrideprotectedvoidonPause() {// Disconnect from the sessionmSession.disconnect(); } @OverridepublicvoidonBackPressed() {// Disconnect from the sessionmSession.disconnect(); } @OverrideprotectedvoidonDestroy() {// Clear participants and publishersmParticipantAdapter.clearParticipants(); super.onDestroy(); }}
classMainActivity : AppCompatActivity(), SessionListener {...overridefunonConnected(participants: List<Participant>) {// client has been connected to the session with the unique sessionId and publishers already in the Session }overridefunonDisconnected() {// client has been disconnected from the session. All connections will be closed.// clear ui and data finish() // Close MainActivity page }overridefunonError(error: SessionError) {// You can implement error handling here. Check API rferences for possible errors. }overridefunonLocalParticipantJoined(participant: Participant) {// Camera preview started, local stream created, update UI mParticipantAdapter?.addLocalParticipant(participant) }overridefunonRemoteParticipantJoined(participant: Participant) {// remote participant joined to the session, update ui.// create addParticipant method in the ParticipantsAdapter as in the sample code mParticipantAdapter?.addRemoteParticipant(participant) }overridefunonUpdateParticipant(participantId: String, participant: Participant) {// participant update the session, update ui mParticipantAdapter?.updateParticipant(participantId, participant) }overridefunonRemoteParticipantLeft(participantId: String) {// remote participant left the session, update ui mParticipantAdapter?.removeParticipant(participantId) }overridefunonParticipantMediaStateChanged( participantId: String, mediaType: MediaConfiguration.MediaType?, mediaState: MediaConfiguration.MediaState? ) {// Method is triggered when remote participant disabled/enabled (mediastate) it's audio/video (mediaType).// On UI it can be reflected with this callback mParticipantAdapter?.updateParticipantMedia(participantId, mediaType, mediaState) }overridefunonMessageReceived(participantId: String, message: String) {// The method is receiving messages from remote participants into the current room Toast.makeText(applicationContext, "participant=$participant, message=$message", Toast.LENGTH_LONG).show() }overridefunonPause() {// Disconnect from the session mSession?.disconnect() }overridefunonBackPressed() {// Disconnect from the session mSession?.disconnect() }overridefunonDestroy() {// Clear participants and publishers mParticipantAdapter?.clearParticipants()super.onDestroy() }}
Managing Reconnection logic
To manage the reconnect session's logic we provide several callbacks and will allow you to customize the interactions you need.
The SessionReconnectListener interface, which the MainActivity implements in the sample application, will allow you to control the flow of logic of the Reconnect you are managing
publicclassMainActivityextendsAppCompatActivityimplementsSessionReconnectListener {... @OverridepublicvoidonParticipantReconnecting(finalString participantId) {// callback which triggers when remote participant loses network // participantId - participant's identificator for updatingmParticipantAdapter.progressConnection(participantId,true); } @OverridepublicvoidonParticipantReconnected(String participantId,String oldParticipantId) { // callback with triggers when remote participant network resumes// oldParticipantId - identificator old participant for updating// participantId - participant's identificator for updatingmParticipantAdapter.progressConnection(oldParticipantId,false); }}
classMainActivity : AppCompatActivity(), SessionReconnectListener {...overridefunonParticipantReconnecting( participantId: String ) {// callback which triggers when remote participant loses network // participantId - participant's identificator for updating mParticipantAdapter?.progressConnection(participantId, true) }overridefunonParticipantReconnected( participantId: String, oldParticipantId: String ) {// callback with triggers when remote participant network resumes// oldPublisherId - identificator old participant for updating// participantId - participant's identificator for updating mParticipantAdapter?.progressConnection(oldParticipantId, false) }}
Managing Connection states logic
To manage the connection states session's logic we provided several callbacks and will allow you to customize the interactions you need.
The SessionConnectionListener interface, which the MainActivity implements in the sample application, will allow you to control the flow of logic of the Connection states you are managing
publicclassMainActivityextendsAppCompatActivityimplementsSessionConnectionListener {... @OverridepublicvoidonLocalConnectionLost() {// callback with triggers when local participant connection lost } @OverridepublicvoidonLocalConnectionResumed() {// callback with triggers when local participant connection resumed } @OverridepublicvoidonRemoteConnectionLost(String participantId) {// callback with triggers when remote participant connection lost// participantId - identificator that participant by id losing connectionif (mParticipantAdapter !=null) {mParticipantAdapter.remoteParticipantConnectionLost(participantId); } }}
classMainActivity : AppCompatActivity(), SessionConnectionListener {...overridefunonLocalConnectionLost() {// callback with triggers when local participant connection lost }overridefunonLocalConnectionResumed() {// callback with triggers when local participant connection resumed }overridefunonRemoteConnectionLost(participantId: String?) {// callback with triggers when remote participant connection lost// participantId - identificator that participant by id losing connection mParticipantAdapter?.remoteParticipantConnectionLost(participantId) }}
Enable/Disable Audio or Video
Bind the Participant view to the Participant data in the ParticipantsAdapter class
@OverridepublicvoidonBindViewHolder(@NonNullfinalViewHolder viewHolder,finalint position) {finalParticipant participant =mParticipants.get(viewHolder.getAdapterPosition());if (participant !=null) {participant.setRenderer(viewHolder.videoRenderer);participant.enableStats();participant.setParticipantStatsListener(newOnParticipantStatsListener() { @OverridepublicvoidonStats(@NullableJSONObject jsonStats, @NotNullParticipant participant) {// The callback with json data about connection quality for audio and videotry {if (jsonStats !=null) {jsonStats.get("audioAvg") // 0 - 4.5 valuejsonStats.get("audioQosMos") // Value of quality {BAD, GOOD, EXCELLENT}jsonStats.get("videoAvg") // 0 - 4.5 valuejsonStats.get("videoQosMos") // Value of quality {BAD, GOOD, EXCELLENT} } } catch (JSONException e) {e.printStackTrace(); } } });viewHolder.mic.setOnClickListener(new View.OnClickListener() { @OverridepublicvoidonClick(finalView v) {if (participant.isAudioEnabled()) {participant.disableAudio(); } else {participant.enableAudio(); }notifyItemChanged(position, participant); } });viewHolder.cam.setOnClickListener(new View.OnClickListener() { @OverridepublicvoidonClick(finalView v) {if (participant.isVideoEnabled()) {participant.disableVideo(); } else {participant.enableVideo(); }notifyItemChanged(position, participant); } });// Update microphone and camera UI icons according to Participant's Audio and Video track stateviewHolder.connectionState.setVisibility(participant.isConnectionLost() ?View.VISIBLE:View.GONE);viewHolder.layoutProgress.setVisibility(participant.isProgressReconnection() ?View.VISIBLE:View.GONE); viewHolder.mic.setBackgroundResource(participant.isAudioEnabled() ? R.drawable.ic_mic_on : R.drawable.ic_mic_off);
viewHolder.cam.setBackgroundResource(participant.isVideoEnabled() ? R.drawable.ic_video_on : R.drawable.ic_video_off);
viewHolder.name.setText(participant.getName()); }}
overridefunonBindViewHolder(viewHolder: ViewHolder, position: Int) {val participant = mParticipants[viewHolder.adapterPosition] participant.setRenderer(viewHolder.videoRenderer) participant.enableStats() participant.setParticipantStatsListener(object : OnParticipantStatsListener {overridefunonStats(jsonStats: JSONObject?, participant: Participant) {// The callback with json data about connection quality for audio and video jsonStats?.get("audioAvg") // 0 - 4.5 value jsonStats?.get("audioQosMos") // Value of quality {BAD, GOOD, EXCELLENT} jsonStats?.get("videoAvg") // 0 - 4.5 value jsonStats?.get("videoQosMos") // Value of quality {BAD, GOOD, EXCELLENT} } }) viewHolder.mic.setOnClickListener {if (participant.isAudioEnabled) { participant.disableAudio() } else { participant.enableAudio() }notifyItemChanged(position, participant) } viewHolder.cam.setOnClickListener {if (participant.isVideoEnabled) { participant.disableVideo() } else { participant.enableVideo() }notifyItemChanged(position, participant) }// Update microphone and camera UI icons according to Participant's Audio and Video track state viewHolder.connectionState.visibility =if (participant.isConnectionLost) View.VISIBLE else View.GONE viewHolder.layoutProgress.visibility =if (participant.isProgressReconnection) View.VISIBLE else View.GONE viewHolder.mic.setBackgroundResource(if (participant.isAudioEnabled) R.drawable.ic_mic_on else R.drawable.ic_mic_off)
viewHolder.cam.setBackgroundResource(if (participant.isVideoEnabled) R.drawable.ic_video_on else R.drawable.ic_video_off)
viewHolder.name.text = participant.name}
Switching camera
During streaming, you can switch from front to back camera by a simple call to the Session’s function switchCamera
Running the application
Once coding is finished, you should be able to run the application in the Android Studio emulator.
You can view the complete Watch Together sample application here