← 使用情境

查詢在線人數與觀看人數

顯示在線或正在觀看的人數,用 presence 計算不重複的使用者,並透過 HTTP API 和 webhook 從伺服器讀取每個房間的人數。

「1,284 人在線」、「38 人正在觀看」、「房間已滿」:一個即時的人數讓產品顯得熱鬧,同一個數字也告訴你的團隊哪些房間最熱門、什麼時候該加派管理員、晚上的高峰有多高。你不必自己數連線。Hibira 本來就知道每個頻道有誰訂閱,而 Pusher 協定提供了四種查詢方式。

使用者得到什麼

  • 隨著有人進出而跳動的「在線人數」或「正在觀看」。
  • 一排頭像加上「還有 120 人」,同一個人開兩個分頁只算一次。
  • 房間在有人嘗試加入之前就顯示「已滿」。
  • 給你的團隊:一個列出最熱門房間的管理頁,以及高峰的歷史紀錄。
示範管理頁:全站在線人數、即時觀看人數和最熱門的房間,全部從 Hibira 讀取。
示範管理頁:全站在線人數、即時觀看人數和最熱門的房間,全部從 Hibira 讀取。

用到的功能

  • 訂閱人數(pusher:subscription_count),在瀏覽器裡取得公開與私人頻道的人數。
  • Presence 頻道(presence-)計算不重複的使用者,大型 presence 頻道(presence-large-)最多可容納 100,000 名成員。
  • HTTP API(GET /channels),讓你的伺服器隨時讀取人數。
  • Webhook(member_added、member_removed、subscription_count),人數變化時通知你,方便保留歷史紀錄。

在瀏覽器裡

Hibira
pusher:subscription_count
每位觀眾「38 人正在觀看」

你的伺服器查詢

管理頁
fetch
你的伺服器
GET /channels?info=user_count
Hibira每個房間的人數

你的伺服器收到通知

Hibira
webhook member_added
你的伺服器歷史紀錄與警示
在瀏覽器裡讀取人數、由你的伺服器查詢,或在人數變化時收到通知

算連線還是算人?

這是兩個不同的數字,值得刻意選擇:

  • subscription_count 算的是連線。一個人開三個分頁就算三次。它在公開與私人頻道上都能用,不需要登入,適合公開頁面上的「正在觀看」計數。
  • user_count 算的是 presence 頻道上不重複的使用者,依據你的伺服器簽署的 user_id。兩個分頁,或一支手機加一台筆電,都只算一次。只要「人」才是重點,例如座位上限或在線名單,就用它。

步驟 1:在瀏覽器裡顯示人數

在應用程式設定裡開啟 傳送訂閱人數。之後公開或私人頻道的每位訂閱者都會收到目前的人數,人數變動時最多每秒一次:

const stream = pusher.subscribe('stream-42');
stream.bind('pusher:subscription_count', ({ subscription_count }) => {
  watching.textContent = subscription_count.toLocaleString();
});

不寫程式也行,一行嵌入腳本可以做到同樣的事:<span data-hibira-online="stream-42">0</span> 人正在觀看。

步驟 2:用 presence 計算人數

在 presence 頻道上,每個人都會收到成員名單,所以人數已經在瀏覽器裡了:

const room = pusher.subscribe('presence-room-' + roomId);
const show = () => online.render(room.members.count, room.members);

room.bind('pusher:subscription_succeeded', show);
room.bind('pusher:member_added', show);
room.bind('pusher:member_removed', show);

Presence 頻道最多 100 名成員,因為每位成員都會收到每一次加入。如果是整個網站或大型活動,就讓每個頁面加入同一個大型 presence 頻道,搭配 @hibira-io/large-presence。它最多容納 100,000 名成員,每秒送一次精確人數和在場者的樣本:

import { LargePresence } from '@hibira-io/large-presence';

const site = LargePresence.subscribe(pusher, 'presence-large-site', { maxMembers: 12 });
site.on('ready', ({ count }) => badge.render(count, site.members));
site.on('change', ({ count }) => badge.render(count, site.members));

步驟 3:從你的伺服器查詢

你的伺服器可以透過 HTTP API 讀取同樣的數字,任何相容 Pusher 的伺服器函式庫都可以。查詢單一頻道:

const res = await pusher.get({
  path: '/channels/presence-room-42',
  params: { info: 'user_count,subscription_count' },
});
const { occupied, user_count, subscription_count } = await res.json();

一次查詢所有房間,最熱門的排在前面:

const res = await pusher.get({
  path: '/channels',
  params: { filter_by_prefix: 'presence-room-', info: 'user_count' },
});
const { channels } = await res.json();
// { "presence-room-7": { "user_count": 42 }, "presence-room-9": { "user_count": 17 } }
const busiest = Object.entries(channels)
  .map(([name, { user_count }]) => ({ name, users: user_count }))
  .sort((a, b) => b.users - a.users);

要在簽署訂閱之前檢查座位上限,就在頻道授權路由裡讀取房間人數,滿了就拒絕:

app.post('/pusher/auth', requireLogin, async (req, res) => {
  const { socket_id, channel_name } = req.body;
  const room = await pusher.get({ path: `/channels/${channel_name}`, params: { info: 'user_count' } });
  const { user_count } = await room.json();
  if (user_count >= 8) return res.status(403).json({ error: 'room_full' });
  res.json(pusher.authorizeChannel(socket_id, channel_name, { user_id: req.user.id, user_info: { name: req.user.name } }));
});

發布事件時,也可以詢問當下有多少連線訂閱,知道這個事件送到了多少人:

const sent = await pusher.trigger('stream-42', 'goal', { minute: 81 }, { info: 'subscription_count' });
const { channels } = await sent.json();
console.log(`reached ${channels['stream-42'].subscription_count} viewers`);

user_count 只能用在 presence 頻道;在頻道列表上查詢時需要加上 filter_by_prefix=presence-。把多個房間的人數相加,同時在兩個房間的人會被算兩次;要取得全站不重複的人數,請改讀 GET /channels/presence-large-site?info=user_count。

步驟 4:用 webhook 保留歷史紀錄

要畫出晚上高峰的圖表,或在房間清空時發出警示,就讓 Hibira 通知你的伺服器。在入口網站中為 member_added 和 member_removed(presence 頻道)、subscription_count(公開與私人頻道,開啟計數時),或 channel_occupied 和 channel_vacated 新增 webhook:

app.post('/hibira/webhooks', express.text({ type: '*/*' }), async (req, res) => {
  const webhook = pusher.webhook({ headers: req.headers, rawBody: req.body });
  if (!webhook.isValid()) return res.sendStatus(401);
  for (const e of webhook.getEvents()) {
    if (e.name === 'member_added') await db.online.add(e.channel, e.user_id);
    if (e.name === 'member_removed') await db.online.remove(e.channel, e.user_id);
    if (e.name === 'subscription_count') await db.viewers.record(e.channel, e.subscription_count);
  }
  res.sendStatus(200);
});

小技巧

  • 讀取人數不會用到你的訊息額度,只有發布的事件才算訊息。不過仍建議在伺服器上把 HTTP API 的結果快取幾秒,而不是每次頁面瀏覽都查一次。
  • Webhook 可能延遲或重複送達。拿它來做歷史紀錄與警示;需要當下精確的數字時,就讀 HTTP API。
  • 在熱門的公開頁面上顯示四捨五入的數字(「1.2k 在線」):跳動較少,依然有熱鬧的感覺。
  • 人數以應用程式為單位。如果你有測試和正式兩個應用程式,各自只看得到自己的連線。
  • 方案限制的是同時連線數,不是人數:每個方案允許的高峰請見價格。