├── log.js ├── README.md ├── index.js ├── server.js ├── serverXHRSignalingChannel.js └── static ├── clientXHRSignaling.js └── index.html /log.js: -------------------------------------------------------------------------------- 1 | var log = console.log 2 | 3 | exports.log = log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-p2p 2 | 3 | ### 测试截图 4 | 5 | PC端(ps:因为pc端没有摄像头,就弄了个虚拟摄像头,摄像头视频播放的是本地视频): 6 | 7 |  8 | 9 |  10 | 11 | 手机端: 12 | 13 |  14 | 15 |  16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var server = require('./server') 2 | var log = require('./log').log 3 | var port = process.argv[2] || 5001 4 | 5 | var requestHandlers = require('./serverXHRSignalingChannel') 6 | 7 | // 返回404 8 | function notFound(info) { 9 | var res = info.res 10 | log('Request handler NotFound was called.') 11 | res.writeHead(404, {'Content-Type': 'text/plain'}) 12 | res.write('404 Page Not Found') 13 | res.end() 14 | } 15 | 16 | var handle = {} 17 | 18 | handle['/'] = notFound 19 | 20 | handle['/connect'] = requestHandlers.connect 21 | handle['/send'] = requestHandlers.send 22 | handle['/get'] = requestHandlers.get 23 | 24 | server.serveFilePath('static') 25 | server.start(handle, port) 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var url = require('url') 3 | var fs = require('fs') 4 | 5 | var log = require('./log').log 6 | var serveFileDir = '' 7 | 8 | // 设置静态文件的路径 9 | function setServeFilePath(path) { 10 | serveFileDir = path 11 | } 12 | 13 | exports.serveFilePath = setServeFilePath 14 | 15 | // 创建路由处理程序 16 | function start(handle, port) { 17 | function onRequest(req, res) { 18 | var urlData = url.parse(req.url, true), 19 | pathName = urlData.pathname, 20 | info = { 'res': res, 'query': urlData.query, 'postData': '' } 21 | log('Request for ' + pathName + ' received') 22 | req.setEncoding('utf8') 23 | req.addListener('data', function(postDataChunk) { 24 | info.postData += postDataChunk 25 | log('Received post data chunk ' + postDataChunk + '.') 26 | }) 27 | req.addListener('end', function(){ 28 | route(handle, pathName, info) 29 | }) 30 | } 31 | 32 | http.createServer(onRequest).listen(port) 33 | log('Server started on port ' + port) 34 | } 35 | 36 | exports.start = start 37 | 38 | // 确定请求的是静态文件还是自定义的路由 39 | function route(handle, pathName, info) { 40 | log('About to route request for ', pathName) 41 | // 检查前导斜杠后的路径是否为可处理的现有文件 42 | var filePath = createFilePath(pathName) 43 | log('Attempting to locate ' + filePath) 44 | fs.stat(filePath, (err, stats) => { 45 | if (!err && stats.isFile()) { // 处理静态文件 46 | serveFile(filePath, info) 47 | } else { // 必须为自定义路径 48 | handleCustom(handle, pathName, info) 49 | } 50 | }) 51 | } 52 | 53 | // 先从给定路径名称中去除... ~等特殊字符,再向其开头添加serveFileDir 54 | function createFilePath(pathName) { 55 | var components = pathName.substr(1).split('/') 56 | var filtered = new Array(), temp 57 | for (var i = 0, len = components.length; i < len; i++) { 58 | temp = components[i] 59 | if (temp == '..') continue // 没有上级目录 60 | if (temp == '') continue // 没有根目录 61 | temp = temp.replace(/~/g, '') // 没有用户目录 62 | filtered.push(temp) 63 | } 64 | return serveFileDir + '/' + filtered.join('/') 65 | } 66 | 67 | // 打开指定文件,读取内容,将内容返回给客户端 68 | function serveFile(filePath, info) { 69 | var res = info.res, 70 | query = info.query 71 | log('Serving file ' + filePath) 72 | fs.open(filePath, 'r', (err, fd) => { 73 | if (err) { 74 | log(err.message) 75 | noHandlerError(filePath, res) 76 | return 77 | } 78 | var readBuffer = new Buffer(20480) 79 | fs.read(fd, readBuffer, 0, 20480, 0, (err, readBytes) => { 80 | if (err) { 81 | log(err.message) 82 | fs.close(fd) 83 | noHandlerError(filePath, res) 84 | return 85 | } 86 | log('Just read ' + readBytes + ' bytes') 87 | if (readBytes > 0) { 88 | res.writeHead(200, { 'Content-Type': contentType(filePath) }) 89 | res.write(addQuery(readBuffer.toString('utf8', 0, readBytes), query)) 90 | } 91 | res.end() 92 | }) 93 | }) 94 | } 95 | 96 | // 确定提取到的文件类型 97 | function contentType(filePath) { 98 | var index = filePath.lastIndexOf('.') 99 | if (index > 0) { 100 | switch (filePath.substr(index + 1)) { 101 | case 'html': 102 | return 'text/html' 103 | case 'js': 104 | return 'application/javascript' 105 | case 'css': 106 | return 'text/css' 107 | case 'txt': 108 | return 'text/plain' 109 | default: 110 | return 'text/html' 111 | } 112 | } 113 | return 'text/html' 114 | } 115 | 116 | // 确定自定义路由的处理 117 | function handleCustom(handle, pathName, info) { 118 | if (typeof handle[pathName] == 'function') { 119 | handle[pathName](info) 120 | } else { 121 | noHandlerError(pathName, info.res) 122 | } 123 | } 124 | 125 | // 404请求 126 | function noHandlerError(pathName, res) { 127 | log('No request handler found for ' + pathName) 128 | res.writeHead(404, {'Content-Type': 'text/plain'}) 129 | res.write('404 Page Not Found') 130 | res.end() 131 | } 132 | 133 | // 将html文件中的第一个空脚本替换成特定对象,该对象包含url中的query参数 134 | function addQuery(str, query) { 135 | if (query) { 136 | return str.replace('', ``) 137 | } 138 | return str 139 | } -------------------------------------------------------------------------------- /serverXHRSignalingChannel.js: -------------------------------------------------------------------------------- 1 | var log = require('./log').log 2 | 3 | var connections = {}, // 链接列表 4 | partner = {}, // 用于在对等端进行映射 5 | messagesFor = {}; // 包含要发送给客户的消息的数组 6 | 7 | // 排队发送json响应 8 | function webrtcResponse(response, res) { 9 | log('Replying with webrtc response ' + JSON.stringify(response)) 10 | res.writeHead(200, { 'Content-Type': 'application/json' }) 11 | res.write(JSON.stringify(response)) 12 | res.end() 13 | } 14 | 15 | // webrtc发送error响应 16 | function webrtcError(err, res) { 17 | log('Replying with webrtc error ' + err) 18 | webrtcResponse({ 'err': err }, res) 19 | } 20 | 21 | // 处理XHR http请求,以使用给定密钥进行连接 22 | function connect(info) { 23 | var res = info.res, 24 | query = info.query, 25 | thisConnection, 26 | newId = function () { 27 | return Math.floor(Math.random() * 1000000000) 28 | }, 29 | connectFirstParty = function () { 30 | if (thisConnection.status === 'connected') { 31 | // 删除配对和任何存储的信息 32 | delete partner[thisConnection.ids[0]] 33 | delete partner[thisConnection.ids[1]] 34 | delete messagesFor[thisConnection.ids[0]] 35 | delete messagesFor[thisConnection.ids[1]] 36 | } 37 | connections[query.key] = {} 38 | thisConnection = connections[query.key] 39 | thisConnection.status = 'waiting' 40 | thisConnection.ids = [newId()] 41 | webrtcResponse({ 42 | id: thisConnection.ids[0], 43 | status: thisConnection.status 44 | }, res) 45 | }, 46 | connectSecondParty = function () { 47 | thisConnection.ids[1] = newId() 48 | partner[thisConnection.ids[0]] = thisConnection.ids[1] 49 | partner[thisConnection.ids[1]] = thisConnection.ids[0] 50 | messagesFor[thisConnection.ids[0]] = [] 51 | messagesFor[thisConnection.ids[1]] = [] 52 | thisConnection.status = 'connected' 53 | webrtcResponse({ 54 | id: thisConnection.ids[1], 55 | status: thisConnection.status 56 | }, res) 57 | } 58 | log('Request handler connect was called.') 59 | if (query && query.key) { 60 | var thisConnection = connections[query.key] || { 'status': 'new' } 61 | if (thisConnection.status === 'waiting') { // 前半部分就绪 62 | connectSecondParty() 63 | return 64 | } else { // 必须为新连接或为‘connected’状态 65 | connectFirstParty() 66 | return 67 | } 68 | } else { 69 | webrtcError('No recognizable query key', res) 70 | } 71 | } 72 | 73 | exports.connect = connect 74 | 75 | // 对info.postData.message中的消息进行排队处理,已发送至具体info.postData.id中的id的伙伴 76 | function sendMessage(info) { 77 | log('PostData received is *** ' + info.postData + ' ***') 78 | var postData = JSON.parse(info.postData), 79 | res = info.res; 80 | if (typeof postData === 'undefined') { 81 | webrtcError('No posted data in JSON format!', res) 82 | return 83 | } 84 | if (typeof (postData.message) === 'undefined') { 85 | webrtcError('No message received!', res) 86 | return 87 | } 88 | if (typeof (postData.id) === 'undefined') { 89 | webrtcError('No id received with message!', res) 90 | return 91 | } 92 | if (typeof (partner[postData.id]) === 'undefined') { 93 | webrtcError('Invalid id ' + postData.id, res) 94 | return 95 | } 96 | if (typeof (messagesFor[partner[postData.id]]) === 'undefined') { 97 | webrtcError('Invalid id ' + postData.id, res) 98 | return 99 | } 100 | messagesFor[partner[postData.id]].push(postData.message) 101 | log('Saving message *** ' + postData.message + ' *** for delivery to id ' + partner[postData.id]) 102 | 103 | webrtcResponse('Saving message *** ' + postData.message + ' *** for delivery to id ' + partner[postData.id], res) 104 | } 105 | 106 | exports.send = sendMessage 107 | 108 | // 返回所有队列获取info.postData.id的消息 109 | function getMessages(info) { 110 | var postData = JSON.parse(info.postData), 111 | res = info.res; 112 | if (typeof postData === 'undefined') { 113 | webrtcError('No posted data in JSON format!', res) 114 | return 115 | } 116 | if (typeof (postData.id) === 'undefined') { 117 | webrtcError('No id received with message!', res) 118 | return 119 | } 120 | if (typeof (messagesFor[partner[postData.id]]) === 'undefined') { 121 | webrtcError('Invalid id ' + postData.id, res) 122 | return 123 | } 124 | log('Sending messages *** ' + JSON.stringify(messagesFor[partner[postData.id]]) + ' *** to id ' + postData.id) 125 | webrtcResponse({ 126 | msgs: messagesFor[postData.id] 127 | }, res) 128 | messagesFor[postData.id] = [] 129 | } 130 | 131 | exports.get = getMessages 132 | -------------------------------------------------------------------------------- /static/clientXHRSignaling.js: -------------------------------------------------------------------------------- 1 | var createSignalingChannel = function (key, handlers) { 2 | 3 | var id, status, 4 | doNothing = function () { }, 5 | handlers = handlers || {}, 6 | initHandler = function (h) { 7 | return ((typeof h === 'function') && h) || doNothing; 8 | }, 9 | waitingHandler = initHandler(handlers.onWaiting), 10 | connectedHandler = initHandler(handlers.onConnected), 11 | messageHandler = initHandler(handlers.onMessage); 12 | 13 | 14 | // Set up connection with signaling server 15 | function connect(failureCB) { 16 | var failureCB = (typeof failureCB === 'function') || 17 | function () { }; 18 | 19 | // Handle connection response, which should be error or status 20 | // of "connected" or "waiting" 21 | function handler() { 22 | if (this.readyState == this.DONE) { 23 | if (this.status == 200 && this.response != null) { 24 | var res = JSON.parse(this.response); 25 | if (res.err) { 26 | failureCB("error: " + res.err); 27 | return; 28 | } 29 | 30 | // if no error, save status and server-generated id, 31 | // then start asynchronouse polling for messages 32 | id = res.id; 33 | status = res.status; 34 | poll(); 35 | 36 | // run user-provided handlers for waiting and connected 37 | // states 38 | if (status === "waiting") { 39 | waitingHandler(); 40 | } else { 41 | connectedHandler(); 42 | } 43 | return; 44 | } else { 45 | failureCB("HTTP error: " + this.status); 46 | return; 47 | } 48 | } 49 | } 50 | 51 | // open XHR and send the connection request with the key 52 | var client = new XMLHttpRequest(); 53 | client.onreadystatechange = handler; 54 | client.open("GET", "/connect?key=" + key); 55 | client.send(); 56 | } 57 | 58 | // poll() waits n ms between gets to the server. n is at 10 ms 59 | // for 10 tries, then 100 ms for 10 tries, then 1000 ms from then 60 | // on. n is reset to 10 ms if a message is actually received. 61 | function poll() { 62 | var msgs; 63 | var pollWaitDelay = (function () { 64 | var delay = 10, counter = 1; 65 | 66 | function reset() { 67 | delay = 10; 68 | counter = 1; 69 | } 70 | 71 | function increase() { 72 | counter += 1; 73 | if (counter > 20) { 74 | delay = 1000; 75 | } else if (counter > 10) { 76 | delay = 100; 77 | } // else leave delay at 10 78 | } 79 | 80 | function value() { 81 | return delay; 82 | } 83 | 84 | return { reset: reset, increase: increase, value: value }; 85 | } ()); 86 | 87 | // getLoop is defined and used immediately here. It retrieves 88 | // messages from the server and then schedules itself to run 89 | // again after pollWaitDelay.value() milliseconds. 90 | (function getLoop() { 91 | get(function (response) { 92 | var i, msgs = (response && response.msgs) || []; 93 | 94 | // if messages property exists, then we are connected 95 | if (response.msgs && (status !== "connected")) { 96 | // switch status to connected since it is now! 97 | status = "connected"; 98 | connectedHandler(); 99 | } 100 | if (msgs.length > 0) { // we got messages 101 | pollWaitDelay.reset(); 102 | for (i = 0; i < msgs.length; i += 1) { 103 | handleMessage(msgs[i]); 104 | } 105 | } else { // didn't get any messages 106 | pollWaitDelay.increase(); 107 | } 108 | 109 | // now set timer to check again 110 | setTimeout(getLoop, pollWaitDelay.value()); 111 | }); 112 | } ()); 113 | } 114 | 115 | 116 | // This function is part of the polling setup to check for 117 | // messages from the other browser. It is called by getLoop() 118 | // inside poll(). 119 | function get(getResponseHandler) { 120 | 121 | // response should either be error or a JSON object. If the 122 | // latter, send it to the user-provided handler. 123 | function handler() { 124 | if (this.readyState == this.DONE) { 125 | if (this.status == 200 && this.response != null) { 126 | var res = JSON.parse(this.response); 127 | if (res.err) { 128 | getResponseHandler("error: " + res.err); 129 | return; 130 | } 131 | getResponseHandler(res); 132 | return res; 133 | } else { 134 | getResponseHandler("HTTP error: " + this.status); 135 | return; 136 | } 137 | } 138 | } 139 | 140 | // open XHR and request messages for my id 141 | var client = new XMLHttpRequest(); 142 | client.onreadystatechange = handler; 143 | client.open("POST", "/get"); 144 | client.send(JSON.stringify({ "id": id })); 145 | } 146 | 147 | 148 | // Schedule incoming messages for asynchronous handling. 149 | // This is used by getLoop() in poll(). 150 | function handleMessage(msg) { // process message asynchronously 151 | setTimeout(function () { messageHandler(msg); }, 0); 152 | } 153 | 154 | 155 | // Send a message to the other browser on the signaling channel 156 | function send(msg, responseHandler) { 157 | var reponseHandler = responseHandler || function () { }; 158 | 159 | // parse response and send to handler 160 | function handler() { 161 | if (this.readyState == this.DONE) { 162 | if (this.status == 200 && this.response != null) { 163 | var res = JSON.parse(this.response); 164 | if (res.err) { 165 | responseHandler("error: " + res.err); 166 | return; 167 | } 168 | responseHandler(res); 169 | return; 170 | } else { 171 | responseHandler("HTTP error: " + this.status); 172 | return; 173 | } 174 | } 175 | } 176 | 177 | // open XHR and send my id and message as JSON string 178 | var client = new XMLHttpRequest(); 179 | client.onreadystatechange = handler; 180 | client.open("POST", "/send"); 181 | var sendData = { "id": id, "message": msg }; 182 | client.send(JSON.stringify(sendData)); 183 | } 184 | 185 | 186 | return { 187 | connect: connect, 188 | send: send 189 | }; 190 | 191 | }; 192 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 |WebRTC 视频聊天demo测试
275 |276 | Key: 277 | 278 | 279 | 282 | 283 |
284 |
304 | 发出的消息
305 |
306 |
307 |
315 | 收到的消息
316 |
317 |
318 |