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  os.environ['OS2_OMP_THREADS'] = "0"
107 
108  # Get the original timeline settings
109  width = get_app().window.timeline_sync.timeline.info.width
110  height = get_app().window.timeline_sync.timeline.info.height
111  fps = get_app().window.timeline_sync.timeline.info.fps
112  sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
113  channels = get_app().window.timeline_sync.timeline.info.channels
114  channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
115 
116  # Create new "export" openshot.Timeline object
117  self.timeline = openshot.Timeline(width, height, openshot.Fraction(fps.num, fps.den),
118  sample_rate, channels, channel_layout)
119  # Init various properties
120  self.timeline.info.channel_layout = get_app().window.timeline_sync.timeline.info.channel_layout
121  self.timeline.info.has_audio = get_app().window.timeline_sync.timeline.info.has_audio
122  self.timeline.info.has_video = get_app().window.timeline_sync.timeline.info.has_video
123  self.timeline.info.video_length = get_app().window.timeline_sync.timeline.info.video_length
124  self.timeline.info.duration = get_app().window.timeline_sync.timeline.info.duration
125  self.timeline.info.sample_rate = get_app().window.timeline_sync.timeline.info.sample_rate
126  self.timeline.info.channels = get_app().window.timeline_sync.timeline.info.channels
127 
128  # Load the "export" Timeline reader with the JSON from the real timeline
129  json_timeline = json.dumps(get_app().project._data)
130  self.timeline.SetJson(json_timeline)
131 
132  # Open the "export" Timeline reader
133  self.timeline.Open()
134 
135  # Default export path
136  recommended_path = recommended_path = os.path.join(info.HOME_PATH)
137  if app.project.current_filepath:
138  recommended_path = os.path.dirname(app.project.current_filepath)
139 
140  export_path = get_app().project.get(["export_path"])
141  if os.path.exists(export_path):
142  # Use last selected export path
143  self.txtExportFolder.setText(export_path)
144  else:
145  # Default to home dir
146  self.txtExportFolder.setText(recommended_path)
147 
148  # Is this a saved project?
149  if not get_app().project.current_filepath:
150  # Not saved yet
151  self.txtFileName.setText(_("Untitled Project"))
152  else:
153  # Yes, project is saved
154  # Get just the filename
155  parent_path, filename = os.path.split(get_app().project.current_filepath)
156  filename, ext = os.path.splitext(filename)
157  self.txtFileName.setText(filename.replace("_", " ").replace("-", " ").capitalize())
158 
159  # Default image type
160  self.txtImageFormat.setText("-%05d.png")
161 
162  # Loop through Export To options
163  export_options = [_("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")]
164  for option in export_options:
165  # append profile to list
166  self.cboExportTo.addItem(option)
167 
168  # Add channel layouts
170  for layout in [(openshot.LAYOUT_MONO, _("Mono (1 Channel)")),
171  (openshot.LAYOUT_STEREO, _("Stereo (2 Channel)")),
172  (openshot.LAYOUT_SURROUND, _("Surround (3 Channel)")),
173  (openshot.LAYOUT_5POINT1, _("Surround (5.1 Channel)")),
174  (openshot.LAYOUT_7POINT1, _("Surround (7.1 Channel)"))]:
175  log.info(layout)
176  self.channel_layout_choices.append(layout[0])
177  self.cboChannelLayout.addItem(layout[1], layout[0])
178 
179  # Connect signals
180  self.btnBrowse.clicked.connect(functools.partial(self.btnBrowse_clicked))
181  self.cboSimpleProjectType.currentIndexChanged.connect(
182  functools.partial(self.cboSimpleProjectType_index_changed, self.cboSimpleProjectType))
183  self.cboProfile.currentIndexChanged.connect(functools.partial(self.cboProfile_index_changed, self.cboProfile))
184  self.cboSimpleTarget.currentIndexChanged.connect(
185  functools.partial(self.cboSimpleTarget_index_changed, self.cboSimpleTarget))
186  self.cboSimpleVideoProfile.currentIndexChanged.connect(
187  functools.partial(self.cboSimpleVideoProfile_index_changed, self.cboSimpleVideoProfile))
188  self.cboSimpleQuality.currentIndexChanged.connect(
189  functools.partial(self.cboSimpleQuality_index_changed, self.cboSimpleQuality))
190  self.cboChannelLayout.currentIndexChanged.connect(self.updateChannels)
191  get_app().window.ExportFrame.connect(self.updateProgressBar)
192 
193  # ********* Advanced Profile List **********
194  # Loop through profiles
195  self.profile_names = []
196  self.profile_paths = {}
197  for profile_folder in [info.USER_PROFILES_PATH, info.PROFILES_PATH]:
198  for file in os.listdir(profile_folder):
199  # Load Profile
200  profile_path = os.path.join(profile_folder, file)
201  profile = openshot.Profile(profile_path)
202 
203  # Add description of Profile to list
204  profile_name = "%s (%sx%s)" % (profile.info.description, profile.info.width, profile.info.height)
205  self.profile_names.append(profile_name)
206  self.profile_paths[profile_name] = profile_path
207 
208  # Sort list
209  self.profile_names.sort()
210 
211  # Loop through sorted profiles
212  box_index = 0
214  for profile_name in self.profile_names:
215 
216  # Add to dropdown
217  self.cboProfile.addItem(self.getProfileName(self.getProfilePath(profile_name)), self.getProfilePath(profile_name))
218 
219  # Set default (if it matches the project)
220  if app.project.get(['profile']) in profile_name:
221  self.selected_profile_index = box_index
222 
223  # increment item counter
224  box_index += 1
225 
226 
227  # ********* Simple Project Type **********
228  # load the simple project type dropdown
229  presets = []
230  for file in os.listdir(info.EXPORT_PRESETS_DIR):
231  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
232  type = xmldoc.getElementsByTagName("type")
233  presets.append(_(type[0].childNodes[0].data))
234 
235  # Exclude duplicates
236  type_index = 0
237  selected_type = 0
238  presets = list(set(presets))
239  for item in sorted(presets):
240  self.cboSimpleProjectType.addItem(item, item)
241  if item == _("All Formats"):
242  selected_type = type_index
243  type_index += 1
244 
245  # Always select 'All Formats' option
246  self.cboSimpleProjectType.setCurrentIndex(selected_type)
247 
248 
249  # Populate all profiles
250  self.populateAllProfiles(app.project.get(['profile']))
251 
252  # Connect framerate signals
253  self.txtFrameRateNum.valueChanged.connect(self.updateFrameRate)
254  self.txtFrameRateDen.valueChanged.connect(self.updateFrameRate)
255  self.txtWidth.valueChanged.connect(self.updateFrameRate)
256  self.txtHeight.valueChanged.connect(self.updateFrameRate)
257  self.txtSampleRate.valueChanged.connect(self.updateFrameRate)
258  self.txtChannels.valueChanged.connect(self.updateFrameRate)
259  self.cboChannelLayout.currentIndexChanged.connect(self.updateFrameRate)
260 
261  # Determine the length of the timeline (in frames)
262  self.updateFrameRate()
263 
264  ##
265  # Callback for fps/profile changed event timer (to delay the timeline mapping so we don't spam libopenshot)
267  # Stop timer
268  self.delayed_fps_timer.stop()
269 
270  # Calculate fps
271  fps_double = self.timeline.info.fps.ToDouble()
272 
273  # Apply mapping if valid fps detected (anything larger than 300 fps is considered invalid)
274  if self.timeline and fps_double <= 300.0:
275  log.info("Valid framerate detected, sending to libopenshot: %s" % fps_double)
276  self.timeline.ApplyMapperToClips()
277  else:
278  log.warning("Invalid framerate detected, not sending it to libopenshot: %s" % fps_double)
279 
280  ##
281  # Get the profile path that matches the name
282  def getProfilePath(self, profile_name):
283  for profile, path in self.profile_paths.items():
284  if profile_name in profile:
285  return path
286 
287  ##
288  # Get the profile name that matches the name
289  def getProfileName(self, profile_path):
290  for profile, path in self.profile_paths.items():
291  if profile_path == path:
292  return profile
293 
294  ##
295  # Update progress bar during exporting
296  def updateProgressBar(self, path, start_frame, end_frame, current_frame):
297  percentage_string = "%4.1f%% " % (( current_frame - start_frame ) / ( end_frame - start_frame ) * 100)
298  self.progressExportVideo.setValue(current_frame)
299  self.progressExportVideo.setFormat(percentage_string)
300  self.setWindowTitle("%s %s" % (percentage_string, path))
301 
302  ##
303  # Update the # of channels to match the channel layout
304  def updateChannels(self):
305  log.info("updateChannels")
306  channels = self.txtChannels.value()
307  channel_layout = self.cboChannelLayout.currentData()
308 
309  if channel_layout == openshot.LAYOUT_MONO:
310  channels = 1
311  elif channel_layout == openshot.LAYOUT_STEREO:
312  channels = 2
313  elif channel_layout == openshot.LAYOUT_SURROUND:
314  channels = 3
315  elif channel_layout == openshot.LAYOUT_5POINT1:
316  channels = 6
317  elif channel_layout == openshot.LAYOUT_7POINT1:
318  channels = 8
319 
320  # Update channels to match layout
321  self.txtChannels.setValue(channels)
322 
323  ##
324  # Callback for changing the frame rate
325  def updateFrameRate(self):
326  # Adjust the main timeline reader
327  self.timeline.info.width = self.txtWidth.value()
328  self.timeline.info.height = self.txtHeight.value()
329  self.timeline.info.fps.num = self.txtFrameRateNum.value()
330  self.timeline.info.fps.den = self.txtFrameRateDen.value()
331  self.timeline.info.sample_rate = self.txtSampleRate.value()
332  self.timeline.info.channels = self.txtChannels.value()
333  self.timeline.info.channel_layout = self.cboChannelLayout.currentData()
334 
335  # Send changes to libopenshot (apply mappings to all framemappers)... after a small delay
336  self.delayed_fps_timer.start()
337 
338  # Determine max frame (based on clips)
339  timeline_length = 0.0
340  fps = self.timeline.info.fps.ToFloat()
341  clips = self.timeline.Clips()
342  for clip in clips:
343  clip_last_frame = clip.Position() + clip.Duration()
344  if clip_last_frame > timeline_length:
345  # Set max length of timeline
346  timeline_length = clip_last_frame
347 
348  # Convert to int and round
349  self.timeline_length_int = round(timeline_length * fps) + 1
350 
351  # Set the min and max frame numbers for this project
352  self.txtStartFrame.setValue(1)
353  self.txtEndFrame.setValue(self.timeline_length_int)
354 
355  # Init progress bar
356  self.progressExportVideo.setMinimum(self.txtStartFrame.value())
357  self.progressExportVideo.setMaximum(self.txtEndFrame.value())
358  self.progressExportVideo.setValue(self.txtStartFrame.value())
359 
360  def cboSimpleProjectType_index_changed(self, widget, index):
361  selected_project = widget.itemData(index)
362 
363  # set the target dropdown based on the selected project type
364  # first clear the combo
365  self.cboSimpleTarget.clear()
366 
367  # get translations
368  app = get_app()
369  _ = app._tr
370 
371  # parse the xml files and get targets that match the project type
372  project_types = []
373  for file in os.listdir(info.EXPORT_PRESETS_DIR):
374  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
375  type = xmldoc.getElementsByTagName("type")
376 
377  if _(type[0].childNodes[0].data) == selected_project:
378  titles = xmldoc.getElementsByTagName("title")
379  for title in titles:
380  project_types.append(_(title.childNodes[0].data))
381 
382  # Add all targets for selected project type
383  preset_index = 0
384  selected_preset = 0
385  for item in sorted(project_types):
386  self.cboSimpleTarget.addItem(item, item)
387 
388  # Find index of MP4/H.264
389  if item == _("MP4 (h.264)"):
390  selected_preset = preset_index
391 
392  preset_index += 1
393 
394  # Select MP4/H.264 as default
395  self.cboSimpleTarget.setCurrentIndex(selected_preset)
396 
397  def cboProfile_index_changed(self, widget, index):
398  selected_profile_path = widget.itemData(index)
399  log.info(selected_profile_path)
400 
401  # get translations
402  app = get_app()
403  _ = app._tr
404 
405  # Load profile
406  profile = openshot.Profile(selected_profile_path)
407 
408  # Load profile settings into advanced editor
409  self.txtWidth.setValue(profile.info.width)
410  self.txtHeight.setValue(profile.info.height)
411  self.txtFrameRateDen.setValue(profile.info.fps.den)
412  self.txtFrameRateNum.setValue(profile.info.fps.num)
413  self.txtAspectRatioNum.setValue(profile.info.display_ratio.num)
414  self.txtAspectRatioDen.setValue(profile.info.display_ratio.den)
415  self.txtPixelRatioNum.setValue(profile.info.pixel_ratio.num)
416  self.txtPixelRatioDen.setValue(profile.info.pixel_ratio.den)
417 
418  # Load the interlaced options
419  self.cboInterlaced.clear()
420  self.cboInterlaced.addItem(_("Yes"), "Yes")
421  self.cboInterlaced.addItem(_("No"), "No")
422  if profile.info.interlaced_frame:
423  self.cboInterlaced.setCurrentIndex(0)
424  else:
425  self.cboInterlaced.setCurrentIndex(1)
426 
427  def cboSimpleTarget_index_changed(self, widget, index):
428  selected_target = widget.itemData(index)
429  log.info(selected_target)
430 
431  # get translations
432  app = get_app()
433  _ = app._tr
434 
435  # don't do anything if the combo has been cleared
436  if selected_target:
437  profiles_list = []
438 
439  # Clear the following options (and remember current settings)
440  previous_quality = self.cboSimpleQuality.currentIndex()
441  if previous_quality < 0:
442  previous_quality = self.cboSimpleQuality.count() - 1
443  previous_profile = self.cboSimpleVideoProfile.currentIndex()
444  if previous_profile < 0:
445  previous_profile = self.selected_profile_index
446  self.cboSimpleVideoProfile.clear()
447  self.cboSimpleQuality.clear()
448 
449  # parse the xml to return suggested profiles
450  profile_index = 0
451  all_profiles = False
452  for file in os.listdir(info.EXPORT_PRESETS_DIR):
453  xmldoc = xml.parse(os.path.join(info.EXPORT_PRESETS_DIR, file))
454  title = xmldoc.getElementsByTagName("title")
455  if _(title[0].childNodes[0].data) == selected_target:
456  profiles = xmldoc.getElementsByTagName("projectprofile")
457 
458  # get the basic profile
459  all_profiles = False
460  if profiles:
461  # if profiles are defined, show them
462  for profile in profiles:
463  profiles_list.append(_(profile.childNodes[0].data))
464  else:
465  # show all profiles
466  all_profiles = True
467  for profile_name in self.profile_names:
468  profiles_list.append(profile_name)
469 
470  # get the video bit rate(s)
471  videobitrate = xmldoc.getElementsByTagName("videobitrate")
472  for rate in videobitrate:
473  v_l = rate.attributes["low"].value
474  v_m = rate.attributes["med"].value
475  v_h = rate.attributes["high"].value
476  self.vbr = {_("Low"): v_l, _("Med"): v_m, _("High"): v_h}
477 
478  # get the audio bit rates
479  audiobitrate = xmldoc.getElementsByTagName("audiobitrate")
480  for audiorate in audiobitrate:
481  a_l = audiorate.attributes["low"].value
482  a_m = audiorate.attributes["med"].value
483  a_h = audiorate.attributes["high"].value
484  self.abr = {_("Low"): a_l, _("Med"): a_m, _("High"): a_h}
485 
486  # get the remaining values
487  vf = xmldoc.getElementsByTagName("videoformat")
488  self.txtVideoFormat.setText(vf[0].childNodes[0].data)
489  vc = xmldoc.getElementsByTagName("videocodec")
490  self.txtVideoCodec.setText(vc[0].childNodes[0].data)
491  sr = xmldoc.getElementsByTagName("samplerate")
492  self.txtSampleRate.setValue(int(sr[0].childNodes[0].data))
493  c = xmldoc.getElementsByTagName("audiochannels")
494  self.txtChannels.setValue(int(c[0].childNodes[0].data))
495  c = xmldoc.getElementsByTagName("audiochannellayout")
496 
497  # check for compatible audio codec
498  ac = xmldoc.getElementsByTagName("audiocodec")
499  audio_codec_name = ac[0].childNodes[0].data
500  if audio_codec_name == "aac":
501  # Determine which version of AAC encoder is available
502  if openshot.FFmpegWriter.IsValidCodec("libfaac"):
503  self.txtAudioCodec.setText("libfaac")
504  elif openshot.FFmpegWriter.IsValidCodec("libvo_aacenc"):
505  self.txtAudioCodec.setText("libvo_aacenc")
506  elif openshot.FFmpegWriter.IsValidCodec("aac"):
507  self.txtAudioCodec.setText("aac")
508  else:
509  # fallback audio codec
510  self.txtAudioCodec.setText("ac3")
511  else:
512  # fallback audio codec
513  self.txtAudioCodec.setText(audio_codec_name)
514 
515  layout_index = 0
516  for layout in self.channel_layout_choices:
517  if layout == int(c[0].childNodes[0].data):
518  self.cboChannelLayout.setCurrentIndex(layout_index)
519  break
520  layout_index += 1
521 
522  # init the profiles combo
523  for item in sorted(profiles_list):
524  self.cboSimpleVideoProfile.addItem(self.getProfileName(self.getProfilePath(item)), self.getProfilePath(item))
525 
526  if all_profiles:
527  # select the project's current profile
528  self.cboSimpleVideoProfile.setCurrentIndex(previous_profile)
529 
530  # set the quality combo
531  # only populate with quality settings that exist
532  if v_l or a_l:
533  self.cboSimpleQuality.addItem(_("Low"), "Low")
534  if v_m or a_m:
535  self.cboSimpleQuality.addItem(_("Med"), "Med")
536  if v_h or a_h:
537  self.cboSimpleQuality.addItem(_("High"), "High")
538 
539  # Default to the highest quality setting
540  self.cboSimpleQuality.setCurrentIndex(previous_quality)
541 
542  def cboSimpleVideoProfile_index_changed(self, widget, index):
543  selected_profile_path = widget.itemData(index)
544  log.info(selected_profile_path)
545 
546  # Populate the advanced profile list
547  self.populateAllProfiles(selected_profile_path)
548 
549  ##
550  # Populate the full list of profiles
551  def populateAllProfiles(self, selected_profile_path):
552  # Look for matching profile in advanced options
553  profile_index = 0
554  for profile_name in self.profile_names:
555  # Check for matching profile
556  if self.getProfilePath(profile_name) == selected_profile_path:
557  # Matched!
558  self.cboProfile.setCurrentIndex(profile_index)
559  break
560 
561  # increment index
562  profile_index += 1
563 
564  def cboSimpleQuality_index_changed(self, widget, index):
565  selected_quality = widget.itemData(index)
566  log.info(selected_quality)
567 
568  # get translations
569  app = get_app()
570  _ = app._tr
571 
572  # Set the video and audio bitrates
573  if selected_quality:
574  self.txtVideoBitRate.setText(_(self.vbr[_(selected_quality)]))
575  self.txtAudioBitrate.setText(_(self.abr[_(selected_quality)]))
576 
577  def btnBrowse_clicked(self):
578  log.info("btnBrowse_clicked")
579 
580  # get translations
581  app = get_app()
582  _ = app._tr
583 
584  # update export folder path
585  file_path = QFileDialog.getExistingDirectory(self, _("Choose a Folder..."), self.txtExportFolder.text())
586  if os.path.exists(file_path):
587  self.txtExportFolder.setText(file_path)
588 
589  # update export folder path in project file
590  get_app().updates.update(["export_path"], file_path)
591 
592  def convert_to_bytes(self, BitRateString):
593  bit_rate_bytes = 0
594 
595  # split the string into pieces
596  s = BitRateString.lower().split(" ")
597  measurement = "kb"
598 
599  try:
600  # Get Bit Rate
601  if len(s) >= 2:
602  raw_number_string = s[0]
603  raw_measurement = s[1]
604 
605  # convert string number to float (based on locale settings)
606  raw_number = locale.atof(raw_number_string)
607 
608  if "kb" in raw_measurement:
609  measurement = "kb"
610  bit_rate_bytes = raw_number * 1000.0
611 
612  elif "mb" in raw_measurement:
613  measurement = "mb"
614  bit_rate_bytes = raw_number * 1000.0 * 1000.0
615 
616  except:
617  pass
618 
619  # return the bit rate in bytes
620  return str(int(bit_rate_bytes))
621 
622  ##
623  # Start exporting video
624  def accept(self):
625 
626  # get translations
627  app = get_app()
628  _ = app._tr
629 
630  # Disable controls
631  self.txtFileName.setEnabled(False)
632  self.txtExportFolder.setEnabled(False)
633  self.tabWidget.setEnabled(False)
634  self.export_button.setEnabled(False)
635  self.exporting = True
636 
637  # Determine type of export (video+audio, video, audio, image sequences)
638  # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence")
639  export_type = self.cboExportTo.currentText()
640 
641  # Determine final exported file path
642  if export_type != _("Image Sequence"):
643  file_name_with_ext = "%s.%s" % (self.txtFileName.text().strip(), self.txtVideoFormat.text().strip())
644  else:
645  file_name_with_ext = "%s%s" % (self.txtFileName.text().strip(), self.txtImageFormat.text().strip())
646  export_file_path = os.path.join(self.txtExportFolder.text().strip(), file_name_with_ext)
647  log.info(export_file_path)
648 
649  # Translate object
650  _ = get_app()._tr
651 
652  file = File.get(path=export_file_path)
653  if file:
654  ret = QMessageBox.question(self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext,
655  QMessageBox.Ok)
656  self.txtFileName.setEnabled(True)
657  self.txtExportFolder.setEnabled(True)
658  self.tabWidget.setEnabled(True)
659  self.export_button.setEnabled(True)
660  self.exporting = False
661  return
662 
663  # Handle exception
664  if os.path.exists(export_file_path) and export_type in [_("Video & Audio"), _("Video Only"), _("Audio Only")]:
665  # File already exists! Prompt user
666  ret = QMessageBox.question(self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext,
667  QMessageBox.No | QMessageBox.Yes)
668  if ret == QMessageBox.No:
669  # Stop and don't do anything
670  # Re-enable controls
671  self.txtFileName.setEnabled(True)
672  self.txtExportFolder.setEnabled(True)
673  self.tabWidget.setEnabled(True)
674  self.export_button.setEnabled(True)
675  self.exporting = False
676  return
677 
678  # Init export settings
679  video_settings = { "vformat": self.txtVideoFormat.text(),
680  "vcodec": self.txtVideoCodec.text(),
681  "fps": { "num" : self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value()},
682  "width": self.txtWidth.value(),
683  "height": self.txtHeight.value(),
684  "pixel_ratio": {"num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value()},
685  "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())),
686  "start_frame": self.txtStartFrame.value(),
687  "end_frame": self.txtEndFrame.value() + 1
688  }
689 
690  audio_settings = {"acodec": self.txtAudioCodec.text(),
691  "sample_rate": self.txtSampleRate.value(),
692  "channels": self.txtChannels.value(),
693  "channel_layout": self.cboChannelLayout.currentData(),
694  "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text()))
695  }
696 
697  # Override vcodec and format for Image Sequences
698  if export_type == _("Image Sequence"):
699  image_ext = os.path.splitext(self.txtImageFormat.text().strip())[1].replace(".", "")
700  video_settings["vformat"] = image_ext
701  if image_ext in ["jpg", "jpeg"]:
702  video_settings["vcodec"] = "mjpeg"
703  else:
704  video_settings["vcodec"] = image_ext
705 
706  # Set MaxSize (so we don't have any downsampling)
707  self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height"))
708 
709  # Set lossless cache settings (temporarily)
710  export_cache_object = openshot.CacheMemory(250)
711  self.timeline.SetCache(export_cache_object)
712 
713  # Create FFmpegWriter
714  try:
715  w = openshot.FFmpegWriter(export_file_path)
716 
717  # Set video options
718  if export_type in [_("Video & Audio"), _("Video Only"), _("Image Sequence")]:
719  w.SetVideoOptions(True,
720  video_settings.get("vcodec"),
721  openshot.Fraction(video_settings.get("fps").get("num"),
722  video_settings.get("fps").get("den")),
723  video_settings.get("width"),
724  video_settings.get("height"),
725  openshot.Fraction(video_settings.get("pixel_ratio").get("num"),
726  video_settings.get("pixel_ratio").get("den")),
727  False,
728  False,
729  video_settings.get("video_bitrate"))
730 
731  # Set audio options
732  if export_type in [_("Video & Audio"), _("Audio Only")]:
733  w.SetAudioOptions(True,
734  audio_settings.get("acodec"),
735  audio_settings.get("sample_rate"),
736  audio_settings.get("channels"),
737  audio_settings.get("channel_layout"),
738  audio_settings.get("audio_bitrate"))
739 
740  # Open the writer
741  w.Open()
742 
743  # Notify window of export started
744  export_file_path = ""
745  get_app().window.ExportStarted.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"))
746 
747  progressstep = max(1 , round(( video_settings.get("end_frame") - video_settings.get("start_frame") ) / 1000))
748  start_time_export = time.time()
749  start_frame_export = video_settings.get("start_frame")
750  end_frame_export = video_settings.get("end_frame")
751  # Write each frame in the selected range
752  for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame")):
753  # Update progress bar (emit signal to main window)
754  if (frame % progressstep) == 0:
755  end_time_export = time.time()
756  if ((( frame - start_frame_export ) != 0) & (( end_time_export - start_time_export ) != 0)):
757  seconds_left = round(( start_time_export - end_time_export )*( frame - end_frame_export )/( frame - start_frame_export ))
758  fps_encode = ((frame - start_frame_export)/(end_time_export-start_time_export))
759  export_file_path = _("%(hours)d:%(minutes)02d:%(seconds)02d Remaining (%(fps)5.2f FPS)") % { 'hours' : seconds_left / 3600,
760  'minutes': (seconds_left / 60) % 60,
761  'seconds': seconds_left % 60,
762  'fps': fps_encode }
763  get_app().window.ExportFrame.emit(export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"), frame)
764 
765  # Process events (to show the progress bar moving)
766  QCoreApplication.processEvents()
767 
768  # Write the frame object to the video
769  w.WriteFrame(self.timeline.GetFrame(frame))
770 
771  # Check if we need to bail out
772  if not self.exporting:
773  break
774 
775  # Close writer
776  w.Close()
777 
778 
779  except Exception as e:
780  # TODO: Find a better way to catch the error. This is the only way I have found that
781  # does not throw an error
782  error_type_str = str(e)
783  log.info("Error type string: %s" % error_type_str)
784 
785  if "InvalidChannels" in error_type_str:
786  log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels")))
787  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")))
788 
789  elif "InvalidSampleRate" in error_type_str:
790  log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate")))
791  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")))
792 
793  elif "InvalidFormat" in error_type_str:
794  log.info("Error setting invalid format (%s)" % (video_settings.get("vformat")))
795  track_metric_error("invalid-format-%s" % (video_settings.get("vformat")))
796 
797  elif "InvalidCodec" in error_type_str:
798  log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
799  track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
800 
801  elif "ErrorEncodingVideo" in error_type_str:
802  log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
803  track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec")))
804 
805  # Show friendly error
806  friendly_error = error_type_str.split("> ")[0].replace("<", "")
807 
808  # Prompt error message
809  msg = QMessageBox()
810  _ = get_app()._tr
811  msg.setWindowTitle(_("Export Error"))
812  msg.setText(_("Sorry, there was an error exporting your video: \n%s") % friendly_error)
813  msg.exec_()
814 
815  # Notify window of export started
816  get_app().window.ExportEnded.emit(export_file_path)
817 
818  # Close timeline object
819  self.timeline.Close()
820 
821  # Clear all cache
822  self.timeline.ClearAllCache()
823 
824  # Re-set OMP thread enabled flag
825  if self.s.get("omp_threads_enabled"):
826  os.environ['OS2_OMP_THREADS'] = "1"
827  else:
828  os.environ['OS2_OMP_THREADS'] = "0"
829 
830  # Accept dialog
831  super(Export, self).accept()
832 
833  def reject(self):
834  # Re-set OMP thread enabled flag
835  if self.s.get("omp_threads_enabled"):
836  os.environ['OS2_OMP_THREADS'] = "1"
837  else:
838  os.environ['OS2_OMP_THREADS'] = "0"
839 
840  # Cancel dialog
841  self.exporting = False
842  super(Export, self).reject()
def cboSimpleQuality_index_changed
Definition: export.py:564
def reject
Definition: export.py:833
Export Dialog.
Definition: export.py:52
def updateFrameRate
Callback for changing the frame rate.
Definition: export.py:325
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:266
channel_layout_choices
Definition: export.py:169
def cboSimpleProjectType_index_changed
Definition: export.py:360
def track_metric_error
Track an error has occurred.
Definition: metrics.py:121
def cboSimpleVideoProfile_index_changed
Definition: export.py:542
def btnBrowse_clicked
Definition: export.py:577
def getProfileName
Get the profile name that matches the name.
Definition: export.py:289
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:213
def cboProfile_index_changed
Definition: export.py:397
def __init__
Definition: export.py:57
timeline_length_int
Definition: export.py:349
def updateProgressBar
Update progress bar during exporting.
Definition: export.py:296
def updateChannels
Update the # of channels to match the channel layout.
Definition: export.py:304
def getProfilePath
Get the profile path that matches the name.
Definition: export.py:282
def cboSimpleTarget_index_changed
Definition: export.py:427
delayed_fps_timer
Definition: export.py:89
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:220
def populateAllProfiles
Populate the full list of profiles.
Definition: export.py:551
def convert_to_bytes
Definition: export.py:592
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44
def accept
Start exporting video.
Definition: export.py:624
tuple ui_path
Definition: export.py:55