def import_lwi(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports Key=1 frames as single-frame scenes. Ignores everything besides Index=0 video stream. ''' from copy import deepcopy AV_CODEC_ID_FIRST_AUDIO = 0x10000 STREAM_INDEX = 0 IS_KEY = 1 pattern = re.compile(r'Index={}.*?Codec=(\d+).*?\n.*?Key=(\d)'.format( STREAM_INDEX )) frame = Frame(0) for match in pattern.finditer(path.read_text(), re.RegexFlag.MULTILINE): if int(match[1]) >= AV_CODEC_ID_FIRST_AUDIO: frame += FrameInterval(1) continue if not int(match[2]) == IS_KEY: frame += FrameInterval(1) continue try: scening_list.add(deepcopy(frame)) except ValueError: out_of_range_count += 1 frame += FrameInterval(1)
def __init__(self, main: AbstractMainWindow) -> None: super().__init__(main, 'Benchmark') self.setup_ui() self.running = False self.unsequenced = False self.buffer: Deque[Future] = deque() self.run_start_time = 0.0 self.start_frame = Frame(0) self. end_frame = Frame(0) self.total_frames = FrameInterval(0) self.frames_left = FrameInterval(0) self.sequenced_timer = Qt.QTimer() self.sequenced_timer.setTimerType(Qt.Qt.PreciseTimer) self.sequenced_timer.setInterval(0) self.update_info_timer = Qt.QTimer() self.update_info_timer.setTimerType(Qt.Qt.PreciseTimer) self.update_info_timer.setInterval( self.main.BENCHMARK_REFRESH_INTERVAL) self. start_frame_control.valueChanged.connect(lambda value: self.update_controls(start= value)) self. start_time_control.valueChanged.connect(lambda value: self.update_controls(start=Frame(value))) self. end_frame_control.valueChanged.connect(lambda value: self.update_controls( end= value)) self. end_time_control.valueChanged.connect(lambda value: self.update_controls( end=Frame(value))) self.total_frames_control.valueChanged.connect(lambda value: self.update_controls(total= value)) self. total_time_control.valueChanged.connect(lambda value: self.update_controls(total=FrameInterval(value))) self. prefetch_checkbox.stateChanged.connect(self.on_prefetch_changed) self. run_abort_button. clicked.connect(self.on_run_abort_pressed) self. sequenced_timer. timeout.connect(self._request_next_frame_sequenced) self. update_info_timer. timeout.connect(self.update_info) set_qobject_names(self)
def calculate_notch_interval_f(self, target_interval_x: int) -> FrameInterval: margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_f = self.x_to_f(target_interval_x, FrameInterval) if target_interval_f >= FrameInterval( round(int(self.notch_intervals_f[-1]) * margin)): return self.notch_intervals_f[-1] for interval in self.notch_intervals_f: if target_interval_f < FrameInterval( round(int(interval) * margin)): return interval raise RuntimeError
def get_prev_frame(self, initial: Frame) -> Optional[Frame]: result = None result_delta = FrameInterval(int(self.main.current_output.end_frame)) for scene in self.items: if FrameInterval(0) < initial - scene.start < result_delta: result = scene.start result_delta = initial - scene.start if FrameInterval(0) < initial - scene.end < result_delta: result = scene.end result_delta = initial - scene.end return result
def get_next_frame(self, initial: Frame) -> Optional[Frame]: result = None result_delta = FrameInterval(int(self.main.current_output.end_frame)) for scene in self.items: if FrameInterval(0) < scene.start - initial < result_delta: result = scene.start result_delta = scene.start - initial if FrameInterval(0) < scene.end - initial < result_delta: result = scene.end result_delta = scene.end - initial return result
def _request_next_frame_sequenced(self) -> None: if self.frames_left <= FrameInterval(0): self.abort() return self.buffer.pop().result() next_frame = self.end_frame + FrameInterval(1) - self.frames_left if next_frame <= self.end_frame: new_future = self.main.current_output.vs_output.get_frame_async( int(next_frame)) self.buffer.appendleft(new_future) self.frames_left -= FrameInterval(1)
def _request_next_frame_unsequenced(self, future: Optional[Future] = None) -> None: if self.frames_left <= FrameInterval(0): self.abort() return if self.running: next_frame = self.end_frame + FrameInterval(1) - self.frames_left new_future = self.main.current_output.vs_output.get_frame_async( int(next_frame)) new_future.add_done_callback(self._request_next_frame_unsequenced) if future is not None: future.result() self.frames_left -= FrameInterval(1)
def play(self) -> None: if self.main.current_frame == self.main.current_output.end_frame: return if self.main.statusbar.label.text() == 'Ready': self.main.statusbar.label.setText('Playing') if not self.main.current_output.has_alpha: play_buffer_size = int( min( self.main.PLAY_BUFFER_SIZE, self.main.current_output.end_frame - self.main.current_frame)) self.play_buffer = deque([], play_buffer_size) for i in range(cast(int, self.play_buffer.maxlen)): future = self.main.current_output.vs_output.get_frame_async( int(self.main.current_frame + FrameInterval(i) + FrameInterval(1))) self.play_buffer.appendleft(future) else: play_buffer_size = int( min(self.main.PLAY_BUFFER_SIZE, (self.main.current_output.end_frame - self.main.current_frame) * 2)) # buffer size needs to be even in case alpha is present play_buffer_size -= play_buffer_size % 2 self.play_buffer = deque([], play_buffer_size) for i in range(cast(int, self.play_buffer.maxlen) // 2): frame = (self.main.current_frame + FrameInterval(i) + FrameInterval(1)) future = self.main.current_output.vs_output.get_frame_async( int(frame)) self.play_buffer.appendleft(future) future = self.main.current_output.vs_alpha.get_frame_async( int(frame)) self.play_buffer.appendleft(future) if self.fps_unlimited_checkbox.isChecked() or self.main.DEBUG_PLAY_FPS: self.play_timer.start(0) if self.main.DEBUG_PLAY_FPS: self.play_start_time = debug.perf_counter_ns() self.play_start_frame = self.main.current_frame else: self.fps_timer.start(self.main.FPS_REFRESH_INTERVAL) else: self.play_timer.start( round(1000 / self.main.current_output.play_fps))
def on_current_output_changed(self, index: int, prev_index: int) -> None: qt_silent_call(self.outputs_combobox.setCurrentIndex, index) qt_silent_call( self.frame_spinbox.setMaximum, self.main.current_output.total_frames - FrameInterval(1)) qt_silent_call(self.time_spinbox.setMaximumTime, timedelta_to_qtime(self.main.current_output.duration))
def seek_n_frames_f(self, checked: Optional[bool] = None) -> None: new_pos = self.main.current_frame + FrameInterval( self.seek_frame_spinbox.value()) if new_pos >= self.main.current_output.total_frames: return self.stop() self.main.current_frame = new_pos
def seek_n_frames_f(self, checked: Optional[bool] = None) -> None: new_pos = (self.main.current_frame + FrameInterval(self.seek_frame_control.value())) if new_pos > self.main.current_output.end_frame: return self.stop() self.main.current_frame = new_pos
def seek_to_prev(self, checked: Optional[bool] = None) -> None: try: new_pos = self.main.current_frame - FrameInterval(1) except ValueError: return self.stop() self.main.current_frame = new_pos
def on_current_output_changed(self, index: int, prev_index: int) -> None: self. start_frame_control.setMaximum(self.main.current_output.end_frame) self. start_time_control.setMaximum(self.main.current_output.end_time) self. end_frame_control.setMaximum(self.main.current_output.end_frame) self. end_time_control.setMaximum(self.main.current_output.end_time) self.total_frames_control.setMaximum(self.main.current_output.total_frames) self. total_time_control.setMaximum(self.main.current_output.total_time) self. total_time_control.setMaximum(TimeInterval(FrameInterval(1)))
def on_current_output_changed(self, index: int, prev_index: int) -> None: qt_silent_call( self.seek_frame_spinbox.setMaximum, self.main.current_output.total_frames - FrameInterval(1)) qt_silent_call(self.seek_time_spinbox.setMaximumTime, timedelta_to_qtime(self.main.current_output.duration)) qt_silent_call(self.fps_spinbox.setValue, self.main.current_output.play_fps)
def _show_next_frame(self) -> None: if not self.main.current_output.has_alpha: try: frame_future = self.play_buffer.pop() except IndexError: self.play_pause_button.click() return next_frame_for_buffer = (self.main.current_frame + self.main.PLAY_BUFFER_SIZE) if next_frame_for_buffer <= self.main.current_output.end_frame: self.play_buffer.appendleft( self.main.current_output.vs_output.get_frame_async( next_frame_for_buffer)) self.main.switch_frame(self.main.current_frame + FrameInterval(1), render_frame=False) image = self.main.current_output.render_raw_videoframe( frame_future.result()) else: try: frame_future = self.play_buffer.pop() alpha_future = self.play_buffer.pop() except IndexError: self.play_pause_button.click() return next_frame_for_buffer = (self.main.current_frame + self.main.PLAY_BUFFER_SIZE // 2) if next_frame_for_buffer <= self.main.current_output.end_frame: self.play_buffer.appendleft( self.main.current_output.vs_output.get_frame_async( next_frame_for_buffer)) self.play_buffer.appendleft( self.main.current_output.vs_alpha.get_frame_async( next_frame_for_buffer)) self.main.switch_frame(self.main.current_frame + FrameInterval(1), render_frame=False) image = self.main.current_output.render_raw_videoframe( frame_future.result(), alpha_future.result()) self.main.current_output.graphics_scene_item.setImage(image) if not self.main.DEBUG_PLAY_FPS: self.update_fps_counter()
def seek_n_frames_b(self, checked: Optional[bool] = None) -> None: try: new_pos = (self.main.current_frame - FrameInterval(self.seek_frame_control.value())) except ValueError: return self.stop() self.main.current_frame = new_pos
def on_current_output_changed(self, index: int, prev_index: int) -> None: qt_silent_call(self.seek_frame_control.setMaximum, self.main.current_output.total_frames) qt_silent_call(self.seek_time_control.setMaximum, self.main.current_output.total_time) qt_silent_call(self.seek_time_control.setMinimum, TimeInterval(FrameInterval(1))) qt_silent_call(self.fps_spinbox.setValue, self.main.current_output.play_fps)
def update_controls(self, start: Optional[Frame] = None, end: Optional[Frame] = None, total: Optional[FrameInterval] = None) -> None: if start is not None: end = self.end_frame_control.value() total = self.total_frames_control.value() if start > end: end = start total = end - start + FrameInterval(1) elif end is not None: start = self.start_frame_control.value() total = self.total_frames_control.value() if end < start: start = end total = end - start + FrameInterval(1) elif total is not None: start = self.start_frame_control.value() end = self.end_frame_control.value() old_total = end - start + FrameInterval(1) delta = total - old_total end += delta if end > self.main.current_output.end_frame: start -= end - self.main.current_output.end_frame end = self.main.current_output.end_frame else: return qt_silent_call(self.start_frame_control.setValue, start) qt_silent_call(self.start_time_control.setValue, Time(start)) qt_silent_call(self.end_frame_control.setValue, end) qt_silent_call(self.end_time_control.setValue, Time(end)) qt_silent_call(self.total_frames_control.setValue, total) qt_silent_call(self.total_time_control.setValue, TimeInterval(total))
def play(self) -> None: if self.main.current_frame == self.main.current_output.total_frames - FrameInterval( 1): return if self.main.statusbar.label.text() == 'Ready': self.main.statusbar.label.setText('Playing') self.fps_prev_frame = self.main.current_frame self.fps_timer.start(self.main.FPS_REFRESH_INTERVAL) self.play_buffer.clear() for i in range(self.main.PLAY_BUFFER_SIZE): future = self.main.current_output.vs_output.get_frame_async( int(self.main.current_frame + FrameInterval(i) + FrameInterval(1))) self.play_buffer.appendleft(future) if self.fps_unlimited_checkbox.isChecked(): self.play_timer.start(0) else: self.play_timer.start( round(1000 / self.main.current_output.play_fps))
def _playback_show_next_frame(self) -> None: try: frame_future = self.play_buffer.pop() except IndexError: self.play_pause_button.click() return self.main.on_current_frame_changed(self.main.current_frame + FrameInterval(1), render_frame=False) pixmap = self.main.render_raw_videoframe(frame_future.result()) self.main.current_output.graphics_scene_item.setPixmap(pixmap) next_frame_for_buffer = self.main.current_frame + self.main.PLAY_BUFFER_SIZE if next_frame_for_buffer < self.main.current_output.total_frames: self.play_buffer.appendleft( self.main.current_output.vs_output.get_frame_async( next_frame_for_buffer))
def run(self) -> None: from copy import deepcopy from vapoursynth import VideoFrame if self.main.BENCHMARK_CLEAR_CACHE: vs_clear_cache() if self.main.BENCHMARK_FRAME_DATA_SHARING_FIX: self.main.current_output.graphics_scene_item.setPixmap( self.main.current_output.graphics_scene_item.pixmap().copy()) self.start_frame = self.start_frame_control.value() self.end_frame = self.end_frame_control.value() self.total_frames = self.total_frames_control.value() self.frames_left = deepcopy(self.total_frames) if self.prefetch_checkbox.isChecked(): concurrent_requests_count = get_usable_cpus_count() else: concurrent_requests_count = 1 self.unsequenced = self.unsequenced_checkbox.isChecked() if not self.unsequenced: self.buffer = deque([], concurrent_requests_count) self.sequenced_timer.start() self.running = True self.run_start_time = perf_counter() for offset in range( min(int(self.frames_left), concurrent_requests_count)): if self.unsequenced: self._request_next_frame_unsequenced() else: frame = self.start_frame + FrameInterval(offset) future = self.main.current_output.vs_output.get_frame_async( int(frame)) self.buffer.appendleft(future) self.update_info_timer.start()
def seek_to_next(self, checked: Optional[bool] = None) -> None: new_pos = self.main.current_frame + FrameInterval(1) if new_pos > self.main.current_output.end_frame: return self.stop() self.main.current_frame = new_pos
class MainWindow(AbstractMainWindow): # those are defaults that can be overriden in runtime or used as fallbacks AUTOSAVE_ENABLED = True AUTOSAVE_INTERVAL = 30 * 1000 # s BASE_PPI = 96 # PPI DARK_THEME = True FPS_REFRESH_INTERVAL = 1000 # ms LOG_LEVEL = logging.DEBUG OPENGL_RENDERING = False OUTPUT_INDEX = 0 PLAY_BUFFER_SIZE = FrameInterval(4) # frames PNG_COMPRESSION_LEVEL = 80 # 0 - 100 SAVE_TEMPLATE = '{script_name}_{frame}' SEEK_STEP = 1 # frames STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000 # s # it's allowed to stretch target interval betweewn notches by 20% at most TIMELINE_LABEL_NOTCHES_MARGIN = 20 # % TIMELINE_MODE = 'frame' DEBUG_TOOLBAR = False DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False storable_attrs = ['toolbars'] __slots__ = storable_attrs + [ 'app', 'opengl_widget', 'main_layout', 'main_toolbar_widget', 'main_toolbar_layout', 'graphics_view', 'script_error_dialog' 'outputs_combobox', 'frame_spinbox', 'copy_frame_button', 'time_spinbox', 'copy_timestamp_button', 'test_button' ] yaml_tag = '!MainWindow' def __init__(self) -> None: from qdarkstyle import load_stylesheet_pyqt5 super().__init__() # logging logging.basicConfig(format='{asctime}: {levelname}: {message}', style='{', level=self.LOG_LEVEL) logging.Formatter.default_msec_format = '%s.%03d' # ??? self.app = Qt.QApplication.instance() if self.DARK_THEME: self.app.setStyleSheet( self.patch_dark_stylesheet(load_stylesheet_pyqt5())) self.ensurePolished() self.display_scale = self.app.primaryScreen().logicalDotsPerInch( ) / self.BASE_PPI self.setWindowTitle('VSPreview') self.move(400, 0) self.setup_ui() # global self.clipboard = self.app.clipboard() self.script_path = Path() self.save_on_exit = True # graphics view self.graphics_scene = Qt.QGraphicsScene(self) self.graphics_view.setScene(self.graphics_scene) if self.OPENGL_RENDERING: self.opengl_widget = Qt.QOpenGLWidget() self.graphics_view.setViewport(self.opengl_widget) self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled) # timeline self.timeline.clicked.connect(self.on_current_frame_changed) # init toolbars and outputs self.toolbars = Toolbars(self) self.main_layout.addWidget(self.toolbars.main) for toolbar in self.toolbars: self.main_layout.addWidget(toolbar) self.toolbars.main.layout().addWidget(toolbar.toggle_button) def setup_ui(self) -> None: from vspreview.widgets import GraphicsView # mainWindow.resize(1300, 808) self.central_widget = Qt.QWidget(self) self.main_layout = Qt.QVBoxLayout(self.central_widget) self.setCentralWidget(self.central_widget) self.graphics_view = GraphicsView(self.central_widget) # self.graphics_view.setOptimizationFlag(Qt.QGraphicsView.OptimizationFlag.DontSavePainterState) # self.graphics_view.setOptimizationFlag(Qt.QGraphicsView.OptimizationFlag.DontAdjustForAntialiasing) self.graphics_view.setBackgroundBrush(self.palette().brush( Qt.QPalette.Window)) self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed, Qt.QSizePolicy.Fixed) self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag) self.main_layout.addWidget(self.graphics_view) self.timeline = Timeline(self.central_widget) self.main_layout.addWidget(self.timeline) # status bar self.statusbar = Qt.QStatusBar(self.central_widget) self.statusbar.total_frames_label = Qt.QLabel(self.central_widget) self.statusbar.addWidget(self.statusbar.total_frames_label) self.statusbar.duration_label = Qt.QLabel(self.central_widget) self.statusbar.addWidget(self.statusbar.duration_label) self.statusbar.resolution_label = Qt.QLabel(self.central_widget) self.statusbar.addWidget(self.statusbar.resolution_label) self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget) self.statusbar.addWidget(self.statusbar.pixel_format_label) self.statusbar.fps_label = Qt.QLabel(self.central_widget) self.statusbar.addWidget(self.statusbar.fps_label) self.statusbar.label = Qt.QLabel(self.central_widget) self.statusbar.addPermanentWidget(self.statusbar.label) self.setStatusBar(self.statusbar) # dialogs self.script_error_dialog = ScriptErrorDialog(self) def patch_dark_stylesheet(self, stylesheet: str) -> str: return stylesheet + 'QGraphicsView { border: 0px; padding: 0px; }' def load_script(self, script_path: Path) -> None: from traceback import print_exc self.toolbars.playback.stop() self.statusbar.label.setText('Evaluating') self.script_path = script_path sys.path.append(str(self.script_path.parent)) try: exec(self.script_path.read_text(), {}) # pylint: disable=exec-used except Exception: # pylint: disable=broad-except logging.error( 'Script contains error(s). Check following lines for details.') self.handle_script_error( 'Script contains error(s). See console output for details.') print_exc() return finally: sys.path.pop() if len(vs.get_outputs()) == 0: logging.error('Script has no outputs set.') self.handle_script_error('Script has no outputs set.') return self.toolbars.main.rescan_outputs() # self.init_outputs() self.switch_output(self.OUTPUT_INDEX) self.load_storage() def load_storage(self) -> None: import yaml storage_path = self.script_path.with_suffix('.yml') if storage_path.exists(): try: yaml.load(storage_path.open(), Loader=yaml.Loader) except yaml.YAMLError as exc: if isinstance(exc, yaml.MarkedYAMLError): logging.warning( 'Storage parsing failed on line {} column {}. Using defaults.' .format(exc.problem_mark.line + 1, exc.problem_mark.column + 1)) # pylint: disable=no-member else: logging.warning('Storage parsing failed. Using defaults.') # logging.getLogger().setLevel(logging.ERROR) else: logging.info('No storage found. Using defaults.') # logging.getLogger().setLevel(logging.ERROR) # logging.getLogger().setLevel(self.LOG_LEVEL) self.statusbar.label.setText('Ready') def init_outputs(self) -> None: self.graphics_scene.clear() for output in self.outputs: frame_pixmap = self.render_frame(output.last_showed_frame, output) frame_item = self.graphics_scene.addPixmap(frame_pixmap) frame_item.hide() output.graphics_scene_item = frame_item def reload_script(self) -> None: if self.toolbars.misc.autosave_enabled: self.toolbars.misc.save() vs.clear_outputs() self.graphics_scene.clear() self.load_script(self.script_path) self.statusbar.showMessage('Reloaded successfully', self.STATUSBAR_MESSAGE_TIMEOUT) def render_frame(self, frame: Frame, output: Optional[Output] = None) -> Qt.QPixmap: if output is None: output = self.current_output return self.render_raw_videoframe( output.vs_output.get_frame(int(frame))) def render_raw_videoframe(self, vs_frame: vs.VideoFrame) -> Qt.QPixmap: import ctypes frame_data = vs_frame.get_read_ptr(0) frame_stride = vs_frame.get_stride(0) # frame_itemsize = vs_frame.get_read_array(0).itemsize frame_itemsize = vs_frame.format.bytes_per_sample # powerful spell. do not touch frame_data = ctypes.cast( frame_data, ctypes.POINTER(ctypes.c_char * (frame_itemsize * vs_frame.width * vs_frame.height)))[0] # type: ignore frame_image = Qt.QImage(frame_data, vs_frame.width, vs_frame.height, frame_stride, Qt.QImage.Format_RGB32) frame_pixmap = Qt.QPixmap.fromImage(frame_image) return frame_pixmap def on_current_frame_changed(self, frame: Optional[Frame] = None, t: Optional[timedelta] = None, render_frame: bool = True) -> None: if t is None and frame is not None: t = self.to_timedelta(frame) elif t is not None and frame is None: frame = self.to_frame(t) elif t is not None and frame is not None: pass else: logging.debug( 'on_current_frame_changed(): both frame and t is None') return if frame >= self.current_output.total_frames: # logging.debug('on_current_frame_changed(): New frame position is out of range') return self.current_output.last_showed_frame = frame self.timeline.set_position(frame) self.toolbars.main.on_current_frame_changed(frame, t) for toolbar in self.toolbars: toolbar.on_current_frame_changed(frame, t) if render_frame: self.current_output.graphics_scene_item.setPixmap( self.render_frame(frame, self.current_output)) def switch_output(self, index: int) -> None: if len(self.outputs) == 0: # TODO: consider returning False return # print(index) # print_stack() prev_index = self.toolbars.main.outputs_combobox.currentIndex() if index < 0 or index >= len(self.outputs): logging.info( 'Output switching: output index is out of range. Switching to first output' ) index = 0 self.toolbars.playback.stop() # current_output relies on outputs_combobox self.toolbars.main.on_current_output_changed(index, prev_index) self.timeline.set_duration(self.current_output.total_frames, self.current_output.duration) self.current_frame = self.current_output.last_showed_frame for output in self.outputs: output.graphics_scene_item.hide() self.current_output.graphics_scene_item.show() self.graphics_scene.setSceneRect( Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect())) self.timeline.update_notches() for toolbar in self.toolbars: toolbar.on_current_output_changed(index, prev_index) self.update_statusbar_output_info() @property def current_output(self) -> Output: # type: ignore output = cast(Output, self.toolbars.main.outputs_combobox.currentData()) # check currentData() return on empty combobox # if data != '': # return cast(Output, data) # return None return output @property # type: ignore def current_frame(self) -> Frame: # type: ignore # if self.current_output is None: # return None return self.current_output.last_showed_frame @current_frame.setter def current_frame(self, value: Frame) -> None: self.on_current_frame_changed(value) @property def outputs(self) -> Outputs: # type: ignore return cast(Outputs, self.toolbars.main.outputs) def handle_script_error(self, message: str) -> None: # logging.error(message) self.script_error_dialog.label.setText(message) self.script_error_dialog.open() def on_wheel_scrolled(self, steps: int) -> None: new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps if new_index < 0: new_index = 0 elif new_index >= len(self.toolbars.main.zoom_levels): new_index = len(self.toolbars.main.zoom_levels) - 1 self.toolbars.main.zoom_combobox.setCurrentIndex(new_index) def update_statusbar_output_info(self, output: Optional[Output] = None) -> None: from vspreview.utils import strfdelta if output is None: output = self.current_output self.statusbar.total_frames_label.setText('{} frames '.format( output.total_frames)) self.statusbar.duration_label.setText('{} '.format( strfdelta(output.duration, '%H:%M:%S.%Z'))) self.statusbar.resolution_label.setText('{}x{} '.format( output.width, output.height)) self.statusbar.pixel_format_label.setText('{} '.format( output.format.name)) if output.fps_den != 0: self.statusbar.fps_label.setText('{}/{} = {:.3f} fps '.format( output.fps_num, output.fps_den, output.fps_num / output.fps_den)) else: self.statusbar.fps_label.setText('{}/{} fps '.format( output.fps_num, output.fps_den)) # misc methods def showEvent(self, event: Qt.QShowEvent) -> None: super().showEvent(event) self.graphics_view.setSizePolicy( Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding)) def closeEvent(self, event: Qt.QCloseEvent) -> None: if (self.toolbars.misc.autosave_enabled and self.save_on_exit): self.toolbars.misc.save() def to_frame(self, t: timedelta) -> Frame: return Frame( round(t.total_seconds() * (self.current_output.fps_num / self.current_output.fps_den))) def to_timedelta(self, frame: Frame) -> timedelta: return timedelta(seconds=( int(frame) / (self.current_output.fps_num / self.current_output.fps_den))) def __getstate__(self) -> Mapping[str, Any]: state = { attr_name: getattr(self, attr_name) for attr_name in self.storable_attrs } state.update({'timeline_mode': self.timeline.mode}) return state def __setstate__(self, state: Mapping[str, Any]) -> None: # toolbars is singleton, so it initialize itself right in its __setstate__() try: timeline_mode = state['timeline_mode'] if not Timeline.Mode.is_valid(timeline_mode): raise TypeError except (KeyError, TypeError): logging.warning( 'Storage loading: failed to parse timeline mode. Using default.' ) timeline_mode = self.TIMELINE_MODE self.timeline.mode = timeline_mode
def setup_ui(self) -> None: layout = Qt.QHBoxLayout(self) layout.setObjectName('PlaybackToolbar.setup_ui.layout') layout.setContentsMargins(0, 0, 0, 0) self.seek_to_start_button = Qt.QToolButton(self) self.seek_to_start_button.setText('⏮') self.seek_to_start_button.setToolTip('Seek to First Frame') layout.addWidget(self.seek_to_start_button) self.seek_n_frames_b_button = Qt.QToolButton(self) self.seek_n_frames_b_button.setText('⏪') self.seek_n_frames_b_button.setToolTip('Seek N Frames Backwards') layout.addWidget(self.seek_n_frames_b_button) self.seek_to_prev_button = Qt.QToolButton(self) self.seek_to_prev_button.setText('◂') self.seek_to_prev_button.setToolTip('Seek 1 Frame Backwards') layout.addWidget(self.seek_to_prev_button) self.play_pause_button = Qt.QToolButton(self) self.play_pause_button.setText('⏯') self.play_pause_button.setToolTip('Play/Pause') self.play_pause_button.setCheckable(True) layout.addWidget(self.play_pause_button) self.seek_to_next_button = Qt.QToolButton(self) self.seek_to_next_button.setText('▸') self.seek_to_next_button.setToolTip('Seek 1 Frame Forward') layout.addWidget(self.seek_to_next_button) self.seek_n_frames_f_button = Qt.QToolButton(self) self.seek_n_frames_f_button.setText('⏩') self.seek_n_frames_f_button.setToolTip('Seek N Frames Forward') layout.addWidget(self.seek_n_frames_f_button) self.seek_to_end_button = Qt.QToolButton(self) self.seek_to_end_button.setText('⏭') self.seek_to_end_button.setToolTip('Seek to Last Frame') layout.addWidget(self.seek_to_end_button) self.seek_frame_control = FrameEdit[FrameInterval](self) self.seek_frame_control.setMinimum(FrameInterval(1)) self.seek_frame_control.setToolTip('Seek N Frames Step') self.seek_frame_control.setValue(FrameInterval(self.main.SEEK_STEP)) layout.addWidget(self.seek_frame_control) self.seek_time_control = TimeEdit[TimeInterval](self) layout.addWidget(self.seek_time_control) self.fps_spinbox = Qt.QDoubleSpinBox(self) self.fps_spinbox.setRange(0.001, 9999.0) self.fps_spinbox.setDecimals(3) self.fps_spinbox.setSuffix(' fps') layout.addWidget(self.fps_spinbox) self.fps_reset_button = Qt.QPushButton(self) self.fps_reset_button.setText('Reset FPS') layout.addWidget(self.fps_reset_button) self.fps_unlimited_checkbox = Qt.QCheckBox(self) self.fps_unlimited_checkbox.setText('Unlimited FPS') layout.addWidget(self.fps_unlimited_checkbox) layout.addStretch()
class MainWindow(AbstractMainWindow): # those are defaults that can be overriden at runtime or used as fallbacks ALWAYS_SHOW_SCENE_MARKS = False AUTOSAVE_INTERVAL = 60 * 1000 # s BASE_PPI = 96 # PPI BENCHMARK_CLEAR_CACHE = False BENCHMARK_REFRESH_INTERVAL = 150 # ms CHECKERBOARD_ENABLED = True CHECKERBOARD_TILE_COLOR_1 = Qt.Qt.white CHECKERBOARD_TILE_COLOR_2 = Qt.Qt.lightGray CHECKERBOARD_TILE_SIZE = 8 # px DARK_THEME = True FPS_AVERAGING_WINDOW_SIZE = FrameInterval(100) FPS_REFRESH_INTERVAL = 150 # ms LOG_LEVEL = logging.DEBUG OPENGL_RENDERING = False ORDERED_OUTPUTS = False OUTPUT_INDEX = 0 PLAY_BUFFER_SIZE = FrameInterval(get_usable_cpus_count()) PNG_COMPRESSION_LEVEL = 0 # 0 - 100 SAVE_TEMPLATE = '{script_name}_{frame}' SEEK_STEP = 1 # frames STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000 # s STORAGE_BACKUPS_COUNT = 2 SYNC_OUTPUTS = False # it's allowed to stretch target interval betweewn notches by N% at most TIMELINE_LABEL_NOTCHES_MARGIN = 20 # % TIMELINE_MODE = 'frame' TOGGLE_TOOLBAR = False VSP_DIR_NAME = '.vspreview' # used for formats with subsampling VS_OUTPUT_RESIZER = Output.Resizer.Bicubic VS_OUTPUT_MATRIX = Output.Matrix.BT709 VS_OUTPUT_TRANSFER = Output.Transfer.BT709 VS_OUTPUT_PRIMARIES = Output.Primaries.BT709 VS_OUTPUT_RANGE = Output.Range.LIMITED VS_OUTPUT_CHROMALOC = Output.ChromaLoc.LEFT VS_OUTPUT_PREFER_PROPS = True VS_OUTPUT_RESIZER_KWARGS = {} # type: Mapping[str, str] BENCHMARK_FRAME_DATA_SHARING_FIX = True DEBUG_PLAY_FPS = False DEBUG_TOOLBAR = False DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False yaml_tag = '!MainWindow' storable_attrs = [ 'toolbars', ] __slots__ = storable_attrs + [ 'app', 'display_scale', 'clipboard', 'script_path', 'save_on_exit', 'timeline', 'main_layout', 'graphics_scene', 'graphics_view', 'script_error_dialog', 'central_widget', 'statusbar', 'opengl_widget', 'external_args', 'script_exec_failed' ] def __init__(self) -> None: from qdarkstyle import load_stylesheet_pyqt5 super().__init__() # logging # ??? self.app = Qt.QApplication.instance() if self.DARK_THEME: self.app.setStyleSheet( self.patch_dark_stylesheet(load_stylesheet_pyqt5())) self.ensurePolished() self.display_scale = self.app.primaryScreen().logicalDotsPerInch( ) / self.BASE_PPI self.setWindowTitle('VSPreview') self.move(400, 0) self.setup_ui() # global self.clipboard = self.app.clipboard() self.external_args: List[str] = [] self.script_path = Path() self.save_on_exit = True self.script_exec_failed = False # graphics view self.graphics_scene = Qt.QGraphicsScene(self) self.graphics_view.setScene(self.graphics_scene) self.opengl_widget = None if self.OPENGL_RENDERING: self.opengl_widget = Qt.QOpenGLWidget() self.graphics_view.setViewport(self.opengl_widget) self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled) # timeline self.timeline.clicked.connect(self.switch_frame) # init toolbars and outputs self.toolbars = Toolbars(self) self.main_layout.addWidget(self.toolbars.main) for toolbar in self.toolbars: self.main_layout.addWidget(toolbar) self.toolbars.main.layout().addWidget(toolbar.toggle_button) set_qobject_names(self) self.setObjectName('MainWindow') def setup_ui(self) -> None: from vspreview.widgets import GraphicsView # mainWindow.resize(1300, 808) self.central_widget = Qt.QWidget(self) self.main_layout = Qt.QVBoxLayout(self.central_widget) self.setCentralWidget(self.central_widget) self.graphics_view = GraphicsView(self.central_widget) self.graphics_view.setBackgroundBrush(self.palette().brush( Qt.QPalette.Window)) self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed, Qt.QSizePolicy.Fixed) self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag) self.graphics_view.setTransformationAnchor( GraphicsView.AnchorUnderMouse) self.main_layout.addWidget(self.graphics_view) self.timeline = Timeline(self.central_widget) self.main_layout.addWidget(self.timeline) # status bar self.statusbar = StatusBar(self.central_widget) self.statusbar.total_frames_label = Qt.QLabel(self.central_widget) self.statusbar.total_frames_label.setObjectName( 'MainWindow.statusbar.total_frames_label') self.statusbar.addWidget(self.statusbar.total_frames_label) self.statusbar.duration_label = Qt.QLabel(self.central_widget) self.statusbar.duration_label.setObjectName( 'MainWindow.statusbar.duration_label') self.statusbar.addWidget(self.statusbar.duration_label) self.statusbar.resolution_label = Qt.QLabel(self.central_widget) self.statusbar.resolution_label.setObjectName( 'MainWindow.statusbar.resolution_label') self.statusbar.addWidget(self.statusbar.resolution_label) self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget) self.statusbar.pixel_format_label.setObjectName( 'MainWindow.statusbar.pixel_format_label') self.statusbar.addWidget(self.statusbar.pixel_format_label) self.statusbar.fps_label = Qt.QLabel(self.central_widget) self.statusbar.fps_label.setObjectName( 'MainWindow.statusbar.fps_label') self.statusbar.addWidget(self.statusbar.fps_label) self.statusbar.label = Qt.QLabel(self.central_widget) self.statusbar.label.setObjectName('MainWindow.statusbar.label') self.statusbar.addPermanentWidget(self.statusbar.label) self.setStatusBar(self.statusbar) # dialogs self.script_error_dialog = ScriptErrorDialog(self) def patch_dark_stylesheet(self, stylesheet: str) -> str: return stylesheet \ + ' QGraphicsView { border: 0px; padding: 0px; }' \ + ' QToolButton { padding: 0px; }' def load_script(self, script_path: Path, external_args: str = '', reloading=False) -> None: import shlex from traceback import FrameSummary, TracebackException self.toolbars.playback.stop() self.statusbar.label.setText('Evaluating') self.script_path = script_path sys.path.append(str(self.script_path.parent)) # Rewrite args so external args will be forwarded correctly if external_args: self.external_args = shlex.split(external_args) try: argv_orig = sys.argv sys.argv = [script_path.name] + self.external_args except AttributeError: pass try: # pylint: disable=exec-used exec(self.script_path.read_text(encoding='utf-8'), {'__file__': sys.argv[0]}) except Exception as e: # pylint: disable=broad-except self.script_exec_failed = True logging.error(e) te = TracebackException.from_exception(e) # remove the first stack frame, which contains our exec() invocation del te.stack[0] # replace <string> with script path only for the first stack frames # in order to keep intact exec() invocations down the stack # that we're not concerned with for i, frame in enumerate(te.stack): if frame.filename == '<string>': te.stack[i] = FrameSummary(str(self.script_path), frame.lineno, frame.name) else: break print(''.join(te.format())) self.handle_script_error( f'''An error occured while evaluating script: \n{str(e)} \nSee console output for details.''') return finally: sys.argv = argv_orig sys.path.pop() self.script_exec_failed = False if len(vs.get_outputs()) == 0: logging.error('Script has no outputs set.') self.handle_script_error('Script has no outputs set.') return if not reloading: self.toolbars.main.rescan_outputs() for toolbar in self.toolbars: toolbar.on_script_loaded() self.switch_output(self.OUTPUT_INDEX) self.load_storage() else: self.load_storage() for toolbar in self.toolbars: toolbar.on_script_loaded() def load_storage(self) -> None: import yaml vsp_dir = self.script_path.parent / self.VSP_DIR_NAME storage_path = vsp_dir / (self.script_path.stem + '.yml') if not storage_path.exists(): storage_path = self.script_path.with_suffix('.yml') if storage_path.exists(): try: yaml.load(storage_path.open(), Loader=yaml.Loader) except yaml.YAMLError as exc: if isinstance(exc, yaml.MarkedYAMLError): logging.warning( 'Storage parsing failed at line {}:{} ({} {}).' 'Using defaults.'.format(exc.problem_mark.line + 1, exc.problem_mark.column + 1, exc.problem, exc.context)) # pylint: disable=no-member else: logging.warning('Storage parsing failed. Using defaults.') else: logging.info('No storage found. Using defaults.') self.statusbar.label.setText('Ready') def init_outputs(self) -> None: from vspreview.widgets import GraphicsImageItem self.graphics_scene.clear() for output in self.outputs: frame_image = output.render_frame(output.last_showed_frame) raw_frame_item = self.graphics_scene.addPixmap( Qt.QPixmap.fromImage(frame_image)) raw_frame_item.hide() frame_item = GraphicsImageItem(raw_frame_item, frame_image) output.graphics_scene_item = frame_item def reload_script(self) -> None: import gc if not self.script_exec_failed: self.toolbars.misc.save_sync() for toolbar in self.toolbars: toolbar.on_script_unloaded() vs.clear_outputs() self.graphics_scene.clear() self.outputs.clear() # make sure old filter graph is freed gc.collect() self.load_script(self.script_path, reloading=True) self.show_message('Reloaded successfully') def render_frame(self, frame: Frame, output: Optional[Output] = None) -> Qt.QImage: return self.current_output.render_frame(frame) def switch_frame(self, pos: Union[Frame, Time], *, render_frame: bool = True) -> None: if isinstance(pos, Frame): frame = pos time = Time(frame) elif isinstance(pos, Time): frame = Frame(pos) time = pos else: logging.debug('switch_frame(): position is neither Frame nor Time') return if frame > self.current_output.end_frame: return self.current_output.last_showed_frame = frame self.timeline.set_position(frame) self.toolbars.main.on_current_frame_changed(frame, time) for toolbar in self.toolbars: toolbar.on_current_frame_changed(frame, time) if render_frame: self.current_output.graphics_scene_item.setImage( self.render_frame(frame)) def switch_output(self, value: Union[int, Output]) -> None: if len(self.outputs) == 0: return if isinstance(value, Output): index = self.outputs.index_of(value) else: index = value if index < 0 or index >= len(self.outputs): return prev_index = self.toolbars.main.outputs_combobox.currentIndex() self.toolbars.playback.stop() # current_output relies on outputs_combobox self.toolbars.main.on_current_output_changed(index, prev_index) self.timeline.set_end_frame(self.current_output.end_frame) if self.current_output.frame_to_show is not None: self.current_frame = self.current_output.frame_to_show else: self.current_frame = self.current_output.last_showed_frame for output in self.outputs: output.graphics_scene_item.hide() self.current_output.graphics_scene_item.show() self.graphics_scene.setSceneRect( Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect())) self.timeline.update_notches() for toolbar in self.toolbars: toolbar.on_current_output_changed(index, prev_index) self.update_statusbar_output_info() @property # type: ignore def current_output(self) -> Output: # type: ignore output = cast(Output, self.toolbars.main.outputs_combobox.currentData()) return output @current_output.setter def current_output(self, value: Output) -> None: self.switch_output(self.outputs.index_of(value)) @property # type: ignore def current_frame(self) -> Frame: # type: ignore return self.current_output.last_showed_frame @current_frame.setter def current_frame(self, value: Frame) -> None: self.switch_frame(value) @property # type: ignore def current_time(self) -> Time: # type: ignore return Time(self.current_output.last_showed_frame) @current_time.setter def current_time(self, value: Time) -> None: self.switch_frame(value) @property def outputs(self) -> Outputs: # type: ignore return cast(Outputs, self.toolbars.main.outputs) def handle_script_error(self, message: str) -> None: # logging.error(message) self.script_error_dialog.label.setText(message) self.script_error_dialog.open() def on_wheel_scrolled(self, steps: int) -> None: new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps if new_index < 0: new_index = 0 elif new_index >= len(self.toolbars.main.zoom_levels): new_index = len(self.toolbars.main.zoom_levels) - 1 self.toolbars.main.zoom_combobox.setCurrentIndex(new_index) def show_message(self, message: str, timeout: Optional[int] = None) -> None: if timeout is None: timeout = self.STATUSBAR_MESSAGE_TIMEOUT self.statusbar.showMessage(message, timeout) def update_statusbar_output_info(self, output: Optional[Output] = None) -> None: if output is None: output = self.current_output self.statusbar.total_frames_label.setText('{} frames'.format( output.total_frames)) self.statusbar.duration_label.setText( # Display duration without -1 offset to match other video tools '{}'.format(TimeInterval(self.current_output.total_frames))) self.statusbar.resolution_label.setText('{}x{}'.format( output.width, output.height)) if not output.has_alpha: self.statusbar.pixel_format_label.setText('{}'.format( output.format.name)) else: self.statusbar.pixel_format_label.setText( 'Clip: {}, Alpha: {}'.format(output.format.name, output.format_alpha.name)) if output.fps_den != 0: self.statusbar.fps_label.setText('{}/{} = {:.3f} fps'.format( output.fps_num, output.fps_den, output.fps_num / output.fps_den)) else: self.statusbar.fps_label.setText('{}/{} fps'.format( output.fps_num, output.fps_den)) def event(self, event: Qt.QEvent) -> bool: if event.type() == Qt.QEvent.LayoutRequest: self.timeline.full_repaint() return super().event(event) # misc methods def showEvent(self, event: Qt.QShowEvent) -> None: super().showEvent(event) self.graphics_view.setSizePolicy( Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding)) def closeEvent(self, event: Qt.QCloseEvent) -> None: if self.save_on_exit: self.toolbars.misc.save() def __getstate__(self) -> Mapping[str, Any]: state = { attr_name: getattr(self, attr_name) for attr_name in self.storable_attrs } state.update({ 'timeline_mode': self.timeline.mode, 'window_geometry': bytes(self.saveGeometry()), 'window_state': bytes(self.saveState()), }) return state def __setstate__(self, state: Mapping[str, Any]) -> None: # toolbars is singleton, # so it initialize itself right in its __setstate__() try: timeline_mode = state['timeline_mode'] if not Timeline.Mode.is_valid(timeline_mode): raise TypeError except (KeyError, TypeError): logging.warning('Storage loading: failed to parse timeline mode.' ' Using default.') timeline_mode = self.TIMELINE_MODE self.timeline.mode = timeline_mode try: window_geometry = state['window_geometry'] if not isinstance(window_geometry, bytes): raise TypeError self.restoreGeometry(window_geometry) except (KeyError, TypeError): logging.warning('Storage loading: failed to parse window geometry.' ' Using default.') try: window_state = state['window_state'] if not isinstance(window_state, bytes): raise TypeError self.restoreState(window_state) except (KeyError, TypeError): logging.warning('Storage loading: failed to parse window state.' ' Using default.')
def setup_ui(self) -> None: layout = Qt.QHBoxLayout(self) layout.setObjectName('BenchmarkToolbar.setup_ui.layout') layout.setContentsMargins(0, 0, 0, 0) start_label = Qt.QLabel(self) start_label.setObjectName('BenchmarkToolbar.setup_ui.start_label') start_label.setText('Start:') layout.addWidget(start_label) self.start_frame_control = FrameEdit[Frame](self) layout.addWidget(self.start_frame_control) self.start_time_control = TimeEdit[Time](self) layout.addWidget(self.start_time_control) end_label = Qt.QLabel(self) end_label.setObjectName('BenchmarkToolbar.setup_ui.end_label') end_label.setText('End:') layout.addWidget(end_label) self.end_frame_control = FrameEdit[Frame](self) layout.addWidget(self.end_frame_control) self.end_time_control = TimeEdit[Time](self) layout.addWidget(self.end_time_control) total_label = Qt.QLabel(self) total_label.setObjectName('BenchmarkToolbar.setup_ui.total_label') total_label.setText('Total:') layout.addWidget(total_label) self.total_frames_control = FrameEdit[FrameInterval](self) self.total_frames_control.setMinimum(FrameInterval(1)) layout.addWidget(self.total_frames_control) self.total_time_control = TimeEdit[TimeInterval](self) layout.addWidget(self.total_time_control) self.prefetch_checkbox = Qt.QCheckBox(self) self.prefetch_checkbox.setText('Prefetch') self.prefetch_checkbox.setChecked(True) self.prefetch_checkbox.setToolTip( 'Request multiple frames in advance.') layout.addWidget(self.prefetch_checkbox) self.unsequenced_checkbox = Qt.QCheckBox(self) self.unsequenced_checkbox.setText('Unsequenced') self.unsequenced_checkbox.setChecked(True) self.unsequenced_checkbox.setToolTip( "If enabled, next frame will be requested each time " "frameserver returns completed frame. " "If disabled, first frame that's currently processing " "will be waited before requesting next. Like for playback. ") layout.addWidget(self.unsequenced_checkbox) self.run_abort_button = Qt.QPushButton(self) self.run_abort_button.setText('Run') self.run_abort_button.setCheckable(True) layout.addWidget(self.run_abort_button) self.info_label = Qt.QLabel(self) layout.addWidget(self.info_label) layout.addStretch()
def calculate_notch_interval_f(self, target_interval_x: int) -> FrameInterval: intervals = ( FrameInterval( 1), FrameInterval( 5), FrameInterval( 10), FrameInterval( 20), FrameInterval( 25), FrameInterval( 50), FrameInterval( 75), FrameInterval( 100), FrameInterval( 200), FrameInterval( 250), FrameInterval( 500), FrameInterval( 750), FrameInterval( 1000), FrameInterval( 2000), FrameInterval( 2500), FrameInterval( 5000), FrameInterval( 7500), FrameInterval(10000), ) margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_f = self.x_to_f(target_interval_x) for interval in intervals: if target_interval_f < interval * margin: return interval return intervals[-1]
def on_seek_time_changed(self, time: TimeInterval) -> None: qt_silent_call(self.seek_frame_control.setValue, FrameInterval(time))
class Timeline(Qt.QWidget): __slots__ = ( 'app', 'main', 'rectF', 'prevRectF', 'totalT', 'totalF', 'notchIntervalTargetX', 'notchHeight', 'fontHeight', 'notchLabelInterval', 'notchScrollInterval', 'scrollHeight', 'cursorX', 'cursorFT', 'needFullRepaint', 'scrollRect', ) class Mode(YAMLObject): yaml_tag = '!Timeline.Mode' FRAME = 'frame' TIME = 'time' @classmethod def is_valid(cls, value: str) -> bool: return value in (cls.FRAME, cls.TIME) clicked = Qt.pyqtSignal(Frame, Time) def __init__(self, parent: Qt.QWidget) -> None: from vspreview.utils import main_window super().__init__(parent) self.app = Qt.QApplication.instance() self.main = main_window() self._mode = self.Mode.TIME self.rect_f = Qt.QRectF() self.end_t = Time(seconds=1) self.end_f = Frame(1) self.notch_interval_target_x = round(75 * self.main.display_scale) self.notch_height = round(6 * self.main.display_scale) self.font_height = round(10 * self.main.display_scale) self.notch_label_interval = round(-1 * self.main.display_scale) self.notch_scroll_interval = round(2 * self.main.display_scale) self.scroll_height = round(10 * self.main.display_scale) self.setMinimumSize(self.notch_interval_target_x, round(33 * self.main.display_scale)) font = self.font() font.setPixelSize(self.font_height) self.setFont(font) self.cursor_x = 0 # used as a fallback when self.rectF.width() is 0, # so cursorX is incorrect self.cursor_ftx: Optional[Union[Frame, Time, int]] = None # False means that only cursor position'll be recalculated self.need_full_repaint = True self.toolbars_notches: Dict[AbstractToolbar, Notches] = {} self.setAttribute(Qt.Qt.WA_OpaquePaintEvent) self.setMouseTracking(True) def paintEvent(self, event: Qt.QPaintEvent) -> None: super().paintEvent(event) self.rect_f = Qt.QRectF(event.rect()) # self.rectF.adjust(0, 0, -1, -1) if self.cursor_ftx is not None: self.set_position(self.cursor_ftx) self.cursor_ftx = None painter = Qt.QPainter(self) self.drawWidget(painter) def drawWidget(self, painter: Qt.QPainter) -> None: from copy import deepcopy from vspreview.utils import strfdelta # calculations if self.need_full_repaint: labels_notches = Notches() label_notch_bottom = (self.rect_f.top() + self.font_height + self.notch_label_interval + self.notch_height + 5) label_notch_top = label_notch_bottom - self.notch_height label_notch_x = self.rect_f.left() if self.mode == self.Mode.TIME: notch_interval_t = self.calculate_notch_interval_t( self.notch_interval_target_x) label_format = self.generate_label_format( notch_interval_t, self.end_t) label_notch_t = Time() while (label_notch_x < self.rect_f.right() and label_notch_t <= self.end_t): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_t), line=line)) label_notch_t += notch_interval_t label_notch_x = self.t_to_x(label_notch_t) elif self.mode == self.Mode.FRAME: notch_interval_f = self.calculate_notch_interval_f( self.notch_interval_target_x) label_notch_f = Frame(0) while (label_notch_x < self.rect_f.right() and label_notch_f <= self.end_f): line = Qt.QLineF(label_notch_x, label_notch_bottom, label_notch_x, label_notch_top) labels_notches.add( Notch(deepcopy(label_notch_f), line=line)) label_notch_f += notch_interval_f label_notch_x = self.f_to_x(label_notch_f) self.scroll_rect = Qt.QRectF( self.rect_f.left(), label_notch_bottom + self.notch_scroll_interval, self.rect_f.width(), self.scroll_height) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: if isinstance(notch.data, Frame): x = self.f_to_x(notch.data) elif isinstance(notch.data, Time): x = self.t_to_x(notch.data) y = self.scroll_rect.top() notch.line = Qt.QLineF(x, y, x, y + self.scroll_rect.height() - 1) cursor_line = Qt.QLineF( self.cursor_x, self.scroll_rect.top(), self.cursor_x, self.scroll_rect.top() + self.scroll_rect.height() - 1) # drawing if self.need_full_repaint: painter.fillRect(self.rect_f, self.palette().color(Qt.QPalette.Window)) painter.setPen( Qt.QPen(self.palette().color(Qt.QPalette.WindowText))) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.drawLines([notch.line for notch in labels_notches]) painter.setRenderHint(Qt.QPainter.Antialiasing) for i, notch in enumerate(labels_notches): line = notch.line anchor_rect = Qt.QRectF(line.x2(), line.y2() - self.notch_label_interval, 0, 0) if self.mode == self.Mode.TIME: time = cast(Time, notch.data) label = strfdelta(time, label_format) if self.mode == self.Mode.FRAME: label = str(notch.data) if i == 0: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignLeft, label) if self.mode == self.Mode.TIME: rect.moveLeft(-2.5) elif i == (len(labels_notches) - 1): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) if rect.right() > self.rect_f.right(): rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignRight, label) else: rect = painter.boundingRect( anchor_rect, Qt.Qt.AlignBottom + Qt.Qt.AlignHCenter, label) painter.drawText(rect, label) painter.setRenderHint(Qt.QPainter.Antialiasing, False) painter.fillRect(self.scroll_rect, Qt.Qt.gray) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: painter.setPen(notch.color) painter.drawLine(notch.line) painter.setPen(Qt.Qt.black) painter.drawLine(cursor_line) self.need_full_repaint = False def full_repaint(self) -> None: self.need_full_repaint = True self.update() def moveEvent(self, event: Qt.QMoveEvent) -> None: super().moveEvent(event) self.full_repaint() def mousePressEvent(self, event: Qt.QMouseEvent) -> None: super().mousePressEvent(event) pos = Qt.QPoint(event.pos()) if self.scroll_rect.contains(pos): self.set_position(pos.x()) self.clicked.emit(self.x_to_f(self.cursor_x, Frame), self.x_to_t(self.cursor_x, Time)) def mouseMoveEvent(self, event: Qt.QMouseEvent) -> None: super().mouseMoveEvent(event) for toolbar, notches in self.toolbars_notches.items(): if not toolbar.is_notches_visible(): continue for notch in notches: line = notch.line if line.x1() - 0.5 <= event.x() <= line.x1() + 0.5: Qt.QToolTip.showText(event.globalPos(), notch.label) return def resizeEvent(self, event: Qt.QResizeEvent) -> None: super().resizeEvent(event) self.full_repaint() def event(self, event: Qt.QEvent) -> bool: if event.type() in (Qt.QEvent.Polish, Qt.QEvent.ApplicationPaletteChange): self.setPalette(self.main.palette()) self.full_repaint() return True return super().event(event) def update_notches(self, toolbar: Optional[AbstractToolbar] = None) -> None: if toolbar is not None: self.toolbars_notches[toolbar] = toolbar.get_notches() if toolbar is None: for t in self.main.toolbars: self.toolbars_notches[t] = t.get_notches() self.full_repaint() @property def mode(self) -> str: # pylint: disable=undefined-variable return self._mode @mode.setter def mode(self, value: str) -> None: if value == self._mode: return self._mode = value self.full_repaint() notch_intervals_t = ( TimeInterval(seconds=1), TimeInterval(seconds=2), TimeInterval(seconds=5), TimeInterval(seconds=10), TimeInterval(seconds=15), TimeInterval(seconds=30), TimeInterval(seconds=60), TimeInterval(seconds=90), TimeInterval(seconds=120), TimeInterval(seconds=300), TimeInterval(seconds=600), TimeInterval(seconds=900), TimeInterval(seconds=1200), TimeInterval(seconds=1800), TimeInterval(seconds=2700), TimeInterval(seconds=3600), TimeInterval(seconds=5400), TimeInterval(seconds=7200), ) def calculate_notch_interval_t(self, target_interval_x: int) -> TimeInterval: margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_t = self.x_to_t(target_interval_x, TimeInterval) if target_interval_t >= self.notch_intervals_t[-1] * margin: return self.notch_intervals_t[-1] for interval in self.notch_intervals_t: if target_interval_t < interval * margin: return interval raise RuntimeError notch_intervals_f = ( FrameInterval(1), FrameInterval(5), FrameInterval(10), FrameInterval(20), FrameInterval(25), FrameInterval(50), FrameInterval(75), FrameInterval(100), FrameInterval(200), FrameInterval(250), FrameInterval(500), FrameInterval(750), FrameInterval(1000), FrameInterval(2000), FrameInterval(2500), FrameInterval(5000), FrameInterval(7500), FrameInterval(10000), FrameInterval(20000), FrameInterval(25000), FrameInterval(50000), FrameInterval(75000), ) def calculate_notch_interval_f(self, target_interval_x: int) -> FrameInterval: margin = 1 + self.main.TIMELINE_LABEL_NOTCHES_MARGIN / 100 target_interval_f = self.x_to_f(target_interval_x, FrameInterval) if target_interval_f >= FrameInterval( round(int(self.notch_intervals_f[-1]) * margin)): return self.notch_intervals_f[-1] for interval in self.notch_intervals_f: if target_interval_f < FrameInterval(round( int(interval) * margin)): return interval raise RuntimeError def generate_label_format(self, notch_interval_t: TimeInterval, end_time: TimeInterval) -> str: if end_time >= TimeInterval(hours=1): return '%h:%M:00' elif notch_interval_t >= TimeInterval(minutes=1): return '%m:00' else: return '%m:%S' def set_end_frame(self, end_f: Frame) -> None: self.end_f = end_f self.end_t = Time(end_f) self.full_repaint() def set_position(self, pos: Union[Frame, Time, int]) -> None: if self.rect_f.width() == 0.0: self.cursor_ftx = pos if isinstance(pos, Frame): self.cursor_x = self.f_to_x(pos) elif isinstance(pos, Time): self.cursor_x = self.t_to_x(pos) elif isinstance(pos, int): self.cursor_x = pos else: raise TypeError self.update() def t_to_x(self, t: TimeType) -> int: width = self.rect_f.width() try: x = round(float(t) / float(self.end_t) * width) except ZeroDivisionError: x = 0 return x def x_to_t(self, x: int, ty: Type[TimeType]) -> TimeType: width = self.rect_f.width() return ty(seconds=(x * float(self.end_t) / width)) def f_to_x(self, f: FrameType) -> int: width = self.rect_f.width() try: x = round(int(f) / int(self.end_f) * width) except ZeroDivisionError: x = 0 return x def x_to_f(self, x: int, ty: Type[FrameType]) -> FrameType: width = self.rect_f.width() value = round(x / width * int(self.end_f)) return ty(value)
class MainWindow(AbstractMainWindow): # those are defaults that can be overriden at runtime or used as fallbacks AUTOSAVE_ENABLED = True AUTOSAVE_INTERVAL = 30 * 1000 # s BASE_PPI = 96 # PPI BENCHMARK_CLEAR_CACHE = False BENCHMARK_REFRESH_INTERVAL = 150 # ms DARK_THEME = True FPS_AVERAGING_WINDOW_SIZE = FrameInterval(100) FPS_REFRESH_INTERVAL = 150 # ms LOG_LEVEL = logging.DEBUG OPENGL_RENDERING = False OUTPUT_INDEX = 0 PLAY_BUFFER_SIZE = FrameInterval(get_usable_cpus_count()) PNG_COMPRESSION_LEVEL = 0 # 0 - 100 SAVE_TEMPLATE = '{script_name}_{frame}' SEEK_STEP = 1 # frames STATUSBAR_MESSAGE_TIMEOUT = 3 * 1000 # s # it's allowed to stretch target interval betweewn notches by N% at most TIMELINE_LABEL_NOTCHES_MARGIN = 20 # % TIMELINE_MODE = 'frame' # would be used for formats with subsampling VS_OUTPUT_RESIZER = Output.Resizer.Bicubic VS_OUTPUT_MATRIX = Output.Matrix.BT709 VS_OUTPUT_TRANSFER = Output.Transfer.BT709 VS_OUTPUT_PRIMARIES = Output.Primaries.BT709 VS_OUTPUT_RANGE = Output.Range.LIMITED VS_OUTPUT_CHROMALOC = Output.ChromaLoc.LEFT VS_OUTPUT_PREFER_PROPS = True VS_OUTPUT_RESIZER_KWARGS = {} # type: Mapping[str, str] BENCHMARK_FRAME_DATA_SHARING_FIX = True DEBUG_PLAY_FPS = False DEBUG_TOOLBAR = False DEBUG_TOOLBAR_BUTTONS_PRINT_STATE = False yaml_tag = '!MainWindow' storable_attrs = [ 'toolbars', ] __slots__ = storable_attrs + [ 'app', 'display_scale', 'clipboard', 'script_path', 'save_on_exit', 'timeline', 'main_layout', 'graphics_scene', 'graphics_view', 'script_error_dialog', 'central_widget', 'statusbar', 'opengl_widget', ] def __init__(self) -> None: from qdarkstyle import load_stylesheet_pyqt5 super().__init__() # logging logging.basicConfig(format='{asctime}: {levelname}: {message}', style='{', level=self.LOG_LEVEL) logging.Formatter.default_msec_format = '%s.%03d' # ??? self.app = Qt.QApplication.instance() if self.DARK_THEME: self.app.setStyleSheet( self.patch_dark_stylesheet(load_stylesheet_pyqt5())) self.ensurePolished() self.display_scale = self.app.primaryScreen().logicalDotsPerInch( ) / self.BASE_PPI self.setWindowTitle('VSPreview') self.move(400, 0) self.setup_ui() # global self.clipboard = self.app.clipboard() self.script_path = Path() self.save_on_exit = True # graphics view self.graphics_scene = Qt.QGraphicsScene(self) self.graphics_view.setScene(self.graphics_scene) self.opengl_widget = None if self.OPENGL_RENDERING: self.opengl_widget = Qt.QOpenGLWidget() self.graphics_view.setViewport(self.opengl_widget) self.graphics_view.wheelScrolled.connect(self.on_wheel_scrolled) # timeline self.timeline.clicked.connect(self.switch_frame) # init toolbars and outputs self.toolbars = Toolbars(self) self.main_layout.addWidget(self.toolbars.main) for toolbar in self.toolbars: self.main_layout.addWidget(toolbar) self.toolbars.main.layout().addWidget(toolbar.toggle_button) set_qobject_names(self) self.setObjectName('MainWindow') def setup_ui(self) -> None: from vspreview.widgets import GraphicsView # mainWindow.resize(1300, 808) self.central_widget = Qt.QWidget(self) self.main_layout = Qt.QVBoxLayout(self.central_widget) self.setCentralWidget(self.central_widget) self.graphics_view = GraphicsView(self.central_widget) self.graphics_view.setBackgroundBrush(self.palette().brush( Qt.QPalette.Window)) self.graphics_view.setSizePolicy(Qt.QSizePolicy.Fixed, Qt.QSizePolicy.Fixed) self.graphics_view.setDragMode(Qt.QGraphicsView.ScrollHandDrag) self.graphics_view.setTransformationAnchor( GraphicsView.AnchorUnderMouse) self.main_layout.addWidget(self.graphics_view) self.timeline = Timeline(self.central_widget) self.main_layout.addWidget(self.timeline) # status bar self.statusbar = StatusBar(self.central_widget) self.statusbar.total_frames_label = Qt.QLabel(self.central_widget) self.statusbar.total_frames_label.setObjectName( 'MainWindow.statusbar.total_frames_label') self.statusbar.addWidget(self.statusbar.total_frames_label) self.statusbar.duration_label = Qt.QLabel(self.central_widget) self.statusbar.duration_label.setObjectName( 'MainWindow.statusbar.duration_label') self.statusbar.addWidget(self.statusbar.duration_label) self.statusbar.resolution_label = Qt.QLabel(self.central_widget) self.statusbar.resolution_label.setObjectName( 'MainWindow.statusbar.resolution_label') self.statusbar.addWidget(self.statusbar.resolution_label) self.statusbar.pixel_format_label = Qt.QLabel(self.central_widget) self.statusbar.pixel_format_label.setObjectName( 'MainWindow.statusbar.pixel_format_label') self.statusbar.addWidget(self.statusbar.pixel_format_label) self.statusbar.fps_label = Qt.QLabel(self.central_widget) self.statusbar.fps_label.setObjectName( 'MainWindow.statusbar.fps_label') self.statusbar.addWidget(self.statusbar.fps_label) self.statusbar.label = Qt.QLabel(self.central_widget) self.statusbar.label.setObjectName('MainWindow.statusbar.label') self.statusbar.addPermanentWidget(self.statusbar.label) self.setStatusBar(self.statusbar) # dialogs self.script_error_dialog = ScriptErrorDialog(self) def patch_dark_stylesheet(self, stylesheet: str) -> str: return stylesheet + 'QGraphicsView { border: 0px; padding: 0px; }' def load_script(self, script_path: Path, external_args: str = '') -> None: from traceback import print_exc import shlex self.toolbars.playback.stop() self.statusbar.label.setText('Evaluating') self.script_path = script_path sys.path.append(str(self.script_path.parent)) # Rewrite args so external args will be forwarded correctly if external_args: self.external_args = shlex.split(external_args) try: argv_orig = sys.argv sys.argv = [sys.argv[1]] + self.external_args except AttributeError: pass try: exec(self.script_path.read_text(), {}) # pylint: disable=exec-used except Exception: # pylint: disable=broad-except logging.error( 'Script contains error(s). Check following lines for details.') self.handle_script_error( 'Script contains error(s). See console output for details.') print_exc() return finally: sys.argv = argv_orig sys.path.pop() if len(vs.get_outputs()) == 0: logging.error('Script has no outputs set.') self.handle_script_error('Script has no outputs set.') return self.toolbars.main.rescan_outputs() # self.init_outputs() self.switch_output(self.OUTPUT_INDEX) self.load_storage() def load_storage(self) -> None: import yaml storage_path = self.script_path.with_suffix('.yml') if storage_path.exists(): try: yaml.load(storage_path.open(), Loader=yaml.Loader) except yaml.YAMLError as exc: if isinstance(exc, yaml.MarkedYAMLError): logging.warning( 'Storage parsing failed on line {} column {}. Using defaults.' .format(exc.problem_mark.line + 1, exc.problem_mark.column + 1)) # pylint: disable=no-member else: logging.warning('Storage parsing failed. Using defaults.') else: logging.info('No storage found. Using defaults.') self.statusbar.label.setText('Ready') def init_outputs(self) -> None: self.graphics_scene.clear() for output in self.outputs: frame_pixmap = output.render_frame(output.last_showed_frame) frame_item = self.graphics_scene.addPixmap(frame_pixmap) frame_item.hide() output.graphics_scene_item = frame_item def reload_script(self) -> None: if self.toolbars.misc.autosave_enabled: self.toolbars.misc.save() vs.clear_outputs() self.graphics_scene.clear() self.load_script(self.script_path) self.show_message('Reloaded successfully') def render_frame(self, frame: Frame, output: Optional[Output] = None) -> Qt.QPixmap: return self.current_output.render_frame(frame) def switch_frame(self, frame: Optional[Frame] = None, time: Optional[Time] = None, *, render_frame: bool = True) -> None: if frame is not None: time = Time(frame) elif time is not None: frame = Frame(time) else: logging.debug('switch_frame(): both frame and time is None') return if frame > self.current_output.end_frame: return self.current_output.last_showed_frame = frame self.timeline.set_position(frame) self.toolbars.main.on_current_frame_changed(frame, time) for toolbar in self.toolbars: toolbar.on_current_frame_changed(frame, time) if render_frame: self.current_output.graphics_scene_item.setPixmap( self.render_frame(frame)) def switch_output(self, value: Union[int, Output]) -> None: if len(self.outputs) == 0: return if isinstance(value, Output): index = self.outputs.index_of(value) else: index = value if index < 0 or index >= len(self.outputs): return prev_index = self.toolbars.main.outputs_combobox.currentIndex() self.toolbars.playback.stop() # current_output relies on outputs_combobox self.toolbars.main.on_current_output_changed(index, prev_index) self.timeline.set_end_frame(self.current_output.end_frame) if self.current_output.frame_to_show is not None: self.current_frame = self.current_output.frame_to_show else: self.current_frame = self.current_output.last_showed_frame for output in self.outputs: output.graphics_scene_item.hide() self.current_output.graphics_scene_item.show() self.graphics_scene.setSceneRect( Qt.QRectF(self.current_output.graphics_scene_item.pixmap().rect())) self.timeline.update_notches() for toolbar in self.toolbars: toolbar.on_current_output_changed(index, prev_index) self.update_statusbar_output_info() @property # type: ignore def current_output(self) -> Output: # type: ignore output = cast(Output, self.toolbars.main.outputs_combobox.currentData()) return output @current_output.setter def current_output(self, value: Output) -> None: self.switch_output(self.outputs.index_of(value)) @property # type: ignore def current_frame(self) -> Frame: # type: ignore return self.current_output.last_showed_frame @current_frame.setter def current_frame(self, value: Frame) -> None: self.switch_frame(value) @property # type: ignore def current_time(self) -> Time: # type: ignore return Time(self.current_output.last_showed_frame) @current_time.setter def current_time(self, value: Time) -> None: self.switch_frame(time=value) @property def outputs(self) -> Outputs: # type: ignore return cast(Outputs, self.toolbars.main.outputs) def handle_script_error(self, message: str) -> None: # logging.error(message) self.script_error_dialog.label.setText(message) self.script_error_dialog.open() def on_wheel_scrolled(self, steps: int) -> None: new_index = self.toolbars.main.zoom_combobox.currentIndex() + steps if new_index < 0: new_index = 0 elif new_index >= len(self.toolbars.main.zoom_levels): new_index = len(self.toolbars.main.zoom_levels) - 1 self.toolbars.main.zoom_combobox.setCurrentIndex(new_index) def show_message(self, message: str, timeout: Optional[int] = None) -> None: if timeout is None: timeout = self.STATUSBAR_MESSAGE_TIMEOUT self.statusbar.showMessage(message, timeout) def update_statusbar_output_info(self, output: Optional[Output] = None) -> None: if output is None: output = self.current_output self.statusbar.total_frames_label.setText('{} frames '.format( output.total_frames)) self.statusbar.duration_label.setText('{} '.format(output.total_time)) self.statusbar.resolution_label.setText('{}x{} '.format( output.width, output.height)) if not output.has_alpha: self.statusbar.pixel_format_label.setText('{} '.format( output.format.name)) else: self.statusbar.pixel_format_label.setText( 'Clip: {}, Alpha: {} '.format(output.format.name, output.format_alpha.name)) if output.fps_den != 0: self.statusbar.fps_label.setText('{}/{} = {:.3f} fps '.format( output.fps_num, output.fps_den, output.fps_num / output.fps_den)) else: self.statusbar.fps_label.setText('{}/{} fps '.format( output.fps_num, output.fps_den)) def event(self, event: Qt.QEvent) -> bool: if event.type() == Qt.QEvent.LayoutRequest: self.timeline.full_repaint() return super().event(event) # misc methods def showEvent(self, event: Qt.QShowEvent) -> None: super().showEvent(event) self.graphics_view.setSizePolicy( Qt.QSizePolicy(Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Expanding)) def closeEvent(self, event: Qt.QCloseEvent) -> None: if (self.toolbars.misc.autosave_enabled and self.save_on_exit): self.toolbars.misc.save() def __getstate__(self) -> Mapping[str, Any]: state = { attr_name: getattr(self, attr_name) for attr_name in self.storable_attrs } state.update({'timeline_mode': self.timeline.mode}) return state def __setstate__(self, state: Mapping[str, Any]) -> None: # toolbars is singleton, # so it initialize itself right in its __setstate__() try: timeline_mode = state['timeline_mode'] if not Timeline.Mode.is_valid(timeline_mode): raise TypeError except (KeyError, TypeError): logging.warning( 'Storage loading: failed to parse timeline mode. Using default.' ) timeline_mode = self.TIMELINE_MODE self.timeline.mode = timeline_mode