Skip to content

Fix freezes, silent deletes and other problems found testing v1.1.0 with real footage - #82

Merged
danielchalmers merged 73 commits into
mainfrom
claude/v1-1-0-local-testing-4af8a5
Oct 7, 2026
Merged

danielchalmers merged 73 commits into
mainfrom
claude/v1-1-0-local-testing-4af8a5

Conversation

@danielchalmers

@danielchalmers danielchalmers commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Fixes for the problems found by testing the v1.1.0 release against a real TeslaCam library (174 clips, 25.5 hours, 271 GB). The app was driven through its real window, like a person reviewing their footage, then each finding was reproduced again from scratch and traced to its cause.

Each fix is its own commit so history stays readable; merge without squashing to keep it. Every commit builds and passes the tests on its own. The last line of each message names the findings it fixes; the ids match the report.

Playback

  • Seeking to the end froze the player for the rest of the session. A click on the last few pixels of the seek bar, or Right within 5 s of the end, seeked every camera to exactly the clip's duration. Flyleaf's accurate seek found no frame there and left the camera's decoder thread parked in a state nothing wakes, so Play showed Pause over frozen video, and every clip opened afterwards stayed frozen until a restart. Seeks now stop 100 ms before each camera's own footage ends. A second commit covers the same freeze at 2x and faster: Flyleaf skips frames during fast playback even in a paused seek, so the seek could still drain to end of file. Paused seeks and frame steps now decode at 1x, and a safety net wakes a decoder found parked this way. A sweep of 18 real clips at 16x went from 20 of 36 runs frozen to 0. (F52)
  • Clicking the seek bar during playback was mostly ignored. WPF's Slider marks a click on the rail handled before the window's PreviewMouseDown handler runs, so the seek gesture never started and the next position update pulled the thumb back. With a real mouse, 6 of 6 clicks while playing were ignored. (F45)
  • Cameras showed different moments after pausing fast playback. Each camera keeps its own clock while playing, so after 8x or 16x the side cameras sat up to 1.9 s away from the front when paused. They are realigned when playback pauses and when it reaches the end, and the end shows the last frame. (F90, RI-4)
  • A held arrow key lost about a third of its 5 s steps. (F51)
  • When a later video file became unreadable mid-playback, recovery rewound up to a minute. (F13)
  • Every camera shows its last frame when fast playback reaches the end. (F66)
  • Stop works on a paused or finished clip, Play after Stop restores the now-playing badge, Event is disabled after Stop, and Stop closes an open trim panel. (F03, F07, F56, F73)
  • Keyboard focus stays on Previous frame, Next frame and Event after pressing them, and holding the seek thumb across a clip change no longer drags the new clip to the old position. (RI-1, RI-3)

Deleting clips

  • "Delete to Recycle Bin" could delete footage permanently without saying so. The menu item now reads "Delete clip…". On a drive without a Recycle Bin (network drives, USB flash drives, very long paths), the shell falls back to a permanent delete, and the app passed flags that suppress its warning. Worse, if a file was in use, the files before it were already destroyed by the time Windows asked. The confirmation now says when a delete is permanent, and a locked file aborts the delete before anything is touched. (F96 and the locked-file case in cluster K)
  • After deleting the playing clip, Next and Previous continue from its place in the list; a cancelled delete keeps the player where it was; a clip whose folder is already gone is simply removed. (F97, F98, F99)

Library, search and navigation

  • A search that hid the playing clip blanked the video to "Select a clip to begin" while the clip kept playing unseen, and re-selecting that clip afterwards left Play, Seek and Trim disabled for good. (F77, F76)
  • With a search active, Next and Previous played clips outside the results while the list still highlighted the old one. They now stay inside the results, and the list scrolls to whatever Next or Previous opens. (F39, F42)
  • With several folders loaded, Next and Previous follow the list's date order, and overlapping folders no longer list clips twice. (F41, F33)
  • Search matches the dates and times the list shows ("Dec 16", "Tue", "3:53 PM"), several words at once, and no longer matches every clip on words from parent folders like "saved". Escape clears the box and then leaves it. (F79, F80, F81, F82)
  • Picked folders are remembered across restarts, so footage on an internal drive doesn't need re-picking every launch. (lead Use ffmpeg to render and play videos #1)
  • Rescan keeps the open clip; an empty or missing folder says so instead of "Select a clip to begin". Ctrl+clicking the open clip's row no longer leaves it playing unseen, and the list shows the playing clip or the top results after a search and opens a newly picked folder at its newest clip. (F31, F29, F28, F130)
  • List rows show each clip's real length, label saved and Sentry clips by their folder when event.json is missing, show the city without coordinates, and fall back to the missing-thumbnail glyph for a corrupt thumb.png. (F84, F85, F22, F86)

Damaged and unusual footage

  • About 4 in 10 saved clips opened at 0:00 with no event marker. Tesla stamps a saved clip's event at the save moment, 0.04 to 1.88 s after the last recorded frame, and the timeline only mapped instants inside the footage. Events within a few seconds after the end now map to the end, which also makes "Save event clip" work for these clips instead of always failing. (F58, F67)
  • An intact numbered copy (front-2.mp4) is used when the original file is damaged; on the test drive this restores a lost minute. (F14)
  • Zero-filled or text files are no longer reported as "encrypted by the vehicle"; unreadable front files say why they can't play. (F15, F18)
  • Clips in very long folder names play, an event.json with a repeated field no longer hides the clip, hidden or system subfolders are scanned, scanning a large tree no longer holds a handle for every folder, and camera tiles are offered only for cameras the clip can play. (F16, F17, F35, F30, F59)

Trim and export

  • Exports show progress and a confirmation in the window, can be cancelled, and closing during an export asks first and stops FFmpeg, where before FFmpeg kept running orphaned and its temp script leaked. (RI-5, RI-6)
  • The delete, export and folder dialogs open over the Sentry Deck window instead of wherever Windows puts them, which could be another monitor.
  • The save dialog starts in Videos with a sortable name (2025-12-16_15-53-27 front 0m39s-2m01s.mp4) instead of on the dashcam drive, the trim panel says which camera an export saves, the selected length rounds correctly, and a failed export no longer hides the playing video. (lead Rename to "Sentry Replay" #2, F70, F69, F67)

First run, About and packaging

  • With FFmpeg missing, Rescan or Select folder removed the Download FFmpeg prompt for the rest of the session and clips "played" as black video. The missing state now persists until FFmpeg is installed. (F26, F32, F34)
  • The release exe crashed silently when run without its five loose WPF DLLs, for example after copying just SentryDeck.exe out of the zip. The publish now embeds them. The MSI still builds; WiX now warns that its publish/<arch>/** include harvests no extra files, which is expected. (F27)
  • About shows 1.1.0 (not 1.1.0.0) and says when the update check failed; playback pauses while About covers the player; several instances share one log and tag each line with their process id; a startup failure exits with a logged error instead of leaving an invisible process. (F111, F107, F108, F109, cluster J)

Accessibility and polish

  • Screen readers hear clips and day groups as dates, reasons and places instead of raw timestamps, and hear Play/Pause, trim and speed state. Selecting a camera tile through UI Automation or Enter switches the camera, Tab skips the invisible stops in the video area, and the search clear button has a name. (F88, F127, lead Add installer as download option #3, RI-2, F128, F87, F126)
  • Disabled buttons look disabled, the seek bar no longer shifts when the time gains a digit or collapses when the sidebar is dragged wide, gap markers stand out from chunk seams, the trim range stays visible over the played part, the event marker's tooltip opens, the trim hint wraps, and the F1/Esc shortcuts appear in tooltips and the About list. (F122, F11, F49, F120, F71, F110)

Testing

  • Unit tests: 398 before, 721 after. Each fix commit adds tests that fail without it, and every commit was built and tested on its own (git rebase --exec).
  • The fixed build was tested again in the real app with every original reproduction plus a control case: 91 of 103 checks passed outright on the first pass. A second round then fixed what that pass found: five small regressions from this PR and five older problems. The re-check passed all of them except one rounding edge in the list length (a 9:28 clip reads "~10 min").
  • The end-of-clip freeze sweep (18 real clips, paused and playing routes, 16x): 20 of 36 runs froze before, 0 after.
  • Six fixes need a real keyboard or a visible screen to confirm and are covered by tests and review only: Escape in the search box, the clear button's name, Enter on a camera tile, the trim range and gap marker styling, and the seek bar with the sidebar dragged wide.

Not changed (your call)

  • Next/Previous direction: Next goes to newer clips, which is up the newest-first list.
  • 16x reaches about 12x on an idle machine (a FlyleafLib decode limit); the readout still says 16x.
  • A trim spanning a gap marker exports continuous video that skips the missing time without saying so.
  • The 2x2 grid leaves out the pillar cameras on six-camera clips; the camera labels over the video are drawn under the native video surface and never show; only media time is shown, not the time of day; the seek bar is still narrow at small window sizes.
  • The fixed 10 s event lead-in at high speed, reason colors taken from the accent, which folder is loaded, and notices for skipped chunks or cameras.

Risks

  • The decoder safety net in the end-of-clip fix sets FlyleafLib 3.11.5's VideoDecoder.Status when it finds a parked decode thread. It is reasoned from the decompiled source and covered by the sweep, but it depends on Flyleaf internals and should be rechecked on any FlyleafLib upgrade.
  • A delete can still be permanent without a warning on a fixed drive whose Recycle Bin is turned off or too small for the clip.
  • In history, the event-time fix lands a few commits before the end-of-clip seek limit, so those intermediate commits can freeze on Jump to event for clips stamped just past their end. HEAD has both.
  • MainWindow.xaml.cs grew to about 485 lines of view-only plumbing (seek gesture, list scrolling, search Escape); it could move into small helper classes later.

A saved clip whose event.json time fell a moment after its last recorded frame opened at 0:00 instead of just before the event.
It showed no event marker, Jump to event was disabled, and "Save event clip" failed with "The event moment isn't within this clip's saved footage."
In the tested library that was 67 of 173 clips.

Tesla stamps event.json at the save moment, and both it and the chunk file names are truncated to whole seconds, so the event lands up to about 2 s past the probed end of the footage as often as just inside it.
ClipMediaSource.ToMediaTime mapped only instants inside the footage or exactly at its end, so these events had no media time at all.

An instant within GapThreshold (5 s) after the last chunk's end now maps to the end of the footage.
These clips now open 10 s before the end with the event marker there, and Save event clip exports the last 30 s.
Instants further past the end still map to nothing.

Found in v1.1.0 local testing: F58, F67, F100.
On a drive that was copied or merged, a camera file can have a truncated original (no moov) beside an intact numbered copy such as front-2.mp4.
The app always played the original, so a single-chunk clip failed with "No front camera footage found." and in a longer clip that minute silently disappeared, although readable footage was on disk.
A damaged rear or side original likewise cut that camera short.

CamChunk picked between a file and its copies only by whether the original was empty, so a non-empty but unreadable original always won, and the playlist builder then dropped it.

When a camera has several files at one timestamp, the first one with a playable duration now wins, originals before copies, per camera.
If none is playable, the old rule still applies.
The playability check (a positive duration of at most 10 minutes) moved from the playlist builder to Mp4DurationReader.TryReadChunkDuration, so the file chosen at scan time is never one the builder rejects.
Only cameras with more than one file are probed, so scanning cost is unchanged for ordinary clips.

Found in v1.1.0 local testing: F14.
A clip whose front file was zero-filled (a power-loss leftover) or plain text renamed to .mp4 was reported as "encrypted by the vehicle", with advice to turn off Encrypt Dashcam Recordings in the car.
That sent the user to change a car setting for a file that is simply damaged.

EncryptedClipDetector treated any file of 8 or more bytes whose first box type wasn't a known MP4 box as encrypted, so a header of zeros or ASCII text qualified, and a single-chunk clip needs only one such file.

The detector now also requires the first 4 KiB to vary the way ciphertext does: a file where one byte value fills more than half the sample (zero or 0xFF filler) or that is at least 90% printable text is no longer called encrypted.
Those clips take the ordinary unreadable-footage path instead.
Random encryption-shaped containers are still detected.

Found in v1.1.0 local testing: F15.
A clip in a folder renamed to a long name without a date (about 223 characters or more) would not play.
It showed a raw "filename, directory name, or volume label syntax is incorrect" error that repeated the whole folder name and an internal temp path.

Such a folder keeps its full name as the clip name, and FfconcatMediaSourceBuilder put that whole name into every camera's playlist file name.
With the path hash and a camera suffix such as -right_repeater.ffconcat added, the file name passed the 255-character limit and writing the playlist failed.

The clip name part of the playlist file name is now capped at 64 characters.
The hash of the clip's folder path still keeps playlists of different clips apart, and date-named Tesla folders (19 characters) get the same file names as before.

Found in v1.1.0 local testing: F16.
An event.json with a repeated field (two "city" entries, say) next to a field the strict parser rejects (like Tesla's occasional blank est_lat) made the whole clip vanish from the list. The lenient fallback read the file through JsonObject, which throws on a repeated name, and CamClip.TryMap then skipped the folder with only a log warning. A field whose name or string value held a lone surrogate escape threw the same way.

The fallback now reads the fields from a JsonDocument, which allows repeated names; the last one wins, as on the strict path, so which path runs can't change the value. A field name or string value that can't be decoded is passed over instead of throwing, so the clip keeps the fields that do parse. Falling back to the lenient path now logs the file and the offending field.

Found in v1.1.0 local testing: F17.
When a folder between the picked root and the clips (SavedClips, SentryClips) carried the Hidden or System attribute, which copy and repair tools sometimes leave behind, none of the clips under it appeared, and nothing in the app or the log said why. The scan used .NET's default enumeration options, which skip Hidden and System folders and never descend into them.

The scan now walks hidden and system folders too. The Recycle Bin and System Volume Information are skipped by name instead, so picking a drive root still doesn't bring back clips the app's own Delete sent to the Recycle Bin. ACL-denied folders, such as the compatibility junctions Windows keeps in every profile, are still skipped as before.

Found in v1.1.0 local testing: F35.
Picking a folder with thousands of clip folders (a full SavedClips or SentryClips) made the scan hold an open handle to every one of them at once: about 500 extra handles for 500 clips, growing with the width of the library. .NET's recursive directory enumeration walks breadth-first and opens a handle to each subfolder when it queues it, closing it only once it gets around to reading that folder.

The scan now walks the tree depth-first, reading one folder's subfolders at a time and closing it before going deeper, so the walk holds at most one directory handle however wide the library is. A folder that can't be listed, such as a corrupt directory on the car's USB drive, now costs only what is beneath it instead of ending the scan, because depth-first order reaches each event folder before its later siblings. Hidden, system and ACL-denied folders, the Recycle Bin, and a folder deleted mid-scan are handled as before.

Found in v1.1.0 local testing: F30.
Most rows in the clip list read a minute longer than the player then showed: a standard ten-minute save said "~11 min" against a 9:28 to 10:56 timeline, and a 10 s clip said "~1 min". The list counted every chunk as a full minute, but Tesla almost always cuts the last chunk short when recording stops, so the estimate always ran high, and a clip with a gap in its recording read several minutes too long.

The scan now reads the front-camera length from the MP4 header of the last chunk, and of any chunk whose next one doesn't start about a minute later (a gap in the recording), and counts the other chunks as full minutes. File-name spacing alone can't bound a chunk, because the car's clock can jump. A chunk with no readable length still counts as a minute, and a header claiming more than a minute is capped there, so a corrupt file can't inflate the estimate. A clip shorter than half a minute reads "<1 min" instead of rounding to "~1 min" or the no-footage dash.

Found in v1.1.0 local testing: F84, F04, F36, F123.
A clip in SavedClips or SentryClips with no event.json (footage from firmware that didn't write one), or with a blank reason, was listed as "Recent" with the grey stripe, and searching "Recent" returned it. The label came from the event reason alone and a missing reason always meant Recent, so the TeslaCam folder the clip sits in was never consulted.

Each clip now records which TeslaCam folder it sits in (RecentClips, SavedClips or SentryClips, from its own folder name or its parent's), and a missing or blank reason falls back to it, so these clips read "Saved" or "Sentry" with that stripe color. The list row, search and the seek bar's event marker (its color and tooltip) use the same label. Clips with a reason keep their reason label, and clips outside Tesla's folders still read "Recent".

Found in v1.1.0 local testing: F85.
A clip whose event.json named a city but had blank, unreadable or 0,0 coordinates showed no place on its card, even though searching that city found it, so there was no way to see why it matched. A clip with coordinates but no city showed a lone pin with nothing beside it. The card's location line was shown or hidden by whether the event had map coordinates, not by whether it had a city to show.

The location line now follows the city: it shows whenever the event names one and stays hidden otherwise, whatever the coordinates say. "Show on map" still needs real coordinates. A long city name that is cut off with an ellipsis now shows in full in a tooltip.

Found in v1.1.0 local testing: F22, F104, F125.
A clip whose thumb.png was corrupt or zero bytes showed an empty grey tile in the list, which looked like a thumbnail that never finished loading rather than a clip with no preview. The glyph was shown only when the file was missing, while a file that exists but can't be decoded produced no image, so neither the image nor the glyph appeared.

The glyph now follows the decoded image itself: it shows whenever there is no image to draw, whether the file is missing, empty or unreadable. This also drops a second file-existence check per list row.

Found in v1.1.0 local testing: F86, F23.
Clicking the right end of the seek bar, or pressing Right within 5 s of the end, seeked every camera to exactly the clip's duration, which is past each stream's last frame. Flyleaf's accurate seek then found no frame: the video kept the stale pre-seek picture while the time read end / end, and the seek left that camera's decoder thread parked in a state nothing ever wakes. From then on Play showed Pause over frozen video, and every clip opened afterwards stayed frozen on those cameras (the rear often kept playing alone) until the app was restarted.

Seeks now stop 100 ms short of where each camera's own footage ends, so they aim at a real frame instead of past the last one, and "go to end" shows the clip's final moments instead of a stale picture. One frame of margin is not enough: Tesla files show their last frame up to about 60 ms before the duration they record, and side cameras often end a few hundred milliseconds before the front (up to about 0.7 s on real footage). The media source therefore reports each camera's footage length, and every seek path clamps each camera to its own end: seek-bar clicks, arrow keys, scrub release, the open-time jump to the event, and the side-camera realignment on resume and frame step. A clip sent to its end still replays from the start on Play.

In later testing one real clip still froze its left and right cameras after a paused seek to the end, so this margin may not cover every file, and the stuck decoder itself needs a follow-up.

Found in v1.1.0 local testing: F52, F38, F46, F47, F57, and cluster A's new finding (only the rear kept playing).
After fast-forwarding at 8x or 16x and pausing, the side camera views showed moments up to about 1.5 s away from the front, each off by a different amount, and the time readout sat about 0.3 s behind the front picture. Frame steps from there could keep the side cameras several frames ahead.

Each camera's player keeps its own clock. A camera that can't decode fast enough drops its late frames and restarts its clock wherever its decoder caught up, so the cameras drift apart while playing fast, and pausing froze that drift as it was. At that speed a camera's reported time also trails the frame it shows, because the next frame is drawn before its time is published.

Pausing now lines the cameras up on the front's moment. After faster-than-real-time playback every camera, the front included, is seeked there, so every view and the readout show the same instant. At real time only a side camera that is actually out of step moves. A frame step that has to pause playback first does the same before stepping.

Found in v1.1.0 local testing: F90, F94, and cluster F's new finding (readout behind the picture after a 16x pause).
At 8x or 16x a clip could finish with the front showing a frame up to 0.64 s before its last one while the readout said the clip had ended. A side camera that ran out of footage could also stop on an early frame.

Past real time the front drops the frames it can't decode in time, and when its decoder runs out it jumps its clock to the end without drawing the rest. The end handler only paused the cameras and set the readout to the end.

When a clip ends, every camera that ran out of footage is now seeked onto its own last frame, and the readout follows the front. At real time a camera already on its last frame is left alone. A side camera that lagged behind and is still mid-stream stays where it stopped, because seeking it to its last moments from there can park its decoder for good: an earlier version that also moved those cameras froze them in one of three 16x runs on real footage. Such a camera can still end up to about 0.7 s short of its end.

Found in v1.1.0 local testing: RI-4, and F66 in part.
If a clip's next video file became unreadable while the clip was playing (locked by another program, moved, or deleted), playback jumped back to the start of the minute just watched, so up to a minute replayed with no explanation.

The concat demuxer fails as it reaches the unreadable file, which surfaces at the end of the healthy chunk before it. Recovery probes the files and correctly drops the bad one, but it always reopened at the start of the chunk under the failure position, which here is the healthy chunk that was playing.

Recovery now maps the failure position onto the rebuilt timeline: it stays on the same footage when that chunk survives, lands where the following footage begins when the chunk itself was dropped, and accounts for chunks dropped before it. So playback carries straight on into the next readable chunk, or parks at the end when nothing follows. The skipped segment is still only logged; telling the user about it is F19.

Found in v1.1.0 local testing: F13.
Holding Left or Right lost about a quarter to a third of its 5 s steps, so a three-second hold landed a minute or more short of where it should have. Separate presses landed exactly, which made the shortfall look random.

Each step is measured from where playback is headed: the target of a seek still waiting to run, or else the on-screen position. But a seek stops counting as waiting the moment it starts running, and the position only moves once every camera has landed. A held key repeats sooner than that, so a repeat arriving mid-seek started again from the old position and the step it added was lost; with several repeats per seek the video could even step back.

A running seek's target now counts as the starting point until that seek lands, so every repeat adds its five seconds.

Found in v1.1.0 local testing: F51.
A clip whose front file was empty, truncated, held open by another program, or moved away after the scan always showed "No front camera footage found.", even with the file sitting right there. A locked file is only a temporary problem, but nothing said so, and pressing Play after the lock was released played the clip under the old error.

The builder drops every chunk whose front file has no readable duration, whatever the reason, and the controller turned that into one generic message.

The controller now looks at the clip's front files when none could be played. A file another program holds open says so and suggests closing that program and pressing Play; a file that opens but can't be read says it may be damaged or incomplete; files that are gone suggest a rescan; and a clip with no front files keeps the old message. When Play then opens the clip, the stale playback error is cleared instead of covering the video.

Found in v1.1.0 local testing: F18.
At 2x and faster, sending a clip to its end (a seek-bar click while paused, or Right while playing) and pressing Play could still leave cameras frozen, often the left and right ones, on that clip and every clip after it until the app restarted. The end-of-footage margin from the earlier fix didn't help, because the seek wasn't looking at every frame.

Flyleaf thins decoding for fast playback: faster than about 1.7x its video decoder keeps only one frame in every speed * fps / 61 (one in ten at 16x) and drops the rest. A paused seek decodes through that same path, so near the end of a camera's footage it could drop every frame left after the target. Flyleaf then marks the decoder ended while its thread is parked, and nothing ever wakes that thread again. Away from the end, the same thinning made paused seeks at fast speeds land up to a quarter of a second late.

Each Flyleaf player now decodes at real time whenever it seeks or steps a frame while paused, and only takes the chosen speed when it starts playing. As a safety net for any other way a paused seek could run out of frames, a decoder thread that Flyleaf left parked behind a finished status is woken before the camera plays and before its clip closes.

On 18 real clips at 16x, seeking to the end and pressing Play froze at least one camera in 20 of 36 runs before this change and in none after it, and the next clip played on every camera each time.

Found in v1.1.0 local testing: F52.
"Delete to Recycle Bin" always promised a recoverable delete, but the shell only recycles when it can. On a network share, a removable drive such as a TeslaCam USB stick, or a clip whose camera file paths reach 260 characters, it deleted the clip permanently instead, without a word, because the delete call suppresses the shell's own permanent-delete warning. That permanent delete also goes one file at a time, so a file held open by another program stopped it partway: the other cameras, event.json and thumb.png were already gone, the app reported "The operation was canceled", and the gutted clip stayed in the list.

The delete now checks first whether Windows can recycle the folder (a network path or drive, a removable or other non-fixed drive, a path of MAX_PATH or more, or SHQueryRecycleBin failing). When it can't, the confirmation asks "Permanently delete this clip?" with the reason and defaults to No; if the check itself fails, nothing is deleted. After playback stops and before anything is deleted, every file in the folder must open for exclusive access; if one is in use, nothing is deleted and the overlay names the file. The menu item now reads "Delete clip…", because only the confirmation can tell whether a clip will go to the Recycle Bin.

Found in v1.1.0 local testing: F96, and the new locked-file data loss in cluster K.
After deleting the clip that was playing, Previous clip was disabled and Next clip opened the oldest clip in the library, which can be years away at the bottom of the list. Removing the open clip from the playlist reset its position to "nothing selected", and from there Next starts at the first clip and there is nothing before it.

The playlist now remembers the slot the deleted clip held, so Next opens the clip that followed it and Previous the clip before it, the same neighbours the user would have reached without the delete. Selecting a clip or loading a new list clears that remembered place.

Found in v1.1.0 local testing: F97.
…lips

When a delete of the open clip didn't go ahead (a file in use, a shell error, or Cancel or Skip in the shell's own dialog), the player stayed stopped and black at 0:00 with seeking disabled. Playback is closed before the delete so Windows can remove the files, and nothing reopened it afterwards. Pressing Cancel or Skip was also reported as "Delete Failed: The operation was canceled.", because the shell delete throws OperationCanceledException for both. Deleting a clip whose folder had already been removed outside the app showed a raw "Could not find directory" error over the video and left the ghost clip in the list.

Now the open clip is reopened at the position it had: paused when a notice covers the video, and playing again after a Cancel, which shows no error. A folder that is already gone counts as deleted, so its clip just leaves the list. VideoPlayerController gains ReopenAsync, which opens a stopped clip at a given position instead of at the event, and does nothing if another clip has been opened since.

Found in v1.1.0 local testing: F98, F99.
"Copy clip name" and "Copy timestamp" both copied the same text, such as "12/16/2025 15:53:27". The clip name is the folder's date reformatted for display, and the timestamp used the invariant culture's US month-first date, so neither gave the name Tesla and the drive use for the clip, and a date like 03/04/2025 reads as April 3 in day-first countries.

"Copy clip name" now copies the clip's folder name (for example 2025-12-16_15-53-27), falling back to the display name for a clip read from the root of a drive. "Copy timestamp" copies a year-first ISO 8601 timestamp (2025-12-16 15:53:27). The copy commands now go through an overridable clipboard action so tests can check what each one copies.

Found in v1.1.0 local testing: F101.
With more than one folder loaded, Next and Previous went folder by folder instead of by date, so they jumped around the list and Next could be disabled partway through it.
Clips sharing a timestamp made them zig-zag up and down the list even with one folder, and a folder picked together with a folder inside it listed every clip in the overlap twice.
The player's playlist was each folder's clips concatenated in pick order, while the list sorted everything by date with a tie-break the playlist didn't mirror.
The merged clips are now sorted once in the list's order (newest first, then name, then path), the list shows that order, and the playlist is its exact reverse.
Repeated folders and folders inside another picked folder are scanned only once.

Found in v1.1.0 local testing: F41, F33, and the tied-timestamp zig-zag (new in cluster E).
Searching for anything the playing clip didn't match blanked the video to "Select a clip to begin" while the clip kept playing out of sight, with Pause, Stop, the seek bar and Trim still acting on it, and Play disabled if it was paused.
Clearing the search didn't bring it back: its row had the now-playing badge but no highlight, and the pane stayed empty.
The list can't keep a hidden row selected, so it wrote null back through its two-way selection binding, and the app took that as the user closing the clip.
The list now binds to a separate list selection that ignores the null for a row the search hides, so the clip stays selected, playing and visible.
When a later search shows the clip again, its row is highlighted again without reopening it or closing an open trim panel.

Found in v1.1.0 local testing: F77, F06, F54, F106, F113.
With a search active, Next and Previous opened clips the search hides, and stayed enabled at the first and last result.
The list can't highlight a row it doesn't show, so it kept highlighting the clip they left (now without its now-playing badge), and clicking that row did nothing.
The player's playlist is the whole library, and Next and Previous stepped through it one clip at a time whatever the search showed.
The playlist now takes a navigation filter, and the clip list hands it the search, so Next and Previous skip to the nearest clip the list shows and are disabled when there is none.
The list highlight follows every clip they open.
A playing clip the search hides stays open, and Next and Previous continue from its place in the library.

Found in v1.1.0 local testing: F39, F78, F112.
Rescanning a picked folder that had been renamed, moved, unplugged, or locked down emptied the list without a word, as if the footage had been deleted.
Picking a folder with no clips (the wrong folder, or a freshly formatted Tesla drive) or deleting the last clip did the same.
Each time, the overlay said "Select a clip to begin. Your dashcam timeline is on the left." beside an empty list.
The scan skips any folder it can't read so one damaged clip can't hide the rest, so a missing or unreadable root came back as an empty library and never reached the scan's own error messages.
A root that yields no clips is now opened once more without that leniency, so a missing folder shows "Folder Not Found" and a denied one shows "Access Denied".
A library that is empty after a clean scan, or after deleting its last clip, now says "No clips found in this folder", names the folders it looked in, and offers Select folder.
That notice, the first-run "No dashcam footage yet" prompt, and a scan error that leaves no clips now stay up until a scan finds clips, because dismissing one only uncovered "Select a clip to begin" beside the empty list again.
They cover only the video area, so the sidebar can still pick a folder or rescan.

Found in v1.1.0 local testing: F29, F28, F102, F114.
Pressing Rescan to pick up newly copied footage closed the clip being reviewed: the list came back with nothing selected, the player read 0:00 / 0:00, and the clip had to be found in the list again and sought back to.
Rescan stopped the player, the scan cleared the selection, and every clip came back as a newly read object that nothing matched to the one that was open.
Rescan now notes where the open clip was (position, playing or paused, and camera) and, when a clip from the same folder is still there, selects it again and opens it at that place.
It opens straight onto the remembered frame instead of starting at the event and seeking back, and keeps the camera on screen rather than jumping to the event's camera.
A clip the user had stopped stays closed, and a scan that reports a folder error still shows that error rather than reopening the clip over it.

Found in v1.1.0 local testing: F31.
Searching "saved" on a real Tesla drive also listed every Honk and Alert clip, and a word from the folder the footage was copied to (such as "Sentry" in "D:\Sentry backups") matched every clip.
Typing a date or time the way a row or day header shows it ("Dec 16", "Tue", "3:53 PM", "October 30") found nothing, and a two-word query like "hutto honk", or a trailing space, emptied the list.
Search compared the whole query, untrimmed, as one substring against the clip's absolute path, its hidden invariant name, the city, and the reason.
Each word of the search now has to match some part of the clip: its own folder name, its name, the city, the reason, or the date, time, and day header the list shows.
A day typed after a month name stays with it, so "October 30" finds that date and not other October days with a time like 5:30 PM.
The list and the search now take those dates from one shared helper, so they read the same in the user's culture.
A pasted "Copy path" value still finds its clip as one term, spaces and all, and it is the only search that looks at the folders above the clip.

Found in v1.1.0 local testing: F79, F80, F81.
Footage copied to the PC, or on any drive that isn't removable, was gone again after every restart: the app opened on "No dashcam footage yet" and the folder had to be picked again each launch.
Copying footage off the car's USB drive is common because the car reformats it, so this hit anyone who kept their clips on the PC.
Auto-discovery only looks next to the app and on removable drives, and the picked folders were held only in memory.
Picking folders now saves them to settings.json under %LOCALAPPDATA%\SentryDeck, and the next launch loads the ones that still exist.
Each window reads them once, at its first scan, so a rescan keeps its own folders even after another window picks different ones.
A remembered folder that is gone, such as one on a drive that isn't plugged in, is skipped without an error, and when none are left the app auto-discovers drives as before.
The settings file never stops the app: one that can't be read or written is logged and treated as empty.

Found in v1.1.0 local testing: lead #1.
Clearing a search that had hidden the open clip, then clicking that clip again, left Play/Pause, the seek bar, frame step and Trim greyed out for good, and hiding the clip again showed a "Loading..." overlay that never went away.
Selecting a clip marks it as loading and asks the player to move to it, but the player ignores a move to the clip it already has, so nothing ever cleared the loading state.
Selecting the clip the player already has now takes the player's own loading state instead: a playing or paused clip carries on where it was, and a stopped or failed one is reopened as a normal selection would.

Found in v1.1.0 local testing: F76, F05, F53, F105.
Opening About or Help (F1 or the info button) while a clip played left it running behind the page. The page covers the player and deliberately ignores Space, and TeslaCam clips have no sound, so nothing showed that footage was passing by. A user who checked a shortcut came back seconds later, or to a clip that had run to its end.

Opening the page now pauses a playing clip. It stays paused when the page closes, on the frame the user left, until they press Play.

Found in v1.1.0 local testing: F108.
Every instance started while another was running found today's log file locked and opened a numbered copy (log-20261005_001.txt, _002, ...). The log folder keeps seven files, not seven days, so each copy deleted the oldest file, and a few extra launches pushed earlier days' logs out of the folder, including the one a bug report needed.

Instances now share today's file and append to it, so the folder holds one file per day again. Each line now names its process, so lines from windows open at the same time can still be told apart.

Found in v1.1.0 local testing: F109.
When the main window failed to load at startup (seen with WPF's native libraries missing beside the exe, and true of any error while building the window), the handler that keeps the app running after unexpected errors caught the failure. No window ever appeared, yet the process stayed running, idle and invisible, until it was ended in Task Manager, and every relaunch added another one.

The app now opens its main window itself and treats a failure there as fatal: it logs the error, shows "Sentry Deck couldn't start" with the reason and the log folder, and exits with code 1. Closing the half-built window during that exit no longer trips over the view-model it never created. Errors after the window is up still keep the app running as before.

Found in v1.1.0 local testing: the invisible process after a startup failure (new in cluster J).
The portable zip held SentryDeck.exe plus five WPF native libraries (PresentationNative_cor3.dll, wpfgfx_cor3.dll, and three more). At 180 MB the exe looked self-contained, but copied out on its own, or run straight from Explorer's view of the zip, it crashed within a second of starting: no window, no message, and nothing in the app log, because it failed before logging was set up.

The release build now bundles those libraries into the exe, which unpacks them to a cache under %TEMP% on first launch. The zip holds just SentryDeck.exe, and that file runs on its own. The MSI installs the same single exe.

Found in v1.1.0 local testing: F27.
Clicking the seek bar's rail while a clip was playing usually did nothing: playback carried on from where it was, and a normal-length click failed almost every time.
Paused clips were unaffected.

With IsMoveToPointEnabled, Slider's own class handler jumps the thumb to a rail press and marks the press handled before any instance handler runs, so the XAML PreviewMouseDown handler never saw it and the seek gesture never started.
While playing, the 100 ms position sync then put the thumb back before mouse-up, and the release seek went to where playback already was.

The window now adds the press and release handlers with handledEventsToo, so every press starts the gesture.
A press off the thumb also captures the mouse, so releasing it outside the bar still ends the gesture instead of leaving playback held paused.

Found in v1.1.0 local testing: F45, F01, F25, F10.
The seek bar's event marker binds a tooltip with the event's reason and time ("Honk · 3:53 PM"), but hovering it never showed anything.
The marker wasn't hit-testable, and the Track's transparent page buttons covered the whole bar above it, so it could never become the hovered element.

The page buttons now let the mouse through (they never act anyway: every press off the thumb becomes a jump to that point first), and the bar's background keeps the whole bar clickable.
The marker is hit-testable, with a few pixels of transparent margin on each side so the tooltip doesn't need pixel-exact aim; the tick itself is drawn where it was.
Clicking the marker still seeks there, and the thumb, drawn above it, still wins when the two overlap.

Found in v1.1.0 local testing: the event-marker tooltip finding in cluster U.
Where a clip's footage skips real time (a missing or excluded chunk, or a Sentry idle period), the seek bar drew a gap tick that nobody could see.
A gap always falls on a chunk seam, and the gap tick was shorter and dimmer than the seam tick at the same spot, so it hid inside it and skipped time looked like an ordinary one-minute seam.

The gap tick is now taller, wider and drawn in the primary text color, so it covers the seam tick and reads as a different mark on both the played and unplayed parts of the bar.
Hovering it explains it ("Gap in the footage: time skips ahead here"), with the same widened hover target as the event marker.

Found in v1.1.0 local testing: F120.
After the usual trim flow (mark in, play, mark out) the selected range sits over the played part of the seek bar, and there it all but vanished: only a faint fringe and the end ticks showed.
The range band was the accent color at 30% opacity, the same color as the played fill underneath it.

The band is now a neutral, slightly taller highlight, so it reads as a lighter section over both the played fill and the unplayed rail.

Found in v1.1.0 local testing: F71.
The seek bar shrank by about 7 px whenever the time readout got one character longer, such as when playback crossed 10:00.
The bar and every marker on it shifted sideways, even in the middle of a drag.

The readout sat in an auto-width column next to the bar, which took up whatever width the text left.
It now keeps the width of the longest time the open clip can show and is right-aligned, so the bar keeps one width while a clip plays and the duration stays in place.
A clip under 10 minutes keeps the narrower readout it had before, so the bar doesn't lose width to digits that clip never shows.

Found in v1.1.0 local testing: F11, F44.
Pressing Next or Previous (or Ctrl+Right/Left) opened a clip whose row the list never scrolled to.
After a few steps no visible row was highlighted and nothing on screen said which clip was playing, so finding it meant scrolling through the whole list.
When the keyboard was in the list, its focus also stayed on the previous clip's row, so the arrow keys carried on from there.

WPF only brings a row into view when the user picks it in the list itself, and nothing in the app asked it to for a selection made any other way.

The list now scrolls to every newly selected clip once its rows are laid out, which works for the grouped, virtualized list where the row may not exist yet.
While the keyboard is in the list, focus moves to the new row as well.

Found in v1.1.0 local testing: F42, F08, F95, F116.
After Ctrl+F, Escape did nothing in the search box: the query stayed, focus stayed, and every playback shortcut kept typing into the query instead (Space added a space and emptied the list).
The only ways back were Tab or the mouse.

The window hands every key to the box while it has focus, and a TextBox gives Escape no meaning of its own, so nothing acted on it.
The clear-search command had no key or button left since the app's own clear button was replaced by the theme's.

Escape in the search box now clears the query, and Escape on an empty box moves the keyboard back to the player so the shortcuts work again (or to the clip list when no clip is open).

Found in v1.1.0 local testing: F82.
Screen-reader users who reached the x button in the search box heard only "button", with nothing saying it clears the search.

The button comes from WPF's Fluent theme, whose template gives it no automation name, and the app relied on it after replacing its own named clear button.

The window now names the button "Clear search" and gives it a tooltip naming the Escape shortcut.
It does so whenever the box gets keyboard focus, which is the only time the theme shows the button, so a template the theme swaps in later is covered too.

Found in v1.1.0 local testing: F87.
Unavailable buttons looked like the most pressable ones on screen.
A disabled Event, Stop, Previous, Next, Rescan or About button kept its full-white icon and label and gained a grey filled, outlined box, while the enabled ones are flat, so Event on a clip with no locatable event looked highlighted and did nothing when clicked.
Select folder kept its full-white label during a scan as well.

These buttons build on the Fluent button style, which gives a disabled button a faint fill and outline and dims only plain-text content.
The app's buttons hold their own icon and label TextBlocks, which take their color from the button, so nothing dimmed them.

The flat icon buttons now stay flat when disabled, and both the flat buttons and the Select folder button dim their icon and label to the theme's disabled text color.

Found in v1.1.0 local testing: F122, F24, F37.
Below about 1390 px of window width, every step of the trim panel's walkthrough lost its last words to an ellipsis ("...then set the..."), so the guidance that should make trimming self-explanatory read as broken at common laptop sizes.

The hint was a single line that trimmed with an ellipsis, in the only column left between the panel's title and its four buttons, and the steps are longer than that column at those widths.

The hint now wraps onto a second line when it doesn't fit, so every step reads in full.

Found in v1.1.0 local testing: F68, F121.
Nothing in the app said that F1 opens About / Help or that Esc leaves it.
Every other toolbar tooltip names its key, so that is where users learn shortcuts, but the About button's tooltip read just "About / Help", the Back button's read "Back to player", and the help page's shortcut list mentioned Esc only for cancelling a trim.
The list also didn't say that the numpad digits switch cameras like the number row does.

The tooltips now read "About / Help (F1)" and "Back to player (Esc)", the shortcut list gains an "F1 / Esc: Open / close this help page" row, and the camera row notes that the numpad works too.

Found in v1.1.0 local testing: F110.
A camera tile could only be chosen with the mouse.
Selecting the Rear tile through UI Automation, as screen readers and other accessibility tools do, highlighted the tile while the front camera stayed enlarged, so the strip and the video disagreed.
Pressing Enter on a tile reached with Tab did nothing either, and Space is the play and pause shortcut, so the number keys were the only keyboard route.

The tiles are radio buttons, which run their command only when clicked: UI Automation's selection just checks them, and they ignore Enter, which buttons accept and radio buttons don't.

The tiles now choose their camera the way a click does when UI Automation selects them or when Enter is pressed on them.

Found in v1.1.0 local testing: RI-2, lead #3.
… player

Screen readers met clutter around the video.
Each camera tile sat inside an extra list item announced as "SentryDeck.CameraViewOption", and the seek bar held one item per chunk seam and gap tick, read out as raw fractions like "0.0936324907".

The camera strip and both tick layers are ItemsControls, which wrap every item in a list item named by the item's text: the class name for a camera option, and the number itself for a tick.

The camera strip is now a group named "Camera views" that holds the tiles directly, and the tick layers stay out of the tree that screen readers walk.

Found in v1.1.0 local testing: F126.
Tabbing from the About button to the player controls took about a dozen extra presses through stops that showed nothing and had no name: the sidebar splitter, the video area, every video slot and the video surface inside it, the camera strip itself, and the empty slot plus surface inside each camera tile.
Screen readers announced most of them as nothing, and keyboard users saw no focus ring move.

WPF makes every ContentControl and ItemsControl a tab stop by default, the video area is focusable on purpose so the window can park focus there after a click, and the splitter can't be moved with the arrow keys because they seek the video.

The slots, surfaces and camera strip are no longer focusable, the video area keeps taking parked focus but is skipped by Tab, and the splitter is out of the tab order, with a name for screen readers that find it.

Found in v1.1.0 local testing: F128.
Screen readers read the clip list as raw timestamps.
Each day group was announced as "12/16/2025 12:00:00 AM", and each clip only as "12/16/2025 15:53:27", without the event type, place or length that the card shows; the list itself had no name.

A day group's name came from the DateTime the list groups by, because WPF leaves the group's header text out of what screen readers see, and a row's name came from the clip's text form, which is its raw timestamp.

Day groups are now named the way their headers read ("Tuesday, December 16, 2025", "Today"), each clip reads what its card shows ("Tuesday, December 16, 2025, 3:53 PM, Honk, Hutto, about 5 min"), and the list is named "Clips". A clip shorter than a minute reads "under 1 min" rather than the list's "<1 min", which a screen reader can shorten to "1 min".

Found in v1.1.0 local testing: F88, F103.
…st it runs

Screen readers couldn't tell the state of the player controls.
The play button was always "Play or pause", the Trim button didn't say whether the trim panel was open, the speed button read "Playback speed" without the speed, and the seek bar reported only a fraction such as 0.09.

Those names were fixed text, the Trim button was a plain button with no on or off state, and the seek bar's value is a fraction of the clip.

The play button is now named "Play" or "Pause" after what a press will do, with a matching tooltip.
Trim is a toggle that is on while the trim panel is open, keeps its flat look, and still opens and closes the panel when a screen reader toggles it or Enter is pressed.
The speed button reads "Playback speed 2x", and the seek bar's status reads the time, such as "0:48 of 10:33".

Found in v1.1.0 local testing: F127.
At the smallest window width, dragging the sidebar to its widest made the seek bar disappear, so the clip could no longer be seeked with the mouse.
The same happened when the sidebar was widened in a large window and the window was then made smaller.

The seek bar is the only part of the player controls that can shrink, and it only got what the fixed-width buttons, time and speed controls left over.
The sidebar could always reach 440 px whatever the window's width, which at 1200 px left the player too narrow for the seek bar to get any width at all.

The sidebar's widest setting now gives way whenever the player would get narrower than it is with the default sidebar at the smallest window, where the seek bar keeps about 90 px.
In a wider window the sidebar still reaches 440 px, and when the window shrinks it narrows instead of the player, then returns to the width it was dragged to once there is room again.

Found in v1.1.0 local testing: F49.
At 8x or 16x a clip could still end with a side camera stopped short of its own end, up to about half a second behind the front, so the final still showed that camera at an earlier moment than the readout. On a six-camera clip with burned-in times, the left camera stopped 0.47 s early in one of three 16x runs.

The end handler only moved the cameras that had run out of footage onto their last frame. A side camera that lagged behind was still mid-stream and stayed where it stopped, because an earlier version that seeked such a camera to its last moments froze it in one of three runs. That freeze came from paused seeks decoding at the fast speed and skipping every frame left near the end. Paused seeks now decode at real time, and a stranded decoder is woken before the next play.

So when a clip ends, every camera now moves onto its own last frame. At real time a camera already showing it is left alone.

On the synthetic six-camera clip, 9 plays to the end at 16x all ended with every camera on its last frame (front 10.003, rear 9.940, left 9.992, right 9.978, pillars 9.992 and 9.978), and the next play moved every camera each time. Two real clips gave the same result over 10 more 16x runs, checked against their decoded frames.

Found in v1.1.0 local testing: F66.
Ctrl+clicking the selected row of the playing clip left it playing behind "Select a clip to begin", with Pause, Stop, Next/Previous, frame step and Trim still acting on it, and Play disabled once it was paused, until the row was clicked again.
The list writes null back through its selection binding when its row is deselected, and the app took that as the clip closing, though nothing closed it.
The list selection now turns down a deselect of the clip the player has open, as it already does when a search hides that clip, and the window highlights the row again.
A stopped clip has nothing playing out of sight, so its row can still be deselected.

Found in v1.1.0 local testing: B/NEW, the Ctrl+click deselect path left after the search fix.
With Sentry Deck on a second monitor, choosing "Save event clip…" or "Delete to Recycle Bin" from a clip's context menu could open the Save dialog or the delete confirmation on the primary monitor instead, and take the focus there.
The trim panel's Export button opened the same Save dialog over the app.

These dialogs were opened without an owner, so Windows gave them whichever window was active.
From a context menu that is the menu's popup, and when another app is in front there is none, so the dialog went to WPF's hidden window at the top left of the primary monitor, or opened with no owner at all.

The delete confirmation, the export Save dialog and the folder picker now belong to the main window, so they open over it and keep it disabled while they are up, whichever window was active.
Before the main window has opened they still open without an owner, since a file dialog refuses an owner that has no window yet.

Found in v1.1.0 local testing: delete confirmation and export Save dialog opening on the primary monitor.
After a search that still matched the playing clip, or after clearing a search, the clip list kept the scroll offset it had before.
That offset belonged to the old list, so it landed on unrelated rows of the new one: neither the playing clip's highlighted row nor the top results were on screen.

The list only scrolled when its selection changed, and a search that keeps the selected clip doesn't change it.
The library now announces each applied search, including a cleared one, and the window then scrolls the list to the selected clip's row, or to the top of the results when no clip is selected.
The scroll waits for the list's next layout, when the rows of the new results exist.
Deleting a clip doesn't count as a search, so the list stays where the user was tidying.

Found in v1.1.0 local testing: the clip list keeps its old scroll offset after a search or a cleared search.
After picking another folder with Select folder, the clip list opened partway down, with the newest clip and its day header above the view.
Scrolling the previous list by a single row was enough, and it happened with no clip selected too.

The list kept the scroll offset it had in the previous folder's clips.
A scan empties the list and fills it again, but a quick one finishes before the list lays out the empty one, so the offset carried over to rows it had nothing to do with.

The library now announces every load the way it already announces a search, and the window then scrolls the list to the top of the new clips, or to the selected clip's row when a rescan reopens the playing clip.
Refresh already started at the top, because it holds the emptied list on screen for a moment, so it behaves as before.

Found in v1.1.0 local testing: F130 (the list keeps the old scroll offset after picking another folder).
A cut marked where the time readout showed 1:35 and 5:01 was suggested as "2025-10-04_23-24-15 left 1m36s-5m02s.mp4", a second later at both ends than the times the user set the marks at.

The file name rounded each mark to the nearest second, while the readout drops the part of a second, so a mark set in the second half of a second gained a second in the name.

The name now drops it too, so it gives the minutes and seconds the readout showed when each mark was set.
"Save event clip" names its range the same way.

Found in v1.1.0 local testing: the export name disagreeing with the trim marks (found while re-testing lead #2).
@danielchalmers
danielchalmers force-pushed the claude/v1-1-0-local-testing-4af8a5 branch from 60eaed3 to 54fb45c Compare October 6, 2026 14:05
@danielchalmers
danielchalmers merged commit a9349ca into main Oct 7, 2026
1 check passed
@danielchalmers
danielchalmers deleted the claude/v1-1-0-local-testing-4af8a5 branch October 7, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant