class YYPlayerBaseWidget(QWidget, updates.UpdateWatcher): previewFrameSignal = pyqtSignal(int) refreshFrameSignal = pyqtSignal() LoadFileSignal = pyqtSignal(str) PlaySignal = pyqtSignal(int) PauseSignal = pyqtSignal() StopSignal = pyqtSignal() SeekSignal = pyqtSignal(int) SpeedSignal = pyqtSignal(float) movePlayheadSignal = pyqtSignal(float) PlayModeChangedSignal = pyqtSignal(int) MaxSizeChanged = pyqtSignal(object) def __init__(self, timeline, parent=None): QWidget.__init__(self, parent) self.layout = QVBoxLayout() self.setLayout(self.layout) # Setup video preview QWidget self.videoPreview = VideoWidget(self) self.layout.addWidget(self.videoPreview) # Set max size of video preview (for speed) viewport_rect = self.videoPreview.centeredViewport(self.videoPreview.width(), self.videoPreview.height()) timeline.SetMaxSize(viewport_rect.width(), viewport_rect.height()) self.setLayout(self.layout) self.initialized = False # Start the preview thread self.preview_parent = PreviewParent() self.preview_parent.Init(self, timeline, self.videoPreview) self.preview_thread = self.preview_parent.worker # Set pause callback self.PauseSignal.connect(self.handlePausedVideo) self.hover = YYPlayerHover(self, self) # Save window settings on close def closeEvent(self, event): # Stop threads self.StopSignal.emit() # Process any queued events QCoreApplication.processEvents() # Stop preview thread (and wait for it to end) self.preview_thread.player.CloseAudioDevice() self.preview_thread.kill() self.preview_parent.background.exit() self.preview_parent.background.wait(5000) def btnPlay_clicked(self, force=None): log.info("btnPlay_clicked") if force == "pause": self.hover.btnPlay.setChecked(False) elif force == "play": self.hover.btnPlay.setChecked(True) if self.hover.btnPlay.isChecked(): log.info('play (icon to pause)') #ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-pause") self.preview_thread.Play(1000)#======todo else: log.info('pause (icon to play)') #ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-start") # to default self.preview_thread.Pause() # Send focus back to toolbar #self.sliderVideo.setFocus() def resizeEvent(self, QResizeEvent): super().resizeEvent(QResizeEvent) self.hover.setGeometry((self.width() - self.hover.width()) / 2, 20, self.hover.width(), self.hover.height()) def onModeChanged(self, current_mode): log.info('onModeChanged %s', current_mode) self.PlayModeChangedSignal.emit(current_mode) def Mode(self): return self.preview_thread.player.Mode() def handlePausedVideo(self): log.info("base handlePausedVideo") def onPlayFinished(self): log.info("base onPlayFinished") def movePlayhead(self, position_frames): log.info("movePlayhead %s", position_frames) """Update playhead position"""
class YYCutPlayerDlg(QDialog): """ Cutting Dialog """ # Path to ui file ui_path = os.path.join(info.PATH, 'windows', 'ui', 'cutting.ui') # Signals for preview thread previewFrameSignal = pyqtSignal(int) refreshFrameSignal = pyqtSignal() LoadFileSignal = pyqtSignal(str) PlaySignal = pyqtSignal(int) PauseSignal = pyqtSignal() SeekSignal = pyqtSignal(int) SpeedSignal = pyqtSignal(float) StopSignal = pyqtSignal() def __init__(self, cuts_json, clips_json, preview=False): _ = get_app()._tr # Create dialog class QDialog.__init__(self) # Load UI from designer ui_util.load_ui(self, self.ui_path) # Init UI ui_util.init_ui(self) # Track metrics track_metric_screen("cutting-screen") # If preview, hide cutting controls if preview: self.lblInstructions.setVisible(False) self.widgetControls.setVisible(False) self.setWindowTitle(_("Preview")) self.start_frame = 1 self.start_image = None self.end_frame = 1 self.end_image = None project = get_app().project # Keep track of file object #self.file = file self.file_path = file.absolute_path() self.video_length = int(file.data['video_length']) self.fps_num = int(file.data['fps']['num']) self.fps_den = int(file.data['fps']['den']) self.fps = float(self.fps_num) / float(self.fps_den) self.width = int(file.data['width']) self.height = int(file.data['height']) self.sample_rate = int(file.data['sample_rate']) self.channels = int(file.data['channels']) self.channel_layout = int(file.data['channel_layout']) # Open video file with Reader log.info(self.file_path) # Create an instance of a libopenshot Timeline object self.r = openshot.Timeline( self.width, self.height, openshot.Fraction(self.fps_num, self.fps_den), self.sample_rate, self.channels, self.channel_layout) self.r.info.channel_layout = self.channel_layout try: # Add clip for current preview file self.clip = openshot.Clip(self.file_path) # Show waveform for audio files if not self.clip.Reader().info.has_video and self.clip.Reader( ).info.has_audio: self.clip.Waveform(True) # Set has_audio property self.r.info.has_audio = self.clip.Reader().info.has_audio if preview: # Display frame #'s during preview self.clip.display = openshot.FRAME_DISPLAY_CLIP self.r.AddClip(self.clip) except: log.error('Failed to load media file into preview player: %s' % self.file_path) return # Add Video Widget self.videoPreview = VideoWidget() self.videoPreview.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.verticalLayout.insertWidget(0, self.videoPreview) # Set max size of video preview (for speed) viewport_rect = self.videoPreview.centeredViewport( self.videoPreview.width(), self.videoPreview.height()) self.r.SetMaxSize(viewport_rect.width(), viewport_rect.height()) # Open reader self.r.Open() # Start the preview thread self.initialized = False self.transforming_clip = False self.preview_parent = PreviewParent() self.preview_parent.Init(self, self.r, self.videoPreview) self.preview_thread = self.preview_parent.worker # Set slider constraints self.sliderIgnoreSignal = False self.sliderVideo.setMinimum(1) self.sliderVideo.setMaximum(self.video_length) self.sliderVideo.setSingleStep(1) self.sliderVideo.setSingleStep(1) self.sliderVideo.setPageStep(24) # Determine if a start or end attribute is in this file start_frame = 1 if 'start' in self.file.data.keys(): start_frame = (float(self.file.data['start']) * self.fps) + 1 # Display start frame (and then the previous frame) QTimer.singleShot( 500, functools.partial(self.sliderVideo.setValue, start_frame + 1)) QTimer.singleShot( 600, functools.partial(self.sliderVideo.setValue, start_frame)) # Connect signals self.actionPlay.triggered.connect(self.actionPlay_Triggered) self.btnPlay.clicked.connect(self.btnPlay_clicked) self.sliderVideo.valueChanged.connect(self.sliderVideo_valueChanged) self.btnStart.clicked.connect(self.btnStart_clicked) self.btnEnd.clicked.connect(self.btnEnd_clicked) self.btnClear.clicked.connect(self.btnClear_clicked) self.btnAddClip.clicked.connect(self.btnAddClip_clicked) self.initialized = True def actionPlay_Triggered(self): # Trigger play button (This action is invoked from the preview thread, so it must exist here) self.btnPlay.click() def movePlayhead(self, frame_number): """Update the playhead position""" # Move slider to correct frame position self.sliderIgnoreSignal = True self.sliderVideo.setValue(frame_number) self.sliderIgnoreSignal = False # Convert frame to seconds seconds = (frame_number - 1) / self.fps # Convert seconds to time stamp time_text = self.secondsToTime(seconds, self.fps_num, self.fps_den) timestamp = "%s:%s:%s:%s" % (time_text["hour"], time_text["min"], time_text["sec"], time_text["frame"]) # Update label self.lblVideoTime.setText(timestamp) def btnPlay_clicked(self, force=None): log.info("btnPlay_clicked") if force == "pause": self.btnPlay.setChecked(False) elif force == "play": self.btnPlay.setChecked(True) if self.btnPlay.isChecked(): log.info('play (icon to pause)') ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-pause") self.preview_thread.Play(self.video_length) else: log.info('pause (icon to play)') ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-start") # to default self.preview_thread.Pause() # Send focus back to toolbar self.sliderVideo.setFocus() def sliderVideo_valueChanged(self, new_frame): if self.preview_thread and not self.sliderIgnoreSignal: log.info('sliderVideo_valueChanged') # Pause video self.btnPlay_clicked(force="pause") # Seek to new frame self.preview_thread.previewFrame(new_frame) def btnStart_clicked(self): """Start of clip button was clicked""" _ = get_app()._tr # Pause video self.btnPlay_clicked(force="pause") # Get the current frame current_frame = self.sliderVideo.value() # Check if starting frame less than end frame if self.btnEnd.isEnabled() and current_frame >= self.end_frame: # Handle exception msg = QMessageBox() msg.setText( _("Please choose valid 'start' and 'end' values for your clip." )) msg.exec_() return # remember frame # self.start_frame = current_frame # Save thumbnail image self.start_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.start_frame) self.r.GetFrame(self.start_frame).Thumbnail(self.start_image, 160, 90, '', '', '#000000', True) # Set CSS on button self.btnStart.setStyleSheet('background-image: url(%s);' % self.start_image.replace('\\', '/')) # Enable end button self.btnEnd.setEnabled(True) self.btnClear.setEnabled(True) # Send focus back to toolbar self.sliderVideo.setFocus() log.info('btnStart_clicked, current frame: %s' % self.start_frame) def btnEnd_clicked(self): """End of clip button was clicked""" _ = get_app()._tr # Pause video self.btnPlay_clicked(force="pause") # Get the current frame current_frame = self.sliderVideo.value() # Check if ending frame greater than start frame if current_frame <= self.start_frame: # Handle exception msg = QMessageBox() msg.setText( _("Please choose valid 'start' and 'end' values for your clip." )) msg.exec_() return # remember frame # self.end_frame = current_frame # Save thumbnail image self.end_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.end_frame) self.r.GetFrame(self.end_frame).Thumbnail(self.end_image, 160, 90, '', '', '#000000', True) # Set CSS on button self.btnEnd.setStyleSheet('background-image: url(%s);' % self.end_image.replace('\\', '/')) # Enable create button self.btnAddClip.setEnabled(True) # Send focus back to toolbar self.sliderVideo.setFocus() log.info('btnEnd_clicked, current frame: %s' % self.end_frame) def btnClear_clicked(self): """Clear the current clip and reset the form""" log.info('btnClear_clicked') # Reset form self.clearForm() def clearForm(self): """Clear all form controls""" # Clear buttons self.start_frame = 1 self.end_frame = 1 self.start_image = '' self.end_image = '' self.btnStart.setStyleSheet('background-image: None;') self.btnEnd.setStyleSheet('background-image: None;') # Clear text self.txtName.setText('') # Disable buttons self.btnEnd.setEnabled(False) self.btnAddClip.setEnabled(False) self.btnClear.setEnabled(False) def btnAddClip_clicked(self): """Add the selected clip to the project""" log.info('btnAddClip_clicked') # Remove unneeded attributes if 'name' in self.file.data.keys(): self.file.data.pop('name') # Save new file self.file.id = None self.file.key = None self.file.type = 'insert' self.file.data['start'] = (self.start_frame - 1) / self.fps self.file.data['end'] = (self.end_frame - 1) / self.fps if self.txtName.text(): self.file.data['name'] = self.txtName.text() self.file.save() # Reset form self.clearForm() def padNumber(self, value, pad_length): format_mask = '%%0%sd' % pad_length return format_mask % value def secondsToTime(self, secs, fps_num, fps_den): # calculate time of playhead milliseconds = secs * 1000 sec = math.floor(milliseconds / 1000) milli = milliseconds % 1000 min = math.floor(sec / 60) sec = sec % 60 hour = math.floor(min / 60) min = min % 60 day = math.floor(hour / 24) hour = hour % 24 week = math.floor(day / 7) day = day % 7 frame = round((milli / 1000.0) * (fps_num / fps_den)) + 1 return { "week": self.padNumber(week, 2), "day": self.padNumber(day, 2), "hour": self.padNumber(hour, 2), "min": self.padNumber(min, 2), "sec": self.padNumber(sec, 2), "milli": self.padNumber(milli, 2), "frame": self.padNumber(frame, 2) } # TODO: Remove these 4 methods def accept(self): """ Ok button clicked """ log.info('accept') def close(self): """ Actually close window and accept dialog """ log.info('close') def closeEvent(self, event): log.info('closeEvent') # Stop playback self.preview_parent.worker.Stop() # Stop preview thread (and wait for it to end) self.preview_parent.worker.kill() self.preview_parent.background.exit() self.preview_parent.background.wait(5000) # Close readers self.r.Close() self.clip.Close() self.r.ClearAllCache() def reject(self): log.info('reject')
class Cutting(QDialog): """ Cutting Dialog """ # Path to ui file ui_path = os.path.join(info.PATH, 'windows', 'ui', 'cutting.ui') # Signals for preview thread previewFrameSignal = pyqtSignal(int) refreshFrameSignal = pyqtSignal() LoadFileSignal = pyqtSignal(str) PlaySignal = pyqtSignal(int) PauseSignal = pyqtSignal() SeekSignal = pyqtSignal(int) SpeedSignal = pyqtSignal(float) StopSignal = pyqtSignal() def __init__(self, file=None, preview=False): _ = get_app()._tr # Create dialog class QDialog.__init__(self) # Load UI from designer ui_util.load_ui(self, self.ui_path) # Init UI ui_util.init_ui(self) # Track metrics track_metric_screen("cutting-screen") # If preview, hide cutting controls if preview: self.lblInstructions.setVisible(False) self.widgetControls.setVisible(False) self.setWindowTitle(_("Preview")) self.start_frame = 1 self.start_image = None self.end_frame = 1 self.end_image = None # Keep track of file object self.file = file self.file_path = file.absolute_path() self.video_length = int(file.data['video_length']) self.fps_num = int(file.data['fps']['num']) self.fps_den = int(file.data['fps']['den']) self.fps = float(self.fps_num) / float(self.fps_den) self.width = int(file.data['width']) self.height = int(file.data['height']) self.sample_rate = int(file.data['sample_rate']) self.channels = int(file.data['channels']) self.channel_layout = int(file.data['channel_layout']) # Open video file with Reader log.info(self.file_path) # Create an instance of a libopenshot Timeline object self.r = openshot.Timeline(self.width, self.height, openshot.Fraction(self.fps_num, self.fps_den), self.sample_rate, self.channels, self.channel_layout) self.r.info.channel_layout = self.channel_layout try: # Add clip for current preview file self.clip = openshot.Clip(self.file_path) # Show waveform for audio files if not self.clip.Reader().info.has_video and self.clip.Reader().info.has_audio: self.clip.Waveform(True) # Set has_audio property self.r.info.has_audio = self.clip.Reader().info.has_audio if preview: # Display frame #'s during preview self.clip.display = openshot.FRAME_DISPLAY_CLIP self.r.AddClip(self.clip) except: log.error('Failed to load media file into preview player: %s' % self.file_path) return # Add Video Widget self.videoPreview = VideoWidget() self.videoPreview.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) self.verticalLayout.insertWidget(0, self.videoPreview) # Set max size of video preview (for speed) viewport_rect = self.videoPreview.centeredViewport(self.videoPreview.width(), self.videoPreview.height()) self.r.SetMaxSize(viewport_rect.width(), viewport_rect.height()) # Open reader self.r.Open() # Start the preview thread self.initialized = False self.transforming_clip = False self.preview_parent = PreviewParent() self.preview_parent.Init(self, self.r, self.videoPreview) self.preview_thread = self.preview_parent.worker # Set slider constraints self.sliderIgnoreSignal = False self.sliderVideo.setMinimum(1) self.sliderVideo.setMaximum(self.video_length) self.sliderVideo.setSingleStep(1) self.sliderVideo.setSingleStep(1) self.sliderVideo.setPageStep(24) # Determine if a start or end attribute is in this file start_frame = 1 if 'start' in self.file.data.keys(): start_frame = (float(self.file.data['start']) * self.fps) + 1 # Display start frame (and then the previous frame) QTimer.singleShot(500, functools.partial(self.sliderVideo.setValue, start_frame + 1)) QTimer.singleShot(600, functools.partial(self.sliderVideo.setValue, start_frame)) # Connect signals self.actionPlay.triggered.connect(self.actionPlay_Triggered) self.btnPlay.clicked.connect(self.btnPlay_clicked) self.sliderVideo.valueChanged.connect(self.sliderVideo_valueChanged) self.btnStart.clicked.connect(self.btnStart_clicked) self.btnEnd.clicked.connect(self.btnEnd_clicked) self.btnClear.clicked.connect(self.btnClear_clicked) self.btnAddClip.clicked.connect(self.btnAddClip_clicked) self.initialized = True def actionPlay_Triggered(self): # Trigger play button (This action is invoked from the preview thread, so it must exist here) self.btnPlay.click() def movePlayhead(self, frame_number): """Update the playhead position""" # Move slider to correct frame position self.sliderIgnoreSignal = True self.sliderVideo.setValue(frame_number) self.sliderIgnoreSignal = False # Convert frame to seconds seconds = (frame_number-1) / self.fps # Convert seconds to time stamp time_text = self.secondsToTime(seconds, self.fps_num, self.fps_den) timestamp = "%s:%s:%s:%s" % (time_text["hour"], time_text["min"], time_text["sec"], time_text["frame"]) # Update label self.lblVideoTime.setText(timestamp) def btnPlay_clicked(self, force=None): log.info("btnPlay_clicked") if force == "pause": self.btnPlay.setChecked(False) elif force == "play": self.btnPlay.setChecked(True) if self.btnPlay.isChecked(): log.info('play (icon to pause)') ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-pause") self.preview_thread.Play(self.video_length) else: log.info('pause (icon to play)') ui_util.setup_icon(self, self.btnPlay, "actionPlay", "media-playback-start") # to default self.preview_thread.Pause() # Send focus back to toolbar self.sliderVideo.setFocus() def sliderVideo_valueChanged(self, new_frame): if self.preview_thread and not self.sliderIgnoreSignal: log.info('sliderVideo_valueChanged') # Pause video self.btnPlay_clicked(force="pause") # Seek to new frame self.preview_thread.previewFrame(new_frame) def btnStart_clicked(self): """Start of clip button was clicked""" _ = get_app()._tr # Pause video self.btnPlay_clicked(force="pause") # Get the current frame current_frame = self.sliderVideo.value() # Check if starting frame less than end frame if self.btnEnd.isEnabled() and current_frame >= self.end_frame: # Handle exception msg = QMessageBox() msg.setText(_("Please choose valid 'start' and 'end' values for your clip.")) msg.exec_() return # remember frame # self.start_frame = current_frame # Save thumbnail image self.start_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.start_frame) self.r.GetFrame(self.start_frame).Thumbnail(self.start_image, 160, 90, '', '', '#000000', True) # Set CSS on button self.btnStart.setStyleSheet('background-image: url(%s);' % self.start_image.replace('\\', '/')) # Enable end button self.btnEnd.setEnabled(True) self.btnClear.setEnabled(True) # Send focus back to toolbar self.sliderVideo.setFocus() log.info('btnStart_clicked, current frame: %s' % self.start_frame) def btnEnd_clicked(self): """End of clip button was clicked""" _ = get_app()._tr # Pause video self.btnPlay_clicked(force="pause") # Get the current frame current_frame = self.sliderVideo.value() # Check if ending frame greater than start frame if current_frame <= self.start_frame: # Handle exception msg = QMessageBox() msg.setText(_("Please choose valid 'start' and 'end' values for your clip.")) msg.exec_() return # remember frame # self.end_frame = current_frame # Save thumbnail image self.end_image = os.path.join(info.USER_PATH, 'thumbnail', '%s.png' % self.end_frame) self.r.GetFrame(self.end_frame).Thumbnail(self.end_image, 160, 90, '', '', '#000000', True) # Set CSS on button self.btnEnd.setStyleSheet('background-image: url(%s);' % self.end_image.replace('\\', '/')) # Enable create button self.btnAddClip.setEnabled(True) # Send focus back to toolbar self.sliderVideo.setFocus() log.info('btnEnd_clicked, current frame: %s' % self.end_frame) def btnClear_clicked(self): """Clear the current clip and reset the form""" log.info('btnClear_clicked') # Reset form self.clearForm() def clearForm(self): """Clear all form controls""" # Clear buttons self.start_frame = 1 self.end_frame = 1 self.start_image = '' self.end_image = '' self.btnStart.setStyleSheet('background-image: None;') self.btnEnd.setStyleSheet('background-image: None;') # Clear text self.txtName.setText('') # Disable buttons self.btnEnd.setEnabled(False) self.btnAddClip.setEnabled(False) self.btnClear.setEnabled(False) def btnAddClip_clicked(self): """Add the selected clip to the project""" log.info('btnAddClip_clicked') # Remove unneeded attributes if 'name' in self.file.data.keys(): self.file.data.pop('name') # Save new file self.file.id = None self.file.key = None self.file.type = 'insert' self.file.data['start'] = (self.start_frame-1) / self.fps self.file.data['end'] = (self.end_frame-1) / self.fps if self.txtName.text(): self.file.data['name'] = self.txtName.text() self.file.save() # Reset form self.clearForm() def padNumber(self, value, pad_length): format_mask = '%%0%sd' % pad_length return format_mask % value def secondsToTime(self, secs, fps_num, fps_den): # calculate time of playhead milliseconds = secs * 1000 sec = math.floor(milliseconds/1000) milli = milliseconds % 1000 min = math.floor(sec/60) sec = sec % 60 hour = math.floor(min/60) min = min % 60 day = math.floor(hour/24) hour = hour % 24 week = math.floor(day/7) day = day % 7 frame = round((milli / 1000.0) * (fps_num / fps_den)) + 1 return { "week":self.padNumber(week,2), "day":self.padNumber(day,2), "hour":self.padNumber(hour,2), "min":self.padNumber(min,2), "sec":self.padNumber(sec,2), "milli":self.padNumber(milli,2), "frame":self.padNumber(frame,2) }; # TODO: Remove these 4 methods def accept(self): """ Ok button clicked """ log.info('accept') def close(self): """ Actually close window and accept dialog """ log.info('close') def closeEvent(self, event): log.info('closeEvent') # Stop playback self.preview_parent.worker.Stop() # Stop preview thread (and wait for it to end) self.preview_parent.worker.kill() self.preview_parent.background.exit() self.preview_parent.background.wait(5000) # Close readers self.r.Close() self.clip.Close() self.r.ClearAllCache() def reject(self): log.info('reject')