🎈

これで盛り上がること間違いなし?な発表イベント用ツールを作った話

2024/06/06に公開

背景

2024/06/21にReact 19に関する現地イベントをちょっと変わった内容で開催するのですが、その1つが「参加者全員LT発表」というものです。(実態は全て発表枠なんです)

モチベーションとしては、全員一言は喋ってコミュニケーションの機会を増やし、今後の繋がりを濃く保ちたいという思いがあり、実験的ではありますがやってみることにしました。

今回は20人の参加者に喋ってもらうとして、1人3分の持ち時間を設定しています。
数字的には、20 x 3 = 60分の枠ですが、発表者全員が各々のPCを使うとなると確実に交代するための時間に1,2分を奪われ、接続不良などのトラブルの確率も上がってしまうことが予想できます。

ということで、思いつくことは事前に資料を全員から受け取り、1台のPCで全員分の発表資料のpdfを投影すればよいでしょう。

なんですが、

なんだか、

それじゃ、

つまらない!

Reactのイベントなんだから一捻りねじ込みたい。

ということで思いつきと勢いで作ったものがこちらです。
https://github.com/t6adev/speaker-slides-party

(片手間で最速リリースを目指し、eslintもprettierも整えずぐちゃぐちゃしてますので、誰か綺麗にしてくれると喜びます。味気ないので、可愛かったりかっこいい名前とロゴなんかも!)

デモはこちら
https://reactfan.tech-space-production.workers.dev/presentations

作ったもの

簡単に言えばイベントで発表者全員の発表資料を映し出すツールです。そこに思いつきで付け足した機能がいくつかあります。

トップページ

全員分の発表タイトルと発表者情報が一覧で表示されます。「Start」ボタンなどでスライドを表示する画面に遷移できます。

アプリにすることでイベント後に発表者情報にアクセスしやすくなります。資料内で自己紹介ページを見逃しても安心です。

発表用ページ

発表資料のpdfを表示します。フルスクリーン対応です。
発表時間を平等にするために、タイマーを左下に置いてます。時間は守りましょう。

賑やかし機能

(記事タイトル回収です)
盛り上がってもらいたい(シーンとした時間を無くしたい)という思いで、紙吹雪を投げつける機能があります。発表を聞く人たちは、手元のPCで発表者と同じ画面を開きマウスを動かすことで紙吹雪でリアクションできます。
こんな感じです。(伝わるのか?)

画面上部に絵文字を切り替えると、紙吹雪がその絵文字になります。ショートカットキーを割り当てているのでキーボードの1,2~9,0で絵文字の切り替え可能です。
「Setting」から紙吹雪のトリガーをキーボードのc押下にすることもできます。もちろん、この機能をオフにもできます。

websocketベースのPartykitを利用しているので、個人的にこの機能をParty Modeと呼んでいます。

備考

発表イベントの区別やPDFのアップロードなど1つのサービスとして完成されているものではなく、主催側が専用にデプロイして使うことを想定しています。ログイン機能的なものは用意していないので、発表者側と視聴者側の区別などもできません。そのため、Party Modeを使う際は発表中のページを視聴者自身が手元で開いて使います。(発表者のスライドが変わると視聴者のスライドも切り替わる、ような機能はありません)

技術

今回これを作るにあたって、最近触っているWakuをベースにしました。

React Confでも何度か登場したWakuですが、皆さんお気づきでした?
(これはPartykitの作者の発表シーン

WakuでPartykit用のビルドが”一応”サポートされているのでこの機会を使ってチャレンジしてみたかったというモチベーションがあります。
また、PartykitがユーザーのCloudflareアカウントへのデプロイをサポート(Beta)してくれているので、Waku x PartykitなアプリをCloudflareへデプロイするチャレンジにもなりました。
(※ちなみにPartykit側でデプロイ先を用意してくれるのでサーバーがなくても既存のwebアプリにPartykitを組み事が可能です)

Waku x Partykit チャレンジ

WakuにPartykitを組み込む方法はどこにも書かれていない

ビルドコマンドは紹介されているものの、他のデプロイ先と毛色が違うPartykitはもう少し説明がほしいのですが、Waku作者のDaishiさんに聞いた所、「実は動かしてないからわからない」という有り難いお言葉を頂きました😇

Partykitの作者のSunilさんがPRを送ってサポートされるに至ったのですが、ご覧の通り"experimental"の一言で終わっています。
https://github.com/dai-shi/waku/pull/494

そもそもPartykitを扱うことも初めてだったので、どういうモノなのか調べつつPRの意図を探しました。
するとどうやら、PRの中身はWakuをCloudflareへデプロイするためのコードと酷似しており、先のアナウンス(PartykitをCloudflareの個人アカウントにデプロイ可能になった件)もあったことから、PartykitのサーバーサイドのコードをWakuに用意してデプロイできる可能性がでてきます。
(こちらがcloudflare用のコード)
https://github.com/dai-shi/waku/blob/8906b73a34d4b418caf70dcb23fe9300cbf7be9c/packages/waku/src/lib/builder/serve-cloudflare.ts#L29-L41
(こちらがpartykit用のコード)
https://github.com/dai-shi/waku/blob/8906b73a34d4b418caf70dcb23fe9300cbf7be9c/packages/waku/src/lib/builder/serve-partykit.ts#L29-L36

上記のonFetchが何者かというと、ドキュメント上に以下のようにありました。上記と全く一緒ですね。ということで、作者本人の実装通りで問題なくWakuも動いてPartykitも扱えそうです。

https://docs.partykit.io/reference/partyserver-api/#static-onfetch )

しかし、ここからどうすればよいかわかりません。
実は上記のコードはWaku側がデプロイ先を区別して専用のコードを吐き出すテンプレートとして扱われるので、onFetchの中身にPartykit用のコードを書くことができないのです。(Related reading: Creating custom endpoints with onFetchとあるので期待してリンク先を開くとComming soonの文字🥲)

もし、真面目にWakuに則ってコードを書くならばmiddlewareを駆使する必要が出てきます。が、そんな真面目なコードを書く前に実験です。

waku build後のファイルをPartykit用に改変してみたら・・・?

先ほど、例のコードはテンプレートとして扱われると述べましたが、waku buildするとデプロイ先のオプション(--with-partykitなど)に応じて、出力される結果が変わります。
それがビルド後にできるdist/serve.jsファイルです。partykit用のオプションを付与してbuildすると以下のような結果になります。

...(省略)...

const servePartykit = {
  onFetch(request, lobby, ctx) {
    if (!serveWaku) {
      serveWaku = runner({
        cmd: "start",
        loadEntries,
        env: lobby
      });
    }
    return app.fetch(request, lobby, ctx);
  }
};
export { servePartykit as default };

幸い、ミニファイなどされてないので楽にいじれます。
ここに、Partykitが提供するonMessage APIを手書きで加えてみます。(アプリ的にやりたいことは一人のユーザーのメッセージを他のユーザーへブロードキャストすることで、ドキュメント的にもそれはonFetchではなくonMessageを使って紹介されています。ものは試しです)

const servePartykit = {
  onMessage(message,sender,room) {
    room.broadcast(message,[sender.id])
  },
  onFetch(request, lobby, ctx) {
    if (!serveWaku) {
      serveWaku = runner({
        cmd: "start",
        loadEntries,
        env: lobby
      });
    }
    return app.fetch(request, lobby, ctx);
  }
};
export { servePartykit as default };

あとは、Wakuの'use client'なコンポーネントにimport usePartySocket from "partysocket/react"; を使ったコードを書いてみると・・・?
(https://docs.partykit.io/reference/partysocket-api/#usage-with-react )

できました🙌

躓きポイントとしては、Partykitのドキュメントはclassベースで書かれており、this.room.broadcast()を紹介されていますが、手書きした箇所は単なるobjectのメソッドですので使えません。駄目かと思いましたが、Partykitのコードを見に行くと第三引数にroomが与えられていたのでそれに沿って書くと無事動作してくれました。

さて、先に述べたようにonFetchのみでやろうとするとwakuのmiddlewareでonMessage相当の処理を書く必要がありそうですが、なんだか骨が折れそうです。怠惰にいきましょう。
injectPartykitMethods.tsというスクリプトをwaku build後に実行することで、partykitMethods.tsに書いたコードをdist/serve.jsに差し込める用にしました。

https://github.com/t6adev/speaker-slides-party/blob/09e19d55b1807b315e280c8d6577b0572e2d11ed/partykitMethods.ts#L1-L3

(余談:TSでimportした関数をtoString()すると型が消えてJSの文字列になるって発想が全くなくて、不思議に感動しました)

その他

PDFを扱うために、react-pdfを使い、紙吹雪の表現にはcanvas-confettiを利用しています。色々工夫が必要でその苦労話もあるのですが、雑に喋ったものをYouTubeにアップしてあるのでお時間ある方&ご興味のある方はご覧いただければです。(動画はタイムスタンプも無いしで見にくいですがこのあたりから話してます)

コードの箇所だけおいておくと、react-pdfはビルドのために以下の工夫が必要だったり、
https://github.com/t6adev/speaker-slides-party/blob/0d892fc4f5b92e5216631e7486fcb846dc6346ef/vite.config.ts#L31-L39

canvas-confettiは、フルスクリーンでも紙吹雪が表示されるように以下の工夫が必要だったりしました。
https://github.com/t6adev/speaker-slides-party/blob/0d892fc4f5b92e5216631e7486fcb846dc6346ef/src/components/confetti.tsx#L48-L70

おわりに

少なからず当初の目的(一捻り入れたい)は達成できて個人的には満足した開発になりました。イベント当日は盛り上がること間違いなしですね!(願望)

せっかくなので興味ある方に触ってもらえたらと思い、publicにしていますので自由に遊んでもらえればです。
また、イベント等で使ってみたい/使ってみた、など有りましたら教えて頂けると嬉しいです!

Wakuもalpha版がリリースされたので、ますます盛り上げていきたいですね。では!

Discussion