Last updated: 2026-07-11
Branch:main
Severity levels: π΄ Critical Β· π High Β· π‘ Medium Β· π’ Low
| 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 |
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);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 MISSINGpauseResumeRecording 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]);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.
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.
Severity: π‘ Medium
File: App.tsx:75, components/Controls.tsx:17β47
The application maintains two separate second counters:
recordingSecondsRefinApp.tsxβ used for enforcing the 10-minute free tier limitsecondsstate in theTimercomponent insideControls.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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'.
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.
Severity: π’ Low
File: App.tsx:60
const [plan] = useState<Plan>('free'); // will be driven by auth/subscription in Phase 3There 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.
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]);- Bugs were originally identified through static analysis and verified with
tsc --noEmit,npm run build, andnpm audit. - Resolved bugs were fixed on 2026-07-11 in the
mainbranch. - Browser compatibility notes:
MediaRecorderwith 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.