openvidu-insecure-angular

Check it on GitHub

A client-side only application built with Angular 6 framework.

If it is the first time you use OpenVidu, it is higly recommended to start with openvidu-hello-world tutorial, as this app is no more than an extension of it with some new features and sytles.

This is the Angular version of openvidu-insecure-js. Try it if you plan to use Angular framework for your frontend.

Understanding this tutorial

OpenVidu is composed by the three modules displayed on the image above in its insecure version.

  • openvidu-browser: NPM package for your Angular app. It allows you to manage your video-calls straight away from your clients
  • openvidu-server: Java application that controls Kurento Media Server
  • Kurento Media Server: server that handles low level operations of media flow transmissions
Tutorial's name includes "insecure" word because this application has no backend and therefore it has no control over the users. Typically you don't want such application in production environments. When you feel comfortable with the client-side of OpenVidu, add your own server or follow one of our super simple secure tutorials.

Running this tutorial

1) Clone the repo:

git clone https://github.com/OpenVidu/openvidu-tutorials.git

2) You will need angular-cli (and of course NPM) to serve the Angular frontend. You can install it with the following command:

npm install -g @angular/cli@6.0.1

3) Run the tutorial:

cd openvidu-tutorials/openvidu-insecure-angular
npm install
ng serve

4) openvidu-server and Kurento Media Server must be up and running in your development machine. The easiest way is running this Docker container which wraps both of them (you will need Docker CE):

docker run -p 4443:4443 --rm -e openvidu.secret=MY_SECRET openvidu/openvidu-server-kms:2.4.0

5) Go to localhost:4200 to test the app once the server is running. The first time you use the docker container, an alert message will suggest you accept the self-signed certificate of openvidu-server when you first try to join a video-call.


If you are using Windows, read this FAQ to properly run the tutorial

To learn some tips to develop with OpenVidu, check this FAQ

Understanding the code

This is an Angular project generated with angular-cli tool, and therefore you will see lots of configuration files and other stuff that doesn't really matter to us. We will focus on the following files under src/app/ folder:

  • app.component.ts: defines AppComponent, main component of the app. It contains the functionalities for joining a video-call and for handling the video-calls themselves.
  • app.component.html: HTML for AppComponent.
  • app.component.css: CSS for AppComponent.
  • user-video.component.ts: defines UserVideoComponent, used to display every user video. It contains one OpenViduVideoComponent, the name of the user and also handles a click event to update the view of AppComponent.
  • ov-video.component.ts: defines OpenViduVideoComponent, which wraps the final HTML <video> that finally displays the media stream.

Let's see first how app.component.ts uses NPM package openvidu-browser:


We import the necessary objects from openvidu-browser:

import { OpenVidu, Session, StreamManager, Publisher, Subscriber, StreamEvent } from 'openvidu-browser';

app.component.ts declares the following properties:

// OpenVidu objects
OV: OpenVidu;
session: Session;
publisher: StreamManager; // Local
subscribers: StreamManager[] = []; // Remotes

// Join form
mySessionId: string;
myUserName: string;

// Main video of the page, will be 'publisher' or one of the 'subscribers',
// updated by an Output event of UserVideoComponent children
@Input() mainStreamManager: StreamManager;

OpenVidu object will allow us to get a Session object, which is declared just after it. publisher StreamManager object will be will be our own local webcam stream and subscribers StreamManager array will store the active streams of other users in the video-call. Finally, mySessionId and myUserName params simply represent the video-call and your participant's nickname, as you will see in a moment.


Whenever a user clicks on the submit input defined in app.component.html, joinSession() method is called:


We first get an OpenVidu object and initialize a Session object with it.

// --- 1) Get an OpenVidu object ---

this.OV = new OpenVidu();

// --- 2) Init a session ---

this.session = this.OV.initSession();

Then we subscribe to the Session events that interest us.

// --- 3) Specify the actions when events take place in the session ---

// On every new Stream received...
this.session.on('streamCreated', (event: StreamEvent) => {

    // Subscribe to the Stream to receive it. Second parameter is undefined
    // so OpenVidu doesn't create an HTML video by its own
    let subscriber: Subscriber = this.session.subscribe(event.stream, undefined);
    this.subscribers.push(subscriber);
});

// On every Stream destroyed...
this.session.on('streamDestroyed', (event: StreamEvent) => {

    // Remove the stream from 'subscribers' array
    this.deleteSubscriber(event.stream.streamManager);
});

As we are using Angular framework, a good approach for managing the remote media streams is to loop across an array of them, feeding a common component with each Subscriber object and let it manage its video. This component will be our UserVideoComponent. To do this, we need to store each new Subscriber we received in array subscribers (of the parent class StreamManager), and we must remove from it every deleted subscriber whenever it is necessary. To achieve this, we use the following events:

  • streamCreated: for each new Stream received by the Session object, we subscribe to it and store the returned Subscriber object in our subscribers array. Method session.subscribe has undefined as second parameter so OpenVidu doesn't insert and HTML video element in the DOM on its own (we will use the video element contained in one of our child components). HTML template of AppComponent loops through subscribers array with an ngFor directive, declaring a UserVideoComponent for each subscriber. We feed them not really as Subscriber objects, but rather as their parent class StreamManager. This way we can reuse UserVideoComponent to also display our Publisher object (that also inhertis from class StreamManager). user-video also declares an output event to let AppComponent know when the user has clicked on it.

    <div *ngFor="let sub of subscribers" class="stream-container col-md-6 col-xs-6">
        <user-video [streamManager]="sub" (clicked)="updateMainStreamManager(sub)"></user-video>
    </div>
    
  • streamDestroyed: for each Stream that has been destroyed from the Session object (which means a user has left the video-call), we remove the associated Subscriber from subscribers array, so Angular will automatically delete the required UserVideoComponent from HTML. Each Stream object has a property streamManager that indicates which Subscriber or Publisher owns it (in the same way, each StreamManager object also has a reference to its Stream).


Get a token from OpenVidu Server

WARNING: This is why this tutorial is an insecure application. We need to ask OpenVidu Server for a user token in order to connect to our session. This process should entirely take place in our server-side, not in our client-side. But due to the lack of an application backend in this tutorial, the Angular front itself will perform the POST operations to OpenVidu Server
// --- 4) Connect to the session with a valid user token ---

// 'getToken' method is simulating what your server-side should do.
// 'token' parameter should be retrieved and returned by your own backend
this.getToken().then(token => {
    // See next point to see how to connect to the session using 'token'
});

Now we need a token from OpenVidu Server. In a production environment we would perform this operations in our application backend, by making use of the API REST, OpenVidu Java Client or OpenVidu Node Client. Here we have implemented the POST requests to OpenVidu Server in a method getToken() that returns a Promise with the token, using @angular/http library. Without going into too much detail, this method performs two POST requests to OpenVidu Server, passing OpenVidu Server secret to authenticate them:

  • First request performs a POST to /api/sessions (we send a customSessionId field to name the session with our mySessionId value retrieved from HTML input)
  • Second request performs a POST to /api/tokens (we send a session field to assign the token to this same session)

You can inspect this method in detail in the GitHub repo.


Finally connect to the session and publish your webcam:

 // --- 4) Connect to the session with a valid user token ---

// 'getToken' method is simulating what your server-side should do.
// 'token' parameter should be retrieved and returned by your own backend
this.getToken().then(token => {

    // First param is the token got from OpenVidu Server. Second param can be retrieved by every user on event
    // 'streamCreated' (property Stream.connection.data), and will be appended to DOM as the user's nickname
    this.session.connect(token, { clientData: this.myUserName })
        .then(() => {

            // --- 5) Get your own camera stream ---

            // Init a publisher passing undefined as targetElement (we don't want OpenVidu to insert a video
            // element: we will manage it on our own) and with the desired properties
            let publisher: Publisher = this.OV.initPublisher(undefined, {
                audioSource: undefined, // The source of audio. If undefined default microphone
                videoSource: undefined, // The source of video. If undefined default webcam
                publishAudio: true,     // Whether you want to start publishing with your audio unmuted or not
                publishVideo: true,     // Whether you want to start publishing with your video enabled or not
                resolution: '640x480',  // The resolution of your video
                frameRate: 30,          // The frame rate of your video
                insertMode: 'APPEND',   // How the video is inserted in the target element 'video-container'
                mirror: false           // Whether to mirror your local video or not
            });

            // --- 6) Publish your stream ---

            this.session.publish(publisher);

            // Set the main video in the page to display our webcam and store our Publisher
            this.mainStreamManager = publisher;
            this.publisher = publisher;
        })
        .catch(error => {
            console.log('There was an error connecting to the session:', error.code, error.message);
        });
});

In session.connect method first param is the recently retrieved user token. Second param is the value every user will receive in event.stream.connection.data property on streamCreated event (this value will be used by UserVideoComponent to append the user's nickname to the his video). So in this case it is an object with a property "clientData" with value "myUserName", which is binded from HTML input <input class="form-control" type="text" id="userName" name="userName" [(ngModel)]="myUserName" required> (filled by the user).

If the method succeeds, we proceed to publish our webcam to the session. To do so we get a Publisher object with the desired properties and publish it to the Session through Session.publish() method. The rest of users will receive our Stream object and will execute their streamCreated event. Finally we make the main video player (which is just another UserVideoComponent) display the Publisher object by default. This is the HTML code that will display the main stream manager:

<div *ngIf="mainStreamManager" id="main-video" class="col-md-6">
    <user-video [streamManager]="mainStreamManager"></user-video>
</div>

And we store the Publisher under this.publisher, which is also of parent class StreamManager. This way our webcam will be appended along all remote subscribers, in exactly the same way they are shown (remember all of them are displayed by UserVideoComponent):

<div *ngIf="publisher" class="stream-container col-md-6 col-xs-6">
    <user-video [streamManager]="publisher" (clicked)="updateMainStreamManager(publisher)"></user-video>
</div>

Last point worth considering is the implementation of UserVideoComonent and OpenViduVideoComponent. Each UserVideoComponent manages one StreamManager object (a Subscriber or a Publisher) that will be fed to its child component OpenViduVideoComponent. Its main task is not managing the final video player (that is OpenViduVideoComponent responsibility), but displaying custom information for each one of them (the user's nickname) and handling the click event on them to update property mainStreamManager of parent AppComponent:

<div (click)="videoClicked()">
    <ov-video [streamManager]="streamManager"></ov-video>
    <div><p>{{getNicknameTag()}}</p></div>
</div>
export class UserVideoComponent {

    @Input()
    streamManager: StreamManager;

    @Output()
    clicked = new EventEmitter();

    getNicknameTag() { // Gets the nickName of the user
        return JSON.parse(this.streamManager.stream.connection.data).clientData;
    }

    videoClicked() { // Triggers event for the parent component to update its main video display
        this.clicked.emit();
    }
}

OpenViduVideoComponent html template is just the video element:

<video #videoElement></video>

And the unique responsibility of the component's logic is letting OpenVidu know the exact HTML DOM video player associated to its StreamManger. To do so we use method StreamManager.addVideoElement, which receives a native HTML video element. The way we implement this is Angular dependant: we get the video element with @ViewChild tag and we call the method once after the view has initialized (ngAfterViewInit) and once every time the StreamManager input changes (set method with @Input tag)

export class OpenViduVideoComponent implements AfterViewInit {

    @ViewChild('videoElement') elementRef: ElementRef;

    _streamManager: StreamManager;

    ngAfterViewInit() {
        this._streamManager.addVideoElement(this.elementRef.nativeElement);
    }

    @Input()
    set streamManager(streamManager: StreamManager) {
        this._streamManager = streamManager;
        if (!!this.elementRef) {
            this._streamManager.addVideoElement(this.elementRef.nativeElement);
        }
    }
}

Leaving the session:

Whenever we want a user to leave the session, we just need to call session.disconnect method in app.component.ts:

  leaveSession() {

    // --- 7) Leave the session by calling 'disconnect' method over the Session object ---

    if (this.session) { this.session.disconnect(); };

    // Empty all properties...
    this.subscribers = [];
    delete this.publisher;
    delete this.session;
    delete this.OV;
    this.generateParticipantInfo();
  }