fix webm metadata duration

This commit is contained in:
Siddharth
2025-10-15 19:11:48 -07:00
parent 52563e6142
commit 9c095e98de
4 changed files with 59 additions and 65 deletions
+26
View File
@@ -8,6 +8,7 @@
"name": "pangolin",
"version": "0.0.0",
"dependencies": {
"@fix-webm-duration/fix": "^1.0.1",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"class-variance-authority": "^0.7.1",
@@ -1134,6 +1135,25 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fix-webm-duration/fix": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@fix-webm-duration/fix/-/fix-1.0.1.tgz",
"integrity": "sha512-rRN4CpWQaXRbCXYqKIxnsUq8OSWSGq/SVlnxxkx0HxMZZ1u0qnB24P766o8QS5YaMMqAOFAzmsMmfZ2OWucOLQ==",
"license": "MIT",
"dependencies": {
"@fix-webm-duration/parser": "1.0.1",
"tslib": "^2.3.0"
}
},
"node_modules/@fix-webm-duration/parser": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@fix-webm-duration/parser/-/parser-1.0.1.tgz",
"integrity": "sha512-Z6el7ohBBpym4iOmxDxumN715cHzLcAbtOtYfIbvoyToVtAuxvHt3ELXGff83iAXEU1Yj7g+obQBdiKJYVhbWw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
}
},
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -8801,6 +8821,12 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+1
View File
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@fix-webm-duration/fix": "^1.0.1",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
"class-variance-authority": "^0.7.1",
+16 -34
View File
@@ -2,7 +2,6 @@
import { useEffect, useRef, useState } from "react";
export default function VideoEditor() {
// --- State ---
const [videoPath, setVideoPath] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -10,11 +9,9 @@ export default function VideoEditor() {
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
// --- Refs ---
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
// --- Load video path on mount ---
useEffect(() => {
async function loadVideo() {
try {
@@ -33,21 +30,21 @@ export default function VideoEditor() {
loadVideo();
}, []);
// --- Canvas drawing and video event listeners ---
useEffect(() => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;
let animationId: number;
function drawFrame() {
if (!video || !canvas) return;
if (video.paused || video.ended) return;
// Keep canvas size in sync with video
if (!video || !canvas || video.paused || video.ended) return;
if (canvas.width !== video.videoWidth || canvas.height !== video.videoHeight) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -56,12 +53,8 @@ export default function VideoEditor() {
animationId = requestAnimationFrame(drawFrame);
}
function handlePlay() {
drawFrame();
}
function handlePause() {
cancelAnimationFrame(animationId);
}
const handlePlay = () => drawFrame();
const handlePause = () => cancelAnimationFrame(animationId);
video.addEventListener('play', handlePlay);
video.addEventListener('pause', handlePause);
@@ -75,32 +68,23 @@ export default function VideoEditor() {
};
}, [videoPath]);
// --- Handlers ---
function togglePlayPause() {
if (!videoRef.current) return;
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
isPlaying ? videoRef.current.pause() : videoRef.current.play();
}
function handleSeek(e: React.ChangeEvent<HTMLInputElement>) {
if (!videoRef.current) return;
const newTime = parseFloat(e.target.value);
videoRef.current.currentTime = newTime;
videoRef.current.currentTime = parseFloat(e.target.value);
}
function formatTime(seconds: number) {
if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) {
return '0:00';
}
if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// --- Early returns for loading/error ---
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-background">
@@ -108,6 +92,7 @@ export default function VideoEditor() {
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-screen bg-background">
@@ -116,7 +101,6 @@ export default function VideoEditor() {
);
}
// --- Main render ---
return (
<div className="flex h-screen bg-background p-6 gap-6">
<div className="flex flex-col flex-[7] min-w-0 h-full">
@@ -142,23 +126,21 @@ export default function VideoEditor() {
preload="metadata"
onLoadedMetadata={e => {
const video = e.currentTarget;
if (isFinite(video.duration) && !isNaN(video.duration) && video.duration > 0) {
if (isFinite(video.duration) && video.duration > 0) {
setDuration(video.duration);
}
}}
onCanPlay={() => {
const video = videoRef.current;
if (video && isFinite(video.duration) && video.duration > 0) {
onDurationChange={e => {
const video = e.currentTarget;
if (isFinite(video.duration) && video.duration > 0) {
setDuration(video.duration);
}
}}
onTimeUpdate={e => {
const time = e.currentTarget.currentTime;
if (isFinite(time) && !isNaN(time)) {
setCurrentTime(time);
}
if (isFinite(time)) setCurrentTime(time);
}}
onError={() => setError('Failed to play video')}
onError={() => setError('Failed to load video')}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onEnded={() => setIsPlaying(false)}
+16 -31
View File
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect } from "react";
import { fixWebmDuration } from "@fix-webm-duration/fix";
type UseScreenRecorderReturn = {
recording: boolean;
@@ -10,13 +11,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const chunksRef = useRef<Blob[]>([]);
const startTimeRef = useRef<number>(0);
useEffect(() => {
return () => {
if (
mediaRecorderRef.current &&
mediaRecorderRef.current.state === "recording"
) {
if (mediaRecorderRef.current?.state === "recording") {
mediaRecorderRef.current.stop();
}
if (streamRef.current) {
@@ -60,15 +59,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
let bitrate: number;
if (totalPixels <= 1920 * 1080) {
bitrate = 150_000_000; // 150 Mbps for 1080p
bitrate = 150_000_000;
} else if (totalPixels <= 2560 * 1440) {
bitrate = 250_000_000; // 250 Mbps for 1440p
bitrate = 250_000_000;
} else {
bitrate = 400_000_000; // 400 Mbps for 4K
bitrate = 400_000_000;
}
console.log(`Recording at ${width}x${height} with bitrate: ${bitrate / 1_000_000} Mbps`);
chunksRef.current = [];
const mimeType = "video/webm;codecs=vp9";
const recorder = new MediaRecorder(streamRef.current, {
@@ -84,45 +81,37 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
recorder.onstop = async () => {
// Don't stop stream here - already stopped in stopRecording for immediate indicator removal
// Just cleanup the ref
streamRef.current = null;
if (chunksRef.current.length === 0) return;
const videoBlob = new Blob(chunksRef.current, { type: mimeType });
const duration = Date.now() - startTimeRef.current;
const buggyBlob = new Blob(chunksRef.current, { type: mimeType });
const timestamp = Date.now();
const videoFileName = `recording-${timestamp}.webm`;
const trackingFileName = `recording-${timestamp}_tracking.json`;
try {
const videoBlob = await fixWebmDuration(buggyBlob, duration);
const arrayBuffer = await videoBlob.arrayBuffer();
console.log(`Saving video: ${videoFileName} (${(arrayBuffer.byteLength / 1024 / 1024).toFixed(2)} MB)`);
const videoResult = await window.electronAPI.storeRecordedVideo(
arrayBuffer,
videoFileName
);
if (videoResult.success) {
console.log('✅ Video stored:', videoResult.path);
} else {
console.error('❌ Failed to store video:', videoResult.message);
if (!videoResult.success) {
console.error('Failed to store video:', videoResult.message);
return;
}
const trackingResult = await window.electronAPI.storeMouseTrackingData(trackingFileName);
if (trackingResult.success) {
console.log('Mouse tracking stored:', trackingResult.path);
console.log(`Captured ${trackingResult.eventCount} mouse events`);
} else {
console.warn('Failed to store tracking:', trackingResult.message);
if (!trackingResult.success) {
console.warn('Failed to store mouse tracking:', trackingResult.message);
}
console.log('Opening editor window...');
await window.electronAPI.switchToEditor();
} catch (error) {
console.error('Error saving recording:', error);
}
@@ -133,6 +122,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
recorder.start(1000);
startTimeRef.current = Date.now();
setRecording(true);
} catch (error) {
console.error('Failed to start recording:', error);
@@ -145,18 +135,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
};
const stopRecording = () => {
if (
mediaRecorderRef.current &&
mediaRecorderRef.current.state === "recording"
) {
// Stop stream tracks IMMEDIATELY to remove macOS status indicator
if (mediaRecorderRef.current?.state === "recording") {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
}
mediaRecorderRef.current.stop();
setRecording(false);
window.electronAPI.stopMouseTracking();
}
};