Review URL: http://webrtc-codereview.appspot.com/269019
git-svn-id: http://webrtc.googlecode.com/svn/trunk@989 4adac7df-926f-26a2-2b94-8c16560cd09d
diff --git a/test/functional_test/README b/test/functional_test/README
new file mode 100644
index 0000000..a855135
--- /dev/null
+++ b/test/functional_test/README
@@ -0,0 +1,41 @@
+This test client is a simple functional test for WebRTC enabled Chrome build.
+
+The following is necessary to run the test:
+- A WebRTC Chrome binary.
+- A peerconnection_server binary (make peerconnection_server).
+
+It can be used in two scenarios:
+1. Single client calling itself with the server test page
+(peerconnection/samples/server/server_test.html) in loopback mode as a fake
+client.
+2. Call between two clients.
+
+To start the test for scenario (1):
+1. Start peerconnection_server.
+2. Start the WebRTC Chrome build: $ <path_to_chome_binary>/chrome
+--enable-media-stream --enable-p2papi --user-data-dir=<path_to_data>
+<path_to_data> is where Chrome looks for all its states, use for example
+"temp/chrome_webrtc_data". If you don't always start the browser from the same
+directory, use an absolute path instead.
+3. Open the server test page, ensure loopback is enabled, choose a name (for
+example "loopback") and connect to the server.
+4. Open the test page, connect to the server, select the loopback peer, click
+call.
+
+To start the test for scenario (2):
+1. Start peerconnection_server.
+2. Start the WebRTC Chrome build, see scenario (1).
+3. Open the test page, connect to the server.
+4. On another machine, start the WebRTC Chrome build.
+5. Open the test page, connect to the server, select the other peer, click call.
+
+Note 1: There is currently a limitation so that the camera device can only be
+accessed once, even if in the same browser instance. Hence the need to use two
+machines for scenario (2).
+
+Note 2: The web page must normally be on a web server to be able to access the
+camera for security reasons.
+See http://blog.chromium.org/2008/12/security-in-depth-local-web-pages.html
+for more details on this topic. This can be overridden with the flag
+--allow-file-access-from-files, in which case running it over the file://
+URI scheme works.
diff --git a/test/functional_test/webrtc_test.html b/test/functional_test/webrtc_test.html
new file mode 100644
index 0000000..e2d8939
--- /dev/null
+++ b/test/functional_test/webrtc_test.html
@@ -0,0 +1,594 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+
+<!--
+Copyright (c) 2011 The WebRTC project authors. All Rights Reserved.
+
+Use of this source code is governed by a BSD-style license
+that can be found in the LICENSE file in the root of the source
+tree. An additional intellectual property rights grant can be found
+in the file PATENTS. All contributing project authors may
+be found in the AUTHORS file in the root of the source tree.
+-->
+
+<html>
+
+<head>
+<title>WebRTC Test</title>
+
+<style type="text/css">
+body, input, button, select, table {
+ font-family:"Lucida Grande", "Lucida Sans", Verdana, Arial, sans-serif;
+ font-size: 13 px;
+}
+body, input:enable, button:enable, select:enable, table {
+ color: rgb(51, 51, 51);
+}
+h1 {font-size: 40 px;}
+</style>
+
+<script type="text/javascript">
+
+// TODO: Catch more exceptions
+
+var server;
+var myId = -1;
+var myName;
+var remoteId = -1;
+var remoteName;
+var request = null;
+var hangingGet = null;
+var pc = null;
+var localStream = null;
+var disconnecting = false;
+var callState = 0; // 0 - Not started, 1 - Call ongoing
+
+
+// General
+
+function toggleExtraButtons() {
+ document.getElementById("createPcBtn").hidden =
+ !document.getElementById("createPcBtn").hidden;
+ document.getElementById("test1Btn").hidden =
+ !document.getElementById("test1Btn").hidden;
+}
+
+function trace(txt) {
+ var elem = document.getElementById("debug");
+ elem.innerHTML += txt + "<br>";
+}
+
+function trace_warning(txt) {
+ var wtxt = "<b>" + txt + "</b>";
+ trace(wtxt);
+}
+
+function trace_exception(e, txt) {
+ var etxt = "<b>" + txt + "</b> (" + e.name + " / " + e.message + ")";
+ trace(etxt);
+}
+
+function setCallState(state) {
+ trace("Changing call state: " + callState + " -> " + state);
+ callState = state;
+}
+
+function checkPeerConnection() {
+ if (!pc) {
+ trace_warning("No PeerConnection object exists");
+ return 0;
+ }
+ return 1;
+}
+
+
+// Local stream generation
+
+function gotStream(s) {
+ var url = webkitURL.createObjectURL(s);
+ document.getElementById("localView").src = url;
+ trace("User has granted access to local media. url = " + url);
+ localStream = s;
+}
+
+function gotStreamFailed(error) {
+ alert("Failed to get access to local media. Error code was " + error.code +
+ ".");
+ trace_warning("Failed to get access to local media. Error code was " +
+ error.code);
+}
+
+function getUserMedia() {
+ try {
+ navigator.webkitGetUserMedia("video,audio", gotStream, gotStreamFailed);
+ trace("Requested access to local media");
+ } catch (e) {
+ trace_exception(e, "getUserMedia error");
+ }
+}
+
+
+// Peer list and remote peer handling
+
+function peerExists(id) {
+ try {
+ var peerList = document.getElementById("peers");
+ for (var i = 0; i < peerList.length; i++) {
+ if (parseInt(peerList.options[i].value) == id)
+ return true;
+ }
+ } catch (e) {
+ trace_exception(e, "Error searching for peer");
+ }
+ return false;
+}
+
+function addPeer(id, pname) {
+ var peerList = document.getElementById("peers");
+ var option = document.createElement("option");
+ option.text = pname;
+ option.value = id;
+ try {
+ // For IE earlier than version 8
+ peerList.add(option, x.options[null]);
+ } catch (e) {
+ peerList.add(option, null);
+ }
+}
+
+function removePeer(id) {
+ try {
+ var peerList = document.getElementById("peers");
+ for (var i = 0; i < peerList.length; i++) {
+ if (parseInt(peerList.options[i].value) == id) {
+ peerList.remove(i);
+ break;
+ }
+ }
+ } catch (e) {
+ trace_exception(e, "Error removing peer");
+ }
+}
+
+function clearPeerList() {
+ var peerList = document.getElementById("peers");
+ while (peerList.length > 0)
+ peerList.remove(0);
+}
+
+function setSelectedPeer(id) {
+ try {
+ var peerList = document.getElementById("peers");
+ for (var i = 0; i < peerList.length; i++) {
+ if (parseInt(peerList.options[i].value) == id) {
+ peerList.options[i].selected = true;
+ return true;
+ }
+ }
+ } catch (e) {
+ trace_exception(e, "Error setting selected peer");
+ }
+ return false;
+}
+
+function getPeerName(id) {
+ try {
+ var peerList = document.getElementById("peers");
+ for (var i = 0; i < peerList.length; i++) {
+ if (parseInt(peerList.options[i].value) == id) {
+ return peerList.options[i].text;
+ }
+ }
+ } catch (e) {
+ trace_exception(e, "Error finding peer name");
+ return;
+ }
+ return;
+}
+
+function storeRemoteInfo() {
+ try {
+ var peerList = document.getElementById("peers");
+ if (peerList.selectedIndex < 0) {
+ alert("Please select a peer.");
+ return false;
+ } else
+ remoteId = parseInt(peerList.options[peerList.selectedIndex].value);
+ remoteName = peerList.options[peerList.selectedIndex].text;
+ } catch (e) {
+ trace_exception(e, "Error storing remote peer info");
+ return false;
+ }
+ return true;
+}
+
+
+// Call control
+
+function createPeerConnection() {
+ if (pc) {
+ trace_warning("PeerConnection object already exists");
+ }
+ trace("Creating PeerConnection object");
+ try {
+ pc = new webkitPeerConnection("STUN stun.l.google.com:19302",
+ onSignalingMessage);
+ pc.onaddstream = onAddStream;
+ pc.onremovestream = onRemoveStream;
+ } catch (e) {
+ trace_exception(e, "Create PeerConnection error");
+ }
+}
+
+function doCall() {
+ if (!storeRemoteInfo())
+ return;
+ document.getElementById("call").disabled = true;
+ document.getElementById("peers").disabled = true;
+ createPeerConnection();
+ trace("Adding stream");
+ pc.addStream(localStream);
+ document.getElementById("hangup").disabled = false;
+ setCallState(1);
+}
+
+function hangUp() {
+ document.getElementById("hangup").disabled = true;
+ trace("Sending BYE to " + remoteName + " (ID " + remoteId + ")");
+ sendToPeer(remoteId, "BYE");
+ closeCall();
+}
+
+function closeCall() {
+ trace("Stopping showing remote stream");
+ document.getElementById("remoteView").src = "dummy";
+ if (pc) {
+ trace("Stopping call [pc.close()]");
+ pc.close();
+ pc = null;
+ } else
+ trace("No pc object to close");
+ remoteId = -1;
+ document.getElementById("call").disabled = false;
+ document.getElementById("peers").disabled = false;
+ setCallState(0);
+}
+
+
+// PeerConnection callbacks
+
+function onAddStream(e) {
+ var stream = e.stream;
+ var url = webkitURL.createObjectURL(stream);
+ document.getElementById("remoteView").src = url;
+ trace("Started showing remote stream. url = " + url);
+}
+
+function onRemoveStream(e) {
+ // Currently if we get this callback, call has ended.
+ document.getElementById("remoteView").src = "";
+ trace("Stopped showing remote stream");
+}
+
+function onSignalingMessage(msg) {
+ trace("Sending message to " + remoteName + " (ID " + remoteId + "):\n" + msg);
+ sendToPeer(remoteId, msg);
+}
+
+// TODO: Add callbacks onconnecting, onopen and onstatechange.
+
+
+// Server interaction
+
+function handleServerNotification(data) {
+ trace("Server notification: " + data);
+ var parsed = data.split(",");
+ if (parseInt(parsed[2]) == 1) { // New peer
+ var peerId = parseInt(parsed[1]);
+ if (!peerExists(peerId)) {
+ var peerList = document.getElementById("peers");
+ if (peerList.length == 1 && peerList.options[0].value == -1)
+ clearPeerList();
+ addPeer(peerId, parsed[0]);
+ document.getElementById("peers").disabled = false;
+ document.getElementById("call").disabled = false;
+ }
+ } else if (parseInt(parsed[2]) == 0) { // Removed peer
+ removePeer(parseInt(parsed[1]));
+ if (document.getElementById("peers").length == 0) {
+ document.getElementById("peers").disabled = true;
+ addPeer(-1, "No other peer connected");
+ }
+ }
+}
+
+function handlePeerMessage(peer_id, msg) {
+ var peerName = getPeerName(peer_id);
+ if (peerName == undefined) {
+ trace_warning("Received message from unknown peer (ID " + peer_id +
+ "), ignoring message:");
+ trace(msg);
+ return;
+ }
+ trace("Received message from " + peerName + " (ID " + peer_id + "):\n" + msg);
+ // Assuming we receive the message from the peer we want to communicate with.
+ // TODO: Only accept messages from peer we communicate with with if call is
+ // ongoing.
+ if (msg.search("BYE") == 0) {
+ // Other side has hung up.
+ document.getElementById("hangup").disabled = true;
+ closeCall()
+ } else {
+ if (!pc) {
+ // Other side is calling us, startup
+ if (!setSelectedPeer(peer_id)) {
+ trace_warning("Recevied message from unknown peer, ignoring");
+ return;
+ }
+ if (!storeRemoteInfo())
+ return;
+ document.getElementById("call").disabled = true;
+ document.getElementById("peers").disabled = true;
+ createPeerConnection();
+ try {
+ pc.processSignalingMessage(msg);
+ } catch (e) {
+ trace_exception(e, "Process signaling message error");
+ }
+ trace("Adding stream");
+ pc.addStream(localStream);
+ document.getElementById("hangup").disabled = false;
+ } else {
+ try {
+ pc.processSignalingMessage(msg);
+ } catch (e) {
+ trace_exception(e, "Process signaling message error");
+ }
+ }
+ }
+}
+
+function getIntHeader(r, name) {
+ var val = r.getResponseHeader(name);
+ trace("header value: " + val);
+ return val != null && val.length ? parseInt(val) : -1;
+}
+
+function hangingGetCallback() {
+ try {
+ if (hangingGet.readyState != 4 || disconnecting)
+ return;
+ if (hangingGet.status != 200) {
+ trace_warning("server error, status: " + hangingGet.status + ", text: " +
+ hangingGet.statusText);
+ disconnect();
+ } else {
+ var peer_id = getIntHeader(hangingGet, "Pragma");
+ if (peer_id == myId) {
+ handleServerNotification(hangingGet.responseText);
+ } else {
+ handlePeerMessage(peer_id, hangingGet.responseText);
+ }
+ }
+
+ if (hangingGet) {
+ hangingGet.abort();
+ hangingGet = null;
+ }
+
+ if (myId != -1)
+ window.setTimeout(startHangingGet, 0);
+ } catch (e) {
+ trace_exception(e, "Hanging get error");
+ }
+}
+
+function onHangingGetTimeout() {
+ trace("hanging get timeout. issuing again");
+ hangingGet.abort();
+ hangingGet = null;
+ if (myId != -1)
+ window.setTimeout(startHangingGet, 0);
+}
+
+function startHangingGet() {
+ try {
+ hangingGet = new XMLHttpRequest();
+ hangingGet.onreadystatechange = hangingGetCallback;
+ hangingGet.ontimeout = onHangingGetTimeout;
+ hangingGet.open("GET", server + "/wait?peer_id=" + myId, true);
+ hangingGet.send();
+ } catch (e) {
+ trace_exception(e, "Start hanging get error");
+ }
+}
+
+function sendToPeer(peer_id, data) {
+ if (myId == -1) {
+ alert("Not connected.");
+ return;
+ }
+ if (peer_id == myId) {
+ alert("Can't send a message to oneself.");
+ return;
+ }
+ var r = new XMLHttpRequest();
+ r.open("POST", server + "/message?peer_id=" + myId + "&to=" + peer_id, false);
+ r.setRequestHeader("Content-Type", "text/plain");
+ r.send(data);
+ r = null;
+}
+
+function signInCallback() {
+ try {
+ if (request.readyState == 4) {
+ if (request.status == 200) {
+ var peers = request.responseText.split("\n");
+ myId = parseInt(peers[0].split(",")[1]);
+ trace("My id: " + myId);
+ clearPeerList();
+ var added = 0;
+ for (var i = 1; i < peers.length; ++i) {
+ if (peers[i].length > 0) {
+ trace("Peer " + i + ": " + peers[i]);
+ var parsed = peers[i].split(",");
+ addPeer(parseInt(parsed[1]), parsed[0]);
+ ++added;
+ }
+ }
+ if (added == 0)
+ addPeer(-1, "No other peer connected");
+ else {
+ document.getElementById("peers").disabled = false;
+ document.getElementById("call").disabled = false;
+ }
+ startHangingGet();
+ request = null;
+ document.getElementById("connect").disabled = true;
+ document.getElementById("disconnect").disabled = false;
+ }
+ }
+ } catch (e) {
+ trace_exception(e, "Sign in error");
+ document.getElementById("connect").disabled = false;
+ }
+}
+
+function signIn() {
+ try {
+ request = new XMLHttpRequest();
+ request.onreadystatechange = signInCallback;
+ request.open("GET", server + "/sign_in?" + myName, true);
+ request.send();
+ } catch (e) {
+ trace_exception(e, "Start sign in error");
+ document.getElementById("connect").disabled = false;
+ }
+}
+
+function connect() {
+ myName = document.getElementById("local").value.toLowerCase();
+ server = document.getElementById("server").value.toLowerCase();
+ if (myName.length == 0) {
+ alert("I need a name please.");
+ document.getElementById("local").focus();
+ } else {
+ // TODO: Disable connect button here, but we need a timeout and check if we
+ // have connected, if so enable it again.
+ signIn();
+ }
+}
+
+function disconnect() {
+ if (callState == 1)
+ hangUp();
+
+ disconnecting = true;
+
+ if (request) {
+ request.abort();
+ request = null;
+ }
+
+ if (hangingGet) {
+ hangingGet.abort();
+ hangingGet = null;
+ }
+
+ if (myId != -1) {
+ request = new XMLHttpRequest();
+ request.open("GET", server + "/sign_out?peer_id=" + myId, false);
+ request.send();
+ request = null;
+ myId = -1;
+ }
+
+ clearPeerList();
+ addPeer(-1, "Not connected");
+ document.getElementById("connect").disabled = false;
+ document.getElementById("disconnect").disabled = true;
+ document.getElementById("peers").disabled = true;
+ document.getElementById("call").disabled = true;
+
+ disconnecting = false;
+}
+
+
+// Window event handling
+
+window.onload = getUserMedia;
+window.onbeforeunload = disconnect;
+
+
+</script>
+</head>
+
+<body>
+<h1>WebRTC</h1>
+You must have a WebRTC capable browser in order to make calls using this test
+page.<br>
+
+<table border="0">
+<tr>
+ <td>Local Preview</td>
+ <td>Remote Video</td>
+</tr>
+<tr>
+ <td>
+ <video width="320" height="240" id="localView" autoplay="autoplay"></video>
+ </td>
+ <td>
+ <video width="640" height="480" id="remoteView" autoplay="autoplay"></video>
+ </td>
+</tr>
+</table>
+
+<table border="0">
+<tr>
+ <td valign="top">
+ <table border="0" cellpaddning="0" cellspacing="0">
+ <tr>
+ <td>Server:</td>
+ <td>
+ <input type="text" id="server" size="30" value="http://localhost:8888"/>
+ </td>
+ </tr>
+ <tr>
+ <td>Name:</td><td><input type="text" id="local" size="30" value="name"/></td>
+ </tr>
+ </table>
+ </td>
+ <td valign="top">
+ <button id="connect" onclick="connect();">Connect</button><br>
+ <button id="disconnect" onclick="disconnect();" disabled="true">Disconnect
+ </button>
+ </td>
+ <td> </td>
+ <td valign="top">
+ Connected peers:<br>
+ <select id="peers" size="5" disabled="true">
+ <option value="-1">Not connected</option>
+ </select>
+ </td>
+ <td valign="top">
+ <!--input type="text" id="peer_id" size="3" value="1"/><br-->
+ <button id="call" onclick="doCall();" disabled="true">Call</button><br>
+ <button id="hangup" onclick="hangUp();" disabled="true">Hang up</button><br>
+ </td>
+ <td> </td>
+ <td valign="top">
+ <button onclick="toggleExtraButtons();">Toggle extra buttons (debug)</button>
+ <br>
+ <button id="createPcBtn" onclick="createPeerConnection();" hidden="true">
+ Create peer connection</button>
+ </td>
+</tr>
+</table>
+
+<button onclick="document.getElementById('debug').innerHTML='';">Clear log
+</button>
+<pre id="debug"></pre>
+
+</body>
+
+</html>
+