OpenShot Library | libopenshot 0.5.0
Loading...
Searching...
No Matches
Timeline.cpp
Go to the documentation of this file.
1
8
9// Copyright (c) 2008-2019 OpenShot Studios, LLC
10//
11// SPDX-License-Identifier: LGPL-3.0-or-later
12
13#include "Timeline.h"
14
15#include "CacheBase.h"
16#include "CacheDisk.h"
17#include "CacheMemory.h"
18#include "CrashHandler.h"
19#include "FrameMapper.h"
20#include "Exceptions.h"
21
22#include <QDir>
23#include <QFileInfo>
24#include <unordered_map>
25#include <cmath>
26#include <cstdint>
27
28using namespace openshot;
29
30// Default Constructor for the timeline (which sets the canvas width and height)
31Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout) :
32 is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0)
33{
34 // Create CrashHandler and Attach (incase of errors)
36
37 // Init viewport size (curve based, because it can be animated)
38 viewport_scale = Keyframe(100.0);
39 viewport_x = Keyframe(0.0);
40 viewport_y = Keyframe(0.0);
41
42 // Init background color
43 color.red = Keyframe(0.0);
44 color.green = Keyframe(0.0);
45 color.blue = Keyframe(0.0);
46
47 // Init FileInfo struct (clear all values)
48 info.width = width;
49 info.height = height;
50 preview_width = info.width;
51 preview_height = info.height;
52 info.fps = fps;
53 info.sample_rate = sample_rate;
54 info.channels = channels;
55 info.channel_layout = channel_layout;
56 info.video_timebase = fps.Reciprocal();
57 info.duration = 60 * 30; // 30 minute default duration
58 info.has_audio = true;
59 info.has_video = true;
60 info.video_length = info.fps.ToFloat() * info.duration;
61 info.display_ratio = openshot::Fraction(width, height);
62 info.display_ratio.Reduce();
63 info.pixel_ratio = openshot::Fraction(1, 1);
64 info.acodec = "openshot::timeline";
65 info.vcodec = "openshot::timeline";
66
67 // Init max image size
68 SetMaxSize(info.width, info.height);
69
70 // Init cache
71 final_cache = new CacheMemory();
72 final_cache->SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels);
73}
74
75// Delegating constructor that copies parameters from a provided ReaderInfo
77 info.width, info.height, info.fps, info.sample_rate,
78 info.channels, info.channel_layout) {}
79
80// Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline)
81Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) :
82 is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0) {
83
84 // Create CrashHandler and Attach (incase of errors)
86
87 // Init final cache as NULL (will be created after loading json)
88 final_cache = NULL;
89
90 // Init viewport size (curve based, because it can be animated)
91 viewport_scale = Keyframe(100.0);
92 viewport_x = Keyframe(0.0);
93 viewport_y = Keyframe(0.0);
94
95 // Init background color
96 color.red = Keyframe(0.0);
97 color.green = Keyframe(0.0);
98 color.blue = Keyframe(0.0);
99
100 // Check if path exists
101 QFileInfo filePath(QString::fromStdString(path));
102 if (!filePath.exists()) {
103 throw InvalidFile("File could not be opened.", path);
104 }
105
106 // Check OpenShot Install Path exists
108 QDir openshotPath(QString::fromStdString(s->PATH_OPENSHOT_INSTALL));
109 if (!openshotPath.exists()) {
110 throw InvalidFile("PATH_OPENSHOT_INSTALL could not be found.", s->PATH_OPENSHOT_INSTALL);
111 }
112 QDir openshotTransPath(openshotPath.filePath("transitions"));
113 if (!openshotTransPath.exists()) {
114 throw InvalidFile("PATH_OPENSHOT_INSTALL/transitions could not be found.", openshotTransPath.path().toStdString());
115 }
116
117 // Determine asset path
118 QString asset_name = filePath.baseName().left(30) + "_assets";
119 QDir asset_folder(filePath.dir().filePath(asset_name));
120 if (!asset_folder.exists()) {
121 // Create directory if needed
122 asset_folder.mkpath(".");
123 }
124
125 // Load UTF-8 project file into QString
126 QFile projectFile(QString::fromStdString(path));
127 projectFile.open(QFile::ReadOnly);
128 QString projectContents = QString::fromUtf8(projectFile.readAll());
129
130 // Convert all relative paths into absolute paths (if requested)
131 if (convert_absolute_paths) {
132
133 // Find all "image" or "path" references in JSON (using regex). Must loop through match results
134 // due to our path matching needs, which are not possible with the QString::replace() function.
135 QRegularExpression allPathsRegex(QStringLiteral("\"(image|path)\":.*?\"(.*?)\""));
136 std::vector<QRegularExpressionMatch> matchedPositions;
137 QRegularExpressionMatchIterator i = allPathsRegex.globalMatch(projectContents);
138 while (i.hasNext()) {
139 QRegularExpressionMatch match = i.next();
140 if (match.hasMatch()) {
141 // Push all match objects into a vector (so we can reverse them later)
142 matchedPositions.push_back(match);
143 }
144 }
145
146 // Reverse the matches (bottom of file to top, so our replacements don't break our match positions)
147 std::vector<QRegularExpressionMatch>::reverse_iterator itr;
148 for (itr = matchedPositions.rbegin(); itr != matchedPositions.rend(); itr++) {
149 QRegularExpressionMatch match = *itr;
150 QString relativeKey = match.captured(1); // image or path
151 QString relativePath = match.captured(2); // relative file path
152 QString absolutePath = "";
153
154 // Find absolute path of all path, image (including special replacements of @assets and @transitions)
155 if (relativePath.startsWith("@assets")) {
156 absolutePath = QFileInfo(asset_folder.absoluteFilePath(relativePath.replace("@assets", "."))).canonicalFilePath();
157 } else if (relativePath.startsWith("@transitions")) {
158 absolutePath = QFileInfo(openshotTransPath.absoluteFilePath(relativePath.replace("@transitions", "."))).canonicalFilePath();
159 } else {
160 absolutePath = QFileInfo(filePath.absoluteDir().absoluteFilePath(relativePath)).canonicalFilePath();
161 }
162
163 // Replace path in JSON content, if an absolute path was successfully found
164 if (!absolutePath.isEmpty()) {
165 projectContents.replace(match.capturedStart(0), match.capturedLength(0), "\"" + relativeKey + "\": \"" + absolutePath + "\"");
166 }
167 }
168 // Clear matches
169 matchedPositions.clear();
170 }
171
172 // Set JSON of project
173 SetJson(projectContents.toStdString());
174
175 // Calculate valid duration and set has_audio and has_video
176 // based on content inside this Timeline's clips.
177 float calculated_duration = 0.0;
178 for (auto clip : clips)
179 {
180 float clip_last_frame = clip->Position() + clip->Duration();
181 if (clip_last_frame > calculated_duration)
182 calculated_duration = clip_last_frame;
183 if (clip->Reader() && clip->Reader()->info.has_audio)
184 info.has_audio = true;
185 if (clip->Reader() && clip->Reader()->info.has_video)
186 info.has_video = true;
187
188 }
189 info.video_length = calculated_duration * info.fps.ToFloat();
190 info.duration = calculated_duration;
191
192 // Init FileInfo settings
193 info.acodec = "openshot::timeline";
194 info.vcodec = "openshot::timeline";
195 info.video_timebase = info.fps.Reciprocal();
196 info.has_video = true;
197 info.has_audio = true;
198
199 // Init max image size
200 SetMaxSize(info.width, info.height);
201
202 // Init cache
203 final_cache = new CacheMemory();
204 final_cache->SetMaxBytesFromInfo(24, info.width, info.height, info.sample_rate, info.channels);
205}
206
208 if (is_open) {
209 // Auto Close if not already
210 Close();
211 }
212
213 // Remove all clips, effects, and frame mappers
214 Clear();
215
216 // Destroy previous cache (if managed by timeline)
217 if (managed_cache && final_cache) {
218 delete final_cache;
219 final_cache = NULL;
220 }
221}
222
223// Add to the tracked_objects map a pointer to a tracked object (TrackedObjectBBox)
224void Timeline::AddTrackedObject(std::shared_ptr<openshot::TrackedObjectBase> trackedObject){
225
226 // Search for the tracked object on the map
227 auto iterator = tracked_objects.find(trackedObject->Id());
228
229 if (iterator != tracked_objects.end()){
230 // Tracked object's id already present on the map, overwrite it
231 iterator->second = trackedObject;
232 }
233 else{
234 // Tracked object's id not present -> insert it on the map
235 tracked_objects[trackedObject->Id()] = trackedObject;
236 }
237
238 return;
239}
240
241// Return tracked object pointer by it's id
242std::shared_ptr<openshot::TrackedObjectBase> Timeline::GetTrackedObject(std::string id) const{
243
244 // Search for the tracked object on the map
245 auto iterator = tracked_objects.find(id);
246
247 if (iterator != tracked_objects.end()){
248 // Id found, return the pointer to the tracked object
249 std::shared_ptr<openshot::TrackedObjectBase> trackedObject = iterator->second;
250 return trackedObject;
251 }
252 else {
253 // Id not found, return a null pointer
254 return nullptr;
255 }
256}
257
258// Return the ID's of the tracked objects as a list of strings
259std::list<std::string> Timeline::GetTrackedObjectsIds() const{
260
261 // Create a list of strings
262 std::list<std::string> trackedObjects_ids;
263
264 // Iterate through the tracked_objects map
265 for (auto const& it: tracked_objects){
266 // Add the IDs to the list
267 trackedObjects_ids.push_back(it.first);
268 }
269
270 return trackedObjects_ids;
271}
272
273#ifdef USE_OPENCV
274// Return the trackedObject's properties as a JSON string
275std::string Timeline::GetTrackedObjectValues(std::string id, int64_t frame_number) const {
276
277 // Initialize the JSON object
278 Json::Value trackedObjectJson;
279
280 // Search for the tracked object on the map
281 auto iterator = tracked_objects.find(id);
282
283 if (iterator != tracked_objects.end())
284 {
285 // Id found, Get the object pointer and cast it as a TrackedObjectBBox
286 std::shared_ptr<TrackedObjectBBox> trackedObject = std::static_pointer_cast<TrackedObjectBBox>(iterator->second);
287
288 // Get the trackedObject values for it's first frame
289 if (trackedObject->ExactlyContains(frame_number)){
290 BBox box = trackedObject->GetBox(frame_number);
291 float x1 = box.cx - (box.width/2);
292 float y1 = box.cy - (box.height/2);
293 float x2 = box.cx + (box.width/2);
294 float y2 = box.cy + (box.height/2);
295 float rotation = box.angle;
296
297 trackedObjectJson["x1"] = x1;
298 trackedObjectJson["y1"] = y1;
299 trackedObjectJson["x2"] = x2;
300 trackedObjectJson["y2"] = y2;
301 trackedObjectJson["rotation"] = rotation;
302
303 } else {
304 BBox box = trackedObject->BoxVec.begin()->second;
305 float x1 = box.cx - (box.width/2);
306 float y1 = box.cy - (box.height/2);
307 float x2 = box.cx + (box.width/2);
308 float y2 = box.cy + (box.height/2);
309 float rotation = box.angle;
310
311 trackedObjectJson["x1"] = x1;
312 trackedObjectJson["y1"] = y1;
313 trackedObjectJson["x2"] = x2;
314 trackedObjectJson["y2"] = y2;
315 trackedObjectJson["rotation"] = rotation;
316 }
317
318 }
319 else {
320 // Id not found, return all 0 values
321 trackedObjectJson["x1"] = 0;
322 trackedObjectJson["y1"] = 0;
323 trackedObjectJson["x2"] = 0;
324 trackedObjectJson["y2"] = 0;
325 trackedObjectJson["rotation"] = 0;
326 }
327
328 return trackedObjectJson.toStyledString();
329}
330#endif
331
332// Add an openshot::Clip to the timeline
334{
335 // Get lock (prevent getting frames while this happens)
336 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
337
338 // Assign timeline to clip
339 clip->ParentTimeline(this);
340
341 // Clear cache of clip and nested reader (if any)
342 if (clip->Reader() && clip->Reader()->GetCache())
343 clip->Reader()->GetCache()->Clear();
344
345 // All clips should be converted to the frame rate of this timeline
346 if (auto_map_clips) {
347 // Apply framemapper (or update existing framemapper)
348 apply_mapper_to_clip(clip);
349 }
350
351 // Add clip to list
352 clips.push_back(clip);
353
354 // Sort clips
355 sort_clips();
356}
357
358// Add an effect to the timeline
360{
361 // Get lock (prevent getting frames while this happens)
362 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
363
364 // Assign timeline to effect
365 effect->ParentTimeline(this);
366
367 // Add effect to list
368 effects.push_back(effect);
369
370 // Sort effects
371 sort_effects();
372}
373
374// Remove an effect from the timeline
376{
377 // Get lock (prevent getting frames while this happens)
378 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
379
380 effects.remove(effect);
381
382 // Delete effect object (if timeline allocated it)
383 if (allocated_effects.count(effect)) {
384 allocated_effects.erase(effect); // erase before nulling the pointer
385 delete effect;
386 effect = NULL;
387 }
388
389 // Sort effects
390 sort_effects();
391}
392
393// Remove an openshot::Clip to the timeline
395{
396 // Get lock (prevent getting frames while this happens)
397 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
398
399 clips.remove(clip);
400
401 // Delete clip object (if timeline allocated it)
402 if (allocated_clips.count(clip)) {
403 allocated_clips.erase(clip); // erase before nulling the pointer
404 delete clip;
405 clip = NULL;
406 }
407
408 // Sort clips
409 sort_clips();
410}
411
412// Look up a clip
413openshot::Clip* Timeline::GetClip(const std::string& id)
414{
415 // Find the matching clip (if any)
416 for (const auto& clip : clips) {
417 if (clip->Id() == id) {
418 return clip;
419 }
420 }
421 return nullptr;
422}
423
424// Look up a timeline effect
426{
427 // Find the matching effect (if any)
428 for (const auto& effect : effects) {
429 if (effect->Id() == id) {
430 return effect;
431 }
432 }
433 return nullptr;
434}
435
437{
438 // Search all clips for matching effect ID
439 for (const auto& clip : clips) {
440 const auto e = clip->GetEffect(id);
441 if (e != nullptr) {
442 return e;
443 }
444 }
445 return nullptr;
446}
447
448// Return the list of effects on all clips
449std::list<openshot::EffectBase*> Timeline::ClipEffects() const {
450
451 // Initialize the list
452 std::list<EffectBase*> timelineEffectsList;
453
454 // Loop through all clips
455 for (const auto& clip : clips) {
456
457 // Get the clip's list of effects
458 std::list<EffectBase*> clipEffectsList = clip->Effects();
459
460 // Append the clip's effects to the list
461 timelineEffectsList.insert(timelineEffectsList.end(), clipEffectsList.begin(), clipEffectsList.end());
462 }
463
464 return timelineEffectsList;
465}
466
467// Compute the end time of the latest timeline element
469 // Return cached max_time variable (threadsafe)
470 return max_time;
471}
472
473// Compute the highest frame# based on the latest time and FPS
475 const double fps = info.fps.ToDouble();
476 const double t = GetMaxTime();
477 // Inclusive start, exclusive end -> ceil at the end boundary
478 return static_cast<int64_t>(std::ceil(t * fps));
479}
480
481// Compute the first frame# based on the first clip position
483 const double fps = info.fps.ToDouble();
484 const double t = GetMinTime();
485 // Inclusive start -> floor at the start boundary, then 1-index
486 return static_cast<int64_t>(std::floor(t * fps)) + 1;
487}
488
489// Compute the start time of the first timeline clip
491 // Return cached min_time variable (threadsafe)
492 return min_time;
493}
494
495// Apply a FrameMapper to a clip which matches the settings of this timeline
496void Timeline::apply_mapper_to_clip(Clip* clip)
497{
498 // Determine type of reader
499 ReaderBase* clip_reader = NULL;
500 if (clip->Reader()->Name() == "FrameMapper")
501 {
502 // Get the existing reader
503 clip_reader = (ReaderBase*) clip->Reader();
504
505 // Update the mapping
506 FrameMapper* clip_mapped_reader = (FrameMapper*) clip_reader;
508
509 } else {
510
511 // Create a new FrameMapper to wrap the current reader
513 allocated_frame_mappers.insert(mapper);
514 clip_reader = (ReaderBase*) mapper;
515 }
516
517 // Update clip reader
518 clip->Reader(clip_reader);
519}
520
521// Apply the timeline's framerate and samplerate to all clips
523{
524 // Clear all cached frames
526
527 // Loop through all clips
528 for (auto clip : clips)
529 {
530 // Apply framemapper (or update existing framemapper)
531 apply_mapper_to_clip(clip);
532 }
533}
534
535// Calculate time of a frame number, based on a framerate
536double Timeline::calculate_time(int64_t number, Fraction rate)
537{
538 // Get float version of fps fraction
539 double raw_fps = rate.ToFloat();
540
541 // Return the time (in seconds) of this frame
542 return double(number - 1) / raw_fps;
543}
544
545// Apply effects to the source frame (if any)
546std::shared_ptr<Frame> Timeline::apply_effects(std::shared_ptr<Frame> frame, int64_t timeline_frame_number, int layer, TimelineInfoStruct* options)
547{
548 // Debug output
550 "Timeline::apply_effects",
551 "frame->number", frame->number,
552 "timeline_frame_number", timeline_frame_number,
553 "layer", layer);
554
555 // Find Effects at this position and layer
556 for (auto effect : effects)
557 {
558 // Does clip intersect the current requested time
559 const double fpsD = info.fps.ToDouble();
560 int64_t effect_start_position = static_cast<int64_t>(std::llround(effect->Position() * fpsD)) + 1;
561 int64_t effect_end_position = static_cast<int64_t>(std::llround((effect->Position() + effect->Duration()) * fpsD));
562
563 bool does_effect_intersect = (effect_start_position <= timeline_frame_number && effect_end_position >= timeline_frame_number && effect->Layer() == layer);
564
565 // Clip is visible
566 if (does_effect_intersect)
567 {
568 // Determine the frame needed for this clip (based on the position on the timeline)
569 int64_t effect_start_frame = static_cast<int64_t>(std::llround(effect->Start() * fpsD)) + 1;
570 int64_t effect_frame_number = timeline_frame_number - effect_start_position + effect_start_frame;
571
572 if (!options->is_top_clip)
573 continue; // skip effect, if overlapped/covered by another clip on same layer
574
575 if (options->is_before_clip_keyframes != effect->info.apply_before_clip)
576 continue; // skip effect, if this filter does not match
577
578 // Debug output
580 "Timeline::apply_effects (Process Effect)",
581 "effect_frame_number", effect_frame_number,
582 "does_effect_intersect", does_effect_intersect);
583
584 // Apply the effect to this frame
585 frame = effect->GetFrame(frame, effect_frame_number);
586 }
587
588 } // end effect loop
589
590 // Return modified frame
591 return frame;
592}
593
594// Get or generate a blank frame
595std::shared_ptr<Frame> Timeline::GetOrCreateFrame(std::shared_ptr<Frame> background_frame, Clip* clip, int64_t number, openshot::TimelineInfoStruct* options)
596{
597 std::shared_ptr<Frame> new_frame;
598
599 // Init some basic properties about this frame
600 int samples_in_frame = Frame::GetSamplesPerFrame(number, info.fps, info.sample_rate, info.channels);
601
602 try {
603 // Debug output
605 "Timeline::GetOrCreateFrame (from reader)",
606 "number", number,
607 "samples_in_frame", samples_in_frame);
608
609 // Attempt to get a frame (but this could fail if a reader has just been closed)
610 new_frame = std::shared_ptr<Frame>(clip->GetFrame(background_frame, number, options));
611
612 // Return real frame
613 return new_frame;
614
615 } catch (const ReaderClosed & e) {
616 // ...
617 } catch (const OutOfBoundsFrame & e) {
618 // ...
619 }
620
621 // Debug output
623 "Timeline::GetOrCreateFrame (create blank)",
624 "number", number,
625 "samples_in_frame", samples_in_frame);
626
627 // Create blank frame
628 return new_frame;
629}
630
631// Process a new layer of video or audio
632void Timeline::add_layer(std::shared_ptr<Frame> new_frame, Clip* source_clip, int64_t clip_frame_number, bool is_top_clip, float max_volume)
633{
634 // Create timeline options (with details about this current frame request)
635 TimelineInfoStruct options{};
636 options.is_top_clip = is_top_clip;
637 options.is_before_clip_keyframes = true;
638
639 // Get the clip's frame, composited on top of the current timeline frame
640 std::shared_ptr<Frame> source_frame;
641 source_frame = GetOrCreateFrame(new_frame, source_clip, clip_frame_number, &options);
642
643 // No frame found... so bail
644 if (!source_frame)
645 return;
646
647 // Debug output
649 "Timeline::add_layer",
650 "new_frame->number", new_frame->number,
651 "clip_frame_number", clip_frame_number);
652
653 /* COPY AUDIO - with correct volume */
654 if (source_clip->Reader()->info.has_audio) {
655 // Debug output
657 "Timeline::add_layer (Copy Audio)",
658 "source_clip->Reader()->info.has_audio", source_clip->Reader()->info.has_audio,
659 "source_frame->GetAudioChannelsCount()", source_frame->GetAudioChannelsCount(),
660 "info.channels", info.channels,
661 "clip_frame_number", clip_frame_number);
662
663 if (source_frame->GetAudioChannelsCount() == info.channels && source_clip->has_audio.GetInt(clip_frame_number) != 0)
664 {
665 // Ensure timeline frame matches the source samples once per frame
666 if (new_frame->GetAudioSamplesCount() != source_frame->GetAudioSamplesCount()){
667 new_frame->ResizeAudio(info.channels, source_frame->GetAudioSamplesCount(), info.sample_rate, info.channel_layout);
668 }
669
670 for (int channel = 0; channel < source_frame->GetAudioChannelsCount(); channel++)
671 {
672 // Get volume from previous frame and this frame
673 float previous_volume = source_clip->volume.GetValue(clip_frame_number - 1);
674 float volume = source_clip->volume.GetValue(clip_frame_number);
675 int channel_filter = source_clip->channel_filter.GetInt(clip_frame_number); // optional channel to filter (if not -1)
676 int channel_mapping = source_clip->channel_mapping.GetInt(clip_frame_number); // optional channel to map this channel to (if not -1)
677
678 // Apply volume mixing strategy
679 if (source_clip->mixing == VOLUME_MIX_AVERAGE && max_volume > 1.0) {
680 // Don't allow this clip to exceed 100% (divide volume equally between all overlapping clips with volume
681 previous_volume = previous_volume / max_volume;
682 volume = volume / max_volume;
683 }
684 else if (source_clip->mixing == VOLUME_MIX_REDUCE && max_volume > 1.0) {
685 // Reduce clip volume by a bit, hoping it will prevent exceeding 100% (but it is very possible it will)
686 previous_volume = previous_volume * 0.77;
687 volume = volume * 0.77;
688 }
689
690 // If channel filter enabled, check for correct channel (and skip non-matching channels)
691 if (channel_filter != -1 && channel_filter != channel)
692 continue; // skip to next channel
693
694 // If no volume on this frame or previous frame, do nothing
695 if (previous_volume == 0.0 && volume == 0.0)
696 continue; // skip to next channel
697
698 // If channel mapping disabled, just use the current channel
699 if (channel_mapping == -1)
700 channel_mapping = channel;
701
702 // Apply ramp to source frame (if needed)
703 if (!isEqual(previous_volume, 1.0) || !isEqual(volume, 1.0))
704 source_frame->ApplyGainRamp(channel_mapping, 0, source_frame->GetAudioSamplesCount(), previous_volume, volume);
705
706 // Copy audio samples (and set initial volume). Mix samples with existing audio samples. The gains are added together, to
707 // be sure to set the gain's correctly, so the sum does not exceed 1.0 (of audio distortion will happen).
708 new_frame->AddAudio(false, channel_mapping, 0, source_frame->GetAudioSamples(channel), source_frame->GetAudioSamplesCount(), 1.0);
709 }
710 }
711 else
712 // Debug output
714 "Timeline::add_layer (No Audio Copied - Wrong # of Channels)",
715 "source_clip->Reader()->info.has_audio",
716 source_clip->Reader()->info.has_audio,
717 "source_frame->GetAudioChannelsCount()",
718 source_frame->GetAudioChannelsCount(),
719 "info.channels", info.channels,
720 "clip_frame_number", clip_frame_number);
721 }
722
723 // Debug output
725 "Timeline::add_layer (Transform: Composite Image Layer: Completed)",
726 "source_frame->number", source_frame->number,
727 "new_frame->GetImage()->width()", new_frame->GetWidth(),
728 "new_frame->GetImage()->height()", new_frame->GetHeight());
729}
730
731// Update the list of 'opened' clips
732void Timeline::update_open_clips(Clip *clip, bool does_clip_intersect)
733{
734 // Get lock (prevent getting frames while this happens)
735 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
736
738 "Timeline::update_open_clips (before)",
739 "does_clip_intersect", does_clip_intersect,
740 "closing_clips.size()", closing_clips.size(),
741 "open_clips.size()", open_clips.size());
742
743 // is clip already in list?
744 bool clip_found = open_clips.count(clip);
745
746 if (clip_found && !does_clip_intersect)
747 {
748 // Remove clip from 'opened' list, because it's closed now
749 open_clips.erase(clip);
750
751 // Close clip
752 clip->Close();
753 }
754 else if (!clip_found && does_clip_intersect)
755 {
756 // Add clip to 'opened' list, because it's missing
757 open_clips[clip] = clip;
758
759 try {
760 // Open the clip
761 clip->Open();
762
763 } catch (const InvalidFile & e) {
764 // ...
765 }
766 }
767
768 // Debug output
770 "Timeline::update_open_clips (after)",
771 "does_clip_intersect", does_clip_intersect,
772 "clip_found", clip_found,
773 "closing_clips.size()", closing_clips.size(),
774 "open_clips.size()", open_clips.size());
775}
776
777// Calculate the max and min duration (in seconds) of the timeline, based on all the clips, and cache the value
778void Timeline::calculate_max_duration() {
779 double last_clip = 0.0;
780 double last_effect = 0.0;
781 double first_clip = std::numeric_limits<double>::max();
782 double first_effect = std::numeric_limits<double>::max();
783
784 // Find the last and first clip
785 if (!clips.empty()) {
786 // Find the clip with the maximum end frame
787 const auto max_clip = std::max_element(
788 clips.begin(), clips.end(), CompareClipEndFrames());
789 last_clip = (*max_clip)->Position() + (*max_clip)->Duration();
790
791 // Find the clip with the minimum start position (ignoring layer)
792 const auto min_clip = std::min_element(
793 clips.begin(), clips.end(), [](const openshot::Clip* lhs, const openshot::Clip* rhs) {
794 return lhs->Position() < rhs->Position();
795 });
796 first_clip = (*min_clip)->Position();
797 }
798
799 // Find the last and first effect
800 if (!effects.empty()) {
801 // Find the effect with the maximum end frame
802 const auto max_effect = std::max_element(
803 effects.begin(), effects.end(), CompareEffectEndFrames());
804 last_effect = (*max_effect)->Position() + (*max_effect)->Duration();
805
806 // Find the effect with the minimum start position
807 const auto min_effect = std::min_element(
808 effects.begin(), effects.end(), [](const openshot::EffectBase* lhs, const openshot::EffectBase* rhs) {
809 return lhs->Position() < rhs->Position();
810 });
811 first_effect = (*min_effect)->Position();
812 }
813
814 // Calculate the max and min time
815 max_time = std::max(last_clip, last_effect);
816 min_time = std::min(first_clip, first_effect);
817
818 // If no clips or effects exist, set min_time to 0
819 if (clips.empty() && effects.empty()) {
820 min_time = 0.0;
821 max_time = 0.0;
822 }
823}
824
825// Sort clips by position on the timeline
826void Timeline::sort_clips()
827{
828 // Get lock (prevent getting frames while this happens)
829 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
830
831 // Debug output
833 "Timeline::SortClips",
834 "clips.size()", clips.size());
835
836 // sort clips
837 clips.sort(CompareClips());
838
839 // calculate max timeline duration
840 calculate_max_duration();
841}
842
843// Sort effects by position on the timeline
844void Timeline::sort_effects()
845{
846 // Get lock (prevent getting frames while this happens)
847 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
848
849 // sort clips
850 effects.sort(CompareEffects());
851
852 // calculate max timeline duration
853 calculate_max_duration();
854}
855
856// Clear all clips from timeline
858{
859 ZmqLogger::Instance()->AppendDebugMethod("Timeline::Clear");
860
861 // Get lock (prevent getting frames while this happens)
862 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
863
864 // Close all open clips
865 for (auto clip : clips)
866 {
867 update_open_clips(clip, false);
868
869 // Delete clip object (if timeline allocated it)
870 bool allocated = allocated_clips.count(clip);
871 if (allocated) {
872 delete clip;
873 }
874 }
875 // Clear all clips
876 clips.clear();
877 allocated_clips.clear();
878
879 // Close all effects
880 for (auto effect : effects)
881 {
882 // Delete effect object (if timeline allocated it)
883 bool allocated = allocated_effects.count(effect);
884 if (allocated) {
885 delete effect;
886 }
887 }
888 // Clear all effects
889 effects.clear();
890 allocated_effects.clear();
891
892 // Delete all FrameMappers
893 for (auto mapper : allocated_frame_mappers)
894 {
895 mapper->Reader(NULL);
896 mapper->Close();
897 delete mapper;
898 }
899 allocated_frame_mappers.clear();
900}
901
902// Close the reader (and any resources it was consuming)
904{
905 ZmqLogger::Instance()->AppendDebugMethod("Timeline::Close");
906
907 // Get lock (prevent getting frames while this happens)
908 const std::lock_guard<std::recursive_mutex> guard(getFrameMutex);
909
910 // Close all open clips
911 for (auto clip : clips)
912 {
913 // Open or Close this clip, based on if it's intersecting or not
914 update_open_clips(clip, false);
915 }
916
917 // Mark timeline as closed
918 is_open = false;
919
920 // Clear all cache (deep clear, including nested Readers)
921 ClearAllCache(true);
922}
923
924// Open the reader (and start consuming resources)
926{
927 is_open = true;
928}
929
930// Compare 2 floating point numbers for equality
931bool Timeline::isEqual(double a, double b)
932{
933 return fabs(a - b) < 0.000001;
934}
935
936// Get an openshot::Frame object for a specific frame number of this reader.
937std::shared_ptr<Frame> Timeline::GetFrame(int64_t requested_frame)
938{
939 // Adjust out of bounds frame number
940 if (requested_frame < 1)
941 requested_frame = 1;
942
943 // Check cache
944 std::shared_ptr<Frame> frame;
945 frame = final_cache->GetFrame(requested_frame);
946 if (frame) {
947 // Debug output
949 "Timeline::GetFrame (Cached frame found)",
950 "requested_frame", requested_frame);
951
952 // Return cached frame
953 return frame;
954 }
955 else
956 {
957 // Prevent async calls to the following code
958 const std::lock_guard<std::recursive_mutex> lock(getFrameMutex);
959
960 // Check cache 2nd time
961 std::shared_ptr<Frame> frame;
962 frame = final_cache->GetFrame(requested_frame);
963 if (frame) {
964 // Debug output
966 "Timeline::GetFrame (Cached frame found on 2nd check)",
967 "requested_frame", requested_frame);
968
969 // Return cached frame
970 return frame;
971 } else {
972 // Get a list of clips that intersect with the requested section of timeline
973 // This also opens the readers for intersecting clips, and marks non-intersecting clips as 'needs closing'
974 std::vector<Clip *> nearby_clips;
975 nearby_clips = find_intersecting_clips(requested_frame, 1, true);
976
977 // Debug output
979 "Timeline::GetFrame (processing frame)",
980 "requested_frame", requested_frame,
981 "omp_get_thread_num()", omp_get_thread_num());
982
983 // Init some basic properties about this frame
984 int samples_in_frame = Frame::GetSamplesPerFrame(requested_frame, info.fps, info.sample_rate, info.channels);
985
986 // Create blank frame (which will become the requested frame)
987 std::shared_ptr<Frame> new_frame(std::make_shared<Frame>(requested_frame, preview_width, preview_height, "#000000", samples_in_frame, info.channels));
988 new_frame->AddAudioSilence(samples_in_frame);
989 new_frame->SampleRate(info.sample_rate);
990 new_frame->ChannelsLayout(info.channel_layout);
991
992 // Debug output
994 "Timeline::GetFrame (Adding solid color)",
995 "requested_frame", requested_frame,
996 "info.width", info.width,
997 "info.height", info.height);
998
999 // Add Background Color to 1st layer (if animated or not black)
1000 if ((color.red.GetCount() > 1 || color.green.GetCount() > 1 || color.blue.GetCount() > 1) ||
1001 (color.red.GetValue(requested_frame) != 0.0 || color.green.GetValue(requested_frame) != 0.0 ||
1002 color.blue.GetValue(requested_frame) != 0.0))
1003 new_frame->AddColor(preview_width, preview_height, color.GetColorHex(requested_frame));
1004
1005 // Debug output
1007 "Timeline::GetFrame (Loop through clips)",
1008 "requested_frame", requested_frame,
1009 "clips.size()", clips.size(),
1010 "nearby_clips.size()", nearby_clips.size());
1011
1012 // Precompute per-clip timing for this requested frame
1013 struct ClipInfo {
1014 Clip* clip;
1015 int64_t start_pos;
1016 int64_t end_pos;
1017 int64_t start_frame;
1018 int64_t frame_number;
1019 bool intersects;
1020 };
1021 std::vector<ClipInfo> clip_infos;
1022 clip_infos.reserve(nearby_clips.size());
1023 const double fpsD = info.fps.ToDouble();
1024
1025 for (auto clip : nearby_clips) {
1026 int64_t start_pos = static_cast<int64_t>(std::llround(clip->Position() * fpsD)) + 1;
1027 int64_t end_pos = static_cast<int64_t>(std::llround((clip->Position() + clip->Duration()) * fpsD));
1028 bool intersects = (start_pos <= requested_frame && end_pos >= requested_frame);
1029 int64_t start_frame = static_cast<int64_t>(std::llround(clip->Start() * fpsD)) + 1;
1030 int64_t frame_number = requested_frame - start_pos + start_frame;
1031 clip_infos.push_back({clip, start_pos, end_pos, start_frame, frame_number, intersects});
1032 }
1033
1034 // Determine top clip per layer (linear, no nested loop)
1035 std::unordered_map<int, int64_t> top_start_for_layer;
1036 std::unordered_map<int, Clip*> top_clip_for_layer;
1037 for (const auto& ci : clip_infos) {
1038 if (!ci.intersects) continue;
1039 const int layer = ci.clip->Layer();
1040 auto it = top_start_for_layer.find(layer);
1041 if (it == top_start_for_layer.end() || ci.start_pos > it->second) {
1042 top_start_for_layer[layer] = ci.start_pos; // strictly greater to match prior logic
1043 top_clip_for_layer[layer] = ci.clip;
1044 }
1045 }
1046
1047 // Compute max_volume across all overlapping clips once
1048 float max_volume_sum = 0.0f;
1049 for (const auto& ci : clip_infos) {
1050 if (!ci.intersects) continue;
1051 if (ci.clip->Reader() && ci.clip->Reader()->info.has_audio &&
1052 ci.clip->has_audio.GetInt(ci.frame_number) != 0) {
1053 max_volume_sum += static_cast<float>(ci.clip->volume.GetValue(ci.frame_number));
1054 }
1055 }
1056
1057 // Compose intersecting clips in a single pass
1058 for (const auto& ci : clip_infos) {
1059 // Debug output
1061 "Timeline::GetFrame (Does clip intersect)",
1062 "requested_frame", requested_frame,
1063 "clip->Position()", ci.clip->Position(),
1064 "clip->Duration()", ci.clip->Duration(),
1065 "does_clip_intersect", ci.intersects);
1066
1067 // Clip is visible
1068 if (ci.intersects) {
1069 // Is this the top clip on its layer?
1070 bool is_top_clip = false;
1071 const int layer = ci.clip->Layer();
1072 auto top_it = top_clip_for_layer.find(layer);
1073 if (top_it != top_clip_for_layer.end())
1074 is_top_clip = (top_it->second == ci.clip);
1075
1076 // Determine the frame needed for this clip (based on the position on the timeline)
1077 int64_t clip_frame_number = ci.frame_number;
1078
1079 // Debug output
1081 "Timeline::GetFrame (Calculate clip's frame #)",
1082 "clip->Position()", ci.clip->Position(),
1083 "clip->Start()", ci.clip->Start(),
1084 "info.fps.ToFloat()", info.fps.ToFloat(),
1085 "clip_frame_number", clip_frame_number);
1086
1087 // Add clip's frame as layer
1088 add_layer(new_frame, ci.clip, clip_frame_number, is_top_clip, max_volume_sum);
1089
1090 } else {
1091 // Debug output
1093 "Timeline::GetFrame (clip does not intersect)",
1094 "requested_frame", requested_frame,
1095 "does_clip_intersect", ci.intersects);
1096 }
1097
1098 } // end clip loop
1099
1100 // Debug output
1102 "Timeline::GetFrame (Add frame to cache)",
1103 "requested_frame", requested_frame,
1104 "info.width", info.width,
1105 "info.height", info.height);
1106
1107 // Set frame # on mapped frame
1108 new_frame->SetFrameNumber(requested_frame);
1109
1110 // Add final frame to cache
1111 final_cache->Add(new_frame);
1112
1113 // Return frame (or blank frame)
1114 return new_frame;
1115 }
1116 }
1117}
1118
1119
1120// Find intersecting clips (or non intersecting clips)
1121std::vector<Clip*> Timeline::find_intersecting_clips(int64_t requested_frame, int number_of_frames, bool include)
1122{
1123 // Find matching clips
1124 std::vector<Clip*> matching_clips;
1125
1126 // Calculate time of frame
1127 const int64_t min_requested_frame = requested_frame;
1128 const int64_t max_requested_frame = requested_frame + (number_of_frames - 1);
1129
1130 // Find Clips at this time
1131 matching_clips.reserve(clips.size());
1132 const double fpsD = info.fps.ToDouble();
1133 for (auto clip : clips)
1134 {
1135 // Does clip intersect the current requested time
1136 int64_t clip_start_position = static_cast<int64_t>(std::llround(clip->Position() * fpsD)) + 1;
1137 int64_t clip_end_position = static_cast<int64_t>(std::llround((clip->Position() + clip->Duration()) * fpsD)) + 1;
1138
1139 bool does_clip_intersect =
1140 (clip_start_position <= min_requested_frame || clip_start_position <= max_requested_frame) &&
1141 (clip_end_position >= min_requested_frame || clip_end_position >= max_requested_frame);
1142
1143 // Debug output
1145 "Timeline::find_intersecting_clips (Is clip near or intersecting)",
1146 "requested_frame", requested_frame,
1147 "min_requested_frame", min_requested_frame,
1148 "max_requested_frame", max_requested_frame,
1149 "clip->Position()", clip->Position(),
1150 "does_clip_intersect", does_clip_intersect);
1151
1152 // Open (or schedule for closing) this clip, based on if it's intersecting or not
1153 update_open_clips(clip, does_clip_intersect);
1154
1155 // Clip is visible
1156 if (does_clip_intersect && include)
1157 // Add the intersecting clip
1158 matching_clips.push_back(clip);
1159
1160 else if (!does_clip_intersect && !include)
1161 // Add the non-intersecting clip
1162 matching_clips.push_back(clip);
1163
1164 } // end clip loop
1165
1166 // return list
1167 return matching_clips;
1168}
1169
1170// Set the cache object used by this reader
1172 // Get lock (prevent getting frames while this happens)
1173 const std::lock_guard<std::recursive_mutex> lock(getFrameMutex);
1174
1175 // Destroy previous cache (if managed by timeline)
1176 if (managed_cache && final_cache) {
1177 delete final_cache;
1178 final_cache = NULL;
1179 managed_cache = false;
1180 }
1181
1182 // Set new cache
1183 final_cache = new_cache;
1184}
1185
1186// Generate JSON string of this object
1187std::string Timeline::Json() const {
1188
1189 // Return formatted string
1190 return JsonValue().toStyledString();
1191}
1192
1193// Generate Json::Value for this object
1194Json::Value Timeline::JsonValue() const {
1195
1196 // Create root json object
1197 Json::Value root = ReaderBase::JsonValue(); // get parent properties
1198 root["type"] = "Timeline";
1199 root["viewport_scale"] = viewport_scale.JsonValue();
1200 root["viewport_x"] = viewport_x.JsonValue();
1201 root["viewport_y"] = viewport_y.JsonValue();
1202 root["color"] = color.JsonValue();
1203 root["path"] = path;
1204
1205 // Add array of clips
1206 root["clips"] = Json::Value(Json::arrayValue);
1207
1208 // Find Clips at this time
1209 for (const auto existing_clip : clips)
1210 {
1211 root["clips"].append(existing_clip->JsonValue());
1212 }
1213
1214 // Add array of effects
1215 root["effects"] = Json::Value(Json::arrayValue);
1216
1217 // loop through effects
1218 for (const auto existing_effect: effects)
1219 {
1220 root["effects"].append(existing_effect->JsonValue());
1221 }
1222
1223 // return JsonValue
1224 return root;
1225}
1226
1227// Load JSON string into this object
1228void Timeline::SetJson(const std::string value) {
1229
1230 // Get lock (prevent getting frames while this happens)
1231 const std::lock_guard<std::recursive_mutex> lock(getFrameMutex);
1232
1233 // Parse JSON string into JSON objects
1234 try
1235 {
1236 const Json::Value root = openshot::stringToJson(value);
1237 // Set all values that match
1238 SetJsonValue(root);
1239 }
1240 catch (const std::exception& e)
1241 {
1242 // Error parsing JSON (or missing keys)
1243 throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
1244 }
1245}
1246
1247// Load Json::Value into this object
1248void Timeline::SetJsonValue(const Json::Value root) {
1249
1250 // Get lock (prevent getting frames while this happens)
1251 const std::lock_guard<std::recursive_mutex> lock(getFrameMutex);
1252
1253 // Close timeline before we do anything (this closes all clips)
1254 bool was_open = is_open;
1255 Close();
1256
1257 // Set parent data
1259
1260 // Set data from Json (if key is found)
1261 if (!root["path"].isNull())
1262 path = root["path"].asString();
1263
1264 if (!root["clips"].isNull()) {
1265 // Clear existing clips
1266 clips.clear();
1267
1268 // loop through clips
1269 for (const Json::Value existing_clip : root["clips"]) {
1270 // Skip NULL nodes
1271 if (existing_clip.isNull()) {
1272 continue;
1273 }
1274
1275 // Create Clip
1276 Clip *c = new Clip();
1277
1278 // Keep track of allocated clip objects
1279 allocated_clips.insert(c);
1280
1281 // When a clip is attached to an object, it searches for the object
1282 // on it's parent timeline. Setting the parent timeline of the clip here
1283 // allows attaching it to an object when exporting the project (because)
1284 // the exporter script initializes the clip and it's effects
1285 // before setting its parent timeline.
1286 c->ParentTimeline(this);
1287
1288 // Load Json into Clip
1289 c->SetJsonValue(existing_clip);
1290
1291 // Add Clip to Timeline
1292 AddClip(c);
1293 }
1294 }
1295
1296 if (!root["effects"].isNull()) {
1297 // Clear existing effects
1298 effects.clear();
1299
1300 // loop through effects
1301 for (const Json::Value existing_effect :root["effects"]) {
1302 // Skip NULL nodes
1303 if (existing_effect.isNull()) {
1304 continue;
1305 }
1306
1307 // Create Effect
1308 EffectBase *e = NULL;
1309
1310 if (!existing_effect["type"].isNull()) {
1311 // Create instance of effect
1312 if ( (e = EffectInfo().CreateEffect(existing_effect["type"].asString())) ) {
1313
1314 // Keep track of allocated effect objects
1315 allocated_effects.insert(e);
1316
1317 // Load Json into Effect
1318 e->SetJsonValue(existing_effect);
1319
1320 // Add Effect to Timeline
1321 AddEffect(e);
1322 }
1323 }
1324 }
1325 }
1326
1327 if (!root["duration"].isNull()) {
1328 // Update duration of timeline
1329 info.duration = root["duration"].asDouble();
1330 info.video_length = info.fps.ToFloat() * info.duration;
1331 }
1332
1333 // Update preview settings
1334 preview_width = info.width;
1335 preview_height = info.height;
1336
1337 // Resort (and recalculate min/max duration)
1338 sort_clips();
1339 sort_effects();
1340
1341 // Re-open if needed
1342 if (was_open)
1343 Open();
1344}
1345
1346// Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete)
1347void Timeline::ApplyJsonDiff(std::string value) {
1348
1349 // Get lock (prevent getting frames while this happens)
1350 const std::lock_guard<std::recursive_mutex> lock(getFrameMutex);
1351
1352 // Parse JSON string into JSON objects
1353 try
1354 {
1355 const Json::Value root = openshot::stringToJson(value);
1356 // Process the JSON change array, loop through each item
1357 for (const Json::Value change : root) {
1358 std::string change_key = change["key"][(uint)0].asString();
1359
1360 // Process each type of change
1361 if (change_key == "clips")
1362 // Apply to CLIPS
1363 apply_json_to_clips(change);
1364
1365 else if (change_key == "effects")
1366 // Apply to EFFECTS
1367 apply_json_to_effects(change);
1368
1369 else
1370 // Apply to TIMELINE
1371 apply_json_to_timeline(change);
1372
1373 }
1374 }
1375 catch (const std::exception& e)
1376 {
1377 // Error parsing JSON (or missing keys)
1378 throw InvalidJSON("JSON is invalid (missing keys or invalid data types)");
1379 }
1380}
1381
1382// Apply JSON diff to clips
1383void Timeline::apply_json_to_clips(Json::Value change) {
1384
1385 // Get key and type of change
1386 std::string change_type = change["type"].asString();
1387 std::string clip_id = "";
1388 Clip *existing_clip = NULL;
1389
1390 // Find id of clip (if any)
1391 for (auto key_part : change["key"]) {
1392 // Get each change
1393 if (key_part.isObject()) {
1394 // Check for id
1395 if (!key_part["id"].isNull()) {
1396 // Set the id
1397 clip_id = key_part["id"].asString();
1398
1399 // Find matching clip in timeline (if any)
1400 for (auto c : clips)
1401 {
1402 if (c->Id() == clip_id) {
1403 existing_clip = c;
1404 break; // clip found, exit loop
1405 }
1406 }
1407 break; // id found, exit loop
1408 }
1409 }
1410 }
1411
1412 // Check for a more specific key (targetting this clip's effects)
1413 // For example: ["clips", {"id:123}, "effects", {"id":432}]
1414 if (existing_clip && change["key"].size() == 4 && change["key"][2] == "effects")
1415 {
1416 // This change is actually targetting a specific effect under a clip (and not the clip)
1417 Json::Value key_part = change["key"][3];
1418
1419 if (key_part.isObject()) {
1420 // Check for id
1421 if (!key_part["id"].isNull())
1422 {
1423 // Set the id
1424 std::string effect_id = key_part["id"].asString();
1425
1426 // Find matching effect in timeline (if any)
1427 std::list<EffectBase*> effect_list = existing_clip->Effects();
1428 for (auto e : effect_list)
1429 {
1430 if (e->Id() == effect_id) {
1431 // Apply the change to the effect directly
1432 apply_json_to_effects(change, e);
1433
1434 // Calculate start and end frames that this impacts, and remove those frames from the cache
1435 int64_t new_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1;
1436 int64_t new_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1;
1437 final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8);
1438
1439 return; // effect found, don't update clip
1440 }
1441 }
1442 }
1443 }
1444 }
1445
1446 // Determine type of change operation
1447 if (change_type == "insert") {
1448
1449 // Create clip
1450 Clip *clip = new Clip();
1451
1452 // Keep track of allocated clip objects
1453 allocated_clips.insert(clip);
1454
1455 // Set properties of clip from JSON
1456 clip->SetJsonValue(change["value"]);
1457
1458 // Add clip to timeline
1459 AddClip(clip);
1460
1461 } else if (change_type == "update") {
1462
1463 // Update existing clip
1464 if (existing_clip) {
1465 // Calculate start and end frames prior to the update
1466 int64_t old_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1;
1467 int64_t old_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1;
1468
1469 // Update clip properties from JSON
1470 existing_clip->SetJsonValue(change["value"]);
1471
1472 // Calculate new start and end frames after the update
1473 int64_t new_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1;
1474 int64_t new_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1;
1475
1476 // Remove both the old and new ranges from the timeline cache
1477 final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8);
1478 final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8);
1479
1480 // Remove cache on clip's Reader (if found)
1481 if (existing_clip->Reader() && existing_clip->Reader()->GetCache()) {
1482 existing_clip->Reader()->GetCache()->Remove(old_starting_frame - 8, old_ending_frame + 8);
1483 existing_clip->Reader()->GetCache()->Remove(new_starting_frame - 8, new_ending_frame + 8);
1484 }
1485
1486 // Apply framemapper (or update existing framemapper)
1487 if (auto_map_clips) {
1488 apply_mapper_to_clip(existing_clip);
1489 }
1490 }
1491
1492 } else if (change_type == "delete") {
1493
1494 // Remove existing clip
1495 if (existing_clip) {
1496 // Remove clip from timeline
1497 RemoveClip(existing_clip);
1498
1499 // Calculate start and end frames that this impacts, and remove those frames from the cache
1500 int64_t old_starting_frame = (existing_clip->Position() * info.fps.ToDouble()) + 1;
1501 int64_t old_ending_frame = ((existing_clip->Position() + existing_clip->Duration()) * info.fps.ToDouble()) + 1;
1502 final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8);
1503 }
1504
1505 }
1506
1507 // Re-Sort Clips (since they likely changed)
1508 sort_clips();
1509}
1510
1511// Apply JSON diff to effects
1512void Timeline::apply_json_to_effects(Json::Value change) {
1513
1514 // Get key and type of change
1515 std::string change_type = change["type"].asString();
1516 EffectBase *existing_effect = NULL;
1517
1518 // Find id of an effect (if any)
1519 for (auto key_part : change["key"]) {
1520
1521 if (key_part.isObject()) {
1522 // Check for id
1523 if (!key_part["id"].isNull())
1524 {
1525 // Set the id
1526 std::string effect_id = key_part["id"].asString();
1527
1528 // Find matching effect in timeline (if any)
1529 for (auto e : effects)
1530 {
1531 if (e->Id() == effect_id) {
1532 existing_effect = e;
1533 break; // effect found, exit loop
1534 }
1535 }
1536 break; // id found, exit loop
1537 }
1538 }
1539 }
1540
1541 // Now that we found the effect, apply the change to it
1542 if (existing_effect || change_type == "insert") {
1543 // Apply change to effect
1544 apply_json_to_effects(change, existing_effect);
1545 }
1546}
1547
1548// Apply JSON diff to effects (if you already know which effect needs to be updated)
1549void Timeline::apply_json_to_effects(Json::Value change, EffectBase* existing_effect) {
1550
1551 // Get key and type of change
1552 std::string change_type = change["type"].asString();
1553
1554 // Calculate start and end frames that this impacts, and remove those frames from the cache
1555 if (!change["value"].isArray() && !change["value"]["position"].isNull()) {
1556 int64_t new_starting_frame = (change["value"]["position"].asDouble() * info.fps.ToDouble()) + 1;
1557 int64_t new_ending_frame = ((change["value"]["position"].asDouble() + change["value"]["end"].asDouble() - change["value"]["start"].asDouble()) * info.fps.ToDouble()) + 1;
1558 final_cache->Remove(new_starting_frame - 8, new_ending_frame + 8);
1559 }
1560
1561 // Determine type of change operation
1562 if (change_type == "insert") {
1563
1564 // Determine type of effect
1565 std::string effect_type = change["value"]["type"].asString();
1566
1567 // Create Effect
1568 EffectBase *e = NULL;
1569
1570 // Init the matching effect object
1571 if ( (e = EffectInfo().CreateEffect(effect_type)) ) {
1572
1573 // Keep track of allocated effect objects
1574 allocated_effects.insert(e);
1575
1576 // Load Json into Effect
1577 e->SetJsonValue(change["value"]);
1578
1579 // Add Effect to Timeline
1580 AddEffect(e);
1581 }
1582
1583 } else if (change_type == "update") {
1584
1585 // Update existing effect
1586 if (existing_effect) {
1587
1588 // Calculate start and end frames that this impacts, and remove those frames from the cache
1589 int64_t old_starting_frame = (existing_effect->Position() * info.fps.ToDouble()) + 1;
1590 int64_t old_ending_frame = ((existing_effect->Position() + existing_effect->Duration()) * info.fps.ToDouble()) + 1;
1591 final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8);
1592
1593 // Update effect properties from JSON
1594 existing_effect->SetJsonValue(change["value"]);
1595 }
1596
1597 } else if (change_type == "delete") {
1598
1599 // Remove existing effect
1600 if (existing_effect) {
1601
1602 // Calculate start and end frames that this impacts, and remove those frames from the cache
1603 int64_t old_starting_frame = (existing_effect->Position() * info.fps.ToDouble()) + 1;
1604 int64_t old_ending_frame = ((existing_effect->Position() + existing_effect->Duration()) * info.fps.ToDouble()) + 1;
1605 final_cache->Remove(old_starting_frame - 8, old_ending_frame + 8);
1606
1607 // Remove effect from timeline
1608 RemoveEffect(existing_effect);
1609 }
1610
1611 }
1612
1613 // Re-Sort Effects (since they likely changed)
1614 sort_effects();
1615}
1616
1617// Apply JSON diff to timeline properties
1618void Timeline::apply_json_to_timeline(Json::Value change) {
1619 bool cache_dirty = true;
1620
1621 // Get key and type of change
1622 std::string change_type = change["type"].asString();
1623 std::string root_key = change["key"][(uint)0].asString();
1624 std::string sub_key = "";
1625 if (change["key"].size() >= 2)
1626 sub_key = change["key"][(uint)1].asString();
1627
1628 // Determine type of change operation
1629 if (change_type == "insert" || change_type == "update") {
1630
1631 // INSERT / UPDATE
1632 // Check for valid property
1633 if (root_key == "color")
1634 // Set color
1635 color.SetJsonValue(change["value"]);
1636 else if (root_key == "viewport_scale")
1637 // Set viewport scale
1638 viewport_scale.SetJsonValue(change["value"]);
1639 else if (root_key == "viewport_x")
1640 // Set viewport x offset
1641 viewport_x.SetJsonValue(change["value"]);
1642 else if (root_key == "viewport_y")
1643 // Set viewport y offset
1644 viewport_y.SetJsonValue(change["value"]);
1645 else if (root_key == "duration") {
1646 // Update duration of timeline
1647 info.duration = change["value"].asDouble();
1648 info.video_length = info.fps.ToFloat() * info.duration;
1649
1650 // We don't want to clear cache for duration adjustments
1651 cache_dirty = false;
1652 }
1653 else if (root_key == "width") {
1654 // Set width
1655 info.width = change["value"].asInt();
1656 preview_width = info.width;
1657 }
1658 else if (root_key == "height") {
1659 // Set height
1660 info.height = change["value"].asInt();
1661 preview_height = info.height;
1662 }
1663 else if (root_key == "fps" && sub_key == "" && change["value"].isObject()) {
1664 // Set fps fraction
1665 if (!change["value"]["num"].isNull())
1666 info.fps.num = change["value"]["num"].asInt();
1667 if (!change["value"]["den"].isNull())
1668 info.fps.den = change["value"]["den"].asInt();
1669 }
1670 else if (root_key == "fps" && sub_key == "num")
1671 // Set fps.num
1672 info.fps.num = change["value"].asInt();
1673 else if (root_key == "fps" && sub_key == "den")
1674 // Set fps.den
1675 info.fps.den = change["value"].asInt();
1676 else if (root_key == "display_ratio" && sub_key == "" && change["value"].isObject()) {
1677 // Set display_ratio fraction
1678 if (!change["value"]["num"].isNull())
1679 info.display_ratio.num = change["value"]["num"].asInt();
1680 if (!change["value"]["den"].isNull())
1681 info.display_ratio.den = change["value"]["den"].asInt();
1682 }
1683 else if (root_key == "display_ratio" && sub_key == "num")
1684 // Set display_ratio.num
1685 info.display_ratio.num = change["value"].asInt();
1686 else if (root_key == "display_ratio" && sub_key == "den")
1687 // Set display_ratio.den
1688 info.display_ratio.den = change["value"].asInt();
1689 else if (root_key == "pixel_ratio" && sub_key == "" && change["value"].isObject()) {
1690 // Set pixel_ratio fraction
1691 if (!change["value"]["num"].isNull())
1692 info.pixel_ratio.num = change["value"]["num"].asInt();
1693 if (!change["value"]["den"].isNull())
1694 info.pixel_ratio.den = change["value"]["den"].asInt();
1695 }
1696 else if (root_key == "pixel_ratio" && sub_key == "num")
1697 // Set pixel_ratio.num
1698 info.pixel_ratio.num = change["value"].asInt();
1699 else if (root_key == "pixel_ratio" && sub_key == "den")
1700 // Set pixel_ratio.den
1701 info.pixel_ratio.den = change["value"].asInt();
1702
1703 else if (root_key == "sample_rate")
1704 // Set sample rate
1705 info.sample_rate = change["value"].asInt();
1706 else if (root_key == "channels")
1707 // Set channels
1708 info.channels = change["value"].asInt();
1709 else if (root_key == "channel_layout")
1710 // Set channel layout
1711 info.channel_layout = (ChannelLayout) change["value"].asInt();
1712 else
1713 // Error parsing JSON (or missing keys)
1714 throw InvalidJSONKey("JSON change key is invalid", change.toStyledString());
1715
1716
1717 } else if (change["type"].asString() == "delete") {
1718
1719 // DELETE / RESET
1720 // Reset the following properties (since we can't delete them)
1721 if (root_key == "color") {
1722 color = Color();
1723 color.red = Keyframe(0.0);
1724 color.green = Keyframe(0.0);
1725 color.blue = Keyframe(0.0);
1726 }
1727 else if (root_key == "viewport_scale")
1728 viewport_scale = Keyframe(1.0);
1729 else if (root_key == "viewport_x")
1730 viewport_x = Keyframe(0.0);
1731 else if (root_key == "viewport_y")
1732 viewport_y = Keyframe(0.0);
1733 else
1734 // Error parsing JSON (or missing keys)
1735 throw InvalidJSONKey("JSON change key is invalid", change.toStyledString());
1736
1737 }
1738
1739 if (cache_dirty) {
1740 // Clear entire cache
1741 ClearAllCache();
1742 }
1743}
1744
1745// Clear all caches
1747
1748 // Clear primary cache
1749 if (final_cache) {
1750 final_cache->Clear();
1751 }
1752
1753 // Loop through all clips
1754 try {
1755 for (const auto clip : clips) {
1756 // Clear cache on clip and reader if present
1757 if (clip->Reader()) {
1758 if (auto rc = clip->Reader()->GetCache())
1759 rc->Clear();
1760
1761 // Clear nested Reader (if deep clear requested)
1762 if (deep && clip->Reader()->Name() == "FrameMapper") {
1763 FrameMapper *nested_reader = static_cast<FrameMapper *>(clip->Reader());
1764 if (nested_reader->Reader()) {
1765 if (auto nc = nested_reader->Reader()->GetCache())
1766 nc->Clear();
1767 }
1768 }
1769 }
1770
1771 // Clear clip cache
1772 if (auto cc = clip->GetCache())
1773 cc->Clear();
1774 }
1775 } catch (const ReaderClosed & e) {
1776 // ...
1777 }
1778}
1779
1780// Set Max Image Size (used for performance optimization). Convenience function for setting
1781// Settings::Instance()->MAX_WIDTH and Settings::Instance()->MAX_HEIGHT.
1782void Timeline::SetMaxSize(int width, int height) {
1783 // Maintain aspect ratio regardless of what size is passed in
1784 QSize display_ratio_size = QSize(info.width, info.height);
1785 QSize proposed_size = QSize(std::min(width, info.width), std::min(height, info.height));
1786
1787 // Scale QSize up to proposed size
1788 display_ratio_size.scale(proposed_size, Qt::KeepAspectRatio);
1789
1790 // Update preview settings
1791 preview_width = display_ratio_size.width();
1792 preview_height = display_ratio_size.height();
1793}
Header file for CacheBase class.
Header file for CacheDisk class.
Header file for CacheMemory class.
Header file for CrashHandler class.
Header file for all Exception classes.
Header file for the FrameMapper class.
Header file for Timeline class.
All cache managers in libopenshot are based on this CacheBase class.
Definition CacheBase.h:35
This class is a memory-based cache manager for Frame objects.
Definition CacheMemory.h:29
float Duration() const
Get the length of this clip (in seconds).
Definition ClipBase.h:90
virtual std::shared_ptr< openshot::Frame > GetFrame(int64_t frame_number)=0
This method is required for all derived classes of ClipBase, and returns a new openshot::Frame object...
float Position() const
Get position on timeline (in seconds).
Definition ClipBase.h:86
virtual openshot::TimelineBase * ParentTimeline()
Get the associated Timeline pointer (if any).
Definition ClipBase.h:91
This class represents a clip (used to arrange readers on the timeline).
Definition Clip.h:89
openshot::VolumeMixType mixing
What strategy should be followed when mixing audio with other clips.
Definition Clip.h:180
openshot::Keyframe channel_filter
A number representing an audio channel to filter (clears all other channels).
Definition Clip.h:347
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
Definition Clip.cpp:1014
openshot::Keyframe has_audio
An optional override to determine if this clip has audio (-1=undefined, 0=no, 1=yes).
Definition Clip.h:351
openshot::TimelineBase * ParentTimeline() override
Get the associated Timeline pointer (if any).
Definition Clip.h:294
std::list< openshot::EffectBase * > Effects()
Return the list of effects on the timeline.
Definition Clip.h:243
openshot::Keyframe volume
Curve representing the volume (0 to 1).
Definition Clip.h:331
openshot::Keyframe channel_mapping
A number representing an audio channel to output (only works when filtering a channel).
Definition Clip.h:348
void Reader(openshot::ReaderBase *new_reader)
Set the current reader.
Definition Clip.cpp:336
static CrashHandler * Instance()
This abstract class is the base class, used by all effects in libopenshot.
Definition EffectBase.h:54
virtual void SetJsonValue(const Json::Value root)
Load Json::Value into this object.
This class returns a listing of all effects supported by libopenshot.
Definition EffectInfo.h:29
This class represents a fraction.
Definition Fraction.h:30
float ToFloat()
Return this fraction as a float (i.e. 1/2 = 0.5).
Definition Fraction.cpp:35
double ToDouble() const
Return this fraction as a double (i.e. 1/2 = 0.5).
Definition Fraction.cpp:40
Fraction Reciprocal() const
Return the reciprocal as a Fraction.
Definition Fraction.cpp:78
This class creates a mapping between 2 different frame rates, applying a specific pull-down technique...
void ChangeMapping(Fraction target_fps, PulldownType pulldown, int target_sample_rate, int target_channels, ChannelLayout target_channel_layout)
Change frame rate or audio mapping details.
ReaderBase * Reader()
Get the current reader.
void Close() override
Close the openshot::FrameMapper and internal reader.
int GetSamplesPerFrame(openshot::Fraction fps, int sample_rate, int channels)
Calculate the # of samples per video frame (for the current frame number).
Definition Frame.cpp:484
Exception for files that can not be found or opened.
Definition Exceptions.h:188
Exception for invalid JSON.
Definition Exceptions.h:218
A Keyframe is a collection of Point instances, which is used to vary a number or property over time.
Definition KeyFrame.h:53
int GetInt(int64_t index) const
Get the rounded INT value at a specific index.
Definition KeyFrame.cpp:282
double GetValue(int64_t index) const
Get the value at a specific index.
Definition KeyFrame.cpp:258
This abstract class is the base class, used by all readers in libopenshot.
Definition ReaderBase.h:76
openshot::ReaderInfo info
Information about the current media file.
Definition ReaderBase.h:88
virtual void SetJsonValue(const Json::Value root)=0
Load Json::Value into this object.
virtual Json::Value JsonValue() const =0
Generate Json::Value for this object.
std::recursive_mutex getFrameMutex
Mutex for multiple threads.
Definition ReaderBase.h:79
virtual openshot::CacheBase * GetCache()=0
Get the cache object used by this reader (note: not all readers use cache).
openshot::ClipBase * clip
Pointer to the parent clip instance (if any).
Definition ReaderBase.h:80
Exception when a reader is closed, and a frame is requested.
Definition Exceptions.h:364
This class is contains settings used by libopenshot (and can be safely toggled at any point).
Definition Settings.h:26
std::string PATH_OPENSHOT_INSTALL
Definition Settings.h:111
static Settings * Instance()
Create or get an instance of this logger singleton (invoke the class with this method).
Definition Settings.cpp:23
int preview_height
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
int preview_width
Optional preview width of timeline image. If your preview window is smaller than the timeline,...
void AddTrackedObject(std::shared_ptr< openshot::TrackedObjectBase > trackedObject)
Add to the tracked_objects map a pointer to a tracked object (TrackedObjectBBox).
Definition Timeline.cpp:224
Json::Value JsonValue() const override
Generate Json::Value for this object.
openshot::Keyframe viewport_scale
Curve representing the scale of the viewport (0 to 100).
Definition Timeline.h:323
void ApplyJsonDiff(std::string value)
Apply a special formatted JSON object, which represents a change to the timeline (add,...
openshot::EffectBase * GetClipEffect(const std::string &id)
Look up a clip effect by ID.
Definition Timeline.cpp:436
void AddClip(openshot::Clip *clip)
Add an openshot::Clip to the timeline.
Definition Timeline.cpp:333
virtual ~Timeline()
Definition Timeline.cpp:207
std::list< openshot::EffectBase * > ClipEffects() const
Return the list of effects on all clips.
Definition Timeline.cpp:449
std::list< std::string > GetTrackedObjectsIds() const
Return the ID's of the tracked objects as a list of strings.
Definition Timeline.cpp:259
std::string Json() const override
Generate JSON string of this object.
int64_t GetMaxFrame()
Look up the end frame number of the latest element on the timeline.
Definition Timeline.cpp:474
double GetMinTime()
Look up the position/start time of the first timeline element.
Definition Timeline.cpp:490
std::shared_ptr< openshot::Frame > GetFrame(int64_t requested_frame) override
Definition Timeline.cpp:937
void ApplyMapperToClips()
Apply the timeline's framerate and samplerate to all clips.
Definition Timeline.cpp:522
openshot::Color color
Background color of timeline canvas.
Definition Timeline.h:328
std::string GetTrackedObjectValues(std::string id, int64_t frame_number) const
Return the trackedObject's properties as a JSON string.
Definition Timeline.cpp:275
Timeline(int width, int height, openshot::Fraction fps, int sample_rate, int channels, openshot::ChannelLayout channel_layout)
Constructor for the timeline (which configures the default frame properties).
Definition Timeline.cpp:31
std::shared_ptr< openshot::TrackedObjectBase > GetTrackedObject(std::string id) const
Return tracked object pointer by it's id.
Definition Timeline.cpp:242
int64_t GetMinFrame()
Look up the start frame number of the first element on the timeline (first frame is 1).
Definition Timeline.cpp:482
openshot::EffectBase * GetEffect(const std::string &id)
Look up a timeline effect by ID.
Definition Timeline.cpp:425
void SetJsonValue(const Json::Value root) override
Load Json::Value into this object.
openshot::Clip * GetClip(const std::string &id)
Look up a single clip by ID.
Definition Timeline.cpp:413
void ClearAllCache(bool deep=false)
void AddEffect(openshot::EffectBase *effect)
Add an effect to the timeline.
Definition Timeline.cpp:359
void SetCache(openshot::CacheBase *new_cache)
void Clear()
Clear all clips, effects, and frame mappers from timeline (and free memory).
Definition Timeline.cpp:857
openshot::Keyframe viewport_x
Curve representing the x coordinate for the viewport.
Definition Timeline.h:324
void RemoveClip(openshot::Clip *clip)
Remove an openshot::Clip from the timeline.
Definition Timeline.cpp:394
void SetMaxSize(int width, int height)
double GetMaxTime()
Look up the end time of the latest timeline element.
Definition Timeline.cpp:468
void RemoveEffect(openshot::EffectBase *effect)
Remove an effect from the timeline.
Definition Timeline.cpp:375
std::shared_ptr< openshot::Frame > apply_effects(std::shared_ptr< openshot::Frame > frame, int64_t timeline_frame_number, int layer, TimelineInfoStruct *options)
Apply global/timeline effects to the source frame (if any).
Definition Timeline.cpp:546
void Open() override
Open the reader (and start consuming resources).
Definition Timeline.cpp:925
void SetJson(const std::string value) override
Load JSON string into this object.
openshot::Keyframe viewport_y
Curve representing the y coordinate for the viewport.
Definition Timeline.h:325
void Close() override
Close the timeline reader (and any resources it was consuming).
Definition Timeline.cpp:903
void AppendDebugMethod(std::string method_name, std::string arg1_name="", float arg1_value=-1.0, std::string arg2_name="", float arg2_value=-1.0, std::string arg3_name="", float arg3_value=-1.0, std::string arg4_name="", float arg4_value=-1.0, std::string arg5_name="", float arg5_value=-1.0, std::string arg6_name="", float arg6_value=-1.0)
Append debug information.
static ZmqLogger * Instance()
Create or get an instance of this logger singleton (invoke the class with this method).
Definition ZmqLogger.cpp:35
This namespace is the default namespace for all code in the openshot library.
Definition Compressor.h:29
@ PULLDOWN_NONE
Do not apply pull-down techniques, just repeat or skip entire frames.
Definition FrameMapper.h:46
ChannelLayout
This enumeration determines the audio channel layout (such as stereo, mono, 5 point surround,...
@ VOLUME_MIX_AVERAGE
Evenly divide the overlapping clips volume keyframes, so that the sum does not exceed 100%.
Definition Enums.h:70
@ VOLUME_MIX_REDUCE
Reduce volume by about %25, and then mix (louder, but could cause pops if the sum exceeds 100%).
Definition Enums.h:71
const Json::Value stringToJson(const std::string value)
Definition Json.cpp:16
This struct holds the information of a bounding-box.
float cy
y-coordinate of the bounding box center
float height
bounding box height
float cx
x-coordinate of the bounding box center
float width
bounding box width
float angle
bounding box rotation angle [degrees]
This struct contains info about a media file, such as height, width, frames per second,...
Definition ReaderBase.h:39
int channels
The number of audio channels used in the audio stream.
Definition ReaderBase.h:61
openshot::Fraction fps
Frames per second, as a fraction (i.e. 24/1 = 24 fps).
Definition ReaderBase.h:48
openshot::ChannelLayout channel_layout
The channel layout (mono, stereo, 5 point surround, etc...).
Definition ReaderBase.h:62
int sample_rate
The number of audio samples per second (44100 is a common sample rate).
Definition ReaderBase.h:60
This struct contains info about the current Timeline clip instance.
bool is_before_clip_keyframes
Is this before clip keyframes are applied.
bool is_top_clip
Is clip on top (if overlapping another clip).