1
votes

Trying to get WebRtc work only for one side. i have media stream server, on which installed Oven Media Engine. Signalling is turned on, and tested via Oven Media Player, everything works as expected. but when trying to reach that content via NativeScript or via any TypeScript free code, stucking on handling "answer" and the result is missing of RemoteVideoStream !

onTrack(event) - is not getting RemoteVideo whatever i do and try...

P.S - first steps in WebRtc, and maybe not figuring out all the logic of that solution, all examples are based on two way connections / call and answers, all i want to do is just get that stream and play without any local streams.

Tried to implement this diagram - 'https://airensoft.gitbook.io/ovenmediaengine/streaming/webrtc-publishing#signalling-protocol' - but unsuccess

Stream Server - 'ws://3.123.0.22:3333/app/test_o' - its turned on and signalling

Oven Media Player - 'http://demo.ovenplayer.com'

import {TNSRTCTrackEvent,TNSRTCPeerConnection,TNSRTCSessionDescription,TNSRTCIceCandidate} from'nativescript-webrtc-plugin';

import { LoggerService } from '../../services';

require('nativescript-websockets');

export class TestTnsRtc {
    public webSocket: any;
    public connection: any = null;

    constructor(private readonly _logger: LoggerService) {
        this.connection = new TNSRTCPeerConnection();

        this.connection.onIceCandidate((candidate: TNSRTCIceCandidate) => {
            this._logger.log('onIceCandidate -> candidate');

            const createdCandidate: any = {
                candidate: candidate.candidate,
                sdp: candidate.sdp,
                sdpMid: candidate.sdpMid,
                sdpMLineIndex: candidate.sdpMLineIndex,
                serverUrl: candidate.serverUrl
            };

            this._addIceCandidate([createdCandidate]);
        });

        this.connection.onTrack((track: TNSRTCTrackEvent) => {
            if (track.streams) {
                this._logger.log('onTrack -> Executed', track.streams[0]);
                // this.remoteView.srcObject = track.streams[0];
            }
        });

        this._createSocketConnection();
    }

    private _createSocketConnection(): void {
        const mySocket: any = new WebSocket('ws://3.123.0.22:3333/app/test_o');

        mySocket.addEventListener('open', (evt: any) => {
            this._logger.log('WebSocket Connected !');
            this.webSocket = evt.target;
            this._emitSocket({ command: 'request_offer' });
        });

            mySocket.addEventListener('message', (evt: any) => {
            this._logger.log('WebSocket got MESSAGE with type: ', JSON.parse(evt.data).command);

            if (JSON.parse(evt.data).command === 'offer') {
                const offer: any = {
                    id: JSON.parse(evt.data).id,
                    sdp: JSON.parse(evt.data).sdp,
                    candidates: JSON.parse(evt.data).candidates
                };

                this._answerOnOffer(offer);
            }
        });

        mySocket.addEventListener('close', (evt: any) => {
            this._logger.log(`WebSocket CLOSED with code: ${evt.code} && reason: ${evt.reason}`);
        });

        mySocket.addEventListener('error', (evt: any) => {
            this._logger.log(`WebSocket got ERROR: ${evt.error}`);
        });
    }

    private _answerOnOffer(offer: any): void {
        if (!this.connection || !offer) return;
        this.connection
            .setRemoteDescription(new TNSRTCSessionDescription(offer.sdp.type, offer.sdp.sdp))
            .then(() => {
                this._addIceCandidate(offer.candidates);
                this.connection
                    .createAnswer({})
                    .then((sdp: TNSRTCSessionDescription) => {
                        this._setLocalDescription(sdp, offer.id);
                    })
                    .catch((e: any) => {
                        this._logger.log(`createAnswer : Error -> ${e}`);
                    });
            })
            .catch((error: any) => {
                this._logger.log(`setRemoteDescription : Error -> ${error}`);
            });
    }

    private _setLocalDescription(sdp: TNSRTCSessionDescription, offerId: any): void {
        if (this.connection == null) return;
        this.connection
            .setLocalDescription(new TNSRTCSessionDescription(sdp.type, sdp.sdp))
            .then(() => {
                this._logger.log('setLocalDescription Done  with offer id: ', offerId);
                this._emitSocket({
                    command: 'answer',
                    id: offerId,
                    sdp: { type: 'answer', sdp: sdp.sdp },
                    candidates: []
                });
            })
            .catch((error: any) => {
                this._logger.log(`setLocalDescription : Error -> ${error}`);
            });
    }

    private _addIceCandidate(iceCandidates: TNSRTCIceCandidate[]): void {
        for (let iceCandidate of iceCandidates) {
            this.connection.addIceCandidate(iceCandidate);
        }
    }

    private _emitSocket(command: any): void {
        if (!this.webSocket) {
            return;
        }

        this.webSocket.send(JSON.stringify(command));
    }
}

Updated Version

I've tried to implement method [A] - and cut lot of code to simplify as possible.

mySocket.addEventListener('open', (evt: any) => {
    evt.target.send(JSON.stringify({ command: 'request_offer' }));
    this._webSocket = evt.target;
});

mySocket.addEventListener('message', (evt: any) => {
    // if type is answer, calling handler
    this._handleOffer(JSON.parse(evt.data));
});

private _handleOffer(offerObject: any): void {
    const config: TNSRTCConfiguration = new TNSRTCConfiguration({
        iceServers: [new TNSRTCIceServer(['stun:stun.l.google.com:19302'])]
    });

    const peerConnection: TNSRTCPeerConnection = new TNSRTCPeerConnection(config);
    const offer: any = {
        command: offerObject.command,
        id: offerObject.id,
        code: offerObject.code,
        peerId: offerObject.peer_id,
        sdp: offerObject.sdp,
        candidates: offerObject.candidates
    };

    for (const ice of offer.candidates) {
        ice.candidate = this._updateIceIp(ice);
        peerConnection.addIceCandidate(ice);
    }

    const remoteDescription: TNSRTCSessionDescription = new TNSRTCSessionDescription(offer.sdp.type, offer.sdp.sdp);
    peerConnection
        .setRemoteDescription(remoteDescription)
        .then(() => {
            peerConnection
            .createAnswer({})
            .then((sdp: TNSRTCSessionDescription) => {
                const localDescription: TNSRTCSessionDescription = new TNSRTCSessionDescription(sdp.type, sdp.sdp);
                peerConnection
                .setLocalDescription(localDescription)
                .then(() => {
                    const generatedAnswer: any = JSON.stringify({
                        id: offer.id,
                        peer_id: offer.peerId,
                        command: 'answer',
                        sdp: { type: sdp.type, sdp: sdp.sdp }
                    });
                    this._webSocket.send(generatedAnswer);
                })
                .catch((err: any) => {
                    this._logger.log(`setLocalDescription -> catch: ${err}`);
                });
            })
            .catch((err: any) => {
                this._logger.log(`createAnswer -> catch: ${err}`);
            });
        })
        .catch((err: any) => {
            this._logger.log(`setRemoteDescription -> catch: ${err}`);
        });

    peerConnection.onIceCandidate((candidate: TNSRTCIceCandidate) => {
        this._logger.log('EVENT -> onIceCandidate');
        if (candidate && candidate.candidate) {
            const iceCandidate: any = {
                candidate: candidate.candidate,
                sdpMid: candidate.sdpMid,
                sdpMLineIndex: candidate.sdpMLineIndex
            };

            const sendData: any = JSON.stringify({
                id: offer.id,
                peer_id: offer.peerId,
                command: 'candidate',
                candidates: [iceCandidate]
            });

            this._webSocket.send(sendData);
        }
    });

    peerConnection.onSignalingStateChange(() => {
        this._logger.log('EVENT -> onSignalingStateChange', peerConnection.connectionState);
    });

    peerConnection.onIceGatheringStateChange(() => {
        this._logger.log('EVENT -> onIceGatheringStateChange', peerConnection.connectionState);
    });

    peerConnection.onConnectionStateChange(() => {
        this._logger.log('EVENT -> onConnectionStateChange', peerConnection.connectionState);
    });

    peerConnection.onTrack((track: TNSRTCTrackEvent) => {
        this._logger.log('EVENT -> onTrack');
    });
}

private _updateIceIp(candidate: TNSRTCIceCandidate): string {
    if (candidate.candidate) {
        const ipRegex: RegExp = new RegExp(
            /\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
            'gi'
        );
        const ipOfCandidate: any = candidate.candidate.match(ipRegex)[0];

        return candidate.candidate.replace(ipOfCandidate, '3.123.0.22');
    }
}
  1. here is offer response from OME - https://prnt.sc/tvccti
  2. .setRemoteDescription - success
  3. .createAnswer - success
  4. .setLocalDescription - success
  5. here is candidates from .onIceCandidate event, which i'm sending to OME server - https://prnt.sc/tvcg9d
  6. and here is my logs output - https://prnt.sc/tvch52

my peerConnection.connectionState is always "new", and .onTrack() event invokes before points 4 and 5 whith empty body.

1

1 Answers

0
votes

Thank you for using our open-source products.

[A] It's possible to have trouble streaming when running OvenMediaEngine behind NAT.

OME running behind NAT creates "ICE candidates" using a private IP address because the ICE candidate provides only the private IP address the player can’t connect to OME.

e.g. The candidate came from OME running behind NAT looks like this. “172.17.0.2“ is a private IP address:

"candidate:0 1 UDP 50 172.17.0.2 10001 typ host"

To avoid this situation, OvenPlayer duplicates the candidate from OME and references the WebRTC signal address to replace the candidate's private IP address with the public IP address. And then OvenPlayer also adds duplicated candidates to the peer connection too.

i.e. If the signalling address is “'ws://3.123.0.22:3333/app/test_o”, then duplicated candidates look like this:

"candidate:0 1 UDP 50 3.123.0.22 10001 typ host"

So these functions need to be implemented while add ICE candidates came from OME to local peer connection.

Add Ice candidates came from OME, and HERE (Click) is how OvenPlayer is doing it. Then, please refer HERE (Click) to clone and replace the IP address.

By the way, looking at you're code, I think you don’t need to this._addIceCandidate([createdCandidate]); because you don’t need to set player’s candidates to player’s peer connection.

this.connection.onIceCandidate((candidate: TNSRTCIceCandidate) => {
    this._logger.log('onIceCandidate -> candidate');

    const createdCandidate: any = {
        candidate: candidate.candidate,
        sdp: candidate.sdp,
        sdpMid: candidate.sdpMid,
        sdpMLineIndex: candidate.sdpMLineIndex,
        serverUrl: candidate.serverUrl
    };

    this._addIceCandidate([createdCandidate]);
});

[B] In Server.xml in OME, there is a way to put a certified IP instead of *. OME automatically finds and puts the IP address according to the * setting. However, the problem is that the private IP is entered when using the cloud.

Existing settings:

<IceCandidates>
    <IceCandidate>*:10000-10005/udp</IceCandidate>
</IceCandidates>

Example:

<IceCandidates>
    <IceCandidate><Public IP>:10000-10005/udp</IceCandidate>
</IceCandidates>

However, the reason why OME and OvenPlayer Demo installed in the cloud will work even if you do not change the settings above is that it works as stated in [A].

This is a special feature of OvenPlayer, so you can perform it as described in [A], or do as described in [B] without [A].

Thank you.