2020 API AWARDS WINNER - BEST IN MEDIA APIs LEARN MORE

Integrate Video

This tutorial is a part of a series; it comes after Create a basic audio conference application.

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

Start the video

  • JavaScript
  • Swift
  • Java

Layout editing

Open the index.html file and create a div with the id video-container, to which the video stream will be dynamically added. Also add a new element with the id actions that will contain a button element with the id start-video-btn:

<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:

joinButton.onclick = () => {
... => VoxeetSDK.conference.join(conference, {}))
    .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 function that will dynamically create 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)
        videoNode.setAttribute("playsinline", true)

        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.

Note: The playsinline attribute used in the example prevents the video from freezing in the Safari browser.

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)
    })
}

Add UI for starting video in ViewController.swift.

1. In ViewController.swift, add some variables to the ViewController class to refer to the user interface elements that will be created in step 2. Set the ViewContoller instance as the conference delegate. The delegate will be called when video streams are added to the conference.

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
    }

    ...
}

2. Modify initConferenceUI to extend the user interface and enable and disable the startVideoButton as appropriate.

...

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 */

    }
}

...

3. Add a method to start video.

...

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

4. Extend ViewController as a VTConferenceDelegate. This allows the controller to act when video streams are added or removed from the conference.

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 3. Put the layout changes for the video step here -->

    <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 3.2. This layout will be upgraded in the stop video step -->

    </LinearLayout>

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

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

    <!-- Step 4. ...

</LinearLayout>

2. Modify the interface linking in the MainActivity class in MainActivity.java:

  • New fields for MainActivity:
@Bind(R.id.video)
protected VideoView video;

@Bind(R.id.videoOther)
protected VideoView videoOther;
  • New methods for MainActivity:
@OnClick(R.id.startVideo)
public void onStartVideo() {

}

private void updateStreams() {

}
  • Add the following code to the onCreate method:
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    //adding the startVideo in the flow
    add(views, R.id.startVideo);
    add(buttonsInConference, R.id.startVideo);
    add(buttonsNotInOwnVideo, R.id.startVideo);
}

3. Add the following logic to the application:

  • In updateViews enable and disable buttons based on the own video state.
private void updateViews() {
    ...

    if (null != current) {
        if (current.isOwnVideoStarted()) {
            setEnabled(buttonsInOwnVideo, true);
            setEnabled(buttonsNotInOwnVideo, false);
        } else {
            setEnabled(buttonsInOwnVideo, false);
            setEnabled(buttonsNotInOwnVideo, true);
        }
    }
}
  • Use the following implementation for onStartVideo:
public void onStartVideo() {
    VoxeetSDK.conference().startVideo()
            .then((result, solver) -> updateViews())
            .error(error());
}
  • Use the following implementation for updateStreams:
private void updateStreams() {
    for (Participant user : VoxeetSDK.conference().getParticipants()) {
        boolean isLocal = user.getId().equals(VoxeetSDK.session().getParticipantId());
        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);
        }
    }
}
  • Add handlers for StreamAddedEvent and StreamUpdatedEvent.
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(StreamAddedEvent event) {
    updateStreams();
    updateViews();
}

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

Stop the video

  • JavaScript
  • Swift
  • Java

Layout editing

Open the index.html file and add a new button with the id 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 stopVideoBtn:

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, stream) => {
        removeVideoNode(participant)
    })
}

Add UI for stopping video in ViewController.swift.

1. In ViewController.swift, add a variable to the ViewController class to refer to the user interface element that will be created in step 2.

class ViewController: UIViewController {
    ...

    // Conference UI.
    ...
    var stopVideoButton: UIButton!

    ...
}

2. Modify initConferenceUI to extend the user interface and enable & disable the stopVideoButton as appropriate.

...

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 */

        }
    }
}

...

3. Add a method to stop the video.

...

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

1. To modify the layout, edit the main_activity.xml file, adding the following content for Step 3.2:

<LinearLayout ...>
    ...
    <LinearLayout ...>
        ...

        <!-- Step 3.2. This layout will be upgraded in the stop video step -->
        <Button
            android:id="@+id/stopVideo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="stop video" />

    </LinearLayout>
    ...
</LinearLayout>

2. Modify the interface linking in the MainActivity class in MainActivity.java:

  • New method for MainActivity:
@OnClick(R.id.stopVideo)
public void onStopVideo() {

}
  • Add the following code to the onCreate method:
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    //adding the stopVideo in the flow
    add(views, R.id.stopVideo);
    add(buttonsInConference, R.id.stopVideo);
    add(buttonsInOwnVideo, R.id.stopVideo);
}

3. Add the following logic to the application:

  • Use the following implementation for onLogin:
@OnClick(R.id.stopVideo)
public void onStopVideo() {
    VoxeetSDK.conference().stopVideo()
            .then((result, solver) -> updateViews())
            .error(error());
}
  • Add a handler for the StreamRemovedEvent:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(StreamRemovedEvent event) {
    updateStreams();
    updateViews();
}

What’s next?

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