def test_get_File(self): """ Test the File.get method """ file = File.get(id=self.file_ids[1]) self.assertTrue(file) # Do not find a File file = File.get(id="invalidID") self.assertEqual(file, None)
def test_get_File(self): """ Test the File.get method """ # Import additional classes that need the app defined first from classes.query import File # Find a File named file1 file = File.get(id=TestQueryClass.file_ids[1]) self.assertTrue(file) # Do not find a File file = File.get(id="invalidID") self.assertEqual(file, None)
def value_updated(self, item): """ Name or tags updated """ # Get translation method _ = get_app()._tr # Determine what was changed file_id = self.files_model.model.item(item.row(), 5).text() name = self.files_model.model.item(item.row(), 1).text() tags = self.files_model.model.item(item.row(), 2).text() # Get file object and update friendly name and tags attribute f = File.get(id=file_id) if name != f.data["path"]: f.data["name"] = name else: f.data["name"] = "" if "tags" in f.data.keys(): if tags != f.data["tags"]: f.data["tags"] = tags elif tags: f.data["tags"] = tags # Tell file model to ignore updates (since this treeview will already be updated) self.files_model.ignore_update_signal = True # Save File f.save() # Re-enable updates self.files_model.ignore_update_signal = False
def test_update_File(self): """ Test the File.save method """ update_id = self.file_ids[0] file = File.get(id=update_id) self.assertTrue(file) # Update File file.data["height"] = 1080 file.data["width"] = 1920 file.save() # Verify updated data file = File.get(id=update_id) self.assertEqual(file.data["height"], 1080) self.assertEqual(file.data["width"], 1920)
def update_file_thumbnail(self, file_id): """Update/re-generate the thumbnail of a specific file""" file = File.get(id=file_id) path, filename = os.path.split(file.data["path"]) name = file.data.get("name", filename) # Refresh thumbnail for updated file self.ignore_updates = True m = self.model if file_id in self.model_ids: # Look up stored index to ID column id_index = self.model_ids[file_id] if not id_index.isValid(): return # Update thumb for file thumb_path = self.get_thumb_path(file_id, 1, clear_cache=True) thumb_index = id_index.sibling(id_index.row(), 0) item = m.itemFromIndex(thumb_index) item.setIcon(QIcon(thumb_path)) item.setText(name) # Emit signal when model is updated self.ModelRefreshed.emit() self.ignore_updates = False
def contextMenuEvent(self, event): # Update selection self.updateSelection() # Set context menu mode app = get_app() app.context_menu_object = "files" menu = QMenu(self) menu.addAction(self.win.actionImportFiles) menu.addAction(self.win.actionThumbnailView) if self.selected: # If file selected, show file related options menu.addSeparator() # Add edit title option (if svg file) selected_file_id = self.win.selected_files[0] file = File.get(id=selected_file_id) if file and file.data.get("path").endswith(".svg"): menu.addAction(self.win.actionEditTitle) menu.addAction(self.win.actionDuplicateTitle) menu.addSeparator() menu.addAction(self.win.actionPreview_File) menu.addAction(self.win.actionSplitClip) menu.addAction(self.win.actionAdd_to_Timeline) menu.addAction(self.win.actionFile_Properties) menu.addSeparator() menu.addAction(self.win.actionRemove_from_Project) menu.addSeparator() # Show menu menu.exec_(QCursor.pos())
def add_file(self, filepath): # Add file into project app = get_app() _ = app._tr # Check for this path in our existing project data # ["1F595-1F3FE", # "openshot-qt-git/src/emojis/color/svg/1F595-1F3FE.svg"] file = File.get(path=filepath) # If this file is already found, exit if file: return file # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader try: reader = clip.Reader() file_data = json.loads(reader.Json()) # Determine media type file_data["media_type"] = "image" # Save new file to the project data file = File() file.data = file_data file.save() return file except Exception as ex: # Log exception log.warning("Failed to import file: {}".format(str(ex)))
def test_delete_File(self): """ Test the File.delete method """ delete_id = self.file_ids[4] file = File.get(id=delete_id) self.assertTrue(file) file.delete() # Verify deleted data deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file) # Delete File again (should do nothing) file.delete() deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file)
def rect_select_clicked(self, widget, param): """Rect select button clicked""" self.context[param["setting"]].update({"button-clicked": True}) # show dialog from windows.region import SelectRegion from classes.query import File, Clip c = Clip.get(id=self.clip_id) reader_path = c.data.get('reader', {}).get('path','') f = File.get(path=reader_path) if f: win = SelectRegion(f, self.clip_instance) # Run the dialog event loop - blocking interaction on this window during that time result = win.exec_() if result == QDialog.Accepted: # self.first_frame = win.current_frame # Region selected (get coordinates if any) topLeft = win.videoPreview.regionTopLeftHandle bottomRight = win.videoPreview.regionBottomRightHandle viewPortSize = win.viewport_rect curr_frame_size = win.videoPreview.curr_frame_size x1 = topLeft.x() / curr_frame_size.width() y1 = topLeft.y() / curr_frame_size.height() x2 = bottomRight.x() / curr_frame_size.width() y2 = bottomRight.y() / curr_frame_size.height() # Get QImage of region if win.videoPreview.region_qimage: region_qimage = win.videoPreview.region_qimage # Resize QImage to match button size resized_qimage = region_qimage.scaled(widget.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) # Draw Qimage onto QPushButton (to display region selection to user) palette = widget.palette() palette.setBrush(widget.backgroundRole(), QBrush(resized_qimage)) widget.setFlat(True) widget.setAutoFillBackground(True) widget.setPalette(palette) # Remove button text (so region QImage is more visible) widget.setText("") # If data found, add to context if topLeft and bottomRight: self.context[param["setting"]].update({"normalized_x": x1, "normalized_y": y1, "normalized_width": x2-x1, "normalized_height": y2-y1, "first-frame": win.current_frame, }) log.info(self.context) else: log.error('No file found with path: %s' % reader_path)
def test_update_File(self): """ Test the File.save method """ # Import additional classes that need the app defined first from classes.query import File # Find a File named file1 update_id = TestQueryClass.file_ids[0] file = File.get(id=update_id) self.assertTrue(file) # Update File file.data["height"] = 1080 file.data["width"] = 1920 file.save() # Verify updated data # Get File again file = File.get(id=update_id) self.assertEqual(file.data["height"], 1080) self.assertEqual(file.data["width"], 1920)
def test_delete_File(self): """ Test the File.delete method """ # Import additional classes that need the app defined first from classes.query import File # Find a File named file1 delete_id = TestQueryClass.file_ids[4] file = File.get(id=delete_id) self.assertTrue(file) # Delete File file.delete() # Verify deleted data deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file) # Delete File again (should do nothing file.delete() # Verify deleted data deleted_file = File.get(id=delete_id) self.assertFalse(deleted_file)
def contextMenuEvent(self, event): # Set context menu mode app = get_app() app.context_menu_object = "files" index = self.indexAt(event.pos()) # Build menu menu = QMenu(self) menu.addAction(self.win.actionImportFiles) menu.addAction(self.win.actionThumbnailView) if index.isValid(): # Look up the model item and our unique ID item = self.files_model.model.itemFromIndex(index) file_id = self.files_model.model.item(item.row(), 5).text() try: # Check whether we know the item is selected i = self.win.selected_files.index(file_id) except ValueError: # Add to our list, if it's not already there self.win.selected_files.append(file_id) # If a valid file is selected, show file related options menu.addSeparator() # Add edit title option (if svg file) file = File.get(id=file_id) if file and file.data.get("path").endswith(".svg"): menu.addAction(self.win.actionEditTitle) menu.addAction(self.win.actionDuplicateTitle) menu.addSeparator() menu.addAction(self.win.actionPreview_File) menu.addAction(self.win.actionSplitClip) menu.addAction(self.win.actionAdd_to_Timeline) menu.addAction(self.win.actionFile_Properties) menu.addSeparator() menu.addAction(self.win.actionRemove_from_Project) menu.addSeparator() # Show menu menu.exec_(event.globalPos())
def add_file(self, filepath): path, filename = os.path.split(filepath) # Add file into project app = get_app() _ = get_app()._tr # Check for this path in our existing project data file = File.get(path=filepath) # If this file is already found, exit if file: return # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader try: reader = clip.Reader() file_data = json.loads(reader.Json()) # Determine media type if file_data["has_video"] and not self.is_image(file_data): file_data["media_type"] = "video" elif file_data["has_video"] and self.is_image(file_data): file_data["media_type"] = "image" elif file_data["has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" # Save new file to the project data file = File() file.data = file_data file.save() return True except: # Handle exception msg = QMessageBox() msg.setText( _("{} is not a valid video, audio, or image file.".format( filename))) msg.exec_() return False
def contextMenuEvent(self, event): event.accept() # Set context menu mode app = get_app() app.context_menu_object = "files" index = self.indexAt(event.pos()) # Build menu menu = QMenu(self) menu.addAction(self.win.actionImportFiles) menu.addAction(self.win.actionDetailsView) if index.isValid(): # Look up the model item and our unique ID model = self.model() # Look up file_id from 5th column of row id_index = index.sibling(index.row(), 5) file_id = model.data(id_index, Qt.DisplayRole) # If a valid file selected, show file related options menu.addSeparator() # Add edit title option (if svg file) file = File.get(id=file_id) if file and file.data.get("path").endswith(".svg"): menu.addAction(self.win.actionEditTitle) menu.addAction(self.win.actionDuplicateTitle) menu.addSeparator() menu.addAction(self.win.actionPreview_File) menu.addAction(self.win.actionSplitClip) menu.addAction(self.win.actionAdd_to_Timeline) menu.addAction(self.win.actionFile_Properties) menu.addSeparator() menu.addAction(self.win.actionRemove_from_Project) menu.addSeparator() # Show menu menu.popup(event.globalPos())
def add_file(self, filepath): """ Add an animation to the project file tree """ path, filename = os.path.split(filepath) # Add file into project app = get_app() _ = get_app()._tr # Check for this path in our existing project data file = File.get(path=filepath) # If this file is already found, exit if file: return # Get the JSON for the clip's internal reader try: # Open image sequence in FFmpegReader reader = openshot.FFmpegReader(filepath) reader.Open() # Serialize JSON for the reader file_data = json.loads(reader.Json()) # Set media type file_data["media_type"] = "video" # Save new file to the project data file = File() file.data = file_data file.save() return True except: # Handle exception msg = QMessageBox() msg.setText( _("{} is not a valid video, audio, or image file.".format( filename))) msg.exec_() return False
def add_file(self, filepath): path, filename = os.path.split(filepath) # Add file into project app = get_app() _ = get_app()._tr # Check for this path in our existing project data file = File.get(path=filepath) # If this file is already found, exit if file: return # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader reader = clip.Reader() file_data = json.loads(reader.Json()) print("file_data:", file_data) # Determine media type if file_data["has_video"]: file_data["media_type"] = "video" elif file_data["has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" # Save new file to the project data file = File() file.data = file_data # Save file file.save() # open in timeline added by yanght====== self.timeline.addNewClip(file) return True
def add_file(self, filepath): filename = os.path.basename(filepath) # Add file into project app = get_app() _ = get_app()._tr # Check for this path in our existing project data file = File.get(path=filepath) # If this file is already found, exit if file: return # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader try: reader = clip.Reader() file_data = json.loads(reader.Json()) # Set media type file_data["media_type"] = "image" # Save new file to the project data file = File() file.data = file_data file.save() return True except Exception as ex: # Handle exception log.error('Could not import {}: {}'.format(filename, str(ex))) msg = QMessageBox() msg.setText(_("{} is not a valid video, audio, or image file.".format(filename))) msg.exec_() return False
def value_updated(self, item): """ Name or tags updated """ if self.files_model.ignore_updates: return # Get translation method _ = get_app()._tr # Determine what was changed file_id = self.files_model.model.item(item.row(), 5).text() name = self.files_model.model.item(item.row(), 1).text() tags = self.files_model.model.item(item.row(), 2).text() # Get file object and update friendly name and tags attribute f = File.get(id=file_id) f.data.update({"name": name or os.path.basename(f.data.get("path"))}) if "tags" in f.data or tags: f.data.update({"tags": tags}) # Save File f.save() # Update file thumbnail self.win.FileUpdated.emit(file_id)
def selected_files(self): """ Get a list of File objects representing the current selection """ files = [] for id in self.selected_file_ids(): files.append(File.get(id=id)) return files
def do_GET(self): # Pause processing of request (since we don't currently use thread pooling, this allows # the threads to be processed without choking the CPU as much # TODO: Make HTTPServer work with a limited thread pool and remove this sleep() hack. time.sleep(0.01) """ Process each GET request and return a value (image or file path)""" # Parse URL url_output = REGEX_THUMBNAIL_URL.match(self.path) if url_output and len(url_output.groups()) == 3: # Path is expected to have 3 matched components (third is optional though) # /thumbnails/FILE-ID/FRAME-NUMBER/ or # /thumbnails/FILE-ID/FRAME-NUMBER/path/ self.send_response_only(200) else: self.send_error(404) return # Get URL parts file_id = url_output.group('file_id') file_frame = int(url_output.group('file_frame')) only_path = url_output.group('only_path') try: # Look up file data file = File.get(id=file_id) # Ensure file location is an absolute path file_path = file.absolute_path() except AttributeError: # Couldn't match file ID self.send_error(404) return # Send headers if not only_path: self.send_header('Content-type', 'image/png') else: self.send_header('Content-type', 'text/html') self.end_headers() # Locate thumbnail thumb_path = os.path.join(info.THUMBNAIL_PATH, file_id, "%s.png" % file_frame) if not os.path.exists(thumb_path) and file_frame == 1: # Try ID with no frame # (for backwards compatibility) thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file_id) if not os.path.exists(thumb_path) and file_frame != 1: # Try with ID and frame # in filename (for backwards compatibility) thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s-%s.png" % (file_id, file_frame)) if not os.path.exists(thumb_path): # Generate thumbnail (since we can't find it) # Determine if video overlay should be applied to thumbnail overlay_path = "" if file.data["media_type"] == "video": overlay_path = os.path.join(info.IMAGES_PATH, "overlay.png") # Create thumbnail image GenerateThumbnail(file_path, thumb_path, file_frame, 98, 64, os.path.join(info.IMAGES_PATH, "mask.png"), overlay_path) # Send message back to client if os.path.exists(thumb_path): if not only_path: self.wfile.write(open(thumb_path, 'rb').read()) else: self.wfile.write(bytes(thumb_path, "utf-8")) return
def add_file(self, filepath): path, filename = os.path.split(filepath) # Add file into project app = get_app() _ = get_app()._tr # Check for this path in our existing project data file = File.get(path=filepath) # If this file is already found, exit if file: return # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader try: reader = clip.Reader() file_data = json.loads(reader.Json()) # Determine media type if file_data["has_video"] and not self.is_image(file_data): file_data["media_type"] = "video" elif file_data["has_video"] and self.is_image(file_data): file_data["media_type"] = "image" elif file_data["has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" # Save new file to the project data file = File() file.data = file_data # Is this file an image sequence / animation? image_seq_details = self.get_image_sequence_details(filepath) if image_seq_details: # Update file with correct path folder_path = image_seq_details["folder_path"] file_name = image_seq_details["file_path"] base_name = image_seq_details["base_name"] fixlen = image_seq_details["fixlen"] digits = image_seq_details["digits"] extension = image_seq_details["extension"] if not fixlen: zero_pattern = "%d" else: zero_pattern = "%%0%sd" % digits # Generate the regex pattern for this image sequence pattern = "%s%s.%s" % (base_name, zero_pattern, extension) # Split folder name (parentPath, folderName) = os.path.split(folder_path) if not base_name: # Give alternate name file.data["name"] = "%s (%s)" % (folderName, pattern) # Load image sequence (to determine duration and video_length) image_seq = openshot.Clip(os.path.join(folder_path, pattern)) # Update file details file.data["path"] = os.path.join(folder_path, pattern) file.data["media_type"] = "video" file.data["duration"] = image_seq.Reader().info.duration file.data["video_length"] = image_seq.Reader( ).info.video_length # Save file file.save() return True except: # Handle exception msg = QMessageBox() msg.setText( _("{} is not a valid video, audio, or image file.".format( filename))) msg.exec_() return False
def accept(self): """ Start exporting video """ # get translations _ = get_app()._tr # Init progress bar self.progressExportVideo.setMinimum(self.txtStartFrame.value()) self.progressExportVideo.setMaximum(self.txtEndFrame.value()) self.progressExportVideo.setValue(self.txtStartFrame.value()) # Prompt error message if self.txtStartFrame.value() == self.txtEndFrame.value(): msg = QMessageBox() msg.setWindowTitle(_("Export Error")) msg.setText( _("Sorry, please select a valid range of frames to export")) msg.exec_() # Do nothing self.enableControls() self.exporting = False return # Disable controls self.disableControls() self.exporting = True # Determine type of export (video+audio, video, audio, image sequences) # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence") export_type = self.cboExportTo.currentText() # Determine final exported file path (and replace blank paths with default ones) default_filename = "Untitled Project" default_folder = os.path.join(info.HOME_PATH) if export_type == _("Image Sequence"): file_name_with_ext = "%s%s" % (self.txtFileName.text().strip() or default_filename, self.txtImageFormat.text().strip()) else: file_ext = self.txtVideoFormat.text().strip() file_name_with_ext = self.txtFileName.text().strip( ) or default_filename # Append extension, if not already present if not file_name_with_ext.endswith(file_ext): file_name_with_ext = '{}.{}'.format(file_name_with_ext, file_ext) export_file_path = os.path.join( self.txtExportFolder.text().strip() or default_folder, file_name_with_ext) log.info("Export path: %s" % export_file_path) # Check if filename is valid (by creating a blank file in a temporary place) try: open(os.path.join(tempfile.gettempdir(), file_name_with_ext), 'w') except OSError: # Invalid path detected, so use default file name instead file_name_with_ext = "%s.%s" % (default_filename, self.txtVideoFormat.text().strip()) export_file_path = os.path.join( self.txtExportFolder.text().strip() or default_folder, file_name_with_ext) log.info("Invalid export path detected, changing to: %s" % export_file_path) file = File.get(path=export_file_path) if file: ret = QMessageBox.question( self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext, QMessageBox.Ok) self.enableControls() self.exporting = False return # Handle exception if os.path.exists(export_file_path) and export_type in [ _("Video & Audio"), _("Video Only"), _("Audio Only") ]: # File already exists! Prompt user ret = QMessageBox.question( self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext, QMessageBox.No | QMessageBox.Yes) if ret == QMessageBox.No: # Stop and don't do anything # Re-enable controls self.enableControls() self.exporting = False return # Init export settings video_settings = { "vformat": self.txtVideoFormat.text(), "vcodec": self.txtVideoCodec.text(), "fps": { "num": self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value() }, "width": self.txtWidth.value(), "height": self.txtHeight.value(), "pixel_ratio": { "num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value() }, "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())), "start_frame": self.txtStartFrame.value(), "end_frame": self.txtEndFrame.value() } audio_settings = { "acodec": self.txtAudioCodec.text(), "sample_rate": self.txtSampleRate.value(), "channels": self.txtChannels.value(), "channel_layout": self.cboChannelLayout.currentData(), "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text())) } # Override vcodec and format for Image Sequences if export_type == _("Image Sequence"): image_ext = os.path.splitext( self.txtImageFormat.text().strip())[1].replace(".", "") video_settings["vformat"] = image_ext if image_ext in ["jpg", "jpeg"]: video_settings["vcodec"] = "mjpeg" else: video_settings["vcodec"] = image_ext # Store updated export folder path in project file get_app().updates.update_untracked(["export_path"], os.path.dirname(export_file_path)) # Mark project file as unsaved get_app().project.has_unsaved_changes = True # Set MaxSize (so we don't have any downsampling) self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height")) # Set lossless cache settings (temporarily) export_cache_object = openshot.CacheMemory(500) self.timeline.SetCache(export_cache_object) # Rescale all keyframes (if needed) if self.export_fps_factor != 1.0: # Get a copy of rescaled project data (this does not modify the active project) rescaled_app_data = get_app().project.rescale_keyframes( self.export_fps_factor) # Load the "export" Timeline reader with the JSON from the real timeline self.timeline.SetJson(json.dumps(rescaled_app_data)) # Re-update the timeline FPS again (since the timeline just got clobbered) self.updateFrameRate() # Create FFmpegWriter try: w = openshot.FFmpegWriter(export_file_path) # Set video options if export_type in [ _("Video & Audio"), _("Video Only"), _("Image Sequence") ]: w.SetVideoOptions( True, video_settings.get("vcodec"), openshot.Fraction( video_settings.get("fps").get("num"), video_settings.get("fps").get("den")), video_settings.get("width"), video_settings.get("height"), openshot.Fraction( video_settings.get("pixel_ratio").get("num"), video_settings.get("pixel_ratio").get("den")), False, False, video_settings.get("video_bitrate")) # Set audio options if export_type in [_("Video & Audio"), _("Audio Only")]: w.SetAudioOptions(True, audio_settings.get("acodec"), audio_settings.get("sample_rate"), audio_settings.get("channels"), audio_settings.get("channel_layout"), audio_settings.get("audio_bitrate")) # Prepare the streams w.PrepareStreams() # These extra options should be set in an extra method # No feedback is given to the user # TODO: Tell user if option is not available if export_type in [_("Audio Only")]: # Muxing options for mp4/mov w.SetOption(openshot.AUDIO_STREAM, "muxing_preset", "mp4_faststart") else: # Muxing options for mp4/mov w.SetOption(openshot.VIDEO_STREAM, "muxing_preset", "mp4_faststart") # Set the quality in case crf was selected if "crf" in self.txtVideoBitRate.text(): w.SetOption(openshot.VIDEO_STREAM, "crf", str(int(video_settings.get("video_bitrate")))) # Open the writer w.Open() # Notify window of export started title_message = "" get_app().window.ExportStarted.emit( export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame")) progressstep = max( 1, round((video_settings.get("end_frame") - video_settings.get("start_frame")) / 1000)) start_time_export = time.time() start_frame_export = video_settings.get("start_frame") end_frame_export = video_settings.get("end_frame") # Write each frame in the selected range for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame") + 1): # Update progress bar (emit signal to main window) if (frame % progressstep) == 0: end_time_export = time.time() if (((frame - start_frame_export) != 0) & ((end_time_export - start_time_export) != 0)): seconds_left = round( (start_time_export - end_time_export) * (frame - end_frame_export) / (frame - start_frame_export)) fps_encode = ((frame - start_frame_export) / (end_time_export - start_time_export)) title_message = _( "%(hours)d:%(minutes)02d:%(seconds)02d Remaining (%(fps)5.2f FPS)" ) % { 'hours': seconds_left / 3600, 'minutes': (seconds_left / 60) % 60, 'seconds': seconds_left % 60, 'fps': fps_encode } # Emit frame exported get_app().window.ExportFrame.emit( title_message, video_settings.get("start_frame"), video_settings.get("end_frame"), frame) # Process events (to show the progress bar moving) QCoreApplication.processEvents() # Write the frame object to the video w.WriteFrame(self.timeline.GetFrame(frame)) # Check if we need to bail out if not self.exporting: break # Close writer w.Close() # Emit final exported frame (with elapsed time) seconds_run = round((end_time_export - start_time_export)) title_message = _( "%(hours)d:%(minutes)02d:%(seconds)02d Elapsed (%(fps)5.2f FPS)" ) % { 'hours': seconds_run / 3600, 'minutes': (seconds_run / 60) % 60, 'seconds': seconds_run % 60, 'fps': fps_encode } get_app().window.ExportFrame.emit( title_message, video_settings.get("start_frame"), video_settings.get("end_frame"), frame) except Exception as e: # TODO: Find a better way to catch the error. This is the only way I have found that # does not throw an error error_type_str = str(e) log.info("Error type string: %s" % error_type_str) if "InvalidChannels" in error_type_str: log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels"))) 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"))) elif "InvalidSampleRate" in error_type_str: log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate"))) 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"))) elif "InvalidFormat" in error_type_str: log.info("Error setting invalid format (%s)" % (video_settings.get("vformat"))) track_metric_error("invalid-format-%s" % (video_settings.get("vformat"))) elif "InvalidCodec" in error_type_str: log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) elif "ErrorEncodingVideo" in error_type_str: log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) # Show friendly error friendly_error = error_type_str.split("> ")[0].replace("<", "") # Prompt error message msg = QMessageBox() msg.setWindowTitle(_("Export Error")) msg.setText( _("Sorry, there was an error exporting your video: \n%s") % friendly_error) msg.exec_() # Notify window of export started get_app().window.ExportEnded.emit(export_file_path) # Close timeline object self.timeline.Close() # Clear all cache self.timeline.ClearAllCache() # Re-set OMP thread enabled flag if self.s.get("omp_threads_enabled"): openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = False else: openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = True # Return scale mode to lower quality scaling (for faster previews) openshot.Settings.Instance().HIGH_QUALITY_SCALING = False # Handle end of export (for non-canceled exports) if self.s.get("show_finished_window") and self.exporting: # Hide cancel and export buttons self.cancel_button.setVisible(False) self.export_button.setVisible(False) # Reveal done button self.close_button.setVisible(True) # Make progress bar green (to indicate we are done) from PyQt5.QtGui import QPalette p = QPalette() p.setColor(QPalette.Highlight, Qt.green) self.progressExportVideo.setPalette(p) # Raise the window self.show() else: # Accept dialog super(Export, self).accept()
def import_xml(): """Import final cut pro XML file""" app = get_app() _ = app._tr # Get FPS info fps_num = app.project.get("fps").get("num", 24) fps_den = app.project.get("fps").get("den", 1) fps_float = float(fps_num / fps_den) # Get XML path recommended_path = app.project.current_filepath or "" if not recommended_path: recommended_path = info.HOME_PATH else: recommended_path = os.path.dirname(recommended_path) file_path = QFileDialog.getOpenFileName(app.window, _("Import XML..."), recommended_path, _("Final Cut Pro (*.xml)"), _("Final Cut Pro (*.xml)"))[0] if not file_path or not os.path.exists(file_path): # User canceled dialog return # Parse XML file xmldoc = minidom.parse(file_path) # Get video tracks video_tracks = [] for video_element in xmldoc.getElementsByTagName("video"): for video_track in video_element.getElementsByTagName("track"): video_tracks.append(video_track) audio_tracks = [] for audio_element in xmldoc.getElementsByTagName("audio"): for audio_track in audio_element.getElementsByTagName("track"): audio_tracks.append(audio_track) # Loop through tracks track_index = 0 for tracks in [audio_tracks, video_tracks]: for track_element in tracks: # Get clipitems on this track (if any) clips_on_track = track_element.getElementsByTagName("clipitem") if not clips_on_track: continue # Get # of tracks track_index += 1 all_tracks = app.project.get("layers") track_number = list( reversed(sorted( all_tracks, key=itemgetter('number'))))[0].get("number") + 1000000 # Create new track above existing layer(s) track = Track() is_locked = False if track_element.getElementsByTagName( "locked")[0].childNodes[0].nodeValue == "TRUE": is_locked = True track.data = { "number": track_number, "y": 0, "label": "XML Import %s" % track_index, "lock": is_locked } track.save() # Loop through clips for clip_element in clips_on_track: # Get clip path xml_file_id = clip_element.getElementsByTagName( "file")[0].getAttribute("id") clip_path = "" if clip_element.getElementsByTagName("pathurl"): clip_path = clip_element.getElementsByTagName( "pathurl")[0].childNodes[0].nodeValue else: # Skip clipitem if no clippath node found # This usually happens for linked audio clips (which OpenShot combines audio and thus ignores this) continue clip_path, is_modified, is_skipped = find_missing_file( clip_path) if is_skipped: continue # Check for this path in our existing project data file = File.get(path=clip_path) # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip_obj = openshot.Clip(clip_path) if not file: # Get the JSON for the clip's internal reader try: reader = clip_obj.Reader() file_data = json.loads(reader.Json()) # Determine media type if file_data["has_video"] and not is_image(file_data): file_data["media_type"] = "video" elif file_data["has_video"] and is_image(file_data): file_data["media_type"] = "image" elif file_data[ "has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" # Save new file to the project data file = File() file.data = file_data # Save file file.save() except Exception: # Ignore errors for now pass if (file.data["media_type"] == "video" or file.data["media_type"] == "image"): # Determine thumb path thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"]) else: # Audio file thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png") # Create Clip object clip = Clip() clip.data = json.loads(clip_obj.Json()) clip.data["file_id"] = file.id clip.data["title"] = clip_element.getElementsByTagName( "name")[0].childNodes[0].nodeValue clip.data["layer"] = track.data.get("number", 1000000) clip.data["image"] = thumb_path clip.data["position"] = float( clip_element.getElementsByTagName("start") [0].childNodes[0].nodeValue) / fps_float clip.data["start"] = float( clip_element.getElementsByTagName("in") [0].childNodes[0].nodeValue) / fps_float clip.data["end"] = float( clip_element.getElementsByTagName("out") [0].childNodes[0].nodeValue) / fps_float # Loop through clip's effects for effect_element in clip_element.getElementsByTagName( "effect"): effectid = effect_element.getElementsByTagName( "effectid")[0].childNodes[0].nodeValue keyframes = effect_element.getElementsByTagName("keyframe") if effectid == "opacity": clip.data["alpha"] = {"Points": []} for keyframe_element in keyframes: keyframe_time = float( keyframe_element.getElementsByTagName("when") [0].childNodes[0].nodeValue) keyframe_value = float( keyframe_element.getElementsByTagName("value") [0].childNodes[0].nodeValue) / 100.0 clip.data["alpha"]["Points"].append({ "co": { "X": round(keyframe_time), "Y": keyframe_value }, "interpolation": 1 # linear }) elif effectid == "audiolevels": clip.data["volume"] = {"Points": []} for keyframe_element in keyframes: keyframe_time = float( keyframe_element.getElementsByTagName("when") [0].childNodes[0].nodeValue) keyframe_value = float( keyframe_element.getElementsByTagName("value") [0].childNodes[0].nodeValue) / 100.0 clip.data["volume"]["Points"].append({ "co": { "X": round(keyframe_time), "Y": keyframe_value }, "interpolation": 1 # linear }) # Save clip clip.save() # Update the preview and reselect current frame in properties app.window.refreshFrameSignal.emit() app.window.propertyTableView.select_frame( app.window.preview_thread.player.Position())
def add_files(self, files, image_seq_details=None, quiet=False): # Access translations app = get_app() _ = app._tr # Make sure we're working with a list of files if not isinstance(files, (list, tuple)): files = [files] start_count = len(files) for count, filepath in enumerate(files): (dir_path, filename) = os.path.split(filepath) # Check for this path in our existing project data new_file = File.get(path=filepath) # If this file is already found, exit if new_file: del new_file continue try: # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip = openshot.Clip(filepath) # Get the JSON for the clip's internal reader reader = clip.Reader() file_data = json.loads(reader.Json()) # Determine media type if file_data["has_video"] and not is_image(file_data): file_data["media_type"] = "video" elif file_data["has_video"] and is_image(file_data): file_data["media_type"] = "image" elif file_data["has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" else: # If none set, just assume video file_data["media_type"] = "video" # Save new file to the project data new_file = File() new_file.data = file_data # Is this an image sequence / animation? seq_info = image_seq_details or self.get_image_sequence_details(filepath) if seq_info: # Update file with correct path folder_path = seq_info["folder_path"] base_name = seq_info["base_name"] fixlen = seq_info["fixlen"] digits = seq_info["digits"] extension = seq_info["extension"] if not fixlen: zero_pattern = "%d" else: zero_pattern = "%%0%sd" % digits # Generate the regex pattern for this image sequence pattern = "%s%s.%s" % (base_name, zero_pattern, extension) # Split folder name folderName = os.path.basename(folder_path) if not base_name: # Give alternate name new_file.data["name"] = "%s (%s)" % (folderName, pattern) # Load image sequence (to determine duration and video_length) image_seq = openshot.Clip(os.path.join(folder_path, pattern)) # Update file details new_file.data["path"] = os.path.join(folder_path, pattern) new_file.data["media_type"] = "video" new_file.data["duration"] = image_seq.Reader().info.duration new_file.data["video_length"] = image_seq.Reader().info.video_length log.info('Imported {} as image sequence {}'.format( filepath, pattern)) # Remove any other image sequence files from the list we're processing match_glob = "{}{}.{}".format(base_name, '[0-9]*', extension) log.debug("Removing files from import list with glob: {}".format(match_glob)) for seq_file in glob.iglob(os.path.join(folder_path, match_glob)): # Don't remove the current file, or we mess up the for loop if seq_file in files and seq_file != filepath: files.remove(seq_file) if not seq_info: # Log our not-an-image-sequence import log.info("Imported media file {}".format(filepath)) # Save file new_file.save() if start_count > 15: message = _("Importing %(count)d / %(total)d") % { "count": count, "total": len(files) - 1 } app.window.statusBar.showMessage(message, 15000) # Let the event loop run to update the status bar get_app().processEvents() prev_path = app.project.get("import_path") if dir_path != prev_path: app.updates.update_untracked(["import_path"], dir_path) except Exception as ex: # Log exception log.warning("Failed to import {}: {}".format(filepath, ex)) if not quiet: # Show message box to user app.window.invalidImage(filename) # Reset list of ignored paths self.ignore_image_sequence_paths = [] message = _("Imported %(count)d files") % {"count": len(files) - 1} app.window.statusBar.showMessage(message, 3000)
def accept(self): """ Start exporting video """ # get translations app = get_app() _ = app._tr # Disable controls self.txtFileName.setEnabled(False) self.txtExportFolder.setEnabled(False) self.tabWidget.setEnabled(False) self.export_button.setEnabled(False) self.exporting = True # Determine type of export (video+audio, video, audio, image sequences) # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence") export_type = self.cboExportTo.currentText() # Determine final exported file path if export_type != _("Image Sequence"): file_name_with_ext = "%s.%s" % (self.txtFileName.text().strip(), self.txtVideoFormat.text().strip()) else: file_name_with_ext = "%s%s" % (self.txtFileName.text().strip(), self.txtImageFormat.text().strip()) export_file_path = os.path.join(self.txtExportFolder.text().strip(), file_name_with_ext) log.info(export_file_path) # Translate object _ = get_app()._tr file = File.get(path=export_file_path) if file: ret = QMessageBox.question( self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext, QMessageBox.Ok) self.txtFileName.setEnabled(True) self.txtExportFolder.setEnabled(True) self.tabWidget.setEnabled(True) self.export_button.setEnabled(True) self.exporting = False return # Handle exception if os.path.exists(export_file_path) and export_type in [ _("Video & Audio"), _("Video Only"), _("Audio Only") ]: # File already exists! Prompt user ret = QMessageBox.question( self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext, QMessageBox.No | QMessageBox.Yes) if ret == QMessageBox.No: # Stop and don't do anything # Re-enable controls self.txtFileName.setEnabled(True) self.txtExportFolder.setEnabled(True) self.tabWidget.setEnabled(True) self.export_button.setEnabled(True) self.exporting = False return # Init export settings video_settings = { "vformat": self.txtVideoFormat.text(), "vcodec": self.txtVideoCodec.text(), "fps": { "num": self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value() }, "width": self.txtWidth.value(), "height": self.txtHeight.value(), "pixel_ratio": { "num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value() }, "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())), "start_frame": self.txtStartFrame.value(), "end_frame": self.txtEndFrame.value() + 1 } audio_settings = { "acodec": self.txtAudioCodec.text(), "sample_rate": self.txtSampleRate.value(), "channels": self.txtChannels.value(), "channel_layout": self.cboChannelLayout.currentData(), "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text())) } # Override vcodec and format for Image Sequences if export_type == _("Image Sequence"): image_ext = os.path.splitext( self.txtImageFormat.text().strip())[1].replace(".", "") video_settings["vformat"] = image_ext if image_ext in ["jpg", "jpeg"]: video_settings["vcodec"] = "mjpeg" else: video_settings["vcodec"] = image_ext # Set MaxSize (so we don't have any downsampling) self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height")) # Set lossless cache settings (temporarily) export_cache_object = openshot.CacheMemory(250) self.timeline.SetCache(export_cache_object) # Create FFmpegWriter try: w = openshot.FFmpegWriter(export_file_path) # Set video options if export_type in [ _("Video & Audio"), _("Video Only"), _("Image Sequence") ]: w.SetVideoOptions( True, video_settings.get("vcodec"), openshot.Fraction( video_settings.get("fps").get("num"), video_settings.get("fps").get("den")), video_settings.get("width"), video_settings.get("height"), openshot.Fraction( video_settings.get("pixel_ratio").get("num"), video_settings.get("pixel_ratio").get("den")), False, False, video_settings.get("video_bitrate")) # Set audio options if export_type in [_("Video & Audio"), _("Audio Only")]: w.SetAudioOptions(True, audio_settings.get("acodec"), audio_settings.get("sample_rate"), audio_settings.get("channels"), audio_settings.get("channel_layout"), audio_settings.get("audio_bitrate")) # Open the writer w.Open() # Notify window of export started get_app().window.ExportStarted.emit( export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame")) progressstep = max( 1, round((video_settings.get("end_frame") - video_settings.get("start_frame")) / 100)) # Write each frame in the selected range for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame")): # Update progress bar (emit signal to main window) if (frame % progressstep) == 0: get_app().window.ExportFrame.emit( export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"), frame) # Process events (to show the progress bar moving) QCoreApplication.processEvents() # Write the frame object to the video w.WriteFrame(self.timeline.GetFrame(frame)) # Check if we need to bail out if not self.exporting: break # Close writer w.Close() except Exception as e: # TODO: Find a better way to catch the error. This is the only way I have found that # does not throw an error error_type_str = str(e) log.info("Error type string: %s" % error_type_str) if "InvalidChannels" in error_type_str: log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels"))) 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"))) elif "InvalidSampleRate" in error_type_str: log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate"))) 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"))) elif "InvalidFormat" in error_type_str: log.info("Error setting invalid format (%s)" % (video_settings.get("vformat"))) track_metric_error("invalid-format-%s" % (video_settings.get("vformat"))) elif "InvalidCodec" in error_type_str: log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) elif "ErrorEncodingVideo" in error_type_str: log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) # Show friendly error friendly_error = error_type_str.split("> ")[0].replace("<", "") # Prompt error message msg = QMessageBox() _ = get_app()._tr msg.setWindowTitle(_("Export Error")) msg.setText( _("Sorry, there was an error exporting your video: \n%s") % friendly_error) msg.exec_() # Notify window of export started get_app().window.ExportEnded.emit(export_file_path) # Close timeline object self.timeline.Close() # Clear all cache self.timeline.ClearAllCache() # Accept dialog super(Export, self).accept()
def create_clip(context, track): """Create a new clip based on this context dict""" app = get_app() _ = app._tr # Get FPS info fps_num = app.project.get("fps").get("num", 24) fps_den = app.project.get("fps").get("den", 1) fps_float = float(fps_num / fps_den) # Get clip path (and prompt user if path not found) clip_path, is_modified, is_skipped = find_missing_file( context.get("clip_path", "")) if is_skipped: return # Get video context video_ctx = context.get("AX", {}).get("V", {}) audio_ctx = context.get("AX", {}).get("A", {}) # Check for this path in our existing project data file = File.get(path=clip_path) # Load filepath in libopenshot clip object (which will try multiple readers to open it) clip_obj = openshot.Clip(clip_path) if not file: # Get the JSON for the clip's internal reader try: reader = clip_obj.Reader() file_data = json.loads(reader.Json()) # Determine media type if file_data["has_video"] and not is_image(file_data): file_data["media_type"] = "video" elif file_data["has_video"] and is_image(file_data): file_data["media_type"] = "image" elif file_data["has_audio"] and not file_data["has_video"]: file_data["media_type"] = "audio" # Save new file to the project data file = File() file.data = file_data # Save file file.save() except: log.warning('Error building File object for %s' % clip_path, exc_info=1) if (file.data["media_type"] == "video" or file.data["media_type"] == "image"): # Determine thumb path thumb_path = os.path.join(info.THUMBNAIL_PATH, "%s.png" % file.data["id"]) else: # Audio file thumb_path = os.path.join(info.PATH, "images", "AudioThumbnail.png") # Create Clip object clip = Clip() clip.data = json.loads(clip_obj.Json()) clip.data["file_id"] = file.id clip.data["title"] = context.get("clip_path", "") clip.data["layer"] = track.data.get("number", 1000000) if video_ctx and not audio_ctx: # Only video clip.data["position"] = timecodeToSeconds( video_ctx.get("timeline_position", "00:00:00:00"), fps_num, fps_den) clip.data["start"] = timecodeToSeconds( video_ctx.get("clip_start_time", "00:00:00:00"), fps_num, fps_den) clip.data["end"] = timecodeToSeconds( video_ctx.get("clip_end_time", "00:00:00:00"), fps_num, fps_den) clip.data["has_audio"] = { "Points": [{ "co": { "X": 1.0, "Y": 0.0 # Disable audio }, "interpolation": 2 }] } elif audio_ctx and not video_ctx: # Only audio clip.data["position"] = timecodeToSeconds( audio_ctx.get("timeline_position", "00:00:00:00"), fps_num, fps_den) clip.data["start"] = timecodeToSeconds( audio_ctx.get("clip_start_time", "00:00:00:00"), fps_num, fps_den) clip.data["end"] = timecodeToSeconds( audio_ctx.get("clip_end_time", "00:00:00:00"), fps_num, fps_den) clip.data["has_video"] = { "Points": [{ "co": { "X": 1.0, "Y": 0.0 # Disable video }, "interpolation": 2 }] } else: # Both video and audio clip.data["position"] = timecodeToSeconds( video_ctx.get("timeline_position", "00:00:00:00"), fps_num, fps_den) clip.data["start"] = timecodeToSeconds( video_ctx.get("clip_start_time", "00:00:00:00"), fps_num, fps_den) clip.data["end"] = timecodeToSeconds( video_ctx.get("clip_end_time", "00:00:00:00"), fps_num, fps_den) # Add volume keyframes if context.get("volume"): clip.data["volume"] = {"Points": []} for keyframe in context.get("volume", []): clip.data["volume"]["Points"].append({ "co": { "X": round( timecodeToSeconds(keyframe.get("time", 0.0), fps_num, fps_den) * fps_float), "Y": keyframe.get("value", 0.0) }, "interpolation": 1 # linear }) # Add alpha keyframes if context.get("opacity"): clip.data["alpha"] = {"Points": []} for keyframe in context.get("opacity", []): clip.data["alpha"]["Points"].append({ "co": { "X": round( timecodeToSeconds(keyframe.get("time", 0.0), fps_num, fps_den) * fps_float), "Y": keyframe.get("value", 0.0) }, "interpolation": 1 # linear }) # Save clip clip.save()
def current_file(self): """ Get the File object for the current files-view item, or the first selection """ cur_id = self.current_file_id() if cur_id: return File.get(id=cur_id)
def accept(self): """ Start exporting video """ # get translations app = get_app() _ = app._tr # Disable controls self.txtFileName.setEnabled(False) self.txtExportFolder.setEnabled(False) self.tabWidget.setEnabled(False) self.export_button.setEnabled(False) self.exporting = True # Determine type of export (video+audio, video, audio, image sequences) # _("Video & Audio"), _("Video Only"), _("Audio Only"), _("Image Sequence") export_type = self.cboExportTo.currentText() # Determine final exported file path if export_type != _("Image Sequence"): file_name_with_ext = "%s.%s" % (self.txtFileName.text().strip(), self.txtVideoFormat.text().strip()) else: file_name_with_ext = "%s%s" % (self.txtFileName.text().strip(), self.txtImageFormat.text().strip()) export_file_path = os.path.join(self.txtExportFolder.text().strip(), file_name_with_ext) log.info(export_file_path) # Translate object _ = get_app()._tr file = File.get(path=export_file_path) if file: ret = QMessageBox.question( self, _("Export Video"), _("%s is an input file.\nPlease choose a different name.") % file_name_with_ext, QMessageBox.Ok) self.txtFileName.setEnabled(True) self.txtExportFolder.setEnabled(True) self.tabWidget.setEnabled(True) self.export_button.setEnabled(True) self.exporting = False return # Handle exception if os.path.exists(export_file_path) and export_type in [ _("Video & Audio"), _("Video Only"), _("Audio Only") ]: # File already exists! Prompt user ret = QMessageBox.question( self, _("Export Video"), _("%s already exists.\nDo you want to replace it?") % file_name_with_ext, QMessageBox.No | QMessageBox.Yes) if ret == QMessageBox.No: # Stop and don't do anything # Re-enable controls self.txtFileName.setEnabled(True) self.txtExportFolder.setEnabled(True) self.tabWidget.setEnabled(True) self.export_button.setEnabled(True) self.exporting = False return # Init export settings video_settings = { "vformat": self.txtVideoFormat.text(), "vcodec": self.txtVideoCodec.text(), "fps": { "num": self.txtFrameRateNum.value(), "den": self.txtFrameRateDen.value() }, "width": self.txtWidth.value(), "height": self.txtHeight.value(), "pixel_ratio": { "num": self.txtPixelRatioNum.value(), "den": self.txtPixelRatioDen.value() }, "video_bitrate": int(self.convert_to_bytes(self.txtVideoBitRate.text())), "start_frame": self.txtStartFrame.value(), "end_frame": self.txtEndFrame.value() + 1 } audio_settings = { "acodec": self.txtAudioCodec.text(), "sample_rate": self.txtSampleRate.value(), "channels": self.txtChannels.value(), "channel_layout": self.cboChannelLayout.currentData(), "audio_bitrate": int(self.convert_to_bytes(self.txtAudioBitrate.text())) } # Override vcodec and format for Image Sequences if export_type == _("Image Sequence"): image_ext = os.path.splitext( self.txtImageFormat.text().strip())[1].replace(".", "") video_settings["vformat"] = image_ext if image_ext in ["jpg", "jpeg"]: video_settings["vcodec"] = "mjpeg" else: video_settings["vcodec"] = image_ext # Set MaxSize (so we don't have any downsampling) self.timeline.SetMaxSize(video_settings.get("width"), video_settings.get("height")) # Set lossless cache settings (temporarily) export_cache_object = openshot.CacheMemory(250) self.timeline.SetCache(export_cache_object) # Rescale all keyframes and reload project if self.export_fps_factor != 1.0: self.keyframes_rescaled = True get_app().project.rescale_keyframes(self.export_fps_factor) # Load the "export" Timeline reader with the JSON from the real timeline json_timeline = json.dumps(get_app().project._data) self.timeline.SetJson(json_timeline) # Re-update the timeline FPS again (since the timeline just got clobbered) self.updateFrameRate() # Create FFmpegWriter try: w = openshot.FFmpegWriter(export_file_path) # Set video options if export_type in [ _("Video & Audio"), _("Video Only"), _("Image Sequence") ]: w.SetVideoOptions( True, video_settings.get("vcodec"), openshot.Fraction( video_settings.get("fps").get("num"), video_settings.get("fps").get("den")), video_settings.get("width"), video_settings.get("height"), openshot.Fraction( video_settings.get("pixel_ratio").get("num"), video_settings.get("pixel_ratio").get("den")), False, False, video_settings.get("video_bitrate")) # Set audio options if export_type in [_("Video & Audio"), _("Audio Only")]: w.SetAudioOptions(True, audio_settings.get("acodec"), audio_settings.get("sample_rate"), audio_settings.get("channels"), audio_settings.get("channel_layout"), audio_settings.get("audio_bitrate")) # Prepare the streams w.PrepareStreams() # These extra options should be set in an extra method # No feedback is given to the user # TODO: Tell user if option is not avaliable # Set the quality in case crf was selected if "crf" in self.txtVideoBitRate.text(): w.SetOption(openshot.VIDEO_STREAM, "crf", str(int(video_settings.get("video_bitrate")))) # Open the writer w.Open() # Notify window of export started export_file_path = "" get_app().window.ExportStarted.emit( export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame")) progressstep = max( 1, round((video_settings.get("end_frame") - video_settings.get("start_frame")) / 1000)) start_time_export = time.time() start_frame_export = video_settings.get("start_frame") end_frame_export = video_settings.get("end_frame") # Write each frame in the selected range for frame in range(video_settings.get("start_frame"), video_settings.get("end_frame")): # Update progress bar (emit signal to main window) if (frame % progressstep) == 0: end_time_export = time.time() if (((frame - start_frame_export) != 0) & ((end_time_export - start_time_export) != 0)): seconds_left = round( (start_time_export - end_time_export) * (frame - end_frame_export) / (frame - start_frame_export)) fps_encode = ((frame - start_frame_export) / (end_time_export - start_time_export)) export_file_path = _( "%(hours)d:%(minutes)02d:%(seconds)02d Remaining (%(fps)5.2f FPS)" ) % { 'hours': seconds_left / 3600, 'minutes': (seconds_left / 60) % 60, 'seconds': seconds_left % 60, 'fps': fps_encode } get_app().window.ExportFrame.emit( export_file_path, video_settings.get("start_frame"), video_settings.get("end_frame"), frame) # Process events (to show the progress bar moving) QCoreApplication.processEvents() # Write the frame object to the video w.WriteFrame(self.timeline.GetFrame(frame)) # Check if we need to bail out if not self.exporting: break # Close writer w.Close() except Exception as e: # TODO: Find a better way to catch the error. This is the only way I have found that # does not throw an error error_type_str = str(e) log.info("Error type string: %s" % error_type_str) if "InvalidChannels" in error_type_str: log.info("Error setting invalid # of channels (%s)" % (audio_settings.get("channels"))) 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"))) elif "InvalidSampleRate" in error_type_str: log.info("Error setting invalid sample rate (%s)" % (audio_settings.get("sample_rate"))) 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"))) elif "InvalidFormat" in error_type_str: log.info("Error setting invalid format (%s)" % (video_settings.get("vformat"))) track_metric_error("invalid-format-%s" % (video_settings.get("vformat"))) elif "InvalidCodec" in error_type_str: log.info("Error setting invalid codec (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("invalid-codec-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) elif "ErrorEncodingVideo" in error_type_str: log.info("Error encoding video frame (%s/%s/%s)" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) track_metric_error("video-encode-%s-%s-%s" % (video_settings.get("vformat"), video_settings.get("vcodec"), audio_settings.get("acodec"))) # Show friendly error friendly_error = error_type_str.split("> ")[0].replace("<", "") # Prompt error message msg = QMessageBox() _ = get_app()._tr msg.setWindowTitle(_("Export Error")) msg.setText( _("Sorry, there was an error exporting your video: \n%s") % friendly_error) msg.exec_() # Notify window of export started get_app().window.ExportEnded.emit(export_file_path) # Close timeline object self.timeline.Close() # Clear all cache self.timeline.ClearAllCache() # Re-set OMP thread enabled flag if self.s.get("omp_threads_enabled"): openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = False else: openshot.Settings.Instance().WAIT_FOR_VIDEO_PROCESSING_TASK = True # Return scale mode to lower quality scaling (for faster previews) openshot.Settings.Instance().HIGH_QUALITY_SCALING = False # Return keyframes to preview scaling if self.keyframes_rescaled: get_app().project.rescale_keyframes(self.original_fps_factor) # Accept dialog super(Export, self).accept()