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>&nbsp;
+
+<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>&nbsp;&nbsp;&nbsp;</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>&nbsp;&nbsp;&nbsp;</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>
+