0

I am trying to connect a Kurento Media Server to React Native on iOS for a group video call. The server is running on Nodejs and Docker, and they already have a TURN server set up.

The app is already running in the web browser via kurento-utils, but I can't stream the web video to RN, instead, the RN video looks just fine on the web.

I'm not usig kurento-utils on client side of React Native.

This is my code:

Video Screen (emmiter and receptor)

import React, { useState, useEffect } from 'react';
import { 
    View,
    StyleSheet,
    Text,
} from 'react-native';

import Display from 'react-native-display';
import InCallManager from 'react-native-incall-manager';
import { TouchableOpacity } from 'react-native-gesture-handler';



import { socket } from '../../screens/User';
import ReceiveScreen from './ReceiveScreen';
import {
    RTCView,
    RTCIceCandidate,
} from 'react-native-webrtc';
import { 
    startCommunication, 
    receiveVideo,
    addIceCandidate, 
    ProcessAnswer,
    ViewPCArray
} from '../../utils/webrtc-utils';
const participants = {};

function sendMessage(message) {
    if (socket) {
        socket.emit('message', message);
    }
}

const VideoScreen = () => {
        const [videoURL, setVideoURL] = useState(null)
        const [remoteURL, setRemoteURL] = useState(null)
        const [userName, setUserName] = useState(socket.id)
        const [roomName, setRoomName] = useState('9b33737f-737f-4a3d-a323-e1cd3f4b68b2')


    useEffect(() => {
        if(socket) {
            var message = {
                event: 'joinRoom',
                userName: userName,
                roomName: roomName
            }
            sendMessage(message);
        }

        socket.on('message', message => {
            messageProcessHandler(message);
        });

        return () => {
            socket.close();
        }
    }, [])

    /**
     * 
     * @param {*} msg 
     */
    const messageProcessHandler = (msg) => {
        // console.log(`MessageProcessHandler: ${msg.event}`);
        switch (msg.event) {
            case 'existingParticipants':
                console.log('LOG:176 onExistingParticipants= ' + JSON.stringify(msg));
                startCommunication(sendMessage, userName, (stream) => {
                    setVideoURL(stream.toURL())
                    msg.existingUsers.forEach((object) => {
                        participants[object.name] = object.id;
                        console.log("participants:" + JSON.stringify(participants));
                        receiveVideo(sendMessage, object.name, (pc) => {
                            console.log('getRemoteStreams ', pc);
                            pc.getRemoteStreams().forEach(track => {
                                // console.log('TRACK: ', track);
                                console.log('STREAM', track);
                                // pc.addTrack(track, stream);
                                setRemoteURL(track.toURL())
                            })
                        });
                    });
                });
                break;
            case 'newParticipantArrived':
                participants[msg.name] = msg.name;            
                if (remoteURL == null || remoteURL === '') {
                    receiveVideo(sendMessage, msg.userName, (pc) => {   
                        pc.getRemoteStreams().forEach(track => {
                            console.log('STREAM', track);
                            setRemoteURL(track.toURL())
                        })
                    });
                }
                break;
            case 'participantLeft':
                participantLeft(msg.name);   
                break;
            case 'receiveVideoAnswer':
                ProcessAnswer(msg.senderid, msg.sdpAnswer, (err) => {
                    if (err) {
                        console.log('the error: ' + err);
                    }
                });
                break;
            case 'candidate':
                addIceCandidate(msg.userid, new RTCIceCandidate(msg.candidate));
                break;
            default:
                console.error('Unrecognized message', msg.message);
        }
    }

    /**
     * 
     * @param {*} name 
     */
    const participantLeft = (name) => {
        if (participants[name]) {
            delete participants[name];
        }

        if (Object.keys(participants).length == 0) {
            setRemoteURL(null)
        }
    }

    return (
        <View style={styles.container}>
            <View style={{position: "absolute", top: 40, right: 20, zIndex: 200}}>
                <Text style={{fontSize: 20, fontWeight: "bold", color: "#fff"}}>{userName}</Text>
            </View>
            <RTCView zOrder={0} objectFit='cover' style={styles.videoContainer} streamURL={videoURL}  />

            <Display enable={remoteURL != null}>
                <View style={styles.floatView}>
                    <ReceiveScreen videoURL={remoteURL} />
                </View>
            </Display>
        </View>
    );

}

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    videoContainer: {
        flex: 1
    },
    floatView: {
        position: 'absolute',
        width: 250,
        height: 210,
        bottom: 15,
        right: 20,
        backgroundColor: 'rgba(255, 255, 255, 0.1)',
        borderRadius: 15
    }
});
export default VideoScreen 

Display (where the browser video is supposed to be)

import React, { Component } from 'react';
import config from "../../config/app.js";
import { 
    View,
    Text,
    ScrollView,
    StyleSheet
} from 'react-native';

import { RTCView } from 'react-native-webrtc';

const styles = StyleSheet.create({
    root: {
        flex: 1
    },
    titleContainer: {
        flex: 0.1,
        paddingVertical: 2,
        paddingLeft: 10
    },
    title: {
        fontSize: 15,
        color: 'rgba(255, 255, 255, 0.5)'
    },
    contentContainer: {
       flex: 1,
       backgroundColor: 'transparent'
    }
});


const ReceiveScreen = (props) => {
    return (
        <View style={styles.root}>
            <View style={styles.titleContainer}>
                <Text style={styles.title}>Nuevo miembro</Text>
            </View>
            <RTCView objectFit='cover' zOrder={1} style={styles.contentContainer} streamURL={props.videoURL} mirror={true} />
        </View>
    )
}

export default ReceiveScreen;

WebRtc Utils

import {
    getUserMedia,
    MediaStreamTrack,
    mediaDevices,
    RTCPeerConnection,
    RTCSessionDescription,
} from 'react-native-webrtc';

import { PERMISSIONS, request, checkMultiple } from 'react-native-permissions';

import { socket } from '../screens/User';

let pcArray = {};
let isFront = true;
let isEnableAudio = true;
let isEnableVideo = true;
let localstream = null;
const ICE_CONFIG = {
    'iceServers': [
        {
            'urls': 'turn:xxx.xxx.xxx.xxx:3478',
            'username': 'xxxxxxxx',
            'credential': 'xxxxxxxxxxxxxxxxxxxxxxx',
            'credentialType': 'password'
        }
    ]
};

/**
 * Obtenga elementos multimedia locales (transmisiones de video)
 *
 * @param {*} _sendMessage
 * @param {*} _name
 * @param {*} callback
 */
export function startCommunication(_sendMessage, _name, callback) {
    getStream(true, stream => {
        localstream = stream;
        let options = {
            audio: true,
            video : {
                mandatory : {
                    maxWidth : 320,
                    maxFrameRate : 15,
                    minFrameRate : 1
                }
            }
        };
        let pc = createPC(_sendMessage, _name, true, options);
        pcArray[_name] = pc;
        callback(stream);
    });
}
/**
 * Obtener transmisión de video remota
 *
 * @param {*} _sendMessae
 * @param {*} _name
 * @param {*} callback
 */
export function receiveVideo(_sendMessae, _name, callback) {
    console.log("receiveVideo:", _sendMessae, _name, callback);
    let options = {
        audio: true,
        video : {
            mandatory : {
                // maxWidth : 320,
                // maxFrameRate : 15,
                // minFrameRate : 15
                maxWidth : 120,
                maxHeight: 80,
                minWidth : 80,
                minHeight: 60,
                maxFrameRate : 10,
                minFrameRate : 10
            }
        }
    };
    let pc = createPC(_sendMessae, _name, true, options);
    // console.log(`PC CREATED FOR ${_name}: ${JSON.stringify(pc)}`);
    pcArray[_name] = pc;
    // callback(pc);
    callback(pcArray);
}
/**
 * Encender/apagar el micrófono
 */
export function toggleAudio() {
    if (localstream) {
        isEnableAudio = !isEnableAudio;
        localstream.getAudioTracks().forEach((track) => {
            track.enabled = isEnableAudio;
        });
    } else {
        console.log('in toggleAudio(), localstream is empty');
    }
    return isEnableAudio;
}
/**
 * Activar/desactivar vídeo
 */
export function toggleVideo() {
    if (localstream) {
        isEnableVideo = !isEnableVideo;
        localstream.getVideoTracks().forEach((track) => {
            track.enabled = isEnableVideo;
        });
    } else {
        console.log('in toggleVideo(), localstream is empty');
    }
    return isEnableVideo;
}
/**
 * cambiar de cámara
 *
 */
export function switchVideoType() {
    if (localstream) {
        localstream.getVideoTracks().forEach(track => {
            track._switchCamera();
        });
    } else {
        console.log('error');
    }
}
/**
 * Crear una transmisión de video local
 *
 * @param {*} isFront
 * @param {*} callback
 */

export function getStream(isFront, callback) {
    mediaDevices.enumerateDevices().then(sourceInfos => {
        //console.log('Log: '+ JSON.stringify(sourceInfos)); // -> Es un objecto de 3 
        let videoSourceId;
        for (let i = 0; i < sourceInfos.length; i++) {
            const sourceInfo = sourceInfos[i];
            if (sourceInfo.kind === 'videoinput' && sourceInfo.facing === (isFront ? 'front' : 'back')) {
                // if (sourceInfo.kind === 'videoinput' && sourceInfo.facing === (isFront ? 'front' : 'back')) { Was video only, not videoinput
                videoSourceId = sourceInfo.id;
            }
        }
        request(PERMISSIONS.IOS.CAMERA).then((response) => {
            if (response === 'granted') {
                request(PERMISSIONS.IOS.CAMERA).then((response) => {
                    if (response === 'granted') {
                        mediaDevices.getUserMedia({
                            audio: true,
                            video: {
                                width: 640,
                                height: 480,
                                frameRate: 30,
                                facingMode: (isFront ? "user" : "environment"),
                                deviceId: videoSourceId
                            }
                        })
                            .then(stream => {
                                callback(stream)
                            })
                            .catch(error => {
                                // Log error
                                console.log(error)
                            });
                    }
                })
            }
        })
    });
}
/**
 *
 * Crear una conexión WebRTC
 *
 * @param {*} sendMessage
 * @param {*} name
 * @param {*} isOffer
 * @param {*} options
 */
export function createPC(sendMessage, name, isOffer, options) {
    let pc = new RTCPeerConnection(ICE_CONFIG);
    pc.onnegotiationneeded = () => {
        console.log('onnegotiationneeded');
        if (isOffer) {
            isOffer = false;
            createOffer();
        }
    };
    pc.onicecandidate = (event) => {
        console.log('onicecandidate');
        if (event.candidate) {
            let msg = {
                event: 'candidate',
                // event: 'onIceCandidate',
                userid: socket.id,
                roomName: '9b33737f-737f-4a3d-a323-e1cd3f4b68b2',
                candidate: event.candidate
            };
            sendMessage(msg);
        }
    };
    pc.oniceconnectionstatechange = (event) => {
        // console.log('oniceconnectionstatechange');
        if (event.target.iceConnectionState === 'disconnected') {
            //localstream.release();
            // localstream = null;
            if (pc !== null) {
                pc.close();
                pc = null;
            }
        }
    };
    pc.onsignalingstatechange = (event) => {
        console.log('onsignalingstatechange: ', event.target.signalingState);
    };
    // send local stream
    pc.addStream(localstream);
    function createOffer() {
        // console.log('createOffer');
        pc.createOffer()
            .then(function (desc) {
                // console.log('...createOffer...');
                pc.setLocalDescription(desc);
                // something to do
                let msg = {
                    event: 'receiveVideoFrom',
                    userid: socket.id,
                    roomName: '9b33737f-737f-4a3d-a323-e1cd3f4b68b2',
                    // sdpOffer: desc.sdp,
                    sdpOffer: desc.sdp,
                };
                sendMessage(msg);
            })
            .catch(err => console.error(err));
    }
    return pc;
}
/**
 * Adición incremental de iceCandidate
 *
 * @param {*} name
 * @param {*} candidate
 */
export function addIceCandidate(name, candidate) {
    let pc = pcArray[name];
    if (pc) {
        pc.addIceCandidate(candidate);
    } else {
        console.log('pc.addIceCandidate failed : pc not exists');
    }
}
/**
 * Proceso SdpAnswer
 *
 * @param {*} name
 * @param {*} sdp
 * @param {*} callback
 */
export async function ProcessAnswer(name, sdp, callback) {
    let pc = pcArray[name];
    if (pc) {
        let answer = {
            type: 'answer',
            sdp: sdp,
        };
        if (pc) {
            // const remoteDesc = new RTCSessionDescription(answer);
            // await pc.setRemoteDescription(remoteDesc);

            await pc.setRemoteDescription(new RTCSessionDescription(answer)).then(function () {
                console.log('LOG:252 Answer = ' + answer);
                callback();
            })
                .catch(function (reason) {
                    callback(reason);
                    console.log('ERROR REASON:', reason);
                });
        }
    } else {
        console.log('ProcessAnswer failed : pc not exists');
    }
}
/**
 *
 * Cierra la conexión y libera el streamer local.
 *
 */


export function ViewPCArray (username) {
    // console.log(pcArray[username].getRemoteStreams()[0]);
    return pcArray
}


export function ReleaseMeidaSource() {
    if (localstream) {
        localstream.release();
        localstream = null;
    }
    if (pcArray !== null) {
        for (let mem in pcArray) {
            pcArray[mem].close();
            delete pcArray[mem];
        }
    }
}
function logError(error) {
    console.log('logError', error);
}

NodeJs Kurento

module.exports = function(io, socket) {
    // variables
const kurento = require('kurento-client');
const minimist = require('minimist');

var kurentoClient = null;
var iceCandidateQueues = {};


// constants
var argv = minimist(process.argv.slice(2), {
    default: {
        // as_uri: 'https://localhost:3000/',
        // ws_uri: 'ws://localhost:8888/kurento'
        as_uri: 'https://localhost:4000/',
        ws_uri: 'ws://localhost:8888/kurento'
    }
});

socket.on('message', function (message) {
    console.log('Message received: ', message.event);

    switch (message.event) {
        case 'joinRoom':
            joinRoom(socket, message.userName, message.roomName, err => {
                if (err) {
                    console.log(err);
                }
            });
            break;
        case 'receiveVideoFrom':
            receiveVideoFrom(socket, message.userid, message.roomName, message.sdpOffer, err => {
                if (err) {
                    console.log(err);
                }
            });
            break;
        case 'candidate':
            addIceCandidate(socket, message.userid, message.roomName, message.candidate, err => {
                if (err) {
                    console.log(err);
                }
            });
            break;
    }
    socket.on('disconnect', () => {
        leaveRoom(socket, message.roomName, err => {
            if (err) {
                console.log(err);
            };
        });
    })
});

// signaling functions
function joinRoom(socket, username, roomname, callback) {
    console.log(('Smeone in the Room').green + socket + " " + username + " " + roomname)
    getRoom(socket, roomname, (err, myRoom) => {
        if (err) {
            return callback(err);
        }

        myRoom.pipeline.create('WebRtcEndpoint', (err, outgoingMedia) => {
            outgoingMedia.setMaxVideoSendBandwidth(100);
            if (err) {
                return callback(err);
            }

            var user = {
                id: socket.id,
                name: username,
                outgoingMedia: outgoingMedia,
                incomingMedia: {}
            }

            let iceCandidateQueue = iceCandidateQueues[user.id];
            if (iceCandidateQueue) {
                while (iceCandidateQueue.length) {
                    let ice = iceCandidateQueue.shift();
                    console.error(`user: ${user.name} collect candidate for outgoing media`);
                    user.outgoingMedia.addIceCandidate(ice.candidate);
                }
            }

            user.outgoingMedia.on('OnIceCandidate', event => {
                let candidate = kurento.register.complexTypes.IceCandidate(event.candidate);
                socket.emit('message', {
                    event: 'candidate',
                    userid: user.id,
                    candidate: candidate
                });
            });

            socket.to(roomname).emit('message', {
                event: 'newParticipantArrived',
                userid: user.id,
                username: user.name
            });

            let existingUsers = [];
            for (let i in myRoom.participants) {
                if (myRoom.participants[i].id != user.id) {
                    existingUsers.push({
                        id: myRoom.participants[i].id,
                        name: myRoom.participants[i].name
                    });
                }
            }
            socket.emit('message', {
                event: 'existingParticipants',
                existingUsers: existingUsers,
                userid: user.id
            });
            myRoom.participants[user.id] = user;
        });
    });
}








// Disconnect from room
function leaveRoom(socket, roomname, callback) {

    if (io.sockets.adapter.rooms[roomname] == null) {
        // ROOMS
        // console.log((io.sockets.adapter.rooms))
        return;
    }

    var userSession = io.sockets.adapter.rooms[roomname].participants[socket.id];

    var myRoom = io.sockets.adapter.rooms[roomname] || { length: 0 };
    // MY ROOM
    // console.log(myRoom);


    if (!userSession) {
        return;
    }

    var room = io.sockets.adapter.rooms[roomname];

    if(!room){
        return;
    }

    console.log('notify all user that ' + userSession.name + ' is leaving the room ' + roomname);
    var usersInRoom = room.participants;
    delete usersInRoom[userSession.id];
    userSession.outgoingMedia.release();
    
    // release incoming media for the leaving user
    for (var i in userSession.incomingMedia) {
        userSession.incomingMedia[i].release();
        delete userSession.incomingMedia[i];
    }

    for (var i in usersInRoom) {
        var user = usersInRoom[i];
        // release viewer from this
        user.incomingMedia[userSession.id].release();
        delete user.incomingMedia[userSession.id];

        // notify all user in the room
        io.emit('message', {
            event: 'participantLeft',
            name: userSession.id
        });

        console.log('Mensaje emitido')
    }

    // Release pipeline and delete room when room is empty
    if (Object.keys(room.participants).length == 0) {
        room.pipeline.release();
        delete rooms[userSession.roomName];
    }
    delete userSession.roomName;
    // console.log(myRoom);
    callback(null);
}




function receiveVideoFrom(socket, userid, roomname, sdpOffer, callback) {
    getEndpointForUser(socket, roomname, userid, (err, endpoint) => {
        if (err) {
            return callback(err);
        }

        endpoint.processOffer(sdpOffer, (err, sdpAnswer) => {
            if (err) {
                return callback(err);
            }

            socket.emit('message', {
                event: 'receiveVideoAnswer',
                senderid: userid,
                sdpAnswer: sdpAnswer
            });

            endpoint.gatherCandidates(err => {
                if (err) {
                    return callback(err);
                }
            });
        });
    });
}

function addIceCandidate(socket, senderid, roomname, iceCandidate, callback) {
    // console.log(io.sockets.adapter.rooms);
    let user = io.sockets.adapter.rooms[roomname].participants[socket.id];
    if (user != null) {
        let candidate = kurento.register.complexTypes.IceCandidate(iceCandidate);
        if (senderid == user.id) {
            if (user.outgoingMedia) {
                user.outgoingMedia.addIceCandidate(candidate);
            } else {
                iceCandidateQueues[user.id].push({ candidate: candidate });
            }
        } else {
            if (user.incomingMedia[senderid]) {
                user.incomingMedia[senderid].addIceCandidate(candidate);
            } else {
                if (!iceCandidateQueues[senderid]) {
                    iceCandidateQueues[senderid] = [];
                }
                iceCandidateQueues[senderid].push({ candidate: candidate });
            }
        }
        callback(null);
    } else {
        callback(new Error("addIceCandidate failed"));
    }
}

// useful functions
function getRoom(socket, roomname, callback) {
    var myRoom = io.sockets.adapter.rooms[roomname] || { length: 0 };
    var numClients = myRoom.length;

    console.log(roomname, ' has ', numClients, ' clients');

    if (numClients == 0) {
        socket.join(roomname, () => {
            myRoom = io.sockets.adapter.rooms[roomname];
            getKurentoClient((error, kurento) => {
                kurento.create('MediaPipeline', (err, pipeline) => {
                    if (error) {
                        return callback(err);
                    }

                    myRoom.pipeline = pipeline;
                    myRoom.participants = {};
                    callback(null, myRoom);
                });
            });
        });
    } else {
        socket.join(roomname);
        callback(null, myRoom);
    }
}

function getEndpointForUser(socket, roomname, senderid, callback) {
    var myRoom = io.sockets.adapter.rooms[roomname];
    var asker = myRoom.participants[socket.id];
    var sender = myRoom.participants[senderid];

    if (asker.id === sender.id) {
        return callback(null, asker.outgoingMedia);
    }

    if (asker.incomingMedia[sender.id]) {
        sender.outgoingMedia.connect(asker.incomingMedia[sender.id], err => {
            if (err) {
                return callback(err);
            }
            callback(null, asker.incomingMedia[sender.id]);
        });
    } else {
        myRoom.pipeline.create('WebRtcEndpoint', (err, incoming) => {
            incoming.setMaxVideoSendBandwidth(100);
            if (err) {
                return callback(err);
            }

            asker.incomingMedia[sender.id] = incoming;

            let iceCandidateQueue = iceCandidateQueues[sender.id];
            if (iceCandidateQueue) {
                while (iceCandidateQueue.length) {
                    let ice = iceCandidateQueue.shift();
                    console.error(`user: ${sender.name} collect candidate for outgoing media`);
                    incoming.addIceCandidate(ice.candidate);
                }
            }

            incoming.on('OnIceCandidate', event => {
                let candidate = kurento.register.complexTypes.IceCandidate(event.candidate);
                console.log("CANDIDATE: ", event.candidate);
                socket.emit('message', {
                    event: 'candidate',
                    userid: sender.id,
                    candidate: candidate
                });
            });

            sender.outgoingMedia.connect(incoming, err => {
                if (err) {
                    return callback(err);
                }
                callback(null, incoming);
            });
        });
    }
}

function getKurentoClient(callback) {
    if (kurentoClient !== null) {
        return callback(null, kurentoClient);
    }

    kurento(argv.ws_uri, function (error, _kurentoClient) {
        if (error) {
            console.log("Could not find media server at address " + argv.ws_uri);
            return callback("Could not find media server at address" + argv.ws_uri
                + ". Exiting with error " + error);
        }

        kurentoClient = _kurentoClient;
        callback(null, kurentoClient);
    });
}


}

0 Answers0