Integrate video

Overview

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

This little tutorial guides you through adding video features to a conference.

Start the video

Layout editing

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

<div id="app">
    <div id="form">...</div>
    <div id="video-container"></div>
    <div id="actions">
        <button id="start-video-btn" disabled="true">Start video</button>
    </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 and enable the startVideoBtn when the user has joined the conference in the onclick function :

    joinButton.onclick = () => {
    ...
        voxeet.createConference({ alias: conferenceAliasValue })
            .then((res) => voxeet.joinConference(res.conferenceId))
            .then(() => {
                // ----->  Enable the button here ------>
                startVideoBtn.disabled = false;
            });
    ...
    };

Then write the onclick handler of the startVideoBtn :

    startVideoBtn.onclick = () => {
        voxeet.startVideoForUser(voxeet.userId)
            .then(() => {
                startVideoBtn.disabled = true;
            });
    };

Logic implementation

Open the client.js file. When a user starts its video, it triggers a participantJoined event. Let's call a function named addVideoNode that will create dynamically a video node element in the html DOM upon receiving this event :

const main = () => {
    /* Events handlers */
    voxeet.on('participantJoined', (userId, stream) => {
        addVideoNode(userId, stream);
    });
};

Then open the ui.js and write the addVideoNode function :

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

    var node = document.createElement('video');
    node.setAttribute('id', 'video-' + userId);
    node.setAttribute('width', 320);
    node.setAttribute('height', 240);

    videoContainer.appendChild(node);
    navigator.attachMediaStream(node, stream);

    node.autoplay = 'autoplay';
    node.muted = true;
};

This function creates a new node element with id = video-userId as a child of the video-container and attaches to it the participant's video stream.

Let's implement video in 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(conferenceID: conferenceID, 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 participantJoined(userID: String, stream: MediaStream) {
        participantUpdated(userID: userID, stream: stream)
    }
    
    func participantUpdated(userID: String, stream: MediaStream) {
        if userID == VoxeetSDK.shared.session.user?.id {
            if stream.videoTracks.isEmpty == false {
                videosView1.attach(userID: userID, stream: stream)
            } else {
                videosView1.unattach() /* Optional */
            }
        } else {
            if stream.videoTracks.isEmpty == false {
                videosView2.attach(userID: userID, stream: stream)
            } else {
                videosView2.unattach() /* Optional */
            }
        }
    }
    
    func participantLeft(userID: String) {}
    func screenShareStarted(userID: String, stream: MediaStream) {}
    func screenShareStopped(userID: String) {}
    func messageReceived(userID: String, message: String) {}
}

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="true">Start video</button>
    <!-- add 'stop-video-btn' just after 'start-video-btn'-->
    <button id="stop-video-btn" disabled="true">Stop video</button>
</div>

Interface linking

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

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

Enable the stopVideoBtn when the user's video has been started. To do so, edit the startVideoBtn onclick function :

    startVideoBtn.onclick = () => {
        voxeet.startVideoForUser(voxeet.userId)
            .then(() => {
                // ------> Enable the button here ------->
                stopVideoBtn.disabled = false;
            });
    };

Then write the onclick handler of the startVideoBtn :

    stopVideoBtn.onclick = () => {
        voxeet.stopVideoForUser(voxeet.userId)
            .then(() => {
                stopVideoBtn.disabled = true;
                startVideoBtn.disabled = false;
            });
    };

Logic implementation

When a user stops its video, it triggers a participantLeft event. Open the client.js file and let's call a function named removeVideoNode that will remove the respective user's video stream from the UI upon receiving this event :

const main = () => {
    /* Events handlers */
    ...
    voxeet.on('participantLeft', (userId) => {
        removeVideoNode(userId);
    });
};

Open the ui.js and write the removeVideoNode function :

/*
removeVideoNode(userId): void
remove the video node element of a participant with its userId
*/
const removeVideoNode = (userId) => {
    var node = document.getElementById('video-' + userId);

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

This function removes a video node element from a specific 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 startButtonAction(sender: UIButton!) {
    ...
        // Join the conference with its id.
        VoxeetSDK.shared.conference.join(conferenceID: conferenceID, success: { response in
            
            ...
            self.stopVideoButton.isEnabled = false /* Update startVideo button state */
            
        }, fail: { error in })
    }, fail: { error in })
}

@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?

You can now add video to your conference. If you want to learn more about creating video conference applications, move to the next step => Manage participants