基於 WebSocket 的 PPT 遠端控制器簡單實現

LongWen 發表於 2021-09-15
WebSocket

這個 idea 是在大學畢設答辯前萌發的,當時正學習實踐前端知識,接觸到了一個通過前端程式碼來編寫並執行在瀏覽器中的 PPT 專案 revealjs

看完後當時的內心獨白:

畢設答辯的 PPT 可以用這個來寫,
學 (biāo)
以 (xīn)
致 (lì)
用 (yì)
,不枉我學過前端技術;

有了 PPT 的頁面,那翻頁還需要滑鼠一下一下點選?有沒有更好的替代翻頁效果的方式

別人 PPT 演說都還需要配置無線翻頁筆,有什麼方法可以實現類似的操作呢?

我可不可以使用手機控制 通過 revealjs 編寫的瀏覽器端的 PPT 頁面呢?

查閱一些資料後,瞭解了 WebSocket 全雙工通訊協議[1],現代瀏覽器支援;

由此想到,可以寫一個有上下左右控制的頁面,連線一個 WebSocket 服務,然後點選上下左右後下發「指令」到 WebSocket 服務,然後 PPT 頁面也連線此 WebSocket 服務 ,最後 WebSocket 服務 轉發 「指令」 給 PPT 頁面,從而做出翻頁操作。

NICE!🍻

基於 WebSocket 和 reveal.js,實現通過手機終端能控制 使用 reveal.js 實現的 PPT 網頁端,進行遠端上下左右翻頁的操作。

目前實現 一個控制器端控制一個 PPT 網頁端

PPT 網頁端和控制器端同時連線 WebSocket Service,由控制器端傳送翻頁操作指令(上下左右)到 WebSocket Service,然後 WebSocket Service 通知 PPT 網頁端,PPT網頁端執行翻頁操作指令。

基本原理

實現效果預覽

將上圖中由瀏覽器開啟的控制端頁面換為連線同一區域網的手機瀏覽器開啟,就實現了最初的想法。

原始碼地址: github.com/wenlong-date/ppt

1. 使用 workerman 提供 WebSocket 服務

PPT 網頁端和控制器端連線 WebSocket Service 後會簡單的「自報家門」,初始化 PPT 網頁端的 connection 和控制器的 connection,

file: server.php

// ... 
(new PptSocketServer())->run();

class PptSocketServer
{
    const CONNECTION_TYPE_PPT = 'connection_ppt';
    const CONNECTION_TYPE_CONTROLLER = 'connection_controller';

    protected $worker;
    protected $globalUid = 0;
    protected $globalPptConnection;
    protected $globalControllerConnection;

    public function __construct(int $port = 2346)
    {
        $this->initWorker($port);
    }

    public function run()
    {
        Worker::runAll();
    }

    protected function initWorker(int $port)
    {
        $this->worker            = new Worker("websocket://0.0.0.0:" . $port);
        $this->worker->count     = 1;
        $this->worker->onConnect = [$this, 'handleConnection'];
        $this->worker->onMessage = [$this, 'handleMessage'];
        $this->worker->onClose   = [$this, 'handleClose'];

    }

    // 簡單記錄連線的 id 資訊
    public function handleConnection($connection)
    {
        $connection->uid = ++$this->globalUid;
    }

    public function handleMessage($connection, $data)
    {
        // 初始化 PPT 網頁端的 connection
        if ($this->setPptConnectionIfNull($connection, $data)) {
            Log::info('ppt online');
            return;
        }
        // 初始化 控制端頁面的 connection
        if ($this->setControllerConnectionIfNull($connection, $data)) {
            Log::info('controller online');
            return;
        }

        // ...
    }

    public function handleClose($connection)
    {
        // 判斷並銷燬 PPT 網頁端或者控制端頁面的 connection
        $this->destructConnection($connection);

        Log::info($connection->uid . ' offline by close websocket');
    }

    protected function destructConnection($connection)
    {
        if (isset($connection->type) && $connection->type === self::CONNECTION_TYPE_PPT) {
            $this->globalPptConnection = null;
            Log::info('ppt offline');
            return true;
        }

        if (isset($connection->type) && $connection->type === self::CONNECTION_TYPE_CONTROLLER) {
            $this->globalControllerConnection = null;
            Log::info('controller offline');
            return true;
        }

        return true;
    }

    /**
     * 根據命令判斷和初始化 PPT 網頁端的 connection
     *
     * @param $connection
     * @param $data
     * @return bool
     */
    protected function setPptConnectionIfNull($connection, $data)
    {
        if (!is_null($this->globalPptConnection)) return false;
        if (!$this->requestConnectionIsPpt($data)) return false;

        $connection->type          = self::CONNECTION_TYPE_PPT;
        $this->globalPptConnection = $connection;
        return true;
    }

    /**
     * 根據命令判斷和初始化控制端頁面的 connection
     *
     * @param $connection
     * @param $data
     * @return bool
     */
    protected function setControllerConnectionIfNull($connection, $data)
    {
        if (!is_null($this->globalControllerConnection)) return false;
        if (!$this->requestConnectionIsController($data)) return false;

        $connection->type                 = self::CONNECTION_TYPE_CONTROLLER;
        $this->globalControllerConnection = $connection;
        return true;
    }

    public function requestConnectionIsPpt($data)
    {
        return $data === 'i am ppt';
    }

    public function requestConnectionIsController($data)
    {
        return $data === 'i am controller';
    }

}

2. 控制端頁面傳送「指令」給 WebSocket Service

file: /web/controller/index.html

// ...

var ws = new WebSocket('ws://' + location.hostname + ":2346");
ws.onopen = function () {
    ws.send('i am controller');
};
ws.onmessage = function (e) {
    console.log('controller get message from server: ' + e.data);
};

var $ = function (dom) {
    return document.getElementById(dom);
}
$('up').onclick = function () {
    sendCommand('up');
}
$('right').onclick = function () {
    sendCommand('right');
}
$('down').onclick = function () {
    sendCommand('down');
}
$('left').onclick = function () {
    sendCommand('left');
}

function sendCommand(status) {
    ws.send(status);
}

// ...

3. WebSocket Service 傳送「指令」給 PPT 網頁端

傳送「指令」前 判斷 PPT 網頁端是否線上,以及只允許一個控制端頁面進行「指令」的下發

file: /web/controller/control.js

// ...

class PptSocketServer
{
    // ...

    public function handleMessage($connection, $data)
    {
        // ...

        if (is_null($this->globalPptConnection)) {
            Log::info('ppt offline; cant control');
            return;
        }

        // 目前只允許一個控制器傳送指令。
        if (!is_null($this->globalControllerConnection)
            && $connection->uid !== $this->globalControllerConnection->uid
        ) {
            Log::info('sorry, you are not correct controller ' . $connection->uid);
            return;
        }
        // 轉發控制端「指令」到 PPT 網頁端
        $this->globalPptConnection->send($data);
    }
    // ...

}

4. PPT 網頁端執行翻頁「指令」

file: server.php

// ...

var ws = new WebSocket('ws://' + location.hostname + ":2346");
ws.onopen = function () {
    ws.send('i am ppt');
};
ws.onmessage = function (e) {
    console.log('ppt get message from server: ' + e.data);
    switch (e.data) {
        case 'up':
            Reveal.up();
            break;
        case 'right':
            Reveal.right();
            break;
        case 'down':
            Reveal.down();
            break;
        case 'left':
            Reveal.left();
            break;
        default:
            console.log('unSupport command : ' . e.data)
    }
};

// ...

注意: 如果要演示自己的 revealjs PPT 則需要先在 PPT 的首頁 html 檔案尾部中引入 control.js

 <script src="/controller/control.js"></script>

1. git clone github.com/wenlong-date/ppt.git && composer install
2. 將 web/ppt 目錄下的檔案 替換為你自己的 reveal.js PPT 相關的前端檔案 (預設有demo演示用的 PPT)
3. 執行 php server.php start [-d] (-d 後臺執行)
4. 另一個視窗執行 cd web && php -S 0.0.0.0:80 (或者使用 Nginx 提供 Web 服務)
5. 網頁端開啟地址 http://{yourip}/ppt/
6. 同一區域網連線的手機瀏覽器開啟地址為 http://{yourip}/ppt/ (會自動跳轉到控制端頁面)即可。(如果公網 IP 地址就沒有區域網路限制了)

倉庫中已經有 Dockerfile 檔案,可以手動 build 。

1. 同時也可以使用 docker pull wenlongdotdate/websocketppt 獲取映象
2. 本地執行 docker run -it -p 2346:2346 -p 80:80 -d wenlongdotdate/websocketppt (可以選擇自己的 PPT 前端檔案目錄 掛載 到容器的 /var/www/html/web/ppt 目錄下)
3. 網頁端開啟 http://{yourip}/ppt/
4. 同一區域網連線的手機瀏覽器開啟地址為 http://{yourip}/ppt/ (會自動跳轉到控制端頁面)即可。(如果公網 IP 地址就沒有區域網路限制了)

目前只是 一對一 的控制一個 PPT,功能很簡陋。

可以考慮為不同使用者提供一個針對 reveal.js 開發的網頁 PPT 提供一個 WebSocket 網頁控制端服務。

每個使用者在後臺可以建立一個 控制器資源, 控制器將會生成一個唯一的通訊頻道和 PPT 可以引入的 JavaScript 檔案(連線 WebSocket 服務並通訊)。

這樣 不同使用者 可以通過手機瀏覽器控制自己基於 Reveal.js 開發的網頁 PPT 需要下面三步:

1. 建立 控制器 資源
2. reveal.js 的 PPT 引入相應 JavaScript 檔案
3. 手機瀏覽器 掃描 當前控制器資源 控制器二維碼,開啟控制端頁面進行遠端控制

面向多使用者的服務

本作品採用《CC 協議》,轉載必須註明作者和本文連結