Vibecode Camtasia
track this build4 phases, 12 steps, beginner friendly0%Camtasia's solo core is compact: build a local screen recording + editing workflow that imports user-owned media, applies the core edit or transformation, previews it, and exports a standard file. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on media pipeline, hosting, distribution.
You are building a lean indie version of Camtasia. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Camtasia · indie build A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The platform screen capture source through FFmpeg, written as fragments or segments | an interrupted normal MP4 is an unreadable file, and the documentation says so | | Processing | FFmpeg for everything after capture, driven from Python | codecs are a solved problem you should not be solving | | Editing | A non-destructive cut list over the original file, never edits in place | the original is the thing that cannot be recreated | | Preview | Fast approximate preview, exact render only on export | waiting for an exact render on every trim makes the tool unusable | | Storage | Recordings and renders in separate directories with a disk check before capture | a recording that stops because the disk filled is the same as no recording | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **FFmpeg with your platform's screen capture support** · free - Why: It is the capture, the processing and the export, and the fragmented container behaviour this build depends on is documented there. - Get it: Your package manager, or ffmpeg.org, then check that your platform's screen capture input is listed. - Verify: ffmpeg -devices lists your screen capture input and a ten second test recording plays - [ ] **Python 3.12 or newer** · free - Why: Drives capture, the cut list and the render queue. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Disk space and a way to measure it** · free - Why: Screen recording fills a disk faster than people expect and a recording that stops because the volume was full is the failure this build exists to avoid. - Get it: Know your free space and measure the bitrate of a one minute test at your settings. - Verify: A one minute test gives a megabytes per minute figure used by the estimate - [ ] **Screen and microphone permissions granted to the right process** · free - Why: On current desktop systems the capture will silently produce a black screen or no audio until the permission is granted to the exact binary doing the capture. - Get it: Grant screen recording and microphone access, then confirm with a test capture from the same process the app uses. - Verify: A test recording contains both picture and sound - [ ] **Your capture settings and retention rules** · free - Why: Resolution, frame rate and what cleanup may delete all become tests, and deciding them afterwards means accepting whatever the code happened to do. - Get it: Write down the capture resolution and frame rate, the scratch retention period, and the rule that recordings are never deleted automatically. - Verify: All three are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for Camtasia, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Camtasia The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screen recorder and trimmer whose recordings survive a crash and whose cuts land where you asked to replace Camtasia. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A recording cannot be re-taken. The demonstration happened, the person spoke, the bug reproduced once. So the recorder is built to survive its own failure before it is built to do anything else. This has a concrete implementation. FFmpeg's own documentation states that a normal MOV or MP4 is undecodable if it is not properly finished, because the index is written at the end, while a fragmented file 'is decodable even if the writing is interrupted'. That is the difference between a crashed two hour recording being a file you can still use and being nothing at all. So record fragmented, or record in segments, verify the file opens as soon as recording stops, and only then produce the tidy deliverable with the index moved to the front for playback. Everything else in this build is negotiable. This is not. ### Stack (fixed, do not substitute) - Capture: The platform screen capture source through FFmpeg, written as fragments or segments · an interrupted normal MP4 is an unreadable file, and the documentation says so - Processing: FFmpeg for everything after capture, driven from Python · codecs are a solved problem you should not be solving - Editing: A non-destructive cut list over the original file, never edits in place · the original is the thing that cannot be recreated - Preview: Fast approximate preview, exact render only on export · waiting for an exact render on every trim makes the tool unusable - Storage: Recordings and renders in separate directories with a disk check before capture · a recording that stops because the disk filled is the same as no recording ### Data model (create this before Phase 1) - recordings(id, started_at, stopped_at, path, container, fragmented, width, height, nominal_fps, audio_streams, bytes, verified_at, verify_result) - sources(recording_id, kind, device, sample_rate, channels) · kind is screen, microphone or system audio, each kept as its own stream - projects(id, recording_id, name, created_at) - cuts(id, project_id, ordinal, start_ms, end_ms, action) · action is keep or drop, applied to the original, never to a previous export - markers(id, project_id, at_ms, label) · set while recording, because that is when you know - renders(id, project_id, preset, output_path, started_at, finished_at, state, log_excerpt, bytes) - The cuts table is a description of the edit. The recording file is never modified, so every export starts from the original. ### Phase 1 · A recording that survives the process dying Build: Capture to a fragmented container, or to timed segments that are stitched afterwards. Check free disk space before starting and estimate how long the space will last at the chosen settings. When recording stops, probe the file and record whether it opened, its duration and its stream list. Refuse to start when the space will not cover the requested duration. Trap: Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. Done when: Killing the recorder process after two minutes leaves a file that opens and plays those two minutes, tested by actually killing it. Every recording is probed after stopping and its duration and streams are stored. Recording refuses to start when free space would not cover the requested duration at the configured bitrate, tested with a small volume. A recording that failed verification is marked in the list rather than sitting there looking normal. Do not build yet: No editing, preview, captions or export presets yet. ### Phase 2 · Audio that stays with the picture Build: Screen capture sources deliver frames when the screen changes, so the real frame rate varies and can drop to nothing during a still moment. Normalise to a constant rate with the frame rate filter at capture or at the first processing step rather than assuming one. Keep the microphone and any system audio as separate streams at one sample rate. Measure drift on a twenty minute test by clapping at both ends. Trap: Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. Done when: A twenty minute recording of a clap at the start and the end shows audio and video offsets within a stated tolerance at both, measured. Microphone and system audio are separate streams in the file and can be muted independently, checked. All audio streams share one sample rate, verified by probing. A minute of a completely static screen does not produce a gap or a timestamp jump, tested. Do not build yet: No cut list, preview, captions or export presets yet. ### Phase 3 · Cuts that land where you asked Build: Store cuts as a list against the original. For preview, seek and copy without re-encoding and tell the user the preview is approximate. For export, re-encode so the cut is frame accurate. If you ever stitch copied segments, set the flag that shifts timestamps to start at zero, or the join will misbehave. Trap: Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. Done when: An export cut at a stated time starts within one frame of it, measured by reading timestamps. The original recording file is unmodified after any number of edits, verified by checksum. A preview built by copying is labelled as approximate wherever it appears. Segments joined for export produce no timestamp gap or duplicated frame at the join, tested with three segments. Do not build yet: No captions, zoom or export presets yet. ### Phase 4 · The things a tutorial actually needs Build: Set markers with a keyboard shortcut while recording, since that is when you know something happened. Support a zoom region over a time range, rendered by cropping and scaling on export. Produce captions as a sidecar subtitle file from a transcript, with burning in as an explicit second output rather than the default. Trap: Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. Done when: A marker set during recording appears at the right time in the editor, within a frame, tested. A zoom region renders as a crop and scale over exactly the stated time range and returns to full frame afterwards, checked visually and by dimensions. Captions are produced as a sidecar file that plays in a standard player. Burning in is a separate command that leaves the sidecar version intact. Do not build yet: No export presets, cleanup or long render handling yet. ### Phase 5 · Exports, long renders and the disk Build: Named export presets with a resolution, bitrate and container, recorded on every render. Render in segments so an interrupted export resumes rather than restarting. Move the index to the front of the delivered file so it can start playing before it has fully downloaded. Report disk usage per recording and project, and delete scratch files on a stated schedule with the recordings themselves never deleted automatically. Trap: An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. Done when: An export interrupted at seventy percent resumes and finishes without re-rendering the first seventy, tested by killing it. The delivered file begins playing from a web server before it has fully downloaded, verified by request. Automatic cleanup never removes a file in the recordings directory, asserted in the suite. Each project reports its disk usage and the total is visible in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-track compositing, transitions and titles, which is a video editor rather than a recorder with a trimmer, and open editors already do it well. - Webcam overlay with background removal, which needs a model choice and a licence check and adds a second capture device to synchronise. - Hosting and sharing, which is a bandwidth bill and a permissions model, and a file on a disk is a fine deliverable. - Hardware encoder tuning per machine, which is a per-graphics-card project and gives you nothing until the rest works. ### README must contain - That recordings are written fragmented or in segments because an unfinished plain MP4 is undecodable, with the documentation reference - That screen capture produces a genuinely variable frame rate and how this build normalises it - That preview cuts are approximate because stream copying lands on a keyframe, while exports are frame accurate - That the original recording is never modified and every export starts from it - Exactly what automatic cleanup deletes, and that recordings are never in that list ===== AGENTS.md ===== # Agent instructions · Camtasia indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The platform screen capture source through FFmpeg, written as fragments or segments, FFmpeg for everything after capture, driven from Python, A non-destructive cut list over the original file, never edits in place, Fast approximate preview, exact render only on export, Recordings and renders in separate directories with a disk check before capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ===== BUILD_PLAN.md ===== # Build plan · Camtasia A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A recording that survives the process dying Killing the recorder mid-capture leaves a file that opens and plays up to the moment it stopped. ### Steps 1. Record fragmented or in segments This is the one decision the whole build is arranged around. Files: `src/capture.py`, `tests/test_kill_recovery.py` 2. Check the disk before starting, not during An estimate at the start beats a truncated file at the end. Files: `src/disk.py` 3. Probe and verify the moment recording stops You want to find out now, not when you go to edit it. Files: `src/verify.py` ### Done when - [ ] A killed recorder leaves a playable file - [ ] Every recording is probed and its result stored - [ ] Recording refuses to start without enough space - [ ] Failed verification is visible in the list ### Watch out - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. ## Phase 2 · Audio that stays with the picture Twenty minutes in, the voice is still aligned with what is on screen. ### Steps 1. Normalise the frame rate explicitly Screen capture is genuinely variable, sometimes zero frames per second. Files: `src/capture.py` 2. Keep audio sources as separate streams Fixing a loud notification later is only possible if it is separable. Files: `src/audio.py` 3. Measure drift with a clap at both ends Twenty minutes, because five minutes hides it. Files: `tests/test_drift.py`, `docs/drift-test.md` ### Done when - [ ] Offsets at both ends of twenty minutes are within tolerance - [ ] Microphone and system audio are independently mutable - [ ] All audio streams share one sample rate - [ ] A static minute produces no timestamp jump ### Watch out - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. ## Phase 3 · Cuts that land where you asked Trimming removes exactly the section selected, and the original file is never touched. ### Steps 1. Keep the cut list, never edit the file The original is the only irreplaceable artefact here. Files: `src/cuts.py` 2. Re-encode on export, copy for preview And label the preview as approximate in the interface. Files: `src/export.py`, `tests/test_cut_accuracy.py` 3. Shift timestamps when joining copied segments Otherwise the join produces artefacts that look like corruption. Files: `src/join.py` ### Done when - [ ] An export cut lands within one frame of the request - [ ] The original file is unchanged after editing - [ ] Approximate previews are labelled as such - [ ] Joined segments show no gap or duplicate frame ### Watch out - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. ## Phase 4 · The things a tutorial actually needs Markers, a zoom on the area that matters, and captions that are a file rather than burnt in. ### Steps 1. Capture markers from the keyboard while recording Afterwards you will not remember when it happened. Files: `src/markers.py` 2. Render zoom as crop and scale on export Not as a preview effect that never reaches the file. Files: `src/zoom.py` 3. Emit captions as a sidecar by default A typo should cost a text edit, not a re-render. Files: `src/captions.py` ### Done when - [ ] A recording marker lands within a frame in the editor - [ ] Zoom applies over exactly its stated range - [ ] Captions play from a sidecar file in a standard player - [ ] Burning in is a separate command ### Watch out - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ## Not in this build - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. ## After v1, if you want it - Automatic zoom from cursor movement, computed after recording from a cursor position log - A silence remover that proposes cuts you accept, driven from the same cut list - A second export preset for a different platform, which is configuration rather than code ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original captures. Never written to by the editor and never touched by cleanup. RECORDINGS_DIR=./data/recordings # Required. Previews and render segments. This is the directory cleanup is allowed to empty. SCRATCH_DIR=./data/scratch # Required. Finished exports, with the index at the front for playback. RENDERS_DIR=./data/renders # Required. The rate the variable capture is normalised to. Higher costs disk linearly. CAPTURE_FPS=30 # Required. Recording segment length, and the resume granularity for long exports. SEGMENT_SECONDS=60 # Required. How long previews and render segments survive. Recordings are not included. SCRATCH_RETENTION_HOURS=48 # Required. Recording refuses to start below this, because a full disk truncates a capture. MIN_FREE_GB=20
You are building a lean indie version of Camtasia. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Camtasia · indie build A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The platform screen capture source through FFmpeg, written as fragments or segments | an interrupted normal MP4 is an unreadable file, and the documentation says so | | Processing | FFmpeg for everything after capture, driven from Python | codecs are a solved problem you should not be solving | | Editing | A non-destructive cut list over the original file, never edits in place | the original is the thing that cannot be recreated | | Preview | Fast approximate preview, exact render only on export | waiting for an exact render on every trim makes the tool unusable | | Storage | Recordings and renders in separate directories with a disk check before capture | a recording that stops because the disk filled is the same as no recording | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **FFmpeg with your platform's screen capture support** · free - Why: It is the capture, the processing and the export, and the fragmented container behaviour this build depends on is documented there. - Get it: Your package manager, or ffmpeg.org, then check that your platform's screen capture input is listed. - Verify: ffmpeg -devices lists your screen capture input and a ten second test recording plays - [ ] **Python 3.12 or newer** · free - Why: Drives capture, the cut list and the render queue. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Disk space and a way to measure it** · free - Why: Screen recording fills a disk faster than people expect and a recording that stops because the volume was full is the failure this build exists to avoid. - Get it: Know your free space and measure the bitrate of a one minute test at your settings. - Verify: A one minute test gives a megabytes per minute figure used by the estimate - [ ] **Screen and microphone permissions granted to the right process** · free - Why: On current desktop systems the capture will silently produce a black screen or no audio until the permission is granted to the exact binary doing the capture. - Get it: Grant screen recording and microphone access, then confirm with a test capture from the same process the app uses. - Verify: A test recording contains both picture and sound - [ ] **Your capture settings and retention rules** · free - Why: Resolution, frame rate and what cleanup may delete all become tests, and deciding them afterwards means accepting whatever the code happened to do. - Get it: Write down the capture resolution and frame rate, the scratch retention period, and the rule that recordings are never deleted automatically. - Verify: All three are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for Camtasia, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Camtasia The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screen recorder and trimmer whose recordings survive a crash and whose cuts land where you asked to replace Camtasia. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A recording cannot be re-taken. The demonstration happened, the person spoke, the bug reproduced once. So the recorder is built to survive its own failure before it is built to do anything else. This has a concrete implementation. FFmpeg's own documentation states that a normal MOV or MP4 is undecodable if it is not properly finished, because the index is written at the end, while a fragmented file 'is decodable even if the writing is interrupted'. That is the difference between a crashed two hour recording being a file you can still use and being nothing at all. So record fragmented, or record in segments, verify the file opens as soon as recording stops, and only then produce the tidy deliverable with the index moved to the front for playback. Everything else in this build is negotiable. This is not. ### Stack (fixed, do not substitute) - Capture: The platform screen capture source through FFmpeg, written as fragments or segments · an interrupted normal MP4 is an unreadable file, and the documentation says so - Processing: FFmpeg for everything after capture, driven from Python · codecs are a solved problem you should not be solving - Editing: A non-destructive cut list over the original file, never edits in place · the original is the thing that cannot be recreated - Preview: Fast approximate preview, exact render only on export · waiting for an exact render on every trim makes the tool unusable - Storage: Recordings and renders in separate directories with a disk check before capture · a recording that stops because the disk filled is the same as no recording ### Data model (create this before Phase 1) - recordings(id, started_at, stopped_at, path, container, fragmented, width, height, nominal_fps, audio_streams, bytes, verified_at, verify_result) - sources(recording_id, kind, device, sample_rate, channels) · kind is screen, microphone or system audio, each kept as its own stream - projects(id, recording_id, name, created_at) - cuts(id, project_id, ordinal, start_ms, end_ms, action) · action is keep or drop, applied to the original, never to a previous export - markers(id, project_id, at_ms, label) · set while recording, because that is when you know - renders(id, project_id, preset, output_path, started_at, finished_at, state, log_excerpt, bytes) - The cuts table is a description of the edit. The recording file is never modified, so every export starts from the original. ### Phase 1 · A recording that survives the process dying Build: Capture to a fragmented container, or to timed segments that are stitched afterwards. Check free disk space before starting and estimate how long the space will last at the chosen settings. When recording stops, probe the file and record whether it opened, its duration and its stream list. Refuse to start when the space will not cover the requested duration. Trap: Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. Done when: Killing the recorder process after two minutes leaves a file that opens and plays those two minutes, tested by actually killing it. Every recording is probed after stopping and its duration and streams are stored. Recording refuses to start when free space would not cover the requested duration at the configured bitrate, tested with a small volume. A recording that failed verification is marked in the list rather than sitting there looking normal. Do not build yet: No editing, preview, captions or export presets yet. ### Phase 2 · Audio that stays with the picture Build: Screen capture sources deliver frames when the screen changes, so the real frame rate varies and can drop to nothing during a still moment. Normalise to a constant rate with the frame rate filter at capture or at the first processing step rather than assuming one. Keep the microphone and any system audio as separate streams at one sample rate. Measure drift on a twenty minute test by clapping at both ends. Trap: Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. Done when: A twenty minute recording of a clap at the start and the end shows audio and video offsets within a stated tolerance at both, measured. Microphone and system audio are separate streams in the file and can be muted independently, checked. All audio streams share one sample rate, verified by probing. A minute of a completely static screen does not produce a gap or a timestamp jump, tested. Do not build yet: No cut list, preview, captions or export presets yet. ### Phase 3 · Cuts that land where you asked Build: Store cuts as a list against the original. For preview, seek and copy without re-encoding and tell the user the preview is approximate. For export, re-encode so the cut is frame accurate. If you ever stitch copied segments, set the flag that shifts timestamps to start at zero, or the join will misbehave. Trap: Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. Done when: An export cut at a stated time starts within one frame of it, measured by reading timestamps. The original recording file is unmodified after any number of edits, verified by checksum. A preview built by copying is labelled as approximate wherever it appears. Segments joined for export produce no timestamp gap or duplicated frame at the join, tested with three segments. Do not build yet: No captions, zoom or export presets yet. ### Phase 4 · The things a tutorial actually needs Build: Set markers with a keyboard shortcut while recording, since that is when you know something happened. Support a zoom region over a time range, rendered by cropping and scaling on export. Produce captions as a sidecar subtitle file from a transcript, with burning in as an explicit second output rather than the default. Trap: Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. Done when: A marker set during recording appears at the right time in the editor, within a frame, tested. A zoom region renders as a crop and scale over exactly the stated time range and returns to full frame afterwards, checked visually and by dimensions. Captions are produced as a sidecar file that plays in a standard player. Burning in is a separate command that leaves the sidecar version intact. Do not build yet: No export presets, cleanup or long render handling yet. ### Phase 5 · Exports, long renders and the disk Build: Named export presets with a resolution, bitrate and container, recorded on every render. Render in segments so an interrupted export resumes rather than restarting. Move the index to the front of the delivered file so it can start playing before it has fully downloaded. Report disk usage per recording and project, and delete scratch files on a stated schedule with the recordings themselves never deleted automatically. Trap: An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. Done when: An export interrupted at seventy percent resumes and finishes without re-rendering the first seventy, tested by killing it. The delivered file begins playing from a web server before it has fully downloaded, verified by request. Automatic cleanup never removes a file in the recordings directory, asserted in the suite. Each project reports its disk usage and the total is visible in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-track compositing, transitions and titles, which is a video editor rather than a recorder with a trimmer, and open editors already do it well. - Webcam overlay with background removal, which needs a model choice and a licence check and adds a second capture device to synchronise. - Hosting and sharing, which is a bandwidth bill and a permissions model, and a file on a disk is a fine deliverable. - Hardware encoder tuning per machine, which is a per-graphics-card project and gives you nothing until the rest works. ### README must contain - That recordings are written fragmented or in segments because an unfinished plain MP4 is undecodable, with the documentation reference - That screen capture produces a genuinely variable frame rate and how this build normalises it - That preview cuts are approximate because stream copying lands on a keyframe, while exports are frame accurate - That the original recording is never modified and every export starts from it - Exactly what automatic cleanup deletes, and that recordings are never in that list ===== AGENTS.md ===== # Agent instructions · Camtasia indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The platform screen capture source through FFmpeg, written as fragments or segments, FFmpeg for everything after capture, driven from Python, A non-destructive cut list over the original file, never edits in place, Fast approximate preview, exact render only on export, Recordings and renders in separate directories with a disk check before capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ===== BUILD_PLAN.md ===== # Build plan · Camtasia A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A recording that survives the process dying Killing the recorder mid-capture leaves a file that opens and plays up to the moment it stopped. ### Steps 1. Record fragmented or in segments This is the one decision the whole build is arranged around. Files: `src/capture.py`, `tests/test_kill_recovery.py` 2. Check the disk before starting, not during An estimate at the start beats a truncated file at the end. Files: `src/disk.py` 3. Probe and verify the moment recording stops You want to find out now, not when you go to edit it. Files: `src/verify.py` ### Done when - [ ] A killed recorder leaves a playable file - [ ] Every recording is probed and its result stored - [ ] Recording refuses to start without enough space - [ ] Failed verification is visible in the list ### Watch out - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. ## Phase 2 · Audio that stays with the picture Twenty minutes in, the voice is still aligned with what is on screen. ### Steps 1. Normalise the frame rate explicitly Screen capture is genuinely variable, sometimes zero frames per second. Files: `src/capture.py` 2. Keep audio sources as separate streams Fixing a loud notification later is only possible if it is separable. Files: `src/audio.py` 3. Measure drift with a clap at both ends Twenty minutes, because five minutes hides it. Files: `tests/test_drift.py`, `docs/drift-test.md` ### Done when - [ ] Offsets at both ends of twenty minutes are within tolerance - [ ] Microphone and system audio are independently mutable - [ ] All audio streams share one sample rate - [ ] A static minute produces no timestamp jump ### Watch out - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. ## Phase 3 · Cuts that land where you asked Trimming removes exactly the section selected, and the original file is never touched. ### Steps 1. Keep the cut list, never edit the file The original is the only irreplaceable artefact here. Files: `src/cuts.py` 2. Re-encode on export, copy for preview And label the preview as approximate in the interface. Files: `src/export.py`, `tests/test_cut_accuracy.py` 3. Shift timestamps when joining copied segments Otherwise the join produces artefacts that look like corruption. Files: `src/join.py` ### Done when - [ ] An export cut lands within one frame of the request - [ ] The original file is unchanged after editing - [ ] Approximate previews are labelled as such - [ ] Joined segments show no gap or duplicate frame ### Watch out - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. ## Phase 4 · The things a tutorial actually needs Markers, a zoom on the area that matters, and captions that are a file rather than burnt in. ### Steps 1. Capture markers from the keyboard while recording Afterwards you will not remember when it happened. Files: `src/markers.py` 2. Render zoom as crop and scale on export Not as a preview effect that never reaches the file. Files: `src/zoom.py` 3. Emit captions as a sidecar by default A typo should cost a text edit, not a re-render. Files: `src/captions.py` ### Done when - [ ] A recording marker lands within a frame in the editor - [ ] Zoom applies over exactly its stated range - [ ] Captions play from a sidecar file in a standard player - [ ] Burning in is a separate command ### Watch out - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ## Not in this build - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. ## After v1, if you want it - Automatic zoom from cursor movement, computed after recording from a cursor position log - A silence remover that proposes cuts you accept, driven from the same cut list - A second export preset for a different platform, which is configuration rather than code ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original captures. Never written to by the editor and never touched by cleanup. RECORDINGS_DIR=./data/recordings # Required. Previews and render segments. This is the directory cleanup is allowed to empty. SCRATCH_DIR=./data/scratch # Required. Finished exports, with the index at the front for playback. RENDERS_DIR=./data/renders # Required. The rate the variable capture is normalised to. Higher costs disk linearly. CAPTURE_FPS=30 # Required. Recording segment length, and the resume granularity for long exports. SEGMENT_SECONDS=60 # Required. How long previews and render segments survive. Recordings are not included. SCRATCH_RETENTION_HOURS=48 # Required. Recording refuses to start below this, because a full disk truncates a capture. MIN_FREE_GB=20
You are building a production product version of Camtasia. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Camtasia · product brief ## Problem Camtasia's solo core is compact: build a local screen recording + editing workflow that imports user-owned media, applies the core edit or transformation, previews it, and exports a standard file. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on media pipeline, hosting, distribution. ## Product outcome A recorder whose files survive a crash, whose audio stays aligned over a long session, whose cuts land where you asked, and whose cleanup can never delete the one thing that cannot be recorded again. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - desktop with sufficient storage ## Explicit non-goals for v1 - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration ## Success criteria - A killed recorder is confirmed to leave a playable file - Twenty minute drift is confirmed within tolerance at both ends - An export cut is confirmed to land within one frame - An interrupted export is confirmed to resume - Cleanup is confirmed unable to touch recordings ===== BRIEF.md ===== # Build brief · Camtasia The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screen recorder and trimmer whose recordings survive a crash and whose cuts land where you asked to replace Camtasia. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A recording cannot be re-taken. The demonstration happened, the person spoke, the bug reproduced once. So the recorder is built to survive its own failure before it is built to do anything else. This has a concrete implementation. FFmpeg's own documentation states that a normal MOV or MP4 is undecodable if it is not properly finished, because the index is written at the end, while a fragmented file 'is decodable even if the writing is interrupted'. That is the difference between a crashed two hour recording being a file you can still use and being nothing at all. So record fragmented, or record in segments, verify the file opens as soon as recording stops, and only then produce the tidy deliverable with the index moved to the front for playback. Everything else in this build is negotiable. This is not. ### Stack (fixed, do not substitute) - Capture: The platform screen capture source through FFmpeg, written as fragments or segments · an interrupted normal MP4 is an unreadable file, and the documentation says so - Processing: FFmpeg for everything after capture, driven from Python · codecs are a solved problem you should not be solving - Editing: A non-destructive cut list over the original file, never edits in place · the original is the thing that cannot be recreated - Preview: Fast approximate preview, exact render only on export · waiting for an exact render on every trim makes the tool unusable - Storage: Recordings and renders in separate directories with a disk check before capture · a recording that stops because the disk filled is the same as no recording ### Data model (create this before Phase 1) - recordings(id, started_at, stopped_at, path, container, fragmented, width, height, nominal_fps, audio_streams, bytes, verified_at, verify_result) - sources(recording_id, kind, device, sample_rate, channels) · kind is screen, microphone or system audio, each kept as its own stream - projects(id, recording_id, name, created_at) - cuts(id, project_id, ordinal, start_ms, end_ms, action) · action is keep or drop, applied to the original, never to a previous export - markers(id, project_id, at_ms, label) · set while recording, because that is when you know - renders(id, project_id, preset, output_path, started_at, finished_at, state, log_excerpt, bytes) - The cuts table is a description of the edit. The recording file is never modified, so every export starts from the original. ### Phase 1 · A recording that survives the process dying Build: Capture to a fragmented container, or to timed segments that are stitched afterwards. Check free disk space before starting and estimate how long the space will last at the chosen settings. When recording stops, probe the file and record whether it opened, its duration and its stream list. Refuse to start when the space will not cover the requested duration. Trap: Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. Done when: Killing the recorder process after two minutes leaves a file that opens and plays those two minutes, tested by actually killing it. Every recording is probed after stopping and its duration and streams are stored. Recording refuses to start when free space would not cover the requested duration at the configured bitrate, tested with a small volume. A recording that failed verification is marked in the list rather than sitting there looking normal. Do not build yet: No editing, preview, captions or export presets yet. ### Phase 2 · Audio that stays with the picture Build: Screen capture sources deliver frames when the screen changes, so the real frame rate varies and can drop to nothing during a still moment. Normalise to a constant rate with the frame rate filter at capture or at the first processing step rather than assuming one. Keep the microphone and any system audio as separate streams at one sample rate. Measure drift on a twenty minute test by clapping at both ends. Trap: Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. Done when: A twenty minute recording of a clap at the start and the end shows audio and video offsets within a stated tolerance at both, measured. Microphone and system audio are separate streams in the file and can be muted independently, checked. All audio streams share one sample rate, verified by probing. A minute of a completely static screen does not produce a gap or a timestamp jump, tested. Do not build yet: No cut list, preview, captions or export presets yet. ### Phase 3 · Cuts that land where you asked Build: Store cuts as a list against the original. For preview, seek and copy without re-encoding and tell the user the preview is approximate. For export, re-encode so the cut is frame accurate. If you ever stitch copied segments, set the flag that shifts timestamps to start at zero, or the join will misbehave. Trap: Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. Done when: An export cut at a stated time starts within one frame of it, measured by reading timestamps. The original recording file is unmodified after any number of edits, verified by checksum. A preview built by copying is labelled as approximate wherever it appears. Segments joined for export produce no timestamp gap or duplicated frame at the join, tested with three segments. Do not build yet: No captions, zoom or export presets yet. ### Phase 4 · The things a tutorial actually needs Build: Set markers with a keyboard shortcut while recording, since that is when you know something happened. Support a zoom region over a time range, rendered by cropping and scaling on export. Produce captions as a sidecar subtitle file from a transcript, with burning in as an explicit second output rather than the default. Trap: Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. Done when: A marker set during recording appears at the right time in the editor, within a frame, tested. A zoom region renders as a crop and scale over exactly the stated time range and returns to full frame afterwards, checked visually and by dimensions. Captions are produced as a sidecar file that plays in a standard player. Burning in is a separate command that leaves the sidecar version intact. Do not build yet: No export presets, cleanup or long render handling yet. ### Phase 5 · Exports, long renders and the disk Build: Named export presets with a resolution, bitrate and container, recorded on every render. Render in segments so an interrupted export resumes rather than restarting. Move the index to the front of the delivered file so it can start playing before it has fully downloaded. Report disk usage per recording and project, and delete scratch files on a stated schedule with the recordings themselves never deleted automatically. Trap: An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. Done when: An export interrupted at seventy percent resumes and finishes without re-rendering the first seventy, tested by killing it. The delivered file begins playing from a web server before it has fully downloaded, verified by request. Automatic cleanup never removes a file in the recordings directory, asserted in the suite. Each project reports its disk usage and the total is visible in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-track compositing, transitions and titles, which is a video editor rather than a recorder with a trimmer, and open editors already do it well. - Webcam overlay with background removal, which needs a model choice and a licence check and adds a second capture device to synchronise. - Hosting and sharing, which is a bandwidth bill and a permissions model, and a file on a disk is a fine deliverable. - Hardware encoder tuning per machine, which is a per-graphics-card project and gives you nothing until the rest works. ### README must contain - That recordings are written fragmented or in segments because an unfinished plain MP4 is undecodable, with the documentation reference - That screen capture produces a genuinely variable frame rate and how this build normalises it - That preview cuts are approximate because stream copying lands on a keyframe, while exports are frame accurate - That the original recording is never modified and every export starts from it - Exactly what automatic cleanup deletes, and that recordings are never in that list ===== ARCHITECTURE.md ===== # Architecture · Camtasia ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The platform screen capture source through FFmpeg, written as fragments or segments | an interrupted normal MP4 is an unreadable file, and the documentation says so | | Processing | FFmpeg for everything after capture, driven from Python | codecs are a solved problem you should not be solving | | Editing | A non-destructive cut list over the original file, never edits in place | the original is the thing that cannot be recreated | | Preview | Fast approximate preview, exact render only on export | waiting for an exact render on every trim makes the tool unusable | | Storage | Recordings and renders in separate directories with a disk check before capture | a recording that stops because the disk filled is the same as no recording | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Fragmented or segmented writing and disk checks. | Any capture source, provided an interrupted file still opens. | | Audio | Separate streams and rate normalisation. | Any device, provided all streams share a sample rate. | | Cuts | The non-destructive cut list. | Not swappable for editing the original file. | | Render | Segmented, resumable export and presets. | Any encoder, provided the cut stays frame accurate. | | Storage | Directories, usage reporting and cleanup rules. | Any layout, provided cleanup cannot reach recordings. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RECORDINGS_DIR` · required · Original captures. Never written to by the editor and never touched by cleanup. - `SCRATCH_DIR` · required · Previews and render segments. This is the directory cleanup is allowed to empty. - `RENDERS_DIR` · required · Finished exports, with the index at the front for playback. - `CAPTURE_FPS` · required · The rate the variable capture is normalised to. Higher costs disk linearly. - `SEGMENT_SECONDS` · required · Recording segment length, and the resume granularity for long exports. - `SCRATCH_RETENTION_HOURS` · required · How long previews and render segments survive. Recordings are not included. - `MIN_FREE_GB` · required · Recording refuses to start below this, because a full disk truncates a capture. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Camtasia product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The platform screen capture source through FFmpeg, written as fragments or segments, FFmpeg for everything after capture, driven from Python, A non-destructive cut list over the original file, never edits in place, Fast approximate preview, exact render only on export, Recordings and renders in separate directories with a disk check before capture. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. - An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. ===== MILESTONES.md ===== # Delivery milestones · Camtasia Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A recording that survives the process dying Killing the recorder mid-capture leaves a file that opens and plays up to the moment it stopped. ### Steps 1. Record fragmented or in segments This is the one decision the whole build is arranged around. Files: `src/capture.py`, `tests/test_kill_recovery.py` 2. Check the disk before starting, not during An estimate at the start beats a truncated file at the end. Files: `src/disk.py` 3. Probe and verify the moment recording stops You want to find out now, not when you go to edit it. Files: `src/verify.py` ### Done when - [ ] A killed recorder leaves a playable file - [ ] Every recording is probed and its result stored - [ ] Recording refuses to start without enough space - [ ] Failed verification is visible in the list ### Watch out - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. ## M2 · Audio that stays with the picture Twenty minutes in, the voice is still aligned with what is on screen. ### Steps 1. Normalise the frame rate explicitly Screen capture is genuinely variable, sometimes zero frames per second. Files: `src/capture.py` 2. Keep audio sources as separate streams Fixing a loud notification later is only possible if it is separable. Files: `src/audio.py` 3. Measure drift with a clap at both ends Twenty minutes, because five minutes hides it. Files: `tests/test_drift.py`, `docs/drift-test.md` ### Done when - [ ] Offsets at both ends of twenty minutes are within tolerance - [ ] Microphone and system audio are independently mutable - [ ] All audio streams share one sample rate - [ ] A static minute produces no timestamp jump ### Watch out - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. ## M3 · Cuts that land where you asked Trimming removes exactly the section selected, and the original file is never touched. ### Steps 1. Keep the cut list, never edit the file The original is the only irreplaceable artefact here. Files: `src/cuts.py` 2. Re-encode on export, copy for preview And label the preview as approximate in the interface. Files: `src/export.py`, `tests/test_cut_accuracy.py` 3. Shift timestamps when joining copied segments Otherwise the join produces artefacts that look like corruption. Files: `src/join.py` ### Done when - [ ] An export cut lands within one frame of the request - [ ] The original file is unchanged after editing - [ ] Approximate previews are labelled as such - [ ] Joined segments show no gap or duplicate frame ### Watch out - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. ## M4 · The things a tutorial actually needs Markers, a zoom on the area that matters, and captions that are a file rather than burnt in. ### Steps 1. Capture markers from the keyboard while recording Afterwards you will not remember when it happened. Files: `src/markers.py` 2. Render zoom as crop and scale on export Not as a preview effect that never reaches the file. Files: `src/zoom.py` 3. Emit captions as a sidecar by default A typo should cost a text edit, not a re-render. Files: `src/captions.py` ### Done when - [ ] A recording marker lands within a frame in the editor - [ ] Zoom applies over exactly its stated range - [ ] Captions play from a sidecar file in a standard player - [ ] Burning in is a separate command ### Watch out - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ## M5 · Exports, long renders and the disk (production only) A long export can be resumed, the deliverable plays without downloading fully, and the scratch files do not fill the disk. ### Steps 1. Render in segments so an export can resume An hour of video is long enough for something to interrupt it. Files: `src/render.py`, `tests/test_resume.py` 2. Move the index to the front of the deliverable It is one flag and it is the difference between playing and waiting. Files: `src/export.py` 3. Never let cleanup touch the recordings directory That directory holds the only copies of unrepeatable events. Files: `src/cleanup.py`, `tests/test_cleanup.py` ### Done when - [ ] An interrupted export resumes from where it stopped - [ ] The deliverable plays before it finishes downloading - [ ] Cleanup never touches recordings - [ ] Disk usage is reported per project ### Watch out - An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. ===== OPERATIONS.md ===== # Operations · Camtasia ## Backup The recordings directory, first and separately, because everything else is derived from it. Exports are regenerable from the cut list, and the cut list is small. ## Restore Restore recordings, re-run verification on each, then rebuild one export from its project and compare durations. A restored recording that fails verification was already broken before the backup. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Free disk space against the configured minimum, recordings whose verification failed, and stalled renders. The disk one is the one that bites during a recording rather than after it. ## Incident checklist The incident is an unusable recording, and the verification result recorded at stop time tells you whether it was broken at capture or broke later. That distinction decides whether the recording can be salvaged by remuxing or not at all. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A killed recorder is confirmed to leave a playable file - [ ] Twenty minute drift is confirmed within tolerance at both ends - [ ] An export cut is confirmed to land within one frame - [ ] An interrupted export is confirmed to resume - [ ] Cleanup is confirmed unable to touch recordings ## Launch constraint Do not market omitted Camtasia capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original captures. Never written to by the editor and never touched by cleanup. RECORDINGS_DIR=./data/recordings # Required. Previews and render segments. This is the directory cleanup is allowed to empty. SCRATCH_DIR=./data/scratch # Required. Finished exports, with the index at the front for playback. RENDERS_DIR=./data/renders # Required. The rate the variable capture is normalised to. Higher costs disk linearly. CAPTURE_FPS=30 # Required. Recording segment length, and the resume granularity for long exports. SEGMENT_SECONDS=60 # Required. How long previews and render segments survive. Recordings are not included. SCRATCH_RETENTION_HOURS=48 # Required. Recording refuses to start below this, because a full disk truncates a capture. MIN_FREE_GB=20
# Camtasia · indie build A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The platform screen capture source through FFmpeg, written as fragments or segments | an interrupted normal MP4 is an unreadable file, and the documentation says so | | Processing | FFmpeg for everything after capture, driven from Python | codecs are a solved problem you should not be solving | | Editing | A non-destructive cut list over the original file, never edits in place | the original is the thing that cannot be recreated | | Preview | Fast approximate preview, exact render only on export | waiting for an exact render on every trim makes the tool unusable | | Storage | Recordings and renders in separate directories with a disk check before capture | a recording that stops because the disk filled is the same as no recording | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **FFmpeg with your platform's screen capture support** · free - Why: It is the capture, the processing and the export, and the fragmented container behaviour this build depends on is documented there. - Get it: Your package manager, or ffmpeg.org, then check that your platform's screen capture input is listed. - Verify: ffmpeg -devices lists your screen capture input and a ten second test recording plays - [ ] **Python 3.12 or newer** · free - Why: Drives capture, the cut list and the render queue. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **Disk space and a way to measure it** · free - Why: Screen recording fills a disk faster than people expect and a recording that stops because the volume was full is the failure this build exists to avoid. - Get it: Know your free space and measure the bitrate of a one minute test at your settings. - Verify: A one minute test gives a megabytes per minute figure used by the estimate - [ ] **Screen and microphone permissions granted to the right process** · free - Why: On current desktop systems the capture will silently produce a black screen or no audio until the permission is granted to the exact binary doing the capture. - Get it: Grant screen recording and microphone access, then confirm with a test capture from the same process the app uses. - Verify: A test recording contains both picture and sound - [ ] **Your capture settings and retention rules** · free - Why: Resolution, frame rate and what cleanup may delete all become tests, and deciding them afterwards means accepting whatever the code happened to do. - Get it: Write down the capture resolution and frame rate, the scratch retention period, and the rule that recordings are never deleted automatically. - Verify: All three are in configuration and named in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration If one of those is essential to you, that is the reason to keep paying for Camtasia, and the README should say so rather than pretend.
# Build brief · Camtasia The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screen recorder and trimmer whose recordings survive a crash and whose cuts land where you asked to replace Camtasia. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A recording cannot be re-taken. The demonstration happened, the person spoke, the bug reproduced once. So the recorder is built to survive its own failure before it is built to do anything else. This has a concrete implementation. FFmpeg's own documentation states that a normal MOV or MP4 is undecodable if it is not properly finished, because the index is written at the end, while a fragmented file 'is decodable even if the writing is interrupted'. That is the difference between a crashed two hour recording being a file you can still use and being nothing at all. So record fragmented, or record in segments, verify the file opens as soon as recording stops, and only then produce the tidy deliverable with the index moved to the front for playback. Everything else in this build is negotiable. This is not. ### Stack (fixed, do not substitute) - Capture: The platform screen capture source through FFmpeg, written as fragments or segments · an interrupted normal MP4 is an unreadable file, and the documentation says so - Processing: FFmpeg for everything after capture, driven from Python · codecs are a solved problem you should not be solving - Editing: A non-destructive cut list over the original file, never edits in place · the original is the thing that cannot be recreated - Preview: Fast approximate preview, exact render only on export · waiting for an exact render on every trim makes the tool unusable - Storage: Recordings and renders in separate directories with a disk check before capture · a recording that stops because the disk filled is the same as no recording ### Data model (create this before Phase 1) - recordings(id, started_at, stopped_at, path, container, fragmented, width, height, nominal_fps, audio_streams, bytes, verified_at, verify_result) - sources(recording_id, kind, device, sample_rate, channels) · kind is screen, microphone or system audio, each kept as its own stream - projects(id, recording_id, name, created_at) - cuts(id, project_id, ordinal, start_ms, end_ms, action) · action is keep or drop, applied to the original, never to a previous export - markers(id, project_id, at_ms, label) · set while recording, because that is when you know - renders(id, project_id, preset, output_path, started_at, finished_at, state, log_excerpt, bytes) - The cuts table is a description of the edit. The recording file is never modified, so every export starts from the original. ### Phase 1 · A recording that survives the process dying Build: Capture to a fragmented container, or to timed segments that are stitched afterwards. Check free disk space before starting and estimate how long the space will last at the chosen settings. When recording stops, probe the file and record whether it opened, its duration and its stream list. Refuse to start when the space will not cover the requested duration. Trap: Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. Done when: Killing the recorder process after two minutes leaves a file that opens and plays those two minutes, tested by actually killing it. Every recording is probed after stopping and its duration and streams are stored. Recording refuses to start when free space would not cover the requested duration at the configured bitrate, tested with a small volume. A recording that failed verification is marked in the list rather than sitting there looking normal. Do not build yet: No editing, preview, captions or export presets yet. ### Phase 2 · Audio that stays with the picture Build: Screen capture sources deliver frames when the screen changes, so the real frame rate varies and can drop to nothing during a still moment. Normalise to a constant rate with the frame rate filter at capture or at the first processing step rather than assuming one. Keep the microphone and any system audio as separate streams at one sample rate. Measure drift on a twenty minute test by clapping at both ends. Trap: Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. Done when: A twenty minute recording of a clap at the start and the end shows audio and video offsets within a stated tolerance at both, measured. Microphone and system audio are separate streams in the file and can be muted independently, checked. All audio streams share one sample rate, verified by probing. A minute of a completely static screen does not produce a gap or a timestamp jump, tested. Do not build yet: No cut list, preview, captions or export presets yet. ### Phase 3 · Cuts that land where you asked Build: Store cuts as a list against the original. For preview, seek and copy without re-encoding and tell the user the preview is approximate. For export, re-encode so the cut is frame accurate. If you ever stitch copied segments, set the flag that shifts timestamps to start at zero, or the join will misbehave. Trap: Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. Done when: An export cut at a stated time starts within one frame of it, measured by reading timestamps. The original recording file is unmodified after any number of edits, verified by checksum. A preview built by copying is labelled as approximate wherever it appears. Segments joined for export produce no timestamp gap or duplicated frame at the join, tested with three segments. Do not build yet: No captions, zoom or export presets yet. ### Phase 4 · The things a tutorial actually needs Build: Set markers with a keyboard shortcut while recording, since that is when you know something happened. Support a zoom region over a time range, rendered by cropping and scaling on export. Produce captions as a sidecar subtitle file from a transcript, with burning in as an explicit second output rather than the default. Trap: Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. Done when: A marker set during recording appears at the right time in the editor, within a frame, tested. A zoom region renders as a crop and scale over exactly the stated time range and returns to full frame afterwards, checked visually and by dimensions. Captions are produced as a sidecar file that plays in a standard player. Burning in is a separate command that leaves the sidecar version intact. Do not build yet: No export presets, cleanup or long render handling yet. ### Phase 5 · Exports, long renders and the disk Build: Named export presets with a resolution, bitrate and container, recorded on every render. Render in segments so an interrupted export resumes rather than restarting. Move the index to the front of the delivered file so it can start playing before it has fully downloaded. Report disk usage per recording and project, and delete scratch files on a stated schedule with the recordings themselves never deleted automatically. Trap: An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. Done when: An export interrupted at seventy percent resumes and finishes without re-rendering the first seventy, tested by killing it. The delivered file begins playing from a web server before it has fully downloaded, verified by request. Automatic cleanup never removes a file in the recordings directory, asserted in the suite. Each project reports its disk usage and the total is visible in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-track compositing, transitions and titles, which is a video editor rather than a recorder with a trimmer, and open editors already do it well. - Webcam overlay with background removal, which needs a model choice and a licence check and adds a second capture device to synchronise. - Hosting and sharing, which is a bandwidth bill and a permissions model, and a file on a disk is a fine deliverable. - Hardware encoder tuning per machine, which is a per-graphics-card project and gives you nothing until the rest works. ### README must contain - That recordings are written fragmented or in segments because an unfinished plain MP4 is undecodable, with the documentation reference - That screen capture produces a genuinely variable frame rate and how this build normalises it - That preview cuts are approximate because stream copying lands on a keyframe, while exports are frame accurate - That the original recording is never modified and every export starts from it - Exactly what automatic cleanup deletes, and that recordings are never in that list
# Agent instructions · Camtasia indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The platform screen capture source through FFmpeg, written as fragments or segments, FFmpeg for everything after capture, driven from Python, A non-destructive cut list over the original file, never edits in place, Fast approximate preview, exact render only on export, Recordings and renders in separate directories with a disk check before capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export.
# Build plan · Camtasia A local screen recorder and trimmer whose hard problems are keeping a recording usable when the process dies, keeping audio aligned with a capture whose frame rate is genuinely variable, and cutting where the user asked rather than at the nearest keyframe. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A recording that survives the process dying Killing the recorder mid-capture leaves a file that opens and plays up to the moment it stopped. ### Steps 1. Record fragmented or in segments This is the one decision the whole build is arranged around. Files: `src/capture.py`, `tests/test_kill_recovery.py` 2. Check the disk before starting, not during An estimate at the start beats a truncated file at the end. Files: `src/disk.py` 3. Probe and verify the moment recording stops You want to find out now, not when you go to edit it. Files: `src/verify.py` ### Done when - [ ] A killed recorder leaves a playable file - [ ] Every recording is probed and its result stored - [ ] Recording refuses to start without enough space - [ ] Failed verification is visible in the list ### Watch out - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. ## Phase 2 · Audio that stays with the picture Twenty minutes in, the voice is still aligned with what is on screen. ### Steps 1. Normalise the frame rate explicitly Screen capture is genuinely variable, sometimes zero frames per second. Files: `src/capture.py` 2. Keep audio sources as separate streams Fixing a loud notification later is only possible if it is separable. Files: `src/audio.py` 3. Measure drift with a clap at both ends Twenty minutes, because five minutes hides it. Files: `tests/test_drift.py`, `docs/drift-test.md` ### Done when - [ ] Offsets at both ends of twenty minutes are within tolerance - [ ] Microphone and system audio are independently mutable - [ ] All audio streams share one sample rate - [ ] A static minute produces no timestamp jump ### Watch out - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. ## Phase 3 · Cuts that land where you asked Trimming removes exactly the section selected, and the original file is never touched. ### Steps 1. Keep the cut list, never edit the file The original is the only irreplaceable artefact here. Files: `src/cuts.py` 2. Re-encode on export, copy for preview And label the preview as approximate in the interface. Files: `src/export.py`, `tests/test_cut_accuracy.py` 3. Shift timestamps when joining copied segments Otherwise the join produces artefacts that look like corruption. Files: `src/join.py` ### Done when - [ ] An export cut lands within one frame of the request - [ ] The original file is unchanged after editing - [ ] Approximate previews are labelled as such - [ ] Joined segments show no gap or duplicate frame ### Watch out - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. ## Phase 4 · The things a tutorial actually needs Markers, a zoom on the area that matters, and captions that are a file rather than burnt in. ### Steps 1. Capture markers from the keyboard while recording Afterwards you will not remember when it happened. Files: `src/markers.py` 2. Render zoom as crop and scale on export Not as a preview effect that never reaches the file. Files: `src/zoom.py` 3. Emit captions as a sidecar by default A typo should cost a text edit, not a re-render. Files: `src/captions.py` ### Done when - [ ] A recording marker lands within a frame in the editor - [ ] Zoom applies over exactly its stated range - [ ] Captions play from a sidecar file in a standard player - [ ] Burning in is a separate command ### Watch out - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ## Not in this build - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. ## After v1, if you want it - Automatic zoom from cursor movement, computed after recording from a cursor position log - A silence remover that proposes cuts you accept, driven from the same cut list - A second export preset for a different platform, which is configuration rather than code
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original captures. Never written to by the editor and never touched by cleanup. RECORDINGS_DIR=./data/recordings # Required. Previews and render segments. This is the directory cleanup is allowed to empty. SCRATCH_DIR=./data/scratch # Required. Finished exports, with the index at the front for playback. RENDERS_DIR=./data/renders # Required. The rate the variable capture is normalised to. Higher costs disk linearly. CAPTURE_FPS=30 # Required. Recording segment length, and the resume granularity for long exports. SEGMENT_SECONDS=60 # Required. How long previews and render segments survive. Recordings are not included. SCRATCH_RETENTION_HOURS=48 # Required. Recording refuses to start below this, because a full disk truncates a capture. MIN_FREE_GB=20
# Camtasia · product brief ## Problem Camtasia's solo core is compact: build a local screen recording + editing workflow that imports user-owned media, applies the core edit or transformation, previews it, and exports a standard file. A competent builder can reach a useful personal version in one sitting, while the paid product mainly wins on media pipeline, hosting, distribution. ## Product outcome A recorder whose files survive a crash, whose audio stays aligned over a long session, whose cuts land where you asked, and whose cleanup can never delete the one thing that cannot be recorded again. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - desktop with sufficient storage ## Explicit non-goals for v1 - Multi-track compositing, transitions and titles. - Webcam overlay with background removal. - Hosting and sharing. - Per-machine hardware encoder tuning. - production codecs, rendering speed, and media templates - hosted recording reliability - large-scale rendering - distribution and podcast hosting - professional codecs, effects, and collaboration ## Success criteria - A killed recorder is confirmed to leave a playable file - Twenty minute drift is confirmed within tolerance at both ends - An export cut is confirmed to land within one frame - An interrupted export is confirmed to resume - Cleanup is confirmed unable to touch recordings
# Build brief · Camtasia The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a screen recorder and trimmer whose recordings survive a crash and whose cuts land where you asked to replace Camtasia. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A recording cannot be re-taken. The demonstration happened, the person spoke, the bug reproduced once. So the recorder is built to survive its own failure before it is built to do anything else. This has a concrete implementation. FFmpeg's own documentation states that a normal MOV or MP4 is undecodable if it is not properly finished, because the index is written at the end, while a fragmented file 'is decodable even if the writing is interrupted'. That is the difference between a crashed two hour recording being a file you can still use and being nothing at all. So record fragmented, or record in segments, verify the file opens as soon as recording stops, and only then produce the tidy deliverable with the index moved to the front for playback. Everything else in this build is negotiable. This is not. ### Stack (fixed, do not substitute) - Capture: The platform screen capture source through FFmpeg, written as fragments or segments · an interrupted normal MP4 is an unreadable file, and the documentation says so - Processing: FFmpeg for everything after capture, driven from Python · codecs are a solved problem you should not be solving - Editing: A non-destructive cut list over the original file, never edits in place · the original is the thing that cannot be recreated - Preview: Fast approximate preview, exact render only on export · waiting for an exact render on every trim makes the tool unusable - Storage: Recordings and renders in separate directories with a disk check before capture · a recording that stops because the disk filled is the same as no recording ### Data model (create this before Phase 1) - recordings(id, started_at, stopped_at, path, container, fragmented, width, height, nominal_fps, audio_streams, bytes, verified_at, verify_result) - sources(recording_id, kind, device, sample_rate, channels) · kind is screen, microphone or system audio, each kept as its own stream - projects(id, recording_id, name, created_at) - cuts(id, project_id, ordinal, start_ms, end_ms, action) · action is keep or drop, applied to the original, never to a previous export - markers(id, project_id, at_ms, label) · set while recording, because that is when you know - renders(id, project_id, preset, output_path, started_at, finished_at, state, log_excerpt, bytes) - The cuts table is a description of the edit. The recording file is never modified, so every export starts from the original. ### Phase 1 · A recording that survives the process dying Build: Capture to a fragmented container, or to timed segments that are stitched afterwards. Check free disk space before starting and estimate how long the space will last at the chosen settings. When recording stops, probe the file and record whether it opened, its duration and its stream list. Refuse to start when the space will not cover the requested duration. Trap: Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. Done when: Killing the recorder process after two minutes leaves a file that opens and plays those two minutes, tested by actually killing it. Every recording is probed after stopping and its duration and streams are stored. Recording refuses to start when free space would not cover the requested duration at the configured bitrate, tested with a small volume. A recording that failed verification is marked in the list rather than sitting there looking normal. Do not build yet: No editing, preview, captions or export presets yet. ### Phase 2 · Audio that stays with the picture Build: Screen capture sources deliver frames when the screen changes, so the real frame rate varies and can drop to nothing during a still moment. Normalise to a constant rate with the frame rate filter at capture or at the first processing step rather than assuming one. Keep the microphone and any system audio as separate streams at one sample rate. Measure drift on a twenty minute test by clapping at both ends. Trap: Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. Done when: A twenty minute recording of a clap at the start and the end shows audio and video offsets within a stated tolerance at both, measured. Microphone and system audio are separate streams in the file and can be muted independently, checked. All audio streams share one sample rate, verified by probing. A minute of a completely static screen does not produce a gap or a timestamp jump, tested. Do not build yet: No cut list, preview, captions or export presets yet. ### Phase 3 · Cuts that land where you asked Build: Store cuts as a list against the original. For preview, seek and copy without re-encoding and tell the user the preview is approximate. For export, re-encode so the cut is frame accurate. If you ever stitch copied segments, set the flag that shifts timestamps to start at zero, or the join will misbehave. Trap: Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. Done when: An export cut at a stated time starts within one frame of it, measured by reading timestamps. The original recording file is unmodified after any number of edits, verified by checksum. A preview built by copying is labelled as approximate wherever it appears. Segments joined for export produce no timestamp gap or duplicated frame at the join, tested with three segments. Do not build yet: No captions, zoom or export presets yet. ### Phase 4 · The things a tutorial actually needs Build: Set markers with a keyboard shortcut while recording, since that is when you know something happened. Support a zoom region over a time range, rendered by cropping and scaling on export. Produce captions as a sidecar subtitle file from a transcript, with burning in as an explicit second output rather than the default. Trap: Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. Done when: A marker set during recording appears at the right time in the editor, within a frame, tested. A zoom region renders as a crop and scale over exactly the stated time range and returns to full frame afterwards, checked visually and by dimensions. Captions are produced as a sidecar file that plays in a standard player. Burning in is a separate command that leaves the sidecar version intact. Do not build yet: No export presets, cleanup or long render handling yet. ### Phase 5 · Exports, long renders and the disk Build: Named export presets with a resolution, bitrate and container, recorded on every render. Render in segments so an interrupted export resumes rather than restarting. Move the index to the front of the delivered file so it can start playing before it has fully downloaded. Report disk usage per recording and project, and delete scratch files on a stated schedule with the recordings themselves never deleted automatically. Trap: An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove. Done when: An export interrupted at seventy percent resumes and finishes without re-rendering the first seventy, tested by killing it. The delivered file begins playing from a web server before it has fully downloaded, verified by request. Automatic cleanup never removes a file in the recordings directory, asserted in the suite. Each project reports its disk usage and the total is visible in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Multi-track compositing, transitions and titles, which is a video editor rather than a recorder with a trimmer, and open editors already do it well. - Webcam overlay with background removal, which needs a model choice and a licence check and adds a second capture device to synchronise. - Hosting and sharing, which is a bandwidth bill and a permissions model, and a file on a disk is a fine deliverable. - Hardware encoder tuning per machine, which is a per-graphics-card project and gives you nothing until the rest works. ### README must contain - That recordings are written fragmented or in segments because an unfinished plain MP4 is undecodable, with the documentation reference - That screen capture produces a genuinely variable frame rate and how this build normalises it - That preview cuts are approximate because stream copying lands on a keyframe, while exports are frame accurate - That the original recording is never modified and every export starts from it - Exactly what automatic cleanup deletes, and that recordings are never in that list
# Architecture · Camtasia ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | The platform screen capture source through FFmpeg, written as fragments or segments | an interrupted normal MP4 is an unreadable file, and the documentation says so | | Processing | FFmpeg for everything after capture, driven from Python | codecs are a solved problem you should not be solving | | Editing | A non-destructive cut list over the original file, never edits in place | the original is the thing that cannot be recreated | | Preview | Fast approximate preview, exact render only on export | waiting for an exact render on every trim makes the tool unusable | | Storage | Recordings and renders in separate directories with a disk check before capture | a recording that stops because the disk filled is the same as no recording | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Fragmented or segmented writing and disk checks. | Any capture source, provided an interrupted file still opens. | | Audio | Separate streams and rate normalisation. | Any device, provided all streams share a sample rate. | | Cuts | The non-destructive cut list. | Not swappable for editing the original file. | | Render | Segmented, resumable export and presets. | Any encoder, provided the cut stays frame accurate. | | Storage | Directories, usage reporting and cleanup rules. | Any layout, provided cleanup cannot reach recordings. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `RECORDINGS_DIR` · required · Original captures. Never written to by the editor and never touched by cleanup. - `SCRATCH_DIR` · required · Previews and render segments. This is the directory cleanup is allowed to empty. - `RENDERS_DIR` · required · Finished exports, with the index at the front for playback. - `CAPTURE_FPS` · required · The rate the variable capture is normalised to. Higher costs disk linearly. - `SEGMENT_SECONDS` · required · Recording segment length, and the resume granularity for long exports. - `SCRATCH_RETENTION_HOURS` · required · How long previews and render segments survive. Recordings are not included. - `MIN_FREE_GB` · required · Recording refuses to start below this, because a full disk truncates a capture. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Camtasia product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The platform screen capture source through FFmpeg, written as fragments or segments, FFmpeg for everything after capture, driven from Python, A non-destructive cut list over the original file, never edits in place, Fast approximate preview, exact render only on export, Recordings and renders in separate directories with a disk check before capture. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. - An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove.
# Delivery milestones · Camtasia Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A recording that survives the process dying Killing the recorder mid-capture leaves a file that opens and plays up to the moment it stopped. ### Steps 1. Record fragmented or in segments This is the one decision the whole build is arranged around. Files: `src/capture.py`, `tests/test_kill_recovery.py` 2. Check the disk before starting, not during An estimate at the start beats a truncated file at the end. Files: `src/disk.py` 3. Probe and verify the moment recording stops You want to find out now, not when you go to edit it. Files: `src/verify.py` ### Done when - [ ] A killed recorder leaves a playable file - [ ] Every recording is probed and its result stored - [ ] Recording refuses to start without enough space - [ ] Failed verification is visible in the list ### Watch out - Recording straight to a plain MP4 and relying on a clean stop means a crash, a full disk or a forced reboot leaves a file with no index, and the documentation is explicit that such a file is undecodable, so the recording you cannot retake is gone. Record fragmented or in segments and verify the file immediately after stopping. ## M2 · Audio that stays with the picture Twenty minutes in, the voice is still aligned with what is on screen. ### Steps 1. Normalise the frame rate explicitly Screen capture is genuinely variable, sometimes zero frames per second. Files: `src/capture.py` 2. Keep audio sources as separate streams Fixing a loud notification later is only possible if it is separable. Files: `src/audio.py` 3. Measure drift with a clap at both ends Twenty minutes, because five minutes hides it. Files: `tests/test_drift.py`, `docs/drift-test.md` ### Done when - [ ] Offsets at both ends of twenty minutes are within tolerance - [ ] Microphone and system audio are independently mutable - [ ] All audio streams share one sample rate - [ ] A static minute produces no timestamp jump ### Watch out - Assuming the capture produced the frame rate you asked for gives a file whose timestamps wander, and the audio appears to drift further out of sync the longer the recording runs, which is discovered at the end of a long tutorial. Normalise the frame rate explicitly, and note that combining a fixed output rate with a passthrough frame rate mode is rejected outright, so choose one deliberately. ## M3 · Cuts that land where you asked Trimming removes exactly the section selected, and the original file is never touched. ### Steps 1. Keep the cut list, never edit the file The original is the only irreplaceable artefact here. Files: `src/cuts.py` 2. Re-encode on export, copy for preview And label the preview as approximate in the interface. Files: `src/export.py`, `tests/test_cut_accuracy.py` 3. Shift timestamps when joining copied segments Otherwise the join produces artefacts that look like corruption. Files: `src/join.py` ### Done when - [ ] An export cut lands within one frame of the request - [ ] The original file is unchanged after editing - [ ] Approximate previews are labelled as such - [ ] Joined segments show no gap or duplicate frame ### Watch out - Trimming by seeking and stream copying looks instant and lands the cut at the nearest keyframe before the point you chose, which with a common encoder default can be several seconds early, so the export keeps the throat clearing you were removing. Re-encode on export for accuracy, keep copying for the preview, and say which is which in the interface. ## M4 · The things a tutorial actually needs Markers, a zoom on the area that matters, and captions that are a file rather than burnt in. ### Steps 1. Capture markers from the keyboard while recording Afterwards you will not remember when it happened. Files: `src/markers.py` 2. Render zoom as crop and scale on export Not as a preview effect that never reaches the file. Files: `src/zoom.py` 3. Emit captions as a sidecar by default A typo should cost a text edit, not a re-render. Files: `src/captions.py` ### Done when - [ ] A recording marker lands within a frame in the editor - [ ] Zoom applies over exactly its stated range - [ ] Captions play from a sidecar file in a standard player - [ ] Burning in is a separate command ### Watch out - Burning captions into the only exported file means a typo in a name costs a full re-render of an hour of video, and the version with the typo is already uploaded. Emit a subtitle file beside the video by default, and make the burnt-in copy a separate deliberate export. ## M5 · Exports, long renders and the disk (production only) A long export can be resumed, the deliverable plays without downloading fully, and the scratch files do not fill the disk. ### Steps 1. Render in segments so an export can resume An hour of video is long enough for something to interrupt it. Files: `src/render.py`, `tests/test_resume.py` 2. Move the index to the front of the deliverable It is one flag and it is the difference between playing and waiting. Files: `src/export.py` 3. Never let cleanup touch the recordings directory That directory holds the only copies of unrepeatable events. Files: `src/cleanup.py`, `tests/test_cleanup.py` ### Done when - [ ] An interrupted export resumes from where it stopped - [ ] The deliverable plays before it finishes downloading - [ ] Cleanup never touches recordings - [ ] Disk usage is reported per project ### Watch out - An automatic cleanup that treats recordings like temporary files eventually deletes the only copy of something that cannot be recorded again, usually while you are on holiday. Delete scratch and preview files automatically, never recordings, and make recording deletion a deliberate action that names what it is about to remove.
# Operations · Camtasia ## Backup The recordings directory, first and separately, because everything else is derived from it. Exports are regenerable from the cut list, and the cut list is small. ## Restore Restore recordings, re-run verification on each, then rebuild one export from its project and compare durations. A restored recording that fails verification was already broken before the backup. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Free disk space against the configured minimum, recordings whose verification failed, and stalled renders. The disk one is the one that bites during a recording rather than after it. ## Incident checklist The incident is an unusable recording, and the verification result recorded at stop time tells you whether it was broken at capture or broke later. That distinction decides whether the recording can be salvaged by remuxing or not at all. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A killed recorder is confirmed to leave a playable file - [ ] Twenty minute drift is confirmed within tolerance at both ends - [ ] An export cut is confirmed to land within one frame - [ ] An interrupted export is confirmed to resume - [ ] Cleanup is confirmed unable to touch recordings ## Launch constraint Do not market omitted Camtasia capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Original captures. Never written to by the editor and never touched by cleanup. RECORDINGS_DIR=./data/recordings # Required. Previews and render segments. This is the directory cleanup is allowed to empty. SCRATCH_DIR=./data/scratch # Required. Finished exports, with the index at the front for playback. RENDERS_DIR=./data/renders # Required. The rate the variable capture is normalised to. Higher costs disk linearly. CAPTURE_FPS=30 # Required. Recording segment length, and the resume granularity for long exports. SEGMENT_SECONDS=60 # Required. How long previews and render segments survive. Recordings are not included. SCRATCH_RETENTION_HOURS=48 # Required. Recording refuses to start below this, because a full disk truncates a capture. MIN_FREE_GB=20
$ choose a build depth, inspect the files, then open the complete pack in your agent
Camtasia: People pay to avoid codec failures, upload limits, rendering surprises, lost recordings, and distribution chores.
xproduction codecs, rendering speed, and media templates
xhosted recording reliability
xlarge-scale rendering
xdistribution and podcast hosting
xprofessional codecs, effects, and collaboration
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Camtasia pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| camtasia starter | — | $3.25/user | Camtasia Online, Snagit and 4K/60fps recording; up to 25 Screencast videos |
| camtasia essentials | — | $14.99/user | Desktop Camtasia Editor, Audiate and Snagit; unlimited captions and multitrack editing |
| camtasia create | — | $20.75/user | Everything in Essentials plus script and voice generation with 200+ voices |
| camtasia pro | — | $49.92/user | 49 avatars, translation and dubbing, image generation, 100M+ stock assets and unlimited sharing |
| business teams | — | $16.50/user | Transferable team licenses for up to 99 users |
| enterprise | custom | — | Custom licensing, deployment, procurement, security and support |
free tierno fully featured free desktop tier; the downloaded editor can be tried but exports carry a watermark, while Camtasia Online has a free tier whose numeric limits are not publicly stated
billingannual subscriptions only for current suites; no monthly self-serve billing
hidden costsIndividual licenses are tied to one user and two devices; transferable team licensing costs more, and AI/stock entitlements differ sharply by suite.
verified 2026-08-14 · source ↗
Vibecode Camtasia
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Camtasia replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Camtasia cost?
Camtasia's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Camtasia?
Honestly: production codecs, rendering speed, and media templates; hosted recording reliability; large-scale rendering; distribution and podcast hosting; professional codecs, effects, and collaboration. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Camtasia?
Yes: Recordly (Screen recording, auto-zoom and a real editor; young enough that audio still occasionally picks a fight.) The prompt is for when you want it exactly your way.