2019年7月3日

[React] React Context API 以及 useContext Hook 的使用

圖片來源:algolia blog

基本概念與使用

透過 React Context API 可以將資料傳到直接傳送到需要的元件,而不需要手動一直透過 props 傳入:
  • Context 是設計來在 React 元件中共享資料,類似在 React 元件的全域(global),這些資料類似「使用者的登入狀態」、「樣式(theme)」、「檢視語言(preferred language)」、「資料快取(cache)」。
  • Context 主要用在當多個不同嵌套層級的元件要用到相同的資料時才會只用,除此之外,應該盡量避免使用,因為它會使得元件更難被復用(reuse)
如果你只是想要避免一直傳遞 props 到每一層, component composition 通常是一個更簡單的方式。

Context 的使用方式

讀取資料Context 的方式是在根元件的地方(例如,App.js)透過 <Context.Provider> 把需要傳遞的資料帶入 Context 中,而這些資料通常會放在根目錄的 state 中;接著,在需要使用到此資料的地方,在透過 contextType 取得 context 內的資料。
修改資料:若有需要修改 Context 中的內容,則是去修改根元件的 state 後重新帶入 <Context.Provider> 中,如此子層的元件就可以再次透過 contextType 取得新的資料,因此把修改 state 的函式放在 state 中是很常見的一種做法
import { MyContext } from './src/contexts';    // 透過 React.createContext 建立

class Foo extends React.Component {
  constructor() {
    this.state = {
      size: '2x',
      changeSize: (size) => this.setState({ size })
    }
  }

  render() {
    return (
      <MyContext.Provider value={this.state}>
        <App/>
      </MyContext.Provider>
    )
  }
}
🔖 所以實際上真正操作資料的地方是在根元件的,而 Context 更像是修改和傳遞資料的一個媒介而已

STEP 0:建立 SwitchThemeButton 元件

// ./src/ThemedButton
import React from 'react';

class ThemedButton extends React.Component {
  render() {
    return (
      <button>
        Change Theme
      </button>
    );
  }
}

export default ThemedButton;

STEP 1:建立 Context

透過 Context API 可以不用透過 props 一直將資料傳到各元件內,透過 React.createContext API 建立一個 context,裡面的內容為預設值
// ./src/AppContext.js

// STEP 1: createContext(<預設值>)
export const AppContext = React.createContext({
  theme: 'light',
  toggleTheme: () => {}
});

STEP 2:使用 Context.Provider

透過 Context.Provider 中的 value 屬性,可以把想要的值傳入內部的每個元件,:
// ./src/App.js
// ...
import { AppContext } from './AppContext';

class App extends React.Component {
  constructor(props) {
    super(props);

    // 定義修改 state 的方法
    this.toggleTheme = () => {
      this.setState(prevState => ({
        theme: prevState.theme === 'dark' ? 'light' : 'dark',
      }))
    }

    // 在 state 中放入修改 state 的方法,並傳入 <Context.Provider> 中
    this.state = {
      theme: 'dark',
      toggleTheme
    }
  }

  render() {
    // STEP 2: Use Context.Provider
    // 將 state 的資料放入 Provider 中
    return (
      <AppContext.Provider
        value={this.state}
      >
        <Toolbar />
      </AppContext.Provider>
    );
  }
⚠️ 因為 value 內如果放物件的話,每次都算是全新的物件,因此為了避免經常重新渲染,可能的話將 Context.Provider 屬性 value 內的值放在 state 中,使用 value={this.state} 這種做法。

STEP 3:定義 contextType 以取得 context 內容

若想要在元件的個生命週期中使用 this.context 取得 Context 的值,需要先將該元件定義 contextType,React 會找在與該元件最接近的 Context Provider,並且將可以在 render 時取用到它的值。定義 contextType 的方法有兩種:
// ./src/ThemedButton.js
// ...
import { AppContext } from './src/AppContext';

class ThemedButton extends React.Component {
  // STEP 3: 方法一,透過 static 定義 contextType
  static contextType = AppContext; // 才可以使用 this.context

  render() {
    // 在 render 中將可以使用 this.context
    const { theme, toggleTheme } = this.context;
    return (
      <button theme={theme} onClick={toggleTheme}>
        {theme}
      </button>
    );
  }
}

// STEP 3: 方法二 定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context

在 Functional Component 中使用 Context 的值

如果 functional component 需要使用 Context 的值,可以透過 Context.Consumer 元件,其內部需要帶入 function,該 function 的參數即可取得 Context 的值
/*
<AppContext.Consumer>
  {(value) => { ... }}
</AppContext.Consumer>
*/
function Toolbar(props) {
  return (
    <div>
      <AppContext.Consumer>
        {({ theme, size }) => (
          <p>
            theme: {theme} <br />
            size: {size}
          </p>
        )}
      </AppContext.Consumer>
      <ThemedButton />
    </div>
  );
}

示範影片與程式碼

React Hooks - useContext

useContext 中可以在 function 中使用 useContext(MyContext),這等同於在 class 中使用 static contextType = MyContext,或者是 <MyContext.Consumer> 的用法。
import { MyContext } from './src/contexts';

const value = useContext(MyContext);
若把上面範例中的 ThemedButton 元件改成 useContext 的寫法,則會變成:
import React, { useContext } from 'react';
import { ThemeContext } from './../contexts/ThemeContext';

const SwitchThemeButton = () => {
  const context = useContext(ThemeContext);
  const { theme, toggleTheme } = context;
  return (
    <button
      style={{
        color: theme.foreground,
        backgroundColor: theme.background,
      }}
      onClick={toggleTheme}
    >
      Change Theme
    </button>
  );
};

export default SwitchThemeButton;

其他

沒有使用 context API 的情況

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />
  }
}

const Toolbar = (props) => {
  // 這裡 Toolbar 元件必須有多一個額外的 "theme" prop,並帶入到 ThemedButton 中
  // 然而,如果每一個在 App 內的按鈕都需要知道 theme 的話,這樣傳遞資料會變得非常麻煩
  return (
    <div>
      <ThemedButton theme={props.theme}/>
    </div>
  )
}

class ThemedButton extends React.Component {
  render() {
    return <button>{this.props.theme}</button>
  }
}

使用 context API 後

要使用 Context Value 的地方要先將該 class 定義 contextType,如此才可以在各個生命週期使用 this.context取得 Context 的內容。定義的方法包括:
const AppContext = React.createContext({
  theme: 'light',
  size: '2x',
});

// 定義 contextType
class ComponentUseContext extends React.Component {
  static contextType = AppContext; // 才可以使用 this.context
}

// 如果在 class 是沒寫 static contextType = AppContext;
// 則需要額外在這定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context
完整程式內容:
import React from 'react';
import logo from './logo.svg';
import './App.css';

// STEP 1: createContext(<defaultValue>)
// 透過 Context API 可以不用透過 props 一直將資料傳到各元件內
// 建立一個 context,並以 light 為預設值
const AppContext = React.createContext({
  theme: 'light',
  size: '2x',
});

class App extends React.Component {
  render() {
    // STEP 2: Use Context
    // 使用 Provider 可以將當前的 theme 傳入內部的每個子元件
    // 每個元件都能讀取到它,不論它有多深
    return (
      <AppContext.Provider
        value={{
          theme: 'dark',
          size: '1x',
        }}
      >
        <Toolbar />
      </AppContext.Provider>
    );
  }
}

// 中間層的元件不需要做任何事
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // STEP 3: 取得 AppContext 的值
  // 指定 contextType 來讀取當前的 AppContext 的值
  // React 會找在 AppContext 的 Provider 最接近的那個,並使用它的值
  // 這裡會是 "dark"
  static contextType = AppContext; // 才可以使用 this.context

  render() {
    const { theme, size } = this.context;
    return (
      <button theme={theme} size={size}>
        {theme}, {size}
      </button>
    );
  }
}

// 如果在 class 是沒寫 static contextType = AppContext;
// 則需要額外在這定義 contextType
ThemedButton.contextType = AppContext; // 才可以使用 this.context

export default App;

參考

[筆記] 使用 react-intl 在 React 實作多語系功能 i18n, internationalization

Photo by Aaron Burden on Unsplash

keywords: i18n, Internationalization, intl, format
這篇文章主要說明如何透過 React Intl 來實作網頁上的 i18n,先備知識必須要已經基本會使用 React。
⚠️ 文章使用的是 react-intl @ 2.9,部分功能在 3.0 後不再支援(文章最下方會說明)。

開始使用 react-intl

我們直接用 create-react-app 的頁面來做練習:
create-react-app

建立 React 專案

直接透過 create-react-app 建立 React 專案:
$ create-react-app react-intl-sandbox
$ cd react-intl-sandbox

安裝 react-intl

$ npm install react-intl
接著就可以透過 npm run start 啟動專案。

使用 IntlProvider

IntlProvider @ react-intl components
<IntlProvider> 包在最外層,讓所有內部 component 都可以存取到它提供的屬性與方法。這裡方便操作,把它多包成一個名為 Root 的 functional component:
imgur

取得使用者語系並帶入 IntlProvider

接著:
  • 透過 navigator.language 取得使用者在瀏覽器上所使用的語系,並且將它代入 IntlProvider 的 local 屬性中
  • 為了讓語系變動後可以重新渲染 React 組件,因此也將取得的語系帶入 key 屬性中(參考:dynamic language selection
  • 若有需要可以定義 defaultLocale 屬性
react-intl
這時候打開瀏覽器會看到錯誤訊息:
react-intl
表示它載不到預設的地區資料。

載入需要的語系資料

在 react-intl 中已經定義好許多地區的語系資料,這些資料會定義某個語系是否有其父子語系的關係,例如 zh-TW 是在 zh-Hant 底下,而 zh-Hant 又在 zh 底下這種關係。除此之外,裡面還定義了關於「時間」、「複數型態」時要怎麼呈現的資訊。
關於各地區的語系,最標準的可以參考 BCP 47 所定義的 locale code 規格文件。
react-intl 所定義好的語系檔都在安裝好的套件內,路徑為 node_modules/react-intl/locale-data/,裡面列出了各語系的定義:
react-intl
例如說這裡我們可以載入英文、中文和日文的語系資料,並且透過 addLocaleData 將語系檔載入:
react-intl
⚠️ 在 react-intl 3 之後不在支援 addLocaleData 的方法,而是直接使用原生的 Intl API,關於使用方式可以參考 Migrate to using native Intl APIs

定義多語系字典檔

做好上述設定後,就可以來定義多語系用的字典檔。假設我們要做 i18n 的內容是範例頁面中的「Learn React」這個部分:
create-react-app
這時候就可以先定義這個字典檔。首先在 ./src 資料夾內再新增一個 i18n 的資料夾,裡面分別放入 en.js, zh.js, ja.js
react-intl
接著分別定義每支字典檔的內容,這裡的 app.learn 可以視為 id,之後的使用 react-intl 提供的方法或組件時,他會根據你提供的這個 id 來帶入不同的內容,而 { } 刮起來的 name 表示它是動態的變數:
// ./src/i18n/en.js
const en = { 'app.learn': 'Learn {name}' };
export default en;
// ./src/i18n/zh.js
const zh_TW = { 'app.learn': '學習 {name}' };
export default zh_TW;
// ./src/i18n/ja.js
const ja_JP = { 'app.learn': '学び {name}' };
export default ja_JP;

撰寫切換語言的功能

因為一開始使用的是 functional component,想要添加狀態(state)的話,可以匯入 useState 近來用:
// ./src/index.js
import React, { useState } from 'react';

// ...
const Root = () => {
  // 使用 useState 定義 locale 這個 state
  const [locale, setLocale] = useState(navigator.language);

  return (
    <IntlProvider
      locale={locale}
      key={locale}
      defaultLocale="en"
    >
      <!-- 將 setLocale 的方法傳到 App 內 -->
      <App setLocale={setLocale} />
    </IntlProvider>
  );
};
在 App.js 的地方,撰寫三個按鈕可以來切換 locale 的狀態:
// ./src/App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

// 從 props 取得從 index 傳入的 setLocale 方法
function App({ setLocale }) {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <div>
          <!-- 建立三顆按鈕可以切換 local 的狀態 -->
          <button onClick={() => setLocale('en')}>英文</button>
          <button onClick={() => setLocale('zh-Hant')}>中文</button>
          <button onClick={() => setLocale('ja')}>日文</button>
        </div>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
這時候的畫面會像這樣:
react-intl

文字訊息多語系

這裡說明的是最一般用來轉換不同語系字串的作法,react-intl 提供了:

根據不同的語言傳入不同的語系檔

可以切換 locale 的狀態後,最後就可以根據使用者選擇的語言傳入不同的語系檔。
先將剛剛撰寫好的語系檔載入:
// ./src/index.js
// ...
import en from './i18n/en.js';
import zh from './i18n/zh.js';
import ja from './i18n/ja.js';
接著定義 messages 這個變數,透過 if 判斷使用者切換到的語言為何,最後將 messages 傳入 <IntlProvider>
// ./src/index.js
import React, { useState } from 'react';
// ...

import en from './i18n/en.js';
import zh from './i18n/zh.js';
import ja from './i18n/ja.js';

addLocaleData([...zhLocaleData, ...enLocaleData, ...jaLocaleData]);

const Root = () => {
  const [locale, setLocale] = useState(navigator.language);
  let messages;

  // 根據使用者選擇的語系 locale 切換使用不同的 messages
  if (locale.includes('zh')) {
    messages = zh;
  } else if (locale.includes('ja')) {
    messages = ja;
  } else {
    messages = en;
  }

  return (
    <!-- 將 messages 傳入 IntlProvider -->
    <IntlProvider
      locale={locale}
      key={locale}
      defaultLocale="en"
      messages={messages}
    >
      <App setLocale={setLocale} />
    </IntlProvider>
  );
};

ReactDOM.render(<Root />, document.getElementById('root'));

在組件中使用語系檔

index.js 中,使用者在切換語言後,我們會提供不同的語系檔到 <IntlProvider>messages 屬性內。現在,在 App.js 中就可以使用 react-intl 提供的 <FormattedMessage> 方法來顯示。
<FormattedMessage /> 中,id 會對應到先前寫在 ./src/i18n/ 資料夾內的語系檔中定義的屬性 app.learn,而 values 是因為在語系檔中有定義
// ./src/App.js
// ...
// 匯入 FormattedMessage 組件
import { FormattedMessage } from 'react-intl';

function App({ setLocale }) {
  return (
    <div className="App">
      <header className="App-header">
        <!-- ... -->
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          <!-- 使用 FormattedMessage -->
          <FormattedMessage id="app.learn" values={{ name: 'React' }} />
        </a>
      </header>
    </div>
  );
}
這時候就完成了簡單的 i18n 語系替換:
react-intl

時間日期多語系

在 react-intl 中也提供了時間、日期的多語系功能,包含:
import {
  FormattedDate,
  FormattedTime,
} from 'react-intl';

function App({ setLocale }) {
  return (
    <div className="App">
      <header className="App-header">
        <!-- ... -->
        <br />
        <!-- 使用多語系的日期格式 -->
        <FormattedDate
          value={new Date()}
          year="numeric"
          month="long"
          day="numeric"
          weekday="long"
        />
        <!-- 使用多語系的時間格式 -->
        <FormattedTime value={new Date()} />
      </header>
    </div>
  );
}
react-intl
完成的結果如下:
react-intl
如果有需要也可以使用 <FormattedRelativeTime>,這個方法會回傳距今的時間:
// ./src/App.js
// ⚠️ 在 2.x 版時使用的是 FormattedRelative
import {
  FormattedRelative,
} from 'react-intl';

function App({ setLocale }) {
  return (
    <div className="App">
      <header className="App-header">
        <!-- ... -->
        <br />
        <!-- ... -->
        <FormattedRelative value={new Date() - 60 * 10}/>
      </header>
    </div>
  );
}

⚠️ 在 v2.x 前使用的是 <FormattedRelative>
顯示的結果如下:
react-intl

數值多語系

若有需要針對數值進行多語系的切換,react-intl 則提供了:

更新到最新 3.x beta 版

務必參考 Upgrade Guide @ react-intl
安裝當前最新版本(非穩定版):
# 撰寫本文時最新的版本為 v3.0.0-beta-11
$ npm install react-intl@next
注意事項:
  • <FormattedRelative> 改為 <FormattedRelativeTime>,且 value 的使用方式不同,改成是用「差距(delta)」,參考 FormattedRelativeTime
  • 改用瀏覽器原生的語系資料,移除 addLocaleData 的方法,參考 Migrate to using native Intl APIs
因此,我們不需要在 addLocaleData
react-intl

2019年7月2日

[筆記] WebRTC 網路影音 -實作篇(demo of media, video, audio)

Photo by freestocks.org on Unsplash
以下內容完全為整理自 30天之即時網路影音開發攻略(小白本) by 我是小馬克 @ iThome 的筆記,無原創內容。

建立串流點播工具

將影音檔轉成 HLS 用的串流檔

透過 ffmpeg 即可將影音檔(例如,mp4)轉成 .m3u8.ts 適用於 HLS 的串流檔。
在 Node.js 中可以使用 fluent-ffmpeg 這個工具進行轉換:
// ./ffmpeg-helper.js
// 把 mp4 檔轉換成 .m3u8 索引檔和多支 .ts 檔
const ffmpeg = require('fluent-ffmpeg');

module.exports = {
  convertToHls: async (file) => {
    return new Promise((resolve) => {
      ffmpeg(file, { timeout: 432000 })
        .addOptions([
          '-profile:v baseline', // for H264 video codec
          '-level 3.0',
          '-s 640x360', // 640px width, 360px height
          '-start_number 0', // start the first .ts segment at index 0
          '-hls_time 10', // 10 second segment duration
          '-hls_list_size 0', // Maximum number of playlist entries
          '-f hls', // HLS format
        ])
        .output('./source-m3u8/output.m3u8')
        .on('end', () => {
          console.log('finish');
          resolve();
        })
        .run();
    });
  },
};
並且建立一支 convert.js 來執行:
// ./convert.js
const ffmpegHelper = require('./ffmpeg-helper');

(async () => {
   await ffmpegHelper.convertToHls('./source.mp4');
})();
轉換好的檔案會被放置到 source-m3u8 的資料夾內:
imgur

建立支援 HLS 的 Server

要建立 HLS 的 Node 伺服器可以使用套件 hls-server
// index.js
const HLSServer = require('hls-server');
const http = require('http');

const hls = new HLSServer(server, {
  path: '/streams', // Base URI to output HLS streams
  dir: 'source-m3u8', // Directory that input files are stored
});

server.listen(8000);

使用 ffplay 播放串流檔

$ node index.js    # 於專案資料夾下執行
$ ffplay http://localhost:8000/streams/output.m3u8
如此即可開始播放:
imgur

在瀏覽器進行播放

若要在瀏覽器進行播放,一樣需要額外的套件,這裡用的是 hls.js
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>HLS Client</title>
  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>

<body>
  <div id="app">
    <video controls id="video"></video>

    <input type="text" />
    <button type="button" id="load">Load</button>
  </div>

  <script>
  const video = document.getElementById('video');
  const button = document.querySelector("#load");

  if (Hls.isSupported()) {
    var hls = new Hls();
    hls.attachMedia(video);
    hls.on(Hls.Events.MANIFEST_PARSED, function () {
      video.play();
    });
    button.addEventListener("click", function () {
      hls.loadSource(document.querySelector("input").value);
    })
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.addEventListener('canplay', function () {
      video.play();
    });
    button.addEventListener("click", function () {
      video.src = document.querySelector("input").value;
    })
  }

  </script>
</body>

</html>
Server 的部分做一些修改,讓它可以 serve 這支 index.html
// index.js
const HLSServer = require('hls-server');
const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/html');
  const html = fs.readFileSync('index.html', 'utf8');
  res.write(html);
  res.end();
});

const hls = new HLSServer(server, {
  path: '/streams', // Base URI to output HLS streams
  dir: 'source-m3u8', // Directory that input files are stored
});

server.listen(8000);
完成後,順利的話只要在表單中輸入 http://localhost:8000/streams/output.m3u8 就可以在網頁上載到影片:
imgur

建立直播工具

直播的部分會分成「推流」和「拉流」兩個部分:
imgur
  • 在 server 的部分,這裡會使用到 Node-Media-Server 這個套件來接收從直撥主送過來的推流,並提供 .flv 檔給 Client。
  • 在 client 的部分,會使用 bilibili 提供的 flv.js 來拉流。
要建立直播,首先要把 media server 給 on 起來:
$ node app.js
接著透過 ffmpeg 假裝是直播主透過 RTMP 協定進行推流:
$ ffmpeg -re -i source.mp4 -c copy -f flv rtmp://localhost/live/mark
這時候 server 就可以收到直播主的推流,client 的部分則可以使用 flv.js 來拉流:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Live Streaming</title>
    <script src="https://cdn.bootcss.com/flv.js/1.4.2/flv.min.js"></script>
  </head>

  <body>
    <video controls id="video" width="100%"></video>

    <input type="text" />
    <button type="button" id="load">Load</button>
    <script>
      const button = document.querySelector('#load');
      if (flvjs.isSupported()) {
        button.addEventListener('click', function() {
          const video = document.getElementById('video');
          const target = document.querySelector('input').value;
          const flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: `http://localhost:8000/live/${target}.flv`,
          });
          flvPlayer.attachMediaElement(video);
          flvPlayer.load();
          flvPlayer.play();
        });
      }
    </script>
  </body>
</html>

影音串流伺服器常見問題

作為一個影音串流伺服器,常見的問題包含「伺服器連線數限制」、「流量頻寬問題」、「效能消耗問題」、「遠距離封包遺失問題」。
透過 Load Balance 可以解決「伺服器連線數限制」和「效能消耗問題」;而「流量頻寬問題」和「遠距離封包遺失問題」則可以透過 CDN 解決。
沒有 CDN 的協助基本上是很難完成影音串流伺服器的。

Content Delivery Network (CDN)

透過內容交付網路(CDN, Content Delivery Network),即使伺服器架設在美國,而使用者人在台灣,依然可以用更短的時間取得這些資源。
CDN 主要可以分成三個部分:
  • 智能 DNS(Intelligent DNS):告訴 client 最近的 Edge CDN 位址在哪
  • 邊緣 CDN(Edge CDN):散佈在世界各地的 CDN 節點
  • 來源伺服器(Origin Server):原始資料存放的位址
imgur
關於如何在 AWS 上設定 CDN 可以參考 30-23之 CDN 的說話島 ( AWS CloudFront CDN 實作 ) by 我是小馬克 @ iThome

WebRTC 與 SDP

透過 WebRTC 可以讓瀏覽器不需要任何外掛的情況下直接進行 P2P 的溝通,因此這裡示範如何用 WebRTC 建立 P2P 的影音連線。
雖然 WebRTC 可以讓使用者直接透過瀏覽器進行 P2P 連線,但在雙方建立連線前仍需要有一個伺服器讓他們知道雙方的位址,才能進行溝通,這個伺服器稱作 Signaling Server,這並沒有規範在 WebRTC 內,可以使用 HTTP 輪詢(一直打 request 詢問)或 websocket 的方式實作。這裡會使用到 peer.js 這個套件來實作。
當雙方還不認識彼此時,需要透過傳送自己的 SDP( Session Description Protocol,會話描述協議) 到 Signaling Server 來認識彼此,SDP 中會包含有發送者的地址、媒體類型、傳輸協議、媒體格式。
WebRTC 加上 SDP 後的流程如下:
  1. 用戶 A 向 Server 發送進行會話,內容包含 A 的 SDP。
  2. Server 將會話 SDP A 請求發送給用戶 B。
  3. 用戶 B 向 Server 進行應答,並回應 B 的 SDP。
  4. Server 向用戶 A 發送 用戶 B 的 SDP。
  5. A、B 雙方使用 SDP 開始建立連線。
imgur

影音串流在 P2P 上的困難與解法

當透過 P2P 的方式進行影音傳輸,因此當使用者有使用「網路位址轉換(NAT, Network Address Translation)」或防火牆時,會沒辦法直接找到目標電腦的位址。
在 WebRTC 則使用了 ICE (Interactive Connectivity Establishment) 這個框架來解決這個問題,ICE 整合了 STUN (Session Traversal Utilities for NAT)TURN (Traversal Using Relay NAT ) 兩個協定,讓 NAT 內的用戶一樣可以找到它的位置。

參考資源