Sharing rooms and clips
Two links carry a player from a chat window into your game: a join link, which puts them in a room with the person who sent it, and a replay link, which plays back a clip of something that already happened. Both are links to your own page. The platform supplies the room code, the clip bytes and a landing card, and your page does the rest.
Join links
room.link is the share link for the room a client is in. In a browser, joinRoom also writes ?room= into the address bar, so a player who copies the URL out of the bar shares a working
link without being told to.
const room = await joinRoom(schema);
await navigator.clipboard.writeText(room.link); A client that opens a link with ?room= joins that room instead of creating one. Nothing extra
is needed for that: it is what joinRoom does when you pass no room option.
Links on a web portal
A game embedded on a portal cannot share a link into your own origin, because the player is inside
the portal’s page. CrazyGames has its own invite link carrying the same room code, and @irtio/crazygames reads and writes it in place of room.link. Poki
needs no link work but does need its external-resources approval before a build can reach irtio at all. A Discord Activity needs no link at all: Discord decides who is
playing together, and discordRoomId derives the room code from that.
Making the link unfurl
Chat clients build their preview from the Open Graph tags in the HTML at the link, and they do
not run your JavaScript. Put static tags in the <head> of the page you deploy, describing the
game rather than the room:
<meta property="og:title" content="Crate Ball" />
<meta property="og:description" content="Two-minute physics football. Join a room and play." />
<meta property="og:image" content="https://mygame.example/preview.png" />
<meta property="og:url" content="https://mygame.example/" />
<meta name="twitter:card" content="summary_large_image" /> The room code cannot appear in those tags. The preview is fetched from your static HTML before
any room exists, and ?room= is a query parameter that most crawlers strip before fetching. Write
the tags for the game and let the room be a detail the player discovers on arrival. If you serve
your page from a framework that renders per request, you can vary the tags by URL, but a room code
is short-lived and a cached preview of it will outlive the room.
Replay links
Clips are saved by the room, in your server code, and the id comes back to the client however you choose to send it. See Replays for the recording side. A typical room saves a clip and sends the id to the players in a message:
// server
const clipId = await ctx.room.replay.clip(10); // the last 10 seconds
ctx.room.messages.clipSaved.send({ clipId }); On the client, replayShareLink turns that id into the link to hand somebody. It is a link to
your own page, because a replay is your game running against recorded bytes, and the only page
that can render your game is yours:
// client
import { replayShareLink } from '@irtio/client';
const link = replayShareLink(clipId, schema.project);
// https://mygame.example/?replay=p_ab12%2F<clipId> Playing one back
playReplayFromLocation reads ?replay= off the page. It returns undefined when the page names
no clip, which is the signal to join a room as usual:
import { joinRoom, playReplayFromLocation } from 'irtio';
const clip = await playReplayFromLocation(schema);
const room = clip ? clip.room : await joinRoom(schema);
startRendering(room); room is an ordinary room. Your render loop, your onAdd handlers and your event subscriptions
all work unchanged, which is the point: a replay that did not run your code would not be a replay
of your game. What differs is that nothing the page writes leaves it, and room.me is a viewer id
that nothing in the clip can equal, so the viewer owns nothing and predicts nothing.
clip.playback is the playback head, for a scrubber or a speed control:
clip.playback.speed(0.5);
clip.playback.seek(4);
clip.playback.pause(); To play a specific clip rather than whatever the page names, use playReplay(schema, ref). A ref
is a project/clipId pair, a bare clip id with { project }, or the blob URL.
A clip is pinned to the schema it was recorded against. After a schema change, an older clip
refuses to play with E_SCHEMA_MISMATCH rather than rendering a game that looks subtly wrong.
Handle it the way you handle a live schema skew.
The clip landing card
https://irt.io/clip/<project>/<clipId> is a small landing page for a clip, so a link posted in a
chat that renders Open Graph tags shows something rather than a bare URL. It is a card, not a
player: it never loads clip data, and it looks the same whether or not the clip exists, because a
clip id is unguessable by design and a page that reacted to a real one would say which ids are
real.
Set your project’s public URL to give the card somewhere to send people:
curl -X PATCH https://irt.io/v1/projects/$PROJECT_ID
-H "authorization: Bearer $IRT_TOKEN"
-H 'content-type: application/json'
-d '{"name":"My game","siteUrl":"https://mygame.example"}' name is required by the route, so send the project’s current name alongside the URL. Send "siteUrl": null to clear it.
With a site URL set, the card offers a “Watch in game” link to https://mygame.example/?replay=<project>/<clipId>, which is the link replayShareLink builds.
Without one, the card says only that the clip opens in the game that recorded it. The URL must be https, and it is published to anyone holding a clip link, which is the point of setting it.
Sharing your own game’s link directly is still the better share. The landing card is for the case where a clip id travels on its own.
The badge
@irtio/badge is a small “made with irt.io” corner badge. It is opt-in: importing the package
registers nothing, joinRoom has no badge option, and mountBadge() is the only thing that puts
one on a page.
import { mountBadge } from '@irtio/badge';
mountBadge();
mountBadge({ corner: 'bottom-left' }); The badge links to irt.io and carries a close button. Closing it hides the badge for the rest of
the browser session, so a player who dismisses it does not meet it again on the next room. Style
it with the --irt-badge-* custom properties:
irt-badge {
--irt-badge-bg: rgba(0, 0, 0, 0.5);
--irt-badge-fg: #fff;
--irt-badge-gap: 20px;
} mountBadge returns a handle with destroy(), and returns undefined where there is no document
to mount into, so it is safe to call during server rendering.
Related: Replays