Bật tính năng giao tiếp theo thời gian thực bằng WebRTC

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn cách xây dựng một ứng dụng để tải video và chụp ảnh nhanh bằng webcam và chia sẻ ngang hàng với WebRTC. Bạn cũng có thể tìm hiểu cách sử dụng các API WebRTC cốt lõi và thiết lập máy chủ nhắn tin bằng Node.js.

Điều kiện tiên quyết

  • Kiến thức cơ bản về HTML, CSS và JavaScript

Sản phẩm bạn sẽ tạo ra

  • Lấy video từ webcam của bạn.
  • Phát trực tuyến video với RTCPeerConnection.
  • Truyền dữ liệu bằng RTCDataChannel.
  • Thiết lập dịch vụ phát tín hiệu để trao đổi tin nhắn.
  • Kết hợp tín hiệu kết nối ngang hàng và tín hiệu.
  • Chụp ảnh và sử dụng kênh dữ liệu để chia sẻ ảnh.

Bạn cần có

  • Chrome 47 trở lên
  • Máy chủ web cho Chrome hoặc máy chủ web mà bạn chọn
  • Trình chỉnh sửa văn bản do bạn chọn
  • Node.js

2. Lấy mã mẫu

Tải mã xuống

  1. Nếu bạn quen với Git, hãy chạy lệnh này để sao chép mã cho lớp học lập trình này từ GitHub:
git clone https://github.com/googlecodelabs/webrtc-web

Ngoài ra, hãy nhấp vào đường liên kết này để tải tệp zip xuống.

  1. Mở tệp zip đã tải xuống để giải nén thư mục dự án có tên là webrtc-web-master. Thư mục này chứa một thư mục cho mỗi bước của lớp học lập trình này và tất cả tài nguyên mà bạn cần.

Bạn thực hiện tất cả mã của mình trong thư mục có tên work.

Các thư mục step-nn chứa phiên bản đã hoàn thành cho mỗi bước của lớp học lập trình này. Chúng tôi luôn sẵn sàng tham khảo.

Cài đặt và xác minh máy chủ web

Mặc dù bạn có thể dùng máy chủ web của riêng mình, nhưng lớp học lập trình này được thiết kế để hoạt động tốt với Máy chủ web dành cho Chrome.

  1. Nếu bạn chưa có Máy chủ web dành cho Chrome, hãy nhấp vào liên kết này để cài đặt máy chủ từ Cửa hàng Chrome trực tuyến:

d0a4649b4920cf3.png

  1. Nhấp vào Thêm vào Chrome để cài đặt Máy chủ web dành cho Chrome và tự động mở ứng dụng Google trong thẻ mới.
  2. Nhấp vào Máy chủ web:

27fce4494f641883.png

Một hộp thoại sẽ xuất hiện, cho phép bạn định cấu hình máy chủ web cục bộ:

a300381a486b9e22.png

  1. Sau đó, hãy nhấp vào Chọn thư mục.
  2. Chọn thư mục work mà bạn đã tạo.

Trong phần (Các) URL máy chủ web, bạn sẽ thấy URL nơi bạn có thể xem công việc của mình đang được tiến hành

Chrome.

  1. Trong mục Tùy chọn (có thể yêu cầu khởi động lại), hãy chọn hộp kiểm Tự động hiển thị index.html.
  2. Bật/tắt Máy chủ web: Đã khởi động hai lần để dừng và khởi động lại máy chủ.

f23cafb3993dfac1.png

  1. Nhấp vào URL trong mục (Các) URL máy chủ web để xem công việc của bạn trên trình duyệt web.

Bạn sẽ thấy một trang trông giống như sau, tương ứng với work/index.html:

18a705cb6ccc5181.png.

Rõ ràng là ứng dụng này chưa làm gì thú vị. Đó chỉ là một bộ xương tối thiểu để đảm bảo rằng máy chủ web của bạn hoạt động đúng cách. Bạn thêm các tính năng bố cục và chức năng trong các bước tiếp theo.

3. Phát video từ webcam của bạn

Phiên bản đầy đủ của bước này nằm trong thư mục step-01.

Thêm dấu gạch ngang HTML

Hãy sao chép và dán mã này vào tệp index.html trong thư mục work để thêm một phần tử videoscript:

<!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>

Thêm một chút JavaScript

Sao chép và dán mã này vào tệp main.js trong thư mục 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);

Dùng thử

Mở tệp index.html trong trình duyệt và bạn sẽ thấy nội dung như sau, nhưng với chế độ xem từ webcam, tất nhiên là được:

9297048e43ed0f3d.png

Cách hoạt động

Sau cuộc gọi getUserMedia(), trình duyệt sẽ yêu cầu quyền truy cập vào máy ảnh của bạn nếu đây là yêu cầu đầu tiên về quyền truy cập vào máy ảnh của nguồn gốc hiện tại.

Nếu thành công, hệ thống sẽ trả về MediaStream, phần tử media có thể sử dụng thông qua thuộc tính srcObject:

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Đối số constraints cho phép bạn chỉ định nội dung nghe nhìn cần nhận. Trong ví dụ này, nội dung nghe nhìn chỉ là video vì âm thanh bị tắt theo mặc định:

const mediaStreamConstraints = {
  video: true,
};

Bạn có thể sử dụng các hạn chế đối với các yêu cầu bổ sung, chẳng hạn như độ phân giải của video:

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

Thông số kỹ thuật của MediaTrackConstraints liệt kê tất cả các loại hạn chế có thể xuất hiện, mặc dù tất cả các tùy chọn đều được một số trình duyệt hỗ trợ. Nếu độ phân giải được yêu cầu không được máy ảnh hiện được chọn hỗ trợ, thì getUserMedia() sẽ bị từ chối bằng OverconstrainedError và bạn sẽ được nhắc cấp quyền truy cập vào máy ảnh.

Nếu getUserMedia() thành công, thì luồng video từ webcam sẽ được đặt làm nguồn của thành phần video:

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

Điểm thưởng điểm

  • Đối tượng localStream được chuyển đến getUserMedia() nằm trong phạm vi toàn cầu, do đó bạn có thể kiểm tra đối tượng này từ bảng điều khiển trình duyệt. Mở bảng điều khiển, nhập stream, rồi nhấn Enter (Return trên máy Mac). Để xem bảng điều khiển trong Chrome, hãy nhấn Control+Shift+J (hoặc Command+Option+J trên máy Mac).
  • localStream.getVideoTracks() trả về gì?
  • Gọi cho localStream.getVideoTracks()[0].stop().
  • Hãy xem đối tượng các hạn chế. Điều gì xảy ra khi bạn thay đổi thành {audio: true, video: true}?
  • Phần tử video có kích thước bao nhiêu? Bạn có thể lấy kích thước tự nhiên của video từ JavaScript như thế nào, thay vì kích thước hiển thị? Sử dụng Công cụ dành cho nhà phát triển Google Chrome để kiểm tra.
  • Thêm bộ lọc CSS vào phần tử video, như trong ví dụ sau:
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • Thêm bộ lọc SVG, như thế này:
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

Mẹo

Phương pháp hay nhất

Hãy đảm bảo rằng thành phần video của bạn không bị tràn bộ chứa. Lớp học lập trình này đã thêm widthmax-width để đặt một kích thước ưu tiên và kích thước tối đa cho video. Trình duyệt của bạn sẽ tự động tính chiều cao.

video {
  max-width: 100%;
  width: 320px;
}

4. Phát trực tuyến video bằng API RTCPeerConnection

Phiên bản đầy đủ của bước này nằm trong thư mục step-2.

Thêm các thành phần và nút điều khiển video

Trong tệp index.html, hãy thay thế phần tử video duy nhất bằng hai phần tử videobutton.

<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>

Một phần tử video hiển thị luồng từ getUserMedia() và thành phần còn lại hiển thị cùng một video được phát trực tuyến qua RTCPeerconnection. (Trong một ứng dụng thực tế, một phần tử video sẽ hiển thị luồng cục bộ và một phần tử khác sẽ hiển thị luồng từ xa.)

Thêm ShimAdapter.js

Sao chép phần tử tập lệnh này và dán phía trên phần tử tập lệnh cho main.js:

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

Tệp index.html của bạn sẽ có dạng như sau:

<!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>

Cài đặt mã RTCPeerConnection

Thay thế main.js bằng phiên bản trong thư mục step-02.

Gọi điện

  1. Mở tệp index.html.
  2. Nhấp vào Bắt đầu để quay video từ webcam.
  3. Nhấp vào Gọi để thực hiện kết nối ngang hàng

Bạn sẽ thấy cùng một video từ webcam của mình trong cả hai phần tử video.

  1. Xem bảng điều khiển trình duyệt để xem nhật ký WebRTC.

Cách hoạt động

Bước này rất hữu ích.

WebRTC sử dụng API RTCPeerConnection để thiết lập kết nối để phát trực tuyến video giữa các ứng dụng WebRTC, được gọi là các ứng dụng ngang hàng.

Trong ví dụ này, hai đối tượng RTCPeerConnection nằm trên cùng một trang: pc1pc2.

Quá trình thiết lập cuộc gọi giữa các ứng dụng ngang hàng WebRTC bao gồm 3 nhiệm vụ:

  1. Tạo RTCPeerConnection cho mỗi kết thúc cuộc gọi và thêm ở cuối mỗi luồng cuộc gọi cục bộ từ getUserMedia().
  2. Nhận và chia sẻ thông tin mạng.

Các điểm cuối kết nối tiềm năng được gọi là ứng viên ICE.

  1. Nhận và chia sẻ nội dung mô tả ở địa phương và từ xa.

Siêu dữ liệu về nội dung nghe nhìn cục bộ có định dạng Giao thức mô tả phiên (SDP).

Hãy tưởng tượng Alice và Bob muốn dùng RTCPeerConnection để thiết lập một cuộc trò chuyện video.

Đầu tiên, Alice và Bob trao đổi thông tin mạng. Biểu thức tìm ứng viên đề cập đến quá trình tìm các giao diện và cổng mạng bằng cách sử dụng khung ICE.

  1. Alice tạo đối tượng RTCPeerConnection bằng một trình xử lý onicecandidate (addEventListener('icecandidate')).

Mã này tương ứng với mã sau từ main.js:

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice gọi getUserMedia() và thêm luồng được chuyển vào đó:
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.');
  1. Trình xử lý onicecandidate từ bước đầu tiên sẽ được gọi khi có ứng viên mạng.
  2. Alice gửi dữ liệu ứng viên tuần tự cho Bob.

Trong một ứng dụng thực tế, quá trình này (gọi là tín hiệu) diễn ra thông qua một dịch vụ nhắn tin. Bạn sẽ tìm hiểu cách làm việc đó ở bước sau. Dĩ nhiên, ở bước này, hai đối tượng RTCPeerConnection nằm trên cùng một trang và có thể giao tiếp trực tiếp mà không cần thông báo ra bên ngoài.

  1. Khi Bob nhận được tin nhắn ứng viên từ Alice, anh ấy gọi addIceCandidate() để thêm ứng viên vào phần mô tả ứng dụng ngang hàng từ xa:
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}.`);
  }
}

Các ứng dụng ngang hàng của WebRTC cũng cần phải khám phá và trao đổi thông tin nội dung nghe nhìn và âm thanh từ xa và cục bộ, chẳng hạn như khả năng giải quyết và codec. Quá trình phát tín hiệu để trao đổi thông tin cấu hình nội dung nghe nhìn tiếp tục với quá trình trao đổi blob trong siêu dữ liệu, còn được gọi là ưu đãi và câu trả lời, sử dụng định dạng SDP.

  1. Alice chạy phương thức RTCPeerConnection createOffer().

Lời hứa đã trả về cung cấp thông tin mô tả phiên hỗ trợ tại địa phương của RTCSessionDescription—Alice\39;

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. Nếu thành công, Alice đặt nội dung mô tả cục bộ bằng setLocalDescription() rồi gửi nội dung mô tả phiên này cho Bob thông qua kênh báo hiệu của họ.
  2. Bob đặt nội dung mô tả mà Alice gửi cho anh ấy làm nội dung mô tả từ xa bằng setRemoteDescription().
  3. Bob chạy phương thức RTCPeerConnection createAnswer() và chuyển phương thức ở dạng mô tả từ xa mà anh nhận được từ Alice để tạo một phiên cục bộ tương thích với phiên của cô.
  4. Lời hứa createAnswer() sẽ chuyển RTCSessionDescription, được Bob đặt làm nội dung mô tả cục bộ và gửi cho Alice.
  5. Khi Alice nhận được nội dung mô tả về phiên hoạt động của Bob, cô ấy đã đặt nó làm nội dung mô tả từ xa với 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);
}

Điểm thưởng điểm

  1. Chuyển đến chrome://webrtc-internals.

Trang này cung cấp số liệu thống kê WebRTC và dữ liệu gỡ lỗi. (Bạn có thể tìm thấy danh sách đầy đủ các URL Chrome tại chrome://about.)

  1. Tạo kiểu cho trang bằng CSS:
  2. Đặt các video cạnh nhau.
  3. Làm cho các nút có cùng chiều rộng với văn bản lớn hơn.
  4. Đảm bảo bố cục hoạt động trên thiết bị di động.
  5. Từ bảng điều khiển Công cụ cho nhà phát triển Chrome, hãy xem localStream, localPeerConnectionremotePeerConnection.
  6. Từ bảng điều khiển, hãy xem localPeerConnectionpc1.localDescription.

Định dạng SDP trông như thế nào?

Mẹo

Phương pháp hay nhất

Để mã của bạn phù hợp với tương lai, hãy sử dụng API dựa trên Promise mới và cho phép khả năng tương thích với những trình duyệt không hỗ trợ API này với Adapter.js.

5. Sử dụng kênh dữ liệu để trao đổi dữ liệu

Phiên bản đầy đủ của bước này nằm trong thư mục step-03.

Cập nhật HTML của bạn

Đối với bước này, bạn sử dụng kênh dữ liệu WebRTC để gửi văn bản giữa hai phần tử textarea trên cùng một trang. Điều đó không hữu ích lắm, nhưng có ví dụ về cách sử dụng WebRTC để chia sẻ dữ liệu, cũng như phát trực tuyến video.

Xóa các phần tử videobutton khỏi index.html, và thay thế chúng bằng các phần tử HTML sau:

<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>

Một textarea là để nhập văn bản, trong khi kia là hiển thị văn bản dưới dạng truyền trực tuyến giữa các ứng dụng ngang hàng.

Tệp index.html của bạn sẽ có dạng như sau:

<!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>

Cập nhật JavaScript của bạn

  1. Thay thế main.js bằng nội dung của step-03/js/main.js.
  1. Thử phát trực tuyến dữ liệu giữa các ứng dụng ngang hàng:
  2. Mở index.html.
  3. Nhấp vào Bắt đầu để thiết lập kết nối ngang hàng.
  4. Nhập một số văn bản vào textarea ở bên trái.
  5. Nhấp vào Gửi để chuyển văn bản bằng kênh dữ liệu WebRTC.

Cách hoạt động

Mã này sử dụng RTCPeerConnectionRTCDataChannel để cho phép trao đổi tin nhắn văn bản.

Phần lớn mã trong bước này giống với mã trong ví dụ RTCPeerConnection. Hàm sendData()createConnection() có hầu hết mã mới:

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);
}

Cú pháp của RTCDataChannel có chủ ý tương tự như WebSocket với phương thức send() và một sự kiện message.

Hãy chú ý đến việc sử dụng dataConstraint. Bạn có thể định cấu hình các kênh dữ liệu để sử dụng nhiều loại dữ liệu chia sẻ, chẳng hạn như ưu tiên phân phối đáng tin cậy hơn hiệu suất.

Điểm thưởng điểm

  1. Với SCSV, giao thức mà các kênh dữ liệu WebRTC sử dụng, phân phối dữ liệu theo thứ tự đáng tin cậy và có thứ tự sẽ bật theo mặc định. Khi nào RTCDataChannel có thể cần cung cấp dữ liệu đáng tin cậy và khi nào hiệu suất có thể quan trọng hơn, ngay cả khi điều đó có nghĩa là mất một số dữ liệu?
  2. Sử dụng CSS để cải thiện bố cục trang và thêm thuộc tính phần giữ chỗ vào dataChannelReceive textarea.
  3. Hãy kiểm tra trang đó trên một thiết bị di động.

Tìm hiểu thêm

6. Thiết lập dịch vụ phát tín hiệu để trao đổi tin nhắn

Bạn đã học cách trao đổi dữ liệu giữa các ứng dụng ngang hàng trên cùng một trang, nhưng làm thế nào để trao đổi dữ liệu giữa các máy? Trước tiên, bạn cần thiết lập một kênh tín hiệu để trao đổi thông báo siêu dữ liệu.

Phiên bản đầy đủ của bước này nằm trong thư mục step-04.

Giới thiệu về ứng dụng

WebRTC sử dụng API JavaScript phía máy khách, nhưng để sử dụng trong thực tế, máy chủ cũng cần có máy chủ báo hiệu (tin nhắn), cũng như máy chủ STUN và Turn. Bạn có thể tìm hiểu thêm tại đây.

Ở bước này, bạn tạo một máy chủ tín hiệu Node.js đơn giản bằng cách sử dụng mô-đun Socket.IO Node.js và thư viện JavaScript để nhắn tin.

Trong ví dụ này, máy chủ (ứng dụng Node.js) được triển khai trong index.js và ứng dụng chạy trên máy chủ (ứng dụng web) được triển khai trong index.html.

Ứng dụng Node.js trong bước này có hai nhiệm vụ.

Đầu tiên, nút này đóng vai trò chuyển tiếp tin nhắn:

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

Thứ hai, ứng dụng này quản lý các phòng trò chuyện video 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);
}

Ứng dụng WebRTC đơn giản cho phép tối đa hai ứng dụng ngang hàng chia sẻ phòng.

HTML và JavaScript

  1. Cập nhật index.html để có dạng như sau:
<!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>

Bạn sẽ không thấy bất kỳ nội dung nào trên trang ở bước này. Tất cả nhật ký đều được thực hiện đối với bảng điều khiển trình duyệt. Để xem bảng điều khiển trong Chrome, hãy nhấn Control+Shift+J (hoặc Command+Option+J trên máy Mac).

  1. Thay thế js/main.js bằng các mục sau:
'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);
});

Thiết lập tệp Socket.IO để chạy trên Node.js

Trong tệp HTML, bạn có thể thấy rằng bạn đang sử dụng tệp Socket.IO:

<script src="/socket.io/socket.io.js"></script>
  1. Ở cấp cao nhất trong thư mục work của bạn, hãy tạo một tệp có tên là package.json với các nội dung sau:
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

Đây là tệp kê khai ứng dụng cho biết Trình quản lý gói nút (npm) biết dự án nào

phần cài đặt.

  1. Để cài đặt các phần phụ thuộc, chẳng hạn như /socket.io/socket.io.js, hãy chạy các mục sau từ thiết bị đầu cuối dòng lệnh trong thư mục work của bạn:
npm install

Bạn sẽ thấy nhật ký cài đặt kết thúc bằng chuỗi như sau:

3ab06b7SHA7664b9.png

Như bạn có thể thấy, npm đã cài đặt các phần phụ thuộc được xác định trong package.json.

  1. Tạo tệp mới index.js ở cấp cao nhất của thư mục work (không phải thư mục js) và thêm mã sau:
'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);
        }
      });
    }
  });

});
  1. Từ thiết bị đầu cuối dòng lệnh, hãy chạy lệnh sau trong thư mục work:
node index.js
  1. Từ trình duyệt của bạn, hãy truy cập vào http://localhost:8080.

Mỗi lần chuyển đến URL này, bạn sẽ được nhắc nhập tên phòng.

Để tham gia cùng một phòng, hãy nhập cùng một tên phòng mỗi lần, chẳng hạn như foo.

  1. Mở một thẻ mới chuyển đến http://localhost:8080 lần nữa và nhập lại tên phòng đó.
  2. Mở một thẻ mới khác, chuyển đến http://localhost:8080 lần nữa rồi nhập lại tên phòng đó.
  3. Hãy kiểm tra bảng điều khiển trong mỗi tab.

Bạn sẽ thấy nhật ký từ JavaScript.

Điểm thưởng điểm

  • Bạn có thể sử dụng những cơ chế nhắn tin nào khác? Bạn có thể gặp phải những vấn đề nào khi sử dụng WebSocket thuần túy?
  • Những vấn đề có thể liên quan đến việc mở rộng quy mô ứng dụng này? Bạn có thể phát triển một phương pháp để thử nghiệm hàng nghìn hoặc hàng triệu yêu cầu đồng thời trong phòng không?
  • Ứng dụng này dùng lời nhắc JavaScript để lấy tên phòng. Tìm hiểu cách lấy tên phòng từ URL. Ví dụ: http://localhost:8080/foo sẽ đặt tên phòng là foo.

Tìm hiểu thêm

7. Kết hợp tín hiệu kết nối ngang hàng và tín hiệu

Phiên bản đầy đủ của bước này nằm trong thư mục step-05.

Thay thế HTML và JavaScript

  1. Thay thế nội dung của index.html bằng các mục sau:
<!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>
  1. Thay thế js/main.js bằng nội dung của step-05/js/main.js.

Chạy máy chủ Node.js

Nếu không tuân theo lớp học lập trình này từ thư mục work, thì bạn có thể cần cài đặt các phần phụ thuộc cho thư mục step-05 hoặc thư mục đang hoạt động của mình.

  1. Chạy lệnh sau từ thư mục đang làm việc:
npm install
  1. Sau khi cài đặt, nếu máy chủ Node.js của bạn không chạy, hãy khởi động máy chủ bằng cách chạy lệnh sau trong thư mục work:
node index.js

Đảm bảo rằng bạn đang sử dụng phiên bản index.js từ bước trước đó sẽ triển khai Socket.IO. Để biết thêm thông tin về Node và Socket IO, hãy xem lại phần Thiết lập dịch vụ phát tín hiệu để trao đổi thông báo.

  1. Từ trình duyệt của bạn, hãy chuyển đến http://localhost:8080.
  2. Mở một thẻ mới rồi chuyển đến http://localhost:8080.

Một phần tử video hiển thị luồng cục bộ từ getUserMedia() và phần tử còn lại hiển thị video từ xa được phát trực tuyến qua RTCPeerconnection.

  1. Xem thông tin ghi nhật ký trong bảng điều khiển trình duyệt.

Điểm b****điểm

  • Ứng dụng này chỉ hỗ trợ trò chuyện video riêng tư. Làm cách nào để thay đổi thiết kế để cho phép nhiều người cùng dùng chung một phòng trò chuyện video?
  • Ví dụ này có tên phòng là foo được mã hóa cứng. Đâu là cách tốt nhất để bật các tên phòng khác?
  • Người dùng sẽ chia sẻ tên phòng như thế nào? Cố gắng xây dựng một giải pháp thay thế cho việc chia sẻ tên phòng.
  • Làm cách nào để bạn có thể thay đổi ứng dụng?

Mẹo

  • Tìm số liệu thống kê của WebRTC và gỡ lỗi dữ liệu tại chrome://webrtc-internals.
  • Hãy dùng Trình khắc phục sự cố WebRTC để kiểm tra môi trường địa phương của bạn, đồng thời kiểm tra máy ảnh và micrô.
  • Nếu bạn gặp sự cố bất thường khi lưu vào bộ nhớ đệm, hãy thử những cách sau:
  1. Nhấn Control và nhấp vào Tải lại trang này.
  2. Khởi động lại trình duyệt.
  3. Chạy npm cache clean từ dòng lệnh.

8. Chụp ảnh và chia sẻ ảnh thông qua kênh dữ liệu

Phiên bản đầy đủ của bước này nằm trong thư mục step-06.

Cách hoạt động

Trước đây, bạn đã học cách trao đổi tin nhắn văn bản bằng RTCDataChannel. Bước này giúp bạn có thể chia sẻ toàn bộ tệp. Trong ví dụ này, ảnh được chụp bằng getUserMedia().

Các phần cốt lõi của bước này như sau:

  1. Thiết lập kênh dữ liệu.

Bạn không thêm bất kỳ luồng phương tiện nào vào kết nối ngang hàng trong bước này.

  1. Ghi lại luồng video webcam của bạn với 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);
  });
}
  1. Nhấp vào Quay để lấy ảnh chụp nhanh (khung video) từ luồng video và hiển thị ảnh đó trong phần tử 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);
}
  1. Nhấp vào Gửi để chuyển đổi hình ảnh thành byte và gửi hình ảnh qua một kênh dữ liệu:
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));
  }
}

Bên nhận chuyển đổi byte của dữ liệu kênh kênh thành một hình ảnh và hiển thị hình ảnh đó cho người dùng:

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);
}

Lấy mã

  1. Thay thế nội dung của thư mục work bằng nội dung của step-06.

Tệp index.html của bạn trong work sẽ có dạng như thế này ngay**:**

<!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 &amp; 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>
  1. Nếu không tuân theo lớp học lập trình này từ thư mục work, thì bạn có thể cần cài đặt các phần phụ thuộc cho thư mục step-06 hoặc thư mục đang hoạt động của mình. Chỉ cần chạy lệnh sau từ thư mục đang làm việc:
npm install
  1. Sau khi cài đặt, nếu máy chủ Node.js của bạn không chạy, hãy khởi động máy chủ bằng cách chạy lệnh sau trong thư mục work của bạn:
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

hãy nhớ khởi động lại máy chủ Node.js của bạn nếu bạn thực hiện các thay đổi.

Để biết thêm thông tin về Node và Socket.IO, hãy xem lại phần Thiết lập tín hiệu

để trao đổi tin nhắn.

  1. Nếu cần, hãy nhấp vào Cho phép để cho phép ứng dụng sử dụng webcam của bạn.

Ứng dụng sẽ tạo một mã phòng ngẫu nhiên và thêm mã đó vào URL.

  1. Mở URL từ thanh địa chỉ trong cửa sổ hoặc thẻ trình duyệt mới.
  2. Nhấp vào Chụp nhanh và gửi, sau đó xem Ảnh đến trong thẻ khác ở cuối trang.

Ứng dụng sẽ chuyển ảnh giữa các thẻ.

Bạn sẽ thấy như sau:

911b40f36ba6ba8.png

Điểm thưởng điểm

Làm cách nào để thay đổi mã để có thể chia sẻ bất kỳ loại tệp nào?

Tìm hiểu thêm

9. Xin chúc mừng

Bạn đã xây dựng một ứng dụng để phát video trực tuyến và trao đổi dữ liệu theo thời gian thực!

Tìm hiểu thêm