4

I have an application. In this application, I cannot change the video that goes to the other party.

'use strict';

var Meeting = function (socketioHost) { 
    var exports = {};
    
    var _isInitiator = false;
    var _localStream;
    var _remoteStream;
    var _turnReady;
    var _pcConfig = {'iceServers': [{'url': 'stun:stun.l.google.com:19302'}]};
    var _constraints = {
        video: {
          width: {ideal: 320},
          height: {ideal: 240},
          frameRate: {ideal: 20}
        },
        audio: {
          googEchoCancellation: true,
          googAutoGainControl: true,
          googNoiseSuppression: true,
          googHighpassFilter: true,
          googEchoCancellation2: true,
          googAutoGainControl2: true,
          googNoiseSuppression2: true
        },
        options: {
            mirror: true
        }
    };

    if(navigator.userAgent.includes("iPhone")) {
        var _constraints =  {
            video : true
        }
    }

    var _defaultChannel;
    var _privateAnswerChannel;
    var _offerChannels = {};
    var _opc = {};
    var _apc = {};
    var _sendChannel = {};
    var _room;
    var _myID;
    var _onRemoteVideoCallback;
    var _onLocalVideoCallback;
    var _onChatMessageCallback;
    var _onChatReadyCallback;
    var _onChatNotReadyCallback;
    var _onParticipantHangupCallback;
    var _host = socketioHost;

    
    ////////////////////////////////////////////////
    // PUBLIC FUNCTIONS
    ////////////////////////////////////////////////
     /**
     *
     * Add callback function to be called when a chat message is available.
     *
     * @param name of the room to join
     */   
    function joinRoom(name) {
        _room = name;
        
        _myID = generateID();
        
        // Open up a default communication channel
        initDefaultChannel();

        if (_room !== '') {
            console.log('Create or join room', _room);
            _defaultChannel.emit('create or join', {room:_room, from:_myID});
        }

        // Open up a private communication channel
        initPrivateChannel();

        //console.log(_devices);

        navigator.mediaDevices.getUserMedia(_constraints)
                                .then(handleUserMedia)
                                .catch(handleUserMediaError);

        window.onbeforeunload = function(e){
            _defaultChannel.emit('message',{type: 'bye', from:_myID});
        }
    }
    
    
    /**
     *
     * Send a chat message to all channels.
     *
     * @param message String message to be send
     */
    function sendChatMessage(message) {
        console.log("Sending "+message)
        for (var channel in _sendChannel) {
            if (_sendChannel.hasOwnProperty(channel)) {
                _sendChannel[channel].send(message);
            }
        }
    }
    
    /**
     *
     * Toggle microphone availability.
     *
     */
    function toggleMic() {
        var tracks = _localStream.getTracks();
        for (var i = 0; i < tracks.length; i++) {
            if (tracks[i].kind=="audio") {
                tracks[i].enabled = !tracks[i].enabled; 
            }
        }
    }
    
    
    /**
     *
     * Toggle video availability.
     *
     */
    function toggleVideo() {
        var tracks = _localStream.getTracks();
        for (var i = 0; i < tracks.length; i++) {
            if (tracks[i].kind=="video") {
                tracks[i].enabled = !tracks[i].enabled; 
            }
        }
    }
    
    /**
     *
     * Add callback function to be called when remote video is available.
     *
     * @param callback of type function(stream, participantID)
     */
    function onRemoteVideo(callback) {
        _onRemoteVideoCallback = callback;
    }
    
    /**
     *
     * Add callback function to be called when local video is available.
     *
     * @param callback function of type function(stream)
     */
    function onLocalVideo(callback) {
        _onLocalVideoCallback = callback;
    }
    
    /**
     *
     * Add callback function to be called when chat is available.
     *
     * @parama callback function of type function()
     */
    function onChatReady(callback) {
        _onChatReadyCallback = callback;
    }
    
    /**
     *
     * Add callback function to be called when chat is no more available.
     *
     * @parama callback function of type function()
     */
    function onChatNotReady(callback) {
        _onChatNotReadyCallback = callback;
    }
    
    /**
     *
     * Add callback function to be called when a chat message is available.
     *
     * @parama callback function of type function(message)
     */
    function onChatMessage(callback) {
        _onChatMessageCallback = callback;
    }
    
    /**
     *
     * Add callback function to be called when a a participant left the conference.
     *
     * @parama callback function of type function(participantID)
     */
    function onParticipantHangup(callback) {
        _onParticipantHangupCallback = callback;
    }
    
    ////////////////////////////////////////////////
    // INIT FUNCTIONS
    ////////////////////////////////////////////////
    
    function initDefaultChannel() {
        _defaultChannel = openSignalingChannel('');
        
        _defaultChannel.on('created', function (room){
          console.log('Created room ' + room);
          _isInitiator = true;
        });

        _defaultChannel.on('join', function (room){
            console.log('Another peer made a request to join room ' + room);
        });

        _defaultChannel.on('joined', function (room){
            console.log('This peer has joined room ' + room);
        });
        
        _defaultChannel.on('message', function (message){
            console.log('Client received message:', message);
            if (message.type === 'newparticipant') {
                var partID = message.from;
                
                // Open a new communication channel to the new participant
                _offerChannels[partID] = openSignalingChannel(partID);

                // Wait for answers (to offers) from the new participant
                _offerChannels[partID].on('message', function (msg){
                    if (msg.dest===_myID) {
                        if (msg.type === 'answer') {
                            _opc[msg.from].setRemoteDescription(new RTCSessionDescription(msg.snDescription))
                                            .then(setRemoteDescriptionSuccess)
                                            .catch(setRemoteDescriptionError);
                        } else if (msg.type === 'candidate') {
                            var candidate = new RTCIceCandidate({sdpMLineIndex: msg.label, candidate: msg.candidate});
                            console.log('got ice candidate from '+msg.from);
                            _opc[msg.from].addIceCandidate(candidate, addIceCandidateSuccess, addIceCandidateError);
                        }
                    }
                });

                // Send an offer to the new participant
                createOffer(partID);

            } else if (message.type === 'bye') {
                hangup(message.from);
            }   else if(message.type === 'change') {
                $('#' + message.from).remove();
                if(_myID !== message.from) {
                    createOffer(message.from);
                }

            }
        });
    }
      
    function initPrivateChannel() {
        // Open a private channel (namespace = _myID) to receive offers
        _privateAnswerChannel = openSignalingChannel(_myID);

        // Wait for offers or ice candidates
        _privateAnswerChannel.on('message', function (message){
            if (message.dest===_myID) {
                if(message.type === 'offer') {
                    var to = message.from;
                    createAnswer(message, _privateAnswerChannel, to);
                } else if (message.type === 'candidate') {
                    var candidate = new RTCIceCandidate({sdpMLineIndex: message.label, candidate: message.candidate});
                    _apc[message.from].addIceCandidate(candidate, addIceCandidateSuccess, addIceCandidateError);
                }
            }
        });
    }
    
    function requestTurn(turn_url) {
        var turnExists = false;
        for (var i in _pcConfig.iceServers) {
            if (_pcConfig.iceServers[i].url.substr(0, 5) === 'turn:') {
                turnExists = true;
                _turnReady = true;
                break;
            }
        }

        if (!turnExists) {
            console.log('Getting TURN server from ', turn_url);
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function(){
                if (xhr.readyState === 4 && xhr.status === 200) {
                    var turnServer = JSON.parse(xhr.responseText);
                     console.log('Got TURN server: ', turnServer);
                    _pcConfig.iceServers.push({
                        'url': 'turn:' + turnServer.username + '@' + turnServer.turn,
                        'credential': turnServer.password
                    });
                    _turnReady = true;
                }
            }
            xhr.open('GET', turn_url, true);
            xhr.send();
        }
    }

    
    ///////////////////////////////////////////
    // UTIL FUNCTIONS
    ///////////////////////////////////////////
    
    /**
     *
     * Call the registered _onRemoteVideoCallback
     *
     */
    function addRemoteVideo(stream, from) {
        // call the callback
        _onRemoteVideoCallback(stream, from);
    }


    /**
     *
     * Generates a random ID.
     *
     * @return a random ID
     */
    function generateID() {
        var s4 = function() {
            return Math.floor(Math.random() * 0x10000).toString(16);
        };
        return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4();
    }

    
    ////////////////////////////////////////////////
    // COMMUNICATION FUNCTIONS
    ////////////////////////////////////////////////
    
    /**
     *
     * Connect to the server and open a signal channel using channel as the channel's name.
     *
     * @return the socket
     */
    function openSignalingChannel(channel) {
        var namespace = _host + '/' + channel;
        var sckt = io.connect(namespace);
        return sckt;
    }

    function logout(from) {
        hangup(from)
        window.dispatchEvent(new Event('beforeunload'))
    }

    /**
     *
     * Send an offer to peer with id participantId
     *
     * @param participantId the participant's unique ID we want to send an offer
     */
    function createOffer(participantId) {
        console.log('Creating offer for peer '+participantId);

        _opc[participantId] = new RTCPeerConnection(_pcConfig);
        _opc[participantId].onicecandidate = handleIceCandidateAnswerWrapper(_offerChannels[participantId], participantId);
        _opc[participantId].onaddstream = handleRemoteStreamAdded(participantId);
        _opc[participantId].onremovestream = handleRemoteStreamRemoved; 
        _opc[participantId].addStream(_localStream);

        try {
            // Reliable Data Channels not yet supported in Chrome
            _sendChannel[participantId] = _opc[participantId].createDataChannel("sendDataChannel", {reliable: false});
            _sendChannel[participantId].onmessage = handleMessage;
            //console.log('Created send data channel');
        } catch (e) {
            alert('Failed to create data channel. ' + 'You need Chrome M25 or later with RtpDataChannel enabled');
            //console.log('createDataChannel() failed with exception: ' + e.message);
        }
        _sendChannel[participantId].onopen = handleSendChannelStateChange(participantId);
        _sendChannel[participantId].onclose = handleSendChannelStateChange(participantId);

        var onSuccess = function(participantId) {
            return function(sessionDescription) {
                var channel = _offerChannels[participantId];

                // Set Opus as the preferred codec in SDP if Opus is present.
                sessionDescription.sdp = preferOpus(sessionDescription.sdp);

                _opc[participantId].setLocalDescription(sessionDescription);  
                console.log('Sending offer to channel '+ channel.name);
                channel.emit('message', {snDescription: sessionDescription, from:_myID, type:'offer', dest:participantId});        
            }
        }

        _opc[participantId].createOffer(onSuccess(participantId), handleCreateOfferError);
    }

    function createAnswer(sdp, cnl, to) {
        _apc[to] = new RTCPeerConnection(_pcConfig);
        _apc[to].onicecandidate = handleIceCandidateAnswerWrapper(cnl, to);
        _apc[to].onaddstream = handleRemoteStreamAdded(to);
        _apc[to].onremovestream = handleRemoteStreamRemoved;
        _apc[to].addStream(_localStream);
        _apc[to].setRemoteDescription(new RTCSessionDescription(sdp.snDescription))
                .then(setRemoteDescriptionSuccess)
                .catch(setRemoteDescriptionError);

        _apc[to].ondatachannel = gotReceiveChannel(to);
        
        var onSuccess = function(channel) {
            return function(sessionDescription) {
                // Set Opus as the preferred codec in SDP if Opus is present.
                sessionDescription.sdp = preferOpus(sessionDescription.sdp);

                _apc[to].setLocalDescription(sessionDescription); 
                console.log('Sending answer to channel '+ channel.name);
                channel.emit('message', {snDescription:sessionDescription, from:_myID,  type:'answer', dest:to});
            }
        }

        _apc[to].createAnswer(onSuccess(cnl), handleCreateAnswerError);
    }

    function hangup(from) {
        console.log('Bye received from '+ from);

            if (_opc.hasOwnProperty(from)) {
                _opc[from].close();
                _opc[from] = null;  
            }
            
            if (_apc.hasOwnProperty(from)) {
                _apc[from].close();
                _apc[from] = null;
            }

            _onParticipantHangupCallback(from);
    }


    ////////////////////////////////////////////////
    // HANDLERS
    ////////////////////////////////////////////////
    
    // SUCCESS HANDLERS

    function handleUserMedia(stream) {
        console.log('Adding local stream');
        _onLocalVideoCallback(stream);
        _localStream = stream;
        _defaultChannel.emit('message', {type:'newparticipant', from: _myID});
    }

    function changeHandleUserMedia(stream) {
        _onLocalVideoCallback(stream);
        _localStream = stream;

        _defaultChannel.emit('message', {type:'change', from: _myID});
    }

    function handleRemoteStreamRemoved(event) {
        console.log('Remote stream removed. Event: ', event);
    }

    function handleRemoteStreamAdded(from) {
        return function(event) {
            //console.log('Remote stream added');
            addRemoteVideo(event.stream, from);
            _remoteStream = event.stream;
        }
    }

    function handleIceCandidateAnswerWrapper(channel, to) {
        return function handleIceCandidate(event) {
            console.log('handleIceCandidate event');
            if (event.candidate) {
                channel.emit('message',
                        {type: 'candidate',
                        label: event.candidate.sdpMLineIndex,
                        id: event.candidate.sdpMid,
                        candidate: event.candidate.candidate,
                        from: _myID, 
                        dest:to}
                    );

            } else {
                console.log('End of candidates.');
            }
        }
    }

    function setLocalDescriptionSuccess() {}

    function setRemoteDescriptionSuccess() {}

    function addIceCandidateSuccess() {}

    function gotReceiveChannel(id) {
        return function(event) {
            console.log('Receive Channel Callback');
            _sendChannel[id] = event.channel;
            _sendChannel[id].onmessage = handleMessage;
            _sendChannel[id].onopen = handleReceiveChannelStateChange(id);
            _sendChannel[id].onclose = handleReceiveChannelStateChange(id);
        }
    }
    
    function handleMessage(event) {
        console.log('Received message: ' + event.data);
        _onChatMessageCallback(event.data);
    }
    
    function handleSendChannelStateChange(participantId) {
        return function() {
            var readyState = _sendChannel[participantId].readyState;
            console.log('Send channel state is: ' + readyState);
            
            // check if we have at least one open channel before we set hat ready to false.
            var open = checkIfOpenChannel();
            enableMessageInterface(open);
        }
    }
    
    function handleReceiveChannelStateChange(participantId) {
        return function() {
            var readyState = _sendChannel[participantId].readyState;            
            // check if we have at least one open channel before we set hat ready to false.
            var open = checkIfOpenChannel();
            enableMessageInterface(open);
        }
    }
    
    function checkIfOpenChannel() {
        var open = false;
        for (var channel in _sendChannel) {
            if (_sendChannel.hasOwnProperty(channel)) {
                open = (_sendChannel[channel].readyState == "open");
                if (open == true) {
                    break;
                }
            }
        }
        
        return open;
    }
    
    function enableMessageInterface(shouldEnable) {
        if (shouldEnable) {
            _onChatReadyCallback();
        } else {
            _onChatNotReadyCallback();
        }
    }
    
    // ERROR HANDLERS
    
    function handleCreateOfferError(event){
        console.log('createOffer() error: ', event);
    }

    function handleCreateAnswerError(event){
        console.log('createAnswer() error: ', event);
    }

    function handleUserMediaError(error){
        console.log('getUserMedia error: ', error);
    }

    function setLocalDescriptionError(error) {
        console.log('setLocalDescription error: ', error);
    }

    function setRemoteDescriptionError(error) {
        console.log('setRemoteDescription error: ', error);
    }

    function addIceCandidateError(error) {}
    
    
    ////////////////////////////////////////////////
    // CODEC
    ////////////////////////////////////////////////

    // Set Opus as the default audio codec if it's present.
    function preferOpus(sdp) {
      var sdpLines = sdp.split('\r\n');
      var mLineIndex;
      // Search for m line.
      for (var i = 0; i < sdpLines.length; i++) {
          if (sdpLines[i].search('m=audio') !== -1) {
            mLineIndex = i;
            break;
          }
      }
      if (mLineIndex === null || mLineIndex === undefined) {
        return sdp;
      }

      // If Opus is available, set it as the default in m line.
      for (i = 0; i < sdpLines.length; i++) {
        if (sdpLines[i].search('opus/48000') !== -1) {
          var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
          if (opusPayload) {
            sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload);
          }
          break;
        }
      }

      // Remove CN in m line and sdp.
      sdpLines = removeCN(sdpLines, mLineIndex);

      sdp = sdpLines.join('\r\n');
      return sdp;
    }

    function extractSdp(sdpLine, pattern) {
      var result = sdpLine.match(pattern);
      return result && result.length === 2 ? result[1] : null;
    }

    // Set the selected codec to the first in m line.
    function setDefaultCodec(mLine, payload) {
      var elements = mLine.split(' ');
      var newLine = [];
      var index = 0;
      for (var i = 0; i < elements.length; i++) {
        if (index === 3) { // Format of media starts from the fourth.
          newLine[index++] = payload; // Put target payload to the first.
        }
        if (elements[i] !== payload) {
          newLine[index++] = elements[i];
        }
      }
      return newLine.join(' ');
    }

    // Strip CN from sdp before CN constraints is ready.
    function removeCN(sdpLines, mLineIndex) {
      var mLineElements = sdpLines[mLineIndex].split(' ');
      // Scan from end for the convenience of removing an item.
      for (var i = sdpLines.length-1; i >= 0; i--) {
        var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
        if (payload) {
          var cnPos = mLineElements.indexOf(payload);
          if (cnPos !== -1) {
            // Remove CN payload from m line.
            mLineElements.splice(cnPos, 1);
          }
          // Remove CN line in sdp
          sdpLines.splice(i, 1);
        }
      }

      sdpLines[mLineIndex] = mLineElements.join(' ');
      return sdpLines;
    }
    

    ////////////////////////////////////////////////
    // EXPORT PUBLIC FUNCTIONS
    ////////////////////////////////////////////////
    
    exports.joinRoom            =       joinRoom;
    exports.toggleMic           =       toggleMic;
    exports.toggleVideo         =       toggleVideo;
    exports.onLocalVideo        =       onLocalVideo;
    exports.onRemoteVideo       =       onRemoteVideo;
    exports.onChatReady         =       onChatReady;
    exports.onChatNotReady      =       onChatNotReady;
    exports.onChatMessage       =       onChatMessage;
    exports.sendChatMessage     =       sendChatMessage;
    exports.onParticipantHangup =       onParticipantHangup;
    exports.changeHandleUserMedia =       changeHandleUserMedia;
    exports.logout              =       logout;
    exports.opc                 =       _opc;
    exports.apc                 =       _apc;
    return exports;
    
};


i am providing my links from here and it works very well. Can you give an example of how I can do this?

$( document ).ready(function() {
    /////////////////////////////////
    // CREATE MEETING
    /////////////////////////////////
    meeting = new Meeting(host);
    
    meeting.onLocalVideo(function(stream) {
            //alert(stream.getVideoTracks().length);
            document.querySelector('#localVideo').srcObject = stream;

            
            $("#micMenu").on("click",function callback(e) {
                $(this).toggleText("mic_off", "mic");
                meeting.toggleMic();
            });
            
            $("#videoMenu").on("click",function callback(e) {
                $(this).toggleText("videocam_off", "videocam");
                meeting.toggleVideo();
            });

            $("#speakerMenu").on("click", function callback(e) {
                $(this).toggleText("volume_off", "volume_up");
                $("#localVideo").prop('muted', true);
            });

            $('#chatMenu').on('click', function callback(e) {
                $(this).toggleText('speaker_notes_off', 'chat');
            });

            $('#close').on('click', function callback(e) {
                meeting.logout($('.videoWrap').eq(1).attr('id'));
            });


            $(document).on('change', '#videoInput', function callback(e) {

                var mediaParams = {
                    video: {mandatory: {sourceId: $(this).val()}}
                };

                navigator.mediaDevices.getUserMedia(mediaParams)
                    .then(function(stream){
                    meeting.handleUserMedia(stream);

                })
                .catch(function(e) { });
            });

        }
    );
    
    meeting.onRemoteVideo(function(stream, participantID) {
            addRemoteVideo(stream, participantID);  
        }
    );
    
    meeting.onParticipantHangup(function(participantID) {
            // Someone just left the meeting. Remove the participants video
            removeRemoteVideo(participantID);
        }
    );
    
    meeting.onChatReady(function() {
            console.log("Chat is ready");
        }
    );

    meeting.onChatNotReady(function() {
            console.log("Chat is not ready");
        }
    );
    
    var room = window.location.pathname.match(/([^\/]*)\/*$/)[1];
    meeting.joinRoom(room);

}); // end of document.ready

obviously i am changing the local video. but I can't change it for other users.

Özgür Can Karagöz
  • 1,039
  • 1
  • 13
  • 32

2 Answers2

0

In my practice, both local and remote video can be viewed,my code is as follows package.json:

{
  "name": "peer to peer",
  "version": "1.0.0",
  "description": "point to point by webrtc & websocket",
  "main": "index.js",
  "scripts": {
    "dev": "node index.js"
  },
  "author": "webrtc",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "express-ws": "^4.0.0"
  }
}

index.js:

const app = require('express')();
const wsInstance = require('express-ws')(app);

app.ws('/', ws => {
    ws.on('message', data => {
        // post cast
        wsInstance.getWss().clients.forEach(server => {
            if (server !== ws) {
                server.send(data);
            }
        });
    });
});

app.get('/', (req, res) => {
    res.sendFile('./client/index.html', { root: __dirname });
});

app.get('/p2p', (req, res) => {
    res.sendFile('./client/p2p.html', { root: __dirname });
});

app.listen(8091);

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>p2p webrtc</title>
    <style>
    .container {
        width: 250px;
        margin: 100px auto;
        padding: 10px 30px;
        border-radius: 4px;
    border: 1px solid #ebeef5;
    box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
    color: #303133;
    }
    </style>
</head>
<body>
    <div class="container">
        <p>seheme:</p>
        <ul>
            <li>open<a href="/p2p?type=answer" target="_blank">answer</a>;</li>
            <li>open<a href="/p2p?type=offer" target="_blank">offer</a>;</li>
            <li> comfirm connected ws ;</li>
            <li> offer send 'start' button;</li>
        </ul>
    </div>
</body>
</html>

p2p.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
    <style>
        * {
            padding: 0;
            margin: 0;
            box-sizing: border-box;
        }
        .container {
            width: 100%;
            display: flex;
            display: -webkit-flex;
            justify-content: space-around;
            padding-top: 20px;
        }
        .video-box {
            position: relative;
            width: 800px;
            height: 400px;
        }
        #remote-video {
            width: 100%;
            height: 100%;
            display: block;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #F2F6FC;
        }
        #local-video {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 240px;
            height: 120px;
            object-fit: cover;
            border: 1px solid #eee;
            background-color: #EBEEF5;
        }
        .start-button {
            position: absolute;
            left: 50%;
            top: 50%;
            width: 100px;
            display: none;
            line-height: 40px;
            outline: none;
            color: #fff;
            background-color: #409eff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transform: translate(-50%, -50%);
        }
        .logger {
            width: 40%;
            padding: 14px;
            line-height: 1.5;
            color: #4fbf40;
            border-radius: 6px;
            background-color: #272727;
        }
        .logger .error {
            color: #DD4A68;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="video-box">
            <video id="remote-video"></video>
            <video id="local-video" muted></video>
            <button class="start-button" onclick="startLive()">start</button>
        </div>
        <div class="logger"></div>
    </div>
    <script>
        const message = {
            el: document.querySelector('.logger'),
            log (msg) {
                this.el.innerHTML += `<span>${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
            },
            error (msg) {
                this.el.innerHTML += `<span class="error">${new Date().toLocaleTimeString()}:${msg}</span><br/>`;
            }
        };
        
        const target = location.search.slice(6);
        const localVideo = document.querySelector('#local-video');
        const remoteVideo = document.querySelector('#remote-video');
        const button = document.querySelector('.start-button');

        localVideo.onloadeddata = () => {
            message.log('play local video');
            localVideo.play();
        }
        remoteVideo.onloadeddata = () => {
            message.log('play remote video');
            remoteVideo.play();
        }

        document.title = target === 'offer' ? 'offer' : 'answer';

        message.log('Multi-hole channel (WebSocket) is being created...');
        const socket = new WebSocket('ws://localhost:8091');
        socket.onopen = () => {
            message.log('The signaling channel is successfully created!');
            target === 'offer' && (button.style.display = 'block');
        }
        socket.onerror = () => message.error('Failed to create signaling channel!');
        socket.onmessage = e => {
            const { type, sdp, iceCandidate } = JSON.parse(e.data)
            if (type === 'answer') {
                peer.setRemoteDescription(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'answer_ice') {
                peer.addIceCandidate(iceCandidate);
            } else if (type === 'offer') {
                startLive(new RTCSessionDescription({ type, sdp }));
            } else if (type === 'offer_ice') {
                peer.addIceCandidate(iceCandidate);
            }
        };

        const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        !PeerConnection && message.error('The browser does not support WebRTC!');
        const peer = new PeerConnection();

        peer.ontrack = e => {
            if (e && e.streams) {
                message.log('Receive the other party's audio/video stream data...');
                remoteVideo.srcObject = e.streams[0];
            }
        };

        peer.onicecandidate = e => {
            if (e.candidate) {
                message.log('Collect and send candidates');
                socket.send(JSON.stringify({
                    type: `${target}_ice`,
                    iceCandidate: e.candidate
                }));
            } else {
                message.log('The candidate collection is complete!');
            }
        };

        async function startLive (offerSdp) {
            target === 'offer' && (button.style.display = 'none');
            let stream;
            try {
                message.log('Try to call the local camera/microphone');
                stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
                message.log('The camera/microphone is successfully acquired!');
                localVideo.srcObject = stream;
            } catch {
                message.error('Camera/microphone acquisition failed!');
                return;
            }

            message.log(`------ WebRTC ${target === 'offer' ? 'offer' : 'answer'}seheme ------`);
            message.log('Add a media track to the track set');
            stream.getTracks().forEach(track => {
                peer.addTrack(track, stream);
            });

            if (!offerSdp) {
                message.log('Create a local SDP');
                const offer = await peer.createOffer();
                await peer.setLocalDescription(offer);
                
                message.log(`Transmission initiator local SDP`);
                socket.send(JSON.stringify(offer));
            } else {
                message.log('SDP received from the sender');
                await peer.setRemoteDescription(offerSdp);

                message.log('Create receiver (answer) SDP');
                const answer = await peer.createAnswer();
                message.log(`Transmission receiver (response) SDP`);
                socket.send(JSON.stringify(answer));
                await peer.setLocalDescription(answer);
            }
        }
    </script>
</body>
</html>

Use code:

npm install                 //npm install dependency

npm run dev                 //open localhost:8091

Hope it can help you!

M_JSL
  • 65
  • 1
  • 9
0

I believe your issue is not about switching WebRTC stream. Having you stated that you can switch the local camera and being a task you find answered elsewhere Like here

This is why I believe the issue is about design: To change Media Device you need a new stream. To get a new stream you need to prompt the user with getUserMedia()

So you have two solutions:

  • either you request the user two (or more) video inputs and keep all of them. Then switch them upon receiving the remote event.
  • or do something similar. Prompting the user, for the new video device, just when you receive the event.

Bear in mind that some browsers, do not allow to programmatically do certain requests. To be precise in some cases you need direct user interaction (like a click) to be allowed to fire certain requests. This may be the case with getUserMedia() if you are not getting prompted when the function is called by the remote event.

Newbie
  • 4,462
  • 11
  • 23