Example: -123|256|192|5 (negative w means absolute time)
Watching passively is fun, but active analysis yields results. Here is a training protocol using the osu replay viewer.
For those who want to analyze replays on the go or share them with friends:
The osu replay viewer is more than a novelty—it is a training laboratory. Whether you use the native F2 menu for a quick rewatch or dive into Gink’s heatmaps for deep statistical insight, every second spent analyzing is a second spent improving.
Your action plan today:
By mastering the osu replay viewer, you are no longer just playing—you are practicing with intent. Now go break your top score.
Do you use a different replay tool? Do you have a unique analysis method? Share your thoughts in the osu! community forums. Happy clicking!
The Art of the Playback: The Role of the Replay Viewer in osu!
In the high-speed world of osu!, where players click, slide, and spin to the rhythm of frantic beats, the action often moves too fast for the human eye to fully process in real-time. This is where the osu! replay viewer becomes an essential bridge between raw gameplay and meaningful analysis. Far from being a simple recording feature, the replay viewer serves as a critical tool for self-improvement, community engagement, and the verification of elite-level skill. A Tool for Technical Mastery
For the competitive player, the replay viewer is a digital mirror. Because osu! requires millisecond precision, players often don't realize why they "missed" a note during the heat of a song. By watching a replay, a player can slow down the footage to identify specific mechanical flaws—perhaps their cursor arrived too early, or their tapping hand fell out of sync with a complex rhythm. This "VOD review" process is the cornerstone of moving from an intermediate level to the global rankings. Preservation and Community
Beyond personal growth, the replay viewer acts as the game’s historical archive. osu! is famous for its "god-tier" plays—moments where a human performs a feat that seems mathematically impossible. The ability to export and share these replay files (.osr) allows the community to celebrate these milestones. These files are the lifeblood of osu! YouTube channels and Twitch highlights, turning a solo rhythm game into a spectator sport. Without the viewer, the legendary performances of players like Shigetora or Mrekk would exist only as hearsay rather than documented history. Integrity and Fair Play
In a free-to-play online game, maintaining a level playing field is a constant battle. The replay viewer is a primary weapon in the community’s fight against cheating. Because the viewer tracks cursor movement frame-by-frame, "replay editors" and "anti-cheat specialists" can analyze the data for unnatural smoothness or pixel-perfect snaps that suggest the use of aimbots or macros. In this sense, the replay viewer isn't just a feature; it’s a safeguard for the game’s competitive integrity. Conclusion
The osu! replay viewer is more than a way to watch a cursor dance across a screen. It is a diagnostic lab for the dedicated, a cinema for the fans, and a courtroom for the skeptics. By capturing the fleeting movements of a high-speed performance, it ensures that every click is remembered and every achievement is earned.
To help me tailor this essay or provide more info, let me know: Is this for a school assignment or a blog post?
Should I include a section on third-party tools (like Danser or Rewind)?
Title: Design and Implementation of an OSU Replay Viewer osu replay viewer
Abstract: OSU is a popular rhythm game with a vast online community, where players can record and share their gameplay replays. An OSU replay viewer is a tool that allows users to visualize and analyze these replays. This paper presents the design and implementation of an OSU replay viewer, which can playback, analyze, and visualize OSU game replays.
Introduction: OSU is a free-to-play rhythm game developed by Osu! Team, where players tap, slide, and spin to the beat of various songs. The game has a large online community, with millions of users worldwide. One of the key features of OSU is its replay system, which allows players to record and share their gameplay. However, the built-in replay viewer has limitations, and players often rely on third-party tools to analyze and visualize their replays. This paper aims to design and implement an OSU replay viewer that can playback, analyze, and visualize OSU game replays.
Related Work: Several OSU replay viewers and analysis tools exist, but they often lack features, have limited accuracy, or are no longer maintained. Some popular alternatives include:
Design and Implementation: The proposed OSU replay viewer is built using C# and Windows Presentation Foundation (WPF). The tool consists of three main components:
Features:
Implementation Details: The replay parser uses a custom-designed file format parser to extract data from OSU replay files. The playback engine utilizes WPF's graphics capabilities to render the replay on a graphical interface. The analysis and visualization module uses statistical algorithms and data visualization techniques to provide insights into the replay data.
Evaluation: The proposed OSU replay viewer was tested with a variety of OSU replays, demonstrating its ability to accurately playback and analyze gameplay data. User feedback and testing results show that the tool is effective in providing valuable insights into gameplay patterns and accuracy.
Conclusion: This paper presents the design and implementation of an OSU replay viewer, which provides a comprehensive tool for analyzing and visualizing OSU game replays. The proposed tool has the potential to benefit the OSU community by providing players with a deeper understanding of their gameplay and helping them improve their skills.
Future Work:
Please let me know if you want me to change or add anything!
Here is a possible BibTeX for this draft:
@miscosu-replay-viewer,
author = Your Name,
title = Design and Implementation of an OSU Replay Viewer,
year = 2023,
note = Draft,
An osu! replay viewer is a specialized tool used to analyze, share, or convert gameplay data stored in files without necessarily running the full osu! game client
. These viewers serve diverse needs, from professional-grade analysis to simple video rendering for social media. Types of osu! Replay Viewers
osu! replay viewer is a tool or feature used to watch, analyze, and share recorded gameplay from the rhythm game
. These tools range from built-in game functions to advanced third-party software designed for deep performance analysis or video rendering. 1. Built-in osu! Replay Features By mastering the osu replay viewer, you are
The official client includes native ways to manage and view replays: Saving Replays
on the ranking screen immediately after finishing a map to save the play as a Viewing Local Records
: Access previously saved replays by selecting a beatmap and switching the leaderboard view to Local Ranking Watching Top Plays
: If you are logged in, you can watch top 1,000 global leaderboard replays directly by clicking the player's name on the beatmap screen. Keyboard Shortcuts to quickly rewatch a replay. Press to hide the replay HUD for a cleaner view. 2. Popular Third-Party Replay Viewers & Analyzers
For deeper analysis without launching the full game, players use specialized external tools: GitHub - nahkd123/osu-replay-viewer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>osu! replay viewer · live visualization</title>
<style>
*
box-sizing: border-box;
user-select: none; /* smoother drag/scrub interactions */
body
background: linear-gradient(145deg, #0a0f1e 0%, #0c1222 100%);
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, 'Roboto', monospace;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 24px;
/* main card */
.viewer-container
max-width: 1300px;
width: 100%;
background: rgba(18, 25, 45, 0.75);
backdrop-filter: blur(2px);
border-radius: 2.5rem;
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* header and replay info */
.header
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
margin-bottom: 1.5rem;
gap: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 0.75rem;
.title
font-size: 1.8rem;
font-weight: 700;
background: linear-gradient(135deg, #ff9a9e, #fad0c4, #fad0c4);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.5px;
.badge
background: #1e2a3e;
padding: 6px 14px;
border-radius: 60px;
font-size: 0.8rem;
font-weight: 500;
color: #b9e6ff;
font-family: monospace;
border: 1px solid #2d4055;
/* two column layout */
.dashboard
display: flex;
flex-wrap: wrap;
gap: 1.8rem;
.visualization-panel
flex: 2;
min-width: 280px;
background: #0b111fcc;
border-radius: 1.8rem;
backdrop-filter: blur(4px);
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.05);
.controls-panel
flex: 1.2;
min-width: 260px;
background: #0b111faa;
border-radius: 1.8rem;
padding: 1rem 1.2rem;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
gap: 1.4rem;
canvas
display: block;
width: 100%;
background: #03060e;
border-radius: 1.5rem;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
cursor: crosshair;
#replayCanvas
width: 100%;
height: auto;
background: radial-gradient(circle at 30% 20%, #141e2c, #010101);
/* slider & time */
.scrub-area
margin-top: 1rem;
input[type="range"]
width: 100%;
height: 4px;
-webkit-appearance: none;
background: #2a3a55;
border-radius: 5px;
outline: none;
input[type="range"]:focus
outline: none;
input[type="range"]::-webkit-slider-thumb
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #ffb347;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 6px #ffaa33;
border: none;
.time-display
display: flex;
justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
color: #bbd9ff;
margin-top: 8px;
.playback-buttons
display: flex;
gap: 12px;
justify-content: center;
margin: 12px 0 8px;
.playback-buttons button
background: #1f2a3e;
border: none;
color: white;
padding: 8px 18px;
border-radius: 40px;
font-weight: bold;
cursor: pointer;
transition: 0.1s linear;
font-family: inherit;
backdrop-filter: blur(4px);
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
.playback-buttons button:hover
background: #ff884d;
transform: scale(0.97);
color: #0a0f1e;
.stats
background: #00000040;
border-radius: 1.2rem;
padding: 0.8rem;
font-size: 0.85rem;
font-family: monospace;
.stat-row
display: flex;
justify-content: space-between;
border-bottom: 1px dashed #2e405b;
padding: 5px 0;
.cursor-status
background: #111a28;
border-radius: 1rem;
padding: 0.8rem;
text-align: center;
.hit-circle
display: inline-block;
width: 12px;
height: 12px;
background: #ff4d6d;
border-radius: 50%;
margin-right: 8px;
box-shadow: 0 0 6px #ff4d6d;
.accuracy
font-size: 1.6rem;
font-weight: 800;
color: #c7f9cc;
kbd
background: #2c3e50;
border-radius: 6px;
padding: 2px 6px;
font-family: monospace;
font-size: 0.7rem;
footer
font-size: 0.7rem;
text-align: center;
margin-top: 1.2rem;
color: #5f7f9e;
.file-zone
background: #0f172ac9;
border-radius: 1.2rem;
padding: 0.7rem;
text-align: center;
border: 1px dashed #3e5a77;
cursor: pointer;
transition: 0.1s;
.file-zone:hover
background: #1a253f;
</style>
</head>
<body>
<div class="viewer-container">
<div class="header">
<span class="title">⌨️ osu! replay viewer · kinetic timeline</span>
<span class="badge">⚡ replay analyzer</span>
</div>
<div class="dashboard">
<!-- canvas visualization area -->
<div class="visualization-panel">
<canvas id="replayCanvas" width="800" height="500" style="width:100%; height:auto; aspect-ratio:800/500"></canvas>
<div class="scrub-area">
<input type="range" id="timelineSlider" min="0" max="100" step="0.1" value="0">
<div class="time-display">
<span>🎵 <span id="currentTimeLabel">0.00</span>s</span>
<span>⏱️ <span id="totalTimeLabel">0.00</span>s</span>
</div>
<div class="playback-buttons">
<button id="playPauseBtn">▶ PLAY</button>
<button id="resetBtn">⟳ RESET</button>
</div>
</div>
</div>
<!-- right panel: stats + replay data -->
<div class="controls-panel">
<div class="file-zone" id="fileUploadZone">
📂 LOAD REPLAY (.json / simulated)<br>
<small style="opacity:0.7">click or drag — demo included</small>
<input type="file" id="replayFileInput" accept=".json" style="display:none">
</div>
<div class="stats">
<div class="stat-row"><span>🎯 Clicks / hits</span><span id="totalHits">0</span></div>
<div class="stat-row"><span>✔️ Max combo (sim)</span><span id="maxCombo">0</span></div>
<div class="stat-row"><span>💥 Accuracy (est.)</span><span id="accuracyStat">0%</span></div>
<div class="stat-row"><span>🖱️ Cursor events</span><span id="cursorEventsCount">0</span></div>
</div>
<div class="cursor-status">
<div><span class="hit-circle"></span> <strong>实时光标轨迹 / 点击事件</strong></div>
<div style="font-size:0.75rem; margin-top:8px;" id="liveCoord">X: --- , Y: ---</div>
<div id="lastAction">⚡ 等待回放</div>
</div>
<div class="stats">
<div class="stat-row"><span>🎮 当前帧点击</span><span id="currentClickFlag">—</span></div>
<div class="stat-row"><span>⏲️ Replay 速率</span><span id="playbackRateDisplay">1.0x</span></div>
</div>
<div style="text-align: center; font-size:0.7rem;">
<kbd>◀</kbd> <kbd>▶</kbd> seek · <kbd>SPACE</kbd> play/pause
</div>
</div>
</div>
<footer>✨ 可视化 replay 关键帧 (光标轨迹 + 点击标记) — 支持自定义 JSON 格式: "replayData": [ "t": ms, "x": 0-800, "y": 0-500, "click": bool ], "durationMs": number 或使用内置范例</footer>
</div>
<script>
(function()
// ---------- canvas elements ----------
const canvas = document.getElementById('replayCanvas');
const ctx = canvas.getContext('2d');
// dimensions fixed to 800x500 (playfield style)
canvas.width = 800;
canvas.height = 500;
// replay data structures
let replayFrames = []; // each: timeMs, x, y, click
let totalDuration = 5000; // ms
let currentTime = 0; // ms
let animationId = null;
let isPlaying = false;
let lastTimestamp = 0;
// UI elements
const timelineSlider = document.getElementById('timelineSlider');
const currentTimeLabel = document.getElementById('currentTimeLabel');
const totalTimeLabel = document.getElementById('totalTimeLabel');
const playPauseBtn = document.getElementById('playPauseBtn');
const resetBtn = document.getElementById('resetBtn');
const totalHitsSpan = document.getElementById('totalHits');
const maxComboSpan = document.getElementById('maxCombo');
const accuracyStatSpan = document.getElementById('accuracyStat');
const cursorEventsCountSpan = document.getElementById('cursorEventsCount');
const liveCoordSpan = document.getElementById('liveCoord');
const lastActionSpan = document.getElementById('lastAction');
const currentClickFlagSpan = document.getElementById('currentClickFlag');
const playbackRateDisplay = document.getElementById('playbackRateDisplay');
// stats accumulators
let totalClicks = 0; // number of clicks in replay
let currentCombo = 0;
let maxComboReached = 0;
let hitAccuracyEstimate = 100; // dummy simulated w/ clicks vs time windows
// helper: update stats from replayFrames
function recomputeStats()
totalClicks = replayFrames.filter(f => f.click === true).length;
// For better simulation: accuracy based on clicking consistency? we simulate "perfect hit ratio" using click density.
// but for visual fun: we assume 95% baseline with a slight variance based on click frequency?
// For cleaner demo: each click is considered a "hit" and we calculate an estimated accuracy relative to beat density?
// better: mock accuracy = min(100, Math.floor(85 + (totalClicks / Math.max(1, replayFrames.length/5)) * 10));
let clickEvents = totalClicks;
let totalFrames = replayFrames.length;
let clickDensity = totalFrames ? (clickEvents / totalFrames) * 100 : 0;
let mockAcc = Math.min(99, Math.floor(78 + clickDensity * 0.4));
if (mockAcc > 98) mockAcc = 96 + (clickEvents % 3);
hitAccuracyEstimate = Math.min(100, Math.max(65, mockAcc));
accuracyStatSpan.innerText = hitAccuracyEstimate + "%";
// compute max combo: consecutive frames with click? but combo is based on hits in rhythm, we use consecutive clicks within time diff < 200ms as combo
let combo = 0;
let bestCombo = 0;
let lastClickTime = -1000;
for (let frame of replayFrames)
if (frame.click)
maxComboReached = bestCombo;
maxComboSpan.innerText = maxComboReached;
totalHitsSpan.innerText = totalClicks;
cursorEventsCountSpan.innerText = replayFrames.length;
// draw the entire scene based on current playback time
function drawVisualization()
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// background gradient
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, '#091222');
grad.addColorStop(1, '#03070f');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw dotted grid (osu! style)
ctx.strokeStyle = '#2a3b55';
ctx.lineWidth = 0.5;
for (let i = 0; i < canvas.width; i += 40)
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(canvas.width, i);
ctx.stroke();
// draw all cursor trail (semi-transparent based on time)
for (let i = 0; i < replayFrames.length; i++)
const frame = replayFrames[i];
if (frame.timeMs > currentTime) continue;
const alpha = 0.25 + (frame.timeMs / totalDuration) * 0.3;
ctx.beginPath();
ctx.arc(frame.x, frame.y, 5, 0, Math.PI*2);
ctx.fillStyle = `rgba(100, 180, 255, $Math.min(0.5, alpha*0.7))`;
ctx.fill();
if (frame.click && frame.timeMs <= currentTime)
ctx.beginPath();
ctx.arc(frame.x, frame.y, 12, 0, Math.PI*2);
ctx.strokeStyle = '#ff6070';
ctx.lineWidth = 2.5;
ctx.stroke();
ctx.beginPath();
ctx.arc(frame.x, frame.y, 5, 0, Math.PI*2);
ctx.fillStyle = '#ff3366cc';
ctx.fill();
// find current interpolated cursor position
let curX = canvas.width/2, curY = canvas.height/2;
let isClickNow = false;
if (replayFrames.length > 0)
let prevFrame = null;
for (let i = 0; i < replayFrames.length; i++)
if (replayFrames[i].timeMs <= currentTime)
prevFrame = replayFrames[i];
else break;
let nextFrame = replayFrames.find(f => f.timeMs > currentTime);
if (prevFrame)
if (nextFrame)
const t = (currentTime - prevFrame.timeMs) / (nextFrame.timeMs - prevFrame.timeMs);
const clampT = Math.min(1, Math.max(0, t));
curX = prevFrame.x + (nextFrame.x - prevFrame.x) * clampT;
curY = prevFrame.y + (nextFrame.y - prevFrame.y) * clampT;
isClickNow = prevFrame.click && (currentTime - prevFrame.timeMs < 50) ? prevFrame.click : false;
if ((nextFrame.click && (nextFrame.timeMs - currentTime) < 30)) isClickNow = true;
else
curX = prevFrame.x;
curY = prevFrame.y;
isClickNow = prevFrame.click && (currentTime - prevFrame.timeMs) < 80;
// also check if any frame exact click within tiny window
const nearClick = replayFrames.find(f => Math.abs(f.timeMs - currentTime) < 45 && f.click);
if (nearClick) isClickNow = true;
// Draw cursor (follow poi)
ctx.shadowBlur = 10;
ctx.shadowColor = '#0af';
ctx.beginPath();
ctx.arc(curX, curY, 14, 0, Math.PI*2);
ctx.fillStyle = isClickNow ? '#ff4d6dc9' : '#ffffffcc';
ctx.fill();
ctx.beginPath();
ctx.arc(curX, curY, 6, 0, Math.PI*2);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.beginPath();
ctx.arc(curX, curY, 3, 0, Math.PI*2);
ctx.fillStyle = '#ffaa55';
ctx.fill();
ctx.shadowBlur = 0;
// show click halo if actively clicking
if (isClickNow)
ctx.beginPath();
ctx.arc(curX, curY, 22, 0, Math.PI*2);
ctx.strokeStyle = '#ff8080';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(curX, curY, 28, 0, Math.PI*2);
ctx.strokeStyle = '#ffa0a0';
ctx.lineWidth = 1;
ctx.stroke();
lastActionSpan.innerHTML = '🔴 CLICK!';
currentClickFlagSpan.innerHTML = '● HIT';
else
lastActionSpan.innerHTML = '⚡ cursor tracking';
currentClickFlagSpan.innerHTML = '○ idle';
liveCoordSpan.innerText = `X: $Math.floor(curX) , Y: $Math.floor(curY)`;
// Draw time progress arc on bottom right
const progress = currentTime / totalDuration;
ctx.font = "bold 14px 'JetBrains Mono'";
ctx.fillStyle = '#ccdeff';
ctx.shadowBlur = 0;
ctx.fillText(`⏵ $(currentTime/1000).toFixed(2)s`, canvas.width-90, 35);
// update slider & time labels
function syncUITime()
timelineSlider.value = (currentTime / totalDuration) * 100;
currentTimeLabel.innerText = (currentTime / 1000).toFixed(2);
totalTimeLabel.innerText = (totalDuration / 1000).toFixed(2);
drawVisualization();
// set current time and update UI, clamp
function setCurrentTime(ms)
currentTime = Math.min(totalDuration, Math.max(0, ms));
syncUITime();
// animation loop (requestAnimationFrame)
let lastFrameTime = 0;
function startAnimation()
if (animationId) cancelAnimationFrame(animationId);
function animate(now)
if (!isPlaying) return;
if (lastFrameTime === 0) lastFrameTime = now;
let delta = Math.min(100, now - lastFrameTime);
if (delta > 0)
let step = delta * 1.0; // 1x speed, can modify
let newTime = currentTime + step;
if (newTime >= totalDuration)
newTime = totalDuration;
setCurrentTime(newTime);
isPlaying = false;
playPauseBtn.innerHTML = '▶ PLAY';
cancelAnimationFrame(animationId);
animationId = null;
lastFrameTime = 0;
return;
setCurrentTime(newTime);
lastFrameTime = now;
animationId = requestAnimationFrame(animate);
lastFrameTime = 0;
animationId = requestAnimationFrame(animate);
function playReplay()
if (isPlaying) return;
if (currentTime >= totalDuration)
setCurrentTime(0);
isPlaying = true;
playPauseBtn.innerHTML = '⏸ PAUSE';
startAnimation();
function pauseReplay()
if (!isPlaying) return;
isPlaying = false;
if (animationId)
cancelAnimationFrame(animationId);
animationId = null;
playPauseBtn.innerHTML = '▶ PLAY';
function togglePlayPause()
if (isPlaying) pauseReplay();
else playReplay();
function resetReplay()
pauseReplay();
setCurrentTime(0);
function loadReplayData(framesArray, durationMs)
// generate built-in demo (smooth circular cursor + clicks)
function generateDemoReplay()
const frames = [];
const duration = 7800;
const steps = 220;
for (let i = 0; i <= steps; i++)
let t = (i / steps) * duration;
let angle = (t / duration) * Math.PI * 4;
let radius = 180;
let centerX = canvas.width/2, centerY = canvas.height/2;
let x = centerX + Math.sin(angle) * radius * (1 + Math.sin(t/700));
let y = centerY + Math.cos(angle * 1.3) * radius * 0.8;
x = Math.min(canvas.width-25, Math.max(25, x));
y = Math.min(canvas.height-30, Math.max(30, y));
let click = false;
if (Math.abs(t - 1200) < 60) click = true;
if (Math.abs(t - 2500) < 50) click = true;
if (Math.abs(t - 3800) < 60) click = true;
if (Math.abs(t - 4900) < 55) click = true;
if (Math.abs(t - 6100) < 70) click = true;
if (Math.abs(t - 7100) < 80) click = true;
frames.push( timeMs: t, x: Math.floor(x), y: Math.floor(y), click );
// extra smooth clicks
replayFrames = frames;
totalDuration = duration;
recomputeStats();
setCurrentTime(0);
syncUITime();
// handle uploaded JSON
function processUploadedJSON(jsonText)
try
const obj = JSON.parse(jsonText);
let frames = null;
let duration = null;
if (obj.replayData && Array.isArray(obj.replayData)) (frames.length ? frames[frames.length-1].timeMs + 200 : 5000);
else if (Array.isArray(obj))
frames = obj;
duration = obj.length ? obj[obj.length-1].timeMs + 200 : 5000;
else
throw new Error("Format error, need replayData array with timeMs, x, y, click");
const validFrames = frames.filter(f => typeof f.timeMs === 'number' && typeof f.x === 'number' && typeof f.y === 'number');
if (validFrames.length === 0) throw new Error("no valid frames");
loadReplayData(validFrames, duration);
lastActionSpan.innerHTML = '📁 自定义 replay 已加载';
catch(e)
alert("Invalid JSON: " + e.message + " — using demo format");
generateDemoReplay();
// event binding
timelineSlider.addEventListener('input', (e) =>
if (isPlaying) pauseReplay();
const percent = parseFloat(e.target.value) / 100;
const newTime = percent * totalDuration;
setCurrentTime(newTime);
);
playPauseBtn.addEventListener('click', togglePlayPause);
resetBtn.addEventListener('click', resetReplay);
document.addEventListener('keydown', (e) =>
if (e.code === 'Space')
e.preventDefault();
togglePlayPause();
if (e.code === 'ArrowLeft')
e.preventDefault();
if (isPlaying) pauseReplay();
setCurrentTime(currentTime - 150);
if (e.code === 'ArrowRight')
e.preventDefault();
if (isPlaying) pauseReplay();
setCurrentTime(currentTime + 150);
);
const uploadZone = document.getElementById('fileUploadZone');
const fileInput = document.getElementById('replayFileInput');
uploadZone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) =>
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) =>
processUploadedJSON(ev.target.result);
fileInput.value = '';
;
reader.readAsText(file);
);
// drag drop
uploadZone.addEventListener('dragover', (e) => e.preventDefault(); uploadZone.style.background = '#1f2e4a'; );
uploadZone.addEventListener('dragleave', () => uploadZone.style.background = ''; );
uploadZone.addEventListener('drop', (e) =>
e.preventDefault();
uploadZone.style.background = '';
const file = e.dataTransfer.files[0];
if(file && file.type === 'application/json')
const reader = new FileReader();
reader.onload = (ev) => processUploadedJSON(ev.target.result);
reader.readAsText(file);
else alert('drop JSON file plz');
);
// initial demo load
generateDemoReplay();
setCurrentTime(0);
playbackRateDisplay.innerText = "1.0x";
)();
</script>
</body>
</html>
Developing a feature for an osu! replay viewer —whether you are contributing to the official client, a third-party tool like
, or building your own—involves enhancing the way players analyze and share their gameplay. Core Feature Ideas
Based on community discussions and current project gaps, here are several high-impact features you could develop:
Replay analyzer improvements · ppy osu · Discussion #31558 - GitHub
The osu! replay viewer is a specialized tool used by the community to watch, analyze, and share gameplay outside of the standard osu! client. It typically handles .osr files, which store movement and timing data rather than actual video. 🕹️ Essential Tools & Features
The "replay viewer" ecosystem includes several distinct types of software:
In-Game Viewer: Standard replays can be viewed in the game by pressing F2 on the results screen to save (osu! wiki).
External Renderers: Tools like osu-replay-viewer on GitHub allow you to view and render replays into video files (MP4) using FFmpeg without launching the full game.
Web-Based Viewers: Platforms like osu!lazer or community projects (e.g., WebOsu) enable replay viewing directly in a browser using HTML5. 📂 How to Manage Replay Files
Replays are small because they only track input data, not video frames. Do you use a different replay tool
File Location: Local replays are stored as .osr files in your osu!/Data/r/ folder.
Sharing: You can send these .osr files to other players; they only need the corresponding beatmap to watch it in their client.
Failed Plays: You can watch a replay of a failed run by pressing F1 on the "Game Over" screen. 🛠️ Advanced Controls & Analysis
Newer versions of the game (especially osu!lazer) offer more granular control for improvement:
Playback Speed: Use W/S keys to speed up or slow down the replay.
Frame Seeking: Use A/D keys to jump forward or backward in time.
Object Tracking: Some external viewers allow you to see exact hit error (UR) and "slider-end" timing to see exactly where you lost accuracy. ⚠️ Common Troubleshooting
Missing Replays: If your local scores disappear, you can sometimes recover them by deleting scores.db in your installation folder and pressing F5 in-game to rebuild the database.
Skinning: Replays will always play back using your currently selected skin, regardless of what skin the original player used. If you tell me more, I can help you: Export a replay to a video format Find a specific tool for high-rank analysis Recover deleted replay data
Replay analyzer improvements · ppy osu · Discussion #31558 - GitHub
By Alex "ClickTheory" Chen
You’ve just set a new personal best on a 7-star Freedom Dive map. Your hands are shaking. The cursor wavers on the “Retry” button—but you don’t click it. Instead, you hover over the small, unassuming button: Watch Replay.
In most rhythm games, replays are a pat on the back. A victory lap. In osu!, they are a confession, a textbook, and occasionally, a courtroom.
The osu! replay viewer is not a feature. It is a culture.
