anten.ir — live sports streaming
Technology recommendations, Scrum, well-tested documented code, heavy SEO work, and a custom professional player for live streams and archives.
- Duration
- Approximate date10 mo
- Sites on one player
- 2
Context
Farakav is an Iranian media-technology company. anten.ir streams live sport — football first [draft] — to an audience that peaks on match nights and watches mostly on mid-range Android phones over mobile data [draft]. heyatonline.ir streams religious ceremonies live, peaking every Muharram [draft], and lets organisers archive and publish their programmes. I joined heyatonline part-time in October 2021 and moved to anten.ir full-time in May 2022, reporting to the frontend lead [draft]. Both frontends were Next.js; the backends spoke SignalR and GraphQL [draft].
Situation. On anten.ir the player was the product, and it was a stock Video.js with a stack of plugins [draft]: it behaved differently on iOS Safari and on cheap Android browsers, live and archive were two different components, and playback failures were invisible — no event reached analytics [draft]. Search was the other gap: match and programme pages were client-rendered lists with duplicate URLs and no structured data, so Google indexed the home page and little else [draft].
On heyatonline the organisers’ panel polled for archive status and drifted, and a set of regulator-driven UI rules (content-classification notices, accessibility) [draft] had to be applied consistently. The constraint that mattered most was the calendar: big matches and ceremonies are fixed dates, and a bad release lands in front of the largest audience of the year.
What I did
- Built one custom player for live and archive. A Video.js-based component with HLS quality switching, DVR seek on live, Persian UI, keyboard shortcuts, picture-in-picture, a “starts at…” pre-match state, and reconnect with backoff on manifest errors [draft]. Every failure fires an event to analytics. Alternatives rejected: raw hls.js (more control, no time for a UI from scratch) and a commercial player (cost, no RTL, no control over iOS quirks) [draft].
- Made the SEO structural, not cosmetic. Match and programme pages moved to server rendering in Next.js, one canonical URL per event, SportsEvent / VideoObject JSON-LD [draft], daily sitemaps, OG images per match, Persian slugs, and Core Web Vitals work on the poster image and fonts [draft]. Alternative rejected: pre-rendering with a headless browser — fragile and a second system to run.
- Wired SignalR into Redux on heyatonline. A middleware turns SignalR events into Redux actions, so archive status (“recording”, “processing”, “published”) updates live without polling; the same pattern later drove anten.ir’s match state [draft]. Shipped the organisers’ panel as a PWA so it installs on their phones [draft].
- Built the design system with the designer. Tokens in Figma mirrored one-to-one in the MUI theme object; I designed some internal pages myself when the designer was the bottleneck (CV).
- Applied the compliance rules once, in components. Notices, classification badges and accessibility rules lived in shared components rather than per page [draft].
- Pushed for tests and written decisions. Component tests with Testing Library, a Playwright smoke test on the player, and written recommendations for RTK Query and the GraphQL client that the team adopted [draft]. Scrum with Jira and Zephyr.
Results (confidence in parentheses)
- One player component serving live and archive on both sites (exact, CV)
- Playback failures went from unmeasured to measured; iOS failure rate down [N %] after the reconnect logic (verify, draft)
- Organic visits to match pages up [N %] within [3 months] of server rendering (verify, draft)
- Archive status on the organisers’ panel: from polling every [30 s] to live (approx, draft)
- [N] concurrent viewers on [the derby] without a player-side incident after the fix below (verify, draft)
- Part-time → full-time inside seven months (exact, CV)
- Duration
- Approximate date10 mo
- Sites on one player
- 2
What went right
- One player, two products. The live/archive split disappeared and every fix landed on both sites at once.
- Server rendering did what it said. New match pages were indexed within hours instead of days [draft].
- The SignalR middleware became a house pattern. Other screens adopted it without help.
- Tokens in Figma = tokens in code. The designer and I stopped arguing about greys. Credit to the designer who kept the Figma file honest.
- Measuring failures changed the conversation. Once the player reported errors, “it works on my phone” stopped ending discussions.
What went wrong
Timeline
[draft — hypothesis] [A derby night, month N]: a player release that changed autoplay behaviour went live the afternoon of the biggest match of the season. iOS Safari blocks autoplay with sound; iOS viewers saw a black frame and no play button. Noticed within minutes through the Telegram support channel; hotfix (muted autoplay + tap-to-unmute) live in [40 minutes].
Causes
- No iOS device in the test matrix; QA and the team tested on Android and desktop.
- The release was scheduled on a match day because the calendar was not part of the release process.
- The player had no feature flag or kill switch; rollback was a full deploy.
- The failure was silent for the first minutes because the “autoplay blocked” error event did not exist yet.
- Browser autoplay policies were known to individuals, not written anywhere.
Impact
iOS viewers lost [N] minutes of the match; support was flooded; the team’s credibility took a hit on the night everyone was watching; a match-day rule was born.
Detection & response: Telegram support → frontend on-call → hotfix branch; then the device matrix and the flag.
Actions
- Match-day and ceremony-day release freeze — frontend lead
- iOS Safari and low-end Android in the Playwright matrix via a device lab [draft] — frontend
- Kill switch and percentage rollout for player changes — frontend
- Player error events, including “autoplay blocked”, to analytics with an alert on rate — frontend + data
- A one-page “browser media policies” doc
Second retro item [draft]: the compliance rules on heyatonline arrived after the first panel screens were designed, so several pages were rebuilt. Cause: the rule list lived with another team and was requested late. Action: ask for the regulator’s list in week one and build it into the shared components before the first page.
If I went back
- KEEPThe single player component, the SignalR middleware and the Figma-to-theme token mirror
- CHANGESet up the device matrix — a real iPhone and a cheap Android — before touching the player
- CHANGEFeature flag and percentage rollout before the first player change, not after the first incident
- CHANGEPut the match calendar into the release process on day one
- CHANGEAsk for the compliance list before the first screen
Lessons
- A player that cannot report its own failures is not done.
- Structural SEO (server rendering, canonical URLs, structured data) beats every meta-tag trick — and it is a backend-shaped job for a frontend engineer.
- Shared components are where compliance rules belong, so they are applied once.