The pages are in English; this contents, the search and the buttons are in Japanese.
Feeds
日本語 · English
A feed is a game as data: the places of the players and the ball over time, and what happened (the shots, the goals, the whistles), with the sides and the periods. The replay plays it flat or in 3D, with the arena's scoreboard showing its score, period and clock. We host no feed and keep no league's data: the feed is yours. It comes from a file you pick, from text you paste, or from an address you give, which your browser fetches. Where to find a feed lists the feeds known for each sport, our own made-up sample first.
The rules we keep#
- No server or proxy of ours. An address is fetched by the reader's browser, with no cookie and no referrer, and nothing goes anywhere else.
- Nothing leaves the browser. The package keeps nothing: no cache beyond the open page, no storage, nothing copied into the repository. The demo keeps the feed files you drop or choose in your own browser, and nowhere else.
- Names come from the feed, at run time. The package ships no league's or team's names, logos or marks; whatever the feed names is shown as text while the page is open.
- The terms are the source's. A note beside the address field says so: you are responsible for following the terms of the source you enter.
- Tests use answers we made up, never a real request.
The open timeline#
kyogijo's own format is the replay timeline, as JSON: frames (each a time t and its objects, every one an id, a team of home, away, ball, puck or official, and x, y), events (a t, a kind such as pass, shot, goal or whistle, a team and a place), and for a scoreboard periods (each a number, a start, an end, and a length on the board's clock when the replay is re-timed) and clock (down, the time left, or up, the time gone). stoppages (each a start, an end, a reason and the place of the restart: the game clock stands still in each) and penalties (each a team, a start, its minutes and who serves it, by) are read as the replay has them (stoppages, penalties). The sides go in teams (home and away, each an abbr, a name and a color), which readFeed gives back as the game's teams. from names the frame the places are in (nhl-api, opta: frames). A game file, as the example game is, wraps a timeline with its sides, rosters and periods (kind: "kyogijo-game"); both read.
{
"from": "nhl-api",
"frames": [{ "t": 0, "objects": [{ "id": "puck", "team": "puck", "x": 0, "y": 0 }] }, { "t": 30, "objects": [{ "id": "puck", "team": "puck", "x": 80, "y": 4 }] }],
"events": [{ "t": 30, "kind": "goal", "team": "home", "x": 80, "y": 4 }],
"periods": [{ "number": 1, "start": 0, "end": 1200 }],
"teams": { "home": { "abbr": "AAA" }, "away": { "abbr": "BBB" } }
}
Every value is checked as it is read: a frame without a time, an object without a place or of an unknown team, an event without a time, a stoppage that ends before it starts and a penalty without a side or its minutes are left out; text is only ever text, cut short; every list is capped (FEED_LIMITS), so that a broken or hostile answer cannot hold the page.
Read a feed#
@johnmorrisdotca/kyogijo-stadiums/feeds reads an answer into a game: its timeline, whether the feed says it is over (ended), and the players it names. readFeed(json, format?) tells the formats apart by itself (auto), or takes kyogijo or nhl; readFeedText(text) reads text first, as a file or a paste gives it. What was wrong is a FeedError with a kind and a sentence.
import { readFeed } from "@johnmorrisdotca/kyogijo-stadiums/feeds";
const game = readFeed({
frames: [{ t: 0, objects: [{ id: "puck", team: "puck", x: 0, y: 0 }] }, { t: 30, objects: [{ id: "puck", team: "puck", x: 80, y: 4 }] }],
events: [{ t: 30, kind: "goal", team: "home" }],
});
console.log(game.format, game.timeline.frames.length, game.ended);
// kyogijo 2 true
From a file or a paste#
A browser that cannot reach a feed can still play it: the reader saves the answer and picks it as a file, or pastes it.
import { mountKyogijoTurn } from "@johnmorrisdotca/kyogijo-stadiums/turn";
import { readFeedText } from "@johnmorrisdotca/kyogijo-stadiums/feeds";
const view = mountKyogijoTurn(document.querySelector("#rink")!, { sport: "nhl" }, { preset: "broadcast" });
document.querySelector<HTMLInputElement>("#file")!.addEventListener("change", async (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file !== undefined) view.replay(readFeedText(await file.text()).timeline).play();
});
Adapters#
An adapter is a pure function from a feed's own shape to the open timeline, tested on answers we made up. Each is listed here with what it reads.
| Format | Function | Reads |
|---|---|---|
kyogijo | kyogijoFeed | The open timeline, or a game file. |
nhl | nhlPlayByPlay | A play-by-play shaped as the NHL's public one: plays (each with periodDescriptor, timeInPeriod, typeDescKey, sortOrder and details with xCoord, yCoord, eventOwnerTeamId and the player's id), homeTeam and awayTeam (id, abbrev, commonName), rosterSpots and gameState. |
A play-by-play has events and no tracking, so nhlPlayByPlay moves the puck between the places of the plays in a straight line (modelled between the known places), marks each shot, goal and whistle where it was, puts every play on the game's own clock (twenty minutes a period, an overtime of five in the regular season and twenty in the playoffs; the shoot-out is left out, and its goals are not the score's), and names the sides by their abbrev and the players by their names, at run time. ended is true once gameState is OFF or FINAL.
From an address#
Load the /feeds entry once, and every mount's and 3D view's replay has fromUrl: it fetches the address by the reader's browser (no cookie, no referrer, no cache), reads it and plays it. loadFeed(url) does the first two without playing.
import "@johnmorrisdotca/kyogijo-stadiums/feeds";
import { mountKyogijoTurn } from "@johnmorrisdotca/kyogijo-stadiums/turn";
declare const address: string;
const view = mountKyogijoTurn(document.querySelector("#rink")!, { sport: "nhl", crowd: { boards: true } }, { preset: "broadcast" });
try {
const replay = await view.replay.fromUrl(address, { format: "auto" });
console.log(replay.feed.teams);
replay.play();
} catch (error) {
// A FeedError: error.kind is "cors", "http" (with error.status), "format", "address", "too-large", "network" or "empty".
}
Only an http or https address is read (a relative one against the page's), never one with a login in it, and an answer is at most 20 MB.
When the browser cannot read it. Many servers do not let a page of another site read them: they send no Access-Control-Allow-Origin, and the browser refuses the answer (CORS). A page cannot tell that from a server that is down, so the error says both (cors). Then the reader can save the answer and pick it as a file or paste it, or point at a relay of their own that adds the header. We run no relay, and the package never asks for one.
Live#
replay.follow(url, options) follows a feed while its game is played: it asks the address now and then every 15 seconds (every, never less than 10), only while the page is shown, plays each answer that changed (from where the reader was, or at its newest moment when they were there), and stops when the feed says the game is over (ended) or the server says no (403 or 429). After an error it waits twice as long, up to two minutes. stop() stops asking and leaves the replay.
import "@johnmorrisdotca/kyogijo-stadiums/feeds";
import type { KyogijoTurn } from "@johnmorrisdotca/kyogijo-stadiums/turn";
declare const view: KyogijoTurn;
declare const address: string;
const live = view.replay.follow(address, { onState: (state) => console.log(state), onError: (error) => console.log(error.kind) });
// later
live.stop();
followFeed(url, { onGame }) does the asking alone, for a page that plays the game its own way. Nothing runs on a timer while the page is hidden, and one timer at most at any time.
In the demo#

Bring your own feed, in the game section of the demo, takes an address (with Follow live), a file or a paste, and lists where to find a feed beside the field, our made-up sample first; Use fills the field.
Feed files from your computer: drop one or more feed files on the panel or on the view (from Finder, a download folder or a synced Dropbox folder), or choose them; an NHL play-by-play file is read exactly as the public play-by-play answers it, and a kyogijo timeline as it is. The first good one plays; a file that is not a feed is refused by name. Every feed is kept in this browser and listed with its game, its file, its sport, the date and its size, with Play, Remove and Remove all, and the space they take; they are still there after a reload. They go in localStorage while they add up to about 4 MB, and in IndexedDB past that. A whole folder can be dropped from a folder of your own: the files are read one at a time while the panel says how far it is, up to 200 files at once and 200 MB kept in all; more is refused in words. The browser is written to only when a file is dropped, chosen or removed, never on a timer, and nothing leaves it; where the browser keeps nothing (a private window, storage turned off or full) a dropped feed still plays, and the panel says it was not kept.