Soccer Player Tracking

A football match, replayed from telemetry.

Twenty-two players and a ball, sampled 10 times per second from broadcast video. Every dot on the pitch below is a row read from Arc.

Same playback engine, timeline and query layer as the NBA demo, on a different sport, sampling rate and coordinate system.

-- querying Arc for the timeline index...
What you just watched

One match. 929,292 observations.

Twenty-two players and a ball at 10 frames per second for 90-odd minutes. Fewer samples per second than the NBA feed, twice the entities, and a detection flag on every player observation because the positions come from broadcast cameras rather than a fixed camera array.

Observations in Arc
929,292
rows in soccer_tracking.tracking for this match
Frames
40,404
31,047 source frames dropped: pre-kickoff, halftime, dead ball
Sampling rate
10 Hz
one frame every 100 ms
Tracked time
67.2 min
75 contiguous segments
Players observed
32
of 36 in the squad lists, plus the ball
Extrapolated
49%
player observations estimated while off camera (is_detected = false)
Time range
2025-05-17
09:39:11 to 11:32:20 UTC, derived from kickoff + frame
Imported
2026-09-22
by demos/soccer-tracking/ingest.py

How the time axis works

SkillCorner publishes a frame counter at 10 fps and a match clock, but no wall-clock timestamps. Arc's time is the listed kickoff time plus 100 ms per frame, which is strictly monotonic across both halves. The frame number, period and match clock are stored next to it. Frames before kickoff, at halftime and while the ball is out of play carry no observations and are dropped; the segments query finds the gaps that leaves.

Same engine, different sport

The playback engine, timeline, query panel, charts and inspector are the components used by the NBA demo. The sport supplies a renderer, a coordinate system (meters from the pitch center here, feet from a corner there), a clock format and units. Adding another telemetry demo means writing those four things and an importer.

Nearest opponent, computed in Arc
WITH me AS (
  SELECT time, x, y
  FROM tracking
  WHERE match_id = '2017461' AND player_id = 51678
    AND time >= TIMESTAMP '2025-05-17 09:39:11.000' AND time < TIMESTAMP '2025-05-17 09:39:41.000'
),
opp AS (
  SELECT time, player_id, x, y
  FROM tracking
  WHERE match_id = '2017461' AND entity_type = 'player' AND team_id <> 868
    AND time >= TIMESTAMP '2025-05-17 09:39:11.000' AND time < TIMESTAMP '2025-05-17 09:39:41.000'
),
pairs AS (
  SELECT me.time, opp.player_id,
         sqrt(power(me.x - opp.x, 2) + power(me.y - opp.y, 2)) AS dist
  FROM me JOIN opp ON opp.time = me.time
)
SELECT epoch_ms(time) AS t,
       arg_min(player_id, dist) AS nearest_player_id,
       min(dist) AS distance_m
FROM pairs
GROUP BY time
ORDER BY time

Tracking data: SkillCorner Open Data, released under the MIT license. Match 2017461, Melbourne Victory vs Auckland FC, A-League 2024/25, 17 May 2025. Broadcast tracking at 10 fps; players off camera carry extrapolated positions, which the demo flags. Data credit: SkillCorner.

The match was imported once by a one-shot importer that fetches the source files, drops the frames that carry no observations (pre-kickoff, halftime, dead ball), derives a monotonic timestamp from the frame counter, and writes one row per entity per frame into Arc. Every figure on this page is read back from that import.

Arc is an open, SQL-native time-series database for high-volume telemetry. Twenty-two tracked objects reporting ten times a second is the shape of a vehicle fleet, a port, or a warehouse floor.