Skip to content

Latest commit

Β 

History

History
355 lines (256 loc) Β· 16.8 KB

File metadata and controls

355 lines (256 loc) Β· 16.8 KB

LiveCap β€” Bug Report

Last updated: 2026-07-11
Branch: main
Severity levels: πŸ”΄ Critical Β· 🟠 High Β· 🟑 Medium Β· 🟒 Low


Summary

ID Severity Component Title Status
BUG-01 🟠 High VideoPreview.tsx Annotation path mutation breaks React immutability βœ… Resolved
BUG-02 🟠 High App.tsx pauseResumeRecording stale closure in keyboard handler βœ… Resolved
BUG-03 🟠 High App.tsx AudioContext is never closed after recording stops βœ… Resolved
BUG-04 🟠 High App.tsx IndexedDB chunks not cleared on mid-recording crash/error βœ… Resolved
BUG-05 🟑 Medium App.tsx Two independent timers can drift out of sync βœ… Resolved
BUG-06 🟑 Medium App.tsx Webcam failure crashes entire recording start flow βœ… Resolved
BUG-07 🟑 Medium VideoPreview.tsx Canvas cursor always crosshair, even when not recording βœ… Resolved
BUG-08 🟑 Medium VideoPreview.tsx setWebcamPosition prop is passed but never called πŸ”„ Open
BUG-09 🟑 Medium App.tsx MediaRecorder ref not nulled on error path βœ… Resolved
BUG-10 🟑 Medium VideoPreview.tsx No touch event support for annotation drawing βœ… Resolved
BUG-11 🟑 Medium App.tsx No loading indicator while IndexedDB chunks are assembled βœ… Resolved
BUG-12 🟒 Low SettingsPanel.tsx SelectControl and CheckboxControl use any type βœ… Resolved
BUG-13 🟒 Low App.tsx Undo history (annotationHistory) is write-only β€” redo unreachable βœ… Resolved
BUG-14 🟒 Low types.ts / VideoPreview.tsx webcamBackground setting stored but never applied to canvas βœ… Resolved
BUG-15 🟒 Low Controls.tsx Timer does not show paused state visually βœ… Resolved
BUG-16 🟒 Low VideoPreview.tsx srcObject check doesn't handle a stream becoming inactive βœ… Resolved
BUG-17 🟒 Low App.tsx Pro features untestable β€” plan is permanently hardcoded to 'free' βœ… Resolved
BUG-18 🟒 Low App.tsx URL.createObjectURL for finished recording never revoked βœ… Resolved

Detailed Bug Descriptions


BUG-01 β€” Annotation path mutation breaks React immutability

Severity: 🟠 High
File: components/VideoPreview.tsx:167–174

const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
    if (!isDrawing.current || !isRecording) return;
    const point = getPoint(e);
    const newPaths = [...annotationPaths];          // shallow copy of array
    const currentPath = newPaths[newPaths.length - 1];
    currentPath.points.push(point);                 // ❌ mutates the original object
    setAnnotationPaths(newPaths);
};

[...annotationPaths] creates a shallow copy of the array but the individual AnnotationPath objects inside are the same references. currentPath.points.push(point) mutates the original object in the previous state. This violates React's immutability contract, can cause missed renders, and makes undo/redo logic unreliable.

Fix: Deep-clone the last path before mutating it:

const lastPath = { ...currentPath, points: [...currentPath.points, point] };
newPaths[newPaths.length - 1] = lastPath;
setAnnotationPaths(newPaths);

BUG-02 β€” pauseResumeRecording stale closure in keyboard shortcut handler

Severity: 🟠 High
File: App.tsx:264–276

useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
        // ...
        case 'P': e.preventDefault(); pauseResumeRecording(); break;  // ❌ stale ref
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
}, [recordingState, startRecording, stopRecording]);  // pauseResumeRecording is MISSING

pauseResumeRecording is not wrapped in useCallback and is not included in the dependency array. When unrelated state changes cause a re-render, the keyboard handler closes over a stale pauseResumeRecording that may have an outdated mediaRecorderRef or recordingState. Pressing Ctrl+Shift+P may silently fail.

Fix: Either add pauseResumeRecording to the dependency array (after wrapping it in useCallback), or use a stable ref pattern:

const pauseResumeRef = useRef(pauseResumeRecording);
useEffect(() => { pauseResumeRef.current = pauseResumeRecording; }, [pauseResumeRecording]);

BUG-03 β€” AudioContext is never closed after recording stops

Severity: 🟠 High
File: App.tsx:131–135

const actuallyStartRecorder = useCallback(async () => {
    if (audioContextRef.current) {
        audioContextRef.current.close();  // only closed on a NEW recording start
    }
    const audioContext = new AudioContext();
    audioContextRef.current = audioContext;
    // ...

When the user stops recording, stopAllStreams() cleans up media tracks, but audioContextRef.current is never closed. The AudioContext (and its connected MediaStreamSource nodes) remain open and consuming resources until the user starts another recording. If the user records once and closes the tab, the AudioContext leaks entirely.

Fix: Close the AudioContext in the mediaRecorderRef.current.onstop callback or in stopAllStreams.


BUG-04 β€” IndexedDB chunks not cleared on mid-recording error state

Severity: 🟠 High
File: App.tsx:147–154

try {
    mediaRecorderRef.current = new MediaRecorder(combinedStream, { ... });
} catch (err) {
    console.error('Failed to create MediaRecorder:', err);
    setRecordingState('error');
    stopAllStreams();
    return;  // ❌ clearRecordingChunks() is NOT called
}

If MediaRecorder construction fails after chunks have already been pre-cleared (line 143: await clearRecordingChunks()), that's fine β€” but if a recording partially completes and the recorder throws an error mid-session (e.g., ondataavailable fails on a large chunk), the onstop callback never fires, leaving stale chunks in IndexedDB. The next recording then calls clearRecordingChunks() to clean up, but only after potentially confusing the user with the stale data.

Additionally, clearRecordingChunks() is not called in the catch block on line 150-154.

Fix: Call clearRecordingChunks() inside the catch block and ensure it's also called when transitioning to the 'error' state.


BUG-05 β€” Two independent timers can drift out of sync

Severity: 🟑 Medium
File: App.tsx:75, components/Controls.tsx:17–47

The application maintains two separate second counters:

  • recordingSecondsRef in App.tsx β€” used for enforcing the 10-minute free tier limit
  • seconds state in the Timer component inside Controls.tsx β€” used for display

Both use setInterval(fn, 1000) independently. Over a long recording session these two timers can drift apart, causing the displayed timer to show a different time than the actual enforced limit. A user watching the timer may be cut off earlier or later than shown.

Fix: Derive the display timer from a single source of truth (e.g., a shared startTime timestamp) or drive the Controls timer off the same ref, exposed via a context or prop.


BUG-06 β€” Webcam failure crashes the entire recording start flow

Severity: 🟑 Medium
File: App.tsx:213

if (settings.webcamEnabled) webcamStreamRef.current = await navigator.mediaDevices.getUserMedia({ video: true });

If the user has webcamEnabled: true but their webcam is unavailable (unplugged, in use by another app, or denied), getUserMedia throws. The catch block at line 217 catches this and calls stopAllStreams() β€” stopping the already-acquired screen share as well. The entire recording attempt fails, and the user loses their screen share permission, requiring them to re-select the screen.

Fix: Wrap the webcam request in its own try/catch. If the webcam fails, warn the user and disable webcam for this session rather than cancelling the entire recording.


BUG-07 β€” Canvas cursor always crosshair, even when not recording

Severity: 🟑 Medium
File: components/VideoPreview.tsx:192–195

<canvas
    ref={ref}
    className="w-full h-full object-contain cursor-crosshair"
    ...

The canvas always shows a crosshair cursor. When not recording (e.g., during countdown or when the screen share has just started but recording hasn't begun), users cannot draw annotations, yet the cursor implies they can. This is misleading UX.

Fix: Conditionally apply the cursor class: cursor-crosshair only when isRecording is true, cursor-default otherwise.


BUG-08 β€” setWebcamPosition prop is passed but never called in VideoPreview

Severity: 🟑 Medium
File: components/VideoPreview.tsx:9, App.tsx:362

// In VideoPreviewProps interface:
setWebcamPosition: (pos: WebcamPosition) => void;
// In App.tsx:
setWebcamPosition={(pos) => setSettings(p => ({...p, webcamPosition: pos}))}

The setWebcamPosition callback is declared in the interface, wired up in App.tsx, but never called anywhere inside VideoPreview.tsx. There is no drag-to-reposition logic implemented. The { x: number; y: number } union in WebcamPosition exists in types.ts and is handled in the canvas render logic, but there's no way to set a custom position through the UI.

Fix: Either implement drag-to-reposition on the canvas, or remove the prop and the custom position type until the feature is ready.


BUG-09 β€” mediaRecorderRef not nulled on error path

Severity: 🟑 Medium
File: App.tsx:147–154

try {
    mediaRecorderRef.current = new MediaRecorder(combinedStream, { ... });
} catch (err) {
    setRecordingState('error');
    stopAllStreams();
    return;  // ❌ mediaRecorderRef.current is NOT set to null
}

When MediaRecorder construction fails, mediaRecorderRef.current retains its previous value (or the partially constructed one). Subsequent calls to stopRecording at line 226 check mediaRecorderRef.current and may attempt to call .stop() on a stale/broken recorder.

Fix: Set mediaRecorderRef.current = null in the catch block.


BUG-10 β€” No touch event support for annotation drawing

Severity: 🟑 Medium
File: components/VideoPreview.tsx:156–178

Only onMouseDown, onMouseMove, onMouseUp, and onMouseLeave are wired up for annotation drawing. On tablet or touch-capable devices (including touch-enabled laptops), touch events (onTouchStart, onTouchMove, onTouchEnd) are not handled. This means annotation drawing is completely non-functional on touch devices.

Fix: Add touch event handlers that map TouchEvent.touches[0] coordinates to the same annotation logic.


BUG-11 β€” No loading indicator while IndexedDB chunks are assembled after stop

Severity: 🟑 Medium
File: App.tsx:163–173

mediaRecorderRef.current.onstop = async () => {
    const db = await dbPromise;
    const allChunks = await db.getAll('recording-chunks');
    // ... assembles blob ...
    setRecordedVideoUrl(URL.createObjectURL(blob));
    setRecordingState('finished');
};

Between calling stop() and setRecordingState('finished') being invoked, there is an asynchronous gap (IndexedDB reads + Blob construction). During this time, recordingState is still 'recording' or 'paused', which means the UI shows stale controls. For long recordings with many chunks, this can take several seconds with no user feedback.

Fix: Add a 'processing' recording state or a separate isProcessing flag, shown with a "Processing your recording…" overlay.


BUG-12 β€” SelectControl and CheckboxControl use any type

Severity: 🟒 Low
File: components/SettingsPanel.tsx:70, 90

const SelectControl: React.FC<any> = ({ ... }) => { ... };
const CheckboxControl: React.FC<any> = ({ ... }) => { ... };

Both internal components use React.FC<any> for their props, which disables TypeScript's type-checking for all prop usages. Typos or wrong prop types will go undetected at compile time.

Fix: Define explicit TypeScript interfaces for each component's props.


BUG-13 β€” Undo history (annotationHistory) is write-only; redo is unreachable

Severity: 🟒 Low
File: App.tsx:68, 248–257

const annotationHistory = useRef<AnnotationPath[]>([]);

const handleUndoAnnotation = () => {
    const lastPath = annotationPaths[annotationPaths.length - 1];
    annotationHistory.current.push(lastPath);   // pushed for redo
    setAnnotationPaths(annotationPaths.slice(0, -1));
};

annotationHistory accumulates undone paths for potential redo, but there is no redo action in the UI or logic. The accumulated paths are never read back. The history also grows unbounded with no cap or eviction policy.

Fix: Either implement a Redo button, or remove the history accumulation entirely since it currently has no effect.


BUG-14 β€” webcamBackground setting is stored but never applied to canvas

Severity: 🟒 Low
File: types.ts:32–33, components/VideoPreview.tsx

// In types.ts:
webcamBackground: 'none' | 'blur' | 'image';
webcamBackgroundImage: string | null;

These fields are defined in the Settings interface and saved to localStorage, but the canvas rendering loop in VideoPreview.tsx never reads or applies them. The SettingsPanel shows a "coming soon" notice. This is a placeholder feature but the settings keys accumulate in localStorage unnecessarily.

Fix: Until the feature is implemented, remove webcamBackground and webcamBackgroundImage from Settings and localStorage to avoid confusion and orphaned data.


BUG-15 β€” Timer shows no visual distinction between recording and paused states

Severity: 🟒 Low
File: components/Controls.tsx:38–47

The Timer component only highlights amber when near the free tier limit. When recording is paused, the timer freezes (correct) but shows the same visual style as active recording. Users have no visual cue from the timer itself that recording is paused vs. actively recording.

Fix: Add a paused visual style (e.g., dimmed opacity, blinking, or a "PAUSED" label) when recordingState === 'paused'.


BUG-16 β€” srcObject check doesn't handle an inactive/ended stream

Severity: 🟒 Low
File: components/VideoPreview.tsx:29–36

const setupVideo = (videoEl: HTMLVideoElement | null, stream: MediaStream | null) => {
    if (!videoEl || !stream) return;
    if (videoEl.srcObject !== stream) {
        videoEl.srcObject = stream;

The guard videoEl.srcObject !== stream prevents re-assigning the same stream object. However, if the same stream object becomes inactive (all tracks ended), the guard prevents reassignment, so the video element shows the last frozen frame rather than detecting the ended state.

Fix: Also check stream.active and handle the case where an active stream has ended.


BUG-17 β€” Pro features are permanently inaccessible β€” plan hardcoded to 'free'

Severity: 🟒 Low
File: App.tsx:60

const [plan] = useState<Plan>('free'); // will be driven by auth/subscription in Phase 3

There is no way to enable Pro features without modifying source code. This blocks internal testing of the Pro tier UI path (pricing modal, MP4 export button, system audio, no-watermark rendering). While intentional for now, it should have a dev/override mechanism.

Fix: Add a query-parameter or localStorage-based override for development: e.g., localStorage.setItem('liveCapDevPlan', 'pro') reads at startup to override the plan during development.


BUG-18 β€” URL.createObjectURL for finished recording never revoked

Severity: 🟠 High
File: App.tsx:168

The blob URL created for the finished recording preview/download is never revoked. Each new recording leaks the previous object URL until the tab is closed, increasing memory usage.

Fix: Store the previous URL and revoke it before creating a new one, and revoke on component unmount:

useEffect(() => () => recordedVideoUrl && URL.revokeObjectURL(recordedVideoUrl), [recordedVideoUrl]);

Environment Notes

  • Bugs were originally identified through static analysis and verified with tsc --noEmit, npm run build, and npm audit.
  • Resolved bugs were fixed on 2026-07-11 in the main branch.
  • Browser compatibility notes: MediaRecorder with VP9 may not be available in Safari. The current fallback chain (vp9 β†’ vp8 β†’ webm) handles this, but Safari may produce an empty MIME type which some versions of Safari refuse to play back.
  • No automated tests exist in the project. All bugs were found via code review.