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 __init__(self, main: AbstractMainWindow) -> None: from concurrent.futures import Future super().__init__(main, 'Playback') self.setup_ui() self.play_buffer: Deque[Future] = deque() self.play_timer = Qt.QTimer() self.play_timer.setTimerType(Qt.Qt.PreciseTimer) self.play_timer.timeout.connect(self._show_next_frame) self.fps_history: Deque[int] = deque( [], int(self.main.FPS_AVERAGING_WINDOW_SIZE) + 1) self.current_fps = 0.0 self.fps_timer = Qt.QTimer() self.fps_timer.setTimerType(Qt.Qt.PreciseTimer) self.fps_timer.timeout.connect( lambda: self.fps_spinbox.setValue(self.current_fps)) self.play_start_time: Optional[int] = None self.play_start_frame = Frame(0) self.play_end_time = 0 self.play_end_frame = Frame(0) self.play_pause_button.clicked.connect(self.on_play_pause_clicked) self.seek_to_prev_button.clicked.connect(self.seek_to_prev) self.seek_to_next_button.clicked.connect(self.seek_to_next) self.seek_n_frames_b_button.clicked.connect(self.seek_n_frames_b) self.seek_n_frames_f_button.clicked.connect(self.seek_n_frames_f) self.seek_to_start_button.clicked.connect(self.seek_to_start) self.seek_to_end_button.clicked.connect(self.seek_to_end) self.seek_frame_control.valueChanged.connect( self.on_seek_frame_changed) self.seek_time_control.valueChanged.connect(self.on_seek_time_changed) self.fps_spinbox.valueChanged.connect(self.on_fps_changed) self.fps_reset_button.clicked.connect(self.reset_fps) self.fps_unlimited_checkbox.stateChanged.connect( self.on_fps_unlimited_changed) add_shortcut(Qt.Qt.Key_Space, self.play_pause_button.click) add_shortcut(Qt.Qt.Key_Left, self.seek_to_prev_button.click) add_shortcut(Qt.Qt.Key_Right, self.seek_to_next_button.click) add_shortcut(Qt.Qt.SHIFT + Qt.Qt.Key_Left, self.seek_n_frames_b_button.click) add_shortcut(Qt.Qt.SHIFT + Qt.Qt.Key_Right, self.seek_n_frames_f_button.click) add_shortcut(Qt.Qt.Key_Home, self.seek_to_start_button.click) add_shortcut(Qt.Qt.Key_End, self.seek_to_end_button.click) set_qobject_names(self)
def import_matroska_xml_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as scenes. Preserve end time and text if they're present. ''' from xml.etree import ElementTree timestamp_pattern = re.compile(r'(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)') try: root = ElementTree.parse(str(path)).getroot() except ElementTree.ParseError as exc: logging.warning( f'Scening import: error occured' ' while parsing \'{path.name}\':') logging.warning(exc.msg) return for chapter in root.iter('ChapterAtom'): start_element = chapter.find('ChapterTimeStart') if start_element is None or start_element.text is None: continue match = timestamp_pattern.match(start_element.text) if match is None: continue start = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) end = None end_element = chapter.find('ChapterTimeEnd') if end_element is not None and end_element.text is not None: match = timestamp_pattern.match(end_element.text) if match is not None: end = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) label = '' label_element = chapter.find('ChapterDisplay/ChapterString') if label_element is not None and label_element.text is not None: label = label_element.text try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def import_matroska_timestamps_v1(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports gaps between timestamps as scenes. ''' pattern = re.compile(r'(\d+),(\d+),(\d+(?:\.\d+)?)') for match in pattern.finditer(path.read_text()): try: scening_list.add(Frame(int(match[1])), Frame(int(match[2])), '{:.3f} fps'.format(float(match[3]))) except ValueError: out_of_range_count += 1
def import_ass(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports lines as scenes. Text is ignored. ''' import pysubs2 subs = pysubs2.load(str(path)) for line in subs: t_start = Time(milliseconds=line.start) t_end = Time(milliseconds=line.end) try: scening_list.add(Frame(t_start), Frame(t_end)) except ValueError: out_of_range_count += 1
def import_vsedit(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports bookmarks as single-frame scenes ''' for bookmark in path.read_text().split(', '): scening_list.add(Frame(int(bookmark)))
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.setImage( self.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 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 import_celltimes(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports cell times as single-frame scenes ''' for line in path.read_text().splitlines(): scening_list.add(Frame(int(line)))
def import_matroska_timestamps_v2(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports intervals of constant FPS as scenes. Uses FPS for scene label. ''' timestamps: List[Time] = [] for line in path.read_text().splitlines(): try: timestamps.append(Time(milliseconds=float(line))) except ValueError: continue if len(timestamps) < 2: logging.warning( 'Scening import: timestamps file contains less than' ' 2 timestamps, so there\'s nothing to import.') return deltas = [ timestamps[i] - timestamps[i - 1] for i in range(1, len(timestamps)) ] scene_delta = deltas[0] scene_start = Frame(0) scene_end: Optional[Frame] = None for i in range(1, len(deltas)): if abs(round(float(deltas[i] - scene_delta), 6)) <= 0.000_001: continue # TODO: investigate, why offset by -1 is necessary here scene_end = Frame(i - 1) try: scening_list.add( scene_start, scene_end, '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1 scene_start = Frame(i) scene_end = None scene_delta = deltas[i] if scene_end is None: try: scening_list.add( scene_start, Frame(len(timestamps) - 1), '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1
def import_tfm(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports TFM's 'OVR HELP INFORMATION'. Single combed frames are put into single-frame scenes. Frame groups are put into regular scenes. Combed probability is used for label. ''' class TFMFrame(Frame): mic: Optional[int] tfm_frame_pattern = re.compile(r'(\d+)\s\((\d+)\)') tfm_group_pattern = re.compile(r'(\d+),(\d+)\s\((\d+(?:\.\d+)%)\)') log = path.read_text() start_pos = log.find('OVR HELP INFORMATION') if start_pos == -1: logging.warning( 'Scening import: TFM log doesn\'t contain' '"OVR Help Information" section.') return log = log[start_pos:] tfm_frames: Set[TFMFrame] = set() for match in tfm_frame_pattern.finditer(log): tfm_frame = TFMFrame(int(match[1])) tfm_frame.mic = int(match[2]) tfm_frames.add(tfm_frame) for match in tfm_group_pattern.finditer(log): try: scene = scening_list.add( Frame(int(match[1])), Frame(int(match[2])), '{} combed'.format(match[3])) except ValueError: out_of_range_count += 1 continue tfm_frames -= set(range(int(scene.start), int(scene.end) + 1)) for tfm_frame in tfm_frames: try: scening_list.add(tfm_frame, label=str(tfm_frame.mic)) except ValueError: out_of_range_count += 1
def import_cue(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports tracks as scenes. Uses TITLE for scene label. ''' from cueparser import CueSheet def offset_to_time(offset: str) -> Optional[Time]: pattern = re.compile(r'(\d{1,2}):(\d{1,2}):(\d{1,2})') match = pattern.match(offset) if match is None: return None return Time( minutes = int(match[1]), seconds = int(match[2]), milliseconds = int(match[3]) / 75 * 1000) cue_sheet = CueSheet() cue_sheet.setOutputFormat('') cue_sheet.setData(path.read_text()) cue_sheet.parse() for track in cue_sheet.tracks: if track.offset is None: continue offset = offset_to_time(track.offset) if offset is None: logging.warning( f'Scening import: INDEX timestamp \'{track.offset}\'' ' format isn\'t suported.') continue start = Frame(offset) end = None if track.duration is not None: end = Frame(offset + TimeInterval(track.duration)) label = '' if track.title is not None: label = track.title try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def on_start_frame_changed(self, value: Union[Frame, int]) -> None: frame = Frame(value) index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = index.siblingAtColumn(SceningList.START_FRAME_COLUMN) if not index.isValid(): return self.scening_list.setData(index, frame, Qt.Qt.UserRole)
def import_dgi(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports IDR frames as single-frame scenes. ''' pattern = re.compile(r'IDR\s\d+\n(\d+):FRM', re.RegexFlag.MULTILINE) for match in pattern.findall(path.read_text()): try: scening_list.add(Frame(match)) except ValueError: out_of_range_count += 1
def import_qp(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports I- and K-frames as single-frame scenes. ''' pattern = re.compile(r'(\d+)\sI|K') for match in pattern.findall(path.read_text()): try: scening_list.add(Frame(int(match))) except ValueError: out_of_range_count += 1
def import_xvid(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports I-frames as single-frame scenes. ''' for i, line in enumerate(path.read_text().splitlines()): if not line.startswith('i'): continue try: scening_list.add(Frame(i - 3)) except ValueError: out_of_range_count += 1
def __init__(self, main_window: AbstractMainWindow) -> None: from vspreview.models import ZoomLevels super().__init__(main_window) self.setup_ui() self.outputs = Outputs() self.outputs_combobox.setModel(self.outputs) self.frame_spinbox.setMinimum(0) self.time_spinbox.setMinimumTime(Qt.QTime()) self.zoom_levels = ZoomLevels( [0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 4.0, 8.0]) self.zoom_combobox.setModel(self.zoom_levels) self.zoom_combobox.setCurrentIndex(3) self.save_file_types = {'Single Image (*.png)': self.save_as_png} self.outputs_combobox.currentIndexChanged.connect( self.main.switch_output) self.frame_spinbox.valueChanged.connect( lambda f: self.main.on_current_frame_changed(Frame(f))) self.time_spinbox.timeChanged.connect( lambda qtime: self.main.on_current_frame_changed( t=qtime_to_timedelta(qtime))) # type: ignore self.copy_frame_button.clicked.connect( self.on_copy_frame_button_clicked) self.copy_timestamp_button.clicked.connect( self.on_copy_timestamp_button_clicked) self.zoom_combobox.currentTextChanged.connect(self.on_zoom_changed) self.save_as_button.clicked.connect(self.on_save_as_clicked) self.switch_timeline_mode.clicked.connect( self.on_switch_timeline_mode_clicked) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_1, lambda: self.main.switch_output(0)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_2, lambda: self.main.switch_output(1)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_3, lambda: self.main.switch_output(2)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_4, lambda: self.main.switch_output(3)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_5, lambda: self.main.switch_output(4)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_6, lambda: self.main.switch_output(5)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_7, lambda: self.main.switch_output(6)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_8, lambda: self.main.switch_output(7)) add_shortcut(Qt.Qt.CTRL + Qt.Qt.Key_9, lambda: self.main.switch_output(8))
def import_ogm_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as signle-frame scenes. Uses NAME for scene label. ''' pattern = re.compile( r'(CHAPTER\d+)=(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)\n\1NAME=(.*)', re.RegexFlag.MULTILINE) for match in pattern.finditer(path.read_text()): time = Time( hours = int(match[2]), minutes = int(match[3]), seconds = float(match[4])) try: scening_list.add(Frame(time), label=match[5]) except ValueError: out_of_range_count += 1
def import_ses(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports bookmarks as single-frame scenes ''' import pickle with path.open('rb') as f: try: session = pickle.load(f) except pickle.UnpicklingError: logging.warning('Scening import: failed to load .ses file.') return if 'bookmarks' not in session: return for bookmark in session['bookmarks']: scening_list.add(Frame(bookmark[0]))
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 seek_to_start(self, checked: Optional[bool] = None) -> None: self.stop() self.main.current_frame = Frame(0)
def to_frame(self, t: timedelta) -> Frame: return Frame( round(t.total_seconds() * (self.current_output.fps_num / self.current_output.fps_den)))
def on_seek_frame_changed(self, frame: Union[Frame, int]) -> None: frame = Frame(frame) qt_silent_call(self.seek_time_spinbox.setTime, timedelta_to_qtime(self.main.to_timedelta(frame)))
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 setData(self, index: Qt.QModelIndex, value: Any, role: int = Qt.Qt.EditRole) -> bool: from copy import deepcopy if not index.isValid(): return False if role not in (Qt.Qt.EditRole, Qt.Qt.UserRole): return False row = index.row() column = index.column() scene = deepcopy(self.items[row]) if column == self.START_FRAME_COLUMN: if not isinstance(value, Frame): raise TypeError if scene.start != scene.end: if value > scene.end: return False scene.start = value else: scene.start = value scene.end = value proper_update = True elif column == self.END_FRAME_COLUMN: if not isinstance(value, Frame): raise TypeError if scene.start != scene.end: if value < scene.start: return False scene.end = value else: scene.start = value scene.end = value proper_update = True elif column == self.START_TIME_COLUMN: if not isinstance(value, Time): raise TypeError frame = Frame(value) if scene.start != scene.end: if frame > scene.end: return False scene.start = frame else: scene.start = frame scene.end = frame proper_update = True elif column == self.END_TIME_COLUMN: if not isinstance(value, Time): raise TypeError frame = Frame(value) if scene.start != scene.end: if frame < scene.start: return False scene.end = frame else: scene.start = frame scene.end = frame proper_update = True elif column == self.LABEL_COLUMN: if not isinstance(value, str): raise TypeError scene.label = value proper_update = False if proper_update is True: i = bisect_right(self.items, scene) if i > row: i -= 1 if i != row: self.beginMoveRows(self.createIndex(row, 0), row, row, self.createIndex(i, 0), i) del self.items[row] self.items.insert(i, scene) self.endMoveRows() else: self.items[index.row()] = scene self.dataChanged.emit(index, index) else: self.items[index.row()] = scene self.dataChanged.emit(index, index) return True