เกี่ยวกับ Codelab นี้
1 ข้อควรทราบก่อนที่จะเริ่มต้น
Codelab นี้จะสอนวิธีสร้างแอปเพื่อรับวิดีโอและถ่ายภาพหน้าจอด้วยเว็บแคม แล้วแชร์ผ่านเพียร์กับเพียร์ทูเพียร์ นอกจากนี้ คุณยังดูวิธีใช้ WebRTC API หลักและตั้งค่าเซิร์ฟเวอร์การรับส่งข้อความด้วย Node.js ได้อีกด้วย
สิ่งที่ต้องมีก่อน
- ความรู้พื้นฐานเกี่ยวกับ HTML, CSS และ JavaScript
สิ่งที่คุณจะสร้าง
- รับวิดีโอจากเว็บแคม
- สตรีมวิดีโอกับ
RTCPeerConnection
- สตรีมข้อมูลกับ
RTCDataChannel
- ตั้งค่าบริการส่งสัญญาณเพื่อแลกเปลี่ยนข้อความ
- รวมการเชื่อมต่อเพียร์และสัญญาณ
- ถ่ายภาพและใช้ช่องทางข้อมูลเพื่อแชร์
สิ่งที่ต้องมี
- Chrome เวอร์ชัน 47 ขึ้นไป
- เว็บเซิร์ฟเวอร์สําหรับ Chrome หรือเว็บเซิร์ฟเวอร์ที่คุณต้องการ
- เครื่องมือแก้ไขข้อความที่ต้องการ
- Node.js
2 รับโค้ดตัวอย่าง
ดาวน์โหลดโค้ด
- หากคุณคุ้นเคยกับ Git ให้เรียกใช้คําสั่งนี้เพื่อโคลนโค้ดสําหรับ Codelab นี้จาก GitHub ดังนี้
git clone https://github.com/googlecodelabs/webrtc-web
หรือคลิกลิงก์นี้เพื่อดาวน์โหลดไฟล์ ZIP ของโค้ด
- เปิดไฟล์ ZIP ที่ดาวน์โหลดเพื่อคลายโฟลเดอร์โฟลเดอร์ชื่อ
webrtc-web-master
ซึ่งมีโฟลเดอร์เดียวสําหรับแต่ละขั้นตอนของ Codelab นี้และทรัพยากรทั้งหมดที่คุณต้องการ
โค้ดทั้งหมดจะทําในไดเรกทอรีชื่อ work
โฟลเดอร์ step-nn
มีเวอร์ชันที่เสร็จสมบูรณ์สําหรับแต่ละขั้นตอนของ Codelab นี้ อยู่ที่นั่นเพื่อเป็นข้อมูลอ้างอิง
ติดตั้งและยืนยันเว็บเซิร์ฟเวอร์
แม้ว่าคุณจะใช้เว็บเซิร์ฟเวอร์ของคุณเองได้ แต่ Codelab นี้ได้รับการออกแบบมาให้ทํางานกับเว็บเซิร์ฟเวอร์สําหรับ Chrome ได้เป็นอย่างดี
- หากคุณไม่มีเว็บเซิร์ฟเวอร์สําหรับ Chrome ให้คลิกลิงก์นี้เพื่อติดตั้งจาก Chrome เว็บสโตร์:
- คลิกเพิ่มใน Chrome ซึ่งจะติดตั้งเว็บเซิร์ฟเวอร์สําหรับ Chrome และเปิดแอป Google โดยอัตโนมัติในแท็บใหม่
- คลิกเว็บเซิร์ฟเวอร์ดังนี้
กล่องโต้ตอบจะปรากฏขึ้น ซึ่งให้คุณกําหนดค่าเว็บเซิร์ฟเวอร์ในเครื่องได้ ดังนี้
- คลิกเลือกโฟลเดอร์
- เลือกโฟลเดอร์
work
ที่คุณสร้าง
ในส่วน URL ของเว็บเซิร์ฟเวอร์ คุณจะเห็น URL ที่คุณสามารถดูงานที่อยู่ระหว่างดําเนินการได้
Chrome
- ในส่วนตัวเลือก (อาจต้องรีสตาร์ท) ให้เลือกช่องทําเครื่องหมายแสดง index.html โดยอัตโนมัติ
- สลับเว็บเซิร์ฟเวอร์: เริ่ม 2 ครั้งเพื่อหยุดและรีสตาร์ทเซิร์ฟเวอร์
- คลิก URL ใต้ URL ของเว็บเซิร์ฟเวอร์เพื่อดูงานของคุณในเว็บเบราว์เซอร์
คุณควรจะเห็นหน้าลักษณะนี้ ซึ่งสอดคล้องกับ work/index.html
:
เห็นได้ชัดว่าแอปนี้ยังไม่ได้ทําอะไรที่น่าสนใจ เพราะเป็นการใช้โครงกระดูกเพียงเล็กน้อยเพื่อให้มั่นใจว่าเว็บเซิร์ฟเวอร์จะทํางานได้อย่างถูกต้อง คุณเพิ่มฟังก์ชันการทํางานและฟีเจอร์เลย์เอาต์ได้ในขั้นตอนต่อไป
3 สตรีมวิดีโอจากเว็บแคมของคุณ
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-01
เพิ่มขีดกลาง HTML
คัดลอกโค้ดนี้และวางลงในไฟล์ index.html
ในไดเรกทอรี work
เพื่อเพิ่มองค์ประกอบ video
และ script
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
</html>
บีบ JavaScript เล็กน้อย
คัดลอกโค้ดนี้และวางลงในไฟล์ main.js
ในโฟลเดอร์ js
:
'use strict';
// In this codelab, you only stream video, not audio (video: true).
const mediaStreamConstraints = {
video: true,
};
// The video element where the stream is displayed
const localVideo = document.querySelector('video');
// The local stream that's displayed on the video
let localStream;
// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
ลองใช้
เปิดไฟล์ index.html
ในเบราว์เซอร์ คุณควรเห็นไฟล์ลักษณะนี้ แต่มียอดดูจากเว็บแคม
วิธีการทำงาน
หลังการเรียกใช้ getUserMedia()
เบราว์เซอร์จะขอสิทธิ์เข้าถึงกล้องหากเป็นคําขอแรกสําหรับการเข้าถึงกล้องสําหรับต้นทางปัจจุบัน
หากยืนยันสําเร็จ ระบบจะแสดงผล MediaStream
ซึ่งองค์ประกอบ media
จะใช้ผ่านแอตทริบิวต์ srcObject
ได้ ดังนี้
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
}
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
}
อาร์กิวเมนต์ constraints
ช่วยให้คุณระบุสื่อที่ต้องการรับได้ ในตัวอย่างนี้ สื่อเป็นแค่วิดีโอเพราะเสียงปิดอยู่โดยค่าเริ่มต้น
const mediaStreamConstraints = {
video: true,
};
คุณใช้ข้อจํากัดเพื่อดูข้อกําหนดเพิ่มเติมได้ เช่น ความละเอียดของวิดีโอ ดังนี้
const hdConstraints = {
video: {
width: {
min: 1280
},
height: {
min: 720
}
}
}
ข้อกําหนดของ MediaTrackConstraints
จะแสดงประเภทของข้อจํากัดที่เป็นไปได้ทั้งหมด แต่มีบางตัวเลือกเท่านั้นที่รองรับ หากกล้องที่เลือกในปัจจุบันไม่รองรับความละเอียดที่ขอ getUserMedia()
จะถูกปฏิเสธพร้อมกับ OverconstrainedError
และคุณจะได้รับแจ้งให้ให้สิทธิ์ในการเข้าถึงกล้อง
หาก getUserMedia()
ประสบความสําเร็จ ระบบจะตั้งค่าสตรีมวิดีโอจากเว็บแคมเป็นแหล่งที่มาขององค์ประกอบวิดีโอ ดังนี้
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
}
คะแนนโบนัส
- ออบเจ็กต์
localStream
ที่ส่งไปยังgetUserMedia()
อยู่ในขอบเขตส่วนกลาง คุณจึงตรวจสอบได้จากคอนโซลเบราว์เซอร์ เปิดคอนโซล จากนั้นพิมพ์stream,
แล้วกดEnter
(Return
ใน Mac) หากต้องการดูคอนโซลใน Chrome ให้กดControl+Shift+J
(หรือCommand+Option+J
ใน Mac) localStream.getVideoTracks()
ส่งคืนอะไร- โทรมาที่
localStream.getVideoTracks()[0].stop()
- ดูออบเจ็กต์ข้อจํากัด จะเกิดอะไรขึ้นเมื่อเปลี่ยนเป็น
{audio: true, video: true}
- องค์ประกอบวิดีโอมีขนาดเท่าใด คุณคงจะได้ขนาดวิดีโอตามธรรมชาติจาก JavaScript มากกว่าขนาดแสดงผลอย่างไร ใช้เครื่องมือสําหรับนักพัฒนาซอฟต์แวร์ของ Google Chrome เพื่อตรวจสอบ
- เพิ่มตัวกรอง CSS ลงในองค์ประกอบวิดีโอ ดังนี้
video {
filter: blur(4px) invert(1) opacity(0.5);
}
- เพิ่มตัวกรอง SVG เช่น
video {
filter: hue-rotate(180deg) saturate(200%);
}
เคล็ดลับ
- อย่าลืมแอตทริบิวต์
autoplay
ในองค์ประกอบvideo
หากไม่มี คุณจะเห็นเฉพาะเฟรมเดียว - มีตัวเลือกเพิ่มเติมสําหรับข้อจํากัด
getUserMedia()
รายการ ดูตัวอย่างเพิ่มเติมได้ที่ข้อจํากัดและสถิติ และข้อจํากัดและสถิติเพิ่มเติมในตัวอย่าง WebRTC
แนวทางปฏิบัติแนะนำ
ตรวจสอบว่าองค์ประกอบวิดีโอไม่ทําให้คอนเทนเนอร์ล้น Codelab นี้เพิ่ม width
และ max-width
เพื่อกําหนดขนาดที่ต้องการและขนาดสูงสุดสําหรับวิดีโอ เบราว์เซอร์จะคํานวณความสูงโดยอัตโนมัติ
video {
max-width: 100%;
width: 320px;
}
4 สตรีมวิดีโอด้วย RTCPeerConnection API
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-2
เพิ่มองค์ประกอบวิดีโอและปุ่มควบคุม
ในไฟล์ index.html
ให้แทนที่องค์ประกอบ video
รายการเดียวด้วยองค์ประกอบ video
และ button
จํานวน 3 รายการ ได้แก่
<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
องค์ประกอบวิดีโอ 1 รายการแสดงสตรีมจาก getUserMedia()
และอีกรายการแสดงวิดีโอเดียวกันที่สตรีมผ่าน RTCPeerconnection
(ในแอปจริง องค์ประกอบ video
รายการหนึ่งจะแสดงสตรีมในเครื่อง และอีกรายการจะแสดงสตรีมระยะไกล)
เพิ่ม Shim ของอะแดปเตอร์.js
คัดลอกองค์ประกอบสคริปต์นี้ แล้ววางไว้เหนือองค์ประกอบสคริปต์สําหรับ main.js
:
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
ไฟล์ index.html
ควรมีลักษณะดังนี้
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
ติดตั้งรหัส RTCPeerConnection
แทนที่ main.js
ด้วยเวอร์ชันในโฟลเดอร์ step-02
โทรออก
- เปิดไฟล์
index.html
- คลิกเริ่มเพื่อรับวิดีโอจากเว็บแคม
- คลิกโทรเพื่อเชื่อมต่อกับเพื่อน
คุณควรเห็นวิดีโอเดียวกันจากเว็บแคมของคุณในองค์ประกอบ video
ทั้งคู่
- ดูคอนโซลเบราว์เซอร์เพื่อดูการบันทึก WebRTC
วิธีการทำงาน
ขั้นตอนนี้ทําได้หลายอย่าง
WebRTC จะใช้ RTCPeerConnection
API เพื่อตั้งค่าการเชื่อมต่อเพื่อสตรีมวิดีโอระหว่างไคลเอ็นต์ WebRTC ที่เรียกว่าแอปเทียบเท่า
ในตัวอย่างนี้ ออบเจ็กต์ RTCPeerConnection
จํานวน 2 รายการอยู่ในหน้าเดียวกัน ได้แก่ pc1
และ pc2
การตั้งค่าการโทรระหว่างกลุ่ม WebRTC เกี่ยวข้องกับงาน 3 อย่างดังนี้
- สร้าง
RTCPeerConnection
สําหรับการโทรแต่ละสายและเพิ่มสตรีมในพื้นที่จากgetUserMedia()
ในแต่ละสาย - รับและแชร์ข้อมูลเครือข่าย
ปลายทางการเชื่อมต่อที่เป็นไปได้ที่รู้จักกันในชื่อผู้สมัคร ICE
- รับและแชร์คําอธิบายในพื้นที่และระยะไกล
ข้อมูลเมตาเกี่ยวกับสื่อท้องถิ่นอยู่ในรูปแบบ Session Description Protocol (SDP)
ลองจินตนาการว่าอลิซและบัญชาต้องการใช้ RTCPeerConnection
เพื่อตั้งค่าวิดีโอแชท
ขั้นแรก Alice และ Bob แลกเปลี่ยนข้อมูลเครือข่าย นิพจน์การค้นหาผู้สมัครหมายถึงกระบวนการค้นหาอินเทอร์เฟซและพอร์ตของเครือข่ายโดยใช้เฟรมเวิร์ก ICE
- ขวัญใจสร้างออบเจ็กต์
RTCPeerConnection
ด้วยเครื่องจัดการonicecandidate (addEventListener('icecandidate'))
โดยสอดคล้องกับโค้ดต่อไปนี้จาก main.js
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
- คุณอลิซโทรหา
getUserMedia()
และเพิ่มสตรีมที่ส่งมาให้
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
then(gotLocalMediaStream).
catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
localVideo.srcObject = mediaStream;
localStream = mediaStream;
trace('Received local stream.');
callButton.disabled = false; // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
- ระบบจะเรียกเครื่องจัดการ
onicecandidate
จากขั้นตอนแรกเมื่อผู้สมัครเครือข่ายพร้อมให้ใช้งาน - ขวัญใจส่งข้อมูลผู้สมัครตามลําดับให้กับบัญชา
ในแอปจริง กระบวนการนี้ (เรียกว่าการส่งสัญญาณ) จะเกิดขึ้นผ่านบริการข้อความ ดูวิธีการได้ในภายหลัง แน่นอนว่าในขั้นตอนนี้ ออบเจ็กต์ RTCPeerConnection
ทั้ง 2 รายการอยู่ในหน้าเดียวกัน และสื่อสารได้โดยตรงโดยไม่ต้องมีการรับส่งข้อความภายนอก
- เมื่อบัญชาได้รับข้อความจากผู้สมัครรับเลือกตั้งจากขวัญใจ เขาโทรหา
addIceCandidate()
เพื่อเพิ่มผู้สมัครลงในคําอธิบายของกลุ่มแอประยะไกล
function handleConnection(event) {
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
const newIceCandidate = new RTCIceCandidate(iceCandidate);
const otherPeer = getOtherPeer(peerConnection);
otherPeer.addIceCandidate(newIceCandidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
`${event.candidate.candidate}.`);
}
}
บริษัท WebRTC จะต้องค้นพบและแลกเปลี่ยนข้อมูลสื่อเสียงและวิดีโอในพื้นที่และระยะไกล เช่น ความละเอียดและตัวแปลงรหัส การส่งสัญญาณเพื่อแลกเปลี่ยนข้อมูลการกําหนดค่าสื่อจะดําเนินการแลกเปลี่ยนข้อมูลข้อมูลเมตาต่างๆ หรือที่เรียกว่าข้อเสนอและคําตอบ โดยใช้รูปแบบ SDP
- ขวัญใจใช้เมธอด
RTCPeerConnection
createOffer()
คําสัญญาที่ส่งคืนให้คําอธิบายเซสชันในพื้นที่ของ RTCSessionDescription
- อลิซ
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
- หากสําเร็จ อลิซตั้งค่าคําอธิบายในพื้นที่โดยใช้
setLocalDescription()
แล้วส่งคําอธิบายเซสชันให้บัญชาผ่านช่องทางส่งสัญญาณ - อานนท์กําหนดคําอธิบายที่อลิซส่งเป็นคําอธิบายระยะไกลด้วย
setRemoteDescription()
- บัญชาเรียกใช้เมธอด
createAnswer()
ของRTCPeerConnection
และส่งคําอธิบายระยะไกลที่อลิซได้รับจากอลิซ เพื่อให้เซสชันในเครื่องสร้างขึ้นเพื่อเข้ากันได้กับกําหนดการของอลิซ - คําสัญญา
createAnswer()
จะผ่านRTCSessionDescription
ซึ่งบัญชากําหนดให้เป็นคําอธิบายในพื้นที่และส่งไปยังอลิซ - เมื่ออลิซได้รับคําอธิบายเซสชันของบัญชาแล้ว เธอจะกําหนดเป็นคําอธิบายระยะไกลด้วย
setRemoteDescription()
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
trace(`Offer from localPeerConnection:\n${description.sdp}`);
trace('localPeerConnection setLocalDescription start.');
localPeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection setRemoteDescription start.');
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection createAnswer start.');
remotePeerConnection.createAnswer()
.then(createdAnswer)
.catch(setSessionDescriptionError);
}
// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
trace('remotePeerConnection setLocalDescription start.');
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('localPeerConnection setRemoteDescription start.');
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
}
คะแนนโบนัส
- ไปที่ chrome://webrtc-internals
หน้านี้ให้สถิติ WebRTC และข้อมูลการแก้ไขข้อบกพร่อง (ดูรายการ URL ของ Chrome ได้ที่ chrome://about)
- จัดรูปแบบหน้าด้วย CSS
- วางวิดีโอไว้ข้างกัน
- ทําให้ปุ่มมีความกว้างเท่ากันด้วยข้อความขนาดใหญ่ขึ้น
- ตรวจสอบว่าเลย์เอาต์ทํางานบนอุปกรณ์เคลื่อนที่
- จากคอนโซลเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์ Chrome ให้ดู
localStream
,localPeerConnection
และremotePeerConnection
- ดู
localPeerConnectionpc1.localDescription
จากคอนโซล
รูปแบบ SDP มีลักษณะอย่างไร
เคล็ดลับ
- ดูข้อมูลเพิ่มเติมเกี่ยวกับอะแดปเตอร์.ads.js ได้ที่ adapter.js ที่เก็บของ GitHub
- ดู AppRTC และโค้ดของแอปซึ่งเป็นแอป Canonical ของ WebRTC สําหรับการเรียก WebRTC เวลาตั้งค่าการโทรน้อยกว่า 500 มิลลิวินาที
แนวทางปฏิบัติแนะนำ
ใช้ API แบบใหม่ของ Promise เพื่อเปิดใช้ความเข้ากันได้กับโค้ดในอนาคตและเปิดใช้ความเข้ากันได้กับเบราว์เซอร์ที่ไม่รองรับ adapter.js
5 ใช้ช่องทางข้อมูลเพื่อแลกเปลี่ยนข้อมูล
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-03
อัปเดต HTML
ในขั้นตอนนี้ คุณจะใช้ช่องข้อมูล WebRTC เพื่อส่งข้อความระหว่างองค์ประกอบ textarea
2 รายการบนหน้าเดียวกัน นั่นไม่มีประโยชน์เท่าไร แต่จะแสดงวิธีใช้ WebRTC ในการแชร์ข้อมูล รวมทั้งสตรีมวิดีโอ
นําองค์ประกอบ video
และ button
ออกจาก index.html,
และแทนที่ด้วย HTML ต่อไปนี้
<textarea id="dataChannelSend" disabled
placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>
<div id="buttons">
<button id="startButton">Start</button>
<button id="sendButton">Send</button>
<button id="closeButton">Stop</button>
</div>
textarea
รายการหนึ่งมีไว้สําหรับป้อนข้อความ ส่วนอีกรายการมีไว้สําหรับแสดงข้อความระหว่างสตรีมแอปเทียบเท่า
ไฟล์ index.html
ควรมีลักษณะดังนี้
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<textarea id="dataChannelSend" disabled
placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>
<div id="buttons">
<button id="startButton">Start</button>
<button id="sendButton">Send</button>
<button id="closeButton">Stop</button>
</div>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
อัปเดต JavaScript
- แทนที่
main.js
ด้วยเนื้อหาของstep-03/js/main.js
- ลองสตรีมมิงระหว่างแอปเทียบเท่า:
- เปิด
index.html
- คลิกเริ่มต้นเพื่อตั้งค่าการเชื่อมต่อเพียร์
- ป้อนข้อความใน
textarea
ทางด้านซ้าย - คลิกส่งเพื่อโอนข้อความโดยใช้ช่องทางข้อมูล WebRTC
วิธีการทำงาน
โค้ดนี้ใช้ RTCPeerConnection
และ RTCDataChannel
เพื่อเปิดใช้การแลกเปลี่ยน SMS
โค้ดส่วนใหญ่ในขั้นตอนนี้เหมือนกับในตัวอย่าง RTCPeerConnection
ฟังก์ชัน sendData()
และ createConnection()
มีโค้ดใหม่ส่วนใหญ่ดังนี้
function createConnection() {
dataChannelSend.placeholder = '';
var servers = null;
pcConstraint = null;
dataConstraint = null;
trace('Using SCTP based data channels');
// For SCTP, reliable and ordered delivery is true by default.
// Add localConnection to global scope to make it visible
// from the browser console.
window.localConnection = localConnection =
new RTCPeerConnection(servers, pcConstraint);
trace('Created local peer connection object localConnection');
sendChannel = localConnection.createDataChannel('sendDataChannel',
dataConstraint);
trace('Created send data channel');
localConnection.onicecandidate = iceCallback1;
sendChannel.onopen = onSendChannelStateChange;
sendChannel.onclose = onSendChannelStateChange;
// Add remoteConnection to global scope to make it visible
// from the browser console.
window.remoteConnection = remoteConnection =
new RTCPeerConnection(servers, pcConstraint);
trace('Created remote peer connection object remoteConnection');
remoteConnection.onicecandidate = iceCallback2;
remoteConnection.ondatachannel = receiveChannelCallback;
localConnection.createOffer().then(
gotDescription1,
onCreateSessionDescriptionError
);
startButton.disabled = true;
closeButton.disabled = false;
}
function sendData() {
var data = dataChannelSend.value;
sendChannel.send(data);
trace('Sent Data: ' + data);
}
ไวยากรณ์ของ RTCDataChannel
จงใจคล้ายกับ WebSocket ที่มีเมธอด send()
และเหตุการณ์ message
สังเกตการใช้ dataConstraint
คุณสามารถกําหนดค่าช่องทางข้อมูลเพื่อให้แชร์ข้อมูลประเภทต่างๆ ได้ เช่น ให้ความสําคัญกับการแสดงโฆษณาที่เชื่อถือได้มากกว่าประสิทธิภาพ
คะแนนโบนัส
- เมื่อใช้ SCTP โดยค่าเริ่มต้นแล้ว โปรโตคอลที่ใช้โดยช่องข้อมูล WebRTC จะมีการส่งข้อมูลที่เชื่อถือได้และเรียงลําดับ
RTCDataChannel
อาจจะต้องให้การนําส่งข้อมูลที่เชื่อถือได้เมื่อใดและเมื่อใดที่ประสิทธิภาพอาจสําคัญกว่านั้น แม้ว่านั่นหมายถึงการสูญเสียข้อมูลบางอย่างก็ตาม - ใช้ CSS เพื่อปรับปรุงเลย์เอาต์ของหน้าและเพิ่มแอตทริบิวต์ตัวยึดตําแหน่งลงใน
dataChannelReceive
textarea
- ทดสอบหน้าเว็บบนอุปกรณ์เคลื่อนที่
ดูข้อมูลเพิ่มเติม
6 ตั้งค่าบริการส่งสัญญาณเพื่อแลกเปลี่ยนข้อความ
คุณได้เรียนรู้วิธีแลกเปลี่ยนข้อมูลระหว่างแอปเทียบเท่าในหน้าเดียวกันแล้ว แต่คุณจะแลกเปลี่ยนระหว่างเครื่องต่างๆ ได้อย่างไร คุณจะต้องตั้งค่าช่องส่งสัญญาณเพื่อแลกเปลี่ยนข้อความข้อมูลเมตาก่อน
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-04
เกี่ยวกับแอป
WebRTC จะใช้ JavaScript API ฝั่งไคลเอ็นต์ แต่สําหรับการใช้งานจริงจะต้องใช้เซิร์ฟเวอร์การส่งสัญญาณ (การรับส่งข้อความ) และเซิร์ฟเวอร์ STUN และ TURN ด้วย ดูข้อมูลเพิ่มเติมได้ที่นี่
ในขั้นตอนนี้ คุณสร้างเซิร์ฟเวอร์การส่งสัญญาณ Node.js แบบง่ายโดยใช้โมดูล Socket.IO Node.js และไลบรารี JavaScript สําหรับส่งข้อความ
ในตัวอย่างนี้ เซิร์ฟเวอร์ (แอป Node.js) มีการใช้งานใน index.js
และไคลเอ็นต์ที่ใช้เซิร์ฟเวอร์นี้ (เว็บแอป) มีการใช้งานใน index.html
แอป Node.js ในขั้นตอนนี้มี 2 งาน
ก่อนอื่นจะทําหน้าที่เป็นการส่งต่อข้อความ
socket.on('message', function (message) {
log('Got message: ', message);
socket.broadcast.emit('message', message);
});
วิธีที่ 2 จะจัดการห้องแชทของ WebRTC วิดีโอ ดังนี้
if (numClients === 0) {
socket.join(room);
socket.emit('created', room, socket.id);
} else if (numClients === 1) {
socket.join(room);
socket.emit('joined', room, socket.id);
io.sockets.in(room).emit('ready');
} else { // max two clients
socket.emit('full', room);
}
แอป WebRTC ที่เรียบง่ายของคุณอนุญาตให้เพื่อนแชร์ห้องได้สูงสุด 2 คน
HTML และ JavaScript
- อัปเดต
index.html
ให้มีลักษณะดังนี้
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<script src="/socket.io/socket.io.js"></script>
<script src="js/main.js"></script>
</body>
</html>
คุณจะไม่เห็นอะไรอยู่ในหน้าในขั้นตอนนี้ การบันทึกทั้งหมดจะทําที่คอนโซลของเบราว์เซอร์ หากต้องการดูคอนโซลใน Chrome ให้กด Control+Shift+J
(หรือ Command+Option+J
ใน Mac)
- แทนที่
js/main.js
ด้วยรายการต่อไปนี้
'use strict';
var isInitiator;
window.room = prompt("Enter room name:");
var socket = io.connect();
if (room !== "") {
console.log('Message from client: Asking to join room ' + room);
socket.emit('create or join', room);
}
socket.on('created', function(room, clientId) {
isInitiator = true;
});
socket.on('full', function(room) {
console.log('Message from client: Room ' + room + ' is full :^(');
});
socket.on('ipaddr', function(ipaddr) {
console.log('Message from client: Server IP address is ' + ipaddr);
});
socket.on('joined', function(room, clientId) {
isInitiator = false;
});
socket.on('log', function(array) {
console.log.apply(console, array);
});
ตั้งค่าไฟล์ Socket.IO ให้ทํางานใน Node.js
ในไฟล์ HTML คุณอาจเห็นว่าคุณกําลังใช้ไฟล์ Socket.IO:
<script src="/socket.io/socket.io.js"></script>
- สร้างไฟล์ชื่อ
package.json
ที่ระดับบนสุดของไดเรกทอรีwork
ที่มีเนื้อหาต่อไปนี้
{
"name": "webrtc-codelab",
"version": "0.0.1",
"description": "WebRTC codelab",
"dependencies": {
"node-static": "^0.7.10",
"socket.io": "^1.2.0"
}
}
นี่คือไฟล์ Manifest ของแอปที่บอกโปรเจ็กต์แพ็กเกจโหนด (npm
) ว่าโปรเจ็กต์ใด
การพึ่งพา
- หากต้องการติดตั้งทรัพยากร Dependency เช่น
/socket.io/socket.io.js
ให้เรียกใช้คําสั่งต่อไปนี้จากเทอร์มินัลบรรทัดคําสั่งในไดเรกทอรีwork
npm install
คุณควรเห็นบันทึกการติดตั้งที่ลงท้ายด้วยสิ่งต่อไปนี้
คุณจะเห็น npm
ติดตั้งทรัพยากร Dependency ที่กําหนดไว้ใน package.json
- สร้างไฟล์ใหม่
index.js
ที่ระดับบนสุดของไดเรกทอรีwork
(ไม่ใช่ไดเรกทอรีjs
) แล้วเพิ่มโค้ดต่อไปนี้
'use strict';
var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');
var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
fileServer.serve(req, res);
}).listen(8080);
var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {
// Convenience function to log server messages on the client
function log() {
var array = ['Message from server:'];
array.push.apply(array, arguments);
socket.emit('log', array);
}
socket.on('message', function(message) {
log('Client said: ', message);
// For a real app, would be room-only (not broadcast)
socket.broadcast.emit('message', message);
});
socket.on('create or join', function(room) {
log('Received request to create or join room ' + room);
var clientsInRoom = io.sockets.adapter.rooms[room];
var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
log('Room ' + room + ' now has ' + numClients + ' client(s)');
if (numClients === 0) {
socket.join(room);
log('Client ID ' + socket.id + ' created room ' + room);
socket.emit('created', room, socket.id);
} else if (numClients === 1) {
log('Client ID ' + socket.id + ' joined room ' + room);
io.sockets.in(room).emit('join', room);
socket.join(room);
socket.emit('joined', room, socket.id);
io.sockets.in(room).emit('ready');
} else { // max two clients
socket.emit('full', room);
}
});
socket.on('ipaddr', function() {
var ifaces = os.networkInterfaces();
for (var dev in ifaces) {
ifaces[dev].forEach(function(details) {
if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
socket.emit('ipaddr', details.address);
}
});
}
});
});
- จากบรรทัดคําสั่ง ให้เรียกใช้คําสั่งต่อไปนี้ในไดเรกทอรี
work
node index.js
- เปิดเบราว์เซอร์แล้วไปที่ http://localhost:8080
ทุกครั้งที่คุณไปยัง URL นี้ ระบบจะแจ้งให้ป้อนชื่อห้องแชท
หากต้องการเข้าร่วมห้องแชทเดียวกัน ให้ป้อนชื่อห้องเดียวกันทุกครั้ง เช่น foo
- เปิดแท็บใหม่เพื่อไปที่ http://localhost:8080 อีกครั้ง และป้อนชื่อห้องเดิมอีกครั้ง
- เปิดแท็บใหม่อีกแท็บแล้วไปที่ http://localhost:8080 แล้วป้อนชื่อห้องเดิมอีกครั้ง
- ตรวจสอบคอนโซลในแต่ละแท็บ
คุณควรเห็นการบันทึกจาก JavaScript
คะแนนโบนัส
- กลไกการรับส่งข้อความอื่นที่อาจเป็นไปได้มีอะไรบ้าง คุณอาจพบปัญหาใดโดยใช้ Pure ของ WebSocket
- ปัญหาใดที่อาจเกี่ยวข้องกับการปรับขนาดแอปนี้ ช่วยพัฒนาวิธีทดสอบคําขอห้องในเวลาเดียวกันหรือหลายพันคําขอได้ไหม
- แอปนี้ใช้ข้อความแจ้ง JavaScript เพื่อรับชื่อห้อง ค้นหาวิธีรับชื่อห้องจาก URL เช่น http://localhost:8080/foo จะตั้งชื่อห้องแชทว่า
foo
ดูข้อมูลเพิ่มเติม
7 รวมการเชื่อมต่อแบบเพียร์และสัญญาณ
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-05
แทนที่ HTML และ JavaScript
- แทนที่เนื้อหาของ
index.html
ด้วยข้อมูลต่อไปนี้
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<div id="videos">
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
- แทนที่
js/main.js
ด้วยเนื้อหาของstep-05/js/main.js
เรียกใช้เซิร์ฟเวอร์ Node.js
หากไม่ทําตาม Codelab นี้จากไดเรกทอรี work
คุณอาจต้องติดตั้งทรัพยากร Dependency สําหรับโฟลเดอร์ step-05
หรือโฟลเดอร์การทํางานปัจจุบัน
- เรียกใช้คําสั่งต่อไปนี้จากไดเรกทอรีการทํางานของคุณ
npm install
- เมื่อติดตั้งแล้ว หากเซิร์ฟเวอร์ Node.js ของคุณไม่ทํางาน ให้เรียกใช้คําสั่งต่อไปนี้โดยเรียกใช้คําสั่งต่อไปนี้ในไดเรกทอรี
work
node index.js
ตรวจสอบว่าคุณใช้ index.js
เวอร์ชันเดิมจากขั้นตอนก่อนหน้าที่ใช้ Socket.IO โปรดดูข้อมูลเพิ่มเติมเกี่ยวกับโหนดและ Socket IO ที่หัวข้อตั้งค่าบริการส่งสัญญาณเพื่อแลกเปลี่ยนข้อความ
- เปิดเบราว์เซอร์แล้วไปที่ http://localhost:8080
- เปิดแท็บใหม่และไปที่ http://localhost:8080 อีกครั้ง
องค์ประกอบ video
รายการหนึ่งแสดงสตรีมท้องถิ่นจาก getUserMedia()
และอีกรายการแสดงวิดีโอระยะไกลที่สตรีมผ่าน RTCPeerconnection
- ดูการบันทึกในคอนโซลเบราว์เซอร์
คะแนน****เพื่อให้คะแนนเพิ่มขึ้น
- แอปนี้รองรับวิดีโอแชทแบบตัวต่อตัวเท่านั้น คุณจะเปลี่ยนการออกแบบเพื่ออนุญาตให้คนมากกว่า 1 คนแชร์ห้องแชททางวิดีโอเดียวกันได้อย่างไร
- ตัวอย่างนี้มีห้องชื่อ
foo
ฮาร์ดโค้ด วิธีใดคือการเปิดใช้ชื่อห้องอื่นๆ ได้ดีที่สุด - ผู้ใช้จะแชร์ชื่อห้องอย่างไร ลองสร้างชื่ออื่นแทนการแชร์ชื่อห้อง
- คุณจะเปลี่ยนแอปได้อย่างไร
เคล็ดลับ
- ดูสถิติ WebRTC และแก้ไขข้อบกพร่องที่ chrome://webrtc-internals
- ใช้เครื่องมือแก้ปัญหา WebRTC เพื่อตรวจสอบสภาพแวดล้อมในเครื่อง รวมถึงทดสอบกล้องและไมโครโฟน
- หากพบปัญหาในการแคช ให้ลองทําดังต่อไปนี้
- กด
Control
และคลิกโหลดหน้านี้ซ้ํา - รีสตาร์ทเบราว์เซอร์
- เรียกใช้
npm cache clean
จากบรรทัดคําสั่ง
8 ถ่ายรูปและแชร์ผ่านช่องทางข้อมูล
ขั้นตอนนี้มีทั้งหมดอยู่ในโฟลเดอร์ step-06
วิธีการทำงาน
ก่อนหน้านี้คุณได้เรียนรู้วิธีแลกเปลี่ยน SMS โดยใช้ RTCDataChannel
ขั้นตอนนี้จะช่วยให้แชร์ไฟล์ทั้งหมดได้ ในตัวอย่างนี้ รูปภาพจะบันทึกด้วย getUserMedia()
องค์ประกอบหลักของขั้นตอนนี้มีดังนี้
- สร้างแชแนลข้อมูล
คุณไม่ควรเพิ่มสตรีมสื่อใดๆ ลงในการเชื่อมต่อเพียร์ในขั้นตอนนี้
- บันทึกวิดีโอวิดีโอจากเว็บแคมด้วย
getUserMedia()
:
var video = document.getElementById('video');
function grabWebCamVideo() {
console.log('Getting user media (video) ...');
navigator.mediaDevices.getUserMedia({
video: true
})
.then(gotStream)
.catch(function(e) {
alert('getUserMedia() error: ' + e.name);
});
}
- คลิกสแนปเพื่อดูภาพรวม (เฟรมวิดีโอ) จากสตรีมวิดีโอและแสดงในองค์ประกอบ
canvas
ดังนี้
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');
function snapPhoto() {
photoContext.drawImage(video, 0, 0, photo.width, photo.height);
show(photo, sendBtn);
}
- คลิกส่งเพื่อแปลงรูปภาพเป็นไบต์และส่งผ่านช่องทางข้อมูล
function sendPhoto() {
// Split the data-channel message in chunks of this byte length.
var CHUNK_LEN = 64000;
var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
len = img.data.byteLength,
n = len / CHUNK_LEN | 0;
console.log('Sending a total of ' + len + ' byte(s)');
dataChannel.send(len);
// Split the photo and send in chunks of approximately 64KB.
for (var i = 0; i < n; i++) {
var start = i * CHUNK_LEN,
end = (i + 1) * CHUNK_LEN;
console.log(start + ' - ' + (end - 1));
dataChannel.send(img.data.subarray(start, end));
}
// Send the reminder, if applicable.
if (len % CHUNK_LEN) {
console.log('last ' + len % CHUNK_LEN + ' byte(s)');
dataChannel.send(img.data.subarray(n * CHUNK_LEN));
}
}
ฝั่งที่รับจะแปลงไบต์ของข้อความช่องข้อมูลให้เป็นรูปภาพและแสดงรูปภาพให้กับผู้ใช้ดังนี้
function receiveDataChromeFactory() {
var buf, count;
return function onmessage(event) {
if (typeof event.data === 'string') {
buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
count = 0;
console.log('Expecting a total of ' + buf.byteLength + ' bytes');
return;
}
var data = new Uint8ClampedArray(event.data);
buf.set(data, count);
count += data.byteLength;
console.log('count: ' + count);
if (count === buf.byteLength) {
// we're done: all data chunks have been received
console.log('Done. Rendering photo.');
renderPhoto(buf);
}
};
}
function renderPhoto(data) {
var canvas = document.createElement('canvas');
canvas.width = photoContextW;
canvas.height = photoContextH;
canvas.classList.add('incomingPhoto');
// The trail is the element that holds the incoming images.
trail.insertBefore(canvas, trail.firstChild);
var context = canvas.getContext('2d');
var img = context.createImageData(photoContextW, photoContextH);
img.data.set(data);
context.putImageData(img, 0, 0);
}
รับโค้ด
- แทนที่เนื้อหาของโฟลเดอร์
work
ด้วยเนื้อหาของstep-06
ไฟล์ index.html
ใน work
ควรมีลักษณะดังนี้****
<!DOCTYPE html>
<html>
<head>
<title>Real-time communication with WebRTC</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<h1>Real-time communication with WebRTC</h1>
<h2>
<span>Room URL: </span><span id="url">...</span>
</h2>
<div id="videoCanvas">
<video id="camera" autoplay></video>
<canvas id="photo"></canvas>
</div>
<div id="buttons">
<button id="snap">Snap</button><span> then </span><button id="send">Send</button>
<span> or </span>
<button id="snapAndSend">Snap & Send</button>
</div>
<div id="incoming">
<h2>Incoming photos</h2>
<div id="trail"></div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
- หากไม่ทําตาม Codelab นี้จากไดเรกทอรี
work
คุณอาจต้องติดตั้งทรัพยากร Dependency สําหรับโฟลเดอร์step-06
หรือโฟลเดอร์การทํางานปัจจุบัน เพียงเรียกใช้คําสั่งต่อไปนี้จากไดเรกทอรีการทํางานของคุณ
npm install
- เมื่อติดตั้งแล้ว หากเซิร์ฟเวอร์ Node.js ของคุณไม่ทํางาน ให้เรียกใช้คําสั่งต่อไปนี้โดยเรียกใช้คําสั่งต่อไปนี้จากไดเรกทอรี
work
ของคุณ
node index.js
Make sure that you're using the version of `index.js` that implements Socket.IO and
อย่าลืมรีสตาร์ทเซิร์ฟเวอร์ Node.js ของคุณหากคุณทําการเปลี่ยนแปลง
ดูข้อมูลเพิ่มเติมเกี่ยวกับโหนดและ Socket.IO ได้ในส่วน "ตั้งค่าการส่งสัญญาณ"
บริการแลกเปลี่ยนข้อความ
- หากจําเป็น ให้คลิกอนุญาตเพื่อให้แอปใช้เว็บแคมของคุณ
แอปจะสร้างรหัสห้องแบบสุ่มและเพิ่มรหัสดังกล่าวลงใน URL
- เปิด URL จากแถบที่อยู่ในแท็บหรือหน้าต่างเบราว์เซอร์ใหม่
- คลิกสแนปและส่ง แล้วดูที่รูปภาพขาเข้าในแท็บอื่นด้านล่างของหน้า
แอปจะโอนรูปภาพระหว่างแท็บ
คุณควรจะมีลักษณะดังนี้
คะแนนโบนัส
คุณสามารถเปลี่ยนโค้ดเพื่อให้สามารถแชร์ไฟล์ประเภทใดได้บ้าง