Enjoy 3 months of Shopify for $1/month ✨

Osu Replay Viewer | HD 2024 |

osu replay viewer, osu! replay player, .osr player, osu replay analyzer, osu replay visualizer, frame-step osu, replay compare osu

If you want this expanded into app store copy, a longer landing page, or actual UI mockups, tell me which and I’ll produce it.


Before it was a creative tool, the replay viewer was the bouncer.

In a game driven by competitive leaderboards, cheating is a constant specter. The replay viewer is the primary tool for the "verification" process. When a player sets an incredible score on a map that seems physically impossible, the community descends upon the replay file.

"We look for 'snapping'," explains one long-time community moderator. "Human movement is fluid. If the cursor snaps from point A to point B instantly without any travel time, or if it follows an inhumanly perfect curve, the replay viewer exposes it."

The viewer allows investigators to slow gameplay down to 0.25x speed, zoom in on specific circles, and analyze the "timewarp" (hit timing) window. It is the courtroom where allegations of hacking are tried and sentenced.

A lightweight tool to play back osu! replays (.osr) with frame-by-frame controls, hit visualization, score/stats overlay, and basic analysis tools to help players review and improve their runs.

If you want to quickly view an .osr without coding: → Use osu! itself (F2 → Open replay folder) or osu!replay-viewer on GitHub Pages.

If you want to build your own: → Start with Rust/Go parser + egui/SDL2 for desktop, or TypeScript + Canvas for web.

Would you like a detailed guide on implementing the LZ4 decompression or the cursor rendering logic for a specific platform?

Mastering Your Gameplay: The Ultimate Guide to osu! Replay Viewers

An osu! replay viewer is an essential tool for any player looking to analyze their performance, share legendary plays, or catch cheaters in the act. While the standard game client offers basic playback, advanced third-party viewers provide features like frame-by-frame analysis, scrub bars, and even browser-based rendering. Core Features of Advanced Replay Viewers

Standard osu! replays are stored as .osr files which contain metadata like hit counts and time-series data of cursor movements and keypresses. Modern viewers expand on this data to offer:

Detailed Analysis: Tools like Rewind allow for pixel-perfect aim analysis to see exactly where you over-aimed or under-aimed.

Enhanced Playback: Unlike the stable client, advanced viewers often include a scrub bar to jump to any part of the song and the ability to change playback speed from 0.25x to 4.0x.

Visual Customization: Many viewers let you instantly swap skins or toggle visual mods like "Hidden" during playback to better see what happened during a play. Top osu! Replay Viewer Tools

Depending on whether you want deep analysis or just a quick way to share a video, different tools excel: Replay · wiki - Gameplay - osu!

<!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)  (frame.timeMs - lastClickTime) <= 350) 
                        combo++;
                     else 
                        combo = 1;
if (combo > bestCombo) bestCombo = combo;
                    lastClickTime = frame.timeMs;
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)  framesArray.length === 0) 
                // fallback demo
                generateDemoReplay();
                return;
replayFrames = [...framesArray].sort((a,b)=> a.timeMs - b.timeMs);
            totalDuration = durationMs > 0 ? durationMs : (replayFrames[replayFrames.length-1]?.timeMs
// 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 = obj.replayData;
                    duration = obj.durationMs  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>

The osu! replay viewer landscape has evolved from simple in-game playback to sophisticated third-party tools that offer pixel-perfect analysis. While the standard client allows you to save and watch scores, external programs like Rewind and Circleguard have become essential for players serious about improvement. Top Replay Viewers & Analyzers

Rewind (Overall Best): Acts like a "YouTube player" for your .osr files. It features a scrub bar, instant skin swapping, and the ability to analyze aim pixel by pixel.

Circleguard: Highly technical tool focused on cheat detection and raw data analysis. It provides frame-by-frame movement and unstable rate (UR) breakdowns.

osu!lazer (Built-in): The modern official client includes a vastly improved replay system with playback speed controls and a scrub bar, though it still lacks deep "pixel-level" analysis.

Web-Based Viewers: Sites like replayviewer.com allow you to watch replays in a browser or on mobile without installing the game. Key Features for Improvement Replay 2.0 · ppy osu · Discussion #21729 - GitHub

// Using ReplayUnpacker (JS library)
const reader = new ReplayReader(arrayBuffer);
const replay = reader.parse();

// Get cursor positions over time replay.frames.forEach(frame => drawCursor(frame.x, frame.y); if (frame.keys & 1) drawTap('M1'); ); osu replay viewer

Best for: If you built this viewer yourself and are releasing it.

Title: I built a lightweight osu! replay viewer for the community 🏸

Body: Hi r/osugame! I'm a developer and an osu! addict. I got frustrated with how heavy it was to just quickly check a replay file, so I spent the last few weeks building my own osu! Replay Viewer.

Features:

It’s open source and free to use. I’d love some feedback on the UI and performance. Let me know if it breaks on any weird map mods!

[Download Link / Demo]


💡 Tips for engagement:

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)?

Mastering the Osu! Replay Viewer: A Complete Guide to Improving Your Gameplay

Whether you’ve just set a new "Personal Best" on a grueling 7-star map or you’re trying to figure out why you keep clicking early on technical sliders, the osu! replay viewer is your most powerful tool for improvement.

In osu!, progress isn’t just about clicking circles; it’s about analyzing your mistakes. Here is everything you need to know about using the replay viewer to its full potential. What is the Osu! Replay Viewer?

The osu! replay viewer is a built-in feature that records every cursor movement, keypress, and hit result during a play. It allows players to watch their performances back from the game client, providing a "spectator" perspective of their own skills. How to Access Your Replays

Local Rankings: After finishing a map, your score is saved locally. Click on the score in your local leaderboard to watch the replay.

Online Rankings: If you have an active internet connection and the score is in your Top 1,000, you can download the replay from the song selection screen by right-clicking the score. osu replay viewer, osu

Exporting: Pressing F2 in the results screen saves the replay as an .osr file in your osu!/Replays folder. Why You Should Watch Your Replays

Many players finish a map and immediately move to the next one. However, the best players in the world spend significant time in the replay viewer. 1. Identifying "Bad Habits"

Are you consistently aiming to the left of notes? Do you tend to "notelock" on fast streams? Watching a replay with Smoke (activated by holding the 'C' key or a specific mouse button) or simply observing the cursor path helps you see mechanical errors that are invisible while you're focused on playing. 2. Analyzing Unstable Rate (UR)

The replay viewer allows you to see the hit error bar at the bottom of the screen. By watching your replay, you can see if your UR is high because you’re rushing the beat or if you’re reacting too slowly to high-approach rate (AR) circles. 3. Studying Top Players

The replay viewer isn’t just for your own plays. You can download replays from legends like Mrekk, WhiteCat, or Vaxei. Watching their cursor movements in slow motion is a masterclass in efficiency and flow. Advanced Features of the Viewer Speed Control

While watching a replay, you can use the following keys to adjust the experience: Spacebar: Pause/Play. F: Toggle the scoreboard. H: Toggle the interface (clean view).

Speed Adjustment: While the standard client doesn't natively allow frame-by-frame scrubbing, using mods like Lazer (the next-gen osu! client) gives you a seek bar and variable playback speeds. The "osu!lazer" Difference

If you want the ultimate replay viewer experience, try osu!lazer. It features: A functional seek bar (jump to any part of the song). Detailed hit diagrams.

The ability to watch multiple replays simultaneously to compare different runs. How to Share Your Replays

The .osr file format is tiny because it doesn't contain video; it only contains data points. To share your play on YouTube or Reddit:

Recording Software: Use OBS (Open Broadcaster Software) to record the replay viewer while it plays.

Rendering Tools: Community tools like ordr allow you to upload an .osr file to a server, which then renders a high-quality video for you and sends you a link. Conclusion

The osu! replay viewer is more than just a way to admire your high scores. It is a diagnostic tool that bridges the gap between a "hardstuck" player and a top-tier performer. By regularly reviewing your missed notes and timing errors, you turn every failure into a lesson.

Next time you see that "X" on a map, don't just restart—watch the replay and find out why.

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.

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. Before it was a creative tool, the replay

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

Title: "Relive the Magic: A Guide to Using an osu Replay Viewer"

Introduction

osu! is a popular rhythm game that has captured the hearts of millions of players worldwide. With its vast array of beatmaps, challenging gameplay, and strong community, it's no wonder why osu! remains one of the most beloved music games out there. For those who have spent countless hours playing osu!, you might have wondered what it would be like to relive some of your favorite moments or analyze your gameplay in detail. This is where an osu replay viewer comes in.

What is an osu Replay Viewer?

An osu replay viewer is a tool that allows you to watch replays of your osu! gameplay. These replays can be generated from files saved by the osu! client or from online sources. With a replay viewer, you can relive your past gameplay experiences, analyze your techniques, and even learn from your mistakes.

Benefits of Using an osu Replay Viewer

So, why would you want to use an osu replay viewer? Here are a few benefits:

How to Use an osu Replay Viewer

Using an osu replay viewer is relatively straightforward. Here's a step-by-step guide:

Popular osu Replay Viewers

Here are some popular osu replay viewers:

Conclusion

An osu replay viewer is a powerful tool that allows you to relive your favorite osu! moments, analyze your gameplay, and improve your skills. Whether you're a seasoned pro or a casual player, using a replay viewer can enhance your osu! experience. So why not give it a try? Download a replay viewer, load up your favorite replay file, and relive the magic of osu!

Additional Tips and Resources

While functional, the native osu replay viewer has blind spots. It does not show:

For serious players, these limitations necessitate third-party tools.

Tracking 2900+ Couriers Worldwide