def __cut_audio__(session_id, edit_info): """ Given a rumba session and the information to mount a video, it cuts the session audio to fit into the mounted video. For this, it takes into account the timestamp of the first frame of the video to mount and the timestamp of the last frame of this new video. VERY IMPORTANT: This method assumes that the timestamps of video and audio are consistent in terms of synchronization. :param session_id: Id of the rumba session. :param edit_info: The informtion provided by the editor containing the list of video slices. :return: Absolute path where the audio cut by this method is located. """ session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with such id.") audio_path = "{}/audio.wav".format(session['folder_url']) video_init_ts = VideoEditorHelper.get_first_video_ts(edit_info=edit_info) audio_init_ts = AudioManager.get_instance().get_audio_init_ts(session_id=session_id) audio_init_offset = VideoEditorHelper.calculate_audio_init_offset(audio_init_ts=audio_init_ts, video_init_ts=video_init_ts) ffmpeg_audio_init_offset = DataTransformer.transform_seconds_to_ffmpeg_offset(float(audio_init_offset)) audio_init_ts = AudioManager.get_instance().get_audio_init_ts(session_id=session_id) audio_end_offset = VideoEditorHelper.calculate_audio_end_offset(audio_init_ts=audio_init_ts, edit_info=edit_info, audio_init_offset=audio_init_offset) audio_output = "{}/audio-{}.wav".format(session['folder_url'], uuid.uuid4().hex) audio_thread = AudioSplitterThread(inputFile=audio_path, outputFile=audio_output, initial_offset=ffmpeg_audio_init_offset, end_offset=audio_end_offset) audio_thread.start() audio_thread.join() if audio_thread.code != 0: raise Exception("FFMpeg command failed.") return audio_output
def add_video(self, session_id, user_id): """ :param session_id: :param user_id: :return: """ LOGGER.info("Adding video to session.") session = SessionManager.get_instance().get_session( session_id=session_id) if session['state'] != SessionStatus.ACTIVE.value: raise IllegalSessionStateException( "Can only add videos to active sessions.") user_videos = Video.objects(session=session['id'], user_id=user_id).count() video_name = "video{}".format(user_videos) session = RumbaSession.objects(id=session_id).first() video_path = FileSystemService.get_instance().create_video_directory( session_folder=session['folder_url'], user_id=user_id, video_name=video_name) video = Video(session=session['id'], user_id=user_id, name=video_name, video_path=video_path).save() LOGGER.info("Video successfully added: [id={}]".format(video)) return str(video['id'])
def __prepare_video_edition__(self, session_id, edit_info, edition_id): """ Given the information provided by the editor to mount a video, this method builds the text file that will serve as input to mount the video. :param session_id: Id of the rumba session. :param edit_info: The informtion provided by the editor containing the list of video slices. :return: Absolute path to the file that can be used with ffmpeg to mount the video. :rtype: str """ try: LOGGER.debug("Preparing video edition..") session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("This session does not exist.") video_slices, video_id = VideoEditorHelper.create_videoslices_info( edit_info) edit_info_filename = VideoEditorHelper.save_edit_info_to_file( session_path=session["folder_url"], video_slices=video_slices, edition_id=edition_id) output_file = edit_info_filename.split(".")[0] + ".mp4" thread = VideoEditorThread(edition_info_file=edit_info_filename, output_file=output_file, edit_info=edit_info, edition_id=edition_id) thread.start() return video_id except Exception as ex: LOGGER.exception("Error preparing video edition - ") raise ex
def stop_session(self, session_id): """ Stops an active session. Stopping a session means that no more users would be able to stream video to the server. Of course, only active sessions could be stopped. :param session_id: Id of the session to stop. :raises: - ValueError, if the specified id is not valid. - IllegalSessionStateException: If the session was not active. """ LOGGER.info("Stopping session: [id={}]".format(session_id)) GenericValidator.validate_id(session_id) session = self.get_session(session_id) valid_states = [ SessionStatus.CREATED.value, SessionStatus.ACTIVE.value ] if session['state'] not in valid_states: raise IllegalSessionStateException( "Only active and initialized sessions can be stopped.") db_session = RumbaSession.objects(id=session_id).first() if session['state'] == SessionStatus.ACTIVE.value: LOGGER.info("Stopping audio recording.") AudioManager.get_instance().stop_audio() db_session.update(set__state=SessionStatus.FINISHED.value) LOGGER.info("Session successfully stopped: [id={}]".format(session_id))
def __merge_audio_and_video__(self, session_id, video_path, edit_info, edition_id): """ Merges the rumba session audio with the video identified by the given id. :param session_id: Id of the rumba session. :param video_path: Absolute path in the server operanting system of the video. :param edit_info: The informtion provided by the editor containing the list of video slices. :param editionid: The id that is being used to identify this edition process. :return: Absolute path where the video merged by this method is located. :rtype: str """ LOGGER.info("Merging session audio with video.") session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with such id.") audio_file = self.__cut_audio__(session_id, edit_info) output_file = "{}/video-{}.mp4".format(session['folder_url'], edition_id) mixer = AudioVideoMixerThread(audio_file=audio_file, video_file=video_path, output_file=output_file) mixer.start() mixer.join() if mixer.code != 0: raise Exception("Error merging video and audio.") LOGGER.info( "Video and session audio merged. [path={}]".format(output_file)) return output_file
def initialize_session(self, session_id): """ Initialize a created session. A session can only be initialized from the "Created" state. When a session is initialized, it starts to record the audio and its state is modified to "Active" state. :param session_id: Session to be created :raises: - IllegalSessionStateException, if the session is no in the "Created" state. - NotExistingResource, if there's no session with such id. """ LOGGER.info("Initializing session. [session_id={}]".format(session_id)) GenericValidator.validate_id(session_id) session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with such id.") if session['state'] != SessionStatus.CREATED.value: raise IllegalSessionStateException( "Session can only be initialized from 'Created' state") LOGGER.debug("Initializing audio record.") initial_timestmap = AudioManager.get_instance().record_audio( session['folder_url']) try: LOGGER.debug("Updating session state.") session.update(set__state=SessionStatus.ACTIVE.value, set__audio_timestamp=str(initial_timestmap)) except Exception as ex: LOGGER.exception( "Could not update session state. Stopping audio recording...") AudioManager.get_instance().stop_audio() raise ex
def get_audio_init_ts(session_id): """ :return: """ GenericValidator.validate_id(session_id) session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("No session with such id.") ts = session['audio_timestamp'] return ts
def delete_session(self, session_id): """ Removes a Rumba session. This method deletes from the database. Sessions can only be remoced if they have been stopped. In order to remove a live session, it is mandatory to remove it first. :param session_id: Id of the session to remove. :raises: - IllegalSessionStateException, if the session does not exist. - IlegalSessionStateException, if the session is active. - ValueError, if the provided id is not a valid id. """ LOGGER.info("Removing session: [id={}]".format(session_id)) GenericValidator.validate_id(session_id) session = self.get_session(session_id) if session['state'] == SessionStatus.ACTIVE.value: raise IllegalSessionStateException( "Session is active: it should be stopped first.") FileSystemService.get_instance().delete_session_directory( band=session['band']) RumbaSession.objects(id=session_id).delete() LOGGER.info("Session successfully removed: [id={}]".format(session_id))
def edit_video(self, session_id, edit_info): """ This video is the main entry point to this class in order to mount a video. The editor would provide the information containing which slices of videos he/she would like for mounting a video about the session. A slice of video is composed of: - The id of the video. - The number of thumb, relative to the initial timestamp of the video. Each thumb represents one second of the video. - The position it would have in the mounted video. Example of slice: { "id":"5ad4b5fdc94b4c6bc260dd3c", "thumb":0, "position":0 } The provided parameter should be a list of these slices. IMPORTANT: A rumba session could only be edited if the session is no longer active. :param session_id: Id of the rumba session. :param edit_info: List of video slices, as described before. :raises: - NotExistingResource, if there's no session with such id, or there's any id that does not belong to an existing video. - IllegalResourceState, if the rumba session is still active. :return: The aboslute path to the mounted video. """ self.__validate_video_edition_input__(session_id, edit_info) session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with sch id.") if session['state'] != SessionStatus.FINISHED.value: raise IllegalResourceState("Only finished sessions can be edited.") edition_id = self.__generate_random_uuid__() edit_info_filename = self.__prepare_video_edition__( session_id=session_id, edit_info=edit_info, edition_id=edition_id) # video_path = self.__create_video__(edit_info_filename) # final_video_path = self.__merge_audio_and_video__(session_id=session_id, video_path=video_path, # edit_info=edit_info, edition_id=edition_id) return edition_id
def list_sessions(self): """ Retrieves and returns all managed sessions. This method querys mongo database in order to retrieve all the sessions. Each session is transformed into a d ictionary and returned as an element of the list. :return: List of managed sessions. Each position of the list is a dictionary representing a session. """ LOGGER.info("Retrieving all sessions.") session_list = [] sessions = RumbaSession.objects() for session in sessions: view_sess = MongoHelper.to_dict(session) view_sess.pop('folder_url') session_list.append(view_sess) LOGGER.info("Sessions sucessfully retrived: [lenght={}]".format( len(session_list))) return session_list
def cut_audio_for_user_video(session_id, video_id, video_init_ts, video_length): """ :param session_id: :param video_id: :return: """ GenericValidator.validate_id(session_id) GenericValidator.validate_id(video_id) session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with such id.") video = Video.objects(id=video_id).first() if video is None: raise NotExistingResource("There's no video with such id.") audio_path = "{}/audio.wav".format(session['folder_url']) audio_output = "{}/audio-{}.wav".format(session['folder_url'], uuid.uuid4().hex) audio_init_ts = AudioManager.get_instance().get_audio_init_ts( session_id=session_id) audio_init_offset = VideoEditorHelper.calculate_audio_init_offset( audio_init_ts=audio_init_ts, video_init_ts=video_init_ts) ffmpeg_audio_init_offset = DataTransformer.transform_seconds_to_ffmpeg_offset( float(audio_init_offset)) audio_end_offset = 12 print("Video_id: {}".format(video_id)) print("Video init ts: {}".format(video_init_ts)) print("Audio init ts: {}".format(audio_init_ts)) print("Audio init offset: {}".format(audio_init_offset)) audio_thread = AudioSplitterThread( inputFile=audio_path, outputFile=audio_output, initial_offset=ffmpeg_audio_init_offset, end_offset=video_length) audio_thread.start() audio_thread.join() if audio_thread.code != 0: raise Exception("FFMpeg command failed.") return audio_output
def get_session(self, session_id): """ Retrieves and returns the session identified with the given id. This method check if there's a session in the database with the provided id. If so, the information of the session is returned in a dictionary. If not, the method raises a IllegalSessionStateException. :return: Dictionary containing the information of the session identified with the id given as parameter. :raises: - NotExistingResource, if there's no session with such id. - ValueError, if the provided id is not a valid id. """ LOGGER.info("Retrieveing session: [id={}].".format(session_id)) GenericValidator.validate_id(session_id) session = RumbaSession.objects(id=session_id).first() if session is None: raise NotExistingResource("There's no session with such id.") LOGGER.info( "Session successfully retrieved: [id={}]".format(session_id)) LOGGER.debug("Session information: {}".format(session)) view_sess = MongoHelper.to_dict(session) view_sess.pop('folder_url') return view_sess
def create_session(self, session_info): """ Creates a new Rumba session. A session is composed by information about the event, such as the concert name, the band and the concert's date. A session can only be created if there's no other active or created session. If the session could be successfully created, a directory will be created in the FS for storing all the files associated to this new RUMBA session. :param session_info: Dictionary containing the information of the session. Mandatory fields are: - concert: String containing the name of the concert. - band: String containing the name of the band. - date: Date of the concert in timestamp format. - is_public: Boolean indicating if the concert is public or not. :return: String containing the id of the created session. :rtype: str :raises: SessionValidationException, if there's any error with the information provided as parameter. """ LOGGER.info("Creating new session.") LOGGER.debug("Validating user input: {}".format(session_info)) try: # First we validate user input SessionValidator.validate_new_session(session_info) LOGGER.info("Checking if there's any active sessions..") created_sessions = RumbaSession.objects( state=SessionStatus.CREATED.value).count() active_sessions = RumbaSession.objects( state=SessionStatus.ACTIVE.value).count() if active_sessions > 0 or created_sessions > 0: LOGGER.error( "Error creating session: There's already an active session." ) raise SessionValidationException( "There's already an active session.") # Secondly we create the working directory. dir_path = FileSystemService.get_instance( ).create_session_directory(band=session_info['band']) except Exception as ex: LOGGER.exception("Error creating session - ") raise ex try: # Store the information int the DB. session = RumbaSession(concert=session_info['concert'], band=session_info['band'], date=session_info['date'], folder_url=dir_path, location=session_info['location']).save() session.update(set__edition_url="{}editor-nice/{}".format( SERVER_URL, str(session['id']))) session.update(set__record_url="{}camera-back".format(SERVER_URL)) session.update( set__master_url="{}master-camera".format(SERVER_URL)) LOGGER.info( "Session successfully created: [id={0}, band={1}]".format( str(session['id']), session['band'])) return str(session['id']) except Exception as ex: LOGGER.exception( "Error storing session in DB - Executing rollback...") FileSystemService.get_instance().delete_session_directory( band=session_info['band']) raise ex