Skip to content

Commit fd620f4

Browse files
committed
Updating rescale_keyframes() method to not modify the current project. This allows the Export dialog to make a copy of the rescaled keyframes, and not modify the active project when exporting to a different FPS. Now Export feels much safer, because it doesn't modify project data.
1 parent 0cf6539 commit fd620f4

File tree

3 files changed

+62
-67
lines changed

3 files changed

+62
-67
lines changed

src/classes/project_data.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,17 @@ def scale_keyframe_value(self, original_value, scale_factor):
399399
return round(original_value * scale_factor)
400400

401401
def rescale_keyframes(self, scale_factor):
402-
"""Adjust all keyframe coordinates from previous FPS to new FPS (using a scale factor)"""
402+
"""Adjust all keyframe coordinates from previous FPS to new FPS (using a scale factor)
403+
and return scaled project data without modifing the current project."""
403404
log.info('Scale all keyframes by a factor of %s' % scale_factor)
404405

406+
# Create copy of active project data
407+
data = copy.deepcopy(self._data)
408+
409+
# Rescale the the copied project data
405410
# Loop through all clips (and look for Keyframe objects)
406411
# Scale the X coordinate by factor (which represents the frame #)
407-
for clip in self._data.get('clips', []):
412+
for clip in data.get('clips', []):
408413
for attribute in clip:
409414
if type(clip.get(attribute)) == dict and "Points" in clip.get(attribute):
410415
for point in clip.get(attribute).get("Points"):
@@ -429,7 +434,7 @@ def rescale_keyframes(self, scale_factor):
429434

430435
# Loop through all effects/transitions (and look for Keyframe objects)
431436
# Scale the X coordinate by factor (which represents the frame #)
432-
for effect in self._data.get('effects', []):
437+
for effect in data.get('effects', []):
433438
for attribute in effect:
434439
if type(effect.get(attribute)) == dict and "Points" in effect.get(attribute):
435440
for point in effect.get(attribute).get("Points"):
@@ -441,9 +446,8 @@ def rescale_keyframes(self, scale_factor):
441446
if "co" in point:
442447
point["co"]["X"] = self.scale_keyframe_value(point["co"].get("X", 0.0), scale_factor)
443448

444-
# Get app, and distribute all project data through update manager
445-
from classes.app import get_app
446-
get_app().updates.load(self._data)
449+
# return the copied and scaled project data
450+
return data
447451

448452
def read_legacy_project_file(self, file_path):
449453
"""Attempt to read a legacy version 1.x openshot project file"""

src/windows/export.py

+41-56
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ def __init__(self):
6161
ui_util.init_ui(self)
6262

6363
# get translations
64-
app = get_app()
65-
_ = app._tr
64+
_ = get_app()._tr
6665

6766
# Get settings
6867
self.s = settings.get_settings()
@@ -92,10 +91,10 @@ def __init__(self):
9291
self.delayed_fps_timer.stop()
9392

9493
# Pause playback (to prevent crash since we are fixing to change the timeline's max size)
95-
app.window.actionPlay_trigger(None, force="pause")
94+
get_app().window.actionPlay_trigger(None, force="pause")
9695

9796
# Clear timeline preview cache (to get more available memory)
98-
app.window.timeline_sync.timeline.ClearAllCache()
97+
get_app().window.timeline_sync.timeline.ClearAllCache()
9998

10099
# Hide audio channels
101100
self.lblChannels.setVisible(False)
@@ -106,41 +105,38 @@ def __init__(self):
106105
openshot.Settings.Instance().HIGH_QUALITY_SCALING = True
107106

108107
# Get the original timeline settings
109-
width = app.window.timeline_sync.timeline.info.width
110-
height = app.window.timeline_sync.timeline.info.height
111-
fps = app.window.timeline_sync.timeline.info.fps
112-
sample_rate = app.window.timeline_sync.timeline.info.sample_rate
113-
channels = app.window.timeline_sync.timeline.info.channels
114-
channel_layout = app.window.timeline_sync.timeline.info.channel_layout
115-
116-
# No keyframe rescaling has happened yet (due to differences in FPS)
117-
self.keyframes_rescaled = False
108+
width = get_app().window.timeline_sync.timeline.info.width
109+
height = get_app().window.timeline_sync.timeline.info.height
110+
fps = get_app().window.timeline_sync.timeline.info.fps
111+
sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
112+
channels = get_app().window.timeline_sync.timeline.info.channels
113+
channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
118114

119115
# Create new "export" openshot.Timeline object
120116
self.timeline = openshot.Timeline(width, height, openshot.Fraction(fps.num, fps.den),
121117
sample_rate, channels, channel_layout)
122118
# Init various properties
123-
self.timeline.info.channel_layout = app.window.timeline_sync.timeline.info.channel_layout
124-
self.timeline.info.has_audio = app.window.timeline_sync.timeline.info.has_audio
125-
self.timeline.info.has_video = app.window.timeline_sync.timeline.info.has_video
126-
self.timeline.info.video_length = app.window.timeline_sync.timeline.info.video_length
127-
self.timeline.info.duration = app.window.timeline_sync.timeline.info.duration
128-
self.timeline.info.sample_rate = app.window.timeline_sync.timeline.info.sample_rate
129-
self.timeline.info.channels = app.window.timeline_sync.timeline.info.channels
119+
self.timeline.info.channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
120+
self.timeline.info.has_audio = get_app().window.timeline_sync.timeline.info.has_audio
121+
self.timeline.info.has_video = get_app().window.timeline_sync.timeline.info.has_video
122+
self.timeline.info.video_length = get_app().window.timeline_sync.timeline.info.video_length
123+
self.timeline.info.duration = get_app().window.timeline_sync.timeline.info.duration
124+
self.timeline.info.sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
125+
self.timeline.info.channels = get_app().window.timeline_sync.timeline.info.channels
130126

131127
# Load the "export" Timeline reader with the JSON from the real timeline
132-
json_timeline = json.dumps(app.project._data)
128+
json_timeline = json.dumps(get_app().project._data)
133129
self.timeline.SetJson(json_timeline)
134130

135131
# Open the "export" Timeline reader
136132
self.timeline.Open()
137133

138134
# Default export path
139135
recommended_path = os.path.join(info.HOME_PATH)
140-
if app.project.current_filepath:
141-
recommended_path = os.path.dirname(app.project.current_filepath)
136+
if get_app().project.current_filepath:
137+
recommended_path = os.path.dirname(get_app().project.current_filepath)
142138

143-
export_path = app.project.get("export_path")
139+
export_path = get_app().project.get("export_path")
144140
if export_path and os.path.exists(export_path):
145141
# Use last selected export path
146142
self.txtExportFolder.setText(export_path)
@@ -149,13 +145,13 @@ def __init__(self):
149145
self.txtExportFolder.setText(recommended_path)
150146

151147
# Is this a saved project?
152-
if not app.project.current_filepath:
148+
if not get_app().project.current_filepath:
153149
# Not saved yet
154150
self.txtFileName.setText(_("Untitled Project"))
155151
else:
156152
# Yes, project is saved
157153
# Get just the filename
158-
filename = os.path.basename(app.project.current_filepath)
154+
filename = os.path.basename(get_app().project.current_filepath)
159155
filename = os.path.splitext(filename)[0]
160156
self.txtFileName.setText(filename)
161157

@@ -191,7 +187,7 @@ def __init__(self):
191187
self.cboSimpleQuality.currentIndexChanged.connect(
192188
functools.partial(self.cboSimpleQuality_index_changed, self.cboSimpleQuality))
193189
self.cboChannelLayout.currentIndexChanged.connect(self.updateChannels)
194-
app.window.ExportFrame.connect(self.updateProgressBar)
190+
get_app().window.ExportFrame.connect(self.updateProgressBar)
195191

196192
# ********* Advanced Profile List **********
197193
# Loop through profiles
@@ -220,7 +216,7 @@ def __init__(self):
220216
self.cboProfile.addItem(self.getProfileName(self.getProfilePath(profile_name)), self.getProfilePath(profile_name))
221217

222218
# Set default (if it matches the project)
223-
if app.project.get(['profile']) in profile_name:
219+
if get_app().project.get(['profile']) in profile_name:
224220
self.selected_profile_index = box_index
225221

226222
# increment item counter
@@ -251,7 +247,7 @@ def __init__(self):
251247

252248

253249
# Populate all profiles
254-
self.populateAllProfiles(app.project.get(['profile']))
250+
self.populateAllProfiles(get_app().project.get(['profile']))
255251

256252
# Connect framerate signals
257253
self.txtFrameRateNum.valueChanged.connect(self.updateFrameRate)
@@ -368,8 +364,7 @@ def cboSimpleProjectType_index_changed(self, widget, index):
368364
self.cboSimpleTarget.clear()
369365

370366
# get translations
371-
app = get_app()
372-
_ = app._tr
367+
_ = get_app()._tr
373368

374369
# parse the xml files and get targets that match the project type
375370
project_types = []
@@ -424,8 +419,7 @@ def cboProfile_index_changed(self, widget, index):
424419
log.info(selected_profile_path)
425420

426421
# get translations
427-
app = get_app()
428-
_ = app._tr
422+
_ = get_app()._tr
429423

430424
# Load profile
431425
profile = openshot.Profile(selected_profile_path)
@@ -454,8 +448,7 @@ def cboSimpleTarget_index_changed(self, widget, index):
454448
log.info(selected_target)
455449

456450
# get translations
457-
app = get_app()
458-
_ = app._tr
451+
_ = get_app()._tr
459452

460453
# don't do anything if the combo has been cleared
461454
if selected_target:
@@ -594,8 +587,7 @@ def cboSimpleQuality_index_changed(self, widget, index):
594587
log.info(selected_quality)
595588

596589
# get translations
597-
app = get_app()
598-
_ = app._tr
590+
_ = get_app()._tr
599591

600592
# Set the video and audio bitrates
601593
if selected_quality:
@@ -606,8 +598,7 @@ def btnBrowse_clicked(self):
606598
log.info("btnBrowse_clicked")
607599

608600
# get translations
609-
app = get_app()
610-
_ = app._tr
601+
_ = get_app()._tr
611602

612603
# update export folder path
613604
file_path = QFileDialog.getExistingDirectory(self, _("Choose a Folder..."), self.txtExportFolder.text())
@@ -677,8 +668,7 @@ def accept(self):
677668
""" Start exporting video """
678669

679670
# get translations
680-
app = get_app()
681-
_ = app._tr
671+
_ = get_app()._tr
682672

683673
# Init progress bar
684674
self.progressExportVideo.setMinimum(self.txtStartFrame.value())
@@ -782,9 +772,9 @@ def accept(self):
782772
video_settings["vcodec"] = image_ext
783773

784774
# Store updated export folder path in project file
785-
app.updates.update_untracked(["export_path"], os.path.dirname(export_file_path))
775+
get_app().updates.update_untracked(["export_path"], os.path.dirname(export_file_path))
786776
# Mark project file as unsaved
787-
app.project.has_unsaved_changes = True
777+
get_app().project.has_unsaved_changes = True
788778

789779
# Set MaxSize (so we don't have any downsampling)
790780
self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height"))
@@ -793,14 +783,13 @@ def accept(self):
793783
export_cache_object = openshot.CacheMemory(500)
794784
self.timeline.SetCache(export_cache_object)
795785

796-
# Rescale all keyframes and reload project
786+
# Rescale all keyframes (if needed)
797787
if self.export_fps_factor != 1.0:
798-
self.keyframes_rescaled = True
799-
app.project.rescale_keyframes(self.export_fps_factor)
788+
# Get a copy of rescaled project data (this does not modify the active project)
789+
rescaled_app_data = get_app().project.rescale_keyframes(self.export_fps_factor)
800790

801791
# Load the "export" Timeline reader with the JSON from the real timeline
802-
json_timeline = json.dumps(app.project._data)
803-
self.timeline.SetJson(json_timeline)
792+
self.timeline.SetJson(json.dumps(rescaled_app_data))
804793

805794
# Re-update the timeline FPS again (since the timeline just got clobbered)
806795
self.updateFrameRate()
@@ -853,7 +842,7 @@ def accept(self):
853842

854843
# Notify window of export started
855844
title_message = ""
856-
app.window.ExportStarted.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"))
845+
get_app().window.ExportStarted.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"))
857846

858847
progressstep = max(1 , round(( video_settings.get("end_frame") - video_settings.get("start_frame") ) / 1000))
859848
start_time_export = time.time()
@@ -874,7 +863,7 @@ def accept(self):
874863
'fps': fps_encode}
875864

876865
# Emit frame exported
877-
app.window.ExportFrame.emit(title_message, video_settings.get("start_frame"), video_settings.get("end_frame"), frame)
866+
get_app().window.ExportFrame.emit(title_message, video_settings.get("start_frame"), video_settings.get("end_frame"), frame)
878867

879868
# Process events (to show the progress bar moving)
880869
QCoreApplication.processEvents()
@@ -897,7 +886,7 @@ def accept(self):
897886
'seconds': seconds_run % 60,
898887
'fps': fps_encode}
899888

900-
app.window.ExportFrame.emit(title_message, video_settings.get("start_frame"),
889+
get_app().window.ExportFrame.emit(title_message, video_settings.get("start_frame"),
901890
video_settings.get("end_frame"), frame)
902891

903892
except Exception as e:
@@ -936,7 +925,7 @@ def accept(self):
936925
msg.exec_()
937926

938927
# Notify window of export started
939-
app.window.ExportEnded.emit(export_file_path)
928+
get_app().window.ExportEnded.emit(export_file_path)
940929

941930
# Close timeline object
942931
self.timeline.Close()
@@ -986,10 +975,6 @@ def reject(self):
986975
# Return scale mode to lower quality scaling (for faster previews)
987976
openshot.Settings.Instance().HIGH_QUALITY_SCALING = False
988977

989-
# Return keyframes to preview scaling
990-
if self.keyframes_rescaled:
991-
get_app().project.rescale_keyframes(self.original_fps_factor)
992-
993978
# Cancel dialog
994979
self.exporting = False
995980
super(Export, self).reject()

src/windows/profile.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,10 @@ def __init__(self):
5656
ui_util.init_ui(self)
5757

5858
# get translations
59-
app = get_app()
60-
_ = app._tr
59+
_ = get_app()._tr
6160

6261
# Pause playback (to prevent crash since we are fixing to change the timeline's max size)
63-
app.window.actionPlay_trigger(None, force="pause")
62+
get_app().window.actionPlay_trigger(None, force="pause")
6463

6564
# Track metrics
6665
track_metric_screen("profile-screen")
@@ -93,7 +92,7 @@ def __init__(self):
9392
self.cboProfile.addItem(profile_name, self.profile_paths[profile_name])
9493

9594
# Set default (if it matches the project)
96-
if app.project.get(['profile']) in profile_name:
95+
if get_app().project.get(['profile']) in profile_name:
9796
self.initial_index = box_index
9897

9998
# increment item counter
@@ -145,7 +144,14 @@ def dropdown_activated(self, index):
145144

146145
# Rescale all keyframes and reload project
147146
if fps_factor != 1.0:
148-
get_app().project.rescale_keyframes(fps_factor)
147+
# Get a copy of rescaled project data (this does not modify the active project... yet)
148+
rescaled_app_data = get_app().project.rescale_keyframes(fps_factor)
149+
150+
# Apply rescaled data to active project
151+
get_app().project._data = rescaled_app_data
152+
153+
# Distribute all project data through update manager
154+
get_app().updates.load(rescaled_app_data)
149155

150156
# Force ApplyMapperToClips to apply these changes
151157
get_app().window.timeline_sync.timeline.ApplyMapperToClips()

0 commit comments

Comments
 (0)