@@ -34,7 +34,7 @@ using namespace openshot;
3434
3535// Default Constructor for the timeline (which sets the canvas width and height)
3636Timeline::Timeline (int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout) :
37- is_open(false ), auto_map_clips(true ), managed_cache(true )
37+ is_open(false ), auto_map_clips(true ), managed_cache(true ), path( " " )
3838{
3939 // Create CrashHandler and Attach (incase of errors)
4040 CrashHandler::Instance ();
@@ -64,6 +64,8 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha
6464 info.display_ratio = openshot::Fraction (width, height);
6565 info.display_ratio .Reduce ();
6666 info.pixel_ratio = openshot::Fraction (1 , 1 );
67+ info.acodec = " openshot::timeline" ;
68+ info.vcodec = " openshot::timeline" ;
6769
6870 // Init max image size
6971 SetMaxSize (info.width , info.height );
@@ -73,6 +75,133 @@ Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int cha
7375 final_cache->SetMaxBytesFromInfo (OPEN_MP_NUM_PROCESSORS * 2 , info.width , info.height , info.sample_rate , info.channels );
7476}
7577
78+ // Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline)
79+ Timeline::Timeline (std::string projectPath, bool convert_absolute_paths) :
80+ is_open(false ), auto_map_clips(true ), managed_cache(true ), path(projectPath) {
81+
82+ // Create CrashHandler and Attach (incase of errors)
83+ CrashHandler::Instance ();
84+
85+ // Init final cache as NULL (will be created after loading json)
86+ final_cache = NULL ;
87+
88+ // Init viewport size (curve based, because it can be animated)
89+ viewport_scale = Keyframe (100.0 );
90+ viewport_x = Keyframe (0.0 );
91+ viewport_y = Keyframe (0.0 );
92+
93+ // Init background color
94+ color.red = Keyframe (0.0 );
95+ color.green = Keyframe (0.0 );
96+ color.blue = Keyframe (0.0 );
97+
98+ // Check if path exists
99+ QFileInfo filePath (QString::fromStdString (path));
100+ if (!filePath.exists ()) {
101+ throw InvalidFile (" File could not be opened." , path);
102+ }
103+
104+ // Check OpenShot Install Path exists
105+ Settings *s = Settings::Instance ();
106+ QDir openshotPath (QString::fromStdString (s->PATH_OPENSHOT_INSTALL ));
107+ if (!openshotPath.exists ()) {
108+ throw InvalidFile (" PATH_OPENSHOT_INSTALL could not be found." , s->PATH_OPENSHOT_INSTALL );
109+ }
110+ QDir openshotTransPath (openshotPath.filePath (" transitions" ));
111+ if (!openshotTransPath.exists ()) {
112+ throw InvalidFile (" PATH_OPENSHOT_INSTALL/transitions could not be found." , openshotTransPath.path ().toStdString ());
113+ }
114+
115+ // Determine asset path
116+ QString asset_name = filePath.baseName ().left (30 ) + " _assets" ;
117+ QDir asset_folder (filePath.dir ().filePath (asset_name));
118+ if (!asset_folder.exists ()) {
119+ // Create directory if needed
120+ asset_folder.mkpath (" ." );
121+ }
122+
123+ // Load UTF-8 project file into QString
124+ QFile projectFile (QString::fromStdString (path));
125+ projectFile.open (QFile::ReadOnly);
126+ QString projectContents = QString::fromUtf8 (projectFile.readAll ());
127+
128+ // Convert all relative paths into absolute paths (if requested)
129+ if (convert_absolute_paths) {
130+
131+ // Find all "image" or "path" references in JSON (using regex). Must loop through match results
132+ // due to our path matching needs, which are not possible with the QString::replace() function.
133+ QRegularExpression allPathsRegex (QStringLiteral (" \" (image|path)\" :.*?\" (.*?)\" " ));
134+ std::vector<QRegularExpressionMatch> matchedPositions;
135+ QRegularExpressionMatchIterator i = allPathsRegex.globalMatch (projectContents);
136+ while (i.hasNext ()) {
137+ QRegularExpressionMatch match = i.next ();
138+ if (match.hasMatch ()) {
139+ // Push all match objects into a vector (so we can reverse them later)
140+ matchedPositions.push_back (match);
141+ }
142+ }
143+
144+ // Reverse the matches (bottom of file to top, so our replacements don't break our match positions)
145+ std::vector<QRegularExpressionMatch>::reverse_iterator itr;
146+ for (itr = matchedPositions.rbegin (); itr != matchedPositions.rend (); itr++) {
147+ QRegularExpressionMatch match = *itr;
148+ QString relativeKey = match.captured (1 ); // image or path
149+ QString relativePath = match.captured (2 ); // relative file path
150+ QString absolutePath = " " ;
151+
152+ // Find absolute path of all path, image (including special replacements of @assets and @transitions)
153+ if (relativePath.startsWith (" @assets" )) {
154+ absolutePath = QFileInfo (asset_folder.absoluteFilePath (relativePath.replace (" @assets" , " ." ))).canonicalFilePath ();
155+ } else if (relativePath.startsWith (" @transitions" )) {
156+ absolutePath = QFileInfo (openshotTransPath.absoluteFilePath (relativePath.replace (" @transitions" , " ." ))).canonicalFilePath ();
157+ } else {
158+ absolutePath = QFileInfo (filePath.absoluteDir ().absoluteFilePath (relativePath)).canonicalFilePath ();
159+ }
160+
161+ // Replace path in JSON content, if an absolute path was successfully found
162+ if (!absolutePath.isEmpty ()) {
163+ projectContents.replace (match.capturedStart (0 ), match.capturedLength (0 ), " \" " + relativeKey + " \" : \" " + absolutePath + " \" " );
164+ }
165+ }
166+ // Clear matches
167+ matchedPositions.clear ();
168+ }
169+
170+ // Set JSON of project
171+ SetJson (projectContents.toStdString ());
172+
173+ // Calculate valid duration and set has_audio and has_video
174+ // based on content inside this Timeline's clips.
175+ float calculated_duration = 0.0 ;
176+ for (auto clip : clips)
177+ {
178+ float clip_last_frame = clip->Position () + clip->Duration ();
179+ if (clip_last_frame > calculated_duration)
180+ calculated_duration = clip_last_frame;
181+ if (clip->Reader () && clip->Reader ()->info .has_audio )
182+ info.has_audio = true ;
183+ if (clip->Reader () && clip->Reader ()->info .has_video )
184+ info.has_video = true ;
185+
186+ }
187+ info.video_length = calculated_duration * info.fps .ToFloat ();
188+ info.duration = calculated_duration;
189+
190+ // Init FileInfo settings
191+ info.acodec = " openshot::timeline" ;
192+ info.vcodec = " openshot::timeline" ;
193+ info.video_timebase = info.fps .Reciprocal ();
194+ info.has_video = true ;
195+ info.has_audio = true ;
196+
197+ // Init max image size
198+ SetMaxSize (info.width , info.height );
199+
200+ // Init cache
201+ final_cache = new CacheMemory ();
202+ final_cache->SetMaxBytesFromInfo (OPEN_MP_NUM_PROCESSORS * 2 , info.width , info.height , info.sample_rate , info.channels );
203+ }
204+
76205Timeline::~Timeline () {
77206 if (is_open)
78207 // Auto Close if not already
@@ -706,7 +835,8 @@ void Timeline::Close()
706835 is_open = false ;
707836
708837 // Clear cache
709- final_cache->Clear ();
838+ if (final_cache)
839+ final_cache->Clear ();
710840}
711841
712842// Open the reader (and start consuming resources)
@@ -984,6 +1114,7 @@ Json::Value Timeline::JsonValue() const {
9841114 root[" viewport_x" ] = viewport_x.JsonValue ();
9851115 root[" viewport_y" ] = viewport_y.JsonValue ();
9861116 root[" color" ] = color.JsonValue ();
1117+ root[" path" ] = path;
9871118
9881119 // Add array of clips
9891120 root[" clips" ] = Json::Value (Json::arrayValue);
@@ -1037,6 +1168,10 @@ void Timeline::SetJsonValue(const Json::Value root) {
10371168 // Set parent data
10381169 ReaderBase::SetJsonValue (root);
10391170
1171+ // Set data from Json (if key is found)
1172+ if (!root[" path" ].isNull ())
1173+ path = root[" path" ].asString ();
1174+
10401175 if (!root[" clips" ].isNull ()) {
10411176 // Clear existing clips
10421177 clips.clear ();
0 commit comments