Integrate video

Note: This tutorial comes after the Create a basic audio conference application. It is highly recommended to follow this previous tutorial before going through this one.

This tutorial guides how to add video features to a conference.

Start the video

Layout editing

Open the index.html file and create a video-container div in it in which the video stream will be dynamically added. Add also a new element named actions that will contain a start-video-btn button element:

<div id="app">
    <div id="form">...</div>
    <div id="actions">
        <button id="start-video-btn" disabled>Start video</button>
    </div>
    <div id="video-container"></div>
</div>

Interface linking

Open the ui.js file and declare the startVideoBtn in the initUI function:

const initUI = () => {
    ...
    const startVideoBtn = document.getElementById('start-video-btn');
    ...
};

Edit the joinButton onclick function to enable the startVideoBtn when the user joins the conference using the onclick function:

joinButton.onclick = () => {
... => VoxeetSDK.conference.join(conference.id))
    .then(() => {
        // ----->  Enable the button here ------>
        startVideoBtn.disabled = false;
    });
...
};

Then, write the startVideoBtn onclick handler:

startVideoBtn.onclick = () => {
    VoxeetSDK.conference.startVideo(VoxeetSDK.session.participant)
        .then(() => {
            startVideoBtn.disabled = true;
        });
};

Logic implementation

Open the ui.js file and write the addVideoNode that will create dynamically a video node element in the html DOM:

/*
addVideoNode(userId, stream): void
create a video node element on the video-container <div> for a participant with userId
*/
const addVideoNode = (participant, stream) => {
    const videoContainer = document.getElementById('video-container');
    let videoNode = document.getElementById('video-' + participant.id);

    if(!videoNode) {
        videoNode = document.createElement('video');
        
        videoNode.setAttribute('id', 'video-' + participant.id);
        videoNode.setAttribute('height', 240);
        videoNode.setAttribute('width', 320);
        
        videoContainer.appendChild(videoNode);
        
        videoNode.autoplay = 'autoplay';
        videoNode.muted = true;
    }

    navigator.attachMediaStream(videoNode, stream);
};

When a participant joins a conference, an event named streamAdded is emitted to all other participants. Note: In this tutorial, the user joins a conference without a video. Thus, the videoNode will be created but it will not display the video until the user starts it with the startVideo method.

Open the client.js file and call addVideoNode upon receiving the streamAdded event:

const main = () => {
    /* Events handlers */
    VoxeetSDK.conference.on('streamAdded', (participant, stream) => {
        addVideoNode(participant, stream);
    });
};

Implement video in the ViewController.swift:

Layout modification

class ViewController: UIViewController {
    ...

    // Conference UI.
    ...
    var startVideoButton: UIButton!
    
    // Videos views.
    var videosView1: VTVideoView!
    var videosView2: VTVideoView!
    
    ...
    
    override func viewDidLoad() {
        ...
        
        // Conference delegate.
        VoxeetSDK.shared.conference.delegate = self
    }
    
    ...
}

Interface linking

...

func initConferenceUI() {
    ...

    // Start video button.
    startVideoButton = UIButton(type: .system) as UIButton
    startVideoButton.frame = CGRect(x: 100, y: leaveButton.frame.origin.y + leaveButton.frame.height + 16, width: 100, height: 30)
    startVideoButton.isEnabled = false
    startVideoButton.isSelected = true
    startVideoButton.setTitle("START VIDEO", for: .normal)
    startVideoButton.addTarget(self, action: #selector(startVideoButtonAction), for: .touchUpInside)
    self.view.addSubview(startVideoButton)
        
    // Video views.
    videosView1 = VTVideoView(frame: CGRect(x: 108, y: startVideoButton.frame.origin.y + startVideoButton.frame.height + 16, width: 84, height: 84))
    videosView1.backgroundColor = .black
    self.view.addSubview(videosView1)
    videosView2 = VTVideoView(frame: CGRect(x: 208, y: startVideoButton.frame.origin.y + startVideoButton.frame.height + 16, width: 84, height: 84))
    videosView2.backgroundColor = .black
    self.view.addSubview(videosView2)
}

...

@objc func startButtonAction(sender: UIButton!) {
    ...
        // Join the conference with its id.
        VoxeetSDK.shared.conference.join(conference: conference, success: { response in
        
            ...
            self.startVideoButton.isEnabled = true /* Update startVideo button state */
            
        }, fail: { error in })
    }, fail: { error in })
}

@objc func leaveButtonAction(sender: UIButton!) {
    VoxeetSDK.shared.conference.leave { error in
    
        ...
        self.startVideoButton.isEnabled = false /* Update startVideo button state */
        
    }
}

...

Logic implementation

    ...

    @objc func startVideoButtonAction(sender: UIButton!) {
        VoxeetSDK.shared.conference.startVideo { error in
            if error == nil {
                self.startVideoButton.isEnabled = false
            }
        }
    }
}

extension ViewController: VTConferenceDelegate {        
    func streamAdded(participant: VTParticipant, stream: MediaStream) {
        streamUpdated(participant: participant, stream: stream)
    }
    
    func streamUpdated(participant: VTParticipant, stream: MediaStream) {
        switch stream.type {
        case .Camera:
            if participant.id == VoxeetSDK.shared.session.participant?.id {
                if !stream.videoTracks.isEmpty {
                    videosView1.attach(participant: participant, stream: stream)
                } else {
                    videosView1.unattach() /* Optional */
                }
            } else {
                if !stream.videoTracks.isEmpty {
                    videosView2.attach(participant: participant, stream: stream)
                } else {
                    videosView2.unattach() /* Optional */
                }
            }
        case .ScreenShare: break
        default: break
        }
    }
    
    func streamRemoved(participant: VTParticipant, stream: MediaStream) {}
    func statusUpdated(status: VTConferenceStatus) {}
}

Layout modification

Edit the main_activity.xml with the following items :

<LinearLayout ...>
    ...

    <!-- Step 4. Please put below the layout modification with the video step -->

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/startVideo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="start video" />

        <!-- Step 5. Below will be put the block concerning the stop video functionnality -->

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <com.voxeet.sdk.views.VideoView
                android:id="@+id/video"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1">

            <com.voxeet.sdk.views.VideoView
                android:id="@+id/videoOther"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

Interface linking

onCreate modification for MainActivity

@Bind(R.id.video)
protected VideoView video;

@Bind(R.id.videoOther)
protected VideoView videoOther;

Methods for MainActivity

@OnClick(R.id.startVideo)
public void onStartVideo() {

}

private void updateStreams() {
    for (User user : VoxeetSDK.conference().getUsers()) {
        boolean isLocal = user.getId().equals(VoxeetSDK.session().getUserId());
        MediaStream stream = user.streamsHandler().getFirst(MediaStreamType.Camera);

        VideoView video = isLocal ? this.video : this.videoOther;

        if (null != stream && !stream.videoTracks().isEmpty()) {
            video.setVisibility(View.VISIBLE);
            video.attach(user.getId(), stream);
        }
    }
}

Logic implementation

Implementation for onStartVideo

public void onStartVideo() {
    VoxeetSDK.conference().startVideo()
            .then((result, solver) -> updateViews())
            .error(error());
}

Implementation for the related events

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(StreamAddedEvent event) {
    updateStreams();
}

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(StreamUpdatedEvent event) {
    updateStreams();
}

Stop the video

Layout editing

Open the index.html file and add a new button named stop-video-btn in the actions div element:

<div id="actions">
    <button id="start-video-btn" disabled>Start video</button>
    <!-- add 'stop-video-btn' just after 'start-video-btn'-->
    <button id="stop-video-btn" disabled>Stop video</button>
</div>

Interface linking

Open the ui.js file and declare the stopVideoBtn in the initUI function:

const initUI = () => {
    ...
    const stopVideoBtn = document.getElementById('stop-video-btn');
    ...
}

Edit the startVideoBtn onclick function to enable the stopVideoBtn when the user's video is started:

    startVideoBtn.onclick = () => {
        VoxeetSDK.conference.startVideo(VoxeetSDK.session.participant)
            .then(() => {
                // ------> Enable the button here ------->
                stopVideoBtn.disabled = false;
            });
    };

Then, write the onclick handler of the startVideoBtn:

    stopVideoBtn.onclick = () => {
        VoxeetSDK.conference.stopVideo(VoxeetSDK.session.participant)
            .then(() => {
                stopVideoBtn.disabled = true;
                startVideoBtn.disabled = false;
            });
    };

Logic implementation

Open the ui.js file and write the removeVideoNode function that removes the specific video element from the UI:

const removeVideoNode = (participant) => {
    let videoNode = document.getElementById('video-' + participant.id);

    if (videoNode) {
        videoNode.parentNode.removeChild(videoNode);
    }
};

When a participant leaves the conference, an event named streamRemoved is emitted to all other participants.

Open client.js file and call removeVideoNode upon receiving the streamRemoved event:

const main = () => {
    /* Events handlers */
    ...
    VoxeetSDK.conference.on('streamRemoved', (participant) => {
        removeVideoNode(participant);
    });
};

Layout modification

class ViewController: UIViewController {
    ...
    
    // Conference UI.
    ...
    var stopVideoButton: UIButton!
    
    ...
}

Interface linking

...

func initConferenceUI() {
    ...
    
    // Stop video button.
    stopVideoButton = UIButton(type: .system) as UIButton
    stopVideoButton.frame = CGRect(x: 200, y: leaveButton.frame.origin.y + leaveButton.frame.height + 16, width: 100, height: 30)
    stopVideoButton.isEnabled = false
    stopVideoButton.isSelected = true
    stopVideoButton.setTitle("STOP VIDEO", for: .normal)
    stopVideoButton.addTarget(self, action: #selector(stopVideoButtonAction), for: .touchUpInside)
    self.view.addSubview(stopVideoButton)
    
    // Video views.
    ...
}

...

@objc func leaveButtonAction(sender: UIButton!) {
    VoxeetSDK.shared.conference.leave { error in
    
        ...
        self.stopVideoButton.isEnabled = false /* Update stopVideo button state */
        
    }
}

@objc func startVideoButtonAction(sender: UIButton!) {
    VoxeetSDK.shared.conference.startVideo { error in
        if error == nil {
        
            ...
            self.stopVideoButton.isEnabled = true /* Update stopVideo button state */
            
        }
    }
}

...

Logic implementation

    ...

    @objc func stopVideoButtonAction(sender: UIButton!) {
        VoxeetSDK.shared.conference.stopVideo { error in
            if error == nil {
                self.startVideoButton.isEnabled = true
                self.stopVideoButton.isEnabled = false
            }
        }
    }
}

Layout modification

Edit the main_activity.xml with the following <Button /> after the comment mentionning the stop video functionnality :

<LinearLayout ...>
    ...

    <LinearLayout ...>

        <!-- Step 5. Below will be put the block concerning the stop video functionnality -->

        <Button
            android:id="@+id/stopVideo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="stop video" />

    </LinearLayout>
    ...
</LinearLayout>

Interface linking

Implementation for onStopVideo

@OnClick(R.id.stopVideo)
public void onStopVideo() {

}

Logic implementation

Implementation for onStopVideo

@OnClick(R.id.stopVideo)
public void onStopVideo() {
    VoxeetSDK.conference().stopVideo()
            .then((result, solver) -> updateViews())
            .error(error());
}

Implementation for the related events

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(StreamRemovedEvent event) {
    updateStreams();
}

What's next?

If you want to learn more about creating conference applications, go to the Manage participants tutorial.