Screenshot API for Gaming & Esports: Tournament Pages, Game Asset Previews, and Stream Monitoring
Screenshot API for Gaming & Esports: Tournament Pages, Game Asset Previews, and Stream Monitoring
Gaming and esports have some of the most dynamic, real-time web content in any industry. Tournament brackets update as matches conclude. Game patch notes go live during peak play hours. Streaming thumbnails change every few minutes. Community wikis get vandalized and need monitoring. Game storefronts generate preview cards for hundreds of titles.
All of these are screenshot problems. This guide covers four patterns used by game studios, esports platforms, and streaming infrastructure teams.
Pattern 1: Tournament Bracket Capture
Esports platforms display live tournament brackets that update as matches conclude. Screenshot captures at key moments — end of each match, end of each round, end of tournament — create a visual history of the event. These are used for highlight reels, social posts, and official records.
import httpx
import asyncio
from datetime import datetime
from pathlib import Path
SCREENSHOT_API = "https://hermesforge.dev/api/screenshot"
API_KEY = "your_api_key"
async def capture_bracket_state(
tournament_id: str,
bracket_url: str,
event_label: str, # e.g. "quarterfinals_complete", "grand_final"
client: httpx.AsyncClient,
) -> dict:
"""
Capture the current state of a tournament bracket.
full_page=True: bracket pages are often tall — capture the entire tree.
delay=2000: bracket components often use React/Vue with animated transitions.
width=1920: esports brackets are designed for desktop viewing.
"""
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
out_dir = Path(f"./tournament-captures/{tournament_id}")
out_dir.mkdir(parents=True, exist_ok=True)
dest = out_dir / f"{timestamp}_{event_label}.png"
resp = await client.get(
SCREENSHOT_API,
params={
"url": bracket_url,
"width": 1920,
"height": 1080,
"format": "png",
"full_page": "true",
"delay": 2000,
"block_ads": "true",
},
headers={"X-API-Key": API_KEY},
timeout=40.0,
)
resp.raise_for_status()
dest.write_bytes(resp.content)
print(f"[{tournament_id}] Captured: {event_label} → {dest} ({len(resp.content):,} bytes)")
return {
"tournament_id": tournament_id,
"event": event_label,
"path": str(dest),
"captured_at": timestamp,
"size": len(resp.content),
}
# Called by tournament management system after each match result is confirmed
async def on_match_completed(tournament_id: str, bracket_url: str, round_name: str, match_number: int):
async with httpx.AsyncClient() as client:
label = f"{round_name}_match{match_number:02d}"
result = await capture_bracket_state(tournament_id, bracket_url, label, client)
return result
# Scheduled capture at tournament milestones
async def capture_tournament_milestones(tournament_id: str, bracket_url: str):
"""Full tournament capture: one screenshot per milestone for the record."""
milestones = [
"tournament_start",
"groups_complete",
"quarterfinals_complete",
"semifinals_complete",
"grand_final_complete",
]
async with httpx.AsyncClient() as client:
for milestone in milestones:
await capture_bracket_state(tournament_id, bracket_url, milestone, client)
# In production: trigger this from your bracket state machine,
# not a sequential loop. Each capture happens when the state actually changes.
await asyncio.sleep(0.5) # Brief pause between captures
The width=1920 setting is important for esports brackets — these pages are designed for large-screen desktop viewing. A 1280px capture will trigger responsive breakpoints that collapse columns and hide data.
Pattern 2: Game Asset Preview Generation
Game storefronts, wikis, and databases display preview thumbnails for thousands of games. Many games have their own web presence — official site, Steam page, Epic Games Store page — and a fresh screenshot provides a current visual that's more informative than a logo.
// game-preview-generator.js
// Generates consistent preview thumbnails for game database entries
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const SCREENSHOT_API = "https://hermesforge.dev/api/screenshot";
const API_KEY = process.env.SCREENSHOT_API_KEY;
const PREVIEW_DIR = process.env.PREVIEW_DIR || "/var/data/game-previews";
/**
* Game pages: Steam, Epic, official sites.
* These are among the most JS-heavy pages on the web.
* Video embeds, 3D model viewers, screenshot carousels all need render time.
*
* delay=3000: Generous delay for Steam/Epic pages with lazy-loaded media.
* clip: Capture above-the-fold hero section only (most impactful visually).
*/
async function generateGamePreview(gameId, pageUrl, source = "official") {
const urlHash = crypto.createHash("sha256").update(pageUrl).digest("hex").slice(0, 8);
const filename = `${gameId}_${source}_${urlHash}.webp`;
const dest = path.join(PREVIEW_DIR, filename);
if (fs.existsSync(dest)) {
return { gameId, path: dest, cached: true };
}
const params = new URLSearchParams({
url: pageUrl,
width: "1280",
height: "720",
format: "webp",
full_page: "false",
delay: "3000", // Steam pages are notoriously slow to fully render
block_ads: "true",
});
const resp = await fetch(`${SCREENSHOT_API}?${params}`, {
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(45000),
});
if (!resp.ok) {
throw new Error(`Preview failed for ${gameId} (${source}): HTTP ${resp.status}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
fs.mkdirSync(PREVIEW_DIR, { recursive: true });
fs.writeFileSync(dest, buffer);
return { gameId, path: dest, cached: false, size: buffer.length };
}
/**
* Batch generate previews for a game catalog.
* Each game may have multiple source URLs (official, Steam, Epic).
* Only one preview per game needed; try sources in priority order.
*/
async function generateCatalogPreviews(games, concurrency = 3) {
const results = [];
for (let i = 0; i < games.length; i += concurrency) {
const batch = games.slice(i, i + concurrency);
const batchResults = await Promise.allSettled(
batch.map(async (game) => {
// Try sources in order; return first success
for (const { url, source } of game.urls) {
try {
return await generateGamePreview(game.id, url, source);
} catch (err) {
console.warn(` ${game.id} (${source}) failed: ${err.message}`);
}
}
throw new Error(`All sources failed for ${game.id}`);
})
);
results.push(...batchResults);
const succeeded = batchResults.filter((r) => r.status === "fulfilled").length;
console.log(`Batch ${Math.ceil((i + concurrency) / concurrency)}: ${succeeded}/${batch.length} ok`);
}
return results;
}
Pattern 3: Stream Thumbnail Monitoring
Streaming platforms display live thumbnails for active streams. A screenshot taken every few minutes captures the current stream state — useful for content moderation, clip generation metadata, and stream health monitoring. A drastically different thumbnail from one interval to the next can signal a stream interruption or content violation.
// stream-monitor.ts
// Periodic screenshot capture for live stream health monitoring
const SCREENSHOT_API = "https://hermesforge.dev/api/screenshot";
const API_KEY = process.env.SCREENSHOT_API_KEY!;
interface StreamConfig {
streamerId: string;
streamUrl: string; // The stream page URL (Twitch, YouTube, etc.)
checkIntervalMs: number;
}
interface StreamSnapshot {
streamerId: string;
capturedAt: string;
path: string;
sizeBytes: number;
anomaly: string | null;
}
class StreamMonitor {
private history = new Map<string, number[]>(); // streamerId → recent sizes
async captureSnapshot(config: StreamConfig): Promise<StreamSnapshot> {
const capturedAt = new Date().toISOString();
const filename = `${config.streamerId}_${capturedAt.replace(/[:.]/g, "-")}.jpg`;
const outputPath = `/var/data/stream-captures/${config.streamerId}/${filename}`;
// JPEG for stream thumbnails: smaller files, good enough quality for monitoring
const params = new URLSearchParams({
url: config.streamUrl,
width: "1280",
height: "720",
format: "jpeg",
full_page: "false",
delay: "2000", // Stream player needs time to buffer first frame
block_ads: "true",
});
const resp = await fetch(`${SCREENSHOT_API}?${params}`, {
headers: { "X-API-Key": API_KEY },
signal: AbortSignal.timeout(30000),
});
if (!resp.ok) {
throw new Error(`Capture failed for ${config.streamerId}: HTTP ${resp.status}`);
}
const buffer = Buffer.from(await resp.arrayBuffer());
require("fs").mkdirSync(require("path").dirname(outputPath), { recursive: true });
require("fs").writeFileSync(outputPath, buffer);
const anomaly = this.detectAnomaly(config.streamerId, buffer.length);
return {
streamerId: config.streamerId,
capturedAt,
path: outputPath,
sizeBytes: buffer.length,
anomaly,
};
}
private detectAnomaly(streamerId: string, currentSize: number): string | null {
const history = this.history.get(streamerId) || [];
if (history.length < 3) {
// Not enough history to compare
this.history.set(streamerId, [...history, currentSize].slice(-10));
return null;
}
const avgSize = history.reduce((a, b) => a + b) / history.length;
const ratio = currentSize / avgSize;
this.history.set(streamerId, [...history, currentSize].slice(-10)); // Keep last 10
if (ratio < 0.5) {
return `SIZE_DROP_${Math.round((1 - ratio) * 100)}pct`; // Stream offline, black screen, or frozen
}
if (ratio < 0.75) {
return `SIZE_REDUCED`; // Possible stream quality drop or static screen
}
return null;
}
async monitorStream(config: StreamConfig): Promise<void> {
console.log(`Starting monitor: ${config.streamerId} (every ${config.checkIntervalMs / 1000}s)`);
while (true) {
try {
const snapshot = await this.captureSnapshot(config);
if (snapshot.anomaly) {
console.warn(`[${snapshot.streamerId}] ANOMALY: ${snapshot.anomaly} at ${snapshot.capturedAt}`);
// In production: trigger alert, escalate to moderation queue
}
} catch (err) {
console.error(`[${config.streamerId}] Capture error:`, err);
}
await new Promise((r) => setTimeout(r, config.checkIntervalMs));
}
}
}
Pattern 4: Game Wiki and Community Site Monitoring
Game wikis and community forums are high-traffic, frequently vandalized. A screenshot monitoring pipeline captures the current state of high-value pages — game home pages, patch notes, character pages — and flags visual anomalies for moderator review.
import httpx
import hashlib
from pathlib import Path
from datetime import datetime
SCREENSHOT_API = "https://hermesforge.dev/api/screenshot"
API_KEY = "your_api_key"
# High-value pages to monitor for visual changes
WIKI_PAGES = [
{"id": "game_homepage", "url": "https://wiki.yourgame.com/Main_Page"},
{"id": "patch_notes_latest", "url": "https://wiki.yourgame.com/Patch_Notes/Latest"},
{"id": "char_meta_tier_list", "url": "https://wiki.yourgame.com/Tier_List"},
]
def capture_wiki_page(page_id: str, url: str) -> dict:
"""Synchronous capture for scheduled monitoring jobs."""
with httpx.Client(timeout=30) as client:
resp = client.get(
SCREENSHOT_API,
params={
"url": url,
"width": 1280,
"height": 900,
"format": "png",
"full_page": "true",
"delay": 1000,
"block_ads": "true",
},
headers={"X-API-Key": API_KEY},
)
resp.raise_for_status()
timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
out_dir = Path(f"./wiki-monitor/{page_id}")
out_dir.mkdir(parents=True, exist_ok=True)
dest = out_dir / f"{timestamp}.png"
dest.write_bytes(resp.content)
# Content hash for change detection — if hash changes, page changed
content_hash = hashlib.md5(resp.content).hexdigest()[:12]
return {
"page_id": page_id,
"path": str(dest),
"size": len(resp.content),
"hash": content_hash,
"captured_at": timestamp,
}
def run_wiki_monitor(pages: list[dict], state_file: str = "./wiki-monitor-state.json") -> list[dict]:
"""
Run one monitoring pass. Compare against last known state.
Flag pages where content hash changed since last check.
"""
import json
# Load previous state
try:
with open(state_file) as f:
last_state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
last_state = {}
results = []
new_state = {}
for page in pages:
try:
capture = capture_wiki_page(page["id"], page["url"])
prev_hash = last_state.get(page["id"])
changed = prev_hash is not None and capture["hash"] != prev_hash
if changed:
print(f"[CHANGED] {page['id']}: {prev_hash} → {capture['hash']}")
# In production: notify moderators, open review ticket
new_state[page["id"]] = capture["hash"]
results.append({**capture, "changed": changed, "prev_hash": prev_hash})
except Exception as e:
results.append({"page_id": page["id"], "error": str(e)})
# Save new state
with open(state_file, "w") as f:
json.dump(new_state, f)
return results
if __name__ == "__main__":
results = run_wiki_monitor(WIKI_PAGES)
changed = [r for r in results if r.get("changed")]
errors = [r for r in results if "error" in r]
print(f"Monitored {len(results)} pages. {len(changed)} changed, {len(errors)} errors.")
The content hash approach is useful for wiki monitoring because it avoids storing full pixel diffs for every check. A hash change is a signal to review the actual screenshot; it doesn't tell you what changed, only that something did. For high-confidence change detection, perceptual hash (pHash) is more robust than MD5 of raw PNG bytes — compression artifacts and minor layout shifts won't produce false positives with pHash.
Common Gaming & Esports Patterns
| Use Case | Width | delay | full_page | Format | Notes |
|---|---|---|---|---|---|
| Tournament brackets | 1920 | 2000ms | true | PNG | Wide viewport prevents responsive collapse |
| Game store previews | 1280 | 3000ms | false | WebP | Steam/Epic are slow — budget extra time |
| Stream thumbnails | 1280 | 2000ms | false | JPEG | Smaller files OK for monitoring use |
| Wiki monitoring | 1280 | 1000ms | true | PNG | Lossless for accurate hash comparison |
The key gaming-specific consideration is viewport width. Esports and gaming content is overwhelmingly designed for desktop viewers. Using a 768px or 1024px viewport on a tournament bracket page will trigger mobile breakpoints and either collapse content or hide it behind accordion menus. Always use 1280px or wider for gaming content.
Getting Started
All examples use the Hermes Screenshot API. Get an API key at /api/keys — verify your email and your key is active immediately.
The free tier covers hundreds of screenshots per day, sufficient for small tournament platforms and single-game wiki monitoring. For high-frequency stream monitoring or large game catalogs, contact for rate limit increases.