Preserve audio-only reader dimensions on pasted clips

-Set the parent timeline before hydrating clips inserted through JSON diffs, matching full project JSON load behavior. This lets copied/pasted clips expose timeline context while their nested readers are rebuilt.
-Avoid clobbering valid stored dimensions for audio-only FFmpeg readers with the 720x480 fallback. The fallback is now only used when width or height are missing, while parent timeline preview dimensions can still override when available.
-Add regression coverage for audio-only FFmpegReader JSON hydration and timeline JSON-diff insertion preserving 1280x720 dimensions.
This commit is contained in:
Jonathan Thomas
2026-04-30 23:45:14 -05:00
parent aea12d2d32
commit fec6e901ed
4 changed files with 80 additions and 2 deletions
+4 -2
View File
@@ -1000,8 +1000,10 @@ void FFmpegReader::UpdateAudioInfo() {
info.fps.den = 1;
info.video_timebase.num = 1;
info.video_timebase.den = 30;
info.width = 720;
info.height = 480;
if (info.width <= 0 || info.height <= 0) {
info.width = 720;
info.height = 480;
}
// Use timeline to set correct width & height (if any)
Clip *parent = static_cast<Clip *>(ParentClip());
+4
View File
@@ -1486,6 +1486,10 @@ void Timeline::apply_json_to_clips(Json::Value change) {
// Keep track of allocated clip objects
allocated_clips.insert(clip);
// Match full timeline JSON loading: parent timeline must be available
// before clip JSON can inflate nested readers/effects.
clip->ParentTimeline(this);
// Set properties of clip from JSON
clip->SetJsonValue(change["value"]);
+30
View File
@@ -143,6 +143,36 @@ TEST_CASE( "Check_Audio_File", "[libopenshot][ffmpegreader]" )
r.Close();
}
TEST_CASE( "Audio_Only_SetJson_Preserves_Stored_Dimensions", "[libopenshot][ffmpegreader][json]" )
{
std::stringstream path;
path << TEST_MEDIA_PATH << "piano.wav";
FFmpegReader r(path.str());
std::stringstream json;
json << "{\"type\":\"FFmpegReader\","
<< "\"path\":\"" << path.str() << "\","
<< "\"has_audio\":true,"
<< "\"has_video\":false,"
<< "\"width\":1280,"
<< "\"height\":720,"
<< "\"fps\":{\"num\":30,\"den\":1},"
<< "\"sample_rate\":44100,"
<< "\"channels\":2,"
<< "\"channel_layout\":3,"
<< "\"duration\":1.0}";
r.SetJson(json.str());
r.Open();
CHECK(r.info.has_audio);
CHECK_FALSE(r.info.has_video);
CHECK(r.info.width == 1280);
CHECK(r.info.height == 720);
CHECK(r.GetFrame(1)->GetWidth() == 1280);
CHECK(r.GetFrame(1)->GetHeight() == 720);
}
TEST_CASE( "Check_Video_File", "[libopenshot][ffmpegreader]" )
{
// Create a reader
+42
View File
@@ -480,6 +480,48 @@ TEST_CASE("ReaderInfo constructor", "[libopenshot][timeline]")
CHECK(r1->info.channel_layout == t1.info.channel_layout);
}
TEST_CASE("JSON diff insert keeps audio-only clip at stored timeline dimensions", "[libopenshot][timeline][json]")
{
std::stringstream path;
path << TEST_MEDIA_PATH << "piano.wav";
Timeline t(1280, 720, Fraction(30, 1), 44100, 2, LAYOUT_STEREO);
std::stringstream diff;
diff << "[{\"type\":\"insert\",\"key\":[\"clips\"],\"value\":{"
<< "\"id\":\"audio-copy\","
<< "\"position\":0,"
<< "\"start\":0,"
<< "\"end\":1,"
<< "\"duration\":1,"
<< "\"layer\":1,"
<< "\"reader\":{\"type\":\"FFmpegReader\","
<< "\"path\":\"" << path.str() << "\","
<< "\"has_audio\":true,"
<< "\"has_video\":false,"
<< "\"width\":1280,"
<< "\"height\":720,"
<< "\"fps\":{\"num\":30,\"den\":1},"
<< "\"sample_rate\":44100,"
<< "\"channels\":2,"
<< "\"channel_layout\":3,"
<< "\"duration\":1.0}"
<< "}}]";
t.ApplyJsonDiff(diff.str());
std::list<Clip*> clips = t.Clips();
REQUIRE(clips.size() == 1);
Clip* clip = clips.front();
CHECK(clip->ParentTimeline() == &t);
CHECK(clip->Reader()->info.width == 1280);
CHECK(clip->Reader()->info.height == 720);
std::shared_ptr<Frame> frame = t.GetFrame(1);
REQUIRE(frame);
CHECK(frame->GetWidth() == 1280);
CHECK(frame->GetHeight() == 720);
}
TEST_CASE( "width and height functions", "[libopenshot][timeline]" )
{
Fraction fps(30000,1000);