├── 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 |
34 |
35 |
--------------------------------------------------------------------------------
/08_chapter/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Chat Room
4 |
5 |
6 |
7 |
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 |
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{
111 | if(e.candidate){
112 | sendMessage(roomid, {
113 | type : 'candidate',
114 | label : event.candidate.sdpMLineIndex,
115 | id:event.candidate.sdpMid,
116 | candidate:event.candidate.candidate
117 | });
118 | }else {
119 | console.log('this is the end candidate');
120 | }
121 | }
122 |
123 | pc.ontrack = getRemoteStream;
124 | } else {
125 | console.waring('the pc have be created!');
126 | }
127 |
128 | return;
129 | }
130 |
131 | function bindTracks(){
132 | console.log('bind tracks into RTCPeerConnection!');
133 |
134 | if(pc === null || pc === undefined){
135 | console.error('pc is null or undefined!');
136 | return;
137 | }
138 |
139 | if(localStream === null || localStream === undefined){
140 | console.error('localStream is null or undefined!');
141 | return;
142 | }
143 |
144 | localStream.getTracks().forEach((track)=>{
145 | pc.addTrack(track, localStream);
146 | });
147 | }
148 |
149 | function createAnswerSuc(desc){
150 | pc.setLocalDescription(desc);
151 |
152 | answer.value = desc.sdp;
153 | optBw.disabled = false;
154 |
155 | sendMessage(roomid, desc);
156 | }
157 |
158 | function createOfferSuc(desc){
159 | pc.setLocalDescription(desc);
160 | offer.value = desc.sdp;
161 | offerdesc = desc;
162 |
163 | //send offer sdp
164 | sendMessage(roomid, offerdesc);
165 | }
166 |
167 | function createOfferFail(err){
168 | console.error('Failed to create answer:', err);
169 | }
170 |
171 | function call(){
172 | if(state === 'joined_conn'){
173 | var offerOptions = {
174 | offerToRecieveAudio : 1,
175 | offerToRecieveVideo : 1
176 | }
177 |
178 | pc.createOffer(offerOptions)
179 | .then(createOfferSuc)
180 | .catch(createOfferFail);
181 | }
182 | }
183 |
184 | function closeLocalMedia(){
185 | if(localStream && localStream.getTracks()){
186 | localStream.getTracks().forEach((track) => {
187 | track.stop();
188 | });
189 | }
190 | localStream = null;
191 | }
192 |
193 | function conn(){
194 | //3 ===============连接信令服务器,并设置回调方法===============
195 | socket = io.connect();
196 | socket.on("joined", (roomid, id) => {
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 |
--------------------------------------------------------------------------------