Skip to content

Fix Motion Tracker keyframes not applying after save/reload#1854

Open
skuznetsov wants to merge 2 commits into
mltframework:masterfrom
skuznetsov:fix/motion-tracker-clock-keyframes
Open

Fix Motion Tracker keyframes not applying after save/reload#1854
skuznetsov wants to merge 2 commits into
mltframework:masterfrom
skuznetsov:fix/motion-tracker-clock-keyframes

Conversation

@skuznetsov

@skuznetsov skuznetsov commented Jun 27, 2026

Copy link
Copy Markdown

Problem

After saving and reopening a project, Load Keyframes from Motion Tracker silently applies zero keyframes — the masked rectangle stays static instead of following the tracked motion.

Root cause

MotionTrackerModel::trackingData() parses each keyframe's time key with QString::toInt, which only accepts integer frame numbers (e.g. 5). When a project is saved, Shotcut serializes animated properties — including the opencv.tracker results — in clock time format (e.g. 00:00:00.167). After a save + reload, toInt then fails on every entry, so trackingData returns an empty list and applyTracking lays down no keyframes.

This is why tracking "works" right after Analyze (in-memory results are frame-numbered) but stops working once the project has been saved and reopened.

Fix

Accept both frame-number and clock/timecode time keys. The parsed frame value is not used downstream (only the rectangles are consumed by applyTracking/reset), so fall back to the running index when toInt fails.

Verification

On a real project whose tracker results were stored as clock timecodes, the parser yielded 0 rectangles before the change and all 46 after, and the masked filter's keyframes are applied across the whole clip as expected.

MotionTrackerModel::trackingData parsed each keyframe's time key with
QString::toInt, which only accepts integer frame numbers ("5"). When a
project is saved, Shotcut serializes animated properties (including the
opencv.tracker `results`) in clock time format (00:00:00.167). After a
save+reload, toInt then failed on every entry, trackingData returned an
empty list, and "Load Keyframes from Motion Tracker" silently applied
zero keyframes (the masked rect stayed static).

Accept both frame-number and clock/timecode keys. The parsed frame value
is not used downstream (only the rectangles are consumed by applyTracking
and reset), so fall back to the running index when toInt fails.
@skuznetsov skuznetsov force-pushed the fix/motion-tracker-clock-keyframes branch from 843c062 to 4b45983 Compare June 27, 2026 13:36
Comment thread src/models/motiontrackermodel.cpp Outdated
// downstream, so the exact frame value is not significant here.
int frame = pair.at(0).toInt(&ok);
if (!ok)
frame = int(result.size());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should provide clear steps to reproduce the bug since I did not reproduce it. I think there was a change in the past year or so to ensure property animations would save as time values instead of frame numbers to ensure they adapt to changing the project video mode's frame rate. So, it would not surprise me if this bug occurred as a result. However, I must be able to confirm a fix.

I added a debug log line inside this method to log each pair. The MLT XML contains

<property name="results">00:00:00.000~=1333 671 192 108 0;00:00:00.167~=1334 669 192 108 0;00:00:00.334~=1332 664 196 110 0;00:00:00.501~=1335 661 192 108 0;00:00:00.667~=1335 657 196 110 0;00:00:00.834~=1338 655 192 108 0;00:00:01.001~=1337 652 196 110 0;00:00:01.168~=1339 649 196 110 0;00:00:01.335~=1342 649 192 108 0;00:00:01.502~=1342 646 196 110 0;00:00:01.668~=1343 646 196 110 0;00:00:01.835~=1342 646 196 110 0;00:00:02.002~=1345 647 192 108 0;00:00:02.169~=1345 648 192 108 0;00:00:02.336~=1345 650 192 108 0;00:00:02.503~=1344 650 192 108 0;00:00:02.669~=1343 652 192 108 0;00:00:02.836~=1342 653 192 108 0;00:00:03.003~=1340 654 192 108 0;00:00:03.170~=1339 654 192 108 0;00:00:03.203~=1339 654 192 108 0</property>

And the debug log shows:

[Debug  ] <MotionTrackerModel::trackingData> QList("0", "1333 671 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("5", "1334 669 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("10", "1332 664 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("15", "1335 661 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("20", "1335 657 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("25", "1338 655 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("30", "1337 652 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("35", "1339 649 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("40", "1342 649 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("45", "1342 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("50", "1343 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("55", "1342 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("60", "1345 647 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("65", "1345 648 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("70", "1345 650 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("75", "1344 650 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("80", "1343 652 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("85", "1342 653 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("90", "1340 654 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("95", "1339 654 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("96", "1339 654 192 108 0") 

The first pair item is already a numeric string. It appears something already converted them from time values.

Secondly, your change is wrong; it cannot simply use the index as the frame number because it currently makes a keyframe every 5 frames. It needs to use MLT->consumer()->time_to_frames().

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review.
Reproduction (it only triggers once the results are read back as clock time, which is what happens here after a save + reload):

  1. Add a Motion Tracker filter to a clip, set the rect, and Analyze.
  2. Add a Mask: Simple Shape filter and use "Load Keyframes from Motion Tracker" - this works.
  3. Save the project, close it, and reopen it.
  4. Select the mask and "Load Keyframes from Motion Tracker" again → zero keyframes are applied (the masked rect stays static).

On reopen the saved results are in clock form, e.g. 00:00:00.000~=789 457 154 13 0;00:00:00.167~=..., and trackingData() receives those clock strings. The original code did pair.at(0).toInt(&ok) and only appended the item if (ok). toInt() fails on 00:00:00.167, so every entry was skipped and trackingData() returned an empty list, so no keyframes applied. That if (ok) gate is the real bug.

Responding to review feedback: rather than falling back to the running
index when QString::toInt fails on a clock-time key, parse the keyframe
time with Mlt::Consumer::time_to_frames. It converts both the clock form
("00:00:00.167") and the frame-number form ("5") to the correct frame
using the project frame rate, so a project reloaded with clock-time
results no longer yields an empty list and the keyframes apply.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants