notebook

都内でWEB系エンジニアやってます。

ブックマークレットを使ってGoogleSlideでの発表中にユーザーツールのコメントを流す

ちょうど社内の勉強会で発表する機会があったのと、もともとこんなことやりたいなーと思っていたので調べてみた

GoogleSlideにはユーザーツールと言ってslidoのような見ているユーザーがスライドに対して質問を投稿できる機能がある(発表者がプレゼンターからユーザーツールを起動することで専用のURLが発行される)

このユーザーツールは発表者側、質問者側ともに質問が投稿されたら一覧に出てくるようになっていてインタラクティブに聞いている側とコミュニケーションを取りやすいようになっている

f:id:swfz:20210805022330p:plain

しかし、スライドに直接反映させる(表示させる)には発表者が「表示」ボタンをクリックする必要がある

  • 実際のユーザーツールの使用イメージ

f:id:swfz:20210805022336g:plain

「ここでいくつか質問きましたね、1つずつ見てみましょうか」みたいな感じのやりとりが必要な感じを想像させる

そこで、スライドを見ているユーザーのコメントを直接スライド上に流せないかなーと思っていた

サービスで言うとcommetsというサービスがそれかなと思う

comets 発表スライド上にコメントが流せるサービス

comets.nabettu.com

ちゃんと調べたわけではないので間違ってたら申し訳ないがおそらくコメントデータなどはサービス側で持つようにして投稿されたデータをブックマークレットで呼び出してスライド上に直接流す感じだと思う

GoogleSlideの場合せっかくユーザーツールがあり質問のデータなどもすでに持っているのでこれを使ってよしなにできないかなと考えた

結論から言うと次の3つを組み合わせて実現できた

  • MutationObserver
  • BroadcastChannel
  • RequestAnimationFrame

手順

  • プレゼンターの質問リスト
    • MutationObserverで質問リストのDOMを監視、変更があればBroadcastChannelで表示側へ通知
  • スライド表示側
    • BroadcastChannelでイベントを受け取りスライド上に表示するコールバックを定義

行けそう! ということで1つずつ分解して試してみる

MutationObserverでDOMの変更を検知

下記を参考にした

MutationObserver.observe() - Web API | MDN

developer.mozilla.org

観測対象の要素を指定して対象要素以下でDOMの変更があった場合に指定したコールバックを呼ぶ

今回の場合だとプレゼンターのウィンドウでDevToolを開いて中のコンテンツ(質問のリスト)が追加されたらコールバックを呼ぶようにする

observeElement = document.querySelector('.punch-viewer-speaker-questions');
observer = new MutationObserver(function(records) {
  console.log(records);console.log('callback that runs when observer is triggered');
});
observer.observe(observeElement, {subtree: true, childList: true});

これで対象の要素以下のDOMの変更を検知してコールバックを実行できる

例だとconsole.logで実際に変更のあったレコードを出力している

試しに適当なコメントを付けてみた

f:id:swfz:20210805022454p:plain

[0].addedNodes[0].children[1].children[2].innerTextとたどるとメッセージのプロパティを発見できた

f:id:swfz:20210805022500p:plain

Broadcast Channel APIでタブ間のデータやりとり

タブ間もしくはウィンドウ間でのメッセージングの方法の1つとしてBroadcast Channel APIというものがある

Broadcast Channel API - Web APIs | MDN

developer.mozilla.org

とりあえずBroadcast Channel APIをつかってみる

参考からコードを拝借してタブ間でのデータのやりとりを行ってみる

devtoolsを立ち上げたタブを2つ用意してConsoleにて下記コードを実行してみる

  • 送信側
bc = new BroadcastChannel('comment_channel');
bc.postMessage('This is a test message.');
  • 受信側
bc = new BroadcastChannel('comment_channel');
bc.onmessage = function (ev) { console.log(ev); }
  • 結果

f:id:swfz:20210805022507g:plain

無事タブ間でのメッセージのやりとりを行えることが確認できた

RequestAnimationFrameで文字列を流す

適当なサンプルを書いてローカルでサーバ立てて動作を確認した

  • index.html
<html>
  <head>
  </head>
  <body>
    <input type="text" id="comment">
    <button id="bt">Add Comments !!!</button>
    <div class="box">
      <div>
        <p class="comment">Contents</p>
      </div>
    </div>
  </body>
  <script src="./app.js"></script>
</html>
  • app.js
const addComment = (comment) => {
  const boxElement = document.querySelector(".box");

  const element = document.createElement("p");
  element.innerText = comment;

  boxElement.appendChild(element);

  const moveUnit = 5;
  const windowWidth = document.body.clientWidth;
  element.style = `translateX(${windowWidth}px)`;
  let moveX = windowWidth;

  const elementWidth = element.clientWidth;

  const moveAnimation = () => {
    if (moveX >= -elementWidth) {
      moveX = moveX - moveUnit;
      element.style.transform = `translateX(${moveX}px)`;
      requestAnimationFrame(moveAnimation);
    }
  };
  moveAnimation();
}

const handleClick = (event) => {
  const commentInput = document.querySelector("#comment");
  const comment = commentInput.value;
  console.log(comment);
  addComment(comment);
};

const button = document.querySelector("#bt");
button.addEventListener("click", handleClick, false);

f:id:swfz:20210805022525g:plain

文字が動いた

組み合わせてブックマークレットを作成する

以上で色々動かしてみた結果を踏まえてユーザーツールのコメントをリアルタイムに流せるようなブックマークレットを作成した

  • 読み取り側
const broadcastChannel = new BroadcastChannel('comment_channel');

const extractComment = (mutationRecords) => {
  const nodes = mutationRecords.filter(record => record.target.className === 'punch-viewer-speaker-questions').map(record => record.addedNodes[0]);
  console.log(nodes);
  const comments = Array.from(nodes).map(node => node.children[1].children[2].innerText);
  console.log(comments);
  return comments;
}

const observeElement = document.querySelector('.punch-viewer-speaker-questions');
const observer = new MutationObserver(function(records) {
  console.log(records);console.log('callback that runs when observer is triggered');
  broadcastChannel.postMessage(extractComment(records));
});

observer.observe(observeElement, {subtree: true, childList: true});
  • 表示側
const broadcastChannel = new BroadcastChannel('comment_channel');

const addComment = (comment) => {
  console.log(comment);

  const element = document.createElement("p");
  element.innerText = comment;

  const boxElement = document.querySelector('.punch-present-iframe').contentWindow.document.querySelector('.punch-viewer-content');
  boxElement.appendChild(element);

  const elementTop = (boxElement.clientHeight - element.clientHeight) * Math.random();

  const moveUnit = 5;
  const windowWidth = document.body.clientWidth;
  element.style.transform = `translateX(${windowWidth}px)`;
  element.style['font-size'] = '4em';
  element.style.position = 'absolute';
  element.style.top = `${elementTop}px`;
  let moveX = windowWidth;

  const elementWidth = element.clientWidth;

  const moveAnimation = () => {
    if (moveX >= -elementWidth) {
      moveX = moveX - moveUnit;
      element.style.transform = `translateX(${moveX}px)`;
      requestAnimationFrame(moveAnimation);
    }
  };
  moveAnimation();
}

const handleEvent = (event) => {
  event.data.forEach(comment => addComment(comment));
}

broadcastChannel.onmessage = handleEvent;

スライド自体がiframe内に展開されるようでdocument.querySelectorの書き方がいつもと若干異なる書き方になった

だいぶ雑さが出てしまった気がするがとりあえず目的とする動作をさせることはできた

やってみた結果

f:id:swfz:20210805022542g:plain

まとめ

  • GoogleSlideのユーザーツールとブックマークレットを利用して質問コメントをスライド発表中に直で流せるようにした
    • プレゼンター側、スライド側でそれぞれブックマークレットの実行が必要
  • すべてGoogle内のツールのデータで完結できるようにしたため「これつかって良いかな?」みたいな心配が不要
  • それぞれ次のような役割をになっている
    • MutationObserver
      • DOMの変更検知
    • BroadcastChannel
      • タブ間、Window間でのメッセージング
    • RequestAnimationFrame
      • 文字列のアニメーション

感想

前からやりたいなーこれで少しでも盛り上がったりしないかなーと思っていたが思ったよりさくっとできた

一度これを用いて社内の勉強会で発表してみたが割と好評だった

ただ、自分の処理能力が足りず発表に集中するあまり発表中盤まで流れてくるコメントが目に入ってこないという失態を犯してしまった

いきなり悪意あるコメントが出てくるような場だと厳しいが、近しい仲間内だったり和やかな雰囲気の場合は選択肢の1つではないかと思う

どうしてもその場で遮ってまで声出しづらいなーと思っている人は少なからずいると思うので…

一応コードも置いてあるのでよければ試してみていただければと思います

(何度か動作確認してたら流れるコメントがスライドの範囲外になってしまうパターンが何度かあった…

swfz/bookmarklets

github.com

参考

Canvasだけじゃない!requestAnimationFrameを使ったアニメーション表現 - ICS MEDIA

ics.media

Window.requestAnimationFrame() - Web APIs | MDN

developer.mozilla.org

Broadcast Channel API - Web APIs | MDN

developer.mozilla.org

MutationObserver.observe() - Web API | MDN

developer.mozilla.org