<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Reading Dashcam Video Without Watching It · Loosely Decoupled</title>
    <link>https://looselydecoupled.com/series/reading-dashcam-video/</link>
    <description>A 2 TB drive of five-camera Rivian R1T footage, sorted, searched and cut by reading what the files already say: GPS, keyframes, motion and color.</description>
    <language>en-us</language>
    
    <copyright>© 2026 Emmanuel Tsouris</copyright>
    <lastBuildDate>Fri, 02 Oct 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://looselydecoupled.com/series/reading-dashcam-video/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Finding a Bridge in 2 TB of Dashcam Video by the Color of Tunnel Lights</title>
      <link>https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/</guid>
      <pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>Python</category>
      
      <description>A red/blue ratio found the sodium-lit tunnel on 71 of 79 bridge drives after auto-exposure hid it from every brightness check.</description>
      <content:encoded><![CDATA[<figure><img src="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/interchange-curve-sunset_hu_6d94b69d2acd59a0.jpg" alt="A side-camera fisheye frame on the I-5/I-90 interchange curve at sunset: the low sun flares at the left edge, and the downtown Seattle skyline stands above the ramps and trees." width="1600" height="1280" loading="lazy" decoding="async">
</figure>
<p>I wanted every crossing of the I-90 floating bridge in 240 drive folders of
dashcam footage, 1,958 GB, for a montage. I wasn&rsquo;t going to watch the footage
to find them, and my Rivian R1T&rsquo;s GPS was too coarse to. What found them was the
color of the lights in the Mount Baker tunnel, which sits at the Seattle end
of the bridge. Brightness couldn&rsquo;t see that
tunnel. A red/blue ratio found it on 71 of the 79 bridge drives
scanned, and a person looking at one frame per candidate confirmed 68 crossings.
<a href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/">How to sort months of dashcam footage with Claude
Code</a> has the rest of the job this was
one stage of.</p>
<h2 id="signals-instead-of-pixels">Signals instead of pixels<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#signals-instead-of-pixels"
     aria-label="Link to section: Signals instead of pixels" data-pagefind-ignore></a>
</h2>
<p>The footage already describes itself if you read the right parts of it. Each
15-minute segment carries <a href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/">one GPS fix, at its start, in the container&rsquo;s
<code>location</code> tag</a>. That sorts drives by area, but one fix per
15 minutes can&rsquo;t place a bridge crossing to the minute, let alone the second.</p>
<p>The finer signal is in the keyframes. The truck writes one keyframe per second.
Decoding only those, downscaled to 64 × 48, gives a per-second record of a
drive and skips the 29 frames between them. Each
second becomes four numbers: the mean change in green from the previous
keyframe, which I called motion, and the mean red, green and blue. That record
was computed once, cached as CSV, and every detector afterward read the cache
instead of the video.</p>
<p>The first calibration read a motion of zero everywhere, parked or moving.
The cause was ffmpeg duplicating the keyframes back up to 30 fps, so every
frame was compared with a copy of itself. <code>-fps_mode passthrough</code> fixed it. The bug
showed up only because the check ran on a known parked segment and a known
moving one before it ran on anything else. A detector that reads zero
on both cases is measuring itself.</p>
<h2 id="why-brightness-failed">Why brightness failed<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#why-brightness-failed"
     aria-label="Link to section: Why brightness failed" data-pagefind-ignore></a>
</h2>
<p>A tunnel should be the easiest thing in a drive to find: the outside goes away
and the picture gets darker. In this footage it doesn&rsquo;t get darker. The camera&rsquo;s
auto-exposure raises the gain as the light drops and holds the frame&rsquo;s
brightness close to what it was in daylight. That&rsquo;s what the exposure control
is for, and it erases the one property a brightness threshold depends on.</p>
<h2 id="why-color-worked">Why color worked<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#why-color-worked"
     aria-label="Link to section: Why color worked" data-pagefind-ignore></a>
</h2>
<p>Exposure scales brightness, and the hue of the lamps comes through. The Mount
Baker tunnel is lit by sodium lamps, and sodium light is orange. Inside the
tunnel, the front camera&rsquo;s mean red rises above its mean blue. That ratio is
the whole detector.</p>
<p>It runs with three guards:</p>
<ul>
<li>The red/blue ratio has to stay above 1.0 for 25–150 seconds, so a single
orange frame or a long orange stretch doesn&rsquo;t count.</li>
<li>A rolling median smooths the ratio first, so one bright frame inside a tunnel
doesn&rsquo;t break the run in two.</li>
<li>The first and last 3 minutes of every drive are ignored, because parking
garages look the same to the ratio.</li>
</ul>
<figure>
  <img src="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/red-blue-ratio-tunnel.svg" alt="Two stacked charts over a 1,600-second window of one drive. The top chart is the smoothed red/blue ratio: it sits near 0.9 and crosses the dashed 1.0 threshold in five shaded runs, and the run at 381 seconds, labeled as the Mount Baker tunnel, peaks near 1.3. The bottom chart is mean green over the same seconds: it wanders between about 50 and 75 and stays in that range through the tunnel." loading="lazy" decoding="async"><figcaption>Drive 07_16_26_120000, front camera, from the cached per-second signals. The ratio is red / (blue + 1), smoothed by the detector&rsquo;s rolling median.</figcaption>
</figure>
<p>On the 79 bridge drives scanned, it found a tunnel on 71. Seven of
the eight misses were after dark. My reading is that the ratio describes
light, and once the light outside is artificial too, a tunnel stops standing
out from the road around it.</p>
<h2 id="detector-proposes-person-confirms">Detector proposes, person confirms<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#detector-proposes-person-confirms"
     aria-label="Link to section: Detector proposes, person confirms" data-pagefind-ignore></a>
</h2>
<p>Finding a tunnel isn&rsquo;t the same as finding the right one. A drive on I-90
passes more than one stretch that reads as a tunnel, including the Mercer
Island lid, and most drives had several candidates. The obvious rule, pick one
tunnel by its position in the drive, was right about two times in three.</p>
<p>I could have spent the effort on a better rule. Instead the tool writes a guess
for every drive and a contact sheet to check it against, then stops. Each row
of the sheet is one drive, and each tile is a frame from the bridge side of one
candidate. The bridge is unmistakable in a frame, so the right
column is obvious, and the edit is one number in a picks file, which <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">the cut</a>
turns into synced clips.
Reviewing that way confirmed 68 crossings. Two night drives couldn&rsquo;t be
confirmed, because a dark frame of a dark bridge proves nothing, and one drive
had no crossing among its candidates.</p>
<figure><img src="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/review-sheet-row_hu_42d4d411e91d9c33.jpg" alt="One row of the review sheet: the drive from the chart above, with one tile per candidate. The first tile is a freeway lined with trees, the second is the floating bridge, and the last three are replaced by gray blocks." width="1600" height="256" loading="lazy" decoding="async"><figcaption>The second tile is the bridge, so the pick is candidate 2. Three frames removed.</figcaption>
</figure>
<p>The layout of the sheet mattered as much as having one. The first version was
a flat grid of every candidate frame, and it was easy to match a tile to the
wrong drive. One row per drive makes that mistake visible rather than merely
possible.</p>
<p>The sheet is also an instrument, and an instrument can be wrong. At one point I
announced that the picks for some drives were the Mercer Island lid, not the Mount
Baker tunnel. The sheets had been built with a seek that ffmpeg applied after
tiling, so they showed a different window than the one they claimed. Extracting
the same frame two ways exposed it, and the picks had been right all along.
When a result contradicts work that was already verified, check the instrument
before the conclusion.</p>
<p>The review step reads like an admission that the detector wasn&rsquo;t good enough.
I think it&rsquo;s the reason the detector could stay simple. Take a rule that&rsquo;s
right two times in three, and add a sheet a person reads at a glance. That
beats a rule that&rsquo;s right more often and still needs someone to check it.</p>
<h2 id="signals-that-could-not-separate-the-cases">Signals that could not separate the cases<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#signals-that-could-not-separate-the-cases"
     aria-label="Link to section: Signals that could not separate the cases" data-pagefind-ignore></a>
</h2>
<p>The same per-second record answered four other questions, and each answer was
no for the same reason: the signal moved for the thing I wanted and for
something I didn&rsquo;t.</p>
<ul>
<li><strong>Parked footage.</strong> Near-zero motion found parked time reliably, but there
was little to find: 23 GB of the 1,760 GB scanned, because with Gear Guard turned off,
the truck stops recording seconds after it parks.</li>
<li><strong>Hard stops.</strong> A sudden change in the picture marks braking, but turning
changes the picture as much. Of 875 candidates, all 12 at the top were turns
into parking lots or ordinary stops.</li>
<li><strong>Traffic jams.</strong> Sustained low motion should mark crawling traffic, but the
neighboring cars keep moving, so the picture keeps changing and a jam looks
like driving.</li>
<li><strong>Garage boundaries.</strong> A gray band at the top of the frame marks a concrete
ceiling on a clear day, but an overcast sky is the same gray. The boundary
was set by timing instead.</li>
</ul>
<p>None of these is fixed by tuning a threshold. When a signal can&rsquo;t tell two
cases apart, a better number for the threshold still can&rsquo;t.</p>
<p>Close passes did work once the frame&rsquo;s overall brightness change was
subtracted from the side cameras: 37 of the top 60 were real passes. A person
still had to sort them.</p>
<h2 id="signals-the-camera-leaves-alone">Signals the camera leaves alone<a class="heading-anchor" href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/#signals-the-camera-leaves-alone"
     aria-label="Link to section: Signals the camera leaves alone" data-pagefind-ignore></a>
</h2>
<p>A camera processes its own picture before anything reads it, and that
processing exists to make the picture look normal. Auto-exposure made a tunnel
look like daylight and left the lamp color intact. Before measuring anything in
recorded video, ask which properties the camera flattens and which it passes
through, and build on the second kind.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Make a Hyperlapse from Keyframes with ffmpeg Without Decoding Every Frame</title>
      <link>https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/</guid>
      <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>macOS</category>
      
      <description>With one keyframe per second, -skip_frame nokey decodes 1 frame in 30 and setpts makes each one a frame at 30 fps: a 36-minute drive becomes 72 seconds.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>When a video has one keyframe per second, decode only the keyframes and give
each one a frame at 30 fps. The result plays 30 times faster than the drive:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">-i</span> <span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">-vf</span> <span class="console__token">&#34;setpts=N/(30*TB)&#34;</span> <span class="console__token">-r</span> <span class="console__token">30</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-map_metadata</span> <span class="console__token">-1</span> <span class="console__token">-c:v</span> <span class="console__token">hevc_videotoolbox</span> <span class="console__token">-q:v</span> <span class="console__token">65</span> <span class="console__token">-tag:v</span> <span class="console__token">hvc1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-color_range</span> <span class="console__token">pc</span> <span class="console__token">-an</span> <span class="console__token">&lt;clip&gt;_30x.mp4</span></span></code></pre>
    </div>
  </div></div><p><code>-skip_frame nokey</code> goes before <code>-i</code>, because it tells the decoder to discard
every frame that isn&rsquo;t a keyframe. <code>setpts=N/(30*TB)</code> gives frame <code>N</code> the
timestamp <code>N/30</code> seconds, so one second of driving becomes one frame of output.</p>
<p>To keep several cameras in sync, render each recording segment separately, pad
every camera&rsquo;s segment with black to the front camera&rsquo;s length, and join the
segments with a stream copy. The script later in this article does that.</p>

</div>

<p>A Rivian R1T films five directions for as long as it&rsquo;s driven. I wanted a
hyperlapse of every drive from a summer of footage, one file per camera, so a
drive could be cut as a multi-camera edit at 30× speed. An ordinary hyperlapse
decodes 30 frames to keep one, and this footage didn&rsquo;t need that: the truck
writes one keyframe every second. Reading only those, a 36-minute drive became
72 seconds of video and took 4.4 minutes to render, with one drive rendering at
a time. The hyperlapses for 218 drives, five cameras each, came to 107 GB.</p>
<p>This is part of a series about reading dashcam video without watching it.
<a href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/">How to sort months of dashcam footage with Claude
Code</a> is the hub, with the prompts and
the checks that drove the whole job.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>ffmpeg</dt> <dd>9.0.1</dd></div>
    <div class="tested__row"><dt>macOS</dt> <dd>27.0, Apple silicon</dd></div>
    <div class="tested__row"><dt>Footage</dt> <dd>Rivian dashcam, H.264, 30 fps, one keyframe per second</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
<h2 id="why-keyframes-are-enough">Why keyframes are enough<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#why-keyframes-are-enough"
     aria-label="Link to section: Why keyframes are enough" data-pagefind-ignore></a>
</h2>
<p>The Rivian&rsquo;s clips are H.264 at 30 fps with I- and P-frames only, and one
keyframe (an I-frame) every second. A keyframe decodes on its own. A P-frame
needs the frames before it, so taking every 30th frame of an ordinary decode
still means decoding the other 29.</p>
<p>With <code>-skip_frame nokey</code> the decoder drops the P-frames before decoding them.
What reaches the filter is one picture per second of driving, already spaced
evenly in time. The <code>setpts=N/(30*TB)</code> filter discards their timestamps and numbers them
off at 30 per second. <code>N</code> is the frame&rsquo;s index and <code>TB</code> is the stream&rsquo;s time
base. Without <code>setpts</code>, the output has the same frames, each still held for a
second, and plays at the speed of the drive.</p>
<p>The speed this produces is fixed by the footage: one keyframe per second, shown
at 30 fps, is 30×. Footage with a keyframe every two seconds would come out at
60× from the same command.</p>
<figure><img src="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/hyperlapse-frames-floating-bridge_hu_316ec0d3423d1748.jpg" alt="Eight front-camera frames from a 30× hyperlapse, one per second of output. Each step covers 30 seconds of driving. The frames show the I-90 floating bridge under an overcast sky, the orange light of the Mount Baker tunnel, and the Mercer Island lid." width="1600" height="640" loading="lazy" decoding="async"><figcaption>Eight frames of a hyperlapse, 30 frames apart. Plates blurred.</figcaption>
</figure>
<!-- style-check: procedure -->
<h2 id="check-the-keyframe-rhythm">Check the keyframe rhythm<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#check-the-keyframe-rhythm"
     aria-label="Link to section: Check the keyframe rhythm" data-pagefind-ignore></a>
</h2>
<p>Before relying on it, count the keyframes in one clip. In a terminal, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-show_entries</span> <span class="console__token">frame=pts_time</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">|</span> <span class="console__token">head</span> <span class="console__token">-3</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">0.000000</span>
</span><span class="line"><span class="console__token">1.000000</span>
</span><span class="line"><span class="console__token">2.000000</span></span></code></pre>
    </div>
  </div></div><p>Pipe the same command to <code>wc -l</code> instead of <code>head</code> for the count. On a
351-second clip it printed <code>351</code>, and a full 15-minute segment from the truck
has 900. On some drives one camera&rsquo;s keyframes fall at 0.967 s past each
second rather than on it, and which camera varies by drive. The spacing is
still one second, which is what the speed depends on.</p>
<p>If the times are not one second apart, the hyperlapse still renders, but its
speed is the keyframe interval times 30 rather than 30×.</p>
<h2 id="render-one-clip">Render one clip<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#render-one-clip"
     aria-label="Link to section: Render one clip" data-pagefind-ignore></a>
</h2>
<p>In the folder that holds the clip, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-nostdin</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">-i</span> <span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-vf</span> <span class="console__token">&#34;setpts=N/(30*TB)&#34;</span> <span class="console__token">-r</span> <span class="console__token">30</span> <span class="console__token">-map_metadata</span> <span class="console__token">-1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-c:v</span> <span class="console__token">hevc_videotoolbox</span> <span class="console__token">-q:v</span> <span class="console__token">65</span> <span class="console__token">-tag:v</span> <span class="console__token">hvc1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-color_range</span> <span class="console__token">pc</span> <span class="console__token">-an</span> <span class="console__token">&lt;clip&gt;_30x.mp4</span></span></code></pre>
    </div>
  </div></div><p>It writes <code>&lt;clip&gt;_30x.mp4</code> beside the source and changes nothing else. On a
351-second clip of a bridge crossing, the output was 351 frames and 11.7
seconds long, 11.2 MB from a 438.8 MB source.</p>
<p>The encoder options are the part to change on another machine:</p>
<ul>
<li><code>hevc_videotoolbox</code> is the Mac&rsquo;s hardware HEVC encoder. The Homebrew build
of ffmpeg used here includes it; <code>ffmpeg -encoders | grep videotoolbox</code>
lists it if yours does.</li>
<li><code>-q:v 65</code> asks it for constant quality rather than a bitrate.</li>
<li><code>-tag:v hvc1</code> labels the HEVC stream the way QuickTime and Apple&rsquo;s players
expect.</li>
<li><code>-color_range pc</code> labels the output full range like the footage, which the
black filler and the join depend on. Apple&rsquo;s encoder ignores
<code>-pix_fmt yuvj420p</code> with a warning, so it isn&rsquo;t set here.</li>
<li><code>-map_metadata -1</code> drops the source&rsquo;s tags, including its GPS fix.</li>
<li><code>-an</code> writes no audio track. The truck records none.</li>
</ul>
<h2 id="pad-each-segment-to-the-front-cameras-length">Pad each segment to the front camera&rsquo;s length<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#pad-each-segment-to-the-front-cameras-length"
     aria-label="Link to section: Pad each segment to the front camera’s length" data-pagefind-ignore></a>
</h2>
<p>The truck splits a drive into 15-minute segments, one file per camera per
segment. All five cameras&rsquo; files for a segment have the same length, unless a
camera stopped recording. If one did, that camera&rsquo;s segment has fewer
keyframes, its hyperlapse has fewer frames, and every later frame of that angle
plays early. A camera that is missing a whole segment is worse.</p>
<p>When I first rendered every camera of one drive, two came out at 1,303 frames
against the front camera&rsquo;s 2,170. The rear camera had stopped 33
seconds into a segment and recorded the rest of the drive <a href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/">in a sibling
folder</a>.</p>
<p>Pad every camera&rsquo;s segment to the front camera&rsquo;s frame count with black, so
each segment ends at the same frame on every camera. The numbers in these steps
are an example, a 351-keyframe front segment and a camera that stopped 33
seconds in:</p>
<ol>
<li>
<p>Count the front camera&rsquo;s keyframes for the segment, as in the first step.
On a 351-second segment, that&rsquo;s 351.</p>
</li>
<li>
<p>Count the other camera&rsquo;s keyframes for the same segment. A clip that
stopped 33 seconds in has 33.</p>
</li>
<li>
<p>Render that camera&rsquo;s segment with <code>tpad</code> adding the difference as black
frames, and <code>-frames:v</code> capping it at the front camera&rsquo;s count:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-nostdin</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">-i</span> <span class="console__token">&lt;rear-segment&gt;.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-vf</span> <span class="console__token">&#34;setpts=N/(30*TB),tpad=stop=318:color=black&#34;</span> <span class="console__token">-r</span> <span class="console__token">30</span> <span class="console__token">-frames:v</span> <span class="console__token">351</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-map_metadata</span> <span class="console__token">-1</span> <span class="console__token">-c:v</span> <span class="console__token">hevc_videotoolbox</span> <span class="console__token">-q:v</span> <span class="console__token">65</span> <span class="console__token">-tag:v</span> <span class="console__token">hvc1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-color_range</span> <span class="console__token">pc</span> <span class="console__token">-an</span> <span class="console__token">&lt;rear-segment&gt;_30x.mp4</span></span></code></pre>
    </div>
  </div></div></li>
</ol>
<p>The <code>-frames:v</code> cap also trims a camera that recorded slightly longer than the
front one.</p>
<h2 id="fill-a-missing-segment-with-full-range-black">Fill a missing segment with full-range black<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#fill-a-missing-segment-with-full-range-black"
     aria-label="Link to section: Fill a missing segment with full-range black" data-pagefind-ignore></a>
</h2>
<p>When a camera has no file at all for a segment, render a black part of the same
size and frame count. The front, rear and side cameras are 1920 × 1536; the
Gear Guard bed camera is 1280 × 720.</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-nostdin</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-f</span> <span class="console__token">lavfi</span> <span class="console__token">-i</span> <span class="console__token">&#34;color=black:s=1920x1536:r=30&#34;</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-frames:v</span> <span class="console__token">351</span> <span class="console__token">-map_metadata</span> <span class="console__token">-1</span> <span class="console__token">-c:v</span> <span class="console__token">hevc_videotoolbox</span> <span class="console__token">-q:v</span> <span class="console__token">65</span> <span class="console__token">-tag:v</span> <span class="console__token">hvc1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-color_range</span> <span class="console__token">pc</span> <span class="console__token">-an</span> <span class="console__token">&lt;missing-segment&gt;_30x.mp4</span></span></code></pre>
    </div>
  </div></div><p>The filler must be full range, the same as the footage. <code>-color_range pc</code> is
what makes it so: without it, generated black came out labeled limited range
(<code>tv</code>), and <code>-pix_fmt yuvj420p</code> doesn&rsquo;t help, because Apple&rsquo;s encoder ignores it. The decoded footage is already full
range, so the same options are safe on every part.</p>
<p>A mismatch doesn&rsquo;t make the next step fail. The join is a stream copy, which
converts nothing, and the joined file takes its range label from the first
part. A camera whose file opens on limited-range filler is labeled limited
range throughout, real footage included. That happened to 11 of the more than a thousand
hyperlapse files in my batch, all in folders that held only a fragment of a
drive, and they had to be rendered again. The script checks each part&rsquo;s range before
joining, because the joined file shows only the first part&rsquo;s.</p>
<h2 id="join-the-segments-without-re-encoding">Join the segments without re-encoding<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#join-the-segments-without-re-encoding"
     aria-label="Link to section: Join the segments without re-encoding" data-pagefind-ignore></a>
</h2>
<p>List the parts in order in a text file, one <code>file</code> line each, then join them
with the concat demuxer and a stream copy:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">printf</span> <span class="console__token">&#34;file</span> <span class="console__token">&#39;%s&#39;\n&#34;</span> <span class="console__token">&lt;part-001&gt;.mp4</span> <span class="console__token">&lt;part-002&gt;.mp4</span> <span class="console__token">&gt;</span> <span class="console__token">list.txt</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-nostdin</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-f</span> <span class="console__token">concat</span> <span class="console__token">-safe</span> <span class="console__token">0</span> <span class="console__token">-i</span> <span class="console__token">list.txt</span> <span class="console__token">-c</span> <span class="console__token">copy</span> <span class="console__token">&lt;camera&gt;_30x.mp4</span></span></code></pre>
    </div>
  </div></div><p>Every part was encoded with the same options, so the copy only rewrites the
container. It takes seconds.</p>
<h2 id="script-the-whole-drive">Script the whole drive<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#script-the-whole-drive"
     aria-label="Link to section: Script the whole drive" data-pagefind-ignore></a>
</h2>
<p>The steps combine into one script per camera. It reads the segment list from
the front camera&rsquo;s files, so a camera missing a segment gets black filler for
it. The macOS <code>._</code> files beside the clips on the truck&rsquo;s exFAT drive don&rsquo;t match
its pattern, so they&rsquo;re skipped. The front camera sets every segment&rsquo;s length,
so all five files come out the same length.</p>
<div class="code-block" data-copyable data-copy-label="hyperlapse.sh">
  <div class="code-block__head"><span class="code-block__lang code-block__file">hyperlapse.sh</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">folder</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl"><span class="nv">camera</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="cl"><span class="nv">size</span><span class="o">=</span><span class="si">${</span><span class="nv">3</span><span class="k">:-</span><span class="nv">1920x1536</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">encode</span><span class="o">=(</span>-map_metadata -1 -c:v hevc_videotoolbox -q:v <span class="m">65</span> -tag:v hvc1
</span></span><span class="line"><span class="cl">        -color_range pc -an<span class="o">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">keyframes<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  ffprobe -v error -skip_frame nokey -select_streams v <span class="se">\
</span></span></span><span class="line"><span class="cl">    -show_entries <span class="nv">frame</span><span class="o">=</span>pts_time -of <span class="nv">csv</span><span class="o">=</span><span class="nv">p</span><span class="o">=</span><span class="m">0</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="p">|</span> wc -l <span class="p">|</span> tr -d <span class="s1">&#39; &#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">work</span><span class="o">=</span><span class="k">$(</span>mktemp -d<span class="k">)</span>
</span></span><span class="line"><span class="cl">: &gt; <span class="s2">&#34;</span><span class="nv">$work</span><span class="s2">/list.txt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> front in <span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">/</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">_frontCenter_&#34;</span>*.mp4<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nv">segment</span><span class="o">=</span><span class="si">${</span><span class="nv">front</span><span class="p">##*_</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">  <span class="nv">frames</span><span class="o">=</span><span class="k">$(</span>keyframes <span class="s2">&#34;</span><span class="nv">$front</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">source</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">/</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">segment</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">part</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$work</span><span class="s2">/</span><span class="nv">$segment</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -f <span class="nv">$source</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">shortfall</span><span class="o">=</span><span class="k">$((</span> frames <span class="o">-</span> <span class="k">$(</span>keyframes <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span><span class="k">)</span> <span class="k">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">((</span> shortfall &lt; <span class="m">0</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span> <span class="nv">shortfall</span><span class="o">=</span>0<span class="p">;</span> <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    ffmpeg -nostdin -v error -skip_frame nokey -i <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -vf <span class="s2">&#34;setpts=N/(30*TB),tpad=stop=</span><span class="si">${</span><span class="nv">shortfall</span><span class="si">}</span><span class="s2">:color=black&#34;</span> -r <span class="m">30</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -frames:v <span class="s2">&#34;</span><span class="nv">$frames</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">encode</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$part</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    ffmpeg -nostdin -v error -f lavfi -i <span class="s2">&#34;color=black:s=</span><span class="si">${</span><span class="nv">size</span><span class="si">}</span><span class="s2">:r=30&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -frames:v <span class="s2">&#34;</span><span class="nv">$frames</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">encode</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$part</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nv">range</span><span class="o">=</span><span class="k">$(</span>ffprobe -v error -select_streams v -show_entries <span class="nv">stream</span><span class="o">=</span>color_range <span class="se">\
</span></span></span><span class="line"><span class="cl">    -of <span class="nv">csv</span><span class="o">=</span><span class="nv">p</span><span class="o">=</span><span class="m">0</span> <span class="s2">&#34;</span><span class="nv">$part</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="nv">$range</span> !<span class="o">=</span> pc <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$part</span><span class="s2">: color_range=</span><span class="nv">$range</span><span class="s2">, expected pc&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;file &#39;</span><span class="nv">$part</span><span class="s2">&#39;&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$work</span><span class="s2">/list.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ffmpeg -nostdin -v error -f concat -safe <span class="m">0</span> -i <span class="s2">&#34;</span><span class="nv">$work</span><span class="s2">/list.txt&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c copy <span class="s2">&#34;</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">_30x.mp4&#34;</span>
</span></span><span class="line"><span class="cl">rm -r <span class="s2">&#34;</span><span class="nv">$work</span><span class="s2">&#34;</span></span></span></code></pre></div></div>
  </div>
</div><p>It writes <code>&lt;drive&gt;_&lt;camera&gt;_30x.mp4</code> in the current folder. Run it once per
camera, from the folder where the hyperlapses belong, and pass <code>1280x720</code> as
the third argument for the Gear Guard camera:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">for</span> <span class="console__token">camera</span> <span class="console__token">in</span> <span class="console__token">frontCenter</span> <span class="console__token">rearCenter</span> <span class="console__token">sideLeft</span> <span class="console__token">sideRight;</span> <span class="console__token">do</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">bash</span> <span class="console__token">hyperlapse.sh</span> <span class="console__token">&lt;drive-folder&gt;</span> <span class="console__token">&#34;$camera&#34;;</span> <span class="console__token">done</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">bash</span> <span class="console__token">hyperlapse.sh</span> <span class="console__token">&lt;drive-folder&gt;</span> <span class="console__token">gearGuard</span> <span class="console__token">1280x720</span></span></code></pre>
    </div>
  </div></div><p>The script assumes the front camera recorded every segment. If it didn&rsquo;t,
the segment is missing from every camera&rsquo;s hyperlapse.</p>
<h2 id="confirm-every-camera-has-the-same-frame-count">Confirm every camera has the same frame count<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#confirm-every-camera-has-the-same-frame-count"
     aria-label="Link to section: Confirm every camera has the same frame count" data-pagefind-ignore></a>
</h2>
<p>Same length is the whole point of the padding, so count the frames in each
output, and read the pixel format while you&rsquo;re there:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">for</span> <span class="console__token">f</span> <span class="console__token">in</span> <span class="console__token">*_30x.mp4;</span> <span class="console__token">do</span> <span class="console__token">echo</span> <span class="console__token">&#34;$f</span> <span class="console__token">$(ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-count_packets</span> <span class="console__token">-of</span> <span class="console__token">compact=p=0</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">-show_entries</span> <span class="console__token">stream=nb_read_packets,pix_fmt,color_range</span> <span class="console__token">&#34;$f&#34;)&#34;;</span> <span class="console__token">done</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">&lt;drive&gt;_frontCenter_30x.mp4</span> <span class="console__token">pix_fmt=yuvj420p|color_range=pc|nb_read_packets=351</span>
</span><span class="line"><span class="console__token">&lt;drive&gt;_gearGuard_30x.mp4</span> <span class="console__token">pix_fmt=yuvj420p|color_range=pc|nb_read_packets=351</span>
</span><span class="line"><span class="console__token">&lt;drive&gt;_rearCenter_30x.mp4</span> <span class="console__token">pix_fmt=yuvj420p|color_range=pc|nb_read_packets=351</span>
</span><span class="line"><span class="console__token">&lt;drive&gt;_sideLeft_30x.mp4</span> <span class="console__token">pix_fmt=yuvj420p|color_range=pc|nb_read_packets=351</span>
</span><span class="line"><span class="console__token">&lt;drive&gt;_sideRight_30x.mp4</span> <span class="console__token">pix_fmt=yuvj420p|color_range=pc|nb_read_packets=351</span></span></code></pre>
    </div>
  </div></div><p>Every camera of a drive prints the same count, and every file prints
<code>yuvj420p</code> and <code>pc</code>. A camera with fewer frames was not padded. The range here
is only the first part&rsquo;s, because a stream-copy join takes its label from the
first part, so the script checks every part&rsquo;s range before it joins them and
stops on one that isn&rsquo;t <code>pc</code>.</p>
<figure><img src="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/hyperlapse-five-cameras-same-frame_hu_49df17819680e522.jpg" alt="Frame 1,970 of each of the five 30× hyperlapse files of one drive. Every tile shows the same instant on the floating bridge: a box truck and a pickup behind the truck appear in both the rearCenter and gearGuard tiles, and the SUV passing on the left appears in frontCenter." width="1600" height="853" loading="lazy" decoding="async"><figcaption>The same frame index from each camera&rsquo;s hyperlapse. Plates blurred.</figcaption>
</figure>
<h2 id="remove-the-location-before-publishing">Remove the location before publishing<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#remove-the-location-before-publishing"
     aria-label="Link to section: Remove the location before publishing" data-pagefind-ignore></a>
</h2>
<div class="admonition admonition--caution" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#i-octagon-alert"/></svg>
    <span>Caution</span>
  </p>
  <p>ffmpeg copies the truck&rsquo;s GPS tag to a re-encoded output by default. Every
command here drops it with <code>-map_metadata -1</code>; check the file before you
publish it, as <a href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/">Read the GPS location from Rivian dashcam video with
ffprobe</a> shows.</p>
</div><p>A hyperlapse runs from the start of a drive to its end. Trim both ends before
you publish one, too.</p>
<!-- style-check: end procedure -->
<h2 id="keyframes-vs-a-setpts-timelapse-of-every-frame">Keyframes vs a setpts timelapse of every frame<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/#keyframes-vs-a-setpts-timelapse-of-every-frame"
     aria-label="Link to section: Keyframes vs a setpts timelapse of every frame" data-pagefind-ignore></a>
</h2>
<p>Reading only keyframes is what makes this fast, and it fixes what the
hyperlapse can be.</p>
<p>On the same 351-second clip, I rendered the 30× hyperlapse twice: once from
keyframes, and once by decoding every frame and keeping every 30th with
<code>select='not(mod(n,30))'</code>. Both came out at 351 frames. The keyframe render
took 4.4 seconds and used about one CPU core. The full decode finished in 7.0
seconds only because it spread across the cores: it used 57 seconds of CPU
time, roughly 13 times the work. On one clip that&rsquo;s a few seconds of waiting.
Across a batch of drives, the CPU time is what adds up.</p>
<p>What the full decode buys you is choice:</p>
<div class="table-wrap" data-scrollable data-label="Table: You want">
  <table>
    <thead>
      <tr><th scope="col">You want</th><th scope="col">Keyframes</th><th scope="col">Every frame</th></tr>
    </thead>
    <tbody>
      <tr><td>30× at 30 fps, from one keyframe per second</td><td>Yes</td><td>Yes, decoding 30 frames per output frame</td></tr>
      <tr><td>60×, 90× or another whole multiple</td><td>Yes, by keeping every second or third keyframe with <code>select</code></td><td>Yes</td></tr>
      <tr><td>Any other speed, such as 20× or 45×</td><td>No</td><td>Yes</td></tr>
      <tr><td>Motion blur from blending neighboring frames</td><td>No</td><td>Yes</td></tr>
      <tr><td>Even spacing on footage with irregular keyframes</td><td>No</td><td>Yes</td></tr>
    </tbody>
  </table>
</div>
<p>For dashcam footage with a steady one-second keyframe, 30× was the speed I
wanted, so keyframes were the right choice. If the speed has to be something
else, decode everything and accept the cost.</p>
<p>The same <code>-skip_frame nokey</code> read is the cheap way to analyze footage, not only
to watch it. When you extract keyframes as raw frames rather than encoding a
video, add <code>-fps_mode passthrough</code>. Without it, ffmpeg duplicates each keyframe
back up to 30 fps, and every frame-to-frame difference you compute reads zero.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Cut Multi-Camera Dashcam Clips with ffmpeg Without Re-Encoding, and Keep Them in Sync</title>
      <link>https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/</guid>
      <pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      
      <description>Stream-copy each dashcam camera and line the files up to the frame, even though each camera&#39;s keyframes fall at their own drifting fraction of a second.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>Cut each camera with one pass of ffmpeg&rsquo;s concat demuxer over its source
segments, with <code>-c copy</code>. In the listing, set the first <code>inpoint</code> to that
camera&rsquo;s own keyframe at or before the moment you want, not to the moment
itself:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">file &#39;&lt;drive-folder&gt;/07_16_26_120000_sideLeft_002.mp4&#39;
</span></span><span class="line"><span class="cl">inpoint 889.966667
</span></span><span class="line"><span class="cl">outpoint 900
</span></span><span class="line"><span class="cl">file &#39;&lt;drive-folder&gt;/07_16_26_120000_sideLeft_003.mp4&#39;
</span></span><span class="line"><span class="cl">inpoint 0
</span></span><span class="line"><span class="cl">outpoint 30</span></span></code></pre></div></div>
  </div>
</div><div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-f</span> <span class="console__token">concat</span> <span class="console__token">-safe</span> <span class="console__token">0</span> <span class="console__token">-i</span> <span class="console__token">sideLeft.txt</span> <span class="console__token">-c</span> <span class="console__token">copy</span> <span class="console__token">sideLeft.mp4</span></span></code></pre>
    </div>
  </div></div><p>Snapping makes an offset camera&rsquo;s file start a few frames early. Confirm that
every file has a <code>start_time</code> of 0 and the same number of frames plus that
lead. In DaVinci Resolve, set each clip&rsquo;s In point that many frames in, then
build the multicam with the angles synced by In point.</p>

</div>

<p>I wanted the same few minutes of a drive, the crossing of the I-90 floating
bridge, from all five cameras of a Rivian R1T, as five files for a multicam edit.
Re-encoding five streams to get exact cuts costs time and picture quality, so
the cut is a stream copy. A stream copy is exact only where the source allows
it. My first three attempts came out of sync, and each failure is the reason
for one step in the procedure.</p>
<h2 id="how-the-truck-records">How the truck records<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#how-the-truck-records"
     aria-label="Link to section: How the truck records" data-pagefind-ignore></a>
</h2>
<p>The truck writes one folder per drive, named <code>MM_DD_YY_HHMMSS</code> in local time.
It holds one file per camera per 15-minute segment,
<code>&lt;drive&gt;_&lt;camera&gt;_&lt;NNN&gt;.mp4</code>, and every segment except the last runs 900 s.
The cameras are <code>frontCenter</code>, <code>rearCenter</code>, <code>sideLeft</code>, <code>sideRight</code>,
and <code>gearGuard</code>.</p>
<p>Three properties of the files decide how they can be cut and synced:</p>
<ul>
<li>The video is H.264 at 30 fps, with I-frames and P-frames only and one
keyframe per second.</li>
<li>The files have no audio track and no timecode track.</li>
<li>All five cameras of a segment have the same 900 s duration; their
<code>creation_time</code> can differ by a couple of seconds, so it can&rsquo;t sync them.</li>
</ul>
<p>With no audio and no timecode, an editor has nothing to sync the angles on but
each file&rsquo;s first frame. The cut has to start every camera on a known frame,
so the edit can line the first frames up.</p>
<figure><img src="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/five-cameras-one-moment_hu_c7373bb90be2c6e0.jpg" alt="One second on the I-90 floating bridge from all five cameras of a synced cut: frontCenter over the hood, rearCenter down the lanes behind, sideLeft and sideRight across the deck, and gearGuard over the truck bed. A red car passing behind appears in the rearCenter, sideLeft and gearGuard tiles at the same instant." width="1600" height="853" loading="lazy" decoding="async"><figcaption>The same frame of each camera&rsquo;s cut of a crossing. Plates blurred.</figcaption>
</figure>
<h2 id="why-stream-copied-cameras-drift">Why stream-copied cameras drift<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#why-stream-copied-cameras-drift"
     aria-label="Link to section: Why stream-copied cameras drift" data-pagefind-ignore></a>
</h2>
<p>A stream copy can&rsquo;t start a file on a P-frame, because a P-frame is stored as a
difference from earlier frames that the new file won&rsquo;t contain. It starts on a
keyframe. Two details of this footage turn that rule into drift:</p>
<ul>
<li><strong>Keyframes don&rsquo;t fall at the same instant on every camera.</strong> On most cameras
they fall on whole seconds. On some drives, one or more cameras put them
0.967 s after each second, one frame before the next whole second.</li>
<li><strong>Some cameras&rsquo; groups of pictures run across segment files.</strong> A segment
can open with frames that come before its first keyframe. Those frames
reference frames at the end of the previous file.</li>
</ul>
<p>Each attempt that failed ran into one of these, and the checks in the last
step of the procedure catch each one:</p>
<div class="table-wrap" data-scrollable data-label="Table: Cut">
  <table>
    <thead>
      <tr><th scope="col">Cut</th><th scope="col">What came out</th><th scope="col">Step that prevents it</th></tr>
    </thead>
    <tbody>
      <tr><td>Each segment cut separately, then joined</td><td>The offset camera 1.9 s short, from a late start and frames lost at the join; the timestamps run on without a gap</td><td>Write one concat listing per camera</td></tr>
      <tr><td>One concat pass, the same in-point for every camera</td><td>The offset camera starts at its next keyframe, short by the frames before it</td><td>Snap the in-point to the camera&rsquo;s own keyframe</td></tr>
      <tr><td>An in-point that lands after the keyframe</td><td>The file carries a <code>start_time</code> of 0.967 s instead of 0</td><td>Snap the in-point to the camera&rsquo;s own keyframe</td></tr>
    </tbody>
  </table>
</div>
<p>Equal durations aren&rsquo;t enough. Two files can be the same length and still
start at different moments, so the checks look at frame counts and at
<code>start_time</code> together.</p>
<!-- style-check: procedure -->
<h2 id="find-the-segments-the-window-touches">Find the segments the window touches<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#find-the-segments-the-window-touches"
     aria-label="Link to section: Find the segments the window touches" data-pagefind-ignore></a>
</h2>
<p>Express the window in seconds from the start of the drive. Segment <code>NNN</code> covers
drive seconds (NNN − 1) × 900 to NNN × 900. The examples here use a window
chosen to straddle a join, because that&rsquo;s where a cut goes wrong: from 1790 to
1830 s, it touches two segments:</p>
<div class="table-wrap" data-scrollable data-label="Table: Segment">
  <table>
    <thead>
      <tr><th scope="col">Segment</th><th scope="col"><code>inpoint</code> before snapping</th><th scope="col"><code>outpoint</code></th></tr>
    </thead>
    <tbody>
      <tr><td><code>002</code></td><td>890</td><td>900</td></tr>
      <tr><td><code>003</code></td><td>0</td><td>30</td></tr>
    </tbody>
  </table>
</div>
<p>Every segment after the first starts at <code>inpoint 0</code>, and every segment before
the last ends at <code>outpoint 900</code>. Only the first segment&rsquo;s in-point is snapped.</p>
<h2 id="find-each-cameras-keyframe-at-the-in-point">Find each camera&rsquo;s keyframe at the in-point<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#find-each-cameras-keyframe-at-the-in-point"
     aria-label="Link to section: Find each camera’s keyframe at the in-point" data-pagefind-ignore></a>
</h2>
<p>Each camera writes a keyframe about once a second, but not at the same
fraction of a second as the others, and not always at the same fraction for a
whole segment. On one drive, the Gear Guard camera&rsquo;s keyframes start on whole
seconds and slip to whole seconds plus 0.933 by the end of the segment:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">-show_entries</span> <span class="console__token">frame=pts_time</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">-read_intervals</span> <span class="console__token">%+3</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">08_02_26_120000_gearGuard_001.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">0.000000</span>
</span><span class="line"><span class="console__token">1.000000</span>
</span><span class="line"><span class="console__token">1.966667</span>
</span><span class="line"><span class="console__token">2.966667</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">-skip_frame</span> <span class="console__token">nokey</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">-show_entries</span> <span class="console__token">frame=pts_time</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">-read_intervals</span> <span class="console__token">887%+3.001</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">08_02_26_120000_gearGuard_001.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">887.933333</span>
</span><span class="line"><span class="console__token">888.933333</span>
</span><span class="line"><span class="console__token">889.933333</span></span></code></pre>
    </div>
  </div></div><p>Across the drives I checked, keyframes sat on whole seconds or at whole seconds
plus anything from 0.733 to 0.967, and most cameras that weren&rsquo;t on whole
seconds drifted within a segment. Which camera is offset, and by how much,
changes from drive to drive. Test your own footage rather than assuming any of
these numbers: list the keyframes in the 3 seconds before the in-point, in the
segment that contains it, for every camera.</p>
<h2 id="snap-the-in-point-to-the-cameras-own-keyframe">Snap the in-point to the camera&rsquo;s own keyframe<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#snap-the-in-point-to-the-cameras-own-keyframe"
     aria-label="Link to section: Snap the in-point to the camera’s own keyframe" data-pagefind-ignore></a>
</h2>
<p>For each camera, move the first in-point back to the latest keyframe at or
before it, from the listing near the in-point. On the drive above, 890 becomes
889.933333 for the Gear Guard camera and stays 890 for the others. Measuring
the offset at the start of the segment and applying it at 890 would give
889.966667, which is after the real keyframe.</p>
<p>Snap in drive time, not segment time. When the in-point falls before the first
keyframe of its segment, start on the last keyframe of the previous segment,
because the first frames of a segment reference the tail of the one before it.
Only at the start of a drive, with nothing before it, does such an in-point
become 0.</p>
<p>Rounding back rather than forward is the point. On the drive these examples
come from, a stream copy given 890 on the offset side-left camera started at
its next keyframe, 890.967, so the file opened 0.967 s late and carried that
as its <code>start_time</code>. Given 889.966667, its keyframe there, it started on the
keyframe exactly, at time 0. The snapped camera&rsquo;s first frames come before
the other cameras&rsquo; first frame, by the gap between its keyframe and the
in-point: 1 frame at 0.967, 8 frames at 0.733. A stream copy can&rsquo;t remove
them, so the edit does: set that clip&rsquo;s In point that many frames in.</p>
<figure>
  <img src="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/keyframe-snap-timeline.svg" alt="A timeline from 887.5 s into segment 002 to 2.5 s into segment 003. frontCenter has a keyframe on every whole second, and sideLeft one at every whole second plus 0.967, on both sides of the join at 900. A dashed line marks the requested in-point at 890. On sideLeft, the snapped in-point is the keyframe at 889.967, 33 ms before it; an unsnapped stream copy starts at the next keyframe, 890.967." loading="lazy" decoding="async"><figcaption>Keyframe times as ffprobe reports them for one drive.</figcaption>
</figure>
<h2 id="write-one-concat-listing-per-camera">Write one concat listing per camera<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#write-one-concat-listing-per-camera"
     aria-label="Link to section: Write one concat listing per camera" data-pagefind-ignore></a>
</h2>
<p>Write a listing for each camera with one <code>file</code>, <code>inpoint</code>, and <code>outpoint</code> entry
per segment. <code>&lt;drive-folder&gt;</code> stands for the drive folder&rsquo;s absolute path, which
is why the cut later needs <code>-safe 0</code>:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">file &#39;&lt;drive-folder&gt;/07_16_26_120000_sideLeft_002.mp4&#39;
</span></span><span class="line"><span class="cl">inpoint 889.966667
</span></span><span class="line"><span class="cl">outpoint 900
</span></span><span class="line"><span class="cl">file &#39;&lt;drive-folder&gt;/07_16_26_120000_sideLeft_003.mp4&#39;
</span></span><span class="line"><span class="cl">inpoint 0
</span></span><span class="line"><span class="cl">outpoint 30</span></span></code></pre></div></div>
  </div>
</div><p>One listing for both segments is what keeps the frames after the join. The
concat demuxer reads segment <code>003</code> straight after the tail of <code>002</code>, so the
frames at the start of <code>003</code> that reference <code>002</code> still have their reference.
Cutting <code>003</code> on its own drops them, and joining the two pieces afterwards
can&rsquo;t bring them back.</p>
<h2 id="cut-each-camera-with-a-stream-copy">Cut each camera with a stream copy<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#cut-each-camera-with-a-stream-copy"
     aria-label="Link to section: Cut each camera with a stream copy" data-pagefind-ignore></a>
</h2>
<p>Run the concat demuxer once per camera. This writes a new file and leaves the
source segments untouched:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-f</span> <span class="console__token">concat</span> <span class="console__token">-safe</span> <span class="console__token">0</span> <span class="console__token">-i</span> <span class="console__token">07_16_26_120000_sideLeft.txt</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">-c</span> <span class="console__token">copy</span> <span class="console__token">07_16_26_120000_sideLeft.mp4</span></span></code></pre>
    </div>
  </div></div><p>The following script does the last four steps for all five cameras. It takes
the drive folder, the window&rsquo;s start and end in drive seconds, and an output
folder, and writes each camera&rsquo;s listing and clip there:</p>
<div class="code-block" data-copyable data-copy-label="Bash">
  <div class="code-block__head"><span class="code-block__lang">Bash</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nv">folder</span><span class="o">=</span><span class="k">$(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">pwd</span><span class="k">)</span> <span class="nv">start</span><span class="o">=</span><span class="nv">$2</span> <span class="nv">end</span><span class="o">=</span><span class="nv">$3</span> <span class="nv">out</span><span class="o">=</span><span class="nv">$4</span>
</span></span><span class="line"><span class="cl"><span class="nv">drive</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">segment_seconds</span><span class="o">=</span><span class="m">900</span>
</span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Stop with the camera and the file named, rather than ffprobe&#39;s bare error.</span>
</span></span><span class="line"><span class="cl">require<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> ! -f <span class="nv">$1</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2">: no file </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Print the latest keyframe at or before $3 seconds into file $1, looking back</span>
</span></span><span class="line"><span class="cl"><span class="c1"># $2 seconds; print nothing if there is none in that span.</span>
</span></span><span class="line"><span class="cl">keyframe_at_or_before<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">local</span> <span class="nv">file</span><span class="o">=</span><span class="nv">$1</span> <span class="nv">back</span><span class="o">=</span><span class="nv">$2</span> <span class="nv">at</span><span class="o">=</span><span class="nv">$3</span> from
</span></span><span class="line"><span class="cl">  <span class="nv">from</span><span class="o">=</span><span class="k">$(</span>awk -v <span class="nv">a</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$at</span><span class="s2">&#34;</span> -v <span class="nv">b</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$back</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { f = a - b; if (f &lt; 0) f = 0; print f }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  ffprobe -v error -select_streams v -skip_frame nokey <span class="se">\
</span></span></span><span class="line"><span class="cl">    -show_entries <span class="nv">frame</span><span class="o">=</span>pts_time -of <span class="nv">csv</span><span class="o">=</span><span class="nv">p</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -read_intervals <span class="s2">&#34;</span><span class="si">${</span><span class="nv">from</span><span class="si">}</span><span class="s2">%</span><span class="k">$(</span>awk -v <span class="nv">a</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$at</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { print a + 0.001 }&#39;</span><span class="k">)</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span> <span class="p">|</span>
</span></span><span class="line"><span class="cl">    cut -d, -f1 <span class="p">|</span> awk -v <span class="nv">a</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$at</span><span class="s2">&#34;</span> -v <span class="nv">f</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$from</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s1">&#39;$1 &gt;= f - 0.0005 &amp;&amp; $1 &lt;= a + 0.0005 { k = $1 } END { if (k != &#34;&#34;) print k }&#39;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> camera in frontCenter rearCenter sideLeft sideRight gearGuard<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nv">segment</span><span class="o">=</span><span class="k">$((</span> start <span class="o">/</span> segment_seconds <span class="o">+</span> <span class="m">1</span> <span class="k">))</span>
</span></span><span class="line"><span class="cl">  <span class="nv">local_in</span><span class="o">=</span><span class="k">$((</span> start <span class="o">-</span> <span class="o">(</span>segment <span class="o">-</span> <span class="m">1</span><span class="o">)</span> <span class="o">*</span> segment_seconds <span class="k">))</span>
</span></span><span class="line"><span class="cl">  <span class="nv">file</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">/</span><span class="si">${</span><span class="nv">drive</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">_</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%03d&#39;</span> <span class="s2">&#34;</span><span class="nv">$segment</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">.mp4&#34;</span>
</span></span><span class="line"><span class="cl">  require <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">length</span><span class="o">=</span><span class="k">$(</span>ffprobe -v error -show_entries <span class="nv">format</span><span class="o">=</span>duration -of <span class="nv">csv</span><span class="o">=</span><span class="nv">p</span><span class="o">=</span><span class="m">0</span> <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> awk -v <span class="nv">l</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$length</span><span class="s2">&#34;</span> -v <span class="nv">i</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$local_in</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { exit !(l &lt;= i) }&#39;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2">: </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2"> ends before </span><span class="si">${</span><span class="nv">local_in</span><span class="si">}</span><span class="s2">s&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nv">keyframe</span><span class="o">=</span><span class="k">$(</span>keyframe_at_or_before <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span> <span class="m">3</span> <span class="s2">&#34;</span><span class="nv">$local_in</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -n <span class="nv">$keyframe</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">snapped</span><span class="o">=</span><span class="k">$(</span>awk -v <span class="nv">k</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$keyframe</span><span class="s2">&#34;</span> -v <span class="nv">b</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span>segment <span class="o">-</span> <span class="m">1</span><span class="o">)</span> <span class="o">*</span> segment_seconds <span class="k">))</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s1">&#39;BEGIN { printf &#34;%.6f&#34;, b + k }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">elif</span> <span class="o">((</span> local_in &gt;<span class="o">=</span> <span class="m">3</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># No keyframe this far into the segment means the file ends before it.</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2">: </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2"> ends before </span><span class="si">${</span><span class="nv">local_in</span><span class="si">}</span><span class="s2">s&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">elif</span> <span class="o">((</span> segment &gt; <span class="m">1</span> <span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The in-point falls before this segment&#39;s first keyframe, so the frames</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># there depend on the previous segment: start on its last keyframe.</span>
</span></span><span class="line"><span class="cl">    <span class="nv">previous</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">/</span><span class="si">${</span><span class="nv">drive</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">_</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%03d&#39;</span> <span class="k">$((</span> segment <span class="o">-</span> <span class="m">1</span> <span class="k">)))</span><span class="s2">.mp4&#34;</span>
</span></span><span class="line"><span class="cl">    require <span class="s2">&#34;</span><span class="nv">$previous</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">keyframe</span><span class="o">=</span><span class="k">$(</span>keyframe_at_or_before <span class="s2">&#34;</span><span class="nv">$previous</span><span class="s2">&#34;</span> <span class="m">3</span> <span class="s2">&#34;</span><span class="nv">$segment_seconds</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[[</span> -z <span class="nv">$keyframe</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2">: segment </span><span class="k">$((</span> segment <span class="o">-</span> <span class="m">1</span> <span class="k">))</span><span class="s2"> ends early&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">      <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nv">snapped</span><span class="o">=</span><span class="k">$(</span>awk -v <span class="nv">k</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$keyframe</span><span class="s2">&#34;</span> -v <span class="nv">b</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span>segment <span class="o">-</span> <span class="m">2</span><span class="o">)</span> <span class="o">*</span> segment_seconds <span class="k">))</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s1">&#39;BEGIN { printf &#34;%.6f&#34;, b + k }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nv">snapped</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2"> starts </span><span class="k">$(</span>awk -v <span class="nv">s</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$snapped</span><span class="s2">&#34;</span> -v <span class="nv">a</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$start</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s1">&#39;BEGIN { printf &#34;%d&#34;, (a - s) * 30 + 0.5 }&#39;</span><span class="k">)</span><span class="s2"> frames early&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">first</span><span class="o">=</span><span class="k">$(</span>awk -v <span class="nv">s</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$snapped</span><span class="s2">&#34;</span> -v <span class="nv">n</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$segment_seconds</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { print int(s / n) + 1 }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="nv">last</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span>end <span class="o">-</span> <span class="m">1</span><span class="o">)</span> <span class="o">/</span> segment_seconds <span class="o">+</span> <span class="m">1</span> <span class="k">))</span>
</span></span><span class="line"><span class="cl">  <span class="nv">listing</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">/</span><span class="si">${</span><span class="nv">drive</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">.txt&#34;</span>
</span></span><span class="line"><span class="cl">  : &gt; <span class="s2">&#34;</span><span class="nv">$listing</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="o">((</span> <span class="nv">segment</span> <span class="o">=</span> first<span class="p">;</span> segment &lt;<span class="o">=</span> last<span class="p">;</span> segment++ <span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">source</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$folder</span><span class="s2">/</span><span class="si">${</span><span class="nv">drive</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">_</span><span class="k">$(</span><span class="nb">printf</span> <span class="s1">&#39;%03d&#39;</span> <span class="s2">&#34;</span><span class="nv">$segment</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">.mp4&#34;</span>
</span></span><span class="line"><span class="cl">    require <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nv">segment_start</span><span class="o">=</span><span class="k">$((</span> <span class="o">(</span>segment <span class="o">-</span> <span class="m">1</span><span class="o">)</span> <span class="o">*</span> segment_seconds <span class="k">))</span>
</span></span><span class="line"><span class="cl">    <span class="nv">clip_in</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="o">((</span> <span class="nv">segment</span> <span class="o">==</span> first <span class="o">))</span> <span class="o">&amp;&amp;</span> <span class="nv">clip_in</span><span class="o">=</span><span class="k">$(</span>awk -v <span class="nv">s</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$snapped</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -v <span class="nv">b</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$segment_start</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { printf &#34;%.6f&#34;, s - b }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">clip_out</span><span class="o">=</span><span class="k">$((</span> end <span class="o">-</span> segment_start <span class="k">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">((</span> clip_out &gt; segment_seconds <span class="o">))</span> <span class="o">&amp;&amp;</span> <span class="nv">clip_out</span><span class="o">=</span><span class="nv">$segment_seconds</span>
</span></span><span class="line"><span class="cl">    <span class="nv">length</span><span class="o">=</span><span class="k">$(</span>ffprobe -v error -show_entries <span class="nv">format</span><span class="o">=</span>duration -of <span class="nv">csv</span><span class="o">=</span><span class="nv">p</span><span class="o">=</span><span class="m">0</span> <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> awk -v <span class="nv">l</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$length</span><span class="s2">&#34;</span> -v <span class="nv">o</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$clip_out</span><span class="s2">&#34;</span> <span class="s1">&#39;BEGIN { exit !(l &lt; o - 0.05) }&#39;</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$camera</span><span class="s2">: </span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2"> ends before </span><span class="si">${</span><span class="nv">clip_out</span><span class="si">}</span><span class="s2">s&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">      <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nb">printf</span> <span class="s2">&#34;file &#39;%s&#39;\ninpoint %s\noutpoint %s\n&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      <span class="s2">&#34;</span><span class="nv">$source</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$clip_in</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$clip_out</span><span class="s2">&#34;</span> &gt;&gt; <span class="s2">&#34;</span><span class="nv">$listing</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl">  ffmpeg -nostdin -v error -f concat -safe <span class="m">0</span> -i <span class="s2">&#34;</span><span class="nv">$listing</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -c copy <span class="s2">&#34;</span><span class="nv">$out</span><span class="s2">/</span><span class="si">${</span><span class="nv">drive</span><span class="si">}</span><span class="s2">_</span><span class="si">${</span><span class="nv">camera</span><span class="si">}</span><span class="s2">.mp4&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div></div>
  </div>
</div><div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">bash</span> <span class="console__token">cut-window.sh</span> <span class="console__token">&lt;drive-folder&gt;</span> <span class="console__token">1790</span> <span class="console__token">1830</span> <span class="console__token">cuts</span></span></code></pre>
    </div>
  </div></div><p>For each camera, the script prints how many frames before the window&rsquo;s start
its file begins: 0 for a camera with a keyframe on the start, more for an
offset one. Keep that list for the edit.</p>
<p>If a camera has no file for a segment the window touches, or its file ends
before the window does, the script stops and names the camera and the file
rather than writing a short clip. The camera usually <a href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/">restarted into a sibling
folder</a> partway through the
drive.</p>
<h2 id="verify-the-start-and-frame-count-of-every-camera">Verify the start and frame count of every camera<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#verify-the-start-and-frame-count-of-every-camera"
     aria-label="Link to section: Verify the start and frame count of every camera" data-pagefind-ignore></a>
</h2>
<p>In the output folder, print each file&rsquo;s <code>start_time</code>, the number of frames that
decode, and the number of packets in the file:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">for</span> <span class="console__token">clip</span> <span class="console__token">in</span> <span class="console__token">*.mp4;</span> <span class="console__token">do</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">printf</span> <span class="console__token">&#39;%s</span> <span class="console__token">&#39;</span> <span class="console__token">&#34;$clip&#34;</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">-count_frames</span> <span class="console__token">-count_packets</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;     </span><span class="console__token">-show_entries</span> <span class="console__token">stream=start_time,nb_read_frames,nb_read_packets</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;     </span><span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">&#34;$clip&#34;</span>
</span><span class="line"><span class="prompt">&gt; </span><span class="console__token">done</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">07_16_26_120000_frontCenter.mp4</span> <span class="console__token">0.000000,1200,1200</span>
</span><span class="line"><span class="console__token">07_16_26_120000_gearGuard.mp4</span> <span class="console__token">0.000000,1200,1200</span>
</span><span class="line"><span class="console__token">07_16_26_120000_rearCenter.mp4</span> <span class="console__token">0.000000,1201,1201</span>
</span><span class="line"><span class="console__token">07_16_26_120000_sideLeft.mp4</span> <span class="console__token">0.000000,1201,1201</span>
</span><span class="line"><span class="console__token">07_16_26_120000_sideRight.mp4</span> <span class="console__token">0.000000,1200,1200</span></span></code></pre>
    </div>
  </div></div><p>The cut is good when all three hold:</p>
<dl class="facts">
  <dt>Start</dt>
  <dd>Every file&rsquo;s <code>start_time</code>, the first column, is <code>0.000000</code>.</dd>
  <dt>Length</dt>
  <dd>The frame counts differ only by each camera&rsquo;s lead, the frames the script reported it starts early.</dd>
  <dt>Join</dt>
  <dd>The decoded frames equal the packets on every file, so every frame after the segment join decoded.</dd>
</dl>

<p>The third check is the one that sees the join. <code>-count_frames</code> decodes the
whole file and counts only the frames that decoded; a frame whose reference is
missing is dropped without an error. The same command on the source segment
<code>003</code> on its own, outside the concat pass, shows the frames that depend on
<code>002</code>:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-select_streams</span> <span class="console__token">v</span> <span class="console__token">-count_frames</span> <span class="console__token">-count_packets</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">-show_entries</span> <span class="console__token">stream=nb_read_frames,nb_read_packets</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">\</span>
</span><span class="line"><span class="prompt">&gt;   </span><span class="console__token">07_16_26_120000_sideLeft_003.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">26971,27000</span></span></code></pre>
    </div>
  </div></div><p>Those 29 frames are the ones a per-segment cut loses. Decoding with
<code>ffmpeg -v error … -f null -</code> prints nothing for them, so a silent decode is not
evidence of a clean join. Compare the counts.</p>
<!-- style-check: end procedure -->
<h2 id="build-the-multicam-clip">Build the multicam clip<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#build-the-multicam-clip"
     aria-label="Link to section: Build the multicam clip" data-pagefind-ignore></a>
</h2>
<p>In DaVinci Resolve, the leads the script printed are the only alignment the
five files need:</p>
<ol>
<li>Import the five files into the Media Pool.</li>
<li>For each camera with a lead, open its file in the source viewer, step
forward that many frames from the first, and mark an In point.</li>
<li>Select all five, right-click, and choose <strong>Create New Multicam Clip Using
Selected Clips</strong>.</li>
<li>Set <strong>Angle Sync</strong> to <strong>In</strong>.</li>
<li>Create the multicam clip, and put it on a timeline.</li>
<li>Switch the Edit page&rsquo;s source viewer to <strong>Multicam</strong> and cut between angles
during playback.</li>
</ol>
<p><strong>In</strong> lines up each file&rsquo;s In point, which is its first frame unless you
mark one. The other sync methods need something
these files don&rsquo;t carry: <strong>Timecode</strong> needs a timecode track and <strong>Sound</strong>
needs audio. A file whose first frame is 0.967 s later than the others&rsquo; is
0.967 s out of sync after an In-point sync, whatever its <code>start_time</code> says.
That&rsquo;s why the verification step checks the start before the files reach
Resolve.</p>
<h2 id="check-each-clip-for-a-location-before-you-share-it">Check each clip for a location before you share it<a class="heading-anchor" href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#check-each-clip-for-a-location-before-you-share-it"
     aria-label="Link to section: Check each clip for a location before you share it" data-pagefind-ignore></a>
</h2>
<p>The concat demuxer drops the truck&rsquo;s GPS fix, but other ways of cutting keep
it; <a href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/">Read the GPS location from Rivian dashcam video with
ffprobe</a> covers which. Check the file
you&rsquo;re about to share:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">exiftool</span> <span class="console__token">-G1</span> <span class="console__token">-a</span> <span class="console__token">&#34;-gps*&#34;</span> <span class="console__token">&#34;-location*&#34;</span> <span class="console__token">07_16_26_120000_sideLeft.mp4</span></span></code></pre>
    </div>
  </div></div><p>Empty output means no location field was found.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>ffmpeg</dt> <dd>9.0.1, 9.0.2</dd></div>
    <div class="tested__row"><dt>macOS</dt> <dd>27.0</dd></div>
    <div class="tested__row"><dt>Footage</dt> <dd>Rivian dashcam, five cameras, 900 s segments</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Rivian Dashcam Camera Missing from a Drive: It Restarted in Another Folder</title>
      <link>https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/</guid>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>Python</category>
      
      <description>A Rivian R1T camera that stops partway through a drive often restarted into a sibling folder; motion correlation, not folder names, gives its offset.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>The camera didn&rsquo;t stop recording. It restarted seconds later into a new drive
folder that holds only the restarted cameras. Look for a folder named a few
seconds after the broken one, with fewer than five cameras in it.</p>
<p>Don&rsquo;t take the offset from the two folder names. Measure it by correlating the
motion of two cameras that face the same way, such as the Gear Guard camera
and the rear camera. Then re-encode the restarted cameras from the sibling folder at that
offset, so that they start on the exact frame.</p>

</div>

<p>I cut a five-camera clip from a drive on August 7 and dropped it into a
multicam timeline. Three angles played to the end. The rear camera and the
side-left camera went black after 33 seconds. Every file was readable and nothing
reported an error. The footage I thought was lost was in the next folder.</p>
<h2 id="rear-or-side-camera-stopped-recording-after-a-few-seconds">Rear or side camera stopped recording after a few seconds<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#rear-or-side-camera-stopped-recording-after-a-few-seconds"
     aria-label="Link to section: Rear or side camera stopped recording after a few seconds" data-pagefind-ignore></a>
</h2>
<p>The symptom has a few forms, depending on what you build from the drive:</p>
<ul>
<li>A camera&rsquo;s only file in the drive folder is a few seconds long, while the
other cameras have a full set of 15-minute segments.</li>
<li>A multicam clip or a grid of the five cameras shows black tiles for one or two
angles for most of the drive.</li>
<li>A hyperlapse of one angle is much shorter than the others, or ends in black.</li>
</ul>
<p>On the August 7 drive, the folder <code>08_07_26_140000</code> holds full segments for the
front, Gear Guard and side-right cameras, and one 33-second file each for the
rear and side-left cameras:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-show_entries</span> <span class="console__token">format=duration</span> <span class="console__token">-of</span> <span class="console__token">csv=p=0</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000/08_07_26_140000_rearCenter_001.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">33.000000</span></span></code></pre>
    </div>
  </div></div><figure><img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/five-cameras-before-stitch_hu_1be5a11e4db848dc.jpg" alt="A five-camera grid on the I-90 floating bridge built from the original folder alone. The front, side-right and Gear Guard tiles show the bridge; the rear and side-left tiles are solid black." width="1600" height="853" loading="lazy" decoding="async"><figcaption>The August 7 drive on the floating bridge, from 08_07_26_140000 only. Plates blurred.</figcaption>
</figure>
<h2 id="cause-the-camera-restarted-into-a-sibling-folder">Cause: the camera restarted into a sibling folder<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#cause-the-camera-restarted-into-a-sibling-folder"
     aria-label="Link to section: Cause: the camera restarted into a sibling folder" data-pagefind-ignore></a>
</h2>
<p>The truck writes one folder per drive, named <code>MM_DD_YY_HHMMSS</code> in local time, and
one file per camera per 15-minute segment inside it. When a camera stops and
restarts partway through a drive, its recording doesn&rsquo;t continue in the
drive&rsquo;s folder. A new folder is created a few seconds later, and it holds only
the cameras that restarted. The rest of the drive stays in the original folder.</p>
<p>The two folders together hold the whole drive. Nothing was lost, but the drive is
now split across two clocks.</p>
<h2 id="find-the-sibling-folder">Find the sibling folder<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#find-the-sibling-folder"
     aria-label="Link to section: Find the sibling folder" data-pagefind-ignore></a>
</h2>
<p>Run this in the <code>DriveStream</code> folder of the truck&rsquo;s drive, or of your copy of it.
It prints each drive folder with the cameras it holds:</p>
<div class="code-block" data-copyable data-copy-label="Bash">
  <div class="code-block__head"><span class="code-block__lang">Bash</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> d in */<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">printf</span> <span class="s1">&#39;%s &#39;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">d</span><span class="p">%/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  ls <span class="s2">&#34;</span><span class="nv">$d</span><span class="s2">&#34;</span> <span class="p">|</span> grep -v <span class="s1">&#39;^\._&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> sed -E <span class="s1">&#39;s/^[0-9_]+_([A-Za-z]+)_[0-9]{3}\.mp4$/\1/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> sort -u <span class="p">|</span> tr <span class="s1">&#39;\n&#39;</span> <span class="s1">&#39; &#39;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div></div>
  </div>
</div><p>The <code>grep -v '^\._'</code> drops the AppleDouble files that macOS leaves beside every
clip on the truck&rsquo;s exFAT drive. The August 7 folders look like this:</p>
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">08_07_26_100000 frontCenter gearGuard rearCenter sideLeft sideRight
</span></span><span class="line"><span class="cl">08_07_26_140000 frontCenter gearGuard rearCenter sideLeft sideRight
</span></span><span class="line"><span class="cl">08_07_26_140034 rearCenter sideLeft
</span></span><span class="line"><span class="cl">08_07_26_160000 frontCenter gearGuard rearCenter sideLeft sideRight</span></span></code></pre></div></div>
  </div>
</div><p>A sibling folder has all three of these properties:</p>
<ul>
<li>Its name is seconds, not minutes, after the previous folder&rsquo;s name.</li>
<li>It holds fewer than five cameras.</li>
<li>Its cameras are the ones that stop early in the previous folder.</li>
</ul>
<p>A short folder with one camera and no partner is a different case: a stray
fragment, not half of a drive. Across 233 drive folders, one was a 21-second
fragment from a single camera.</p>
<h2 id="folder-name-offset-is-wrong">Folder name offset is wrong<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#folder-name-offset-is-wrong"
     aria-label="Link to section: Folder name offset is wrong" data-pagefind-ignore></a>
</h2>
<p>The folder names differ by 34 seconds, so the obvious fix is to place the
restarted cameras 34 seconds into the drive. Measured from the footage, the
restarted cameras begin 32.7 seconds into the drive. The names are wrong by more
than a second. The files&rsquo; <code>creation_time</code> tags are no better: the two folders'
differ by 35 seconds, a third answer.</p>
<p>A folder name is a label, not a clock: nothing ties it to the first frame of
the video inside. An error of more than a second is visible in a multicam edit.
A car that passes the truck crosses the rear view and the side view at moments
that no longer match. The truck&rsquo;s files carry no timecode track to correct it, so
the offset has to come from the pictures.</p>
<h2 id="measure-the-offset-by-motion-correlation">Measure the offset by motion correlation<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#measure-the-offset-by-motion-correlation"
     aria-label="Link to section: Measure the offset by motion correlation" data-pagefind-ignore></a>
</h2>
<p>Two cameras that see the same scene change at the same moments. When the truck
hits a bump or a car passes behind it, both backward-facing pictures move at
once. The script reduces each camera to a motion signal and slides one signal
along the other until they match best:</p>
<ol>
<li>Decode a window of each clip at a fixed sample rate, scaled to a tiny
grayscale frame.</li>
<li>Take the mean absolute change between consecutive frames. That&rsquo;s the motion
signal: high when the picture changes, low when it holds still.</li>
<li>Standardize the reference, and each stretch of the other signal it&rsquo;s
compared with, so that a brighter or busier camera or stretch doesn&rsquo;t
outweigh the rest.</li>
<li>Try every lag within a search range around your guess, and keep the lag with
the highest correlation.</li>
</ol>
<p>It needs Python 3 and <code>ffmpeg</code> on your path, and nothing else:</p>
<div class="code-block" data-copyable data-copy-label="measure_offset.py">
  <div class="code-block__head"><span class="code-block__lang code-block__file">measure_offset.py</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;Measure the time offset between two dashcam clips by correlating their motion.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Usage:
</span></span></span><span class="line"><span class="cl"><span class="s2">    python3 measure_offset.py REFERENCE OTHER --at SECONDS [--guess SECONDS]
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Reads a window of REFERENCE starting at --at, and a slightly wider window of
</span></span></span><span class="line"><span class="cl"><span class="s2">OTHER around --at minus --guess. Prints the offset that lines the two up:
</span></span></span><span class="line"><span class="cl"><span class="s2">a moment at time t in REFERENCE is at time t - offset in OTHER.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">argparse</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">WIDTH</span><span class="p">,</span> <span class="n">HEIGHT</span> <span class="o">=</span> <span class="mi">32</span><span class="p">,</span> <span class="mi">24</span>
</span></span><span class="line"><span class="cl"><span class="n">SAMPLES_PER_SECOND</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">motion</span><span class="p">(</span><span class="n">clip</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">seconds</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Mean absolute change between consecutive tiny grayscale frames.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">raw</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s2">&#34;ffmpeg&#34;</span><span class="p">,</span> <span class="s2">&#34;-nostdin&#34;</span><span class="p">,</span> <span class="s2">&#34;-v&#34;</span><span class="p">,</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-ss&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">start</span><span class="si">:</span><span class="s2">.3f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;-t&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">seconds</span><span class="si">:</span><span class="s2">.3f</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;-i&#34;</span><span class="p">,</span> <span class="n">clip</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-vf&#34;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;fps=</span><span class="si">{</span><span class="n">SAMPLES_PER_SECOND</span><span class="si">}</span><span class="s2">,scale=</span><span class="si">{</span><span class="n">WIDTH</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">HEIGHT</span><span class="si">}</span><span class="s2">,format=gray&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-f&#34;</span><span class="p">,</span> <span class="s2">&#34;rawvideo&#34;</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">stdout</span>
</span></span><span class="line"><span class="cl">    <span class="n">size</span> <span class="o">=</span> <span class="n">WIDTH</span> <span class="o">*</span> <span class="n">HEIGHT</span>
</span></span><span class="line"><span class="cl">    <span class="n">frames</span> <span class="o">=</span> <span class="p">[</span><span class="n">raw</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span> <span class="o">+</span> <span class="n">size</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span> <span class="o">-</span> <span class="n">size</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="nb">sum</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">before</span><span class="p">,</span> <span class="n">after</span><span class="p">))</span> <span class="o">/</span> <span class="n">size</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">before</span><span class="p">,</span> <span class="n">after</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">frames</span><span class="p">,</span> <span class="n">frames</span><span class="p">[</span><span class="mi">1</span><span class="p">:])]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">standardize</span><span class="p">(</span><span class="n">values</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">mean</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">variance</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">((</span><span class="n">v</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span> <span class="o">**</span> <span class="mi">2</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">)</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">values</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">spread</span> <span class="o">=</span> <span class="n">variance</span> <span class="o">**</span> <span class="mf">0.5</span> <span class="ow">or</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">v</span> <span class="o">-</span> <span class="n">mean</span><span class="p">)</span> <span class="o">/</span> <span class="n">spread</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">values</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">best_lag</span><span class="p">(</span><span class="n">reference</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">],</span> <span class="n">other</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Slide reference along other; return the best correlation and its lag.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">scores</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">lag</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">other</span><span class="p">)</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">window</span> <span class="o">=</span> <span class="n">standardize</span><span class="p">(</span><span class="n">other</span><span class="p">[</span><span class="n">lag</span><span class="p">:</span><span class="n">lag</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">        <span class="n">score</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">a</span> <span class="o">*</span> <span class="n">b</span> <span class="k">for</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">reference</span><span class="p">,</span> <span class="n">window</span><span class="p">))</span> <span class="o">/</span> <span class="nb">len</span><span class="p">(</span><span class="n">reference</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">scores</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">score</span><span class="p">,</span> <span class="n">lag</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">max</span><span class="p">(</span><span class="n">scores</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;reference&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;other&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--at&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;window start in REFERENCE, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--guess&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;expected offset, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--length&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">120.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;window length, seconds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">&#34;--search&#34;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">float</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mf">5.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">help</span><span class="o">=</span><span class="s2">&#34;how far either side of the guess to look&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">reference</span> <span class="o">=</span> <span class="n">standardize</span><span class="p">(</span><span class="n">motion</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">reference</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">at</span><span class="p">,</span> <span class="n">args</span><span class="o">.</span><span class="n">length</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The other window starts --search seconds early, so lag 0 means guess + search.</span>
</span></span><span class="line"><span class="cl">    <span class="n">other_start</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">at</span> <span class="o">-</span> <span class="n">args</span><span class="o">.</span><span class="n">guess</span> <span class="o">-</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span>
</span></span><span class="line"><span class="cl">    <span class="n">other_length</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">length</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span>
</span></span><span class="line"><span class="cl">    <span class="n">other</span> <span class="o">=</span> <span class="n">motion</span><span class="p">(</span><span class="n">args</span><span class="o">.</span><span class="n">other</span><span class="p">,</span> <span class="n">other_start</span><span class="p">,</span> <span class="n">other_length</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">,</span> <span class="n">lag</span> <span class="o">=</span> <span class="n">best_lag</span><span class="p">(</span><span class="n">reference</span><span class="p">,</span> <span class="n">other</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">offset</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">guess</span> <span class="o">+</span> <span class="n">args</span><span class="o">.</span><span class="n">search</span> <span class="o">-</span> <span class="n">lag</span> <span class="o">/</span> <span class="n">SAMPLES_PER_SECOND</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;window at </span><span class="si">{</span><span class="n">args</span><span class="o">.</span><span class="n">at</span><span class="si">:</span><span class="s2">g</span><span class="si">}</span><span class="s2"> s: correlation </span><span class="si">{</span><span class="n">score</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">, offset </span><span class="si">{</span><span class="n">offset</span><span class="si">:</span><span class="s2">+.1f</span><span class="si">}</span><span class="s2"> s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span></span></span></code></pre></div></div>
  </div>
</div><p>Give it a camera from the original folder as the reference, a camera from the
sibling folder as the other, and the folder-name difference as the guess. Both
folders number their segments from <code>001</code> and every segment but the last runs
900 seconds, so segment <code>002</code> of the sibling starts the same offset after
segment <code>002</code> of the original. Compare files with the same segment number, and
keep the window inside one segment.</p>
<p>In the <code>DriveStream</code> folder, run:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">measure_offset.py</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000/08_07_26_140000_gearGuard_001.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140034/08_07_26_140034_rearCenter_001.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">--at</span> <span class="console__token">300</span> <span class="console__token">--guess</span> <span class="console__token">34</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">window</span> <span class="console__token">at</span> <span class="console__token">300</span> <span class="console__token">s:</span> <span class="console__token">correlation</span> <span class="console__token">0.84,</span> <span class="console__token">offset</span> <span class="console__token">+32.6</span> <span class="console__token">s</span></span></code></pre>
    </div>
  </div></div><p>One window is one measurement. Repeat it in windows that don&rsquo;t overlap, from
different segments, and trust the offset only when they agree. Four windows on
the August 7 drive gave this:</p>
<div class="table-wrap" data-scrollable data-label="Table: Segment">
  <table>
    <thead>
      <tr><th scope="col">Segment</th><th scope="col"><code>--at</code></th><th scope="col">Correlation</th><th scope="col">Offset</th></tr>
    </thead>
    <tbody>
      <tr><td><code>001</code></td><td>300</td><td>0.84</td><td>+32.6 s</td></tr>
      <tr><td><code>001</code></td><td>700</td><td>0.73</td><td>+32.7 s</td></tr>
      <tr><td><code>002</code></td><td>600</td><td>0.85</td><td>+32.8 s</td></tr>
      <tr><td><code>003</code></td><td>200</td><td>0.69</td><td>+32.7 s</td></tr>
    </tbody>
  </table>
</div>
<p>That&rsquo;s 32.7 seconds, plus or minus 0.1, from four independent windows. A moment
at time <em>t</em> in the original folder is at <em>t</em> − 32.7 in the sibling folder.</p>
<figure>
  <img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/motion-correlation-offset.svg" alt="Two plots of motion against time, from 300 to 330 seconds into the Gear Guard file, with the Gear Guard signal in blue and the restarted rear camera in amber. Shifted by the folder-name offset of 34 seconds, each amber peak lands about a second after the blue one. Shifted by the measured 32.7 seconds, the peaks fall together." loading="lazy" decoding="async"><figcaption>Motion from measure_offset.py&rsquo;s motion() function, standardized, for the first window in the table.</figcaption>
</figure>
<h3 id="compare-cameras-that-face-the-same-way">Compare cameras that face the same way<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#compare-cameras-that-face-the-same-way"
     aria-label="Link to section: Compare cameras that face the same way" data-pagefind-ignore></a>
</h3>
<p>The front camera was my first choice for the reference, because it recorded the
whole drive. Against the restarted rear camera it gave two answers:</p>
<div class="table-wrap" data-scrollable data-label="Table: Segment">
  <table>
    <thead>
      <tr><th scope="col">Segment</th><th scope="col"><code>--at</code></th><th scope="col">Correlation</th><th scope="col">Offset</th></tr>
    </thead>
    <tbody>
      <tr><td><code>001</code></td><td>300</td><td>0.74</td><td>+31.9 s</td></tr>
      <tr><td><code>002</code></td><td>600</td><td>0.52</td><td>+32.8 s</td></tr>
    </tbody>
  </table>
</div>
<p>The two windows disagree by 0.9 seconds, and each one looks plausible on its
own. The front camera sees the road ahead and the rear camera sees the road
behind. Their pictures change for different reasons most of the time, so the
best match moves from one window to the next. The Gear Guard camera looks backward over
the bed, the same direction as the rear camera, and the two agree.</p>
<p>Pick a reference camera that kept recording in the original folder and faces
the same direction as one of the restarted cameras. The files in a sibling
folder share one start time and the same segment lengths, so the offset you
measure for one of its cameras applies to all of them.</p>
<h2 id="stitch-the-restarted-cameras">Stitch the restarted cameras<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#stitch-the-restarted-cameras"
     aria-label="Link to section: Stitch the restarted cameras" data-pagefind-ignore></a>
</h2>
<p>A stream copy can only start on a keyframe, and the truck writes one keyframe per
second. The stream-copied angles from the original folder start on their own
keyframes. An offset of 32.7 seconds isn&rsquo;t a whole number of seconds, so no
keyframe in the sibling folder falls on the same instant. Re-encode only the restarted
cameras, cut at the exact frame, and leave the rest as stream copies.</p>
<p>Suppose your clip covers <code>START</code> to <code>END</code> seconds of the original folder, and
<code>OFFSET</code> is the measured offset. The window has to begin after the restart, so
<code>START</code> is greater than <code>OFFSET</code>. In the sibling folder, the clip begins at
<code>START</code> minus <code>OFFSET</code> and lasts <code>END</code> minus <code>START</code> seconds.</p>
<p>In the <code>DriveStream</code> folder, the following commands write a concat list of the
restarted camera&rsquo;s segments in the sibling folder, and then cut the clip from
the joined segments, re-encoding to HEVC. They create two files and change
nothing in the drive folders:</p>
<div class="code-block" data-copyable data-copy-label="Bash">
  <div class="code-block__head"><span class="code-block__lang">Bash</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">printf</span> <span class="s2">&#34;file &#39;%s&#39;\n&#34;</span> <span class="s2">&#34;</span><span class="nv">$PWD</span><span class="s2">&#34;</span>/08_07_26_140034/08_07_26_140034_rearCenter_*.mp4 <span class="se">\
</span></span></span><span class="line"><span class="cl">  &gt; rearCenter.txt
</span></span><span class="line"><span class="cl">ffmpeg -f concat -safe <span class="m">0</span> -i rearCenter.txt -ss &lt;sibling-start&gt; -t &lt;duration&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  -c:v hevc_videotoolbox -q:v <span class="m">80</span> -tag:v hvc1 -color_range pc -an <span class="se">\
</span></span></span><span class="line"><span class="cl">  &lt;clip-folder&gt;/rearCenter.mp4</span></span></code></pre></div></div>
  </div>
</div><p>Run the same two commands for every other camera in the sibling folder.</p>
<p><code>-ss</code> after <code>-i</code> decodes from the start of the list and discards frames up to
the exact time, so the output starts on that instant rather than on the nearest
keyframe. It&rsquo;s slow for a window late in the drive, because ffmpeg decodes
everything before the window first. Joining the segments in one concat pass,
rather than cutting each file and joining the cuts, keeps the frames whose
group of pictures runs across a segment boundary. <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">Cut multi-camera dashcam
clips with ffmpeg</a> covers the
stream-copied cameras and that join in full.</p>
<p>The encoder <code>hevc_videotoolbox</code> is the hardware HEVC encoder on macOS. On
another system, use the encoder you&rsquo;d use for the rest of the project. The
option <code>-color_range pc</code> labels the re-encoded angle full range like the
truck&rsquo;s footage, so it matches the stream-copied ones; check it with
<code>ffprobe -show_entries stream=color_range</code>. The truck
records no audio, so <code>-an</code> drops nothing.</p>
<p>Every file now starts at time 0. The re-encoded angles start on the exact
frame, and a stream-copied one starts early by the lead <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/#build-the-multicam-clip">the cut
script</a>
printed for it. In DaVinci Resolve, mark each lead as an In point, then create
the multicam clip with angle sync set to the In point.</p>
<figure><img src="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/five-cameras-after-stitch_hu_dedc50ea5b067455.jpg" alt="The same moment on the floating bridge after the stitch. All five tiles are filled: the rear tile shows the car following behind, which the Gear Guard tile also shows over the truck bed, and the side-left tile shows the bridge deck and railing." width="1600" height="853" loading="lazy" decoding="async"><figcaption>Rear and side-left from 08_07_26_140034, cut at the measured 32.7 s offset. Plates and the following driver blurred.</figcaption>
</figure>
<h2 id="confirm-the-stitch">Confirm the stitch<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/#confirm-the-stitch"
     aria-label="Link to section: Confirm the stitch" data-pagefind-ignore></a>
</h2>
<p>Run the same script on the finished clip, with the guess left at zero. In the
clip&rsquo;s folder, compare the Gear Guard camera, which was stream-copied from the original
folder, with the re-encoded rear camera:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">measure_offset.py</span> <span class="console__token">08_07_26_140000_gearGuard.mp4</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">08_07_26_140000_rearCenter.mp4</span> <span class="console__token">--at</span> <span class="console__token">300</span> <span class="console__token">--length</span> <span class="console__token">90</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">window</span> <span class="console__token">at</span> <span class="console__token">300</span> <span class="console__token">s:</span> <span class="console__token">correlation</span> <span class="console__token">0.79,</span> <span class="console__token">offset</span> <span class="console__token">+0.1</span> <span class="console__token">s</span></span></code></pre>
    </div>
  </div></div><p>An offset of +0.1 seconds is one sample step of <code>SAMPLES_PER_SECOND</code>, the
resolution of the method. The angles are in sync. The same script reported
32.7 seconds on the unstitched folders, so it does notice an offset when one
exists. A zero from a check that has never reported anything else proves
nothing.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Read the GPS Location from Rivian Dashcam Video with ffprobe</title>
      <link>https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/</guid>
      <pubDate>Mon, 28 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>Python</category>
      
      <description>A Rivian R1T dashcam clip carries one GPS fix, taken where its segment began, in the MP4 location tag, and ffprobe reads it in one command.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>The GPS position is in the MP4 container&rsquo;s <code>location</code> tag. Read it with
ffprobe:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-show_entries</span> <span class="console__token">format_tags=location</span> <span class="console__token">-of</span> <span class="console__token">default=nw=1:nk=1</span> <span class="console__token">\</span>
</span><span class="line">    <span class="console__token">&lt;clip&gt;.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">47.589462-122.2655</span></span></code></pre>
    </div>
  </div></div><p>The value is a signed latitude immediately followed by a signed longitude, in
decimal degrees: <code>47.589462</code> north, <code>-122.2655</code> west. That clip starts on the
I-90 floating bridge. <code>0,0</code> means the truck had no fix.</p>
<p>There is one fix per file, taken where the 15-minute segment began. There is
no per-second GPS track.</p>

</div>

<figure><img src="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/floating-bridge-segment-start_hu_2b5d759a1bdfb1b3.jpg" alt="The front camera half a second into a segment that begins on the I-90 floating bridge under an overcast sky, with two cars ahead and a distant skyline low on the horizon." width="1600" height="1280" loading="lazy" decoding="async"><figcaption>Half a second into the clip. Its tag reads 47.589462-122.2655: 47.589462, -122.2655. Plates blurred.</figcaption>
</figure>
<h2 id="spellings-of-the-tag">Spellings of the tag<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/#spellings-of-the-tag"
     aria-label="Link to section: Spellings of the tag" data-pagefind-ignore></a>
</h2>
<p>The value has no separator between the two numbers. The sign of the longitude
is the separator, so a parser has to split on the second <code>+</code> or <code>-</code>, not on a
comma or a space. The truck never writes a leading plus sign. Across 475
front-camera segments, all from encoder tag Rivian Firmware ver. 2022-47, three
spellings turned up, and <code>0,0</code> stands in when there is no fix:</p>
<dl class="facts">
  <dt>Usual</dt>
  <dd><code>47.589462-122.2655</code>, on 439 of the 475 segments.</dd>
  <dt>Trailing slash</dt>
  <dd>the same form ending in <code>/</code>, on the other 36.</dd>
  <dt>After ffmpeg copies the file</dt>
  <dd><code>+47.5894-122.2655/</code>, the ISO 6709 form, with a leading plus sign and four decimal places. This is ffmpeg&rsquo;s rewrite, not a firmware variant.</dd>
  <dt>No fix</dt>
  <dd><code>0,0</code>, which is a position in the Gulf of Guinea, not a reading.</dd>
</dl>

<p>ffprobe also prints a <code>location-eng</code> tag with the same value. Either one
works; the commands here read <code>location</code>.</p>
<h2 id="what-one-fix-per-segment-rules-out">What one fix per segment rules out<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/#what-one-fix-per-segment-rules-out"
     aria-label="Link to section: What one fix per segment rules out" data-pagefind-ignore></a>
</h2>
<p>The truck records one file per camera per 15-minute segment, and each file
carries a single fix from the moment that segment started. Nothing inside the
file records where the truck was a minute later. That rules out:</p>
<ul>
<li>A route drawn from the video. A segment gives you a point every 15 minutes,
which is too coarse to follow a road.</li>
<li>Speed. Two fixes 15 minutes apart give an average over the whole segment, not
the speed at any moment.</li>
<li>Finding a place by GPS alone. A drive across the floating bridge takes less
than a segment, so the bridge usually falls between two fixes.</li>
</ul>
<p>The five cameras of a segment each carry their own fix. In the clip earlier,
the front and rear files read <code>47.589462-122.2655</code> and the two side cameras and
the Gear Guard camera read <code>47.589474-122.2659</code>. Treat any one of them as the
segment&rsquo;s position.</p>
<p>To place a moment inside a segment, read the picture instead of the
container. <a href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/">Finding a bridge in dashcam video by the color of tunnel
lights</a> locates the I-90
floating bridge crossing from the picture, where the GPS can&rsquo;t.</p>
<h2 id="parse-the-tag-in-python">Parse the tag in Python<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/#parse-the-tag-in-python"
     aria-label="Link to section: Parse the tag in Python" data-pagefind-ignore></a>
</h2>
<p><code>parse_location</code> returns a <code>(latitude, longitude)</code> tuple, or <code>None</code> when the
tag is missing, unreadable, or <code>0,0</code>. <code>read_location</code> runs ffprobe on one
file. The standard library is enough; Python 3.10 or later, for the <code>|</code> in the
type hints.</p>
<div class="code-block" data-copyable data-copy-label="rivian_location.py">
  <div class="code-block__head"><span class="code-block__lang code-block__file">rivian_location.py</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># A signed latitude immediately followed by a signed longitude. The leading</span>
</span></span><span class="line"><span class="cl"><span class="c1"># plus sign and the trailing slash are optional because not every writer</span>
</span></span><span class="line"><span class="cl"><span class="c1"># emits them.</span>
</span></span><span class="line"><span class="cl"><span class="n">LOCATION</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;([+-]?\d+(?:\.\d+)?)([+-]\d+(?:\.\d+)?)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_location</span><span class="p">(</span><span class="n">tag</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">float</span><span class="p">]</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">match</span> <span class="o">=</span> <span class="n">LOCATION</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="k">match</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="n">latitude</span><span class="p">,</span> <span class="n">longitude</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="k">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span> <span class="nb">float</span><span class="p">(</span><span class="k">match</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># The truck writes 0,0 when it has no GPS fix.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">latitude</span> <span class="o">==</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">longitude</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">latitude</span><span class="p">,</span> <span class="n">longitude</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">read_location</span><span class="p">(</span><span class="n">clip</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">float</span><span class="p">]</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">tag</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s2">&#34;ffprobe&#34;</span><span class="p">,</span> <span class="s2">&#34;-v&#34;</span><span class="p">,</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span> <span class="s2">&#34;-show_entries&#34;</span><span class="p">,</span> <span class="s2">&#34;format_tags=location&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">         <span class="s2">&#34;-of&#34;</span><span class="p">,</span> <span class="s2">&#34;default=nw=1:nk=1&#34;</span><span class="p">,</span> <span class="n">clip</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">check</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">stdout</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">parse_location</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">clip</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span>
</span></span><span class="line"><span class="cl">        <span class="n">point</span> <span class="o">=</span> <span class="n">read_location</span><span class="p">(</span><span class="n">clip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">clip</span><span class="p">,</span> <span class="s2">&#34;no fix&#34;</span> <span class="k">if</span> <span class="n">point</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">point</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">point</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
  </div>
</div><p>Pass it one or more clips:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">rivian_location.py</span> <span class="console__token">&lt;clip&gt;.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">47.589462,</span> <span class="console__token">-122.2655</span></span></code></pre>
    </div>
  </div></div><p>The tests cover every spelling in the list earlier, a southern and eastern
position so both signs are exercised, and each way the tag can mean &ldquo;no fix&rdquo;:</p>
<div class="code-block" data-copyable data-copy-label="test_rivian_location.py">
  <div class="code-block__head"><span class="code-block__lang code-block__file">test_rivian_location.py</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">rivian_location</span> <span class="kn">import</span> <span class="n">parse_location</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">((</span><span class="s2">&#34;tag&#34;</span><span class="p">,</span> <span class="s2">&#34;expected&#34;</span><span class="p">),</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;47.589462-122.2655&#34;</span><span class="p">,</span> <span class="p">(</span><span class="mf">47.589462</span><span class="p">,</span> <span class="o">-</span><span class="mf">122.2655</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;47.589462-122.2655/&#34;</span><span class="p">,</span> <span class="p">(</span><span class="mf">47.589462</span><span class="p">,</span> <span class="o">-</span><span class="mf">122.2655</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;+47.5894-122.2655/&#34;</span><span class="p">,</span> <span class="p">(</span><span class="mf">47.5894</span><span class="p">,</span> <span class="o">-</span><span class="mf">122.2655</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="s2">&#34;-33.8688+151.2093/&#34;</span><span class="p">,</span> <span class="p">(</span><span class="o">-</span><span class="mf">33.8688</span><span class="p">,</span> <span class="mf">151.2093</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl"><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_reads_each_spelling_of_the_tag</span><span class="p">(</span><span class="n">tag</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">parse_location</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span> <span class="o">==</span> <span class="n">expected</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span><span class="s2">&#34;tag&#34;</span><span class="p">,</span> <span class="p">[</span><span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;garbage&#34;</span><span class="p">,</span> <span class="s2">&#34;+0.0000+0.0000/&#34;</span><span class="p">,</span> <span class="s2">&#34;0-0&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_returns_none_for_no_fix</span><span class="p">(</span><span class="n">tag</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">parse_location</span><span class="p">(</span><span class="n">tag</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">None</span></span></span></code></pre></div></div>
  </div>
</div><div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">python3</span> <span class="console__token">-m</span> <span class="console__token">pytest</span> <span class="console__token">-q</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">.........</span>                                                                <span class="console__token">[100%]</span>
</span><span class="line"><span class="console__token">9</span> <span class="console__token">passed</span> <span class="console__token">in</span> <span class="console__token">0.01s</span></span></code></pre>
    </div>
  </div></div><p>Dropping the optional sign from the latitude&rsquo;s half of the pattern fails the
southern-hemisphere case, so the tests do notice a broken pattern.</p>
<h2 id="remove-the-gps-location-from-a-clip-before-sharing-it">Remove the GPS location from a clip before sharing it<a class="heading-anchor" href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/#remove-the-gps-location-from-a-clip-before-sharing-it"
     aria-label="Link to section: Remove the GPS location from a clip before sharing it" data-pagefind-ignore></a>
</h2>
<p>The tag travels with the file. A raw clip posted online tells anyone who
downloads it where it was recorded, and a folder of them maps where the truck
goes every day. Trimming the clip doesn&rsquo;t necessarily remove it: a stream copy
with ffmpeg kept the tag, rewritten in the <code>+…/</code> form, and exiftool reports it
as a GPS position:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-i</span> <span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">-t</span> <span class="console__token">2</span> <span class="console__token">-c</span> <span class="console__token">copy</span> <span class="console__token">copy.mp4</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">exiftool</span> <span class="console__token">-G1</span> <span class="console__token">-a</span> <span class="console__token">&#34;-gps*&#34;</span> <span class="console__token">&#34;-location*&#34;</span> <span class="console__token">copy.mp4</span></span></code></pre>
    </div>
  </div><div class="code-block code-block--wrap console__output">
    <p class="console__output-label">Output</p>
    <div class="code-block__scroll"><pre><code><span class="line"><span class="console__token">[Composite]</span>     <span class="console__token">GPS</span> <span class="console__token">Altitude</span>                    <span class="console__token">:</span> <span class="console__token">0</span> <span class="console__token">m</span>
</span><span class="line"><span class="console__token">[Composite]</span>     <span class="console__token">GPS</span> <span class="console__token">Altitude</span> <span class="console__token">Ref</span>                <span class="console__token">:</span> <span class="console__token">Above</span> <span class="console__token">Sea</span> <span class="console__token">Level</span>
</span><span class="line"><span class="console__token">[Composite]</span>     <span class="console__token">GPS</span> <span class="console__token">Latitude</span>                    <span class="console__token">:</span> <span class="console__token">47</span> <span class="console__token">deg</span> <span class="console__token">35&#39;</span> <span class="console__token">22.02&#34;</span> <span class="console__token">N</span>
</span><span class="line"><span class="console__token">[Composite]</span>     <span class="console__token">GPS</span> <span class="console__token">Longitude</span>                   <span class="console__token">:</span> <span class="console__token">122</span> <span class="console__token">deg</span> <span class="console__token">15&#39;</span> <span class="console__token">55.76&#34;</span> <span class="console__token">W</span>
</span><span class="line"><span class="console__token">[Composite]</span>     <span class="console__token">GPS</span> <span class="console__token">Position</span>                    <span class="console__token">:</span> <span class="console__token">47</span> <span class="console__token">deg</span> <span class="console__token">35&#39;</span> <span class="console__token">22.02&#34;</span> <span class="console__token">N,</span> <span class="console__token">122</span> <span class="console__token">deg</span> <span class="console__token">15&#39;</span> <span class="console__token">55.76&#34;</span> <span class="console__token">W</span>
</span><span class="line"><span class="console__token">[UserData]</span>      <span class="console__token">Location</span> <span class="console__token">Information</span>            <span class="console__token">:</span> <span class="console__token">(none)</span> <span class="console__token">Role=shooting</span> <span class="console__token">Lat=47.58945</span> <span class="console__token">Lon=-122.26549</span> <span class="console__token">Alt=0.00</span> <span class="console__token">Body=earth</span> <span class="console__token">Notes=</span></span></code></pre>
    </div>
  </div></div><p>Other ways of writing a file behave differently. The <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">multi-camera clips cut
with ffmpeg&rsquo;s concat demuxer</a>
came out with no <code>location</code> tag at all, because the demuxer drops file-level
metadata. The hyperlapse tool I used kept it, because it copies the
first source file&rsquo;s metadata on purpose. Check
each file rather than trusting the tool. To drop the container&rsquo;s metadata while copying,
add <code>-map_metadata -1</code>; the same exiftool command then prints nothing:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffmpeg</span> <span class="console__token">-i</span> <span class="console__token">&lt;clip&gt;.mp4</span> <span class="console__token">-t</span> <span class="console__token">2</span> <span class="console__token">-c</span> <span class="console__token">copy</span> <span class="console__token">-map_metadata</span> <span class="console__token">-1</span> <span class="console__token">strip.mp4</span>
</span><span class="line"><span class="prompt">$ </span><span class="console__token">exiftool</span> <span class="console__token">-G1</span> <span class="console__token">-a</span> <span class="console__token">&#34;-gps*&#34;</span> <span class="console__token">&#34;-location*&#34;</span> <span class="console__token">strip.mp4</span></span></code></pre>
    </div>
  </div></div><p>Empty output is the result you want. Run the check on the file you&rsquo;re about
to upload, not on its source: each tool between the two can keep the tag or
drop it.</p>
<div class="tested" id="tested">
  <p class="label label--seam">Tested with</p><dl class="tested__rows">
    <div class="tested__row"><dt>ffprobe</dt> <dd>9.0.1</dd></div>
    <div class="tested__row"><dt>exiftool</dt> <dd>13.55</dd></div>
    <div class="tested__row"><dt>Python</dt> <dd>3.12.11</dd></div>
    <div class="tested__row"><dt>pytest</dt> <dd>9.1.1</dd></div>
    <div class="tested__row"><dt>OS</dt> <dd>macOS 27.0</dd></div>
    <div class="tested__row"><dt>Footage</dt> <dd>Rivian, encoder tag Rivian Firmware ver. 2022-47</dd></div>
    <div class="tested__row"><dt>Date</dt> <dd>October 2026</dd></div>
  </dl>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>How to Sort Months of Dashcam Footage with Claude Code</title>
      <link>https://looselydecoupled.com/triage-dashcam-footage-claude-code/</link>
      <guid isPermaLink="true">https://looselydecoupled.com/triage-dashcam-footage-claude-code/</guid>
      <pubDate>Sun, 27 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Emmanuel Tsouris</dc:creator>
      <category>Automation</category>
      <category>Python</category>
      <category>macOS</category>
      
      <description>Five copy-paste Claude Code prompts sort months of Rivian R1T dashcam footage by GPS and keyframes, each with the check that caught a real failure.</description>
      <content:encoded><![CDATA[<div class="answer" data-answer="true">
  <p class="answer__label" data-pagefind-ignore>TL;DR</p>
  <p>Give Claude Code one prompt per stage: survey, move, find, cut, publish. Each
prompt states what the agent can&rsquo;t touch and carries three demands: run on
one drive first, break your own check on purpose and confirm that it fails, and
list what you couldn&rsquo;t verify. The agent reads the GPS tag and the keyframes instead
of the pixels and proposes candidates. You confirm every pick on a contact sheet
before anything is cut, moved, or deleted.</p>

</div>

<p>Driving from July to September filled the truck&rsquo;s 2 TB dashcam drive, a
<a href="https://www.amazon.com/dp/B08HN37XC1?tag=loosely-decoupled-20" rel="external noopener sponsored" target="_blank">SanDisk Extreme Portable SSD<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#i-external-link"/></svg><span class="visually-hidden"> (opens in a new tab)</span></a>
(affiliate link): 240 drive folders,
1,958 GB, five cameras recording on every trip. I wanted to keep only what might
make a video, and the backup drive couldn&rsquo;t hold all of it. Watching it wasn&rsquo;t an
option. I directed Claude Code in plain language instead. It sorted the drives
by where they went using GPS, found the I-90 floating bridge by the color of the tunnel
lights, cut synced clips, and rendered a hyperlapse of every drive.</p>
<figure><img src="https://looselydecoupled.com/triage-dashcam-footage-claude-code/five-cameras-floating-bridge_hu_c7373bb90be2c6e0.jpg" alt="Five fisheye views from the same second on the I-90 floating bridge: front, rear, both sides and the Gear Guard camera over the truck bed. A red car passing behind appears in three views at once." width="1600" height="853" loading="lazy" decoding="async"><figcaption>The five cameras at one second of a crossing.</figcaption>
</figure>
<p>Almost every failure in that work was caught by a check: a size comparison, a
frame count, a positive control, a contact sheet, or me. One failure was caught by
nothing for hours. The prompts in this guide are general versions of the ones I
typed, rewritten for your own footage, and each one carries the guardrail that
the failure taught.</p>
<h2 id="signals-already-in-the-files">Signals already in the files<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#signals-already-in-the-files"
     aria-label="Link to section: Signals already in the files" data-pagefind-ignore></a>
</h2>
<p>A Rivian writes one folder per drive and one MP4 per camera per 15-minute segment.
Two things in those files do most of the work, and neither needs anyone to watch
the video:</p>
<ul>
<li><strong>GPS.</strong> The container&rsquo;s <code>location</code> tag holds one fix per segment, taken at its
start. That&rsquo;s the only GPS in the file; there&rsquo;s no per-second track.</li>
<li><strong>Keyframes.</strong> Every camera writes one keyframe per second. Decoding only the
keyframes, downscaled to 64 × 48, gives a per-second record of motion and color
without decoding the frames between them.</li>
</ul>
<p>Read the tag from any clip:</p>
<div class="console"><div class="code-block code-block--wrap console__command" data-copyable data-copy-label="command">
    <div class="code-block__head"><span class="code-block__lang">Command</span></div>
    <div class="code-block__scroll"><pre data-copy-source><code><span class="line"><span class="prompt">$ </span><span class="console__token">ffprobe</span> <span class="console__token">-v</span> <span class="console__token">error</span> <span class="console__token">-show_entries</span> <span class="console__token">format_tags=location</span> <span class="console__token">-of</span> <span class="console__token">default=nw=1:nk=1</span> <span class="console__token">&lt;clip&gt;.mp4</span></span></code></pre>
    </div>
  </div></div><p>It prints a signed latitude immediately followed by a signed longitude, such as
<code>47.589462-122.2655</code>. Some tags end in <code>/</code> and most don&rsquo;t, and <code>0,0</code> means the truck
had no fix. <a href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/">Read the GPS location from Rivian dashcam video with
ffprobe</a> covers every spelling. Other
dashcams store GPS differently, which is why the first prompt asks the agent to
find out rather than assume.</p>
<h2 id="tools-and-rules-for-every-stage">Tools and rules for every stage<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#tools-and-rules-for-every-stage"
     aria-label="Link to section: Tools and rules for every stage" data-pagefind-ignore></a>
</h2>
<p>You need:</p>
<ul>
<li>Claude Code, run in a terminal on the machine that has the footage mounted</li>
<li>The <code>ffmpeg</code> and <code>ffprobe</code> commands on the <code>PATH</code></li>
<li>The <code>exiftool</code> command, for the privacy stage</li>
<li>A second drive with room for whatever you keep</li>
</ul>
<p>Every prompt carries three rules, worded for its stage. They&rsquo;re repeated on
purpose, so that a prompt pasted on its own still carries them:</p>
<ul>
<li><strong>One drive first.</strong> A bug found on the first drive costs one drive&rsquo;s time. The
same bug found late in a batch costs every drive before it.</li>
<li><strong>Break the check on purpose.</strong> A check that has never failed proves nothing.
The agent damages its own input or output, confirms the check catches it, and
then restores it.</li>
<li><strong>Say what couldn&rsquo;t be verified.</strong> Matching file sizes aren&rsquo;t matching content,
and a report that doesn&rsquo;t say so reads as if they were.</li>
</ul>
<!-- style-check: procedure -->
<h2 id="survey-every-drive-without-changing-anything">Survey every drive without changing anything<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#survey-every-drive-without-changing-anything"
     aria-label="Link to section: Survey every drive without changing anything" data-pagefind-ignore></a>
</h2>
<p>In Claude Code, from the folder that holds the drive folders, paste:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">The folder &lt;footage-folder&gt; holds video from my truck&#39;s dashcam: one subfolder per drive, and one MP4 per camera per segment. Survey it without changing anything.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For every drive folder, use ffprobe to read each segment&#39;s duration and GPS location tag. Report each drive&#39;s start time, length, GPS points, the distance between its two farthest points, and its size on disk. Ignore any ._ files macOS has left beside the clips on a FAT32 or exFAT drive.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl">- Read only. Do not move, rename, re-encode, or delete anything.
</span></span><span class="line"><span class="cl">- Run on one drive first and show me the result before you run the rest.
</span></span><span class="line"><span class="cl">- The location tag is written in more than one format. Parse it permissively, and treat 0,0 as no fix, not as a place.
</span></span><span class="line"><span class="cl">- Before you run the whole batch, feed your parser a malformed tag on purpose and confirm that it reports the drive instead of skipping it silently.
</span></span><span class="line"><span class="cl">- Cache everything you read in one file, so that later steps don&#39;t read the drives again.
</span></span><span class="line"><span class="cl">- Then propose a way to sort the drives into routine and unusual, and wait. I&#39;ll tell you my regular routes.
</span></span><span class="line"><span class="cl">- Finish with a list of the drives you couldn&#39;t read and anything you couldn&#39;t verify.</span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p><strong>What went wrong here.</strong> I asked for road trips, and the agent ranked drives by
distance. The ranking itself showed there were none: no drive in the batch went
far. Distance was the wrong question, and only I could supply the right one: the
corridor across the I-90 bridges that most of the drives used. With that, the
classification became a corridor test. A drive whose every GPS point falls
inside a box around that corridor is a corridor drive; anything else is worth a
look. The test found 168 corridor drives, 1,594 GB, and 14 off the corridor
worth keeping,
171 GB. That&rsquo;s why the prompt ends by
waiting for you. Your routes are context the agent can&rsquo;t get from the files.</p>
<p>The parser also failed on its first real run, on a tag that ended in <code>/</code>. The
fix was a pattern that finds two signed numbers wherever they sit.
Expect the same from your own camera&rsquo;s metadata, and test both spellings.</p>
<h2 id="move-the-drives-you-keep">Move the drives you keep<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#move-the-drives-you-keep"
     aria-label="Link to section: Move the drives you keep" data-pagefind-ignore></a>
</h2>
<p>Decide which drives to keep from the survey, then paste:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Move these drives from &lt;footage-folder&gt; to &lt;backup-drive&gt;/&lt;kept-folder&gt;: &lt;list of drive folders&gt;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl">- Before you copy anything, compare the total size with the free space on &lt;backup-drive&gt;, and stop if it won&#39;t fit.
</span></span><span class="line"><span class="cl">- Copy first. After each drive, compare every file&#39;s size on the destination with the source.
</span></span><span class="line"><span class="cl">- Do not delete anything from the source until I say so. Show me the drives whose sizes all match, and wait.
</span></span><span class="line"><span class="cl">- When I confirm, remove the source files with a tool that removes only what it has copied, such as rsync --remove-source-files. Do not rely on shell-specific glob features.
</span></span><span class="line"><span class="cl">- Skip any folder that another process, such as Finder, is still writing to or moving.
</span></span><span class="line"><span class="cl">- Report the empty folders that a move leaves behind. Do not delete them, and make every later step skip them.
</span></span><span class="line"><span class="cl">- Move one drive first and show me the result.
</span></span><span class="line"><span class="cl">- Before the batch, truncate a copy in a scratch folder and confirm your size check reports it.
</span></span><span class="line"><span class="cl">- Say plainly that a size match isn&#39;t a content match, and list anything you couldn&#39;t verify.</span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<div class="admonition admonition--warning" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#i-triangle-alert"/></svg>
    <span>Warning</span>
  </p>
  <p>Do not let an agent delete source footage in the same step that copies it. Copy,
verify on the destination, and approve the removal yourself. A move that copies
and then fails to delete costs disk space; a move that deletes and then fails to
copy costs the footage.</p>
</div><p><strong>What went wrong here.</strong> A queued move used a <code>zsh</code> glob qualifier that the
shell Claude Code runs commands in doesn&rsquo;t enable. It would have copied every file
and deleted none. Inspecting the running process caught it, and the move switched
to <code>rsync --remove-source-files</code>, which removes only what it copied. Later, a
cleanup command of the form <code>rm -f $B/*.jpg</code> was blocked by Claude Code&rsquo;s own
guard, because an unset <code>$B</code> would expand the path to <code>/*</code>. The cleanup wasn&rsquo;t
needed, and the guard was right to stop it.</p>
<p>The moves also left empty drive folders behind. Nothing failed because of them
until the hyperlapse run, in a later stage, which is why this prompt asks for
them to be reported now.</p>
<h2 id="find-a-landmark-from-computed-signals">Find a landmark from computed signals<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#find-a-landmark-from-computed-signals"
     aria-label="Link to section: Find a landmark from computed signals" data-pagefind-ignore></a>
</h2>
<p>GPS places a drive to within a 15-minute segment, which is too coarse to find a
bridge crossing. The picture has to do it, and the cheap way to read the picture
is to compute a few numbers per second once and reuse them. Paste:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">I want the moment in each drive where the truck passes &lt;landmark&gt;. The GPS has one point per segment, which is too coarse, so find it from the picture.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. From keyframes only, downscaled to 64x48, compute per-second signals for each camera: the mean change in green from the previous keyframe, and the mean red, green, and blue. Cache them per drive and per camera, so that no later step decodes video again.
</span></span><span class="line"><span class="cl">2. Propose a signal that marks &lt;landmark&gt;, and explain why it should separate the landmark from things that look like it.
</span></span><span class="line"><span class="cl">3. Propose candidates. Do not pick for me.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl">- Read only on &lt;footage-folder&gt;. Write only under &lt;work-folder&gt;.
</span></span><span class="line"><span class="cl">- When you extract keyframes as raw frames, make sure ffmpeg doesn&#39;t duplicate them back up to the source frame rate.
</span></span><span class="line"><span class="cl">- Before you scan everything, calibrate the signal on a drive where I know the landmark is present and a drive where I know it isn&#39;t. If both read the same, suspect your extraction before the signal.
</span></span><span class="line"><span class="cl">- Run on one drive first.
</span></span><span class="line"><span class="cl">- Build a contact sheet with one row per drive, labeled with the drive&#39;s name, and one frame per candidate in time order. Write a picks file with your best guess for each drive, which I can edit. Then wait for me.
</span></span><span class="line"><span class="cl">- Ignore the first and last few minutes of each drive, where the truck is parking or leaving.
</span></span><span class="line"><span class="cl">- If a result contradicts something already confirmed, check your tool before you report the contradiction: extract the same frame two ways and compare them.
</span></span><span class="line"><span class="cl">- List the drives with no candidate, and anything you couldn&#39;t verify.</span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p>For a tunnel, the signal turned out to be color. Auto-exposure keeps a tunnel&rsquo;s
brightness close to daylight&rsquo;s, but sodium lighting tips the front camera&rsquo;s red/blue
ratio above 1.0. A run of 25–150 s above that ratio, away from the first and last
3 minutes of the drive, found a tunnel on 71 of the 79 bridge drives
scanned. Seven of the eight misses were after dark. <a href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/">Finding a bridge in
dashcam video by the color of tunnel
lights</a> is the full account,
including what didn&rsquo;t work.</p>
<figure><img src="https://looselydecoupled.com/triage-dashcam-footage-claude-code/bridge-daylight-vs-tunnel-sodium_hu_addb249c3b37c46e.jpg" alt="Two front-camera frames side by side. On the floating bridge in daylight the picture is blue-gray, with a red/blue ratio of 0.81. Inside the Mount Baker tunnel it is orange-brown under the sodium lamps, with a ratio of 1.37, and the road and cars are still well exposed." width="1600" height="640" loading="lazy" decoding="async"><figcaption>Mean red over mean blue for each whole frame, from the same drive. Plates blurred.</figcaption>
</figure>
<p><strong>What went wrong here.</strong> The first calibration read zero change between every
pair of keyframes, parked or moving. A signal that&rsquo;s flat on a known positive and
a known negative is a broken instrument, not a finding. The cause was ffmpeg
duplicating keyframes back up to 30 fps, so consecutive frames were identical.
<code>-fps_mode passthrough</code> fixed it. That bug is why the prompt asks for a known
positive and a known negative before the scan.</p>
<p>The automatic pick, one tunnel chosen by its position in the drive, was right
about two times in three. The rest landed on other tunnels. The contact sheet is
what turned an unreliable detector into a usable one. It showed one frame per
candidate, taken from the bridge side of the tunnel. From it, I confirmed 68
crossings, couldn&rsquo;t confirm two night drives, and found none on one. The first sheet was a flat grid
of every candidate frame, and it was easy to match a frame to the wrong drive.
One row per drive makes that mistake visible.</p>
<figure><img src="https://looselydecoupled.com/triage-dashcam-footage-claude-code/review-sheet-one-row-per-drive_hu_bceefb337d7a67ac.jpg" alt="Three rows of a review contact sheet, one drive per row, each tile a frame from the bridge side of one tunnel candidate. Each row has a floating-bridge tile among freeway tiles, so the right pick stands out at a glance. The first row’s last three tiles are replaced by gray blocks." width="1600" height="768" loading="lazy" decoding="async"><figcaption>Three rows of a review sheet. Some frames are removed.</figcaption>
</figure>
<p>The worst wrong claim of the whole session came at this stage. Partway through,
the agent reported that the picks for some drives were the wrong tunnel, the Mercer
Island lid instead of the Mount Baker tunnel. The contact sheets had been built
with an output-side <code>-ss</code>, which ffmpeg applied after tiling, so the sheets showed
a different window from the one the picks named. Extracting one frame two ways
exposed it. The picks had been right, and the claim was retracted.</p>
<h2 id="cut-synced-clips-and-render-hyperlapses">Cut synced clips and render hyperlapses<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#cut-synced-clips-and-render-hyperlapses"
     aria-label="Link to section: Cut synced clips and render hyperlapses" data-pagefind-ignore></a>
</h2>
<p>With picks confirmed, paste:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">For each confirmed pick in &lt;picks-file&gt;, cut a clip from every camera, from &lt;seconds-before&gt; seconds before the landmark to &lt;seconds-after&gt; seconds after it, without re-encoding. Then render a 30x hyperlapse of every drive for every camera, decoding keyframes only.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl">- Read only on &lt;footage-folder&gt;. Write only under &lt;work-folder&gt;.
</span></span><span class="line"><span class="cl">- A stream copy can start only on a keyframe. Measure each camera&#39;s keyframe timing first; do not assume the cameras share it.
</span></span><span class="line"><span class="cl">- A camera&#39;s first frames in a segment file can depend on frames at the end of the previous file. Cut across segment files in one pass, and check the joins.
</span></span><span class="line"><span class="cl">- After cutting, compare every camera&#39;s frame count, duration, and start_time. Same length isn&#39;t the same as in sync.
</span></span><span class="line"><span class="cl">- Pad a camera that stopped early with black frames, per segment, so that every camera stays the same length as the front camera. Give the black the footage&#39;s pixel format and color range, and check the range of every joined file.
</span></span><span class="line"><span class="cl">- Do not trust folder names or file timestamps for timing. If a camera restarted into another folder, measure its offset from cameras that see the same scene, in more than one window. If that offset doesn&#39;t land on one of the camera&#39;s keyframes, re-encode only that camera, cut at the exact frame, and tell me which files were re-encoded.
</span></span><span class="line"><span class="cl">- Test on one drive first, and include a drive where a camera stopped early.
</span></span><span class="line"><span class="cl">- Before the batch, shift one camera by a second on purpose and confirm that your sync check fails. Then restore it.
</span></span><span class="line"><span class="cl">- In the batch, one bad drive must be logged and skipped, not stop the run. Write each output to a temporary name and rename it when it&#39;s complete, so that a rerun resumes.
</span></span><span class="line"><span class="cl">- Time the test drive and estimate the batch from that.
</span></span><span class="line"><span class="cl">- If the run will outlive this session, start a watcher that tells me when the run ends or stops making progress.
</span></span><span class="line"><span class="cl">- List the drives that were skipped or padded, and anything you couldn&#39;t verify.</span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p>Each keyframe becomes one frame at 30 fps, so a hyperlapse plays at 30× speed. A
36-minute drive became 72 s and took 4.4 minutes to render. The hyperlapses of
218 drives, five cameras each, came to 107 GB. <a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">Cut multi-camera dashcam clips
with ffmpeg</a> and <a href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/">make a
hyperlapse from keyframes with ffmpeg</a> cover each
step in full.</p>
<p><strong>What went wrong here.</strong> On the first drive I cut, the side-left camera came out
short. On other drives it&rsquo;s a different camera, or none, so the prompt measures
every camera. The frame-count comparison caught it twice:</p>
<ol>
<li>Its GOPs run across segment files. Cutting each segment separately and joining
them dropped the frames that reference the previous file: the camera came out
1.9 s short, with a jump at the join. One concat-demuxer pass over the source
segments keeps them.</li>
<li>It was still 29 frames short, because its keyframes fall at n + 0.967 s, not on
whole seconds like the other cameras&rsquo;. Landing each camera&rsquo;s in-point exactly
on its own keyframe makes every file start at time 0. An offset camera
starts a few frames early, and setting that clip&rsquo;s In point that many frames
in lines it up with the others.</li>
</ol>
<figure><img src="https://looselydecoupled.com/triage-dashcam-footage-claude-code/synced-clip-five-cameras_hu_321fa648b5047a9c.jpg" alt="The five cameras of a finished synced clip at the same second on the floating bridge. A container truck passing on the left fills the front and side-left views, and the pickup behind appears in the rear and Gear Guard views." width="1600" height="853" loading="lazy" decoding="async"><figcaption>One frame from each camera&rsquo;s cut file, all at the same timestamp. Plates blurred.</figcaption>
</figure>
<p>A third check, on <code>start_time</code>, caught a version that had the right length but
started 0.967 s late, which an editor might ignore.</p>
<p>The hyperlapse test drive caught two more. Two cameras came out at 1,303 frames
against the front camera&rsquo;s 2,170. The first guess was the black filler&rsquo;s pixel
format; the real cause was the rear camera stopping 33 s into a segment. The fix
is to pad every segment to the front camera&rsquo;s length. The filler has a trap of
its own: it has to be full range like the footage, which takes <code>-color_range pc</code>;
Apple&rsquo;s hardware encoder ignores <code>-pix_fmt yuvj420p</code>. A mismatched part doesn&rsquo;t make the stream-copy join fail.
The joined file takes the first part&rsquo;s label, so a camera whose file opens on
filler was labeled limited range throughout: 11 of more than a thousand hyperlapse files, found
and re-rendered later. Across 233 drive folders, six hyperlapse sets needed
black-filled segments.</p>
<p>On that same drive, the rear and side-left cameras had restarted into a sibling
folder whose name was 34 s later. The name was wrong by more than a second.
Correlating motion between the Gear Guard camera and the restarted rear camera,
which both face backward, measured 32.7 s ± 0.1 s across four independent
windows. Front against rear disagreed by 0.9 s between windows, because those two
cameras don&rsquo;t see the same scene. <a href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/">Rivian dashcam camera missing from a
drive</a> covers the restart.</p>
<p>Then the batch failed, and no check caught it. Background jobs in the session
stopped at a time limit, so the run was relaunched detached. A detached process
sends no notice when it ends. It crashed at 10:11 on one of the empty folders the
moves had left behind, and it sat dead until 16:48, when I asked what was next:
more than six and a half hours. Two
fixes came out of it, and both are in the prompt: an empty or bad drive is now a
logged skip, and a long run gets a watcher.</p>
<h2 id="keep-location-out-of-anything-you-publish">Keep location out of anything you publish<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#keep-location-out-of-anything-you-publish"
     aria-label="Link to section: Keep location out of anything you publish" data-pagefind-ignore></a>
</h2>
<p>Paste, for each file you mean to share:</p>
<!-- style-check: off -->
<div class="code-block code-block--wrap" data-copyable data-copy-label="Text">
  <div class="code-block__head"><span class="code-block__lang">Text</span>
  </div>
  <div class="code-block__scroll">
    <div data-copy-source><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Prepare &lt;export-file&gt; for publishing.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Rules:
</span></span><span class="line"><span class="cl">- Treat GPS in any clip as private. List every metadata field that holds a location, with: exiftool -G1 -a &#34;-gps*&#34; &#34;-location*&#34; &lt;file&gt;
</span></span><span class="line"><span class="cl">- First run that command on an original clip from the truck and confirm that it finds the location. Then run it on the export.
</span></span><span class="line"><span class="cl">- If the export carries a location, write a copy without it, run the same command again, and show me the empty result.
</span></span><span class="line"><span class="cl">- List the first and last frames of every clip, and flag any frame from either end of a drive, any license plate, and any face. Do not decide for me what to blur or cut.
</span></span><span class="line"><span class="cl">- Also list creation dates, and any drive date or time in file names. Strip them with -map_metadata -1 and rename the files, unless I say otherwise.
</span></span><span class="line"><span class="cl">- List what this check can&#39;t see, such as a street sign or a house number in the picture.</span></span></code></pre></div></div>
  </div>
</div><!-- style-check: on -->
<p>Run the check on every export rather than reasoning about which files carry a
tag. In this work the answer differed by tool. The raw clips carry it. The
hyperlapse tool I used copies the first source file&rsquo;s metadata, GPS included,
so the set stays traceable; the <a href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/">published hyperlapse
script</a> strips it. Clips cut with the concat demuxer came
out without it, but a plain <code>-c copy</code> of a single file keeps it, and
<code>-map_metadata -1</code> removes it. Show only
public landmarks on screen, and never frames from either end of a drive.</p>
<figure><img src="https://looselydecoupled.com/triage-dashcam-footage-claude-code/interchange-curve-sunset_hu_6d94b69d2acd59a0.jpg" alt="A side-camera fisheye frame on the I-5/I-90 interchange curve at sunset: the low sun flares at the left edge, and the downtown Seattle skyline stands above the ramps and trees." width="1600" height="1280" loading="lazy" decoding="async"><figcaption>A public landmark mid-drive, raw fisheye, which keeps the sun in frame.</figcaption>
</figure>
<div class="admonition admonition--caution" role="note">
  <p class="admonition__label">
    <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#i-octagon-alert"/></svg>
    <span>Caution</span>
  </p>
  <p>A clip from the truck&rsquo;s drive carries a GPS fix for the start of every segment, and
the first segment of a drive usually starts at home. Run the <code>exiftool</code> check on
every file before it leaves your machine.</p>
</div><!-- style-check: end procedure -->
<h2 id="detectors-that-didnt-work">Detectors that didn&rsquo;t work<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#detectors-that-didnt-work"
     aria-label="Link to section: Detectors that didn’t work" data-pagefind-ignore></a>
</h2>
<p>Four ideas failed, and each failure says something about the signal rather than
the threshold:</p>
<ul>
<li><strong>Parked footage.</strong> The hypothesis was that recording while parked padded every
drive. The measurement found 23 GB of 1,760 GB: with Gear Guard turned off, the
truck stops recording seconds after parking. The scan still paid for itself, because its per-second
signals fed every detector after it.</li>
<li><strong>Hard stops.</strong> The detector returned 875 candidates, and all 12 at the top
were turns into parking lots or ordinary stops. Turning changes the picture as
much as braking does.</li>
<li><strong>Traffic jams.</strong> Crawling traffic still changes the picture, because the
neighboring cars move.</li>
<li><strong>Garage boundaries by color.</strong> An overcast sky is as gray as a concrete
ceiling. It worked on a clear day and failed on a cloudy one, which is the case
to test first.</li>
</ul>
<p>One detector worked with a person in the loop. Close passes, read as a
one-sided spike on a side camera while the front camera shows steady driving,
were swamped by overpass shadows until the frame&rsquo;s overall brightness change was
subtracted. Of the top 60, 37 were real passes, and a contact sheet sorted them.
A human pass was cheaper than a better detector.</p>
<h2 id="failures-and-the-checks-that-caught-them">Failures and the checks that caught them<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#failures-and-the-checks-that-caught-them"
     aria-label="Link to section: Failures and the checks that caught them" data-pagefind-ignore></a>
</h2>
<div class="table-wrap" data-scrollable data-label="Table: Failure">
  <table>
    <thead>
      <tr><th scope="col">Failure</th><th scope="col">Caught by</th></tr>
    </thead>
    <tbody>
      <tr><td>No road trips in the footage</td><td>The distance ranking itself</td></tr>
      <tr><td>GPS parser failed on a tag ending in <code>/</code></td><td>An exception on the first run</td></tr>
      <tr><td>Move would copy but never delete</td><td>Inspecting the running process</td></tr>
      <tr><td><code>rm</code> with a possibly unset variable</td><td>Claude Code&rsquo;s command guard</td></tr>
      <tr><td>Every frame difference read zero</td><td>A known parked and a known moving segment</td></tr>
      <tr><td>Automatic tunnel pick wrong about a third of the time</td><td>A one-row-per-drive contact sheet</td></tr>
      <tr><td>Claim that the picks were the wrong tunnel</td><td>Extracting one frame two ways</td></tr>
      <tr><td>One camera short in a synced cut, 1.9 s and then 29 frames</td><td>Frame counts across cameras</td></tr>
      <tr><td>The same camera starting 0.967 s late</td><td>Inspecting <code>start_time</code></td></tr>
      <tr><td>Hyperlapse cameras at 1,303 frames, not 2,170</td><td>Frame counts on a test drive with a stopped camera</td></tr>
      <tr><td>Folder name said 34 s; the offset was 32.7 s</td><td>Motion correlation in four windows</td></tr>
      <tr><td>Detached run dead for more than six and a half hours</td><td>Nothing, until I asked; now a watcher</td></tr>
      <tr><td>Hard stops, jams, and garage color</td><td>A contact sheet, a count, and an overcast day</td></tr>
    </tbody>
  </table>
</div>
<h2 id="each-stage-in-depth">Each stage in depth<a class="heading-anchor" href="https://looselydecoupled.com/triage-dashcam-footage-claude-code/#each-stage-in-depth"
     aria-label="Link to section: Each stage in depth" data-pagefind-ignore></a>
</h2>
<p>Each later part of this series takes one stage and stands on its own:</p>
<ol start="2">
<li><a href="https://looselydecoupled.com/rivian-dashcam-gps-location-ffprobe/">Read the GPS location from Rivian dashcam video with
ffprobe</a>: the tag&rsquo;s spellings, a
parser, and how to strip it.</li>
<li><a href="https://looselydecoupled.com/rivian-dashcam-camera-missing-sibling-folder/">Rivian dashcam camera missing from a drive: it restarted in another
folder</a>: finding the sibling
folder and measuring its real offset.</li>
<li><a href="https://looselydecoupled.com/ffmpeg-cut-multi-camera-dashcam-clips-in-sync/">Cut multi-camera dashcam clips with ffmpeg without re-encoding, and keep them
in sync</a>: snapping each
camera to its own keyframe.</li>
<li><a href="https://looselydecoupled.com/ffmpeg-keyframe-hyperlapse/">Make a hyperlapse from keyframes with ffmpeg without decoding every
frame</a>: the 30× render, the padding, and the
full-range filler.</li>
<li><a href="https://looselydecoupled.com/find-landmark-dashcam-video-tunnel-light-color/">Finding a bridge in dashcam video by the color of tunnel
lights</a>: the detector, the
review sheet, and what didn&rsquo;t work.</li>
</ol>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
