@blockcast/window-multicast-bridge

中文文档
0.6.0发布于 2026/11/05MITES Module零依赖构建来源已验证近 30 天下载 3202

Browser ESM polyfill that discovers a local Blockcast bridge and installs window.multicast over WebTransport.

原始使用文档
文档目录8 节
  1. Page bootstrap coordinator
  2. Browser-page scope
  3. Connect Beacon (iOS)
  4. Discovery boundary
  5. Batched packet delivery ( packets event)
  6. Declared delivery ( subscription.delivery )
  7. Building and testing
  8. Releasing

@blockcast/window-multicast-bridge

Browser ESM polyfill that finds a local Blockcast bridge (the Beacon app, an Isolated Web App, or a native BCDS bridge), connects to it over WebTransport, and installs window.multicast on the page.

import { initMulticastBridge } from '@blockcast/window-multicast-bridge';

window.addEventListener('multicast-bridge-ready', () => {
  // window.multicast is installed; resolve it with @blockcast/multicast-browser.
});
initMulticastBridge({ autoStart: true });

The package ships a minified ESM build in dist/:

exportfilewhat it is
.dist/multicast-bridge.jsdiscovery, the BCDS-v1 WebTransport client, and window.multicast
./bootstrapdist/bridge-bootstrap.jsthe page-owned bootstrap coordinator described below
./beacon-connectdist/beacon-connect.jsthe iOS Connect Beacon link, described below

Source lives in src/. Browsers load the built module directly, and bundlers can consume it as an ordinary dependency.

Page bootstrap coordinator

src/bridge-bootstrap.js is the canonical page-owned coordinator. createBridgeBootstrap() owns the layers above discovery — capability diagnostics, terminal status, a bounded discovery budget, and recovery — while initMulticastBridge() keeps owning discovery, the BCDS-v1 WebTransport handshake, and window.multicast installation. It never parses media and never embeds a player.

Capabilities are reported before any probe (probeBridgeCapabilities()): WebTransport support, secure context, per-host-class mixed-content viability, and the fine-grained Local Network Access permissions. A missing capability is a status with a reason (webtransport-unsupported, insecure-context, local-network-denied), never an open-ended probe loop. The older local-network-access alias stays informational: it reports denied on Chromium builds where both fine-grained permissions are granted. A throw on the launch path is held to the same contract rather than escaping: it terminates with launch-threw, so a page carrying a hostile window.multicast accessor gets a fallback instead of a checking status that never resolves.

Every stop is recoverable through a trigger rather than a timer: extension readiness, a Local Network permission change, an origin approval (originApproved()), or a bridge-instance change. A bridge instance is identified by endpoint plus certificate pin, because the native bridge mints a fresh certificate on each runtime start; an explicit descriptor instanceId wins when one exists. Retries are bounded by maxDiscoveryAttempts and maxRetries.

transportPolicy is honoured, not assumed. Only auto (and moq-only, which skips the bridge outright) reports fallback: 'moq-quic', returning the page to the QUIC/MoQ path it started on. Under multicast-only a terminal status carries no fallback: a forced native/AMT request that silently continued over QUIC would read as multicast playback.

Browser-page scope

Discovery (the . and ./bootstrap entries) is a generic browser bridge with no native handoff or app-association contract: Universal Links, App Links, AASA/asset-links files, the /live* route, and blockcast.network hosts are not part of bridge discovery or authorization. The optional ./beacon-connect entry (below) builds Beacon's universal link; discovery and authorization never read it. initMulticastBridge() discovers a local BCDS bridge and may install window.multicast on any browser page (including a non-Blockcast HTTPS origin, a mobile browser page, or a local development page); it does not inspect the page host or pathname. Discovery uses the configured browser origin for the bridge's exact origin allowlist and then connects directly over WebTransport. A player may import and arm the bridge from any page; /live* is not required for browser use.

Certificate pinning does not grant iOS background execution: when Beacon is the same-device bridge, suspending Beacon suspends that bridge for the browser. A separate always-running LAN gateway is independent of the phone app's lifecycle.

Connect Beacon (iOS)

Safari finds Beacon's bridge through Beacon's Safari extension. Chrome, Firefox, Edge and the other iOS browsers have no extension, and WebKit blocks an HTTPS page's loopback /health probe, so discovery cannot reach Beacon from them. ./beacon-connect gives any page a one-tap pairing link instead:

<a id="beacon-connect" hidden>Connect Beacon</a>
import { initMulticastBridge } from '@blockcast/window-multicast-bridge';
import { offerBeaconConnect } from '@blockcast/window-multicast-bridge/beacon-connect';

offerBeaconConnect(document.getElementById('beacon-connect'));
initMulticastBridge({ autoStart: true });

The offer follows the bridge's window events, so it works whoever builds the controller in this window with this package (0.4.0 or later): this page, createBridgeBootstrap(), or a player library. While a controller runs, every transition initMulticastBridge()'s onStatus reports is also dispatched on window as multicast-bridge-status, with detail: { phase, ok, reason, fromCache, takeover } and no endpoint or pin. phase is discovery, connect, disconnect or extension; ok is set on discovery and connect; reason (miss or lock-busy) on a discovery miss. The events are not replayed, so create the offer before the controller starts, or feed offer.onStatus yourself (it also takes createBridgeBootstrap()'s own statuses, which are not dispatched). The link appears on a discovery miss or a failed connect, so a page that is already paired never flashes it, and a stale pin offers it again. It is withdrawn on multicast-bridge-ready.

It is offered only on iOS, over HTTPS, in the top-level page. The tap goes to https://www.blockcast.network/beacon/connect, or to https://blockcast.network/beacon/connect from a page on www. return= names Chrome, Firefox or Edge; every other iOS browser, and an iPad browser that requests desktop sites without its product token, is returned to Safari. to= names the page, without credentials or discovery-steering fields and escaped to RFC 3986, as Beacon parses it. Beacon starts its bridge and reopens the page in that browser with the bridge's pin in the fragment. initMulticastBridge() keeps the pin as a pairing, so later visits connect with no tap while Beacon's bridge is running; a pin that no longer connects offers the link again. Beacon applies its own origin admission to to.

iOS opens Beacon only for a navigation made while the user's tap is being handled, in the top-level frame, to a host other than the top-level page's. Keep the control a real link; the helper rebuilds its href inside the tap. The helper shows and hides it with hidden, so do not give it a display rule without a matching [hidden] { display: none }. When iOS does not hand the tap to Beacon (Beacon not installed, or a private tab, where Chrome and Firefox never open apps from links), it opens the web page at that address instead. beaconConnectUrl() returns the bare href for pages that render their own control.

Discovery boundary

When a trusted caller supplies a known hostname and certificate pin, the browser connects directly; it does not fetch /health or /certificate.sha256:

https://player.example/watch?mc_bridge=beacon.local&wt_port=33445&fingerprint=<64-hex-SHA256>

The OS must resolve the actual advertised .local hostname; a DNS-SD service instance name alone is not a hostname. The pin is SHA-256 of the complete leaf certificate DER, supplied through an authenticated channel, not learned from HTTP or an unsigned Bonjour TXT record. Explicit pins bypass discovery caches and saved gateways. A wrong, expired, or malformed pin fails closed. A known hostname does not establish the initial pin's trust, and a server pin does not replace the bridge's origin-admission policy.

Presence of both mc_bridge and fingerprint (or fp) selects this strict path, even if a value is empty. Empty or malformed values are configuration errors, not permission to discover another gateway; remove the parameters to request ordinary discovery. A bare fingerprint without mc_bridge is not bridge configuration.

An explicit loopback pin on an HTTPS page is dialled directly and remains subject to browser local-network permission. It is never rerouted through a saved AutoTLS gateway to bypass that policy.

WebTransport pinned certificates must be valid for no more than 14 days. The native bridge currently issues a fresh certificate on each runtime start, so the caller must supply the current pin after a restart or rotation. This direct path does not add a pin-distribution service or manual certificate UI.

Ordinary HTTPS JavaScript cannot enumerate DNS-SD/mDNS services or issue DNS-SD/mDNS queries. Without explicit pinned configuration, discovery probes the native bridge's loopback /health endpoint (localhost or 127.0.0.1; localhost also covers a bridge bound to ::1), validates the BCDS-v1 capability and complete SHA-256 certificate fingerprint, and then opens a direct WebTransport (QUIC) connection pinned to that fingerprint. This HTTP bootstrap depends on browser policy and is blocked by Safari on HTTPS pages; it is not a universal mobile discovery mechanism.

LAN discovery is a separate platform capability. An installed IWA, browser extension, or native platform proxy can use its platform DNS-SD stack to discover _multicast-bridge._udp.local. and provide a concrete configured private endpoint to the browser. A regular page does not enumerate that service itself. The bridge remains bridge-only: it carries multicast bytes to window.multicast, while the browser owns presentation resolution and media rendering.

Batched packet delivery (packets event)

A bytes subscription returned by this bridge also emits packets: every packet the reader decoded in one dispatch batch, in arrival order, as one frozen array. Each element is exactly what one bytes event would have carried (an ArrayBuffer, or { data, receiveTime, receiveTimeOrigin?, receiveTimestampSource } when the frame had timing metadata). The batch ends where the reader's synchronous run already ends — at its existing yield point, or at the end of a WebTransport read — so no packet waits on a timer and the batch adds no size constant of its own.

packets is a provider extension, not part of the frozen v1 window.multicast contract. Feature-detect it before use and listen to exactly one of the two surfaces:

if (subscription.eventTypes?.includes('packets')) {
  subscription.addEventListener('packets', (event) => {
    for (const packet of event.detail) consume(packet);
  });
} else {
  subscription.addEventListener('bytes', (event) => consume(event.detail));
}

A subscription only builds the events some listener asked for, so a packets-only consumer never pays for a per-packet DOM event (measured dispatchEvent + CustomEvent at about 22 ms of main thread per second at ~450 packets/s on a Kirin 659). bench/packet-dispatch.mjs drives the real reader into a real subscription and reports DOM events and CPU time per packet for both surfaces.

Declared delivery (subscription.delivery)

A bytes subscription reports how its packets reach the page, as the bridge server declared it in subscribe_response:

  • 'ordered-relay': every packet travels the subscription's one reliable BCDS uni stream in socket-read order. The server may drop its oldest queued packet on overflow but never reorders, so the page sees a subsequence of the socket's receive order. native-bcds-bridge (the iOS Beacon bridge) declares this; it sends no datagram media.
  • 'datagram', or undefined when the server made no declaration: no ordering promise.

@blockcast/mmt-transport's MulticastTransport reads it once per join and, for 'ordered-relay', has FECManager decide FEC block fate by stream order instead of the datagram inactivity deadline: a relay pause carries no loss information. With that deadline, 50-200 ms pauses on the stream failed ~7-9% of blocks on iPhone Safari, with only ~0.2% wire loss. The client passes the declaration through and never infers it; a server that can also send WT datagram media for a subscription (for example the IWA service worker with datagram media enabled) must not declare 'ordered-relay'. The value is fixed for the life of a page subscription; a reconnect replay that declares differently is logged, not applied.

Building and testing

npm --prefix packages/window-multicast-bridge install --include=dev
npm --prefix packages/window-multicast-bridge run build   # src/ -> minified dist/
npm --prefix packages/window-multicast-bridge test
npm --prefix packages/window-multicast-bridge run test:dist  # same suite on the minified dist/

test/vectors/ holds the cross-language contract vectors the native bridge also tests against (packet-id-allow-list-v1.json, bridge-enrollment-v1.json). Change them in step with the bridge servers; test/vectors.test.mjs pins their digests, so a change has to update the pin too.

Releasing

The bridge versions independently of the contract and the adapter, so it rides its own tag: push window-multicast-bridge-v<version>, matching version in this package.json. CI builds, tests and packs it, checks the tag against the packed version, and publishes with provenance through npm trusted publishing. The first publish of the name needs the one-time bootstrap in ../multicast-contract/docs/RELEASE.md.

版本与发布记录

@blockcast/window-multicast-bridge 共发布 6 个版本,首次发布于 2026/10/21;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.0latest2026/11/053 天前
0.4.02026/10/2514 天前
0.3.12026/10/2118 天前
0.3.02026/10/2118 天前
0.0.02026/10/2118 天前
0.0.0-stage2026/10/2118 天前

关于 @blockcast/window-multicast-bridge 的常见问题

@blockcast/window-multicast-bridge 是什么?

Browser ESM polyfill that discovers a local Blockcast bridge and installs window.multicast over WebTransport.

如何安装 @blockcast/window-multicast-bridge?

在项目目录运行 npm install @blockcast/window-multicast-bridge,或使用 pnpm add / yarn add / bun add。

@blockcast/window-multicast-bridge 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/blockcast__window-multicast-bridge 是否存在。

@blockcast/window-multicast-bridge 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@blockcast/window-multicast-bridge 可以商用吗?

@blockcast/window-multicast-bridge 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blockcast/window-multicast-bridge 的下载量有多少?

根据 npm 官方下载统计,@blockcast/window-multicast-bridge 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,202 次,日均约 107 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@blockcast/window-multicast-bridge 还在维护吗?

最新版本 0.6.0 发布于 2026/11/05(3 天前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/10/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。