From 3acc786fdd6becc9e683623cc3ccc6766d85278a Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Thu, 9 Jul 2026 17:22:24 -0500 Subject: [PATCH 1/2] Explain Tesla 2026.20 clip encryption instead of a generic corrupt-file error Tesla software 2026.20 (May 2026) turns on "Encrypt Dashcam Recordings" by default on Ryzen-infotainment vehicles, writing AES-encrypted containers to the USB drive instead of plain MP4s. The decryption keys are only obtainable from Tesla's servers through the owner's account, so the app cannot play the footage -- but until now it also could not say why: every chunk failed the moov probe, got auto-excluded, and the user saw "No front camera footage found." on a drive full of files. EncryptedClipDetector sniffs the first box header of each chunk's front file: a clip whose files all have content but none starts with a known ISO-BMFF box type is flagged as encrypted (a merely corrupt or truncated recording still carries its ftyp header, so it stays on the existing recovery path). The two give-up sites in VideoPlayerController now show a message that points at the in-car toggle (Controls > Safety > Encrypt Dashcam Recordings) and Tesla's own dashcam.tesla.com viewer. Verified live: a clip of encryption-shaped files (20-byte header + IV-prefixed 4 KiB chunk, no box structure) shows the explanation in the error overlay; header-carrying truncated files still report the ordinary missing-footage message. --- .../Playback/EncryptedClipDetector.cs | 95 ++++++++++++++++ .../EncryptedClipDetectorTests.cs | 102 ++++++++++++++++++ SentryDeck.Tests/Fixtures/TestMp4.cs | 18 ++++ .../VideoPlayerControllerTests.cs | 53 +++++++++ SentryDeck/Playback/VideoPlayerController.cs | 22 +++- 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 SentryDeck.Data/Playback/EncryptedClipDetector.cs create mode 100644 SentryDeck.Tests/EncryptedClipDetectorTests.cs diff --git a/SentryDeck.Data/Playback/EncryptedClipDetector.cs b/SentryDeck.Data/Playback/EncryptedClipDetector.cs new file mode 100644 index 0000000..99986c0 --- /dev/null +++ b/SentryDeck.Data/Playback/EncryptedClipDetector.cs @@ -0,0 +1,95 @@ +using System.Text; +using Serilog; + +namespace SentryDeck; + +/// +/// Heuristics for Tesla's encrypted dashcam recordings. Software update 2026.20 turns on +/// "Encrypt Dashcam Recordings" by default (Controls > Safety), writing AES-encrypted +/// containers to the USB drive instead of plain MP4s, so the files no longer begin with an +/// ISO-BMFF box header. Decryption keys are only obtainable from Tesla's servers via the +/// owner's account (dashcam.tesla.com), so the app can detect the state but not play it. +/// +public static class EncryptedClipDetector +{ + /// + /// Top-level box types an unencrypted recording can plausibly start with. Tesla files start + /// with ftyp; the rest keep the sniff from misreporting other muxers' output — a file + /// starting with any of these is ordinary video (playable or merely corrupt), not encrypted. + /// + private static readonly string[] KnownLeadingBoxTypes = + [ + "ftyp", + "styp", + "moov", + "mdat", + "free", + "skip", + "wide", + "pdin", + "sidx", + "uuid", + ]; + + /// + /// True when the clip's front-camera files are all present with content but none starts like + /// an MP4 — the signature of a drive written with encryption enabled. A merely corrupt or + /// truncated clip still has a valid ftyp header on at least some chunks, so it stays + /// on the ordinary unreadable-file path. + /// + public static bool LooksEncrypted(CamClip clip) + { + if (clip is null || clip.Chunks.Count == 0) + { + return false; + } + + var sawFrontFile = false; + + foreach (var chunk in clip.Chunks) + { + if (!chunk.Files.TryGetValue(CameraNames.Front, out var frontFile)) + { + continue; + } + + sawFrontFile = true; + + if (!LooksEncrypted(frontFile.FullPath)) + { + return false; + } + } + + return sawFrontFile; + } + + /// + /// True when the file has content but does not start with a recognizable MP4 box. + /// + public static bool LooksEncrypted(string path) + { + try + { + using var stream = File.OpenRead(path); + + Span header = stackalloc byte[8]; + if (stream.ReadAtLeast(header, header.Length, throwOnEndOfStream: false) < header.Length) + { + // Shorter than one box header: a truncated write, not an encrypted container + // (those carry a fixed header plus at least one 4 KiB payload chunk). + return false; + } + + var leadingBoxType = Encoding.ASCII.GetString(header[4..]); + return !KnownLeadingBoxTypes.Contains(leadingBoxType); + } + catch (Exception ex) + { + // Unreadable at the filesystem level (missing, locked, ...) is not an encryption + // signal; let the ordinary unreadable-file handling describe it. + Log.Debug(ex, "Could not sniff file header for encryption. File={File}", path); + return false; + } + } +} diff --git a/SentryDeck.Tests/EncryptedClipDetectorTests.cs b/SentryDeck.Tests/EncryptedClipDetectorTests.cs new file mode 100644 index 0000000..814df0d --- /dev/null +++ b/SentryDeck.Tests/EncryptedClipDetectorTests.cs @@ -0,0 +1,102 @@ +using System.IO; + +namespace SentryDeck.Tests; + +public sealed class EncryptedClipDetectorTests : IDisposable +{ + private readonly string _root = Directory.CreateDirectory( + Path.Combine(Path.GetTempPath(), $"SentryDeckTests-{Guid.NewGuid():N}")).FullName; + + public void Dispose() => Directory.Delete(_root, recursive: true); + + private string WriteFile(string name, byte[] bytes) + { + var path = Path.Combine(_root, name); + File.WriteAllBytes(path, bytes); + return path; + } + + [Fact] + public void ValidMp4_IsNotEncrypted() + { + var path = WriteFile("valid.mp4", TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60))); + + EncryptedClipDetector.LooksEncrypted(path).ShouldBeFalse(); + } + + [Fact] + public void EncryptedLookingFile_IsEncrypted() + { + var path = WriteFile("encrypted.mp4", TestMp4.EncryptedLookingBytes); + + EncryptedClipDetector.LooksEncrypted(path).ShouldBeTrue(); + } + + [Fact] + public void TruncatedTinyFile_IsNotEncrypted() + { + // Shorter than one box header: a power-loss truncation, not an encrypted container. + var path = WriteFile("truncated.mp4", [0x00, 0x00, 0x01]); + + EncryptedClipDetector.LooksEncrypted(path).ShouldBeFalse(); + } + + [Fact] + public void TruncatedButValidHeader_IsNotEncrypted() + { + // A recording cut off mid-write still starts with its ftyp box; that's the corrupt + // path, not the encrypted one. + var valid = TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60)); + var path = WriteFile("cutoff.mp4", valid[..12]); + + EncryptedClipDetector.LooksEncrypted(path).ShouldBeFalse(); + } + + [Fact] + public void MissingFile_IsNotEncrypted() + { + EncryptedClipDetector.LooksEncrypted(Path.Combine(_root, "nope.mp4")).ShouldBeFalse(); + } + + private CamClip ClipWithFrontFiles(params byte[][] frontFileContents) + { + var start = new DateTime(2026, 7, 9, 10, 0, 0); + var chunks = frontFileContents.Select((bytes, index) => + { + var timestamp = start.AddMinutes(index); + var path = WriteFile($"{timestamp:yyyy-MM-dd_HH-mm-ss}-front.mp4", bytes); + return new CamChunk(timestamp, [new CamFile(path, timestamp, CameraNames.Front)]); + }).ToList(); + + return new CamClip(_root, "Clip", start, chunks, camEvent: null); + } + + [Fact] + public void Clip_WithAllFrontFilesEncrypted_IsEncrypted() + { + var clip = ClipWithFrontFiles(TestMp4.EncryptedLookingBytes, TestMp4.EncryptedLookingBytes); + + EncryptedClipDetector.LooksEncrypted(clip).ShouldBeTrue(); + } + + [Fact] + public void Clip_WithOnePlayableChunk_IsNotEncrypted() + { + // One valid header among the chunks means ordinary video with some corruption — + // the recovery path should keep handling it. + var clip = ClipWithFrontFiles( + TestMp4.EncryptedLookingBytes, + TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60))); + + EncryptedClipDetector.LooksEncrypted(clip).ShouldBeFalse(); + } + + [Fact] + public void Clip_WithNoChunks_IsNotEncrypted() + { + var clip = new CamClip(_root, "Empty", new DateTime(2026, 7, 9), [], camEvent: null); + + EncryptedClipDetector.LooksEncrypted(clip).ShouldBeFalse(); + EncryptedClipDetector.LooksEncrypted((CamClip)null).ShouldBeFalse(); + } +} diff --git a/SentryDeck.Tests/Fixtures/TestMp4.cs b/SentryDeck.Tests/Fixtures/TestMp4.cs index 780b646..e54ec84 100644 --- a/SentryDeck.Tests/Fixtures/TestMp4.cs +++ b/SentryDeck.Tests/Fixtures/TestMp4.cs @@ -15,6 +15,24 @@ internal static class TestMp4 /// public static byte[] GarbageBytes => [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]; + /// + /// Bytes shaped like Tesla's 2026.20 encrypted recordings: a 20-byte header (with an embedded + /// UUID) followed by an IV-prefixed 4 KiB ciphertext chunk — no MP4 box structure anywhere. + /// + public static byte[] EncryptedLookingBytes + { + get + { + var bytes = new byte[20 + 16 + 4096]; + for (var i = 0; i < bytes.Length; i++) + { + bytes[i] = (byte)(i * 197 + 13); // deterministic pseudo-noise, never ASCII "ftyp" + } + + return bytes; + } + } + /// /// A minimal valid mp4 whose mvhd encodes the given duration (version 0, millisecond timescale). /// diff --git a/SentryDeck.Tests/VideoPlayerControllerTests.cs b/SentryDeck.Tests/VideoPlayerControllerTests.cs index 285c3e6..66b3ee6 100644 --- a/SentryDeck.Tests/VideoPlayerControllerTests.cs +++ b/SentryDeck.Tests/VideoPlayerControllerTests.cs @@ -94,6 +94,59 @@ public async Task SelectingClip_WhenFrontFileMissing_ReportsOpenFailure() front.OpenedPaths.ShouldBeEmpty(); } + [Fact] + public async Task SelectingClip_WhenAllFilesAreEncrypted_ExplainsTheEncryptionToggle() + { + // A drive written by Tesla software 2026.20+ with "Encrypt Dashcam Recordings" on: every + // file exists but none is a playable MP4. The real builder probes and excludes every + // chunk, and the error must point at the encryption toggle, not claim missing footage. + using var clipFiles = TestClipFiles.Create(chunkCount: 2); + foreach (var chunk in clipFiles.Clip.Chunks) + { + foreach (var file in chunk.Files.Values) + { + File.WriteAllBytes(file.FullPath, TestMp4.EncryptedLookingBytes); + } + } + + var front = new FakeCameraPlayer(); + using var controller = CreateController(front, mediaSourceBuilder: new FfconcatMediaSourceBuilder()); + + controller.LoadClips([clipFiles.Clip]); + controller.Playlist.MoveTo(0); + + await WaitUntilAsync(() => controller.ErrorMessage is not null); + + controller.ErrorMessage.ShouldBe(VideoPlayerController.EncryptedClipMessage); + controller.ErrorMessage.ShouldContain("Encrypt Dashcam Recordings"); + controller.IsPlaying.ShouldBeFalse(); + front.OpenedPaths.ShouldBeEmpty(); + } + + [Fact] + public async Task SelectingClip_WhenAllFilesAreGarbage_ButNotEncrypted_KeepsTheCorruptMessage() + { + // Same all-unreadable shape, but the files still carry MP4 headers (truncated writes): + // that's ordinary corruption and must NOT be blamed on encryption. + using var clipFiles = TestClipFiles.Create(chunkCount: 1); + var truncated = TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60))[..12]; + foreach (var file in clipFiles.Clip.Chunks[0].Files.Values) + { + File.WriteAllBytes(file.FullPath, truncated); + } + + var front = new FakeCameraPlayer(); + using var controller = CreateController(front, mediaSourceBuilder: new FfconcatMediaSourceBuilder()); + + controller.LoadClips([clipFiles.Clip]); + controller.Playlist.MoveTo(0); + + await WaitUntilAsync(() => controller.ErrorMessage is not null); + + controller.ErrorMessage.ShouldBe("No front camera footage found."); + controller.IsPlaying.ShouldBeFalse(); + } + [Fact] public async Task PauseSeekAndStop_ControlOpenPlayers() { diff --git a/SentryDeck/Playback/VideoPlayerController.cs b/SentryDeck/Playback/VideoPlayerController.cs index c68c127..b521583 100644 --- a/SentryDeck/Playback/VideoPlayerController.cs +++ b/SentryDeck/Playback/VideoPlayerController.cs @@ -22,6 +22,16 @@ public sealed partial class VideoPlayerController : ObservableObject, IDisposabl private const int MaxRecoveryAttemptsPerClip = 3; + /// + /// Shown when a clip's files carry Tesla's 2026.20+ dashcam encryption instead of plain MP4s. + /// The keys live behind the owner's Tesla account, so pointing at the in-car toggle and + /// Tesla's own viewer is the most useful thing the app can do. + /// + internal const string EncryptedClipMessage = + "This clip appears to be encrypted by the vehicle (Tesla software 2026.20 and later encrypts " + + "dashcam recordings by default). To record playable clips, turn off Controls > Safety > " + + "Encrypt Dashcam Recordings. Already-encrypted clips can be viewed at dashcam.tesla.com."; + /// /// One-shot guard for the reopen/seek race after a recovery: how long to wait after issuing /// the resume seek before verifying the front player actually landed near the target, and how @@ -614,7 +624,13 @@ private async Task OpenClipInternalAsync( clip.FullPath, mediaSource.CameraPlaylistPaths.Keys.Order().ToArray(), requestId); - ErrorMessage = $"No {CameraNames.DisplayName(_primaryCamera)} camera footage found."; + + // A fully encrypted clip lands here too: the builder probes every chunk's front file, + // finds no readable moov in any of them, and excludes them all — indistinguishable + // from "no footage" without sniffing the files themselves. + ErrorMessage = EncryptedClipDetector.LooksEncrypted(clip) + ? EncryptedClipMessage + : $"No {CameraNames.DisplayName(_primaryCamera)} camera footage found."; return; } @@ -1165,7 +1181,9 @@ private void GiveUpOnClip(CamClip clip, int badChunkIndex) clip.FullPath, badChunkIndex, _recoveryAttempts); - ErrorMessage = "Playback stopped: too many unreadable video files."; + ErrorMessage = EncryptedClipDetector.LooksEncrypted(clip) + ? EncryptedClipMessage + : "Playback stopped: too many unreadable video files."; IsMediaOpen = false; } From 1cf5febf729b7fd608ab6c9ebd5a48deaafae7fb Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Thu, 9 Jul 2026 17:36:39 -0500 Subject: [PATCH 2/2] Reflow new comments and the encryption message to one line per sentence Per Daniel's style preference: no column-driven hard wraps. The encryption message string is now one literal instead of a three-line + concatenation, and the doc comments / inline comments I added break only at sentence boundaries. --- .../Playback/EncryptedClipDetector.cs | 25 +++++++------------ .../EncryptedClipDetectorTests.cs | 6 ++--- SentryDeck.Tests/Fixtures/TestMp4.cs | 3 +-- .../VideoPlayerControllerTests.cs | 8 +++--- SentryDeck/Playback/VideoPlayerController.cs | 11 +++----- 5 files changed, 18 insertions(+), 35 deletions(-) diff --git a/SentryDeck.Data/Playback/EncryptedClipDetector.cs b/SentryDeck.Data/Playback/EncryptedClipDetector.cs index 99986c0..34965bd 100644 --- a/SentryDeck.Data/Playback/EncryptedClipDetector.cs +++ b/SentryDeck.Data/Playback/EncryptedClipDetector.cs @@ -4,18 +4,15 @@ namespace SentryDeck; /// -/// Heuristics for Tesla's encrypted dashcam recordings. Software update 2026.20 turns on -/// "Encrypt Dashcam Recordings" by default (Controls > Safety), writing AES-encrypted -/// containers to the USB drive instead of plain MP4s, so the files no longer begin with an -/// ISO-BMFF box header. Decryption keys are only obtainable from Tesla's servers via the -/// owner's account (dashcam.tesla.com), so the app can detect the state but not play it. +/// Heuristics for Tesla's encrypted dashcam recordings. +/// Software update 2026.20 turns on "Encrypt Dashcam Recordings" by default (Controls > Safety), writing AES-encrypted containers to the USB drive instead of plain MP4s, so the files no longer begin with an ISO-BMFF box header. +/// Decryption keys are only obtainable from Tesla's servers via the owner's account (dashcam.tesla.com), so the app can detect the state but not play it. /// public static class EncryptedClipDetector { /// - /// Top-level box types an unencrypted recording can plausibly start with. Tesla files start - /// with ftyp; the rest keep the sniff from misreporting other muxers' output — a file - /// starting with any of these is ordinary video (playable or merely corrupt), not encrypted. + /// Top-level box types an unencrypted recording can plausibly start with. + /// Tesla files start with ftyp; the rest keep the sniff from misreporting other muxers' output — a file starting with any of these is ordinary video (playable or merely corrupt), not encrypted. /// private static readonly string[] KnownLeadingBoxTypes = [ @@ -32,10 +29,8 @@ public static class EncryptedClipDetector ]; /// - /// True when the clip's front-camera files are all present with content but none starts like - /// an MP4 — the signature of a drive written with encryption enabled. A merely corrupt or - /// truncated clip still has a valid ftyp header on at least some chunks, so it stays - /// on the ordinary unreadable-file path. + /// True when the clip's front-camera files are all present with content but none starts like an MP4 — the signature of a drive written with encryption enabled. + /// A merely corrupt or truncated clip still has a valid ftyp header on at least some chunks, so it stays on the ordinary unreadable-file path. /// public static bool LooksEncrypted(CamClip clip) { @@ -76,8 +71,7 @@ public static bool LooksEncrypted(string path) Span header = stackalloc byte[8]; if (stream.ReadAtLeast(header, header.Length, throwOnEndOfStream: false) < header.Length) { - // Shorter than one box header: a truncated write, not an encrypted container - // (those carry a fixed header plus at least one 4 KiB payload chunk). + // Shorter than one box header: a truncated write, not an encrypted container (those carry a fixed header plus at least one 4 KiB payload chunk). return false; } @@ -86,8 +80,7 @@ public static bool LooksEncrypted(string path) } catch (Exception ex) { - // Unreadable at the filesystem level (missing, locked, ...) is not an encryption - // signal; let the ordinary unreadable-file handling describe it. + // Unreadable at the filesystem level (missing, locked, ...) is not an encryption signal; let the ordinary unreadable-file handling describe it. Log.Debug(ex, "Could not sniff file header for encryption. File={File}", path); return false; } diff --git a/SentryDeck.Tests/EncryptedClipDetectorTests.cs b/SentryDeck.Tests/EncryptedClipDetectorTests.cs index 814df0d..f1ac222 100644 --- a/SentryDeck.Tests/EncryptedClipDetectorTests.cs +++ b/SentryDeck.Tests/EncryptedClipDetectorTests.cs @@ -44,8 +44,7 @@ public void TruncatedTinyFile_IsNotEncrypted() [Fact] public void TruncatedButValidHeader_IsNotEncrypted() { - // A recording cut off mid-write still starts with its ftyp box; that's the corrupt - // path, not the encrypted one. + // A recording cut off mid-write still starts with its ftyp box; that's the corrupt path, not the encrypted one. var valid = TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60)); var path = WriteFile("cutoff.mp4", valid[..12]); @@ -82,8 +81,7 @@ public void Clip_WithAllFrontFilesEncrypted_IsEncrypted() [Fact] public void Clip_WithOnePlayableChunk_IsNotEncrypted() { - // One valid header among the chunks means ordinary video with some corruption — - // the recovery path should keep handling it. + // One valid header among the chunks means ordinary video with some corruption — the recovery path should keep handling it. var clip = ClipWithFrontFiles( TestMp4.EncryptedLookingBytes, TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60))); diff --git a/SentryDeck.Tests/Fixtures/TestMp4.cs b/SentryDeck.Tests/Fixtures/TestMp4.cs index e54ec84..087755d 100644 --- a/SentryDeck.Tests/Fixtures/TestMp4.cs +++ b/SentryDeck.Tests/Fixtures/TestMp4.cs @@ -16,8 +16,7 @@ internal static class TestMp4 public static byte[] GarbageBytes => [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]; /// - /// Bytes shaped like Tesla's 2026.20 encrypted recordings: a 20-byte header (with an embedded - /// UUID) followed by an IV-prefixed 4 KiB ciphertext chunk — no MP4 box structure anywhere. + /// Bytes shaped like Tesla's 2026.20 encrypted recordings: a 20-byte header (with an embedded UUID) followed by an IV-prefixed 4 KiB ciphertext chunk — no MP4 box structure anywhere. /// public static byte[] EncryptedLookingBytes { diff --git a/SentryDeck.Tests/VideoPlayerControllerTests.cs b/SentryDeck.Tests/VideoPlayerControllerTests.cs index 66b3ee6..813f942 100644 --- a/SentryDeck.Tests/VideoPlayerControllerTests.cs +++ b/SentryDeck.Tests/VideoPlayerControllerTests.cs @@ -97,9 +97,8 @@ public async Task SelectingClip_WhenFrontFileMissing_ReportsOpenFailure() [Fact] public async Task SelectingClip_WhenAllFilesAreEncrypted_ExplainsTheEncryptionToggle() { - // A drive written by Tesla software 2026.20+ with "Encrypt Dashcam Recordings" on: every - // file exists but none is a playable MP4. The real builder probes and excludes every - // chunk, and the error must point at the encryption toggle, not claim missing footage. + // A drive written by Tesla software 2026.20+ with "Encrypt Dashcam Recordings" on: every file exists but none is a playable MP4. + // The real builder probes and excludes every chunk, and the error must point at the encryption toggle, not claim missing footage. using var clipFiles = TestClipFiles.Create(chunkCount: 2); foreach (var chunk in clipFiles.Clip.Chunks) { @@ -126,8 +125,7 @@ public async Task SelectingClip_WhenAllFilesAreEncrypted_ExplainsTheEncryptionTo [Fact] public async Task SelectingClip_WhenAllFilesAreGarbage_ButNotEncrypted_KeepsTheCorruptMessage() { - // Same all-unreadable shape, but the files still carry MP4 headers (truncated writes): - // that's ordinary corruption and must NOT be blamed on encryption. + // Same all-unreadable shape, but the files still carry MP4 headers (truncated writes): that's ordinary corruption and must NOT be blamed on encryption. using var clipFiles = TestClipFiles.Create(chunkCount: 1); var truncated = TestMp4.BuildWithDuration(TimeSpan.FromSeconds(60))[..12]; foreach (var file in clipFiles.Clip.Chunks[0].Files.Values) diff --git a/SentryDeck/Playback/VideoPlayerController.cs b/SentryDeck/Playback/VideoPlayerController.cs index b521583..4e11483 100644 --- a/SentryDeck/Playback/VideoPlayerController.cs +++ b/SentryDeck/Playback/VideoPlayerController.cs @@ -24,13 +24,10 @@ public sealed partial class VideoPlayerController : ObservableObject, IDisposabl /// /// Shown when a clip's files carry Tesla's 2026.20+ dashcam encryption instead of plain MP4s. - /// The keys live behind the owner's Tesla account, so pointing at the in-car toggle and - /// Tesla's own viewer is the most useful thing the app can do. + /// The keys live behind the owner's Tesla account, so pointing at the in-car toggle and Tesla's own viewer is the most useful thing the app can do. /// internal const string EncryptedClipMessage = - "This clip appears to be encrypted by the vehicle (Tesla software 2026.20 and later encrypts " + - "dashcam recordings by default). To record playable clips, turn off Controls > Safety > " + - "Encrypt Dashcam Recordings. Already-encrypted clips can be viewed at dashcam.tesla.com."; + "This clip appears to be encrypted by the vehicle (Tesla software 2026.20 and later encrypts dashcam recordings by default). To record playable clips, turn off Controls > Safety > Encrypt Dashcam Recordings. Already-encrypted clips can be viewed at dashcam.tesla.com."; /// /// One-shot guard for the reopen/seek race after a recovery: how long to wait after issuing @@ -625,9 +622,7 @@ private async Task OpenClipInternalAsync( mediaSource.CameraPlaylistPaths.Keys.Order().ToArray(), requestId); - // A fully encrypted clip lands here too: the builder probes every chunk's front file, - // finds no readable moov in any of them, and excludes them all — indistinguishable - // from "no footage" without sniffing the files themselves. + // A fully encrypted clip lands here too: the builder probes every chunk's front file, finds no readable moov in any of them, and excludes them all — indistinguishable from "no footage" without sniffing the files themselves. ErrorMessage = EncryptedClipDetector.LooksEncrypted(clip) ? EncryptedClipMessage : $"No {CameraNames.DisplayName(_primaryCamera)} camera footage found.";