예제 #1
0
 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)
예제 #2
0
    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
예제 #3
0
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)
예제 #4
0
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
예제 #5
0
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)
예제 #6
0
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)
예제 #7
0
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)
예제 #8
0
    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
예제 #9
0
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
예제 #10
0
def delete_lesson_on_disk(lesson) -> InternalOperationResult:
    client = FileSystemClient()
    client.delete_recursively(lesson.os_automontage_path)
    return client.delete_recursively(lesson.os_path)
예제 #11
0
 def __exit__(self, *exc_info):
     self.wave_form.close()
     FileSystemClient.remove_file(self.audio_path)
예제 #12
0
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
예제 #13
0
 def __init__(self):
     self.fs_client = FileSystemClient()
     self.logger = logging.getLogger(
         'stepic_studio.postprocessing.TSConverter')
예제 #14
0
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()
예제 #16
0
 def __init__(self):
     self._fs_client = FileSystemClient()
     self.__logger = logging.getLogger(__name__)
예제 #17
0
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