跳至主要内容

[WebService] Tokbox, OpenTok 開發筆記

筆記

  • 一個使用者在連上 session 後,會得到一個 connectionId,所以一個使用者只會有一個獨立的 connectionId
  • 但一個使用者可以建立多個 publishersubscriber,每 publish 一個 publisher 就會產生一個 streamId,因此一個使用者可以有多個 streamId

Connection Id(連上 session 時)

  • 連結 Session:有任何使用者(包含自己) Connect Session 時(session.connect())會觸發每個人 Session 物件的 connectionCreated 事件。如果你是第一次連入此 Session,則也會一併觸發所有先前已經在該 Session 內的使用者。在此 session 物件中(或 sessionConnected 事件)可以取得 connectionId (i.e., session.connection.connectionId)。
  • 斷開 Session:有任何除了自己以外的使用者斷開與 session 的連結時(session.disconnect()),會觸發 Session 物件的 connectionDestroyed 事件。使用者自己斷開連結時,則不會觸發 connectionDestroyed 事件,而是會觸發 sessionDisconnected 事件,因此當接收到 sessionDisconnected 事件時,一定是該使用者自己與 session 斷開連接
  • 取得 ConnectionEvent 物件:在 connectionCreatedconnectionDestroyed 事件中,都可以得到 ConnectionEvent 物件,可以從此物件中得到連接/斷連者的 connectionId (i.e., event.connection.connectionId)。
  • 取得 StreamEvent 物件:有任何除了自己以外的使用者在 session 發佈串流時,會觸發 streamCreated 事件,在這事件中會得 StreamEvent 物件,裡面可以取得用來訂閱(subscribe)的 stream 物件(即,event.stream),同時也包含 Connection 物件 在內,因此可以由 Connection 物件中的 connectionId (i.e., event.stream.connection.connectionId) 來判斷是屬於哪個使用者的串流。

Stream Id(發佈串流時 - publisher)

  • 一個使用者在一個 Session 中只會有一個 connectionId,但他可以建立多個 Publisher,每個 publisher 都可以有獨自的串流,因此可以有多個 streamId
  • 取得自己的 streamId 可以透過 publisher.streamId
  • 取得他人的 streamId 可以透過監聽 streamCreated 事件中的 StreamEvent 取得(即,event.stream.streamId)。
session.on('connectionCreated', handleConnectionCreated); // 自己和他人的都同時會觸發,可以取得 connectionId
session.on('connectionDestroyed', handleConnectionDestroyed); // 只有他人的會觸發,可以取得 connectionId
session.on('streamCreated', handleStreamCreated); // 只有他人的會觸發,可以取得 stream 和 connectionId
session.on('streamDestroyed', handleStreamDestroyed); // 只有他人的會觸發,可以取得 stream 和 connectionId

/* DEPRECATED */
session.on('sessionDisconnected', handleSessionDisconnected); // 只有自己的會觸發,可以取得 connectionId

Moderator

Moderation @ Tokbox

只有 Moderator 可以讓某個 clients 中斷連線(session.forceDisconnect(connection)),和中斷發佈(session.forceUnpublish(stream))。

Set up a Basic Web Client

Set up a Basic Web Client @ Tokbox Developer

  1. 建立專案並載入 @opentok/client
  2. 設定驗證資訊,包含 apiKeysessionIdtoken
  3. 初始化 session 並與 session 建立連線;初始化 publisher 並發佈到 session 上
    • session = OT.initSession(apiKey, sessionId):初始化 session
    • session.connect(token, (error) => {...}):與 session 建立連線。在傳送或接收影音串連時前需要先與 session 建立連線。
  4. 註冊和 session 相關的事件
    • session.on('streamCreated', (event) => {...})
  5. 初始化 publisher,並在需要時將其發佈
    • publisher = OT.initPublisher(<element>, options, errorHandler):初始化 publisher
    • session.publish(publisher):將 publisher 的影音串流發佈到 session
  6. 初始化 subscriber,並訂閱他人的影音串流
    • 當新的影音串流在 session 中被建立時,Session 物件會發送 streamCreated 的事件,client 偵測到此事件時,可以選擇訂閱此串流(stream)
    • session.on('streamCreated', (event) => {session.subscribe(event.stream), <element>, options, errorHandler})

共享螢幕 Screen Sharing

keywords: 共享螢幕, screen sharing, 分享螢幕, 畫面分享, 擷取螢幕

Screen Sharing @ Developer Guides

注意項目

  • 在 Chrome 72+, Firefox 52+ 和 Opera 59+ 之後,不再需要安裝瀏覽器的套件才能做到 screen sharing 的功能。
  • 在 macOS Catalina 後的作業系統,若要使用 screen-sharing 的功能,需要在「系統偏好設定 > 安全與隱私 > 隱私 > 螢幕錄影」中提供瀏覽器存取權限,否則 Publisher 會發送 accessDenied 的事件。
  • 目前在 Safari 和 Edge 不支援發佈螢幕錄影 @ 2020.01.02
  • 網頁需要透過 HTTPs 傳送資料才能使用

檢查瀏覽器是否支援分享螢幕

透過 OT.checkScreenSharingCapability(<callback(response)>) 可以檢查該瀏覽器是否支援分享螢幕畫面:

OT.checkScreenSharingCapability((response) => {
const {
supported, // Boolean,該瀏覽器是否支援
supportedSources, // Object,顯示 application, screen, window 為 true / false
extensionRequired, // String,"chrome" 或 "Opera",針對較舊的瀏覽器須額外安裝套件
extensionRegistered, // Boolean,針對較舊的瀏覽器須額外註冊套件
extensionInstalled, // Boolean,針對較舊的瀏覽器須額外安裝套件
} = response;

if (!supported || extensionRegistered === false) {
// This browser does not support screen sharing
console.log('This browser does not support screen sharing');
} else if (extensionInstalled === false) {
// Prompt to install the extension
console.log('Prompt to install the extension');
} else {
// Screen sharing is available.
console.log('Screen sharing is available.');
}
});

將共享螢幕串流發佈

要讓 client 共享螢幕,只需要設定 OT.initPublisher(<targetElement>, <properties>, <completionHandler>)propertiesvideoSource 即可,可接受的參數為 "screen", "application", "window"

  • 在 Firefox 中,所有這三個設定都是支援的
  • 在 Chrome 中,參數帶入 screen 後使用者可以選擇想要分享的來源
const publisher = OT.initPublisher(
'screen-preview',
{
videoSource: 'screen',
},
(error) => {
if (error) {
// Look at error.message to see what went wrong.
} else {
session.publish(publisher, (error) => {
if (error) {
// Look error.message to see what went wrong.
}
});
}
},
);

initPublisher 中 property 在分享螢幕時預設的設定如下:

const publisher = OT.initPublisher(
'screen-preview',
{
videoSource: 'screen',
// 在分享螢幕時,預設的屬性值
maxResolution: { width: 1920, height: 1920 },
mirror: false,
fitMode: 'contain',
publishAudio: false,
},
error => {...}
);

其中在共享螢幕時,不要設定 resolution 這個屬性

避免共享畫面中看到自己的共享畫面

此外在共享螢幕畫面時,如果使用者又能看到自己的螢幕畫面,會出現螢幕畫面裡又有螢幕畫面的情況,這種情形稱作 recursive "hall of mirrors" effect。要避免這樣的情況,只需額外建立一個 div 即可:

var screenPublisherElement = document.createElement('div');
OT.initPublisher(screenPublisherElement, { videoSource: 'screen' });

透過 stream.videoType 了解串流的來源

可以透過 stream 物件中的 videoType 屬性來得知該串流的 videoSource 為何,videoType 可以是 camerascreen

// 透過 event.stream.videoType 可以得知該串流的 videoSource
session.on('streamCreated', function (event) {
var subOptions = { insertMode: 'append' };
if (event.stream.videoType === 'screen') {
// <div id="screens"></div>
session.subscribe(event.stream, 'screens', subOptions);
} else {
// <div id="people"></div>
session.subscribe(event.stream, 'people', subOptions);
}
});

偵測使用者畫面大小改變

當 client 共享的螢幕尺寸有變更時,publisher 和 subscriber 都會收到 videoDimensionsChanged 事件,這個事件(event)中會帶有 newValue, oldValuetarget 這些屬性,你可以用這些屬性來調整 publisher 在 DOM 中的大小:

var publisher = OT.initPublisher('some-element', { videoSource: 'screen' });

publisher.on('videoDimensionsChanged', function (event) {
const {
newValue,
oldValue,
target, // publisher 或 subscriber
} = event;
publisher.element.style.width = newValue.width + 'px';
publisher.element.style.height = newValue.height + 'px';
});

當使用者停止共享螢幕

當使用者停止共享螢幕時,Publisher 物件會發出 mediaStopped 事件。

若此 publisher 同時有發佈串流到 session 上,則會同時發送 streamDestroyed 事件,並且在該事件(event)中的 reason 屬性的值會是 mediaStopped;同時,所有有連結到該 session 的 clients,他們的 Session 物件都會發送 streamDestroyed 事件,並且事件(event)中的 reason 屬性值會是 mediaStopped

// 當使用者點擊停止共享畫面時
publisher.on('mediaStopped', (event) => {...});

// 當使用者同時將串流發佈到 session 上時
publisher.on('streamDestroyed', (event) => {
const { reason } = event;
if (reason === 'mediaStopped') {
// 若使用者點擊停止共享...
} else if (reason === 'forceUnpublished') {
// 若 moderator 強制使用者停止共享
}
});

不論是 mediaStoppedstreamDestroyed 事件,他們預設的行為都是刪除 publisher(或 subscriber),如果要避免被刪除的話,可以呼叫 preventDefault() 方法。

訂閱共享螢幕的串流

訂閱共享螢幕串流的方式與訂閱其他影音串流的方式相同,stream 物件中的 videoType 可以幫助判斷串流的來源是屬於螢幕(screen)或攝像頭(camera)。

session.on('streamCreated', function (event) {
var subOptions = {
appendMode: 'append',
};

var parentElementId =
event.stream.videoType === 'screen' ? 'sub-screen-sharing-container' : 'sub-camera-container';
subscriber = session.subscribe(event.stream, parentElementId, subOptions);
});

當 client 分享畫面的大小有所改變時,subscriber 一樣可以透過 videoDimensionsChanged 的事件來改變 HTML DOM 元素的大小:

subscriber.on('videoDimensionsChanged', function (event) {
const { newValue, oldValue, target } = event;
subscriber.element.style.width = newValue.width + 'px';
subscriber.element.style.height = newValue.height + 'px';
// You may want to adjust other UI.
});

Tokbox Quick Start (Hello World Example)

html

<!-- STEP 0: load tokbox library -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>React App</title>
<!-- OpenTok.js library -->
<script src="https://static.opentok.com/v2/js/opentok.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

JavaScript

/* global OT */
import React, { useEffect } from 'react';
import './App.css';

// STEP 0: import opentok library
import OT from '@opentok/client';

// STEP 1: Authentication credentials
const apiKey = '45828062';
const sessionId = '2_MX40NTgyODA2Mn5-MTU3NzY3NDc3Njc1NH4wajIwUVNFUWpvemxGMk5QQnhzQW1aL1J-UH4';
const token =
'T1==cGFydG5lcl9pZD00NTgyODA2MiZzaWc9ZGJmZGMzMzA5ODM5Nzc3MmY3MWU4Y2ZhYjlhMTQ4Y2Y0ZGU2NzRjMDpzZXNzaW9uX2lkPTJfTVg0ME5UZ3lPREEyTW41LU1UVTNOelkzTkRjM05qYzFOSDR3YWpJd1VWTkZVV3B2ZW14R01rNVFRbmh6UVcxYUwxSi1VSDQmY3JlYXRlX3RpbWU9MTU3NzY3NDc4NCZub25jZT0wLjk3NTk5NTYzMTY3OTE2NTEmcm9sZT1wdWJsaXNoZXImZXhwaXJlX3RpbWU9MTU3Nzc2MTE4NA==';

function App() {
useEffect(() => {
// STEP 2: connect to Session and create a Publisher,這時候就會請求前鏡頭權限
var session = OT.initSession(apiKey, sessionId);
var publisher = OT.initPublisher();
session.connect(token, function (err) {
// STEP 3: publish publisher,這時候會把影音串流發佈到 session
session.publish(publisher);
});

// STEP 4: create subscriber,訂閱其他人在同一 session 上的影音串流
session.on('streamCreated', function (event) {
session.subscribe(event.stream);
});
});

return (
<div className="App">
<header className="App-header"></header>
</div>
);
}

export default App;