We’re sorry we missed you at re:Invent, but we can still meet!

Next.jsとMomento 🎯を使ったインタラクティブなライブリアクションアプリの構築

絵文字のリアクションで聴衆を惹きつける

アレン・ヘルトン
著者

Share

対面でプレゼンテーションをしたことがあるだろうか。話しながら部屋を見渡すと、うなずいたりメモを取ったりしている人がいる。誰かが手を挙げて、あなたが言ったことについて質問するかもしれない。相手を直接見ることができれば、相手の関心を引くのは本当に簡単です。

しかし、オンライン・プレゼンテーションは少し難しいものです。あなたは(一般的に)プレゼンする相手を見ることができません。一人でリハーサルをしていても、1000人の聴衆に向かってプレゼンをしていても、まったく同じように感じます。あなたと聴衆の間には断絶があり、あなたがうまくいっているのか、それとも失敗しているのかを知ることができないのです。

数ヶ月前、 Michael Liendo の投稿を目にしました。彼は、アップルのKeynoteとWebSocketを使って、プレゼンテーションのエンゲージメントを最大限に高めていました。彼は、聴衆のメンバーがプレゼンテーション中にリアルタイムでスクリーンに絵文字を送ることができるウェブサイトを構築した。なんてクールなんでしょう!

私もそれが欲しかった!コメントを送る機能も追加したいと思いました。それで彼のブログ記事を読んで、すぐにインスピレーションを受けたんだけど、問題がありました。私はMacを持っていないので(わかっている、わかっている)、Keynoteを使うことができません。それに、彼のデザインはAWSコンソールを使ってAppSyncでWebSocket APIを作るというものでした。これは1つのプレゼンテーションとしては信じられないほどクールだが、うまくスケールするようには思えませんでした。

私はすでに、おそらく必要以上にプレゼンテーションの構築に労力を費やしており、毎回毎回、観客のエンゲージメントを高めるためにウェブアプリを作り直す余裕はありません。そんな時間があればいいのですが、現実はそうではありません。このことを念頭に置いて、私はマイケルの驚異的なアイデアを強化するためのいくつかの要件を持っていました:

・Googleスライドとの互換性(無料が嫌いな人はいないでしょう!)。
・プレゼンテーションを構築しながら動的にサポート
・アーキテクチャとデプロイの要件を最小限に抑える
・プレゼンテーションの最後に楽しい統計を表示する

これらの要件を満たすために、私がどのようにライブリアクションアプリを作ったか見てみましょう。

グーグル・スライドのサポート

PowerPointやKeynoteを使ってプレゼンテーションを作ったことがある人なら、Googleスライドがそうでないことはわかるだろう。機能やアニメーションは限られているが、その分とてもよくできている。しかも無料で、素晴らしいオンライン・コラボレーション機能を備えています。すでにKeynoteやPowerPointのプレゼンテーションを持っているなら、Googleスライドに問題なくインポートできます。

このアプリのビルドに着手したとき、私は2つのことを念頭に置いていました。プレゼンテーションのhtmlをいじらないようにすることと、複数の作成者によるプレゼンテーションを許可することです。そこでSlidesのユーザーインターフェイスをあれこれ調べ、Publish to Web機能を使ってプレゼンテーションを一般公開する方法を見つけました。

プレゼンテーションをウェブに公開する際、埋め込むオプションが提供されます。これにより、スライドへのリンクを含むiframeが作成されます。これを任意のウェブページにドロップすれば、アプリでホストされているスライドをそのまま見ることができます!埋め込みコードをよく見てみると、特に便利なことに気づきました:

これはパラメータ化できるように見えました!プレゼンテーションIDは、URLのhttps://docs.google.com/presentation/d/e/。つまり、Next.jsアプリのページにiframeをドロップして、src要素をパラメータ化して、プレゼンテーションを汎用的にレンダリングすればよかったのです!https://docs.google.com/presentation/d/e/${slidesId}/embed?start=false&loop=false&delayms=60000

要約すると、Googleスライドをウェブに公開し、埋め込まれたiframeからidを取得して、それを私に返さなければならなかった。さて、それをどうするか考えなければなりません。

ダイナミック・プレゼンテーションのサポート

先ほども言ったように、新しいプレゼンテーションをするたびにこのアプリを作り直したくはありません。ただ自分のプレゼンテーションIDをアプリに渡して終わりたいのです。それができれば、勝利だと思います。

私は、プレゼンテーションを動的に検索するようにウェブアプリを構成しました。私のページ構造は以下のようになります:

├─pages
│ ├─[name]
│ │  ├─index.js
│ │  ├─react.js
│ │  ├─results.js

ここにはいくつかのページがあり、それぞれに「ダイナミックさ」の持ち味があります。

プレゼンテーションのページ

あのプレゼンテーションIDはまったくユーザーフレンドリーではありません。私はそれをフレンドリーな名前でエイリアスにしたかったので、人々はその怪物を入力する必要はありません。そこで、マッピングを行うAPIエンドポイントをアプリ内に作成しました。今のところ、ハードコードされたリストを使っていますが、プレゼンテーションの回数が増えるにつれて、データベースに詳細を保存して更新するプレゼンテーション管理ページに移行するつもりです。


import slides from '../../../lib/slides';
export default async function handler(req, res) {
  try {
    const { name } = req.query;

    const presentation = slides.find(m => m.name == name.toLowerCase());
    if (!presentation) {
      return res.status(404).json({ message: `A presentation with the name "${name}" could not be found.` });
    }

    return res.status(200).json({ id: presentation.id, title: presentation.title });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: 'Something went wrong' });
  }
};

slidesのインポートは、Google Slidesのid、タイトル、プレゼンテーションのフレンドリー名を持つjson配列だけです:


const slides = [
  {
    name: 'caching-use-cases',
    id: '2PACX-1vQxQnmKrdy1FX3KzTWs7mC89UHDNH5kVeiUJpeZBnQiWNYXX6QjupaUln',
    title: 'You DO Have A Use Case For Caching'
  },
  {
    name: 'building-a-serverless-cache',
    id: '2PACX-1vSmwWzT1uMNfXpfwujfHFyOCrFjKbL8X43sd5xOpAmlK01lEICEm2kg',
    title: 'Behind the Scenes: Building a Serverless Caching Service'
  }
];

誰かが私のアプリの/caching-use-casesエンドポイントをヒットすると、ページはサーバー側のコンポーネントからGoogleスライドのIDとタイトルを取得し、それを使ってiframe内のコンテンツをレンダリングします。

Reaction page

私はマイケルのようになりたいと願いました。そのために、私がプレゼンをしている最中に、人々が私のプレゼンにリアクションできるようなユーザーインターフェースを提供する必要がありました。そこで、/[name]/reactパスが活躍します。

まず、そのページに人を集めなければなりませんでした。しかし、ハードコーディングはしたくなかったのです。幸運なことに、ReactアプリでQRコードを動的に作成してレンダリングしてくれるreact-qr-codeライブラリを偶然見つけました。そこで、プレゼンテーション・ディスプレイの下に常に表示されるカードを追加し、ユーザーが携帯電話でスキャンして直接リアクションにジャンプできるようにしました。

念のために言っておくと、このプロジェクトではAmplify UIコンポーネントを使用しています。私はUIにはあまり詳しくないので、このようなスタイル付きコンポーネントがあると助かります!とにかく、プレゼンテーションの下にカードを追加すると、このようになります:

これはプレゼンテーションの間ずっと表示されるので、観客が早く来ても遅く来ても関係なく、リアクションページにアクセスして絵文字を送ったり質問をしたりすることができます。リアクションページはモバイルでの閲覧に最適化されており、聴衆は3つの絵文字を選ぶか、自分の質問やコメントを追加することができます。

聴衆が絵文字を押したり、質問を入力したりすると、私たちの管理するWebSocket(詳細は後ほど)を介してプレゼンテーション・ページにメッセージが送信され、スクリーンに表示されます。心配ご無用、避けられない罵声のために、コメントのスロットリングと冒涜的なフィルターを組み込んであります。

小規模展開‍

このプロジェクトのもうひとつの目的は、大規模なバック・アーキテクチャに依存せず、自己完結型の小さなウェブ・アプリを作ることでした。これはシンプルであることを意図しています。WebSocketをいじったり、AWSのコンソールでたくさんのクラウドリソースを作ったりしたくありませんでした。その代わりに、私はMomentoを利用することにしました。

すべてはNext.jsアプリで自己完結しています。フレンドリー名とプレゼンテーションIDのマッピングはアプリのサーバー側コンポーネントで行われ、WebSocketはMomento Topicsを介して処理されます。WebSocketチャンネル/トピックやサブスクリプションのようなクラウドリソースを管理する必要はありません。Momento Web SDKをプラグインすれば、あとは動くだけだ。文字通りです。

クラウドでこれを利用するためにしなければならないのは、ウェブホスティングのセットアップだけです。特定のクラウド・ベンダーに依存するわけではないので、VercelFastly、あるいはAWS Amplifyのようなもの(私の個人的な好み)でホストすることができます。しかし、セットアップする前に、まずやらなければならないことが2つあります:

1.プレゼンテーションで/lib/slides.jsファイルを更新する
2.3つの環境変数を設定する

MOMENTO_AUTH – Momento コンソール経由で発行された API キー。このトークンは、サーバー側のコンポーネントがブラウザのセッションに送信する、短命の API トークンを設定するために使用されます。
NEXT_PUBLIC_CACHE_NAME – Momento で使用するキャッシュの名前。APIキーと同じリージョンに存在する必要があります。アプリがすべてやってくれるので、好きな名前でキャッシュを作成すれば問題ありません。
NEXT_PUBLIC_DOMAIN_NAME – アプリのカスタムドメインのベースURL。カスタムドメインである必要はなく、デプロイ後に生成されたドメインに更新することもできます。

それからデプロイを実行します!一度デプロイすれば、あとは動き出すだけです。

楽しい統計

プレゼンの最後に楽しい統計を見せることが、私の要求のひとつだと言いいました。誰が一番反応したか、一番使われたリアクションは何かを見ること以上に楽しいことがあるでしょうか?‍

誰かがリアクション・ボタンを押すたびに、リアクションをした人とリアクションをした人の両方に得点が加算されます。

await cacheClientRef.current.sortedSetIncrementScore(process.env.NEXT_PUBLIC_CACHE_NAME, `${name}-reacters`, data.username);
await cacheClientRef.current.sortedSetIncrementScore(process.env.NEXT_PUBLIC_CACHE_NAME, name, data.reaction);

ソートされたセットを使うことで、私は難しい作業をすることなくリーダーボードを構築しています。特定のユーザー名と特定のリアクションのスコアをキャッシュでインクリメントしています。プレゼンテーションが終わって結果を見るときには、降順でスコアをフェッチすることで、自動的にリーダーボード効果を得ることができます。


const getLeaderboard = async (cacheClient, leaderboardName) => {
  let board;
  const leaderboardResponse = await cacheClient.sortedSetFetchByRank(process.env.NEXT_PUBLIC_CACHE_NAME, leaderboardName, { startRank: 0, order: 'DESC' });
  if (leaderboardResponse instanceof CacheSortedSetFetch.Hit) {
    const results = leaderboardResponse.valueArrayStringElements();
    if (results.length) {
      board = results.map((result, index) => {
        return {
          rank: index + 1,
          username: result.value,
          score: result.score
        };
      });
    }
  }
  return board;
};

リーダーボードの結果は、データを保存するキーとしてプレゼンテーションのフレンドリーネームを使用し、動的であることがわかります。この結果、ページはこのようになります:

これはプレゼンテーションにちょっとした競争と楽しみをもたらし、聴衆を十分に惹きつけておくことを期待するものである。

自分でやってみる

ぜひ試してみてください。聴衆を巻き込んで、あなたのプレゼンテーションを際立たせましょう。

いくらかかるんだろうと思っていたら、答えは「かかりません」!

Vercelのようなホスティング・プラットフォームでは、趣味のプロジェクトをホストするのに費用はかかりません。Momentoはデータ転送量1GBあたり0.50ドルで、毎月5GBが無料です。各リアクションは80バイトのメッセージを送信するので、無料で利用できるリアクションの量は次のように計算できます:

5 GB / (80 bytes x 2 (data in from publisher and out to subscriber)) = 33.5 million 

つまり、1ヶ月のリアクション数を3,350万以下に抑えれば、無料ティアの範囲内です 🙂 しかし、それを超える場合は、1ドルで1300万リアクションを得ることができます。

結局のところ、ゴールは人々があなたのメッセージを理解し、覚えてもらうことです。リアクションを変えたり、コメントを増やしたり、取り除いたり、あなたのコンテンツに注意を向け続けられるようなことなら何でも自由にやってください。

これを作るインスピレーションを与えてくれたマイケル・リエンドに感謝します。とても楽しいし、将来的な拡張の可能性もたくさんあります。魅力的なプレゼンテーションを提供し、リアルタイムで聴衆のフィードバックを得られることに興奮しています。

デプロイやMomentoトークンの取得、プレゼンテーションの公開方法など、いつでもお手伝いします。私かMomentoチームの誰かがDiscordMomentoのウェブサイトから連絡します。

Happy coding!

Share