def __init__(self): super().__init__() self.__fs_client = FileSystemClient() self.__command = FFMPEGcommand self.__process = None self.__logger = logging.getLogger( 'stepic_studio.video_recorders.camera_recorder') self.last_processed_path = None self.last_processed_file = None self._load_postprocessing_pipe(settings.SERVER_POSTPROCESSING_PIPE)
def __init__(self): self.__fs_client = FileSystemClient() self.__logger = logging.getLogger( 'stepic_studio.postprocessing.VideSynchronizer') self.__max_diff = 2 # seconds self.__min_diff = 0.1 self.__camera_noise_tolerance = '-11.5dB' self.__tablet_noise_tolerance = '-12dB' self.__min_silence_duration = 0.001
def export_obj_to_prproj(db_object, files_extractor) -> InternalOperationResult: """Creates PPro project in .prproj format using ExtendScript script. Project includes video files of each subitem of corresponding object. Screencasts and camera recordings puts on different tracks of single sequence. :param files_extractor: function for extracting target filenames from db_object; :param db_object: db single object. """ ppro_dir = os.path.dirname(settings.ADOBE_PPRO_PATH) if not os.path.isfile(os.path.join(ppro_dir, PRPROJ_REQUIRED_FILE)): return InternalOperationResult( ExecutionStatus.FATAL_ERROR, '\'{0}\' is missing. Please, place \'{0}\' empty file to \n\'{1}\'.' .format(PRPROJ_REQUIRED_FILE, ppro_dir)) if FileSystemClient().process_with_name_exists(PPRO_WIN_PROCESS_NAME): return InternalOperationResult( ExecutionStatus.FATAL_ERROR, 'Only one instance of PPro may exist. Please, close PPro and try again.' ) screen_files, prof_files, marker_times, sync_offsets = files_extractor( db_object) if not screen_files or not prof_files: return InternalOperationResult( ExecutionStatus.FATAL_ERROR, 'Object is empty or subitems are broken.') try: ppro_command = build_ppro_command( db_object.os_path, PRPROJ_TEMPLATES_PATH, screen_files, prof_files, marker_times, sync_offsets, translate_non_alphanumerics(db_object.name)) except Exception as e: return InternalOperationResult(ExecutionStatus.FATAL_ERROR, e) exec_status = FileSystemClient().execute_command_sync( ppro_command, allowable_code=1) # may return 1 - it's OK if exec_status.status is not ExecutionStatus.SUCCESS: logger.error('Cannot execute PPro command: %s \n PPro command: %s', exec_status.message, ppro_command) return InternalOperationResult( ExecutionStatus.FATAL_ERROR, 'Cannot execute PPro command. Check PPro configuration.') logger.info('Execution of PPro command started; \n PPro command: %s', ppro_command) return InternalOperationResult(ExecutionStatus.SUCCESS)
class TSConverter(PostprocessorInterface): def __init__(self): self.fs_client = FileSystemClient() self.logger = logging.getLogger( 'stepic_studio.postprocessing.TSConverter') def process(self, path: str, filename: str) -> (str, str): new_filename = os.path.splitext(filename)[ 0] + MP4_EXTENSION # change file extension from .TS to .mp4 source_file = os.path.join(path, filename) target_file = os.path.join(path, new_filename) reencode_command = settings.FFMPEG_PATH + ' ' + \ settings.CAMERA_REENCODE_TEMPLATE.format(source_file, target_file) result, _ = self.fs_client.execute_command(reencode_command) if result.status is ExecutionStatus.SUCCESS: self.logger.info( 'Successfully start converting TS to mp4 (FFMPEG command: %s)', reencode_command) else: self.logger.error('Converting failed: %s; FFMPEG command: %s', result.message, reencode_command) return path, new_filename
def delete_substep_on_disk(substep) -> InternalOperationResult: client = FileSystemClient() cam_removing_info = client.remove_file(substep.os_path) if cam_removing_info.status is not ExecutionStatus.SUCCESS: return cam_removing_info screencast_removing_info = client.remove_file(substep.os_screencast_path) if screencast_removing_info.status is not ExecutionStatus.SUCCESS: return screencast_removing_info raw_cut_removing_info = client.remove_file(substep.os_automontage_file) if raw_cut_removing_info.status is not ExecutionStatus.SUCCESS: return raw_cut_removing_info return InternalOperationResult(ExecutionStatus.SUCCESS)
def start_recording(substep) -> InternalOperationResult: create_status = FileSystemClient().create_recursively(substep.dir_path) if create_status.status is not ExecutionStatus.SUCCESS: logger.error('Can\'t create folder for new substep: %s', create_status.message) return create_status tablet_exec_info = TabletScreenRecorder().start_recording( substep.os_tablet_dir, substep.screencast_name) if tablet_exec_info.status is not ExecutionStatus.SUCCESS: return tablet_exec_info ffmpeg_status = ServerCameraRecorder().start_recording( substep.dir_path, substep.camera_recording_name) if ffmpeg_status.status is not ExecutionStatus.SUCCESS: TabletScreenRecorder().stop_recording() return ffmpeg_status db_camera = CameraStatus.objects.get(id='1') if not db_camera.status: db_camera.status = True db_camera.start_time = int(round(time.time() * 1000)) db_camera.save() return InternalOperationResult(ExecutionStatus.SUCCESS)
def convert_mkv_to_mp4(path: str, filename: str): new_filename = os.path.splitext( filename)[0] + MP4_EXTENSION # change file extension from .mkv to .mp4 source_file = os.path.join(path, filename) target_file = os.path.join(path, new_filename) fs_client = FileSystemClient() if not os.path.isfile(source_file): logger.error('Converting mkv to mp4 failed; file %s doesn\'t exist', source_file) return reencode_command = settings.FFMPEG_PATH + ' ' + \ settings.TABLET_REENCODE_TEMPLATE.format(source_file, target_file) result, _ = fs_client.execute_command(reencode_command) if result.status is ExecutionStatus.SUCCESS: logger.info( 'Successfully start converting mkv to mp4 (FFMPEG command: %s)', reencode_command) else: logger.error('Converting mkv to mp4 failed: %s; FFMPEG command: %s', result.message, reencode_command)
def __enter__(self): self.audio_path = os.path.splitext(self.video_path)[0] + '.wav' extract_result = FileSystemClient.execute_command_sync([ settings.FFMPEG_PATH, '-y', '-i', self.video_path, '-vn', '-ac', '1', '-ar', str(TEMP_AUDIO_FREQUENCY), '-f', 'wav', self.audio_path ]) if extract_result.status is not ExecutionStatus.SUCCESS: raise Exception(extract_result.message) self.wave_form = wave.open(self.audio_path, 'r') return self.wave_form
class FileRemover(PostprocessorInterface): def __init__(self): self.fs_client = FileSystemClient() self.logger = logging.getLogger(__name__) def process(self, path: str, filename: str) -> (str, str): file_to_remove = os.path.splitext(filename)[0] + TS_EXTENSION path = os.path.join(path, file_to_remove) if not os.path.isfile(path): self.logger.warning( 'Removing %s file failed: file %s is not valid.', TS_EXTENSION, path) return path, filename remove_status = self.fs_client.remove_file(path) if remove_status.status is not ExecutionStatus.SUCCESS: self.logger.error('Removing file %s failed: %s', path, remove_status.message) return path, filename
def delete_lesson_on_disk(lesson) -> InternalOperationResult: client = FileSystemClient() client.delete_recursively(lesson.os_automontage_path) return client.delete_recursively(lesson.os_path)
def __exit__(self, *exc_info): self.wave_form.close() FileSystemClient.remove_file(self.audio_path)
class ServerCameraRecorder(PostprocessableRecorder): def __init__(self): super().__init__() self.__fs_client = FileSystemClient() self.__command = FFMPEGcommand self.__process = None self.__logger = logging.getLogger( 'stepic_studio.video_recorders.camera_recorder') self.last_processed_path = None self.last_processed_file = None self._load_postprocessing_pipe(settings.SERVER_POSTPROCESSING_PIPE) def start_recording(self, path: str, filename: str) -> InternalOperationResult: if self.is_active(): self.__logger.error( 'Can\'t start FFMPEG for file %s: camera is acctually recording (process with PID %s)', os.path.join(path, filename), self.__process.pid) return InternalOperationResult(ExecutionStatus.FATAL_ERROR, 'Camera is actually recording') local_command = self.__command + os.path.join(path, filename) result, self.__process = self.__fs_client.execute_command( local_command, stdin=subprocess.PIPE) if result.status is ExecutionStatus.SUCCESS: self.__logger.info( 'Successfully start camera recording (FFMPEG PID: %s; FFMPEG command: %s)', self.__process.pid, local_command) self.last_processed_file = filename self.last_processed_path = path else: self.__logger.error( 'Camera recording start failed: %s; FFMPEG command: %s', result.message, local_command) return result def stop_recording(self) -> InternalOperationResult: if not self.is_active(): if self.__process is None: pid = None else: pid = self.__process.pid self.__process = None self.__logger.warning( 'Camera isn\'t active: can\'t stop non existing FFMPEG process ' '(try to stop process with PID %s)', pid) return InternalOperationResult( ExecutionStatus.SUCCESS, 'Camera isn\'t active: can\'t stop non existing FFMPEG process' ) result = self.__fs_client.send_quit_signal(self.__process) # try to kill ffmpeg process (sending of quit signal may have no effect) TaskManager().run_with_delay(FileSystemClient.kill_process, [self.__process.pid], delay=KILL_DELAY) if result.status is ExecutionStatus.SUCCESS: self.__logger.info( 'Successfully stop camera recording (FFMPEG PID: %s)', self.__process.pid) self.__process = None self._apply_pipe(self.last_processed_path, self.last_processed_file) return result else: self.__logger.error( 'Problems while stop camera recording (FFMPEG PID: %s) : %s', self.__process.pid, result.message) return result def is_active(self) -> bool: return self.__process is not None and self.__process.poll() is None
def __init__(self): self.fs_client = FileSystemClient() self.logger = logging.getLogger( 'stepic_studio.postprocessing.TSConverter')
class VideoSynchronizer(object): """Video synchronization via durations of silence at the start of videos""" def __init__(self): self.__fs_client = FileSystemClient() self.__logger = logging.getLogger( 'stepic_studio.postprocessing.VideSynchronizer') self.__max_diff = 2 # seconds self.__min_diff = 0.1 self.__camera_noise_tolerance = '-11.5dB' self.__tablet_noise_tolerance = '-12dB' self.__min_silence_duration = 0.001 def sync(self, screen_path, camera_path) -> InternalOperationResult: screen_path = os.path.splitext(screen_path)[0] + '.mp4' camera_path = os.path.splitext(camera_path)[0] + '.mp4' if not os.path.isfile(screen_path) or not os.path.isfile(camera_path): self.__logger.warning('Invalid paths to videos: (%s; %s)', screen_path, camera_path) return InternalOperationResult(ExecutionStatus.FATAL_ERROR) try: duration_1 = self.__get_silence_duration( screen_path, self.__tablet_noise_tolerance, self.__min_silence_duration) duration_2 = self.__get_silence_duration( camera_path, self.__camera_noise_tolerance, self.__min_silence_duration) except Exception: self.__logger.warning('Can\'t get silence duration of %s, %s.', screen_path, camera_path) return InternalOperationResult(ExecutionStatus.FATAL_ERROR) longer = '' try: silence_diff = self.__get_valid_silence_diff( duration_1, duration_2) if silence_diff == 0: self.__logger.info( 'Video synchronizing: no need to be synchronized, silence difference < %ssec.', self.__min_diff) return InternalOperationResult(ExecutionStatus.SUCCESS) if silence_diff > 0: longer = screen_path self.__add_empty_frames(camera_path, silence_diff) elif silence_diff < 0: longer = camera_path self.__add_empty_frames(screen_path, abs(silence_diff)) except Exception as e: self.__logger.warning('Invalide difference: %s', e) return InternalOperationResult(ExecutionStatus.FATAL_ERROR) self.__logger.info( 'Videos successfully synchronized (difference: %s sec.; longer silence in %s; ' 'silence duration of %s - %s sec.; silence duration of %s - %s sec.)', '%.3f' % silence_diff, longer, screen_path, duration_1, camera_path, duration_2) return InternalOperationResult(ExecutionStatus.SUCCESS) def __extract_audio(self, video_path): wo_extension = os.path.splitext(video_path)[0] audio_output = wo_extension + '.wav' extract_result = self.__fs_client.execute_command_sync([ settings.FFMPEG_PATH, '-y', '-i', video_path, '-vn', '-ac', '1', '-f', 'wav', audio_output ]) if extract_result.status is not ExecutionStatus.SUCCESS: raise Exception(extract_result.message) else: return audio_output def __get_silence_duration(self, audio_path, noise_level='-30dB', min_duration=0.1): command = settings.FFMPEG_PATH + ' ' + settings.SILENCE_DETECT_TEMPLATE.format( audio_path, noise_level, min_duration) status, output = self.__fs_client.exec_and_get_output( command, stderr=subprocess.STDOUT) if status.status is not ExecutionStatus.SUCCESS: raise Exception(status.message) info = self.__extract_silence_info(output) try: self.__info_validate(info) return info['silence_duration'] except Exception as e: raise e def __info_validate(self, info: dict): try: start_time = float(info['silence_start']) if start_time > 0: raise Exception('Start of audio doesn\'t contain silence') except Exception as e: raise e # extract info about first silent part def __extract_silence_info(self, raw_info) -> dict: result = [] for row in raw_info.decode('UTF-8').split('\n'): if 'silencedetect' in row: result.append(row) keys = [] digits = [] for elem in result: for s in elem.split(): if 'silence_' in s: keys.append(s[:-1]) # pass ':'******'Silence duration difference exceeds allowable value, difference: {}' .format(abs(val_1 - val_2))) if abs(val_1 - val_2) < self.__min_diff: return 0 return val_1 - val_2 def __add_empty_frames(self, path, duration): splitted_path = os.path.splitext(path) new_path = splitted_path[0] + SYNC_LABEL + splitted_path[1] duration = '%.3f' % duration command = settings.FFMPEG_PATH + ' ' + settings.VIDEO_OFFSET_TEMPLATE.format( duration, path, new_path) status = self.__fs_client.execute_command_sync(command) if status.status is not ExecutionStatus.SUCCESS: raise Exception(status.message)
def __init__(self): self.__postprocessor_pipe = [] self.__logger = logging.getLogger( 'stepic_studio.video_recorders.postprocessable_recorder') self.__fs_client = FileSystemClient()
def __init__(self): self._fs_client = FileSystemClient() self.__logger = logging.getLogger(__name__)
class RawCutter(object): def __init__(self): self._fs_client = FileSystemClient() self.__logger = logging.getLogger(__name__) def raw_cut(self, substep_id: int) -> InternalOperationResult: substep = SubStep.objects.get(pk=substep_id) if substep.automontage_exist: return InternalOperationResult(ExecutionStatus.SUCCESS) screen_full_path = substep.os_screencast_path prof_full_path = substep.os_path output_dir = substep.os_automontage_path status = self._fs_client.create_recursively(output_dir) if status.status is not ExecutionStatus.SUCCESS: return status full_output = os.path.join( output_dir, substep.name + RAW_CUT_LABEL + MP4_EXTENSION) substep.is_locked = True substep.save() internal_status = self._internal_raw_cut(screen_full_path, prof_full_path, full_output) substep = SubStep.objects.get(pk=substep_id) substep.is_locked = False substep.save() return internal_status def raw_cut_async(self, substep_id: int): try: thread = Thread(target=self.raw_cut, args=[substep_id]) thread.start() except Exception as e: self.__logger.warning('Can\'t launch raw_cut asynchronously: %s', e) def _internal_raw_cut(self, video_path1: str, video_path2: str, output_path: str) -> InternalOperationResult: status_1 = os.path.isfile(video_path1) status_2 = os.path.isfile(video_path2) if not status_1 or not status_2: self.__logger.warning('Can\'t process invalid videos: %s, %s', video_path1, video_path2) return InternalOperationResult(ExecutionStatus.FATAL_ERROR) command = settings.FFMPEG_PATH + ' ' + \ settings.RAW_CUT_TEMPLATE.format(video_path1, video_path2, output_path) status = self._fs_client.execute_command_sync(command) if status.status is not ExecutionStatus.SUCCESS: return status def raw_cut_step(self, step_id: int): try: substep_ids = SubStep.objects.filter( from_step=step_id).values_list('id', flat=True) self._internal_raw_cut_step(substep_ids) except Exception as e: self.__logger.warning('Can\'t launch raw_cut_step: %s', e) def raw_cut_step_async(self, step_id: int): try: substep_ids = SubStep.objects.filter( from_step=step_id).values_list('id', flat=True) thread = Thread(target=self._internal_raw_cut_step, args=[substep_ids]) thread.start() except Exception as e: self.__logger.warning( 'Can\'t launch raw_cut_step asynchronously: %s', e) def _internal_raw_cut_step(self, substep_ids): for ss_id in substep_ids: self.raw_cut(ss_id) def raw_cut_lesson_async(self, lesson_id: int): try: step_ids = Step.objects.filter(from_lesson=lesson_id).values_list( 'id', flat=True) thread = Thread(target=self._internal_raw_cut_lesson, args=[step_ids]) thread.start() except Exception as e: self.__logger.warning( 'Can\'t launch raw_cut_lesson asynchronously: %s', e) def _internal_raw_cut_lesson(self, step_ids): for step_id in step_ids: self.raw_cut_step(step_id) def _cancel(self): if self.__process is not None: pid = self.__process.pid if self._fs_client.is_process_exists(pid): self._fs_client.kill_process(pid) self.__process = None