Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to detect if user has camera #180

Open
wardoUtil opened this issue Mar 19, 2014 · 14 comments
Open

How to detect if user has camera #180

wardoUtil opened this issue Mar 19, 2014 · 14 comments

Comments

@wardoUtil
Copy link

I'm trying to detect if the user has camera.

The idea behind this is to make sure the user has a camera before starting my web app.

Also facing a problem on starting captureUserMedia before joining a channel, whenever I try to call it again after switching channels it never goes through onStream again.

@wardoUtil
Copy link
Author

I'm using MultiConnection.

@muaz-khan
Copy link
Owner

Remember, we can't check presence of a webcam without making a getUserMedia request.

RTCMultiConnection Docs has caniuse object; which has properties like:

// is WebRTC compatible browser?
console.log( connection.caniuse.RTCPeerConnection );

// getUserMedia API exists?
console.log( connection.caniuse.getUserMedia );

// have WebAudio support?
console.log( connection.caniuse.AudioContext );

// chrome screen capturing support
console.log( connection.caniuse.ScreenSharing );

// chrome RTP data channels support
console.log( connection.caniuse.RtpDataChannels );

// chrome SCTP data channels support
console.log( connection.caniuse.SctpDataChannels );

// to verify if flag "chrome://flags/#enable-usermedia-screen-capture" is enabled
connection.caniuse.checkIfScreenSharingFlagEnabled(function (isFlagEnabled, warning) {
    if (isFlagEnabled) {
        console.error('Multi-capturing of screen is not allowed. Capturing process is denied. Try chrome >= M31.');
    }

    if (warning) console.error(warning);

    else if (!isFlagEnabled) {
        console.error('It seems that "Enable screen capture support in getUserMedia" flag is not enabled.');
    }
});

captureUserMedia invokes getUserMedia API once for a stream to be captured; then it stores MediaStream in currentUserMediaRequest.streams object. Which keeps media stream until it is manually stopped by the user.

Subsequent captureUserMedia requests checks presence of media in currentUserMediaRequest.streams object; and if exists, then it skips to fire onstream event.

The EVENT onstream is fired for first invocation only. However, it is possible to "manually" fire for second or 3rd invocations:

var session = {
    audio: true
};

connection.captureUserMedia(function (mediaStream) {

    // if it is second invocation
    // pass "streamObject" over "onstream"
    var streamObject = connection.streams[mediaStream.streamid].streamObject;
    connection.onstream(streamObject);

}, session);

If previously you captured only audio; and now you captured both audio and video; then onstream will be fired again for new media i.e. audio+video. If you try to capture screen; then onstream will be fired again for screen media.

We can't fire onstream multiple times for same media capturing. Otherwise, you'll see multiple-self videos while connecting with multiple users in a room.

@wardoUtil
Copy link
Author

Hey muaz-khan,

Thanks for the help, you fixed my problem of detecting whether the user has webcam or not.

The app seems to be working fine now, but there seems to be quite some lag (then again, I'm testing this on my old laptop).

Could you please look at the code bellow and see if I'm making any big mystakes?

<script> //Initialize RTCMultiConnection var connection = new RTCMultiConnection(); var socket; connection.session = { audio: true, video: false }; connection.openSignalingChannel = function (config) { var SIGNALING_SERVER = 'http://192.168.1.67:8888/'; var channel = config.channel || this.channel; var sender = Math.round(Math.random() * 60535) + 5000; socket = io.connect(SIGNALING_SERVER); socket.channel = channel; socket.on('connect', function () { if (config.callback) config.callback(socket); }); socket.on('test', function (message) { console.log("DIDIT"); }); //On signaling server finding peers socket.on('conference', function (session) { connection = new RTCMultiConnection(session.channel); connection.userid = session.userID; connection.session = { audio: false, video: true }; connection.onCustomMessage = function (message) { console.log(message); }; //On new stream connection.onstream = function (stream) { if (stream.type === 'local') { var video = getVideo(stream.mediaElement); document.body.appendChild(video); } if (stream.type === 'remote') { var video = getVideo(stream.mediaElement, stream.extra); document.body.appendChild(video); } }; function getVideo(video, extra) { var div = document.createElement('div'); div.className = 'video-container'; div.appendChild(video); if (extra) { var h2 = document.createElement('h2'); h2.innerHTML = extra.username; div.appendChild(h2); } return div; }; //On host creating the session connection.onNewSession = function (session) { connection.join(session); }; //Connecting to the channel received by signaling server connection.connect(); //If host, then open the channel if (session.host == true) { connection.open(); } }); }; //Connect to the signaling server connection.connect(); connection.captureUserMedia(function (mediaStream) { mediaStream.stop(); }); //Tell server the client is ready to find peers $("#init").click(function () { socket.emit('start'); }); </script>

Thank you so much.

@wardoUtil
Copy link
Author

//Initialize RTCMultiConnection
var connection = new RTCMultiConnection();
var socket;
connection.session = {
    audio: true,
    video: false
};

connection.openSignalingChannel = function (config) {
    var SIGNALING_SERVER = 'http://192.168.1.67:8888/';
    var channel = config.channel || this.channel;
    var sender = Math.round(Math.random() * 60535) + 5000;

    socket = io.connect(SIGNALING_SERVER);
    socket.channel = channel;
    socket.on('connect', function () {
        if (config.callback) config.callback(socket);
    });
    socket.on('test', function (message) {
        console.log("DIDIT");

    });

    //On signaling server finding peers
    socket.on('conference', function (session)
    {
        connection = new RTCMultiConnection(session.channel);
        connection.userid = session.userID;

        connection.session = {
            audio: false,
            video: true
        };

        connection.onCustomMessage = function (message) {
            console.log(message);
        };

        //On new stream
        connection.onstream = function (stream) {             
            if (stream.type === 'local') {
                var video = getVideo(stream.mediaElement);
                document.body.appendChild(video);
            }

            if (stream.type === 'remote') {
                var video = getVideo(stream.mediaElement, stream.extra);
                document.body.appendChild(video);
            }
        };

        function getVideo(video, extra) {
            var div = document.createElement('div');
            div.className = 'video-container';

            div.appendChild(video);

            if (extra) {
                var h2 = document.createElement('h2');
                h2.innerHTML = extra.username;
                div.appendChild(h2);
            }
            return div;
        };

        //On host creating the session
        connection.onNewSession = function (session) {
            connection.join(session);
        };

        //Connecting to the channel received by signaling server
        connection.connect();

        //If host, then open the channel
        if (session.host == true) {
            connection.open();

        }
    });   
};

//Connect to the signaling server
connection.connect();

connection.captureUserMedia(function (mediaStream) {
    mediaStream.stop();
});

  //Tell server the client is ready to find peers
  $("#init").click(function () {
      socket.emit('start');
  });

@muaz-khan
Copy link
Owner

Unfortunately I'm unable to understand the logic behind your code. If you're planning to get full control over signaling or you wanted to use existing 3rd party implementations like SingalMaster or PeerServer then simply understand how openSignalingChannel method works.

captureUserMedia is useful in scenarios when you want to manually capture media streams and override default capturing behaviors by setting dontAttachStream=true.

aButton.onclick = function () {
    connection.dontAttackStream = false;
    connection.captureUserMedia(function (stream) {
        connection.dontAttackStream = true;
        someButton.disabled = true;
    });
};

anotherButton.onclick = function() {
    connection.open();
};

var room;
connection.onNewSession = function(session) {
     if(!room) room = session;
};

btnJoinRoom.onclick = function() {
     if(room) room.join();
};

captureUserMedia auto pushes media streams to attachStreams array; which is used to add media stream(s) to peer connection(s).

dontAttachStream can be used to prevent capturing of all media streams. It MUST be set true before calling open or join method (according to application needs).

@wardoUtil
Copy link
Author

Yes, my plan is to fully control the channels and conversations by signalling.

The idea is to have clients joining a queue, while the server connects 8 peers into each conversation.

Did I mess up openSignallingChannel?
Is creating a new RTCMultiConnection object unacceptable? (I did it because I was having trouble changing channels).

Should I use captureUserMedia with dontAttachStreams = true for testing the webcam, and then attach the stream later?

I had this in mind, but the problem is that it seems that if I do this, when the user joins the conference the client didn't go through onStream for his peers.

@wardoUtil
Copy link
Author

Ok, I think the peer's stream not attaching was just a bug in my code, the code is now working with only 1 captureUserMedia, as you advised.

Still wondering about the signalling part though, am I doing it wrong?

@muaz-khan
Copy link
Owner

You should open single socket; as explained here. You can set default channel like this:

connection.channel = 'default-channel';

This is the same thing passed over the constructor.

First Step: Initialize a global array-like object

This array-like object will store onmessage callbacks.

var onMessageCallbacks = {};

Second Step: Initialize Signaling Server

var websocket = new WebSocket('wss://something:port/');
var socket = io.connect('https://domain:port/');
var firebase = new Firebase('https://user.firebaseio.com/' + connection.channel);

For socket.io; you can pass default channel as URL parameter:

var socket = io.connect('https://domain:port/?channel=' + connection.channel);

3rd Step: Subscribe to server messages

Capture server messages:

websocket.onmessage = function (event) {
    onMessageCallBack(event.data);
};

socket.on('message', function (data) {
    onMessageCallBack(data);
});

firebase.on('child_added', function (snap) {
    onMessageCallBack(snap.val());
    snap.ref().remove(); // for socket.io live behaviour
});

and onMessageCallBack:

function onMessageCallBack(data) {
    data = JSON.parse(e.data);

    if (data.sender == connection.userid) return;

    if (onMessageCallbacks[data.channel]) {
        onMessageCallbacks[data.channel](data.message);
    };
}

4th and final Step: Override openSignalingChannel method

connection.openSignalingChannel = function (config) {
    var channel = config.channel || this.channel;
    onMessageCallbacks[channel] = config.onmessage;

    if (config.onopen) setTimeout(config.onopen, 1000);
    return {
        send: function (message) {
            websocket.send(JSON.stringify({
                sender: connection.userid,
                channel: channel,
                message: message
            }));
        },
        channel: channel
    };
};

You can see that you've single websocket/socketio/firebase connection; where single default-channel is used and can easily be managed via signaling server.

I usually pass both channel-id and user-id via socket.io URL parameters:

var socket = io.connect('https://domain:port/?channel=' + connection.channel + '&userid=' + connection.userid);

See MultiRTC example:

var rooms = {};

io.sockets.on('connection', function (socket) {
    // data passed via query-string parameters
    var room = socket.handshake.query.room;
    var userid = socket.handshake.query.userid;

    var isRoomExists = !! rooms[room];

    // emit to tell browser room exists!
    socket.emit('room-found', isRoomExists);

    // store in an array
    if (!isRoomExists) {
        rooms[room] = {
            room: room,
            users: [userid]
        };
    } else rooms[room].users.push(userid);


    socket.on('message', function (data) {
        socket.broadcast.emit('message', data);
    });

    // capture "disconnect" event
    socket.on('disconnect', function () {
        // emit "user-left" event and pass channel-id as well as userid!
        socket.broadcast.emit('user-left', {
            userid: userid,
            room: room
        });
    });
});

See how browser passed roomid and userid:

var signaling_url = '/?userid=' + rtcMultiConnection.userid + '&room=' + location.href.split('/').pop();
socket = io.connect(signaling_url);

... and how user-left event is captured!

@wardoUtil
Copy link
Author

I see what you are saying, but if do everything on the default channel, having in mind that my signalling server must select 8 users from the queue and connect them togheter in the same room, how can I make make these 8 users join the same session?

My current implementation was making use of onNewSession, the idea was to connect the 8 users to a different room, make the host create the session and then have all the users join it via onNewSession.

I guess there is an easier way to join the session, I only did it this way because I was having trouble joining sessions outside of onNewSession.

Is there a way to send the session object via the signalling server? or can I join the session via session id somehow? (When I pass session-id to join I get an error).

@wardoUtil
Copy link
Author

Hey again,
Sorry for being such a pain ahahah.
It's my first time using both WebRTC and Node servers.

I fixed most problems you pointed out, the only current problem is that the code only works if I dont override openSignallingChannel, which makes me question what it does. (I'm able to communicate with node, to change channels and to connect with peers on the same session).

My code is bellow.

var connection = new RTCMultiConnection('default');
connection.session = {
audio: false,
video: true,
};

var SIGNALING_SERVER = 'http://192.168.1.67:8888/';
var socket = io.connect(SIGNALING_SERVER);

socket.on('conference', function (session) {

    connection.channel = session.channel;
    connection.userid = session.userID;

    //On host creating the session
    connection.onNewSession = function (session) {
        connection.join(session);
    };

    //Connecting to the channel received by signaling server
    connection.connect();

    //If host, then open the channel
    if (session.host == true) {
        connection.open();
    }
});

connection.captureUserMedia(function (mediaStream) {
}, connection.session);

  //Tell server the client is ready to find peers
  $("#init").click(function () {
      socket.emit('start');
  });

@muaz-khan
Copy link
Owner

You can use default socket like this:

defaultSocket.on('message', function (data) {
    onMessageCallBack(data);
});

function onMessageCallBack(data) {
    if (data.userid == connection.userid) return;

    if (data.RTCMultiConnectionPrivateMessage && onMessageCallbacks[data.channel]) {
        onMessageCallbacks[data.channel](data.message);
    };

    // here you can add your custom code
    if (!data.RTCMultiConnectionPrivateMessage) {
        yourCustomCallBack(data);
    }
}

connection.openSignalingChannel = function (config) {
    var channel = config.channel || this.channel;
    onMessageCallbacks[channel] = config.onmessage;

    if (config.onopen) setTimeout(config.onopen, 1000);
    return {
        send: function (message) {
            defaultSocket.emit('message', {
                userid: connection.userid,
                channel: channel,
                message: message,
                RTCMultiConnectionPrivateMessage: true // ------------- see this line!
            });
        },
        channel: channel
    };
};

// and here you can initialize "yourCustomCallBack"

function yourCustomCallBack(data) {
    if (data.newRoomFound) {
        connection.join(data.roomDetails);
    }

    if (data.whoisthere) {
        defaultSocket.emit('message', {
            userid: connection.userid, // sender userid
            iamhere: true // boolean
        });
    }

    if (connection.iamhere) {
        defaultSocket.emit('message', {
            userid: connection.userid, // sender userid
            canyouchat: true // boolean
        });
    }

    if (connection.canyouchat) {
        defaultSocket.emit('message', {
            userid: connection.userid, // sender userid
            yes_icanchat: true // boolean
        });
    }

    if (connection.yes_icanchat) {
        alert('wow!');
    }
}

// your button which can be clicked to open room; and emit socketio message
customButton.onclick = function () {
    var roomDetails = {
        sessionid: connection.sessionid,
        userid: connection.userid,
        session: connection.session,
        extra: connection.extra
    };

    defaultSocket.emit('message', {
        roomFound: true, // boolean
        userid: connection.userid, // message sender
        roomDetails: roomDetails // room details that can be passed over "join" method!
    });

    // www.rtcmulticonnection.org/docs/transmitRoomOnce/
    connection.transmitRoomOnce = true;
    connection.open();
};

// another button that can be clicked to check existnig users
anotherButton.onclick = function () {
    defaultSocket.emit('message', {
        userid: connection.userid, // message sender
        whoisthere: true // boolean
    });
};

You can see that you can use default socket for custom messages transmission as well!

You can even manually share "room-details" object that can be passed directly over "join" method. Don't forget calling "connect" method before invoking "join"!

Above all custom scenarios can be implemented using sendCustomMessage method! Which is easier to use and capture messages using onCustomMessage.

@wardoUtil
Copy link
Author

Thank you so much, I changed the code as you said and it's now working.
The code looks a lot cleaner since the first submission too!

Just one last question, my plan was to make it cross platform between mobile and web, and I would like to start working on an android version as soon as I finish my web version.

So far I have found a few git projects for libjingle singnalling, but have yet to find a good project with built-in socket.io support for android.

I guess it's possible to use libjingle supported projects and use a socket.io lib for android to take care of the signaling, right?

How harder would it be to work with appRTC itself instead of an abstraction, like your project?

Thank you so much for your support so far!

@muaz-khan
Copy link
Owner

It works on Chrome+Firefox+Opera both on desktop and android.

I don't know much about 3rd party services; however if your plan to support IE/Safari or native applications then you need to clone chromium code and use in your c++ applications; same as node-webkit do (behind the scene). You may like to try crosswalk to compile js for android or node-webkit-hipster-seed to compile on desktop!

@wardoUtil
Copy link
Author

Thanks again, your suggestions helped me find a solution.

Just out of curiosity, how does the built in signalling system work?
I tried connecting 2 different clients to a channel, and opening a session, without any signalling server or overwritting openSignalingServer, and it still worked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants