OpenShot Video Editor  2.0.0
export.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the Video Export dialog (i.e where is all preferences)
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2018 OpenShot Studios, LLC
10 # (http://www.openshotstudios.com). This file is part of
11 # OpenShot Video Editor (http://www.openshot.org), an open-source project
12 # dedicated to delivering high quality video editing and animation solutions
13 # to the world.
14 #
15 # OpenShot Video Editor is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # OpenShot Video Editor is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27 #
28 import time
29 import os
30 import locale
31 import xml.dom.minidom as xml
32 import functools
33 
34 from PyQt5.QtCore import *
35 from PyQt5.QtWidgets import *
36 import openshot # Python module for libopenshot (required video editing module installed separately)
37 
38 from classes import info, ui_util, settings
39 from classes.app import get_app
40 from classes.query import File
41 from classes.logger import log
42 from classes.metrics import *
43 
44 try:
45  import json
46 except ImportError:
47  import simplejson as json
48 
49 
50 ##
51 # Export Dialog
52 class Export(QDialog):
53 
54  # Path to ui file
55  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'export.ui')
56 
57  def __init__(self):
58 
59  # Create dialog class
60  QDialog.__init__(self)
61 
62  # Load UI from designer
63  ui_util.load_ui(self, self.ui_path)
64 
65  # Init UI
66  ui_util.init_ui(self)
67 
68  # get translations
69  app = get_app()
70  _ = app._tr
71 
72  # Get settings
74 
75  # Track metrics
76  track_metric_screen("export-screen")
77 
78  # Dynamically load tabs from settings data
79  self.settings_data = settings.get_settings().get_all_settings()
80 
81  # Add buttons to interface
82  self.export_button = QPushButton(_('Export Video'))
83  self.buttonBox.addButton(self.export_button, QDialogButtonBox.AcceptRole)
84  self.buttonBox.addButton(QPushButton(_('Cancel')), QDialogButtonBox.RejectRole)
85  self.exporting = False
86 
87  # Update FPS / Profile timer
88  # Timer to use a delay before applying new profile/fps data (so we don't spam libopenshot)
89  self.delayed_fps_timer = None
90  self.delayed_fps_timer = QTimer()
91  self.delayed_fps_timer.setInterval(200)
92  self.delayed_fps_timer.timeout.connect(self.delayed_fps_callback)
93  self.delayed_fps_timer.stop()
94 
95  # Pause playback (to prevent crash since we are fixing to change the timeline's max size)
96  get_app().window.actionPlay_trigger(None, force="pause")
97 
98  # Clear timeline preview cache (to get more available memory)
99  get_app().window.timeline_sync.timeline.ClearAllCache()
100 
101  # Hide audio channels
102  self.lblChannels.setVisible(False)
103  self.txtChannels.setVisible(False)
104 
105  # Set OMP thread disabled flag (for stability)
106  openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = True
107  openshot.Settings.Instance().HIGH_QUALITY_SCALING = True
108 
109  # Get the original timeline settings
110  width = get_app().window.timeline_sync.timeline.info.width
111  height = get_app().window.timeline_sync.timeline.info.height
112  fps = get_app().window.timeline_sync.timeline.info.fps
113  sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
114  channels = get_app().window.timeline_sync.timeline.info.channels
115  channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
116 
117  # No keyframe rescaling has happened yet (due to differences in FPS)
118  self.keyframes_rescaled = False
119 
120  # Create new "export" openshot.Timeline object
121  self.timeline = openshot.Timeline(width, height, openshot.Fraction(fps.num, fps.den),
122  sample_rate, channels, channel_layout)
123  # Init various properties
124  self.timeline.info.channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
125  self.timeline.info.has_audio = get_app().window.timeline_sync.timeline.info.has_audio
126  self.timeline.info.has_video = get_app().window.timeline_sync.timeline.info.has_video
127  self.timeline.info.video_length = get_app().window.timeline_sync.timeline.info.video_length
128  self.timeline.info.duration = get_app().window.timeline_sync.timeline.info.duration
129  self.timeline.info.sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
130  self.timeline.info.channels = get_app().window.timeline_sync.timeline.info.channels
131 
132  # Load the "export" Timeline reader with the JSON from the real timeline
133  json_timeline = json.dumps(get_app().project._data)
134  self.timeline.SetJson(json_timeline)
135 
136  # Open the "export" Timeline reader
137  self.timeline.Open()
138 
139  # Default export path
140  recommended_path = recommended_path = os.path.join(info.HOME_PATH)
141  if app.project.current_filepath:
142  recommended_path = os.path.dirname(app.project.current_filepath)
143 
144  export_path = get_app().project.get(["export_path"])
145  if os.path.exists(export_path):
146  # Use last selected export path
147  self.txtExportFolder.setText(export_path)
148  else:
149  # Default to home dir
150  self.txtExportFolder.setText(recommended_path)
151 
152  # Is this a saved project?
153  if not get_app().project.current_filepath:
154  # Not saved yet
155  self.txtFileName.setText(_("Untitled Project"))
156  else:
157  # Yes, project is saved
158  # Get just the filename
159  parent_path, filename = os.path.split(get_app().project.current_filepath)
160  filename, ext = os.path.splitext(filename)
161  self.txtFileName.setText(filename.replace("_", " ").replace("-", " ").capitalize())
162 
163  # Default image type
164  self.txtImageFormat.setText("-%05d.png")
165 
166  # Loop through Export To options
167  export_options = [_("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")]
168  for option in export_options:
169  # append profile to list
170  self.cboExportTo.addItem(option)
171 
172  # Add channel layouts
174  for layout in [(openshot.LAYOUT_MONO, _("Mono (1 Channel)")),
175  (openshot.LAYOUT_STEREO, _("Stereo (2 Channel)")),
176  (openshot.LAYOUT_SURROUND, _("Surround (3 Channel)")),
177  (openshot.LAYOUT_5POINT1, _("Surround (5.1 Channel)")),
178  (openshot.LAYOUT_7POINT1, _("Surround (7.1 Channel)"))]:
179  log.info(layout)
180  self.channel_layout_choices.append(layout[0])
181  self.cboChannelLayout.addItem(layout[1], layout[0])
182 
183  # Connect signals
184  self.btnBrowse.clicked.connect(functools.partial(self.btnBrowse_clicked))
185  self.cboSimpleProjectType.currentIndexChanged.connect(
186  functools.partial(self.cboSimpleProjectType_index_changed, self.cboSimpleProjectType))
187  self.cboProfile.currentIndexChanged.connect(functools.partial(self.cboProfile_index_changed, self.cboProfile))
188  self.cboSimpleTarget.currentIndexChanged.connect(
189  functools.partial(self.cboSimpleTarget_index_changed, self.cboSimpleTarget))
190  self.cboSimpleVideoProfile.currentIndexChanged.connect(
191  functools.partial(self.cboSimpleVideoProfile_index_changed, self.cboSimpleVideoProfile))
192  self.cboSimpleQuality.currentIndexChanged.connect(
193  functools.partial(self.cboSimpleQuality_index_changed, self.cboSimpleQuality))
194  self.cboChannelLayout.currentIndexChanged.connect(self.updateChannels)
195  get_app().window.ExportFrame.connect(self.updateProgressBar)
196 
197  # ********* Advanced Profile List **********
198  # Loop through profiles
199  self.profile_names = []
200  self.profile_paths = {}
201  for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
202  for file in os.listdir(profile_folder):
203  # Load Profile
204  profile_path = os.path.join(profile_folder, file)
205  profile = openshot.Profile(profile_path)
206 
207  # Add description of Profile to list
208  profile_name = "%s (%sx%s)" % (profile.info.description, profile.info.width, profile.info.height)
209  self.profile_names.append(profile_name)
210  self.profile_paths[profile_name] = profile_path
211 
212  # Sort list
213  self.profile_names.sort()
214 
215  # Loop through sorted profiles
216  box_index = 0
218  for profile_name in self.profile_names:
219 
220  # Add to dropdown
221  self.cboProfile.addItem(self.getProfileName(self.getProfilePath(profile_name)), self.getProfilePath(profile_name))
222 
223  # Set default (if it matches the project)
224  if app.project.get(['profile']) in profile_name:
225  self.selected_profile_index = box_index
226 
227  # increment item counter
228  box_index += 1
229 
230 
231  # ********* Simple Project Type **********
232  # load the simple project type dropdown
233  presets = []
234  for preset_path in [info.EXPORT_PRESETS_PATH, info.USER_PRESETS_PATH]:
235  for file in os.listdir(preset_path):
236  xmldoc = xml.parse(os.path.join(preset_path, file))
237  type = xmldoc.getElementsByTagName("type")
238  presets.append(_(type[0].childNodes[0].data))
239 
240  # Exclude duplicates
241  type_index = 0
242  selected_type = 0
243  presets = list(set(presets))
244  for item in sorted(presets):
245  self.cboSimpleProjectType.addItem(item, item)
246  if item == _("All Formats"):
247  selected_type = type_index
248  type_index += 1
249 
250  # Always select 'All Formats' option
251  self.cboSimpleProjectType.setCurrentIndex(selected_type)
252 
253 
254  # Populate all profiles
255  self.populateAllProfiles(app.project.get(['profile']))
256 
257  # Connect framerate signals
258  self.txtFrameRateNum.valueChanged.connect(self.updateFrameRate)
259  self.txtFrameRateDen.valueChanged.connect(self.updateFrameRate)
260  self.txtWidth.valueChanged.connect(self.updateFrameRate)
261  self.txtHeight.valueChanged.connect(self.updateFrameRate)
262  self.txtSampleRate.valueChanged.connect(self.updateFrameRate)
263  self.txtChannels.valueChanged.connect(self.updateFrameRate)
264  self.cboChannelLayout.currentIndexChanged.connect(self.updateFrameRate)
265 
266  # Determine the length of the timeline (in frames)
267  self.updateFrameRate()
268 
269  ##
270  # Callback for fps/profile changed event timer (to delay the timeline mapping so we don't spam libopenshot)
272  # Stop timer
273  self.delayed_fps_timer.stop()
274 
275  # Calculate fps
276  fps_double = self.timeline.info.fps.ToDouble()
277 
278  # Apply mapping if valid fps detected (anything larger than 300 fps is considered invalid)
279  if self.timeline and fps_double <= 300.0:
280  log.info("Valid framerate detected, sending to libopenshot: %s" % fps_double)
281  self.timeline.ApplyMapperToClips()
282  else:
283  log.warning("Invalid framerate detected, not sending it to libopenshot: %s" % fps_double)
284 
285  ##
286  # Get the profile path that matches the name
287  def getProfilePath(self, profile_name):
288  for profile, path in self.profile_paths.items():
289  if profile_name in profile:
290  return path
291 
292  ##
293  # Get the profile name that matches the name
294  def getProfileName(self, profile_path):
295  for profile, path in self.profile_paths.items():
296  if profile_path == path:
297  return profile
298 
299  ##
300  # Update progress bar during exporting
301  def updateProgressBar(self, path, start_frame, end_frame, current_frame):
302  percentage_string = "%4.1f%% " % (( current_frame - start_frame ) / ( end_frame - start_frame ) * 100)
303  self.progressExportVideo.setValue(current_frame)
304  self.progressExportVideo.setFormat(percentage_string)
305  self.setWindowTitle("%s %s" % (percentage_string, path))
306 
307  ##
308  # Update the # of channels to match the channel layout
309  def updateChannels(self):
310  log.info("updateChannels")
311  channels = self.txtChannels.value()
312  channel_layout = self.cboChannelLayout.currentData()
313 
314  if channel_layout == openshot.LAYOUT_MONO:
315  channels = 1
316  elif channel_layout == openshot.LAYOUT_STEREO:
317  channels = 2
318  elif channel_layout == openshot.LAYOUT_SURROUND:
319  channels = 3
320  elif channel_layout == openshot.LAYOUT_5POINT1:
321  channels = 6
322  elif channel_layout == openshot.LAYOUT_7POINT1:
323  channels = 8
324 
325  # Update channels to match layout
326  self.txtChannels.setValue(channels)
327 
328  ##
329  # Callback for changing the frame rate
330  def updateFrameRate(self):
331  # Adjust the main timeline reader
332  self.timeline.info.width = self.txtWidth.value()
333  self.timeline.info.height = self.txtHeight.value()
334  self.timeline.info.fps.num = self.txtFrameRateNum.value()
335  self.timeline.info.fps.den = self.txtFrameRateDen.value()
336  self.timeline.info.sample_rate = self.txtSampleRate.value()
337  self.timeline.info.channels = self.txtChannels.value()
338  self.timeline.info.channel_layout = self.cboChannelLayout.currentData()
339 
340  # Send changes to libopenshot (apply mappings to all framemappers)... after a small delay
341  self.delayed_fps_timer.start()
342 
343  # Determine max frame (based on clips)
344  timeline_length = 0.0
345  fps = self.timeline.info.fps.ToFloat()
346  clips = self.timeline.Clips()
347  for clip in clips:
348  clip_last_frame = clip.Position() + clip.Duration()
349  if clip_last_frame > timeline_length:
350  # Set max length of timeline
351  timeline_length = clip_last_frame
352 
353  # Convert to int and round
354  self.timeline_length_int = round(timeline_length * fps) + 1
355 
356  # Set the min and max frame numbers for this project
357  self.txtStartFrame.setValue(1)
358  self.txtEndFrame.setValue(self.timeline_length_int)
359 
360  # Init progress bar
361  self.progressExportVideo.setMinimum(self.txtStartFrame.value())
362  self.progressExportVideo.setMaximum(self.txtEndFrame.value())
363  self.progressExportVideo.setValue(self.txtStartFrame.value())
364 
365  # Calculate differences between editing/preview FPS and export FPS
366  current_fps = get_app().project.get(["fps"])
367  current_fps_float = float(current_fps["num"]) / float(current_fps["den"])
368  new_fps_float = float(self.txtFrameRateNum.value()) / float(self.txtFrameRateDen.value())
369  self.export_fps_factor = new_fps_float / current_fps_float
370  self.original_fps_factor = current_fps_float / new_fps_float
371 
372  def cboSimpleProjectType_index_changed(self, widget, index):
373  selected_project = widget.itemData(index)
374 
375  # set the target dropdown based on the selected project type
376  # first clear the combo
377  self.cboSimpleTarget.clear()
378 
379  # get translations
380  app = get_app()
381  _ = app._tr
382 
383  # parse the xml files and get targets that match the project type
384  project_types = []
385  for preset_path in [info.EXPORT_PRESETS_PATH, info.USER_PRESETS_PATH]:
386  for file in os.listdir(preset_path):
387  xmldoc = xml.parse(os.path.join(preset_path, file))
388  type = xmldoc.getElementsByTagName("type")
389 
390  if _(type[0].childNodes[0].data) == selected_project:
391  titles = xmldoc.getElementsByTagName("title")
392  for title in titles:
393  project_types.append(_(title.childNodes[0].data))
394 
395  # Add all targets for selected project type
396  preset_index = 0
397  selected_preset = 0
398  for item in sorted(project_types):
399  self.cboSimpleTarget.addItem(item, item)
400 
401  # Find index of MP4/H.264
402  if item == _("MP4 (h.264)"):
403  selected_preset = preset_index
404 
405  preset_index += 1
406 
407  # Select MP4/H.264 as default
408  self.cboSimpleTarget.setCurrentIndex(selected_preset)
409 
410  def cboProfile_index_changed(self, widget, index):
411  selected_profile_path = widget.itemData(index)
412  log.info(selected_profile_path)
413 
414  # get translations
415  app = get_app()
416  _ = app._tr
417 
418  # Load profile
419  profile = openshot.Profile(selected_profile_path)
420 
421  # Load profile settings into advanced editor
422  self.txtWidth.setValue(profile.info.width)
423  self.txtHeight.setValue(profile.info.height)
424  self.txtFrameRateDen.setValue(profile.info.fps.den)
425  self.txtFrameRateNum.setValue(profile.info.fps.num)
426  self.txtAspectRatioNum.setValue(profile.info.display_ratio.num)
427  self.txtAspectRatioDen.setValue(profile.info.display_ratio.den)
428  self.txtPixelRatioNum.setValue(profile.info.pixel_ratio.num)
429  self.txtPixelRatioDen.setValue(profile.info.pixel_ratio.den)
430 
431  # Load the interlaced options
432  self.cboInterlaced.clear()
433  self.cboInterlaced.addItem(_("Yes"), "Yes")
434  self.cboInterlaced.addItem(_("No"), "No")
435  if profile.info.interlaced_frame:
436  self.cboInterlaced.setCurrentIndex(0)
437  else:
438  self.cboInterlaced.setCurrentIndex(1)
439 
440  def cboSimpleTarget_index_changed(self, widget, index):
441  selected_target = widget.itemData(index)
442  log.info(selected_target)
443 
444  # get translations
445  app = get_app()
446  _ = app._tr
447 
448  # don't do anything if the combo has been cleared
449  if selected_target:
450  profiles_list = []
451 
452  # Clear the following options (and remember current settings)
453  previous_quality = self.cboSimpleQuality.currentIndex()
454  if previous_quality < 0:
455  previous_quality = self.cboSimpleQuality.count() - 1
456  previous_profile = self.cboSimpleVideoProfile.currentIndex()
457  if previous_profile < 0:
458  previous_profile = self.selected_profile_index
459  self.cboSimpleVideoProfile.clear()
460  self.cboSimpleQuality.clear()
461 
462  # parse the xml to return suggested profiles
463  profile_index = 0
464  all_profiles = False
465  for preset_path in [info.EXPORT_PRESETS_PATH, info.USER_PRESETS_PATH]:
466  for file in os.listdir(preset_path):
467  xmldoc = xml.parse(os.path.join(preset_path, file))
468  title = xmldoc.getElementsByTagName("title")
469  if _(title[0].childNodes[0].data) == selected_target:
470  profiles = xmldoc.getElementsByTagName("projectprofile")
471 
472  # get the basic profile
473  all_profiles = False
474  if profiles:
475  # if profiles are defined, show them
476  for profile in profiles:
477  profiles_list.append(_(profile.childNodes[0].data))
478  else:
479  # show all profiles
480  all_profiles = True
481  for profile_name in self.profile_names:
482  profiles_list.append(profile_name)
483 
484  # get the video bit rate(s)
485  videobitrate = xmldoc.getElementsByTagName("videobitrate")
486  for rate in videobitrate:
487  v_l = rate.attributes["low"].value
488  v_m = rate.attributes["med"].value
489  v_h = rate.attributes["high"].value
490  self.vbr = {_("Low"): v_l, _("Med"): v_m, _("High"): v_h}
491 
492  # get the audio bit rates
493  audiobitrate = xmldoc.getElementsByTagName("audiobitrate")
494  for audiorate in audiobitrate:
495  a_l = audiorate.attributes["low"].value
496  a_m = audiorate.attributes["med"].value
497  a_h = audiorate.attributes["high"].value
498  self.abr = {_("Low"): a_l, _("Med"): a_m, _("High"): a_h}
499 
500  # get the remaining values
501  vf = xmldoc.getElementsByTagName("videoformat")
502  self.txtVideoFormat.setText(vf[0].childNodes[0].data)
503  vc = xmldoc.getElementsByTagName("videocodec")
504  self.txtVideoCodec.setText(vc[0].childNodes[0].data)
505  sr = xmldoc.getElementsByTagName("samplerate")
506  self.txtSampleRate.setValue(int(sr[0].childNodes[0].data))
507  c = xmldoc.getElementsByTagName("audiochannels")
508  self.txtChannels.setValue(int(c[0].childNodes[0].data))
509  c = xmldoc.getElementsByTagName("audiochannellayout")
510 
511  # check for compatible audio codec
512  ac = xmldoc.getElementsByTagName("audiocodec")
513  audio_codec_name = ac[0].childNodes[0].data
514  if audio_codec_name == "aac":
515  # Determine which version of AAC encoder is available
516  if openshot.FFmpegWriter.IsValidCodec("libfaac"):
517  self.txtAudioCodec.setText("libfaac")
518  elif openshot.FFmpegWriter.IsValidCodec("libvo_aacenc"):
519  self.txtAudioCodec.setText("libvo_aacenc")
520  elif openshot.FFmpegWriter.IsValidCodec("aac"):
521  self.txtAudioCodec.setText("aac")
522  else:
523  # fallback audio codec
524  self.txtAudioCodec.setText("ac3")
525  else:
526  # fallback audio codec
527  self.txtAudioCodec.setText(audio_codec_name)
528 
529  layout_index = 0
530  for layout in self.channel_layout_choices:
531  if layout == int(c[0].childNodes[0].data):
532  self.cboChannelLayout.setCurrentIndex(layout_index)
533  break
534  layout_index += 1
535 
536  # init the profiles combo
537  for item in sorted(profiles_list):
538  self.cboSimpleVideoProfile.addItem(self.getProfileName(self.getProfilePath(item)), self.getProfilePath(item))
539 
540  if all_profiles:
541  # select the project's current profile
542  self.cboSimpleVideoProfile.setCurrentIndex(previous_profile)
543 
544  # set the quality combo
545  # only populate with quality settings that exist
546  if v_l or a_l:
547  self.cboSimpleQuality.addItem(_("Low"), "Low")
548  if v_m or a_m:
549  self.cboSimpleQuality.addItem(_("Med"), "Med")
550  if v_h or a_h:
551  self.cboSimpleQuality.addItem(_("High"), "High")
552 
553  # Default to the highest quality setting (or previous quality setting)
554  if previous_quality <= self.cboSimpleQuality.count() - 1:
555  self.cboSimpleQuality.setCurrentIndex(previous_quality)
556  else:
557  self.cboSimpleQuality.setCurrentIndex(self.cboSimpleQuality.count() - 1)
558 
559  def cboSimpleVideoProfile_index_changed(self, widget, index):
560  selected_profile_path = widget.itemData(index)
561  log.info(selected_profile_path)
562 
563  # Populate the advanced profile list
564  self.populateAllProfiles(selected_profile_path)
565 
566  ##
567  # Populate the full list of profiles
568  def populateAllProfiles(self, selected_profile_path):
569  # Look for matching profile in advanced options
570  profile_index = 0
571  for profile_name in self.profile_names:
572  # Check for matching profile
573  if self.getProfilePath(profile_name) == selected_profile_path:
574  # Matched!
575  self.cboProfile.setCurrentIndex(profile_index)
576  break
577 
578  # increment index
579  profile_index += 1
580 
581  def cboSimpleQuality_index_changed(self, widget, index):
582  selected_quality = widget.itemData(index)
583  log.info(selected_quality)
584 
585  # get translations
586  app = get_app()
587  _ = app._tr
588 
589  # Set the video and audio bitrates
590  if selected_quality:
591  self.txtVideoBitRate.setText(_(self.vbr[_(selected_quality)]))
592  self.txtAudioBitrate.setText(_(self.abr[_(selected_quality)]))
593 
594  def btnBrowse_clicked(self):
595  log.info("btnBrowse_clicked")
596 
597  # get translations
598  app = get_app()
599  _ = app._tr
600 
601  # update export folder path
602  file_path = QFileDialog.getExistingDirectory(self, _("Choose a Folder..."), self.txtExportFolder.text())
603  if os.path.exists(file_path):
604  self.txtExportFolder.setText(file_path)
605 
606  # update export folder path in project file
607  get_app().updates.update(["export_path"], file_path)
608 
609  def convert_to_bytes(self, BitRateString):
610  bit_rate_bytes = 0
611 
612  # split the string into pieces
613  s = BitRateString.lower().split(" ")
614  measurement = "kb"
615 
616  try:
617  # Get Bit Rate
618  if len(s) >= 2:
619  raw_number_string = s[0]
620  raw_measurement = s[1]
621 
622  # convert string number to float (based on locale settings)
623  raw_number = locale.atof(raw_number_string)
624 
625  if "kb" in raw_measurement:
626  measurement = "kb"
627  bit_rate_bytes = raw_number * 1000.0
628 
629  elif "mb" in raw_measurement:
630  measurement = "mb"
631  bit_rate_bytes = raw_number * 1000.0 * 1000.0
632 
633  elif "crf" in raw_measurement:
634  measurement = "crf"
635  if raw_number > 63:
636  raw_number = 63
637  if raw_number < 0:
638  raw_number = 0
639  bit_rate_bytes = raw_number
640 
641  except:
642  pass
643 
644  # return the bit rate in bytes
645  return str(int(bit_rate_bytes))
646 
647  ##
648  # Start exporting video
649  def accept(self):
650 
651  # get translations
652  app = get_app()
653  _ = app._tr
654 
655  # Disable controls
656  self.txtFileName.setEnabled(False)
657  self.txtExportFolder.setEnabled(False)
658  self.tabWidget.setEnabled(False)
659  self.export_button.setEnabled(False)
660  self.exporting = True
661 
662  # Determine type of export (video+audio, video, audio, image sequences)
663  # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")
664  export_type = self.cboExportTo.currentText()
665 
666  # Determine final exported file path
667  if export_type != _("Image Sequence"):
668  file_name_with_ext = "%s.%s" % (self.txtFileName.text().strip(), self.txtVideoFormat.text().strip())
669  else:
670  file_name_with_ext = "%s%s" % (self.txtFileName.text().strip(), self.txtImageFormat.text().strip())
671  export_file_path = os.path.join(self.txtExportFolder.text().strip(), file_name_with_ext)
672  log.info(export_file_path)
673 
674  # Translate object
675  _ = get_app()._tr
676 
677  file = File.get(path=export_file_path)
678  if file:
679  ret = QMessageBox.question(self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext,
680  QMessageBox.Ok)
681  self.txtFileName.setEnabled(True)
682  self.txtExportFolder.setEnabled(True)
683  self.tabWidget.setEnabled(True)
684  self.export_button.setEnabled(True)
685  self.exporting = False
686  return
687 
688  # Handle exception
689  if os.path.exists(export_file_path) and export_type in [_("Video & Audio"), _("Video Only"), _("Audio Only")]:
690  # File already exists! Prompt user
691  ret = QMessageBox.question(self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext,
692  QMessageBox.No | QMessageBox.Yes)
693  if ret == QMessageBox.No:
694  # Stop and don't do anything
695  # Re-enable controls
696  self.txtFileName.setEnabled(True)
697  self.txtExportFolder.setEnabled(True)
698  self.tabWidget.setEnabled(True)
699  self.export_button.setEnabled(True)
700  self.exporting = False
701  return
702 
703  # Init export settings
704  video_settings = { "vformat": self.txtVideoFormat.text(),
705  "vcodec": self.txtVideoCodec.text(),
706  "fps": { "num" : self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value()},
707  "width": self.txtWidth.value(),
708  "height": self.txtHeight.value(),
709  "pixel_ratio": {"num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value()},
710  "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())),
711  "start_frame": self.txtStartFrame.value(),
712  "end_frame": self.txtEndFrame.value() + 1
713  }
714 
715  audio_settings = {"acodec": self.txtAudioCodec.text(),
716  "sample_rate": self.txtSampleRate.value(),
717  "channels": self.txtChannels.value(),
718  "channel_layout": self.cboChannelLayout.currentData(),
719  "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text()))
720  }
721 
722  # Override vcodec and format for Image Sequences
723  if export_type == _("Image Sequence"):
724  image_ext = os.path.splitext(self.txtImageFormat.text().strip())[1].replace(".", "")
725  video_settings["vformat"] = image_ext
726  if image_ext in ["jpg", "jpeg"]:
727  video_settings["vcodec"] = "mjpeg"
728  else:
729  video_settings["vcodec"] = image_ext
730 
731  # Set MaxSize (so we don't have any downsampling)
732  self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height"))
733 
734  # Set lossless cache settings (temporarily)
735  export_cache_object = openshot.CacheMemory(250)
736  self.timeline.SetCache(export_cache_object)
737 
738  # Rescale all keyframes and reload project
739  if self.export_fps_factor != 1.0:
740  self.keyframes_rescaled = True
741  get_app().project.rescale_keyframes(self.export_fps_factor)
742 
743  # Load the "export" Timeline reader with the JSON from the real timeline
744  json_timeline = json.dumps(get_app().project._data)
745  self.timeline.SetJson(json_timeline)
746 
747  # Re-update the timeline FPS again (since the timeline just got clobbered)
748  self.updateFrameRate()
749 
750  # Create FFmpegWriter
751  try:
752  w = openshot.FFmpegWriter(export_file_path)
753 
754  # Set video options
755  if export_type in [_("Video & Audio"), _("Video Only"), _("Image Sequence")]:
756  w.SetVideoOptions(True,
757  video_settings.get("vcodec"),
758  openshot.Fraction(video_settings.get("fps").get("num"),
759  video_settings.get("fps").get("den")),
760  video_settings.get("width"),
761  video_settings.get("height"),
762  openshot.Fraction(video_settings.get("pixel_ratio").get("num"),
763  video_settings.get("pixel_ratio").get("den")),
764  False,
765  False,
766  video_settings.get("video_bitrate"))
767 
768  # Set audio options
769  if export_type in [_("Video & Audio"), _("Audio Only")]:
770  w.SetAudioOptions(True,
771  audio_settings.get("acodec"),
772  audio_settings.get("sample_rate"),
773  audio_settings.get("channels"),
774  audio_settings.get("channel_layout"),
775  audio_settings.get("audio_bitrate"))
776 
777  # Prepare the streams
778  w.PrepareStreams()
779 
780  # These extra options should be set in an extra method
781  # No feedback is given to the user
782  # TODO: Tell user if option is not avaliable
783  # Set the quality in case crf was selected
784  if "crf" in self.txtVideoBitRate.text():
785  w.SetOption(openshot.VIDEO_STREAM, "crf", str(int(video_settings.get("video_bitrate"))) )
786 
787  # Open the writer
788  w.Open()
789 
790  # Notify window of export started
791  export_file_path = ""
792  get_app().window.ExportStarted.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"))
793 
794  progressstep = max(1 , round(( video_settings.get("end_frame") - video_settings.get("start_frame") ) / 1000))
795  start_time_export = time.time()
796  start_frame_export = video_settings.get("start_frame")
797  end_frame_export = video_settings.get("end_frame")
798  # Write each frame in the selected range
799  for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame")):
800  # Update progress bar (emit signal to main window)
801  if (frame % progressstep) == 0:
802  end_time_export = time.time()
803  if ((( frame - start_frame_export ) != 0) & (( end_time_export - start_time_export ) != 0)):
804  seconds_left = round(( start_time_export - end_time_export )*( frame - end_frame_export )/( frame - start_frame_export ))
805  fps_encode = ((frame - start_frame_export)/(end_time_export-start_time_export))
806  export_file_path = _("%(hours)d:%(minutes)02d:%(seconds)02d Remaining (%(fps)5.2f FPS)") % { 'hours' : seconds_left / 3600,
807  'minutes': (seconds_left / 60) % 60,
808  'seconds': seconds_left % 60,
809  'fps': fps_encode }
810  get_app().window.ExportFrame.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"), frame)
811 
812  # Process events (to show the progress bar moving)
813  QCoreApplication.processEvents()
814 
815  # Write the frame object to the video
816  w.WriteFrame(self.timeline.GetFrame(frame))
817 
818  # Check if we need to bail out
819  if not self.exporting:
820  break
821 
822  # Close writer
823  w.Close()
824 
825 
826  except Exception as e:
827  # TODO: Find a better way to catch the error. This is the only way I have found that
828  # does not throw an error
829  error_type_str = str(e)
830  log.info("Error type string: %s" % error_type_str)
831 
832  if "InvalidChannels" in error_type_str:
833  log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels")))
834  track_metric_error("invalid-channels-%s-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"), audio_settings.get("channels")))
835 
836  elif "InvalidSampleRate" in error_type_str:
837  log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate")))
838  track_metric_error("invalid-sample-rate-%s-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"), audio_settings.get("sample_rate")))
839 
840  elif "InvalidFormat" in error_type_str:
841  log.info("Error setting invalid format (%s)" % (video_settings.get("vformat")))
842  track_metric_error("invalid-format-%s" % (video_settings.get("vformat")))
843 
844  elif "InvalidCodec" in error_type_str:
845  log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
846  track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
847 
848  elif "ErrorEncodingVideo" in error_type_str:
849  log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
850  track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
851 
852  # Show friendly error
853  friendly_error = error_type_str.split("> ")[0].replace("<", "")
854 
855  # Prompt error message
856  msg = QMessageBox()
857  _ = get_app()._tr
858  msg.setWindowTitle(_("Export Error"))
859  msg.setText(_("Sorry, there was an error exporting your video: \n%s") % friendly_error)
860  msg.exec_()
861 
862  # Notify window of export started
863  get_app().window.ExportEnded.emit(export_file_path)
864 
865  # Close timeline object
866  self.timeline.Close()
867 
868  # Clear all cache
869  self.timeline.ClearAllCache()
870 
871  # Re-set OMP thread enabled flag
872  if self.s.get("omp_threads_enabled"):
873  openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = False
874  else:
875  openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = True
876 
877  # Return scale mode to lower quality scaling (for faster previews)
878  openshot.Settings.Instance().HIGH_QUALITY_SCALING = False
879 
880  # Return keyframes to preview scaling
881  if self.keyframes_rescaled:
882  get_app().project.rescale_keyframes(self.original_fps_factor)
883 
884  # Accept dialog
885  super(Export, self).accept()
886 
887  def reject(self):
888  # Re-set OMP thread enabled flag
889  if self.s.get("omp_threads_enabled"):
890  openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = False
891  else:
892  openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = True
893 
894  # Return scale mode to lower quality scaling (for faster previews)
895  openshot.Settings.Instance().HIGH_QUALITY_SCALING = False
896 
897  # Return keyframes to preview scaling
898  if self.keyframes_rescaled:
899  get_app().project.rescale_keyframes(self.original_fps_factor)
900 
901  # Cancel dialog
902  self.exporting = False
903  super(Export, self).reject()
def cboSimpleQuality_index_changed
Definition: export.py:581
def reject
Definition: export.py:887
Export Dialog.
Definition: export.py:52
def updateFrameRate
Callback for changing the frame rate.
Definition: export.py:330
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def delayed_fps_callback
Callback for fps/profile changed event timer (to delay the timeline mapping so we don't spam libopens...
Definition: export.py:271
channel_layout_choices
Definition: export.py:173
def cboSimpleProjectType_index_changed
Definition: export.py:372
def track_metric_error
Track an error has occurred.
Definition: metrics.py:121
def cboSimpleVideoProfile_index_changed
Definition: export.py:559
def btnBrowse_clicked
Definition: export.py:594
original_fps_factor
Definition: export.py:370
def getProfileName
Get the profile name that matches the name.
Definition: export.py:294
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
selected_profile_index
Definition: export.py:217
def cboProfile_index_changed
Definition: export.py:410
def __init__
Definition: export.py:57
timeline_length_int
Definition: export.py:354
def updateProgressBar
Update progress bar during exporting.
Definition: export.py:301
def updateChannels
Update the # of channels to match the channel layout.
Definition: export.py:309
def getProfilePath
Get the profile path that matches the name.
Definition: export.py:287
def cboSimpleTarget_index_changed
Definition: export.py:440
delayed_fps_timer
Definition: export.py:89
keyframes_rescaled
Definition: export.py:118
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:215
def populateAllProfiles
Populate the full list of profiles.
Definition: export.py:568
def convert_to_bytes
Definition: export.py:609
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44
def accept
Start exporting video.
Definition: export.py:649
tuple ui_path
Definition: export.py:55