├── 12_chapter ├── client_communication.xmind ├── public │ └── peerconnection │ │ ├── index.html │ │ ├── room.html │ │ ├── css │ │ └── main.css │ │ ├── main.js │ │ └── third_party │ │ └── graph.js ├── cert │ ├── test.key │ └── test.pem └── server.js ├── 06_chapter ├── public │ ├── 01_capture │ │ ├── index.html │ │ └── client.js │ └── 02_permission │ │ ├── index.html │ │ └── client.js ├── server.js └── cert │ ├── test.key │ └── test.pem ├── 05_chapter ├── public │ ├── index.html │ └── client.js ├── server.js └── cert │ ├── test.key │ └── test.pem ├── 10_chapter ├── public │ ├── index.html │ ├── css │ │ └── main.css │ ├── main.js │ ├── sdp_pc2.txt │ └── sdp_pc1.txt ├── cert │ ├── test.key │ └── test.pem └── server.js ├── 08_chapter ├── public │ ├── index.html │ ├── client.js │ └── css │ │ └── main.css ├── cert │ ├── test.key │ └── test.pem └── server.js └── README.md /12_chapter/client_communication.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrYeLiang/WebRTC/HEAD/12_chapter/client_communication.xmind -------------------------------------------------------------------------------- /06_chapter/public/01_capture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC capture video and audio 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /05_chapter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC get audio and video devices 4 | 5 | 6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /06_chapter/public/01_capture/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //后去到video标签 4 | var videoplay = document.querySelector('video#player'); 5 | 6 | //将流赋值给video标签 7 | function gotMediaStream(stream){ 8 | videoplay.srcObject = stream; 9 | } 10 | 11 | //打印错误日志 12 | function handleError(err){ 13 | console.log('getUserMedia error : ', err); 14 | } 15 | 16 | if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ 17 | console.log('getUserMedia is not supported'); 18 | }else{ 19 | var constraints = { 20 | video : true, 21 | audio : true 22 | } 23 | 24 | navigator.mediaDevices.getUserMedia(constraints) 25 | .then(gotMediaStream) 26 | .catch(handleError) 27 | } 28 | -------------------------------------------------------------------------------- /05_chapter/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var fs = require('fs'); 6 | 7 | var serveIndex = require('serve-index'); 8 | 9 | var express = require('express'); 10 | var app = express(); 11 | 12 | //顺序不能换 13 | app.use(serveIndex('./public')); 14 | app.use(express.static('./public')); 15 | 16 | var options = { 17 | key : fs.readFileSync('./cert/test.key'), 18 | cert : fs.readFileSync('./cert/test.pem') 19 | } 20 | 21 | var https_server = https.createServer(options, app); 22 | https_server.listen(443, '0.0.0.0'); 23 | 24 | var http_server = http.createServer(app); 25 | http_server.listen(80, '0.0.0.0'); 26 | 27 | 28 | -------------------------------------------------------------------------------- /06_chapter/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var fs = require('fs'); 6 | 7 | var serveIndex = require('serve-index'); 8 | 9 | var express = require('express'); 10 | var app = express(); 11 | 12 | //顺序不能换 13 | app.use(serveIndex('./public')); 14 | app.use(express.static('./public')); 15 | 16 | var options = { 17 | key : fs.readFileSync('./cert/test.key'), 18 | cert : fs.readFileSync('./cert/test.pem') 19 | } 20 | 21 | var https_server = https.createServer(options, app); 22 | https_server.listen(443, '0.0.0.0'); 23 | 24 | var http_server = http.createServer(app); 25 | http_server.listen(80, '0.0.0.0'); 26 | 27 | 28 | -------------------------------------------------------------------------------- /10_chapter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | RTCPeerConnection 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 |
15 |

Local:

16 | 17 |

Local SDP:

18 | 19 |
20 |
21 |

Remote:

22 | 23 |

Remote SDP:

24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /12_chapter/public/peerconnection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | really peer connection 4 | 5 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | 27 | 32 | 33 |
20 |
21 | 22 | 23 |
24 |
28 |
29 | 30 |
31 |
34 | 35 | -------------------------------------------------------------------------------- /08_chapter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat Room 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 21 | 22 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 38 | 39 |
10 | 11 | 12 |
16 | 17 | 18 | 19 | 20 |
24 |
25 | 26 |
30 |
31 | 32 |
36 | 37 |
40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /05_chapter/public/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var audioSource = document.querySelector("select#audioSource"); 3 | var audioOutput = document.querySelector("select#audioOutput"); 4 | var videoSource = document.querySelector("select#videoSource"); 5 | 6 | if(!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices){ 7 | console.log('enumerateDevices is not support!'); 8 | }else{ 9 | navigator.mediaDevices.enumerateDevices() 10 | .then(gotDevices) 11 | .catch(handleError); 12 | } 13 | 14 | function gotDevices(devicesInfos){ 15 | devicesInfos.forEach(function(deviceInfo){ 16 | console.log(deviceInfo.kind 17 | + ": label = "+deviceInfo.label 18 | + ": id = " + deviceInfo.deviceId 19 | + ": groupId = " + deviceInfo.groupId); 20 | 21 | var option = document.createElement('option'); 22 | option.text = deviceInfo.label; 23 | option.value = deviceInfo.deviceId; 24 | 25 | if(deviceInfo.kind === 'audioinput'){ 26 | audioSource.appendChild(option); 27 | }else if(deviceInfo.kind === 'audiooutput'){ 28 | audioOutput.appendChild(option); 29 | }else if(deviceInfo.kind === 'videoinput'){ 30 | videoSource.appendChild(option); 31 | } 32 | }) 33 | } 34 | 35 | function handleError(err){ 36 | console.log(err.name + " : "+ err.message); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /05_chapter/cert/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAgECrBRI9h/EE1QhLQBIxqeZFAFh4obAm7jGIR3QMUimHxR5X 3 | SRmkVKbkMMQLXtZPG/vgva8SmUSSbdOTvOXXjL3cBet8gRuQ2sxlypMDJbkawPyr 4 | Gct5T497op0jvLCxX6E3LJOe8AF22bVRYL5oMQFxZV+1cfTDYGDHZKPowJbQmtwp 5 | JknKnD3f2jp48eJ2s+5NyZdQB2O48SGkRiwXfmosTmcNgiXR46WIvvhkbVQSKMOZ 6 | It1lZtZXp7B6NxllY1D+ayG7LbaW6Y2tldFMuXrNRnK5pKfXuAVE8qKSZBqqwBdr 7 | lmcWQfAnCZBXHjlHwmN7+TFh99dQcAWmfhp4AQIDAQABAoIBADAChUKnulrqMWnh 8 | 6fF5HhhAt/HSLI4yV9BwwFgnK/mRzF5Q5lHaBHcf5moO6Ua+KO0L0+yN3gN79Oih 9 | /DsJOzBXXkblCT13aTlOrIgxkksLyOUXcKF+VtIenySXeGpJJozq17GbLTMhCTGk 10 | O2tgu7YkmSNeuZiIDJ9Gez3EUfvNrexlpIndjHIiyMh6Hv2TmGofZsX48Sl6ocgH 11 | bGlgnLEExJCacX1KyWpL2QraV9jhF7WCXgEPLWcOq4r+aKkMpP/v1gZKPNMHq0OM 12 | 7ytCDrcvChnxG8azWsiXuptCqillm3QZSItzUBPjMFUzbBairikHdtPdzuzm6B+a 13 | F8T3A1UCgYEA41mTEVrzY0EWiUo5WWFBDndS/3WdIRpW3N9NhFG+JeIHQJf20bXV 14 | egUFj5d2eR7H2pjRYRuhCfdeIXZ1kkZchAfrYoMoI/dKbCl1DtG1+au/Id+UUf1Z 15 | nczbRERD3Ijx/1F+c8nHLutfG1/d3J+8A3f/61YnWuhVw9HAyUTWSB8CgYEAkGop 16 | 5KhpMZvrvBTHhIP82Tf3DQhmbGIfLj+KuJFRfK5UZlGzFtR17CYnT4mqMM/A6u6v 17 | He++rz8M/zwtRWwN+DbATKF2pEDiNCu6+Duq8MkS5wxNR2azm170gC+Cf21IEeVG 18 | 0/VH08/0ChVIp6flw6Q0zBIcwABzQNBKsYLdu98CgYEA0mPKXX01Ttyk5lfxymzd 19 | r21tOUq2JjQhvjRHn/Ola5lH7Na0ak1DSK/s/XzE/kEl4X0aBitzU62/RmhBVSQX 20 | 6XcKtRd6xg3KqV2UHiqjpHDzZ31n3Jf/nrA9GWezBRsWF5hq1Owdj6XxVXvZ7JlF 21 | fFBIXJhVwyLOe3BYX+l8AzECgYBZ3ibpyz+DBqOA7HFobnZXenM74gFS6xC6SAJ8 22 | broF27pb3fWTfG1RokCOR33oWDCWQigpefrwtUzSPFFzxRVAZFnwlf3tow7hJmF4 23 | fjEXHBmuPEKO70NPqZx/dJFB2PCjaklUN2wWTG9yIuKOqnXZ9IKCh7bqaJ1QNQ52 24 | PpjbgwKBgQCTT7sbZynbNUWoQ303gM1DZP0Yl7c7OnOucbrP+pn13d4ipi7jdwiS 25 | 3yk0VU7+cn/yqkockt8UTLPRQ0tql5/o2ekENp8qHqsJ1hevlDHRxmD/H8RltJ6X 26 | C6XKac6oWlJXee/3pUoixJFtFIMUl4cruf2A2HDd0l2CrBSVJCiMOA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /06_chapter/cert/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAgECrBRI9h/EE1QhLQBIxqeZFAFh4obAm7jGIR3QMUimHxR5X 3 | SRmkVKbkMMQLXtZPG/vgva8SmUSSbdOTvOXXjL3cBet8gRuQ2sxlypMDJbkawPyr 4 | Gct5T497op0jvLCxX6E3LJOe8AF22bVRYL5oMQFxZV+1cfTDYGDHZKPowJbQmtwp 5 | JknKnD3f2jp48eJ2s+5NyZdQB2O48SGkRiwXfmosTmcNgiXR46WIvvhkbVQSKMOZ 6 | It1lZtZXp7B6NxllY1D+ayG7LbaW6Y2tldFMuXrNRnK5pKfXuAVE8qKSZBqqwBdr 7 | lmcWQfAnCZBXHjlHwmN7+TFh99dQcAWmfhp4AQIDAQABAoIBADAChUKnulrqMWnh 8 | 6fF5HhhAt/HSLI4yV9BwwFgnK/mRzF5Q5lHaBHcf5moO6Ua+KO0L0+yN3gN79Oih 9 | /DsJOzBXXkblCT13aTlOrIgxkksLyOUXcKF+VtIenySXeGpJJozq17GbLTMhCTGk 10 | O2tgu7YkmSNeuZiIDJ9Gez3EUfvNrexlpIndjHIiyMh6Hv2TmGofZsX48Sl6ocgH 11 | bGlgnLEExJCacX1KyWpL2QraV9jhF7WCXgEPLWcOq4r+aKkMpP/v1gZKPNMHq0OM 12 | 7ytCDrcvChnxG8azWsiXuptCqillm3QZSItzUBPjMFUzbBairikHdtPdzuzm6B+a 13 | F8T3A1UCgYEA41mTEVrzY0EWiUo5WWFBDndS/3WdIRpW3N9NhFG+JeIHQJf20bXV 14 | egUFj5d2eR7H2pjRYRuhCfdeIXZ1kkZchAfrYoMoI/dKbCl1DtG1+au/Id+UUf1Z 15 | nczbRERD3Ijx/1F+c8nHLutfG1/d3J+8A3f/61YnWuhVw9HAyUTWSB8CgYEAkGop 16 | 5KhpMZvrvBTHhIP82Tf3DQhmbGIfLj+KuJFRfK5UZlGzFtR17CYnT4mqMM/A6u6v 17 | He++rz8M/zwtRWwN+DbATKF2pEDiNCu6+Duq8MkS5wxNR2azm170gC+Cf21IEeVG 18 | 0/VH08/0ChVIp6flw6Q0zBIcwABzQNBKsYLdu98CgYEA0mPKXX01Ttyk5lfxymzd 19 | r21tOUq2JjQhvjRHn/Ola5lH7Na0ak1DSK/s/XzE/kEl4X0aBitzU62/RmhBVSQX 20 | 6XcKtRd6xg3KqV2UHiqjpHDzZ31n3Jf/nrA9GWezBRsWF5hq1Owdj6XxVXvZ7JlF 21 | fFBIXJhVwyLOe3BYX+l8AzECgYBZ3ibpyz+DBqOA7HFobnZXenM74gFS6xC6SAJ8 22 | broF27pb3fWTfG1RokCOR33oWDCWQigpefrwtUzSPFFzxRVAZFnwlf3tow7hJmF4 23 | fjEXHBmuPEKO70NPqZx/dJFB2PCjaklUN2wWTG9yIuKOqnXZ9IKCh7bqaJ1QNQ52 24 | PpjbgwKBgQCTT7sbZynbNUWoQ303gM1DZP0Yl7c7OnOucbrP+pn13d4ipi7jdwiS 25 | 3yk0VU7+cn/yqkockt8UTLPRQ0tql5/o2ekENp8qHqsJ1hevlDHRxmD/H8RltJ6X 26 | C6XKac6oWlJXee/3pUoixJFtFIMUl4cruf2A2HDd0l2CrBSVJCiMOA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /08_chapter/cert/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAgECrBRI9h/EE1QhLQBIxqeZFAFh4obAm7jGIR3QMUimHxR5X 3 | SRmkVKbkMMQLXtZPG/vgva8SmUSSbdOTvOXXjL3cBet8gRuQ2sxlypMDJbkawPyr 4 | Gct5T497op0jvLCxX6E3LJOe8AF22bVRYL5oMQFxZV+1cfTDYGDHZKPowJbQmtwp 5 | JknKnD3f2jp48eJ2s+5NyZdQB2O48SGkRiwXfmosTmcNgiXR46WIvvhkbVQSKMOZ 6 | It1lZtZXp7B6NxllY1D+ayG7LbaW6Y2tldFMuXrNRnK5pKfXuAVE8qKSZBqqwBdr 7 | lmcWQfAnCZBXHjlHwmN7+TFh99dQcAWmfhp4AQIDAQABAoIBADAChUKnulrqMWnh 8 | 6fF5HhhAt/HSLI4yV9BwwFgnK/mRzF5Q5lHaBHcf5moO6Ua+KO0L0+yN3gN79Oih 9 | /DsJOzBXXkblCT13aTlOrIgxkksLyOUXcKF+VtIenySXeGpJJozq17GbLTMhCTGk 10 | O2tgu7YkmSNeuZiIDJ9Gez3EUfvNrexlpIndjHIiyMh6Hv2TmGofZsX48Sl6ocgH 11 | bGlgnLEExJCacX1KyWpL2QraV9jhF7WCXgEPLWcOq4r+aKkMpP/v1gZKPNMHq0OM 12 | 7ytCDrcvChnxG8azWsiXuptCqillm3QZSItzUBPjMFUzbBairikHdtPdzuzm6B+a 13 | F8T3A1UCgYEA41mTEVrzY0EWiUo5WWFBDndS/3WdIRpW3N9NhFG+JeIHQJf20bXV 14 | egUFj5d2eR7H2pjRYRuhCfdeIXZ1kkZchAfrYoMoI/dKbCl1DtG1+au/Id+UUf1Z 15 | nczbRERD3Ijx/1F+c8nHLutfG1/d3J+8A3f/61YnWuhVw9HAyUTWSB8CgYEAkGop 16 | 5KhpMZvrvBTHhIP82Tf3DQhmbGIfLj+KuJFRfK5UZlGzFtR17CYnT4mqMM/A6u6v 17 | He++rz8M/zwtRWwN+DbATKF2pEDiNCu6+Duq8MkS5wxNR2azm170gC+Cf21IEeVG 18 | 0/VH08/0ChVIp6flw6Q0zBIcwABzQNBKsYLdu98CgYEA0mPKXX01Ttyk5lfxymzd 19 | r21tOUq2JjQhvjRHn/Ola5lH7Na0ak1DSK/s/XzE/kEl4X0aBitzU62/RmhBVSQX 20 | 6XcKtRd6xg3KqV2UHiqjpHDzZ31n3Jf/nrA9GWezBRsWF5hq1Owdj6XxVXvZ7JlF 21 | fFBIXJhVwyLOe3BYX+l8AzECgYBZ3ibpyz+DBqOA7HFobnZXenM74gFS6xC6SAJ8 22 | broF27pb3fWTfG1RokCOR33oWDCWQigpefrwtUzSPFFzxRVAZFnwlf3tow7hJmF4 23 | fjEXHBmuPEKO70NPqZx/dJFB2PCjaklUN2wWTG9yIuKOqnXZ9IKCh7bqaJ1QNQ52 24 | PpjbgwKBgQCTT7sbZynbNUWoQ303gM1DZP0Yl7c7OnOucbrP+pn13d4ipi7jdwiS 25 | 3yk0VU7+cn/yqkockt8UTLPRQ0tql5/o2ekENp8qHqsJ1hevlDHRxmD/H8RltJ6X 26 | C6XKac6oWlJXee/3pUoixJFtFIMUl4cruf2A2HDd0l2CrBSVJCiMOA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /10_chapter/cert/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAgECrBRI9h/EE1QhLQBIxqeZFAFh4obAm7jGIR3QMUimHxR5X 3 | SRmkVKbkMMQLXtZPG/vgva8SmUSSbdOTvOXXjL3cBet8gRuQ2sxlypMDJbkawPyr 4 | Gct5T497op0jvLCxX6E3LJOe8AF22bVRYL5oMQFxZV+1cfTDYGDHZKPowJbQmtwp 5 | JknKnD3f2jp48eJ2s+5NyZdQB2O48SGkRiwXfmosTmcNgiXR46WIvvhkbVQSKMOZ 6 | It1lZtZXp7B6NxllY1D+ayG7LbaW6Y2tldFMuXrNRnK5pKfXuAVE8qKSZBqqwBdr 7 | lmcWQfAnCZBXHjlHwmN7+TFh99dQcAWmfhp4AQIDAQABAoIBADAChUKnulrqMWnh 8 | 6fF5HhhAt/HSLI4yV9BwwFgnK/mRzF5Q5lHaBHcf5moO6Ua+KO0L0+yN3gN79Oih 9 | /DsJOzBXXkblCT13aTlOrIgxkksLyOUXcKF+VtIenySXeGpJJozq17GbLTMhCTGk 10 | O2tgu7YkmSNeuZiIDJ9Gez3EUfvNrexlpIndjHIiyMh6Hv2TmGofZsX48Sl6ocgH 11 | bGlgnLEExJCacX1KyWpL2QraV9jhF7WCXgEPLWcOq4r+aKkMpP/v1gZKPNMHq0OM 12 | 7ytCDrcvChnxG8azWsiXuptCqillm3QZSItzUBPjMFUzbBairikHdtPdzuzm6B+a 13 | F8T3A1UCgYEA41mTEVrzY0EWiUo5WWFBDndS/3WdIRpW3N9NhFG+JeIHQJf20bXV 14 | egUFj5d2eR7H2pjRYRuhCfdeIXZ1kkZchAfrYoMoI/dKbCl1DtG1+au/Id+UUf1Z 15 | nczbRERD3Ijx/1F+c8nHLutfG1/d3J+8A3f/61YnWuhVw9HAyUTWSB8CgYEAkGop 16 | 5KhpMZvrvBTHhIP82Tf3DQhmbGIfLj+KuJFRfK5UZlGzFtR17CYnT4mqMM/A6u6v 17 | He++rz8M/zwtRWwN+DbATKF2pEDiNCu6+Duq8MkS5wxNR2azm170gC+Cf21IEeVG 18 | 0/VH08/0ChVIp6flw6Q0zBIcwABzQNBKsYLdu98CgYEA0mPKXX01Ttyk5lfxymzd 19 | r21tOUq2JjQhvjRHn/Ola5lH7Na0ak1DSK/s/XzE/kEl4X0aBitzU62/RmhBVSQX 20 | 6XcKtRd6xg3KqV2UHiqjpHDzZ31n3Jf/nrA9GWezBRsWF5hq1Owdj6XxVXvZ7JlF 21 | fFBIXJhVwyLOe3BYX+l8AzECgYBZ3ibpyz+DBqOA7HFobnZXenM74gFS6xC6SAJ8 22 | broF27pb3fWTfG1RokCOR33oWDCWQigpefrwtUzSPFFzxRVAZFnwlf3tow7hJmF4 23 | fjEXHBmuPEKO70NPqZx/dJFB2PCjaklUN2wWTG9yIuKOqnXZ9IKCh7bqaJ1QNQ52 24 | PpjbgwKBgQCTT7sbZynbNUWoQ303gM1DZP0Yl7c7OnOucbrP+pn13d4ipi7jdwiS 25 | 3yk0VU7+cn/yqkockt8UTLPRQ0tql5/o2ekENp8qHqsJ1hevlDHRxmD/H8RltJ6X 26 | C6XKac6oWlJXee/3pUoixJFtFIMUl4cruf2A2HDd0l2CrBSVJCiMOA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /12_chapter/cert/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAgECrBRI9h/EE1QhLQBIxqeZFAFh4obAm7jGIR3QMUimHxR5X 3 | SRmkVKbkMMQLXtZPG/vgva8SmUSSbdOTvOXXjL3cBet8gRuQ2sxlypMDJbkawPyr 4 | Gct5T497op0jvLCxX6E3LJOe8AF22bVRYL5oMQFxZV+1cfTDYGDHZKPowJbQmtwp 5 | JknKnD3f2jp48eJ2s+5NyZdQB2O48SGkRiwXfmosTmcNgiXR46WIvvhkbVQSKMOZ 6 | It1lZtZXp7B6NxllY1D+ayG7LbaW6Y2tldFMuXrNRnK5pKfXuAVE8qKSZBqqwBdr 7 | lmcWQfAnCZBXHjlHwmN7+TFh99dQcAWmfhp4AQIDAQABAoIBADAChUKnulrqMWnh 8 | 6fF5HhhAt/HSLI4yV9BwwFgnK/mRzF5Q5lHaBHcf5moO6Ua+KO0L0+yN3gN79Oih 9 | /DsJOzBXXkblCT13aTlOrIgxkksLyOUXcKF+VtIenySXeGpJJozq17GbLTMhCTGk 10 | O2tgu7YkmSNeuZiIDJ9Gez3EUfvNrexlpIndjHIiyMh6Hv2TmGofZsX48Sl6ocgH 11 | bGlgnLEExJCacX1KyWpL2QraV9jhF7WCXgEPLWcOq4r+aKkMpP/v1gZKPNMHq0OM 12 | 7ytCDrcvChnxG8azWsiXuptCqillm3QZSItzUBPjMFUzbBairikHdtPdzuzm6B+a 13 | F8T3A1UCgYEA41mTEVrzY0EWiUo5WWFBDndS/3WdIRpW3N9NhFG+JeIHQJf20bXV 14 | egUFj5d2eR7H2pjRYRuhCfdeIXZ1kkZchAfrYoMoI/dKbCl1DtG1+au/Id+UUf1Z 15 | nczbRERD3Ijx/1F+c8nHLutfG1/d3J+8A3f/61YnWuhVw9HAyUTWSB8CgYEAkGop 16 | 5KhpMZvrvBTHhIP82Tf3DQhmbGIfLj+KuJFRfK5UZlGzFtR17CYnT4mqMM/A6u6v 17 | He++rz8M/zwtRWwN+DbATKF2pEDiNCu6+Duq8MkS5wxNR2azm170gC+Cf21IEeVG 18 | 0/VH08/0ChVIp6flw6Q0zBIcwABzQNBKsYLdu98CgYEA0mPKXX01Ttyk5lfxymzd 19 | r21tOUq2JjQhvjRHn/Ola5lH7Na0ak1DSK/s/XzE/kEl4X0aBitzU62/RmhBVSQX 20 | 6XcKtRd6xg3KqV2UHiqjpHDzZ31n3Jf/nrA9GWezBRsWF5hq1Owdj6XxVXvZ7JlF 21 | fFBIXJhVwyLOe3BYX+l8AzECgYBZ3ibpyz+DBqOA7HFobnZXenM74gFS6xC6SAJ8 22 | broF27pb3fWTfG1RokCOR33oWDCWQigpefrwtUzSPFFzxRVAZFnwlf3tow7hJmF4 23 | fjEXHBmuPEKO70NPqZx/dJFB2PCjaklUN2wWTG9yIuKOqnXZ9IKCh7bqaJ1QNQ52 24 | PpjbgwKBgQCTT7sbZynbNUWoQ303gM1DZP0Yl7c7OnOucbrP+pn13d4ipi7jdwiS 25 | 3yk0VU7+cn/yqkockt8UTLPRQ0tql5/o2ekENp8qHqsJ1hevlDHRxmD/H8RltJ6X 26 | C6XKac6oWlJXee/3pUoixJFtFIMUl4cruf2A2HDd0l2CrBSVJCiMOA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /12_chapter/public/peerconnection/room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC PeerConnection 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 27 | kbps 28 |
29 | 30 |
31 |
32 |

Local:

33 | 34 |

Offer SDP:

35 | 36 |
37 |
38 |

Romote:

39 | 40 |

Answer SDP:

41 | 42 |
43 |
44 | 45 |
46 |
BitRate
47 | 48 |
49 |
50 |
Packets send per second
51 | 52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /08_chapter/public/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var userName = document.querySelector('input#username'); 4 | var inputRoom = document.querySelector('input#room'); 5 | var btnConnect = document.querySelector('button#connect'); 6 | var btnLeave = document.querySelector('button#leave'); 7 | var outputArea = document.querySelector('textarea#output'); 8 | var inputArea = document.querySelector('textarea#input'); 9 | var btnSend = document.querySelector('button#send'); 10 | 11 | var socket; 12 | var room; 13 | 14 | btnConnect.onclick = () =>{ 15 | //connect 16 | socket = io.connect(); 17 | 18 | //receive message 19 | socket.on('joined', (room,id) =>{ 20 | btnConnect.disabled = true; 21 | btnLeave.disabled = false; 22 | inputArea.disabled = false; 23 | btnSend.disabled = false; 24 | }); 25 | 26 | socket.on('leaved', (room, id)=>{ 27 | btnConnect.disabled = false; 28 | btnLeave.disabled = true; 29 | inputArea.disabled = true; 30 | btnSend.disabled = true; 31 | 32 | socket.disconnect(); 33 | }); 34 | 35 | socket.on('message',(room, id, data) =>{ 36 | outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容 37 | outputArea.value = outputArea.value + data + '\r'; 38 | }); 39 | 40 | socket.on('disconnect', (socket)=>{ 41 | btnConnect.disabled = false; 42 | btnLeave.disabled = true; 43 | inputArea.disabled = true; 44 | btnSend.disabled = true; 45 | 46 | }); 47 | 48 | room = inputRoom.value; 49 | socket.emit('join', room); 50 | } 51 | 52 | btnSend.onclick = ()=>{ 53 | var data = inputArea.value; 54 | data = userName.value + ':' + data; 55 | socket.emit('message', room, data); 56 | inputArea.value = ''; 57 | } 58 | 59 | btnLeave.onclick = ()=>{ 60 | room = inputRoom.value; 61 | socket.emit('leave', room); 62 | } 63 | 64 | inputArea.onkeypress = (event)=>{ 65 | if(event.keyCode == 13){ 66 | var data = inputArea.value; 67 | data = userName.value + ":" + data; 68 | socket.emit('message', room, data); 69 | inputArea.value = ''; 70 | event.preventDefault();//阻止默认行文 71 | } 72 | } 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /06_chapter/public/02_permission/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebRTC capture video and audio 4 | 23 | 24 | 25 | ============== 用于显示和选择设备 ================== 26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 | ============== 视频效果选择 ================== 39 |
40 | 41 | 48 |
49 | ============== 显示视频图像 ================== 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 |
67 | 68 |
69 | 70 |
71 |
72 | 截取快照 73 |
74 | 75 | ============== js ================== 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /10_chapter/public/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | button { 10 | margin: 0 20px 25px 0; 11 | vertical-align: top; 12 | width: 134px; 13 | } 14 | 15 | textarea { 16 | color: #444; 17 | font-size: 0.9em; 18 | font-weight: 300; 19 | height: 20.0em; 20 | padding: 5px; 21 | width: calc(100% - 10px); 22 | } 23 | 24 | div#getUserMedia { 25 | padding: 0 0 8px 0; 26 | } 27 | 28 | div.input { 29 | display: inline-block; 30 | margin: 0 4px 0 0; 31 | vertical-align: top; 32 | width: 310px; 33 | } 34 | 35 | div.input > div { 36 | margin: 0 0 20px 0; 37 | vertical-align: top; 38 | } 39 | 40 | div.output { 41 | background-color: #eee; 42 | display: inline-block; 43 | font-family: 'Inconsolata', 'Courier New', monospace; 44 | font-size: 0.9em; 45 | padding: 10px 10px 10px 25px; 46 | position: relative; 47 | top: 10px; 48 | white-space: pre; 49 | width: 270px; 50 | } 51 | 52 | div#preview { 53 | border-bottom: 1px solid #eee; 54 | margin: 0 0 1em 0; 55 | padding: 0 0 0.5em 0; 56 | } 57 | 58 | div#preview > div { 59 | display: inline-block; 60 | vertical-align: top; 61 | width: calc(50% - 12px); 62 | } 63 | 64 | section#statistics div { 65 | display: inline-block; 66 | font-family: 'Inconsolata', 'Courier New', monospace; 67 | vertical-align: top; 68 | width: 308px; 69 | } 70 | 71 | section#statistics div#senderStats { 72 | margin: 0 20px 0 0; 73 | } 74 | 75 | section#constraints > div { 76 | margin: 0 0 20px 0; 77 | } 78 | 79 | h2 { 80 | margin: 0 0 1em 0; 81 | } 82 | 83 | 84 | section#constraints label { 85 | display: inline-block; 86 | width: 156px; 87 | } 88 | 89 | section { 90 | margin: 0 0 20px 0; 91 | padding: 0 0 15px 0; 92 | } 93 | 94 | video { 95 | background: #222; 96 | margin: 0 0 0 0; 97 | --width: 100%; 98 | width: var(--width); 99 | height: 225px; 100 | } 101 | 102 | @media screen and (max-width: 720px) { 103 | button { 104 | font-weight: 500; 105 | height: 56px; 106 | line-height: 1.3em; 107 | width: 90px; 108 | } 109 | 110 | div#getUserMedia { 111 | padding: 0 0 40px 0; 112 | } 113 | 114 | section#statistics div { 115 | width: calc(50% - 14px); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /08_chapter/public/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | button { 9 | margin: 0 20px 25px 0; 10 | vertical-align: top; 11 | width: 134px; 12 | } 13 | 14 | div#getUserMedia { 15 | padding: 0 0 8px 0; 16 | } 17 | 18 | div.input { 19 | display: inline-block; 20 | margin: 0 4px 0 0; 21 | vertical-align: top; 22 | width: 310px; 23 | } 24 | 25 | div.input > div { 26 | margin: 0 0 20px 0; 27 | vertical-align: top; 28 | } 29 | 30 | div.output { 31 | background-color: #eee; 32 | display: inline-block; 33 | font-family: 'Inconsolata', 'Courier New', monospace; 34 | font-size: 0.9em; 35 | padding: 10px 10px 10px 25px; 36 | position: relative; 37 | top: 10px; 38 | white-space: pre; 39 | width: 270px; 40 | } 41 | 42 | section#statistics div { 43 | display: inline-block; 44 | font-family: 'Inconsolata', 'Courier New', monospace; 45 | vertical-align: top; 46 | width: 308px; 47 | } 48 | 49 | section#statistics div#senderStats { 50 | margin: 0 20px 0 0; 51 | } 52 | 53 | section#constraints > div { 54 | margin: 0 0 20px 0; 55 | } 56 | 57 | section#video > div { 58 | display: inline-block; 59 | margin: 0 20px 0 0; 60 | vertical-align: top; 61 | width: calc(50% - 22px); 62 | } 63 | 64 | section#video > div div { 65 | font-size: 0.9em; 66 | margin: 0 0 0.5em 0; 67 | width: 320px; 68 | } 69 | 70 | h2 { 71 | margin: 0 0 1em 0; 72 | } 73 | 74 | section#constraints label { 75 | display: inline-block; 76 | width: 156px; 77 | } 78 | 79 | section { 80 | margin: 0 0 20px 0; 81 | padding: 0 0 15px 0; 82 | } 83 | 84 | section#video { 85 | width: calc(100% + 20px); 86 | } 87 | 88 | video { 89 | --width: 90%; 90 | display: inline-block; 91 | width: var(--width); 92 | height: calc(var(--width) * 0.75); 93 | margin: 0 0 10px 0; 94 | } 95 | 96 | @media screen and (max-width: 720px) { 97 | button { 98 | font-weight: 500; 99 | height: 56px; 100 | line-height: 1.3em; 101 | width: 90px; 102 | } 103 | 104 | div#getUserMedia { 105 | padding: 0 0 40px 0; 106 | } 107 | 108 | section#statistics div { 109 | width: calc(50% - 14px); 110 | } 111 | 112 | video { 113 | display: inline-block; 114 | width: var(--width); 115 | height: 96px; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /12_chapter/public/peerconnection/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | button { 10 | margin: 10px 20px 25px 0; 11 | vertical-align: top; 12 | width: 134px; 13 | } 14 | 15 | table { 16 | margin: 200px (50% - 100) 0 0; 17 | } 18 | 19 | textarea { 20 | color: #444; 21 | font-size: 0.9em; 22 | font-weight: 300; 23 | height: 20.0em; 24 | padding: 5px; 25 | width: calc(100% - 10px); 26 | } 27 | 28 | div#getUserMedia { 29 | padding: 0 0 8px 0; 30 | } 31 | 32 | div.input { 33 | display: inline-block; 34 | margin: 0 4px 0 0; 35 | vertical-align: top; 36 | width: 310px; 37 | } 38 | 39 | div.input > div { 40 | margin: 0 0 20px 0; 41 | vertical-align: top; 42 | } 43 | 44 | div.output { 45 | background-color: #eee; 46 | display: inline-block; 47 | font-family: 'Inconsolata', 'Courier New', monospace; 48 | font-size: 0.9em; 49 | padding: 10px 10px 10px 25px; 50 | position: relative; 51 | top: 10px; 52 | white-space: pre; 53 | width: 270px; 54 | } 55 | 56 | div#preview { 57 | border-bottom: 1px solid #eee; 58 | margin: 0 0 1em 0; 59 | padding: 0 0 0.5em 0; 60 | } 61 | 62 | div#preview > div { 63 | display: inline-block; 64 | vertical-align: top; 65 | width: calc(50% - 12px); 66 | } 67 | 68 | section#statistics div { 69 | display: inline-block; 70 | font-family: 'Inconsolata', 'Courier New', monospace; 71 | vertical-align: top; 72 | width: 308px; 73 | } 74 | 75 | section#statistics div#senderStats { 76 | margin: 0 20px 0 0; 77 | } 78 | 79 | section#constraints > div { 80 | margin: 0 0 20px 0; 81 | } 82 | 83 | h2 { 84 | margin: 0 0 1em 0; 85 | } 86 | 87 | 88 | section#constraints label { 89 | display: inline-block; 90 | width: 156px; 91 | } 92 | 93 | section { 94 | margin: 0 0 20px 0; 95 | padding: 0 0 15px 0; 96 | } 97 | 98 | video { 99 | background: #222; 100 | margin: 0 0 0 0; 101 | --width: 100%; 102 | width: var(--width); 103 | height: 225px; 104 | } 105 | 106 | @media screen and (max-width: 720px) { 107 | button { 108 | font-weight: 500; 109 | height: 56px; 110 | line-height: 1.3em; 111 | width: 90px; 112 | } 113 | 114 | div#getUserMedia { 115 | padding: 0 0 40px 0; 116 | } 117 | 118 | section#statistics div { 119 | width: calc(50% - 14px); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /12_chapter/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var https = require('https'); 4 | var fs = require('fs'); 5 | 6 | var express = require('express'); 7 | var serveIndex = require('serve-index'); 8 | 9 | //socket.io 10 | var socketIo = require('socket.io'); 11 | 12 | // 13 | var log4js = require('log4js'); 14 | 15 | log4js.configure({ 16 | appenders: { 17 | file: { 18 | type: 'file', 19 | filename: 'app.log', 20 | layout: { 21 | type: 'pattern', 22 | pattern: '%r %p - %m', 23 | } 24 | } 25 | }, 26 | categories: { 27 | default: { 28 | appenders: ['file'], 29 | level: 'debug' 30 | } 31 | } 32 | }); 33 | 34 | var logger = log4js.getLogger(); 35 | 36 | var app = express(); 37 | app.use(serveIndex('./public')); 38 | app.use(express.static('./public')); 39 | 40 | var options = { 41 | key : fs.readFileSync('./cert/test.key'), 42 | cert: fs.readFileSync('./cert/test.pem') 43 | } 44 | 45 | //https server 46 | var https_server = https.createServer(options, app); 47 | 48 | //bind socket.io with https_server 49 | var https_socket = socketIo.listen(https_server); 50 | 51 | //connection 52 | https_socket.sockets.on('connection', (socket)=>{ 53 | 54 | socket.on('message', (room, data)=>{ 55 | socket.to(room).emit('message', room, data); 56 | }); 57 | 58 | //该函数应该加锁 59 | socket.on('join', (room)=> { 60 | 61 | socket.join(room); 62 | 63 | var myRoom = https_socket.sockets.adapter.rooms[room]; 64 | var users = (myRoom) ? Object.keys(myRoom.sockets).length : 0; 65 | 66 | logger.log('the number of user in room is: ' + users); 67 | 68 | //在这里可以控制进入房间的人数,现在一个房间最多 2个人 69 | //为了便于客户端控制,如果是多人的话,应该将目前房间里 70 | //人的个数当做数据下发下去。 71 | if(users < 3) { 72 | socket.emit('joined', room, socket.id); 73 | if (users > 1) { 74 | socket.to(room).emit('otherjoin', room, socket.id); 75 | } 76 | }else { 77 | socket.leave(room); 78 | socket.emit('full', room, socket.id); 79 | } 80 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 81 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 82 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 83 | }); 84 | 85 | socket.on('leave', (room)=> { 86 | var myRoom = https_socket.sockets.adapter.rooms[room]; 87 | var users = Object.keys(myRoom.sockets).length; 88 | //users - 1; 89 | 90 | logger.log('the number of user in room is: ' + (users-1)); 91 | 92 | socket.to(room).emit('bye', room, socket.id)//房间内所有人,除自己外 93 | socket.emit('leaved', room, socket.id); 94 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 95 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 96 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 97 | }); 98 | 99 | }); 100 | 101 | 102 | https_server.listen(443, '0.0.0.0'); 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /10_chapter/public/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var localVideo = document.querySelector('video#localvideo'); 4 | var remoteVideo = document.querySelector('video#remotevideo'); 5 | 6 | var btnStart = document.querySelector('button#start'); 7 | var btnCall = document.querySelector('button#call'); 8 | var btnHangup = document.querySelector('button#hangup'); 9 | 10 | var offerSdpTextarea = document.querySelector('textarea#offer'); 11 | var answerSdpTextarea = document.querySelector('textarea#answer'); 12 | 13 | var localStream; 14 | var pc1; 15 | var pc2; 16 | 17 | function getMediaStream(stream){ 18 | localVideo.srcObject = stream; 19 | localStream = stream; 20 | } 21 | 22 | function handleError(err){ 23 | console.error('Failed to get Media Stream!', err); 24 | } 25 | 26 | function start(){ 27 | if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ 28 | console.error('the getUserMedia is not supported!'); 29 | return; 30 | }else { 31 | var constraints = { 32 | video:true, 33 | audio:false 34 | } 35 | 36 | navigator.mediaDevices.getUserMedia(constraints) 37 | .then(getMediaStream) 38 | .catch(handleError); 39 | 40 | btnStart.disabled = true; 41 | btnCall.disabled = false; 42 | btnHangup.disabled = true; 43 | } 44 | } 45 | 46 | function getRemoteStream(e){ 47 | remoteVideo.srcObject = e.streams[0]; 48 | } 49 | 50 | function handleOfferError(err){ 51 | console.error('Failed to create offer: ', err); 52 | } 53 | 54 | function handleAnswerError(err){ 55 | console.error('Failed to create answer: ', err); 56 | } 57 | 58 | function createAnswerSuc(desc){ 59 | pc2.setLocalDescription(desc); 60 | answerSdpTextarea.value = desc.sdp; 61 | 62 | //send desc to signal 63 | // receive desc from signal 64 | 65 | pc1.setRemoteDescription(desc); 66 | } 67 | 68 | function createOfferSuc(desc){ 69 | pc1.setLocalDescription(desc); 70 | offerSdpTextarea.value = desc.sdp; 71 | 72 | //send desc to signal 73 | //receive desc from signal 74 | 75 | pc2.setRemoteDescription(desc); 76 | 77 | pc2.createAnswer() 78 | .then(createAnswerSuc) 79 | .catch(handleAnswerError); 80 | } 81 | 82 | function call(){ 83 | pc1 = new RTCPeerConnection(); 84 | pc2 = new RTCPeerConnection(); 85 | 86 | pc1.onicecandidate = (e) =>{ 87 | pc2.addIceCandidate(e.candidate); 88 | } 89 | 90 | pc2.onicecandidate = (e) =>{ 91 | pc1.addIceCandidate(e.candidate); 92 | } 93 | 94 | pc2.ontrack = getRemoteStream; 95 | localStream.getTracks().forEach((track)=>{ 96 | pc1.addTrack(track, localStream); 97 | }); 98 | 99 | var offerOptions = { 100 | offerToRecieveAudio:0, 101 | offerToRecieveVideo:1 102 | } 103 | 104 | pc1.createOffer(offerOptions) 105 | .then(createOfferSuc) 106 | .catch(handleOfferError); 107 | 108 | btnCall.disabled = true; 109 | btnHangup.disabled = false; 110 | } 111 | 112 | function hangup(){ 113 | pc1.close(); 114 | pc2.close(); 115 | pc1 = null; 116 | pc2 = null; 117 | 118 | btnCall.disabled = false; 119 | btnHangup.disabled = true; 120 | } 121 | 122 | btnStart.onclick = start; 123 | btnCall.onclick = call; 124 | btnHangup.onclick = hangup; 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /05_chapter/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFmTCCBIGgAwIBAgIQA/0Ssnj2KNvPpAAwE8RHPTANBgkqhkiG9w0BAQsFADBu 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg 5 | RFYgVExTIENBIC0gRzEwHhcNMTgxMTI3MDAwMDAwWhcNMTkxMTI3MTIwMDAwWjAd 6 | MRswGQYDVQQDExJ3d3cubGVhcm5pbmdydGMuY24wggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQCAQKsFEj2H8QTVCEtAEjGp5kUAWHihsCbuMYhHdAxSKYfF 8 | HldJGaRUpuQwxAte1k8b++C9rxKZRJJt05O85deMvdwF63yBG5DazGXKkwMluRrA 9 | /KsZy3lPj3uinSO8sLFfoTcsk57wAXbZtVFgvmgxAXFlX7Vx9MNgYMdko+jAltCa 10 | 3CkmScqcPd/aOnjx4naz7k3Jl1AHY7jxIaRGLBd+aixOZw2CJdHjpYi++GRtVBIo 11 | w5ki3WVm1lensHo3GWVjUP5rIbsttpbpja2V0Uy5es1Gcrmkp9e4BUTyopJkGqrA 12 | F2uWZxZB8CcJkFceOUfCY3v5MWH311BwBaZ+GngBAgMBAAGjggKCMIICfjAfBgNV 13 | HSMEGDAWgBRVdE+yck/1YLpQ0dfmUVyaAYca1zAdBgNVHQ4EFgQUGCuoNOqYS8DF 14 | 1dd4XIP/YilDUJEwLQYDVR0RBCYwJIISd3d3LmxlYXJuaW5ncnRjLmNugg5sZWFy 15 | bmluZ3J0Yy5jbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 16 | CCsGAQUFBwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEW 17 | HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUF 18 | BwEBBHEwbzAhBggrBgEFBQcwAYYVaHR0cDovL29jc3AuZGNvY3NwLmNuMEoGCCsG 19 | AQUFBzAChj5odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRW5jcnlwdGlvbkV2 20 | ZXJ5d2hlcmVEVlRMU0NBLUcxLmNydDAJBgNVHRMEAjAAMIIBBAYKKwYBBAHWeQIE 21 | AgSB9QSB8gDwAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFn 22 | VArhKwAABAMARjBEAiBYzdYfv9uZCl7ItYugZ8rKwBdkl64L3Bo4hMyM2oLPdAIg 23 | OOy3aJnqp31jGrtIG5u6hPfAWNkiBPfGQCEDeBsRhaYAdwCHdb/nWXz4jEOZX73z 24 | bv9WjUdWNv9KtWDBtOr/XqCDDwAAAWdUCuH+AAAEAwBIMEYCIQD4eai+g9Dx4ZhW 25 | h8+VDwRjrspTNycWeg0ehjf+p5NwBAIhAPQpUvUrdJp/KqLKz4TNnyJtU0ezPZdY 26 | XGQVeYtwkDOQMA0GCSqGSIb3DQEBCwUAA4IBAQAZwr2CFBCmPw4H16UpsbEK4Wie 27 | ldbsrBhRMX2bH47Sr2CQvAJLm2MODVDi7XtF1ZR1XmLQOiKsHNVXveDq5UJomWIn 28 | NDkXxYPNMQzVB6WLxO9HZsM302CIrE4ds9PUWWZ8wVtyv6o/nqczu+uuyX0Vs0/J 29 | dclkw7r3TntrPwgTj/3dCSBchdT33vdTGjnyc9Hz7gN0aU8Ksnzf7Vxm53lmk4t1 30 | aHKYUDQtPle5MKNgg88fjCsrfMZAfpcR3GKfCSa3I4f4vhvsg2ap4fJsXKjHtOLN 31 | 8qfw7B8Qm5/PpsRzYHB+WEPkfwIKxR9gIifQEbNnSSCCl3GJVqH4c1HJcb1z 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh 35 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 36 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 37 | QTAeFw0xNzExMjcxMjQ2MTBaFw0yNzExMjcxMjQ2MTBaMG4xCzAJBgNVBAYTAlVT 38 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 39 | b20xLTArBgNVBAMTJEVuY3J5cHRpb24gRXZlcnl3aGVyZSBEViBUTFMgQ0EgLSBH 40 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPeP6wkab41dyQh6mKc 41 | oHqt3jRIxW5MDvf9QyiOR7VfFwK656es0UFiIb74N9pRntzF1UgYzDGu3ppZVMdo 42 | lbxhm6dWS9OK/lFehKNT0OYI9aqk6F+U7cA6jxSC+iDBPXwdF4rs3KRyp3aQn6pj 43 | pp1yr7IB6Y4zv72Ee/PlZ/6rK6InC6WpK0nPVOYR7n9iDuPe1E4IxUMBH/T33+3h 44 | yuH3dvfgiWUOUkjdpMbyxX+XNle5uEIiyBsi4IvbcTCh8ruifCIi5mDXkZrnMT8n 45 | wfYCV6v6kDdXkbgGRLKsR4pucbJtbKqIkUGxuZI2t7pfewKRc5nWecvDBZf3+p1M 46 | pA8CAwEAAaOCAU8wggFLMB0GA1UdDgQWBBRVdE+yck/1YLpQ0dfmUVyaAYca1zAf 47 | BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYw 48 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8C 49 | AQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 50 | Y2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQu 51 | Y29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG 52 | /WwBAjAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 53 | MAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAQEAK3Gp6/aGq7aBZsxf/oQ+TD/B 54 | SwW3AU4ETK+GQf2kFzYZkby5SFrHdPomunx2HBzViUchGoofGgg7gHW0W3MlQAXW 55 | M0r5LUvStcr82QDWYNPaUy4taCQmyaJ+VB+6wxHstSigOlSNF2a6vg4rgexixeiV 56 | 4YSB03Yqp2t3TeZHM9ESfkus74nQyW7pRGezj+TC44xCagCQQOzzNmzEAP2SnCrJ 57 | sNE2DpRVMnL8J6xBRdjmOsC3N6cQuKuRXbzByVBjCqAA8t1L0I+9wXJerLPyErjy 58 | rMKWaBFLmfK/AHNF4ZihwPGOc7w6UHczBZXH5RFzJNnww+WnKuTPI0HfnVH8lg== 59 | -----END CERTIFICATE----- 60 | -------------------------------------------------------------------------------- /06_chapter/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFmTCCBIGgAwIBAgIQA/0Ssnj2KNvPpAAwE8RHPTANBgkqhkiG9w0BAQsFADBu 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg 5 | RFYgVExTIENBIC0gRzEwHhcNMTgxMTI3MDAwMDAwWhcNMTkxMTI3MTIwMDAwWjAd 6 | MRswGQYDVQQDExJ3d3cubGVhcm5pbmdydGMuY24wggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQCAQKsFEj2H8QTVCEtAEjGp5kUAWHihsCbuMYhHdAxSKYfF 8 | HldJGaRUpuQwxAte1k8b++C9rxKZRJJt05O85deMvdwF63yBG5DazGXKkwMluRrA 9 | /KsZy3lPj3uinSO8sLFfoTcsk57wAXbZtVFgvmgxAXFlX7Vx9MNgYMdko+jAltCa 10 | 3CkmScqcPd/aOnjx4naz7k3Jl1AHY7jxIaRGLBd+aixOZw2CJdHjpYi++GRtVBIo 11 | w5ki3WVm1lensHo3GWVjUP5rIbsttpbpja2V0Uy5es1Gcrmkp9e4BUTyopJkGqrA 12 | F2uWZxZB8CcJkFceOUfCY3v5MWH311BwBaZ+GngBAgMBAAGjggKCMIICfjAfBgNV 13 | HSMEGDAWgBRVdE+yck/1YLpQ0dfmUVyaAYca1zAdBgNVHQ4EFgQUGCuoNOqYS8DF 14 | 1dd4XIP/YilDUJEwLQYDVR0RBCYwJIISd3d3LmxlYXJuaW5ncnRjLmNugg5sZWFy 15 | bmluZ3J0Yy5jbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 16 | CCsGAQUFBwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEW 17 | HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUF 18 | BwEBBHEwbzAhBggrBgEFBQcwAYYVaHR0cDovL29jc3AuZGNvY3NwLmNuMEoGCCsG 19 | AQUFBzAChj5odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRW5jcnlwdGlvbkV2 20 | ZXJ5d2hlcmVEVlRMU0NBLUcxLmNydDAJBgNVHRMEAjAAMIIBBAYKKwYBBAHWeQIE 21 | AgSB9QSB8gDwAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFn 22 | VArhKwAABAMARjBEAiBYzdYfv9uZCl7ItYugZ8rKwBdkl64L3Bo4hMyM2oLPdAIg 23 | OOy3aJnqp31jGrtIG5u6hPfAWNkiBPfGQCEDeBsRhaYAdwCHdb/nWXz4jEOZX73z 24 | bv9WjUdWNv9KtWDBtOr/XqCDDwAAAWdUCuH+AAAEAwBIMEYCIQD4eai+g9Dx4ZhW 25 | h8+VDwRjrspTNycWeg0ehjf+p5NwBAIhAPQpUvUrdJp/KqLKz4TNnyJtU0ezPZdY 26 | XGQVeYtwkDOQMA0GCSqGSIb3DQEBCwUAA4IBAQAZwr2CFBCmPw4H16UpsbEK4Wie 27 | ldbsrBhRMX2bH47Sr2CQvAJLm2MODVDi7XtF1ZR1XmLQOiKsHNVXveDq5UJomWIn 28 | NDkXxYPNMQzVB6WLxO9HZsM302CIrE4ds9PUWWZ8wVtyv6o/nqczu+uuyX0Vs0/J 29 | dclkw7r3TntrPwgTj/3dCSBchdT33vdTGjnyc9Hz7gN0aU8Ksnzf7Vxm53lmk4t1 30 | aHKYUDQtPle5MKNgg88fjCsrfMZAfpcR3GKfCSa3I4f4vhvsg2ap4fJsXKjHtOLN 31 | 8qfw7B8Qm5/PpsRzYHB+WEPkfwIKxR9gIifQEbNnSSCCl3GJVqH4c1HJcb1z 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh 35 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 36 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 37 | QTAeFw0xNzExMjcxMjQ2MTBaFw0yNzExMjcxMjQ2MTBaMG4xCzAJBgNVBAYTAlVT 38 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 39 | b20xLTArBgNVBAMTJEVuY3J5cHRpb24gRXZlcnl3aGVyZSBEViBUTFMgQ0EgLSBH 40 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPeP6wkab41dyQh6mKc 41 | oHqt3jRIxW5MDvf9QyiOR7VfFwK656es0UFiIb74N9pRntzF1UgYzDGu3ppZVMdo 42 | lbxhm6dWS9OK/lFehKNT0OYI9aqk6F+U7cA6jxSC+iDBPXwdF4rs3KRyp3aQn6pj 43 | pp1yr7IB6Y4zv72Ee/PlZ/6rK6InC6WpK0nPVOYR7n9iDuPe1E4IxUMBH/T33+3h 44 | yuH3dvfgiWUOUkjdpMbyxX+XNle5uEIiyBsi4IvbcTCh8ruifCIi5mDXkZrnMT8n 45 | wfYCV6v6kDdXkbgGRLKsR4pucbJtbKqIkUGxuZI2t7pfewKRc5nWecvDBZf3+p1M 46 | pA8CAwEAAaOCAU8wggFLMB0GA1UdDgQWBBRVdE+yck/1YLpQ0dfmUVyaAYca1zAf 47 | BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYw 48 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8C 49 | AQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 50 | Y2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQu 51 | Y29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG 52 | /WwBAjAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 53 | MAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAQEAK3Gp6/aGq7aBZsxf/oQ+TD/B 54 | SwW3AU4ETK+GQf2kFzYZkby5SFrHdPomunx2HBzViUchGoofGgg7gHW0W3MlQAXW 55 | M0r5LUvStcr82QDWYNPaUy4taCQmyaJ+VB+6wxHstSigOlSNF2a6vg4rgexixeiV 56 | 4YSB03Yqp2t3TeZHM9ESfkus74nQyW7pRGezj+TC44xCagCQQOzzNmzEAP2SnCrJ 57 | sNE2DpRVMnL8J6xBRdjmOsC3N6cQuKuRXbzByVBjCqAA8t1L0I+9wXJerLPyErjy 58 | rMKWaBFLmfK/AHNF4ZihwPGOc7w6UHczBZXH5RFzJNnww+WnKuTPI0HfnVH8lg== 59 | -----END CERTIFICATE----- 60 | -------------------------------------------------------------------------------- /08_chapter/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFmTCCBIGgAwIBAgIQA/0Ssnj2KNvPpAAwE8RHPTANBgkqhkiG9w0BAQsFADBu 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg 5 | RFYgVExTIENBIC0gRzEwHhcNMTgxMTI3MDAwMDAwWhcNMTkxMTI3MTIwMDAwWjAd 6 | MRswGQYDVQQDExJ3d3cubGVhcm5pbmdydGMuY24wggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQCAQKsFEj2H8QTVCEtAEjGp5kUAWHihsCbuMYhHdAxSKYfF 8 | HldJGaRUpuQwxAte1k8b++C9rxKZRJJt05O85deMvdwF63yBG5DazGXKkwMluRrA 9 | /KsZy3lPj3uinSO8sLFfoTcsk57wAXbZtVFgvmgxAXFlX7Vx9MNgYMdko+jAltCa 10 | 3CkmScqcPd/aOnjx4naz7k3Jl1AHY7jxIaRGLBd+aixOZw2CJdHjpYi++GRtVBIo 11 | w5ki3WVm1lensHo3GWVjUP5rIbsttpbpja2V0Uy5es1Gcrmkp9e4BUTyopJkGqrA 12 | F2uWZxZB8CcJkFceOUfCY3v5MWH311BwBaZ+GngBAgMBAAGjggKCMIICfjAfBgNV 13 | HSMEGDAWgBRVdE+yck/1YLpQ0dfmUVyaAYca1zAdBgNVHQ4EFgQUGCuoNOqYS8DF 14 | 1dd4XIP/YilDUJEwLQYDVR0RBCYwJIISd3d3LmxlYXJuaW5ncnRjLmNugg5sZWFy 15 | bmluZ3J0Yy5jbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 16 | CCsGAQUFBwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEW 17 | HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUF 18 | BwEBBHEwbzAhBggrBgEFBQcwAYYVaHR0cDovL29jc3AuZGNvY3NwLmNuMEoGCCsG 19 | AQUFBzAChj5odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRW5jcnlwdGlvbkV2 20 | ZXJ5d2hlcmVEVlRMU0NBLUcxLmNydDAJBgNVHRMEAjAAMIIBBAYKKwYBBAHWeQIE 21 | AgSB9QSB8gDwAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFn 22 | VArhKwAABAMARjBEAiBYzdYfv9uZCl7ItYugZ8rKwBdkl64L3Bo4hMyM2oLPdAIg 23 | OOy3aJnqp31jGrtIG5u6hPfAWNkiBPfGQCEDeBsRhaYAdwCHdb/nWXz4jEOZX73z 24 | bv9WjUdWNv9KtWDBtOr/XqCDDwAAAWdUCuH+AAAEAwBIMEYCIQD4eai+g9Dx4ZhW 25 | h8+VDwRjrspTNycWeg0ehjf+p5NwBAIhAPQpUvUrdJp/KqLKz4TNnyJtU0ezPZdY 26 | XGQVeYtwkDOQMA0GCSqGSIb3DQEBCwUAA4IBAQAZwr2CFBCmPw4H16UpsbEK4Wie 27 | ldbsrBhRMX2bH47Sr2CQvAJLm2MODVDi7XtF1ZR1XmLQOiKsHNVXveDq5UJomWIn 28 | NDkXxYPNMQzVB6WLxO9HZsM302CIrE4ds9PUWWZ8wVtyv6o/nqczu+uuyX0Vs0/J 29 | dclkw7r3TntrPwgTj/3dCSBchdT33vdTGjnyc9Hz7gN0aU8Ksnzf7Vxm53lmk4t1 30 | aHKYUDQtPle5MKNgg88fjCsrfMZAfpcR3GKfCSa3I4f4vhvsg2ap4fJsXKjHtOLN 31 | 8qfw7B8Qm5/PpsRzYHB+WEPkfwIKxR9gIifQEbNnSSCCl3GJVqH4c1HJcb1z 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh 35 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 36 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 37 | QTAeFw0xNzExMjcxMjQ2MTBaFw0yNzExMjcxMjQ2MTBaMG4xCzAJBgNVBAYTAlVT 38 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 39 | b20xLTArBgNVBAMTJEVuY3J5cHRpb24gRXZlcnl3aGVyZSBEViBUTFMgQ0EgLSBH 40 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPeP6wkab41dyQh6mKc 41 | oHqt3jRIxW5MDvf9QyiOR7VfFwK656es0UFiIb74N9pRntzF1UgYzDGu3ppZVMdo 42 | lbxhm6dWS9OK/lFehKNT0OYI9aqk6F+U7cA6jxSC+iDBPXwdF4rs3KRyp3aQn6pj 43 | pp1yr7IB6Y4zv72Ee/PlZ/6rK6InC6WpK0nPVOYR7n9iDuPe1E4IxUMBH/T33+3h 44 | yuH3dvfgiWUOUkjdpMbyxX+XNle5uEIiyBsi4IvbcTCh8ruifCIi5mDXkZrnMT8n 45 | wfYCV6v6kDdXkbgGRLKsR4pucbJtbKqIkUGxuZI2t7pfewKRc5nWecvDBZf3+p1M 46 | pA8CAwEAAaOCAU8wggFLMB0GA1UdDgQWBBRVdE+yck/1YLpQ0dfmUVyaAYca1zAf 47 | BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYw 48 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8C 49 | AQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 50 | Y2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQu 51 | Y29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG 52 | /WwBAjAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 53 | MAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAQEAK3Gp6/aGq7aBZsxf/oQ+TD/B 54 | SwW3AU4ETK+GQf2kFzYZkby5SFrHdPomunx2HBzViUchGoofGgg7gHW0W3MlQAXW 55 | M0r5LUvStcr82QDWYNPaUy4taCQmyaJ+VB+6wxHstSigOlSNF2a6vg4rgexixeiV 56 | 4YSB03Yqp2t3TeZHM9ESfkus74nQyW7pRGezj+TC44xCagCQQOzzNmzEAP2SnCrJ 57 | sNE2DpRVMnL8J6xBRdjmOsC3N6cQuKuRXbzByVBjCqAA8t1L0I+9wXJerLPyErjy 58 | rMKWaBFLmfK/AHNF4ZihwPGOc7w6UHczBZXH5RFzJNnww+WnKuTPI0HfnVH8lg== 59 | -----END CERTIFICATE----- 60 | -------------------------------------------------------------------------------- /10_chapter/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFmTCCBIGgAwIBAgIQA/0Ssnj2KNvPpAAwE8RHPTANBgkqhkiG9w0BAQsFADBu 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg 5 | RFYgVExTIENBIC0gRzEwHhcNMTgxMTI3MDAwMDAwWhcNMTkxMTI3MTIwMDAwWjAd 6 | MRswGQYDVQQDExJ3d3cubGVhcm5pbmdydGMuY24wggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQCAQKsFEj2H8QTVCEtAEjGp5kUAWHihsCbuMYhHdAxSKYfF 8 | HldJGaRUpuQwxAte1k8b++C9rxKZRJJt05O85deMvdwF63yBG5DazGXKkwMluRrA 9 | /KsZy3lPj3uinSO8sLFfoTcsk57wAXbZtVFgvmgxAXFlX7Vx9MNgYMdko+jAltCa 10 | 3CkmScqcPd/aOnjx4naz7k3Jl1AHY7jxIaRGLBd+aixOZw2CJdHjpYi++GRtVBIo 11 | w5ki3WVm1lensHo3GWVjUP5rIbsttpbpja2V0Uy5es1Gcrmkp9e4BUTyopJkGqrA 12 | F2uWZxZB8CcJkFceOUfCY3v5MWH311BwBaZ+GngBAgMBAAGjggKCMIICfjAfBgNV 13 | HSMEGDAWgBRVdE+yck/1YLpQ0dfmUVyaAYca1zAdBgNVHQ4EFgQUGCuoNOqYS8DF 14 | 1dd4XIP/YilDUJEwLQYDVR0RBCYwJIISd3d3LmxlYXJuaW5ncnRjLmNugg5sZWFy 15 | bmluZ3J0Yy5jbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 16 | CCsGAQUFBwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEW 17 | HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUF 18 | BwEBBHEwbzAhBggrBgEFBQcwAYYVaHR0cDovL29jc3AuZGNvY3NwLmNuMEoGCCsG 19 | AQUFBzAChj5odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRW5jcnlwdGlvbkV2 20 | ZXJ5d2hlcmVEVlRMU0NBLUcxLmNydDAJBgNVHRMEAjAAMIIBBAYKKwYBBAHWeQIE 21 | AgSB9QSB8gDwAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFn 22 | VArhKwAABAMARjBEAiBYzdYfv9uZCl7ItYugZ8rKwBdkl64L3Bo4hMyM2oLPdAIg 23 | OOy3aJnqp31jGrtIG5u6hPfAWNkiBPfGQCEDeBsRhaYAdwCHdb/nWXz4jEOZX73z 24 | bv9WjUdWNv9KtWDBtOr/XqCDDwAAAWdUCuH+AAAEAwBIMEYCIQD4eai+g9Dx4ZhW 25 | h8+VDwRjrspTNycWeg0ehjf+p5NwBAIhAPQpUvUrdJp/KqLKz4TNnyJtU0ezPZdY 26 | XGQVeYtwkDOQMA0GCSqGSIb3DQEBCwUAA4IBAQAZwr2CFBCmPw4H16UpsbEK4Wie 27 | ldbsrBhRMX2bH47Sr2CQvAJLm2MODVDi7XtF1ZR1XmLQOiKsHNVXveDq5UJomWIn 28 | NDkXxYPNMQzVB6WLxO9HZsM302CIrE4ds9PUWWZ8wVtyv6o/nqczu+uuyX0Vs0/J 29 | dclkw7r3TntrPwgTj/3dCSBchdT33vdTGjnyc9Hz7gN0aU8Ksnzf7Vxm53lmk4t1 30 | aHKYUDQtPle5MKNgg88fjCsrfMZAfpcR3GKfCSa3I4f4vhvsg2ap4fJsXKjHtOLN 31 | 8qfw7B8Qm5/PpsRzYHB+WEPkfwIKxR9gIifQEbNnSSCCl3GJVqH4c1HJcb1z 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh 35 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 36 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 37 | QTAeFw0xNzExMjcxMjQ2MTBaFw0yNzExMjcxMjQ2MTBaMG4xCzAJBgNVBAYTAlVT 38 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 39 | b20xLTArBgNVBAMTJEVuY3J5cHRpb24gRXZlcnl3aGVyZSBEViBUTFMgQ0EgLSBH 40 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPeP6wkab41dyQh6mKc 41 | oHqt3jRIxW5MDvf9QyiOR7VfFwK656es0UFiIb74N9pRntzF1UgYzDGu3ppZVMdo 42 | lbxhm6dWS9OK/lFehKNT0OYI9aqk6F+U7cA6jxSC+iDBPXwdF4rs3KRyp3aQn6pj 43 | pp1yr7IB6Y4zv72Ee/PlZ/6rK6InC6WpK0nPVOYR7n9iDuPe1E4IxUMBH/T33+3h 44 | yuH3dvfgiWUOUkjdpMbyxX+XNle5uEIiyBsi4IvbcTCh8ruifCIi5mDXkZrnMT8n 45 | wfYCV6v6kDdXkbgGRLKsR4pucbJtbKqIkUGxuZI2t7pfewKRc5nWecvDBZf3+p1M 46 | pA8CAwEAAaOCAU8wggFLMB0GA1UdDgQWBBRVdE+yck/1YLpQ0dfmUVyaAYca1zAf 47 | BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYw 48 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8C 49 | AQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 50 | Y2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQu 51 | Y29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG 52 | /WwBAjAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 53 | MAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAQEAK3Gp6/aGq7aBZsxf/oQ+TD/B 54 | SwW3AU4ETK+GQf2kFzYZkby5SFrHdPomunx2HBzViUchGoofGgg7gHW0W3MlQAXW 55 | M0r5LUvStcr82QDWYNPaUy4taCQmyaJ+VB+6wxHstSigOlSNF2a6vg4rgexixeiV 56 | 4YSB03Yqp2t3TeZHM9ESfkus74nQyW7pRGezj+TC44xCagCQQOzzNmzEAP2SnCrJ 57 | sNE2DpRVMnL8J6xBRdjmOsC3N6cQuKuRXbzByVBjCqAA8t1L0I+9wXJerLPyErjy 58 | rMKWaBFLmfK/AHNF4ZihwPGOc7w6UHczBZXH5RFzJNnww+WnKuTPI0HfnVH8lg== 59 | -----END CERTIFICATE----- 60 | -------------------------------------------------------------------------------- /12_chapter/cert/test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFmTCCBIGgAwIBAgIQA/0Ssnj2KNvPpAAwE8RHPTANBgkqhkiG9w0BAQsFADBu 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMS0wKwYDVQQDEyRFbmNyeXB0aW9uIEV2ZXJ5d2hlcmUg 5 | RFYgVExTIENBIC0gRzEwHhcNMTgxMTI3MDAwMDAwWhcNMTkxMTI3MTIwMDAwWjAd 6 | MRswGQYDVQQDExJ3d3cubGVhcm5pbmdydGMuY24wggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQCAQKsFEj2H8QTVCEtAEjGp5kUAWHihsCbuMYhHdAxSKYfF 8 | HldJGaRUpuQwxAte1k8b++C9rxKZRJJt05O85deMvdwF63yBG5DazGXKkwMluRrA 9 | /KsZy3lPj3uinSO8sLFfoTcsk57wAXbZtVFgvmgxAXFlX7Vx9MNgYMdko+jAltCa 10 | 3CkmScqcPd/aOnjx4naz7k3Jl1AHY7jxIaRGLBd+aixOZw2CJdHjpYi++GRtVBIo 11 | w5ki3WVm1lensHo3GWVjUP5rIbsttpbpja2V0Uy5es1Gcrmkp9e4BUTyopJkGqrA 12 | F2uWZxZB8CcJkFceOUfCY3v5MWH311BwBaZ+GngBAgMBAAGjggKCMIICfjAfBgNV 13 | HSMEGDAWgBRVdE+yck/1YLpQ0dfmUVyaAYca1zAdBgNVHQ4EFgQUGCuoNOqYS8DF 14 | 1dd4XIP/YilDUJEwLQYDVR0RBCYwJIISd3d3LmxlYXJuaW5ncnRjLmNugg5sZWFy 15 | bmluZ3J0Yy5jbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG 16 | CCsGAQUFBwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEW 17 | HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUF 18 | BwEBBHEwbzAhBggrBgEFBQcwAYYVaHR0cDovL29jc3AuZGNvY3NwLmNuMEoGCCsG 19 | AQUFBzAChj5odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRW5jcnlwdGlvbkV2 20 | ZXJ5d2hlcmVEVlRMU0NBLUcxLmNydDAJBgNVHRMEAjAAMIIBBAYKKwYBBAHWeQIE 21 | AgSB9QSB8gDwAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YUAAAFn 22 | VArhKwAABAMARjBEAiBYzdYfv9uZCl7ItYugZ8rKwBdkl64L3Bo4hMyM2oLPdAIg 23 | OOy3aJnqp31jGrtIG5u6hPfAWNkiBPfGQCEDeBsRhaYAdwCHdb/nWXz4jEOZX73z 24 | bv9WjUdWNv9KtWDBtOr/XqCDDwAAAWdUCuH+AAAEAwBIMEYCIQD4eai+g9Dx4ZhW 25 | h8+VDwRjrspTNycWeg0ehjf+p5NwBAIhAPQpUvUrdJp/KqLKz4TNnyJtU0ezPZdY 26 | XGQVeYtwkDOQMA0GCSqGSIb3DQEBCwUAA4IBAQAZwr2CFBCmPw4H16UpsbEK4Wie 27 | ldbsrBhRMX2bH47Sr2CQvAJLm2MODVDi7XtF1ZR1XmLQOiKsHNVXveDq5UJomWIn 28 | NDkXxYPNMQzVB6WLxO9HZsM302CIrE4ds9PUWWZ8wVtyv6o/nqczu+uuyX0Vs0/J 29 | dclkw7r3TntrPwgTj/3dCSBchdT33vdTGjnyc9Hz7gN0aU8Ksnzf7Vxm53lmk4t1 30 | aHKYUDQtPle5MKNgg88fjCsrfMZAfpcR3GKfCSa3I4f4vhvsg2ap4fJsXKjHtOLN 31 | 8qfw7B8Qm5/PpsRzYHB+WEPkfwIKxR9gIifQEbNnSSCCl3GJVqH4c1HJcb1z 32 | -----END CERTIFICATE----- 33 | -----BEGIN CERTIFICATE----- 34 | MIIEqjCCA5KgAwIBAgIQAnmsRYvBskWr+YBTzSybsTANBgkqhkiG9w0BAQsFADBh 35 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 36 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 37 | QTAeFw0xNzExMjcxMjQ2MTBaFw0yNzExMjcxMjQ2MTBaMG4xCzAJBgNVBAYTAlVT 38 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 39 | b20xLTArBgNVBAMTJEVuY3J5cHRpb24gRXZlcnl3aGVyZSBEViBUTFMgQ0EgLSBH 40 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPeP6wkab41dyQh6mKc 41 | oHqt3jRIxW5MDvf9QyiOR7VfFwK656es0UFiIb74N9pRntzF1UgYzDGu3ppZVMdo 42 | lbxhm6dWS9OK/lFehKNT0OYI9aqk6F+U7cA6jxSC+iDBPXwdF4rs3KRyp3aQn6pj 43 | pp1yr7IB6Y4zv72Ee/PlZ/6rK6InC6WpK0nPVOYR7n9iDuPe1E4IxUMBH/T33+3h 44 | yuH3dvfgiWUOUkjdpMbyxX+XNle5uEIiyBsi4IvbcTCh8ruifCIi5mDXkZrnMT8n 45 | wfYCV6v6kDdXkbgGRLKsR4pucbJtbKqIkUGxuZI2t7pfewKRc5nWecvDBZf3+p1M 46 | pA8CAwEAAaOCAU8wggFLMB0GA1UdDgQWBBRVdE+yck/1YLpQ0dfmUVyaAYca1zAf 47 | BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYw 48 | HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8C 49 | AQAwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp 50 | Y2VydC5jb20wQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQu 51 | Y29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG 52 | /WwBAjAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT 53 | MAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAQEAK3Gp6/aGq7aBZsxf/oQ+TD/B 54 | SwW3AU4ETK+GQf2kFzYZkby5SFrHdPomunx2HBzViUchGoofGgg7gHW0W3MlQAXW 55 | M0r5LUvStcr82QDWYNPaUy4taCQmyaJ+VB+6wxHstSigOlSNF2a6vg4rgexixeiV 56 | 4YSB03Yqp2t3TeZHM9ESfkus74nQyW7pRGezj+TC44xCagCQQOzzNmzEAP2SnCrJ 57 | sNE2DpRVMnL8J6xBRdjmOsC3N6cQuKuRXbzByVBjCqAA8t1L0I+9wXJerLPyErjy 58 | rMKWaBFLmfK/AHNF4ZihwPGOc7w6UHczBZXH5RFzJNnww+WnKuTPI0HfnVH8lg== 59 | -----END CERTIFICATE----- 60 | -------------------------------------------------------------------------------- /10_chapter/public/sdp_pc2.txt: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 2581386738677590034 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE 0 6 | a=msid-semantic: WMS 7 | m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 114 115 116 8 | c=IN IP4 0.0.0.0 9 | a=rtcp:9 IN IP4 0.0.0.0 10 | a=ice-ufrag:DsVU 11 | a=ice-pwd:SruLGhzuoBxBWFh82C396RC0 12 | a=ice-options:trickle 13 | a=fingerprint:sha-256 03:F7:F8:A7:14:DE:DB:A9:EC:CE:FB:40:3C:56:A7:4A:E7:A0:1D:66:0D:85:83:EF:F7:98:52:0B:47:A4:13:BD 14 | a=setup:active 15 | a=mid:0 16 | a=extmap:14 urn:ietf:params:rtp-hdrext:toffset 17 | a=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 18 | a=extmap:12 urn:3gpp:video-orientation 19 | a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 20 | a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay 21 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type 22 | a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing 23 | a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 24 | a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space 25 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 26 | a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 27 | a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id 28 | a=recvonly 29 | a=rtcp-mux 30 | a=rtcp-rsize 31 | a=rtpmap:96 VP8/90000 32 | a=rtcp-fb:96 goog-remb 33 | a=rtcp-fb:96 transport-cc 34 | a=rtcp-fb:96 ccm fir 35 | a=rtcp-fb:96 nack 36 | a=rtcp-fb:96 nack pli 37 | a=rtpmap:97 rtx/90000 38 | a=fmtp:97 apt=96 39 | a=rtpmap:98 VP9/90000 40 | a=rtcp-fb:98 goog-remb 41 | a=rtcp-fb:98 transport-cc 42 | a=rtcp-fb:98 ccm fir 43 | a=rtcp-fb:98 nack 44 | a=rtcp-fb:98 nack pli 45 | a=fmtp:98 profile-id=0 46 | a=rtpmap:99 rtx/90000 47 | a=fmtp:99 apt=98 48 | a=rtpmap:100 VP9/90000 49 | a=rtcp-fb:100 goog-remb 50 | a=rtcp-fb:100 transport-cc 51 | a=rtcp-fb:100 ccm fir 52 | a=rtcp-fb:100 nack 53 | a=rtcp-fb:100 nack pli 54 | a=fmtp:100 profile-id=2 55 | a=rtpmap:101 rtx/90000 56 | a=fmtp:101 apt=100 57 | a=rtpmap:102 H264/90000 58 | a=rtcp-fb:102 goog-remb 59 | a=rtcp-fb:102 transport-cc 60 | a=rtcp-fb:102 ccm fir 61 | a=rtcp-fb:102 nack 62 | a=rtcp-fb:102 nack pli 63 | a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 64 | a=rtpmap:122 rtx/90000 65 | a=fmtp:122 apt=102 66 | a=rtpmap:127 H264/90000 67 | a=rtcp-fb:127 goog-remb 68 | a=rtcp-fb:127 transport-cc 69 | a=rtcp-fb:127 ccm fir 70 | a=rtcp-fb:127 nack 71 | a=rtcp-fb:127 nack pli 72 | a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f 73 | a=rtpmap:121 rtx/90000 74 | a=fmtp:121 apt=127 75 | a=rtpmap:125 H264/90000 76 | a=rtcp-fb:125 goog-remb 77 | a=rtcp-fb:125 transport-cc 78 | a=rtcp-fb:125 ccm fir 79 | a=rtcp-fb:125 nack 80 | a=rtcp-fb:125 nack pli 81 | a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 82 | a=rtpmap:107 rtx/90000 83 | a=fmtp:107 apt=125 84 | a=rtpmap:108 H264/90000 85 | a=rtcp-fb:108 goog-remb 86 | a=rtcp-fb:108 transport-cc 87 | a=rtcp-fb:108 ccm fir 88 | a=rtcp-fb:108 nack 89 | a=rtcp-fb:108 nack pli 90 | a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f 91 | a=rtpmap:109 rtx/90000 92 | a=fmtp:109 apt=108 93 | a=rtpmap:124 H264/90000 94 | a=rtcp-fb:124 goog-remb 95 | a=rtcp-fb:124 transport-cc 96 | a=rtcp-fb:124 ccm fir 97 | a=rtcp-fb:124 nack 98 | a=rtcp-fb:124 nack pli 99 | a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032 100 | a=rtpmap:120 rtx/90000 101 | a=fmtp:120 apt=124 102 | a=rtpmap:123 H264/90000 103 | a=rtcp-fb:123 goog-remb 104 | a=rtcp-fb:123 transport-cc 105 | a=rtcp-fb:123 ccm fir 106 | a=rtcp-fb:123 nack 107 | a=rtcp-fb:123 nack pli 108 | a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032 109 | a=rtpmap:119 rtx/90000 110 | a=fmtp:119 apt=123 111 | a=rtpmap:114 red/90000 112 | a=rtpmap:115 rtx/90000 113 | a=fmtp:115 apt=114 114 | a=rtpmap:116 ulpfec/90000 115 | 116 | -------------------------------------------------------------------------------- /08_chapter/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var fs = require('fs'); 6 | 7 | var express = require('express'); 8 | var serveIndex = require('serve-index'); 9 | 10 | //socket.io 11 | var socketIo = require('socket.io'); 12 | 13 | // 14 | var log4js = require('log4js'); 15 | 16 | log4js.configure({ 17 | appenders: { 18 | file: { 19 | type: 'file', 20 | filename: 'app.log', 21 | layout: { 22 | type: 'pattern', 23 | pattern: '%r %p - %m', 24 | } 25 | } 26 | }, 27 | categories: { 28 | default: { 29 | appenders: ['file'], 30 | level: 'debug' 31 | } 32 | } 33 | }); 34 | 35 | var logger = log4js.getLogger(); 36 | 37 | var app = express(); 38 | app.use(serveIndex('./public')); 39 | app.use(express.static('./public')); 40 | 41 | //http server 42 | var http_server = http.createServer(app); 43 | http_server.listen(80, '0.0.0.0'); 44 | 45 | var options = { 46 | key : fs.readFileSync('./cert/test.key'), 47 | cert: fs.readFileSync('./cert/test.pem') 48 | } 49 | 50 | //https server 51 | var https_server = https.createServer(options, app); 52 | 53 | //bind socket.io with https_server 54 | var https_socket = socketIo.listen(https_server); 55 | var http_socket = socketIo.listen(http_server); 56 | 57 | //connection 58 | https_socket.sockets.on('connection', (socket)=>{ 59 | 60 | socket.on('message', (room, data)=>{ 61 | https_socket.to(room).emit('message', room, socket.id, data)//房间内所有人,除自己外 62 | }); 63 | 64 | //该函数应该加锁 65 | socket.on('join', (room)=> { 66 | 67 | socket.join(room); 68 | 69 | var myRoom = https_socket.sockets.adapter.rooms[room]; 70 | var users = Object.keys(myRoom.sockets).length; 71 | 72 | logger.log('the number of user in room is: ' + users); 73 | 74 | //在这里可以控制进入房间的人数,现在一个房间最多 2个人 75 | //为了便于客户端控制,如果是多人的话,应该将目前房间里 76 | //人的个数当做数据下发下去。 77 | if(users < 3) { 78 | socket.emit('joined', room, socket.id); 79 | if (users > 1) { 80 | socket.to(room).emit('otherjoin', room);//除自己之外 81 | } 82 | }else { 83 | socket.leave(room); 84 | socket.emit('full', room, socket.id); 85 | } 86 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 87 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 88 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 89 | }); 90 | 91 | socket.on('leave', (room)=> { 92 | var myRoom = io.sockets.adapter.rooms[room]; 93 | var users = Object.keys(myRoom.sockets).length; 94 | //users - 1; 95 | 96 | logger.log('the number of user in room is: ' + (users-1)); 97 | 98 | socket.leave(room); 99 | socket.to(room).emit('bye', room, socket.id)//房间内所有人,除自己外 100 | socket.emit('leaved', room, socket.id); 101 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 102 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 103 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 104 | }); 105 | 106 | }); 107 | 108 | //connection 109 | http_socket.sockets.on('connection', (socket)=>{ 110 | 111 | socket.on('message', (room, data)=>{ 112 | http_socket.in(room).emit('message', room, socket.id, data)//房间内所有人 113 | }); 114 | 115 | socket.on('join', (room)=> { 116 | socket.join(room); 117 | var myRoom = http_socket.sockets.adapter.rooms[room]; 118 | var users = Object.keys(myRoom.sockets).length; 119 | logger.log('the number of user in room is: ' + users); 120 | socket.emit('joined', room, socket.id); 121 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 122 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 123 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 124 | }); 125 | 126 | socket.on('leave', (room)=> { 127 | var myRoom = http_socket.sockets.adapter.rooms[room]; 128 | var users = Object.keys(myRoom.sockets).length; 129 | //users - 1; 130 | 131 | logger.log('the number of user in room is: ' + (users-1)); 132 | 133 | socket.leave(room); 134 | socket.emit('leaved', room, socket.id); 135 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 136 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 137 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 138 | }); 139 | }); 140 | 141 | https_server.listen(443, '0.0.0.0'); 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /10_chapter/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var http = require('http'); 4 | var https = require('https'); 5 | var fs = require('fs'); 6 | 7 | var express = require('express'); 8 | var serveIndex = require('serve-index'); 9 | 10 | //socket.io 11 | var socketIo = require('socket.io'); 12 | 13 | // 14 | var log4js = require('log4js'); 15 | 16 | log4js.configure({ 17 | appenders: { 18 | file: { 19 | type: 'file', 20 | filename: 'app.log', 21 | layout: { 22 | type: 'pattern', 23 | pattern: '%r %p - %m', 24 | } 25 | } 26 | }, 27 | categories: { 28 | default: { 29 | appenders: ['file'], 30 | level: 'debug' 31 | } 32 | } 33 | }); 34 | 35 | var logger = log4js.getLogger(); 36 | 37 | var app = express(); 38 | app.use(serveIndex('./public')); 39 | app.use(express.static('./public')); 40 | 41 | //http server 42 | var http_server = http.createServer(app); 43 | http_server.listen(80, '0.0.0.0'); 44 | 45 | var options = { 46 | key : fs.readFileSync('./cert/test.key'), 47 | cert: fs.readFileSync('./cert/test.pem') 48 | } 49 | 50 | //https server 51 | var https_server = https.createServer(options, app); 52 | 53 | //bind socket.io with https_server 54 | var https_socket = socketIo.listen(https_server); 55 | var http_socket = socketIo.listen(http_server); 56 | 57 | //connection 58 | https_socket.sockets.on('connection', (socket)=>{ 59 | 60 | socket.on('message', (room, data)=>{ 61 | https_socket.to(room).emit('message', room, socket.id, data)//房间内所有人,除自己外 62 | }); 63 | 64 | //该函数应该加锁 65 | socket.on('join', (room)=> { 66 | 67 | socket.join(room); 68 | 69 | var myRoom = https_socket.sockets.adapter.rooms[room]; 70 | var users = Object.keys(myRoom.sockets).length; 71 | 72 | logger.log('the number of user in room is: ' + users); 73 | 74 | //在这里可以控制进入房间的人数,现在一个房间最多 2个人 75 | //为了便于客户端控制,如果是多人的话,应该将目前房间里 76 | //人的个数当做数据下发下去。 77 | if(users < 3) { 78 | socket.emit('joined', room, socket.id); 79 | if (users > 1) { 80 | socket.to(room).emit('otherjoin', room);//除自己之外 81 | } 82 | }else { 83 | socket.leave(room); 84 | socket.emit('full', room, socket.id); 85 | } 86 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 87 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 88 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 89 | }); 90 | 91 | socket.on('leave', (room)=> { 92 | var myRoom = io.sockets.adapter.rooms[room]; 93 | var users = Object.keys(myRoom.sockets).length; 94 | //users - 1; 95 | 96 | logger.log('the number of user in room is: ' + (users-1)); 97 | 98 | socket.leave(room); 99 | socket.to(room).emit('bye', room, socket.id)//房间内所有人,除自己外 100 | socket.emit('leaved', room, socket.id); 101 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 102 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 103 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 104 | }); 105 | 106 | }); 107 | 108 | //connection 109 | http_socket.sockets.on('connection', (socket)=>{ 110 | 111 | socket.on('message', (room, data)=>{ 112 | http_socket.in(room).emit('message', room, socket.id, data)//房间内所有人 113 | }); 114 | 115 | socket.on('join', (room)=> { 116 | socket.join(room); 117 | var myRoom = http_socket.sockets.adapter.rooms[room]; 118 | var users = Object.keys(myRoom.sockets).length; 119 | logger.log('the number of user in room is: ' + users); 120 | socket.emit('joined', room, socket.id); 121 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 122 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 123 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 124 | }); 125 | 126 | socket.on('leave', (room)=> { 127 | var myRoom = http_socket.sockets.adapter.rooms[room]; 128 | var users = Object.keys(myRoom.sockets).length; 129 | //users - 1; 130 | 131 | logger.log('the number of user in room is: ' + (users-1)); 132 | 133 | socket.leave(room); 134 | socket.emit('leaved', room, socket.id); 135 | //socket.to(room).emit('joined', room, socket.id);//除自己之外 136 | //io.in(room).emit('joined', room, socket.id)//房间内所有人 137 | //socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点 138 | }); 139 | }); 140 | 141 | https_server.listen(443, '0.0.0.0'); 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /10_chapter/public/sdp_pc1.txt: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 2510751529507770530 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE 0 6 | a=msid-semantic: WMS UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN 7 | m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 114 115 116 8 | c=IN IP4 0.0.0.0 9 | a=rtcp:9 IN IP4 0.0.0.0 10 | a=ice-ufrag:cHvA 11 | a=ice-pwd:H2JJrFRuXwIKBsQ7Aa01M7kU 12 | a=ice-options:trickle 13 | a=fingerprint:sha-256 39:0E:67:55:E1:CE:9C:E3:65:5B:BB:EF:CE:DF:C1:59:27:95:3F:6F:A3:50:D8:B8:82:13:0E:FB:18:80:3D:86 14 | a=setup:actpass 15 | a=mid:0 16 | a=extmap:14 urn:ietf:params:rtp-hdrext:toffset 17 | a=extmap:13 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 18 | a=extmap:12 urn:3gpp:video-orientation 19 | a=extmap:2 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 20 | a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay 21 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type 22 | a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing 23 | a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 24 | a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space 25 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 26 | a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id 27 | a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id 28 | a=sendrecv 29 | a=msid:UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN af6eaad8-69c6-44fc-ad63-b5be8558a449 30 | a=rtcp-mux 31 | a=rtcp-rsize 32 | a=rtpmap:96 VP8/90000 33 | a=rtcp-fb:96 goog-remb 34 | a=rtcp-fb:96 transport-cc 35 | a=rtcp-fb:96 ccm fir 36 | a=rtcp-fb:96 nack 37 | a=rtcp-fb:96 nack pli 38 | a=rtpmap:97 rtx/90000 39 | a=fmtp:97 apt=96 40 | a=rtpmap:98 VP9/90000 41 | a=rtcp-fb:98 goog-remb 42 | a=rtcp-fb:98 transport-cc 43 | a=rtcp-fb:98 ccm fir 44 | a=rtcp-fb:98 nack 45 | a=rtcp-fb:98 nack pli 46 | a=fmtp:98 profile-id=0 47 | a=rtpmap:99 rtx/90000 48 | a=fmtp:99 apt=98 49 | a=rtpmap:100 VP9/90000 50 | a=rtcp-fb:100 goog-remb 51 | a=rtcp-fb:100 transport-cc 52 | a=rtcp-fb:100 ccm fir 53 | a=rtcp-fb:100 nack 54 | a=rtcp-fb:100 nack pli 55 | a=fmtp:100 profile-id=2 56 | a=rtpmap:101 rtx/90000 57 | a=fmtp:101 apt=100 58 | a=rtpmap:102 H264/90000 59 | a=rtcp-fb:102 goog-remb 60 | a=rtcp-fb:102 transport-cc 61 | a=rtcp-fb:102 ccm fir 62 | a=rtcp-fb:102 nack 63 | a=rtcp-fb:102 nack pli 64 | a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 65 | a=rtpmap:122 rtx/90000 66 | a=fmtp:122 apt=102 67 | a=rtpmap:127 H264/90000 68 | a=rtcp-fb:127 goog-remb 69 | a=rtcp-fb:127 transport-cc 70 | a=rtcp-fb:127 ccm fir 71 | a=rtcp-fb:127 nack 72 | a=rtcp-fb:127 nack pli 73 | a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f 74 | a=rtpmap:121 rtx/90000 75 | a=fmtp:121 apt=127 76 | a=rtpmap:125 H264/90000 77 | a=rtcp-fb:125 goog-remb 78 | a=rtcp-fb:125 transport-cc 79 | a=rtcp-fb:125 ccm fir 80 | a=rtcp-fb:125 nack 81 | a=rtcp-fb:125 nack pli 82 | a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 83 | a=rtpmap:107 rtx/90000 84 | a=fmtp:107 apt=125 85 | a=rtpmap:108 H264/90000 86 | a=rtcp-fb:108 goog-remb 87 | a=rtcp-fb:108 transport-cc 88 | a=rtcp-fb:108 ccm fir 89 | a=rtcp-fb:108 nack 90 | a=rtcp-fb:108 nack pli 91 | a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f 92 | a=rtpmap:109 rtx/90000 93 | a=fmtp:109 apt=108 94 | a=rtpmap:124 H264/90000 95 | a=rtcp-fb:124 goog-remb 96 | a=rtcp-fb:124 transport-cc 97 | a=rtcp-fb:124 ccm fir 98 | a=rtcp-fb:124 nack 99 | a=rtcp-fb:124 nack pli 100 | a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032 101 | a=rtpmap:120 rtx/90000 102 | a=fmtp:120 apt=124 103 | a=rtpmap:123 H264/90000 104 | a=rtcp-fb:123 goog-remb 105 | a=rtcp-fb:123 transport-cc 106 | a=rtcp-fb:123 ccm fir 107 | a=rtcp-fb:123 nack 108 | a=rtcp-fb:123 nack pli 109 | a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032 110 | a=rtpmap:119 rtx/90000 111 | a=fmtp:119 apt=123 112 | a=rtpmap:114 red/90000 113 | a=rtpmap:115 rtx/90000 114 | a=fmtp:115 apt=114 115 | a=rtpmap:116 ulpfec/90000 116 | a=ssrc-group:FID 790110816 3858362499 117 | a=ssrc:790110816 cname:IXwkxlCRc+MAD5lr 118 | a=ssrc:790110816 msid:UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN af6eaad8-69c6-44fc-ad63-b5be8558a449 119 | a=ssrc:790110816 mslabel:UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN 120 | a=ssrc:790110816 label:af6eaad8-69c6-44fc-ad63-b5be8558a449 121 | a=ssrc:3858362499 cname:IXwkxlCRc+MAD5lr 122 | a=ssrc:3858362499 msid:UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN af6eaad8-69c6-44fc-ad63-b5be8558a449 123 | a=ssrc:3858362499 mslabel:UlWZUrYSZtB5e3Aiw4IBfp6eivcA9UWPSFvN 124 | a=ssrc:3858362499 label:af6eaad8-69c6-44fc-ad63-b5be8558a449 125 | 126 | -------------------------------------------------------------------------------- /06_chapter/public/02_permission/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var audioSource = document.querySelector('select#audioSource'); 4 | var audioOutput = document.querySelector('select#audioOutput'); 5 | var videoSource = document.querySelector('select#videoSource'); 6 | 7 | //视频效果标签 8 | var filtersSelect = document.querySelector('select#filter'); 9 | 10 | //用于拍照的btn和显示截取快照的图片 11 | var snapshort = document.querySelector('button#snapshort'); 12 | var picture = document.querySelector('canvas#picture'); 13 | picture.width = 320; 14 | picture.height = 240; 15 | 16 | //用于显示视频流参数信息 17 | var divConstraints = document.querySelector('div#constraints'); 18 | 19 | //获取到video标签 20 | var videoplay = document.querySelector('video#player'); 21 | //var audioplay = document.querySelector('audio#audioplayer'); 22 | 23 | //录制相关 24 | var recvideo = document.querySelector('video#recplayer'); 25 | var btnRecord = document.querySelector('button#record'); 26 | var btnPlay = document.querySelector('button#recplay'); 27 | var btnDownload = document.querySelector('button#download'); 28 | 29 | var buffer; 30 | var mediaRecorder; 31 | 32 | //将流赋值给video标签 33 | function gotMediaStream(stream){ 34 | videoplay.srcObject = stream; 35 | //audioplay.srcObject = stream; 36 | 37 | //视频的所有轨 38 | var videoTrack = stream.getVideoTracks()[0]; 39 | var videoConstraints = videoTrack.getSettings(); 40 | 41 | divConstraints.textContent = JSON.stringify(videoConstraints, null, 2); 42 | window.stream = stream; 43 | return navigator.mediaDevices.enumerateDevices(); 44 | } 45 | 46 | //打印错误日志 47 | function handleError(err){ 48 | console.log('getUserMedia error : ', err); 49 | } 50 | 51 | //设备信息数组 52 | function gotDevices(deviceInfos){ 53 | 54 | deviceInfos.forEach(function(deviceinfo){ 55 | var option = document.createElement('option'); 56 | option.text = deviceinfo.label; 57 | option.value = deviceinfo.deviceId; 58 | 59 | if(deviceinfo.kind == 'audioinput'){ 60 | audioSource.appendChild(option); 61 | }else if(deviceinfo.kind === 'audiooutput'){ 62 | audioOutput.appendChild(option); 63 | }else if(deviceinfo.kind === 'videoinput'){ 64 | videoSource.appendChild(option); 65 | } 66 | }) 67 | } 68 | 69 | function start(){ 70 | 71 | if(!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia){ 72 | console.log('getUserMedia is not supported'); 73 | return; 74 | }else{ 75 | var deviceId = videoSource.value; 76 | var constraints = { 77 | video : { 78 | //修改视频宽高 79 | width : 320, 80 | height : 240, 81 | 82 | //设置帧率 83 | frameRate : 15, 84 | facingMode : 'enviroment', 85 | deviceId : deviceId ? {exact:deviceId} : undefined 86 | }, 87 | audio : false 88 | } 89 | 90 | navigator.mediaDevices.getDisplayMedia(constraints) 91 | .then(gotMediaStream) 92 | .then(gotDevices) 93 | .catch(handleError) 94 | } 95 | 96 | } 97 | 98 | start(); 99 | 100 | 101 | //每次选择时,都会触发start函数 102 | videoSource.onchange = start 103 | 104 | filtersSelect.onchange = function(){ 105 | //获取css名字 106 | videoplay.className = filtersSelect.value; 107 | } 108 | 109 | //截取快照事件 110 | snapshort.onclick = function(){ 111 | picture.className = filtersSelect.value; 112 | picture.getContext('2d').drawImage(videoplay, 0,0, picture.width,picture.height); 113 | } 114 | 115 | function handleDataAvailable(e){ 116 | if(e && e.data && e.data.size > 0){ 117 | buffer.push(e.data); 118 | } 119 | } 120 | 121 | function startRecord(){ 122 | buffer = []; 123 | var options = { 124 | mimeType : 'video/webm; codecs = vp8' 125 | } 126 | 127 | if(!MediaRecorder.isTypeSupported(options.mimeType)){ 128 | console.error('${options.mimeType} is not supported!'); 129 | return; 130 | } 131 | 132 | try{ 133 | mediaRecorder = new MediaRecorder(window.stream, options); 134 | }catch(e){ 135 | console.error('Failed to create MediaRecorder:',e); 136 | return 137 | } 138 | mediaRecorder.ondataavailable = handleDataAvailable; 139 | mediaRecorder.start(10); 140 | } 141 | 142 | function stopRecord(){ 143 | mediaRecorder.stop(); 144 | } 145 | 146 | //录制按钮监听 147 | btnRecord.onclick = ()=>{ 148 | if(btnRecord.textContent === 'Start Record'){ 149 | startRecord(); 150 | btnRecord.textContent = 'Stop Record'; 151 | btnPlay.disabled = true; 152 | btnDownload.disabled = true; 153 | }else{ 154 | stopRecord(); 155 | btnRecord.textContent = 'Start Record'; 156 | btnPlay.disabled = false; 157 | btnDownload.disabled = false; 158 | } 159 | } 160 | 161 | //播放按钮监听 162 | btnPlay.onclick = ()=>{ 163 | var blob = new Blob(buffer, {type : 'video/webm'}); 164 | recvideo.src = window.URL.createObjectURL(blob); 165 | recvideo.srcObject = null; 166 | recvideo.controls = true; 167 | recvideo.play(); 168 | } 169 | 170 | //下载按钮监听 171 | btnDownload.onclick = ()=>{ 172 | var blob = new Blob(buffer, {type: 'video/webm'}); 173 | var url = window.URL.createObjectURL(blob); 174 | var a = document.createElement('a'); 175 | 176 | a.href = url; 177 | a.style.display = 'none'; 178 | a.download = 'aaa.webm'; 179 | a.click(); 180 | } 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## (一) 环境搭建 2 | ### 2 安装nodejs 3 | brew install nodejs 4 | 5 | ### 3 安装依赖库 6 | brew install npm 7 | 8 | ### 4 创建一个简单的http服务 9 | - require 引入http模块 10 | - 创建http服务 11 | - 监听端口 12 | 13 | ``` 14 | var http = require('http'); 15 | 16 | var app = http.createServer(function(req, res){ 17 | res.writeHead(200, {'Content-Type':'text/plain'}); 18 | res.end('Hello World\n'); 19 | }).listen(8080, '0.0.0.0'); 20 | ``` 21 | ### 5 启动服务 22 | 23 | ``` 24 | node server.js 25 | ``` 26 | 至此一个简单的服务端程序就完成了,在浏览器中输入域名和对应的端口好就可以看到屏幕中输出Hello World。 27 | 28 | ### 6 引入其他模块 29 | 功能强大的框架 30 | ``` 31 | npm install express 32 | ``` 33 | 批量发布框架 34 | ``` 35 | npm install serve-index 36 | ``` 37 | ### 7 创建一个https服务 38 | 申请摄像头麦克风的使用权限必须是有https的服务。 39 | ``` 40 | 'use strict' 41 | var https = require('https'); 42 | var fs = require('fs'); 43 | 44 | var options = { 45 | key :fs.readFileSync('./cert/test.key'), 46 | cert:fs.readFileSync('./cert/test.pem') 47 | } 48 | 49 | var app = https.createServer(options, function(req, res){ 50 | 51 | res.writeHead(200, {'Content-Type': 'text/plain'}); 52 | res.end('Hello World!\n'); 53 | 54 | 55 | }).listen(443, '0.0.0.0'); 56 | ``` 57 | ## (二) 设备信息获取 58 | 59 | ### 1 html代码,用于显示当前设备的信息 60 | 61 | ``` 62 | 63 | 64 | WebRTC get audio and video devices 65 | 66 | 67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | ``` 84 | ### 2 js代码,用于获取当前设备信息 85 | ``` 86 | 'use strict' 87 | var audioSource = document.querySelector("select#audioSource"); 88 | var audioOutput = document.querySelector("select#audioOutput"); 89 | var videoSource = document.querySelector("select#videoSource"); 90 | 91 | if(!navigator.mediaDevices || !navigator.mediaDevices.emurateDevices){ 92 | console.log('emurateDevices is not support!'); 93 | }else{ 94 | navigator.mediaDevices.emurateDevices 95 | .then(gotDevices) 96 | .catch(handleError); 97 | } 98 | 99 | function gotDevices(devicesInfo){ 100 | devicesInfos.forEash(function(deviceInfo)){ 101 | console.log(deviceInfo.kind 102 | + ": label = "+deviceInfo.label 103 | + ": id = " + deviceInfo.deviceId 104 | + ": groupId = " + deviceInfo.groupId); 105 | 106 | var option = document.createElement('option'); 107 | option.html = deviceInfo.label; 108 | option.value = deviceInfo.deviceId; 109 | 110 | if(devicesInfo.kind === 'audioinput'){ 111 | audioSource.appendChild(option); 112 | }else if(devicesInfo.kind === 'audiooutput'){ 113 | audioOutput.appendChild(option); 114 | }else if(){ 115 | videoSource.appendChild(option); 116 | } 117 | } 118 | } 119 | 120 | function handleError(err){ 121 | console.log(err.name + “ : ” + err.message); 122 | } 123 | 124 | ``` 125 | ### 3 js打印结果 126 | 127 | ``` 128 | audioinput: label = : id = default: groupId = c8c3ef9c92cf11acba2a3a50b317b9bf2cf30f5d401f9874445ed51e731ef4bd 129 | client.js:17 audioinput: label = : id = b12c633cd7017ae872295cc07950aa4f72c8dfcbb038481cda0a0ef3d0cc2f09: groupId = c8c3ef9c92cf11acba2a3a50b317b9bf2cf30f5d401f9874445ed51e731ef4bd 130 | client.js:17 videoinput: label = : id = b95c88c72235c31f445c13258cdb2d4ab5a774225b3757281d58b453fc14b0c0: groupId = cb329ecdbf1a92f2eadd2c3c5e6ca0cfbdb6a2ae3f93d5a2967c830444503c4b 131 | client.js:17 audiooutput: label = : id = default: groupId = c8c3ef9c92cf11acba2a3a50b317b9bf2cf30f5d401f9874445ed51e731ef4bd 132 | client.js:17 audiooutput: label = : id = a8c76b962c5b479073378dbde69fcf8edc05f125aa58d885a231bfc608c0cae2: groupId = c8c3ef9c92cf11acba2a3a50b317b9bf2cf30f5d401f9874445ed51e731ef4bd 133 | ``` 134 | 可以看到label没有值,这是因为没有获取到音频设备的权限。 135 | 通过下一节调用如下方法即可拿到权限。 136 | 137 | ### 4 获取权限方法 138 | ``` 139 | navigator.mediaDevices.getUserMedia 140 | ``` 141 | 142 | ## (三) 音视频数据采集及处理 143 | 144 | ### html代码,用于显示视频 145 | ``` 146 | 147 | 148 | WebRTC capture video and audio 149 | 150 | 151 | 152 | 153 | 154 | 155 | ``` 156 | ### js代码,用于获取视频流 157 | ``` 158 | 'use strict' 159 | 160 | //后去到video标签 161 | var videoplay = document.querySelector('video#player'); 162 | 163 | //将流赋值给video标签 164 | function gotMediaStream(stream){ 165 | videoplay.srcObject = stream; 166 | } 167 | 168 | //打印错误日志 169 | function handleError(err){ 170 | console.log('getUserMedia error : ', err); 171 | } 172 | 173 | if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ 174 | console.log('getUserMedia is not supported'); 175 | }else{ 176 | var constraints = { 177 | video : true, 178 | audio : true 179 | } 180 | 181 | navigator.mediaDevices.getUserMedia(constraints) 182 | .then(gotMediaStream) 183 | .catch(handleError) 184 | } 185 | ``` 186 | 通过以上js和html代码,就能将采集出摄像头的数据和音频数据。 187 | 但是以上方法是有浏览器的兼容性的问题。所以需要在js里面加入以下开源库 188 | 189 | ``` 190 | https://webrtc.github.io/adapter/adapter-latest.js 191 | ``` 192 | 完整html代码 193 | ``` 194 | 195 | 196 | WebRTC capture video and audio 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | ``` 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /12_chapter/public/peerconnection/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var localVideo = document.querySelector('video#localvideo'); 4 | var remoteVideo = document.querySelector('video#remotevideo'); 5 | 6 | var btnConn = document.querySelector('button#connserver'); 7 | var btnLeave = document.querySelector('button#leave'); 8 | 9 | var optBw = document.querySelector('select#bandwidth'); 10 | 11 | var offer = document.querySelector('textarea#offer') 12 | var answer = document.querySelector('textarea#answer'); 13 | 14 | var shareDeskBox = document.querySelector('input#shareDesk'); 15 | 16 | var bitrateGraph; 17 | var bitrateSeries; 18 | 19 | var packetGraph; 20 | var packetSeries; 21 | 22 | var lastResult; 23 | 24 | var pcConfig = { 25 | 'iceServers':[{ 26 | 'urls' : 'turn:stun.al.learning.cn:3478', 27 | 'credential': "mypassword", 28 | 'username':"garrylea" 29 | }] 30 | }; 31 | 32 | var localStream = null; 33 | var remoteStream = null; 34 | 35 | var pc = null; 36 | 37 | var roomid; 38 | var socket = null; 39 | 40 | var offerdesc = null; 41 | var state = 'init'; 42 | 43 | // 以下代码是从网上找的 44 | //========================================================================================= 45 | 46 | //如果返回的是false说明当前操作系统是手机端,如果返回的是true则说明当前的操作系统是电脑端 47 | function IsPC() { 48 | var userAgentInfo = navigator.userAgent; 49 | var Agents = ["Android", "iPhone","SymbianOS", "Windows Phone","iPad", "iPod"]; 50 | var flag = true; 51 | 52 | for (var v = 0; v < Agents.length; v++) { 53 | if (userAgentInfo.indexOf(Agents[v]) > 0) { 54 | flag = false; 55 | break; 56 | } 57 | } 58 | 59 | return flag; 60 | } 61 | 62 | //如果返回true 则说明是Android false是ios 63 | function is_android() { 64 | var u = navigator.userAgent, app = navigator.appVersion; 65 | var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //g 66 | var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 67 | if (isAndroid) { 68 | //这个是安卓操作系统 69 | return true; 70 | } 71 | 72 | if (isIOS) { 73 |   //这个是ios操作系统 74 |    return false; 75 | } 76 | } 77 | 78 | //获取url参数 79 | function getQueryVariable(variable) 80 | { 81 | var query = window.location.search.substring(1); 82 | var vars = query.split("&"); 83 | for (var i=0;i { 197 | state = 'joined' 198 | 199 | createPeerConnection(); 200 | bindTracks(); 201 | 202 | btnConn.disabled = true; 203 | btnLeave.disabled = false; 204 | console.log('receive joined message, state = ',state,",roomid = ",roomid, ",id = ",id); 205 | }); 206 | 207 | socket.on('otherjoin', (roomid) =>{ 208 | if(state === 'joined_unbind'){ 209 | createPeerConnection(); 210 | bindTracks(); 211 | } 212 | state = 'joined_conn'; 213 | call(); 214 | 215 | console.log('receive other_joined message, state = ', state); 216 | }); 217 | 218 | socket.on('full',(roomid, id) => { 219 | console.log('receive full message ', roomid, id); 220 | hangup(); 221 | closeLocalMedia(); 222 | state = 'leaved'; 223 | console.log('receive full message, state = ',state); 224 | alert('the room is full! '); 225 | }) 226 | 227 | socket.on('leaved', (roomid, id) =>{ 228 | console.log('receive leaved message', roomid, id); 229 | 230 | state = 'leaved'; 231 | socket.disconnect(); 232 | 233 | console.log('receive leaved message, state = ', state); 234 | 235 | btnConn.disabled = false; 236 | btnLeave.disabled = true; 237 | optBw.disabled = true; 238 | }); 239 | 240 | socket.on('bye', (room,id)=>{ 241 | console.log('receive bye message', roomid, id); 242 | 243 | state = 'joined_unbind'; 244 | hangup(); 245 | offer.value = ''; 246 | answer.value = ''; 247 | console.log('receive bye message, state = ', state); 248 | }) 249 | 250 | socket.on('disconnect', (socket)=>{ 251 | console.log('receive disconnect message!', roomid); 252 | 253 | if(!(state === 'leaved')){ 254 | hangup(); 255 | closeLocalMedia(); 256 | } 257 | 258 | state = 'leaved'; 259 | 260 | btnConn.disabled = false; 261 | btnLeave.leaved = true; 262 | optBw.disabled = true; 263 | }); 264 | 265 | socket.on('message', (roomid, data) => { 266 | 267 | if(data === null || data === undefined){ 268 | console.err('the message is invalid!'); 269 | return; 270 | } 271 | 272 | if(data.hasOwnProperty('type') && data.type === 'offer'){ 273 | console.log('receive offer', roomid, data); 274 | offer.value = data.sdp; 275 | 276 | pc.setRemoteDescription(new RTCSessionDescription(data)); 277 | 278 | //create answer 279 | pc.createAnswer() 280 | .then(createAnswerSuc) 281 | .catch(handleAnswerError); 282 | }else if(data.hasOwnProperty('type') && data.type === 'answer'){ 283 | console.log('receive answer', roomid, data); 284 | 285 | optBw.disabled = false; 286 | answer.value = data.sdp; 287 | pc.setRemoteDescription(new RTCSessionDescription(data)); 288 | }else if(data.hasOwnProperty('type') && data.type === 'candidate'){ 289 | console.log('receive candidate', roomid, data); 290 | 291 | var candidate = new RTCIceCandidate({ 292 | sdpMLineIndex: data.label, 293 | candidate:data.candidate 294 | }); 295 | pc.addIceCandidate(candidate); 296 | }else{ 297 | console.log('the message is invalid', data); 298 | } 299 | }); 300 | 301 | roomid = getQueryVariable('room'); 302 | socket.emit('join', roomid); 303 | return true; 304 | } 305 | 306 | 307 | function getMediaStream(stream){ 308 | if(localStream){ 309 | stream.getAudioTracks().forEach((track)=>{ 310 | localStream.addTrack(track); 311 | stream.removeTrack(track); 312 | }) 313 | } else { 314 | localStream = stream; 315 | } 316 | 317 | //2 ===============显示本地视频=============== 318 | localVideo.srcObject = localStream; 319 | 320 | //这个函数的调用时机特别重要 一定要在getMediaStream之后再调用,否则会出现绑定失败的情况 321 | conn(); 322 | 323 | bitrateSeries = new TimelineDataSeries(); 324 | bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); 325 | bitrateGraph.updateEndDate(); 326 | 327 | packetSeries = new TimelineDataSeries(); 328 | packetGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas'); 329 | packetGraph.updateEndDate(); 330 | } 331 | 332 | function handleError(err){ 333 | console.error("Failed to get Media Stream!", err); 334 | } 335 | 336 | function getDeskStream(stream){ 337 | localStream = stream; 338 | 339 | localVideo.srcObject = localStream; 340 | conn(); 341 | } 342 | 343 | function shareDesk(){ 344 | if(IsPC()){ 345 | navigator.mediaDevices.getDisplayMedia({video:true}) 346 | .then(getDeskStream) 347 | .catch(handleError); 348 | 349 | return true; 350 | } 351 | 352 | return false; 353 | } 354 | 355 | function start(){ 356 | if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){ 357 | console.error('the getUserMedia is not supported!'); 358 | return; 359 | }else { 360 | //1 ===============配置音视频参数=============== 361 | var constraints; 362 | 363 | if(shareDeskBox.checked && shareDesk()){ 364 | constraints = { 365 | video : true, 366 | audio : { 367 | echoCancellation : true, 368 | noiseSuppression : true, 369 | autoGainControl : true 370 | } 371 | } 372 | }else{ 373 | constraints = { 374 | video : true, 375 | audio : { 376 | echoCancellation : true, 377 | noiseSuppression : true, 378 | autoGainControl : true 379 | } 380 | } 381 | 382 | navigator.mediaDevices.getUserMedia(constraints) 383 | .then(getMediaStream) 384 | .catch(handleError); 385 | } 386 | } 387 | } 388 | 389 | function leave(){ 390 | if(socket){ 391 | socket.emit('leave', roomid); 392 | } 393 | 394 | hangup(); 395 | closeLocalMedia(); 396 | 397 | btnConn.disabled = false; 398 | btnLeave.disabled = true; 399 | optBw.disabled = true; 400 | } 401 | 402 | function hangup(){ 403 | if(pc){ 404 | offerdesc = null; 405 | pc.close(); 406 | pc = null; 407 | } 408 | } 409 | 410 | function connSignalServer(){ 411 | //开启本地视频 412 | start(); 413 | 414 | return true; 415 | } 416 | 417 | //设置带宽 418 | function change_bw(){ 419 | optBw.disabled = true; 420 | var bw = optBw.options[optBw.selectIndex].value; 421 | 422 | var vsender = null; 423 | var senders = pc.getSenders(); 424 | 425 | senders.forEach(senders => { 426 | if(sender && sender.track.kind === 'video'){ 427 | vsender = sender; 428 | } 429 | }); 430 | 431 | var parameters = vsender.getParameters(); 432 | if(!parameters.encodings){ 433 | return; 434 | } 435 | 436 | if(bw == 'unlimited'){ 437 | return; 438 | } 439 | 440 | parameters.encodings[0].maxBitrate = bw * 1000; 441 | vsender.setParameters(parameters) 442 | .then(()=>{ 443 | optBw.disabled = false; 444 | console.log('Successed to set parameters!'); 445 | }) 446 | .catch(err =>{ 447 | console.log(err); 448 | }) 449 | } 450 | 451 | window.setInterval(() => { 452 | if(!pc){ 453 | return; 454 | } 455 | 456 | const sender = pc.getSenders()[0]; 457 | if(!sender){ 458 | return; 459 | } 460 | 461 | sender.getStats().then(res =>{ 462 | res.forEach(report => { 463 | let bytes; 464 | let packets; 465 | if(report.type === 'outbound-rtp'){ 466 | if(report.isRemote){ 467 | return; 468 | } 469 | 470 | const now = report.timestamp; 471 | bytes = report.packetsSent; 472 | packets = report.packetsSent; 473 | if(lastResult && lastResult.has(report.id)){ 474 | //calculate bitrate 475 | const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent)/ 476 | (now - lastResult.get(report.id).timestamp); 477 | 478 | //append to chart 479 | bitrateSeries.addPoint(now, bitrate); 480 | bitrateGraph.setDataSeries([bitrateSeries]); 481 | bitrateGraph.updateEndDate(); 482 | 483 | //caculate number of packets and apend to chart 484 | packetSeries.addPoint(now, packets - lastResult.get(report.id).packetsSent); 485 | packetGraph.setDataSeries([packetSeries]); 486 | packetGraph.updateEndDate(); 487 | 488 | } 489 | } 490 | }); 491 | lastResult = res; 492 | }); 493 | 494 | }, 1000); 495 | 496 | 497 | 498 | btnConn.onclick = connSignalServer; 499 | btnLeave.onclick = leave; 500 | optBw.onchange = change_bw; 501 | 502 | -------------------------------------------------------------------------------- /12_chapter/public/peerconnection/third_party/graph.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | // taken from chrome://webrtc-internals with jshint adaptions 9 | 10 | 'use strict'; 11 | /* exported TimelineDataSeries, TimelineGraphView */ 12 | 13 | // The maximum number of data points bufferred for each stats. Old data points 14 | // will be shifted out when the buffer is full. 15 | const MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000; 16 | 17 | const TimelineDataSeries = (function() { 18 | /** 19 | * @constructor 20 | */ 21 | function TimelineDataSeries() { 22 | // List of DataPoints in chronological order. 23 | this.dataPoints_ = []; 24 | 25 | // Default color. Should always be overridden prior to display. 26 | this.color_ = 'red'; 27 | // Whether or not the data series should be drawn. 28 | this.isVisible_ = true; 29 | 30 | this.cacheStartTime_ = null; 31 | this.cacheStepSize_ = 0; 32 | this.cacheValues_ = []; 33 | } 34 | 35 | TimelineDataSeries.prototype = { 36 | /** 37 | * @override 38 | */ 39 | toJSON: function() { 40 | if (this.dataPoints_.length < 1) { 41 | return {}; 42 | } 43 | 44 | let values = []; 45 | for (let i = 0; i < this.dataPoints_.length; ++i) { 46 | values.push(this.dataPoints_[i].value); 47 | } 48 | return { 49 | startTime: this.dataPoints_[0].time, 50 | endTime: this.dataPoints_[this.dataPoints_.length - 1].time, 51 | values: JSON.stringify(values), 52 | }; 53 | }, 54 | 55 | /** 56 | * Adds a DataPoint to |this| with the specified time and value. 57 | * DataPoints are assumed to be received in chronological order. 58 | */ 59 | addPoint: function(timeTicks, value) { 60 | let time = new Date(timeTicks); 61 | this.dataPoints_.push(new DataPoint(time, value)); 62 | 63 | if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) { 64 | this.dataPoints_.shift(); 65 | } 66 | }, 67 | 68 | isVisible: function() { 69 | return this.isVisible_; 70 | }, 71 | 72 | show: function(isVisible) { 73 | this.isVisible_ = isVisible; 74 | }, 75 | 76 | getColor: function() { 77 | return this.color_; 78 | }, 79 | 80 | setColor: function(color) { 81 | this.color_ = color; 82 | }, 83 | 84 | getCount: function() { 85 | return this.dataPoints_.length; 86 | }, 87 | /** 88 | * Returns a list containing the values of the data series at |count| 89 | * points, starting at |startTime|, and |stepSize| milliseconds apart. 90 | * Caches values, so showing/hiding individual data series is fast. 91 | */ 92 | getValues: function(startTime, stepSize, count) { 93 | // Use cached values, if we can. 94 | if (this.cacheStartTime_ === startTime && 95 | this.cacheStepSize_ === stepSize && 96 | this.cacheValues_.length === count) { 97 | return this.cacheValues_; 98 | } 99 | 100 | // Do all the work. 101 | this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); 102 | this.cacheStartTime_ = startTime; 103 | this.cacheStepSize_ = stepSize; 104 | 105 | return this.cacheValues_; 106 | }, 107 | 108 | /** 109 | * Returns the cached |values| in the specified time period. 110 | */ 111 | getValuesInternal_: function(startTime, stepSize, count) { 112 | let values = []; 113 | let nextPoint = 0; 114 | let currentValue = 0; 115 | let time = startTime; 116 | for (let i = 0; i < count; ++i) { 117 | while (nextPoint < this.dataPoints_.length && 118 | this.dataPoints_[nextPoint].time < time) { 119 | currentValue = this.dataPoints_[nextPoint].value; 120 | ++nextPoint; 121 | } 122 | values[i] = currentValue; 123 | time += stepSize; 124 | } 125 | return values; 126 | } 127 | }; 128 | 129 | /** 130 | * A single point in a data series. Each point has a time, in the form of 131 | * milliseconds since the Unix epoch, and a numeric value. 132 | * @constructor 133 | */ 134 | function DataPoint(time, value) { 135 | this.time = time; 136 | this.value = value; 137 | } 138 | 139 | return TimelineDataSeries; 140 | })(); 141 | 142 | const TimelineGraphView = (function() { 143 | // Maximum number of labels placed vertically along the sides of the graph. 144 | let MAX_VERTICAL_LABELS = 6; 145 | 146 | // Vertical spacing between labels and between the graph and labels. 147 | let LABEL_VERTICAL_SPACING = 4; 148 | // Horizontal spacing between vertically placed labels and the edges of the 149 | // graph. 150 | let LABEL_HORIZONTAL_SPACING = 3; 151 | // Horizintal spacing between two horitonally placed labels along the bottom 152 | // of the graph. 153 | // var LABEL_LABEL_HORIZONTAL_SPACING = 25; 154 | 155 | // Length of ticks, in pixels, next to y-axis labels. The x-axis only has 156 | // one set of labels, so it can use lines instead. 157 | let Y_AXIS_TICK_LENGTH = 10; 158 | 159 | let GRID_COLOR = '#CCC'; 160 | let TEXT_COLOR = '#000'; 161 | let BACKGROUND_COLOR = '#FFF'; 162 | 163 | let MAX_DECIMAL_PRECISION = 2; 164 | 165 | /** 166 | * @constructor 167 | */ 168 | function TimelineGraphView(divId, canvasId) { 169 | this.scrollbar_ = {position_: 0, range_: 0}; 170 | 171 | this.graphDiv_ = document.getElementById(divId); 172 | this.canvas_ = document.getElementById(canvasId); 173 | 174 | // Set the range and scale of the graph. Times are in milliseconds since 175 | // the Unix epoch. 176 | 177 | // All measurements we have must be after this time. 178 | this.startTime_ = 0; 179 | // The current rightmost position of the graph is always at most this. 180 | this.endTime_ = 1; 181 | 182 | this.graph_ = null; 183 | 184 | // Horizontal scale factor, in terms of milliseconds per pixel. 185 | this.scale_ = 1000; 186 | 187 | // Initialize the scrollbar. 188 | this.updateScrollbarRange_(true); 189 | } 190 | 191 | TimelineGraphView.prototype = { 192 | setScale: function(scale) { 193 | this.scale_ = scale; 194 | }, 195 | 196 | // Returns the total length of the graph, in pixels. 197 | getLength_: function() { 198 | let timeRange = this.endTime_ - this.startTime_; 199 | // Math.floor is used to ignore the last partial area, of length less 200 | // than this.scale_. 201 | return Math.floor(timeRange / this.scale_); 202 | }, 203 | 204 | /** 205 | * Returns true if the graph is scrolled all the way to the right. 206 | */ 207 | graphScrolledToRightEdge_: function() { 208 | return this.scrollbar_.position_ === this.scrollbar_.range_; 209 | }, 210 | 211 | /** 212 | * Update the range of the scrollbar. If |resetPosition| is true, also 213 | * sets the slider to point at the rightmost position and triggers a 214 | * repaint. 215 | */ 216 | updateScrollbarRange_: function(resetPosition) { 217 | let scrollbarRange = this.getLength_() - this.canvas_.width; 218 | if (scrollbarRange < 0) { 219 | scrollbarRange = 0; 220 | } 221 | 222 | // If we've decreased the range to less than the current scroll position, 223 | // we need to move the scroll position. 224 | if (this.scrollbar_.position_ > scrollbarRange) { 225 | resetPosition = true; 226 | } 227 | 228 | this.scrollbar_.range_ = scrollbarRange; 229 | if (resetPosition) { 230 | this.scrollbar_.position_ = scrollbarRange; 231 | this.repaint(); 232 | } 233 | }, 234 | 235 | /** 236 | * Sets the date range displayed on the graph, switches to the default 237 | * scale factor, and moves the scrollbar all the way to the right. 238 | */ 239 | setDateRange: function(startDate, endDate) { 240 | this.startTime_ = startDate.getTime(); 241 | this.endTime_ = endDate.getTime(); 242 | 243 | // Safety check. 244 | if (this.endTime_ <= this.startTime_) { 245 | this.startTime_ = this.endTime_ - 1; 246 | } 247 | 248 | this.updateScrollbarRange_(true); 249 | }, 250 | 251 | /** 252 | * Updates the end time at the right of the graph to be the current time. 253 | * Specifically, updates the scrollbar's range, and if the scrollbar is 254 | * all the way to the right, keeps it all the way to the right. Otherwise, 255 | * leaves the view as-is and doesn't redraw anything. 256 | */ 257 | updateEndDate: function(optDate) { 258 | this.endTime_ = optDate || (new Date()).getTime(); 259 | this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); 260 | }, 261 | 262 | getStartDate: function() { 263 | return new Date(this.startTime_); 264 | }, 265 | 266 | /** 267 | * Replaces the current TimelineDataSeries with |dataSeries|. 268 | */ 269 | setDataSeries: function(dataSeries) { 270 | // Simply recreates the Graph. 271 | this.graph_ = new Graph(); 272 | for (let i = 0; i < dataSeries.length; ++i) { 273 | this.graph_.addDataSeries(dataSeries[i]); 274 | } 275 | this.repaint(); 276 | }, 277 | 278 | /** 279 | * Adds |dataSeries| to the current graph. 280 | */ 281 | addDataSeries: function(dataSeries) { 282 | if (!this.graph_) { 283 | this.graph_ = new Graph(); 284 | } 285 | this.graph_.addDataSeries(dataSeries); 286 | this.repaint(); 287 | }, 288 | 289 | /** 290 | * Draws the graph on |canvas_|. 291 | */ 292 | repaint: function() { 293 | this.repaintTimerRunning_ = false; 294 | 295 | let width = this.canvas_.width; 296 | let height = this.canvas_.height; 297 | let context = this.canvas_.getContext('2d'); 298 | 299 | // Clear the canvas. 300 | context.fillStyle = BACKGROUND_COLOR; 301 | context.fillRect(0, 0, width, height); 302 | 303 | // Try to get font height in pixels. Needed for layout. 304 | let fontHeightString = context.font.match(/([0-9]+)px/)[1]; 305 | let fontHeight = parseInt(fontHeightString); 306 | 307 | // Safety check, to avoid drawing anything too ugly. 308 | if (fontHeightString.length === 0 || fontHeight <= 0 || 309 | fontHeight * 4 > height || width < 50) { 310 | return; 311 | } 312 | 313 | // Save current transformation matrix so we can restore it later. 314 | context.save(); 315 | 316 | // The center of an HTML canvas pixel is technically at (0.5, 0.5). This 317 | // makes near straight lines look bad, due to anti-aliasing. This 318 | // translation reduces the problem a little. 319 | context.translate(0.5, 0.5); 320 | 321 | // Figure out what time values to display. 322 | let position = this.scrollbar_.position_; 323 | // If the entire time range is being displayed, align the right edge of 324 | // the graph to the end of the time range. 325 | if (this.scrollbar_.range_ === 0) { 326 | position = this.getLength_() - this.canvas_.width; 327 | } 328 | let visibleStartTime = this.startTime_ + position * this.scale_; 329 | 330 | // Make space at the bottom of the graph for the time labels, and then 331 | // draw the labels. 332 | let textHeight = height; 333 | height -= fontHeight + LABEL_VERTICAL_SPACING; 334 | this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); 335 | 336 | // Draw outline of the main graph area. 337 | context.strokeStyle = GRID_COLOR; 338 | context.strokeRect(0, 0, width - 1, height - 1); 339 | 340 | if (this.graph_) { 341 | // Layout graph and have them draw their tick marks. 342 | this.graph_.layout( 343 | width, height, fontHeight, visibleStartTime, this.scale_); 344 | this.graph_.drawTicks(context); 345 | 346 | // Draw the lines of all graphs, and then draw their labels. 347 | this.graph_.drawLines(context); 348 | this.graph_.drawLabels(context); 349 | } 350 | 351 | // Restore original transformation matrix. 352 | context.restore(); 353 | }, 354 | 355 | /** 356 | * Draw time labels below the graph. Takes in start time as an argument 357 | * since it may not be |startTime_|, when we're displaying the entire 358 | * time range. 359 | */ 360 | drawTimeLabels: function(context, width, height, textHeight, startTime) { 361 | // Draw the labels 1 minute apart. 362 | let timeStep = 1000 * 60; 363 | 364 | // Find the time for the first label. This time is a perfect multiple of 365 | // timeStep because of how UTC times work. 366 | let time = Math.ceil(startTime / timeStep) * timeStep; 367 | 368 | context.textBaseline = 'bottom'; 369 | context.textAlign = 'center'; 370 | context.fillStyle = TEXT_COLOR; 371 | context.strokeStyle = GRID_COLOR; 372 | 373 | // Draw labels and vertical grid lines. 374 | while (true) { 375 | let x = Math.round((time - startTime) / this.scale_); 376 | if (x >= width) { 377 | break; 378 | } 379 | let text = (new Date(time)).toLocaleTimeString(); 380 | context.fillText(text, x, textHeight); 381 | context.beginPath(); 382 | context.lineTo(x, 0); 383 | context.lineTo(x, height); 384 | context.stroke(); 385 | time += timeStep; 386 | } 387 | }, 388 | 389 | getDataSeriesCount: function() { 390 | if (this.graph_) { 391 | return this.graph_.dataSeries_.length; 392 | } 393 | return 0; 394 | }, 395 | 396 | hasDataSeries: function(dataSeries) { 397 | if (this.graph_) { 398 | return this.graph_.hasDataSeries(dataSeries); 399 | } 400 | return false; 401 | }, 402 | 403 | }; 404 | 405 | /** 406 | * A Graph is responsible for drawing all the TimelineDataSeries that have 407 | * the same data type. Graphs are responsible for scaling the values, laying 408 | * out labels, and drawing both labels and lines for its data series. 409 | */ 410 | const Graph = (function() { 411 | /** 412 | * @constructor 413 | */ 414 | function Graph() { 415 | this.dataSeries_ = []; 416 | 417 | // Cached properties of the graph, set in layout. 418 | this.width_ = 0; 419 | this.height_ = 0; 420 | this.fontHeight_ = 0; 421 | this.startTime_ = 0; 422 | this.scale_ = 0; 423 | 424 | // The lowest/highest values adjusted by the vertical label step size 425 | // in the displayed range of the graph. Used for scaling and setting 426 | // labels. Set in layoutLabels. 427 | this.min_ = 0; 428 | this.max_ = 0; 429 | 430 | // Cached text of equally spaced labels. Set in layoutLabels. 431 | this.labels_ = []; 432 | } 433 | 434 | /** 435 | * A Label is the label at a particular position along the y-axis. 436 | * @constructor 437 | */ 438 | /* 439 | function Label(height, text) { 440 | this.height = height; 441 | this.text = text; 442 | } 443 | */ 444 | 445 | Graph.prototype = { 446 | addDataSeries: function(dataSeries) { 447 | this.dataSeries_.push(dataSeries); 448 | }, 449 | 450 | hasDataSeries: function(dataSeries) { 451 | for (let i = 0; i < this.dataSeries_.length; ++i) { 452 | if (this.dataSeries_[i] === dataSeries) { 453 | return true; 454 | } 455 | } 456 | return false; 457 | }, 458 | 459 | /** 460 | * Returns a list of all the values that should be displayed for a given 461 | * data series, using the current graph layout. 462 | */ 463 | getValues: function(dataSeries) { 464 | if (!dataSeries.isVisible()) { 465 | return null; 466 | } 467 | return dataSeries.getValues(this.startTime_, this.scale_, this.width_); 468 | }, 469 | 470 | /** 471 | * Updates the graph's layout. In particular, both the max value and 472 | * label positions are updated. Must be called before calling any of the 473 | * drawing functions. 474 | */ 475 | layout: function(width, height, fontHeight, startTime, scale) { 476 | this.width_ = width; 477 | this.height_ = height; 478 | this.fontHeight_ = fontHeight; 479 | this.startTime_ = startTime; 480 | this.scale_ = scale; 481 | 482 | // Find largest value. 483 | let max = 0; 484 | let min = 0; 485 | for (let i = 0; i < this.dataSeries_.length; ++i) { 486 | let values = this.getValues(this.dataSeries_[i]); 487 | if (!values) { 488 | continue; 489 | } 490 | for (let j = 0; j < values.length; ++j) { 491 | if (values[j] > max) { 492 | max = values[j]; 493 | } else if (values[j] < min) { 494 | min = values[j]; 495 | } 496 | } 497 | } 498 | 499 | this.layoutLabels_(min, max); 500 | }, 501 | 502 | /** 503 | * Lays out labels and sets |max_|/|min_|, taking the time units into 504 | * consideration. |maxValue| is the actual maximum value, and 505 | * |max_| will be set to the value of the largest label, which 506 | * will be at least |maxValue|. Similar for |min_|. 507 | */ 508 | layoutLabels_: function(minValue, maxValue) { 509 | if (maxValue - minValue < 1024) { 510 | this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); 511 | return; 512 | } 513 | 514 | // Find appropriate units to use. 515 | let units = ['', 'k', 'M', 'G', 'T', 'P']; 516 | // Units to use for labels. 0 is '1', 1 is K, etc. 517 | // We start with 1, and work our way up. 518 | let unit = 1; 519 | minValue /= 1024; 520 | maxValue /= 1024; 521 | while (units[unit + 1] && maxValue - minValue >= 1024) { 522 | minValue /= 1024; 523 | maxValue /= 1024; 524 | ++unit; 525 | } 526 | 527 | // Calculate labels. 528 | this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION); 529 | 530 | // Append units to labels. 531 | for (let i = 0; i < this.labels_.length; ++i) { 532 | this.labels_[i] += ' ' + units[unit]; 533 | } 534 | 535 | // Convert |min_|/|max_| back to unit '1'. 536 | this.min_ *= Math.pow(1024, unit); 537 | this.max_ *= Math.pow(1024, unit); 538 | }, 539 | 540 | /** 541 | * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the 542 | * maximum number of decimal digits allowed. The minimum allowed 543 | * difference between two adjacent labels is 10^-|maxDecimalDigits|. 544 | */ 545 | layoutLabelsBasic_: function(minValue, maxValue, maxDecimalDigits) { 546 | this.labels_ = []; 547 | let range = maxValue - minValue; 548 | // No labels if the range is 0. 549 | if (range === 0) { 550 | this.min_ = this.max_ = maxValue; 551 | return; 552 | } 553 | 554 | // The maximum number of equally spaced labels allowed. |fontHeight_| 555 | // is doubled because the top two labels are both drawn in the same 556 | // gap. 557 | let minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; 558 | 559 | // The + 1 is for the top label. 560 | let maxLabels = 1 + this.height_ / minLabelSpacing; 561 | if (maxLabels < 2) { 562 | maxLabels = 2; 563 | } else if (maxLabels > MAX_VERTICAL_LABELS) { 564 | maxLabels = MAX_VERTICAL_LABELS; 565 | } 566 | 567 | // Initial try for step size between conecutive labels. 568 | let stepSize = Math.pow(10, -maxDecimalDigits); 569 | // Number of digits to the right of the decimal of |stepSize|. 570 | // Used for formating label strings. 571 | let stepSizeDecimalDigits = maxDecimalDigits; 572 | 573 | // Pick a reasonable step size. 574 | while (true) { 575 | // If we use a step size of |stepSize| between labels, we'll need: 576 | // 577 | // Math.ceil(range / stepSize) + 1 578 | // 579 | // labels. The + 1 is because we need labels at both at 0 and at 580 | // the top of the graph. 581 | 582 | // Check if we can use steps of size |stepSize|. 583 | if (Math.ceil(range / stepSize) + 1 <= maxLabels) { 584 | break; 585 | } 586 | // Check |stepSize| * 2. 587 | if (Math.ceil(range / (stepSize * 2)) + 1 <= maxLabels) { 588 | stepSize *= 2; 589 | break; 590 | } 591 | // Check |stepSize| * 5. 592 | if (Math.ceil(range / (stepSize * 5)) + 1 <= maxLabels) { 593 | stepSize *= 5; 594 | break; 595 | } 596 | stepSize *= 10; 597 | if (stepSizeDecimalDigits > 0) { 598 | --stepSizeDecimalDigits; 599 | } 600 | } 601 | 602 | // Set the min/max so it's an exact multiple of the chosen step size. 603 | this.max_ = Math.ceil(maxValue / stepSize) * stepSize; 604 | this.min_ = Math.floor(minValue / stepSize) * stepSize; 605 | 606 | // Create labels. 607 | for (let label = this.max_; label >= this.min_; label -= stepSize) { 608 | this.labels_.push(label.toFixed(stepSizeDecimalDigits)); 609 | } 610 | }, 611 | 612 | /** 613 | * Draws tick marks for each of the labels in |labels_|. 614 | */ 615 | drawTicks: function(context) { 616 | let x1; 617 | let x2; 618 | x1 = this.width_ - 1; 619 | x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; 620 | 621 | context.fillStyle = GRID_COLOR; 622 | context.beginPath(); 623 | for (let i = 1; i < this.labels_.length - 1; ++i) { 624 | // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 625 | // lines. 626 | let y = Math.round(this.height_ * i / (this.labels_.length - 1)); 627 | context.moveTo(x1, y); 628 | context.lineTo(x2, y); 629 | } 630 | context.stroke(); 631 | }, 632 | 633 | /** 634 | * Draws a graph line for each of the data series. 635 | */ 636 | drawLines: function(context) { 637 | // Factor by which to scale all values to convert them to a number from 638 | // 0 to height - 1. 639 | let scale = 0; 640 | let bottom = this.height_ - 1; 641 | if (this.max_) { 642 | scale = bottom / (this.max_ - this.min_); 643 | } 644 | 645 | // Draw in reverse order, so earlier data series are drawn on top of 646 | // subsequent ones. 647 | for (let i = this.dataSeries_.length - 1; i >= 0; --i) { 648 | let values = this.getValues(this.dataSeries_[i]); 649 | if (!values) { 650 | continue; 651 | } 652 | context.strokeStyle = this.dataSeries_[i].getColor(); 653 | context.beginPath(); 654 | for (let x = 0; x < values.length; ++x) { 655 | // The rounding is needed to avoid ugly 2-pixel wide anti-aliased 656 | // horizontal lines. 657 | context.lineTo( 658 | x, bottom - Math.round((values[x] - this.min_) * scale)); 659 | } 660 | context.stroke(); 661 | } 662 | }, 663 | 664 | /** 665 | * Draw labels in |labels_|. 666 | */ 667 | drawLabels: function(context) { 668 | if (this.labels_.length === 0) { 669 | return; 670 | } 671 | let x = this.width_ - LABEL_HORIZONTAL_SPACING; 672 | 673 | // Set up the context. 674 | context.fillStyle = TEXT_COLOR; 675 | context.textAlign = 'right'; 676 | 677 | // Draw top label, which is the only one that appears below its tick 678 | // mark. 679 | context.textBaseline = 'top'; 680 | context.fillText(this.labels_[0], x, 0); 681 | 682 | // Draw all the other labels. 683 | context.textBaseline = 'bottom'; 684 | let step = (this.height_ - 1) / (this.labels_.length - 1); 685 | for (let i = 1; i < this.labels_.length; ++i) { 686 | context.fillText(this.labels_[i], x, step * i); 687 | } 688 | } 689 | }; 690 | 691 | return Graph; 692 | })(); 693 | 694 | return TimelineGraphView; 695 | })(); 696 | --------------------------------------------------------------------------------