def test_generate(self, mock_data, mock_call, mock_codec, mock_scalar, mock_passes, mock_destination, mock_logger): """ Tests `generate` method works correctly. """ ffcommand = ['dummy-ffcommand-arg'] job_id = mock_data.get('job_id', None) command_generate = CommandGenerate( VideoObject=mock_data.get('video_object', self.video), EncodeObject=mock_data.get('encode_object', self.encode), jobid=job_id) self.assertEqual(command_generate.ffcommand, []) self.assertIsNone(command_generate.workdir) command_generate.ffcommand = ffcommand result_command = command_generate.generate() if mock_data.get('error_message', ''): mock_logger.error.assert_called_with( mock_data.get('error_message')) else: self.assertIsNotNone(command_generate.workdir) expected_workdir = os.path.join( ENCODE_WORK_DIR, job_id) if job_id else ENCODE_WORK_DIR self.assertEqual(command_generate.workdir, expected_workdir) self.assertEqual(result_command, ' '.join(ffcommand)) if mock_data.get('ENFORCE_TARGET_ASPECT', False): self.assertTrue(mock_scalar.called) self.assertTrue(mock_call.called) self.assertTrue(mock_codec.called) self.assertTrue(mock_passes.called) self.assertTrue(mock_destination.called)
def setUp(self): """ Setup for command generate tests. """ self.video = Video(veda_id='XXXXXXXX2016-V00TEST') self.encode = Encode(video_object=self.video, profile_name=None) self.command_generate = CommandGenerate(VideoObject=self.video, EncodeObject=self.encode)
def _generate_encode(self): """ Generate the (shell) command / Encode Object """ encoding = Encode(video_object=self.VideoObject, profile_name=self.encode_profile) encoding.pull_data() if encoding.filetype is None: return self.ffcommand = CommandGenerate(VideoObject=self.VideoObject, EncodeObject=encoding, jobid=self.jobid, workdir=self.workdir, settings=self.settings).generate()
class CommandGenerateTest(unittest.TestCase): """ Test class for Command Generate. """ def setUp(self): """ Setup for command generate tests. """ self.video = Video(veda_id='XXXXXXXX2016-V00TEST') self.encode = Encode(video_object=self.video, profile_name=None) self.command_generate = CommandGenerate(VideoObject=self.video, EncodeObject=self.encode) @data( ({ 'video_object': None, 'error_message': 'Command generation: No Video object' }), ({ 'encode_object': None, 'error_message': 'Command generation: No Encode object' }), ({ 'job_id': 'dummy-job-id', 'ENFORCE_TARGET_ASPECT': True }), ) @patch('video_worker.generate_encode.logger') @patch.object(CommandGenerate, '_destination') @patch.object(CommandGenerate, '_passes') @patch.object(CommandGenerate, '_scalar') @patch.object(CommandGenerate, '_codec') @patch.object(CommandGenerate, '_call') def test_generate(self, mock_data, mock_call, mock_codec, mock_scalar, mock_passes, mock_destination, mock_logger): """ Tests `generate` method works correctly. """ ffcommand = ['dummy-ffcommand-arg'] job_id = mock_data.get('job_id', None) command_generate = CommandGenerate( VideoObject=mock_data.get('video_object', self.video), EncodeObject=mock_data.get('encode_object', self.encode), jobid=job_id) self.assertEqual(command_generate.ffcommand, []) self.assertIsNone(command_generate.workdir) command_generate.ffcommand = ffcommand result_command = command_generate.generate() if mock_data.get('error_message', ''): mock_logger.error.assert_called_with( mock_data.get('error_message')) else: self.assertIsNotNone(command_generate.workdir) expected_workdir = os.path.join( ENCODE_WORK_DIR, job_id) if job_id else ENCODE_WORK_DIR self.assertEqual(command_generate.workdir, expected_workdir) self.assertEqual(result_command, ' '.join(ffcommand)) if mock_data.get('ENFORCE_TARGET_ASPECT', False): self.assertTrue(mock_scalar.called) self.assertTrue(mock_call.called) self.assertTrue(mock_codec.called) self.assertTrue(mock_passes.called) self.assertTrue(mock_destination.called) @data(({ 'veda_id': 'dummy-veda-id', 'mezz_extension': 'mp4' }), ({ 'mezz_extension': 'mp4' }), ({ 'filetype': 'mp3' })) def test_call(self, mock_data): """ Test that `_call` method works correctly. """ veda_id = mock_data.get('veda_id', None) file_type = mock_data.get('filetype', None) mezz_extension = mock_data.get('mezz_extension', '') mezz_filepath = mock_data.get('mezz_filepath', 'dummy-mezz-path') self.command_generate.workdir = ENCODE_WORK_DIR self.command_generate.ffcommand = ['dummy-ffcommand-arg'] self.command_generate.settings.update( {'ffmpeg_compiled': '-ffmpeg_compiled'}) self.command_generate.VideoObject.veda_id = veda_id self.command_generate.VideoObject.mezz_extension = mezz_extension self.command_generate.VideoObject.mezz_filepath = mezz_filepath self.command_generate.EncodeObject.filetype = file_type self.command_generate._call() expected_ffcommand = [ 'dummy-ffcommand-arg', '-ffmpeg_compiled', '-hide_banner', '-y', '-i' ] expected_ffcommand.append( '{workdir}/{file_name}{file_extension}'.format( workdir=ENCODE_WORK_DIR, file_name=veda_id if veda_id else mezz_filepath, file_extension='.' + mezz_extension if mezz_extension else '')) if file_type != 'mp3': expected_ffcommand.append('-c:v') else: expected_ffcommand.append('-c:a') self.assertEqual(self.command_generate.ffcommand, expected_ffcommand) @data(({ 'filetype': 'mp3' }), ({ 'filetype': 'mp3', 'ffcommand': ['dummy-ffcommand-arg'], 'expected_ffcommand': ['dummy-ffcommand-arg', 'libmp3lame'] }), ({ 'filetype': 'mp4', 'ffcommand': ['dummy-ffcommand-arg'], 'expected_ffcommand': ['dummy-ffcommand-arg', 'libx264'] }), ({ 'filetype': 'webm', 'ffcommand': ['dummy-ffcommand-arg'], 'expected_ffcommand': ['dummy-ffcommand-arg', 'libvpx'] })) def test_codec(self, mock_data): """ Tests that `_codec` mothod works correctly. """ self.command_generate.ffcommand = mock_data.get('ffcommand', None) self.command_generate.EncodeObject.filetype = mock_data.get( 'filetype', None) self.command_generate._codec() self.assertEqual(self.command_generate.ffcommand, mock_data.get('expected_ffcommand', None)) @data( ({ 'ffcommand': None, 'expected_ffcommand': None }), ({ 'file_type': 'mp3', 'expected_ffcommand': [] }), ({ 'resolution': 480, 'mezz_bitrate': 'test mezz rate', 'mezz_resolution': '720x480', 'expected_ffcommand': [] }), ({ 'resolution': 480, 'mezz_resolution': '720x420', 'expected_ffcommand': ['-vf', 'scale=853:480'] }), # Add more coverage for _scalar method - See EDUCATOR-1071 ) def test_scalar(self, mock_data): """ Tests `_scalar` method works correctly. """ self.command_generate.ffcommand = mock_data.get('ffcommand', []) self.command_generate.EncodeObject.filetype = mock_data.get( 'file_type', 'mp4') self.command_generate.EncodeObject.resolution = mock_data.get( 'resolution', '480') self.command_generate.VideoObject.mezz_resolution = mock_data.get( 'mezz_resolution', 'Unparsed') self.command_generate.VideoObject.mezz_bitrate = mock_data.get( 'mezz_bitrate', 'Unparsed') self.command_generate._scalar() self.assertEqual(self.command_generate.ffcommand, mock_data.get('expected_ffcommand', [])) @data(('mp3', 100, 50, ['-b:a', '100k']), ('mp4', 100, 50, ['-crf', '100']), ('webm', 100, 50, [ '-b:v', '50k', '-minrate', '10k', '-maxrate', '62k', '-bufsize', '26k' ]), ('webm', 100, 200, [ '-b:v', '100k', '-minrate', '10k', '-maxrate', '125k', '-bufsize', '76k' ])) @unpack def test_passes(self, file_type, rate_factor, mezz_bitrate, expected_ffcommand): """ Tests that `_passes` works correctly. """ self.command_generate.EncodeObject.filetype = file_type self.command_generate.EncodeObject.rate_factor = rate_factor self.command_generate.VideoObject.mezz_bitrate = mezz_bitrate self.command_generate._passes() self.assertEqual(self.command_generate.ffcommand, expected_ffcommand) @data( { 'file_type': 'mp4', 'veda_id': 'dummy-veda-id', 'expected_ffcommand': ['-movflags', 'faststart'] }, { 'file_type': 'webm', 'veda_id': 'dummy-veda-id', 'expected_ffcommand': ['-c:a', 'libvorbis'] }, { 'file_type': 'mp4', 'veda_id': None, 'expected_ffcommand': ['-movflags', 'faststart'] }, ) @unpack def test_destination(self, file_type, veda_id, expected_ffcommand): """ Tests that `_destination` works correctly. """ encode_suffix = 'dummy-encode-suffix' mezz_file_path = 'dummy-mezz-path' self.command_generate.workdir = ENCODE_WORK_DIR self.command_generate.EncodeObject.encode_suffix = encode_suffix self.command_generate.EncodeObject.filetype = file_type self.command_generate.VideoObject.veda_id = veda_id self.command_generate.VideoObject.mezz_filepath = mezz_file_path # Add a correct expected ffcommand path expected_ffcommand.append( '{workdir}/{veda_id}_{encode_suffix}.{file_type}'.format( workdir=ENCODE_WORK_DIR, veda_id=veda_id if veda_id else mezz_file_path, encode_suffix=encode_suffix, file_type=file_type)) self.command_generate._destination() self.assertEqual(self.command_generate.ffcommand, expected_ffcommand)
class VideoWorker(object): def __init__(self, **kwargs): self.settings = None self.veda_id = kwargs.get('veda_id', None) self.setup = kwargs.get('setup', False) self.jobid = kwargs.get('jobid', None) self.update_val_status = kwargs.get('update_val_status') self.encode_profile = kwargs.get('encode_profile', None) self.VideoObject = kwargs.get('VideoObject', None) self.instance_yaml = kwargs.get( 'instance_yaml', os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'instance_config.yaml')) self.workdir = kwargs.get('workdir', self.determine_workdir()) self.ffcommand = None self.source_file = kwargs.get('source_file', None) self.output_file = None self.endpoint_url = None # Pipeline Steps self.encoded = False self.delivered = False def determine_workdir(self): if not os.path.exists(ENCODE_WORK_DIR): os.mkdir(ENCODE_WORK_DIR) if self.jobid is None: return ENCODE_WORK_DIR else: return os.path.join(ENCODE_WORK_DIR, self.jobid) def run(self): self.settings = get_config() if self.encode_profile is None: logger.error('No Encode Profile Specified') return self.VideoObject = Video(veda_id=self.veda_id, ) if self.source_file is not None: self.VideoObject.mezz_filepath = os.path.join( self.workdir, self.source_file) self.VideoObject.activate() if not self.VideoObject.valid: logger.error('{id} : Invalid Video Data'.format( id=self.VideoObject.veda_id)) return if not os.path.exists(self.workdir): os.mkdir(self.workdir) logger.info('{id} | {encoding} : Ready for Encode'.format( id=self.VideoObject.veda_id, encoding=self.encode_profile)) # Pipeline Steps : # I. Intake # Ib. Validate Mezz # II. change status in APIs # III. Generate Encode Command # IV. Execute Encodes # IVa. Validate Products # (*)V. Deliver Encodes (sftp and others?), retrieve URLs # (*)VI. Change Status in APIs, add URLs # VII. Clean Directory self._engine_intake() if not self.VideoObject.valid: logger.error('Invalid Video / Local') return if self.VideoObject.val_id is not None: self._update_api() # generate video images command and update S3 and edxval # run against 'hls' encode only if self.encode_profile == 'hls': # Run HLS encode self._hls_pipeline() # Auto-video Images VideoImages(video_object=self.VideoObject, work_dir=self.workdir, source_file=self.source_file, jobid=self.jobid, settings=self.settings).create_and_update() else: self._static_pipeline() logger.info('{id} | {encoding} : Encode Complete'.format( id=self.VideoObject.veda_id, encoding=self.encode_profile)) if self.endpoint_url is not None and self.VideoObject.veda_id is not None: # Integrate with main veda_id = self.veda_id encode_profile = self.encode_profile deliverable_route.apply_async( (veda_id, encode_profile), queue=self.settings['celery_deliver_queue']) logger.info( '{id} | {encoding} : encoded file queued for delivery'.format( id=self.VideoObject.veda_id, encoding=self.encode_profile)) # Clean up workdir if self.jobid is not None: shutil.rmtree(self.workdir) def _static_pipeline(self): self._generate_encode() if self.ffcommand is None: return logger.info('ffcommand is written as %s', self.ffcommand) self._execute_encode() if self.encode_profile == 'audio_mp3': self.encoded = True else: self._validate_encode() if self.encoded and self.VideoObject.veda_id is not None: self._deliver_file() def _hls_pipeline(self): """ Activate HLS, use hls lib to upload """ if not os.path.exists(os.path.join(self.workdir, self.source_file)): logger.error( ': {id} | {encoding} Local raw video file not found'.format( id=self.VideoObject.veda_id, encoding=self.encode_profile)) return os.chdir(self.workdir) if self.settings['onsite_worker'] is True: hls_chunk_instance = Chunkey( mezz_file=os.path.join(self.workdir, self.source_file), DELIVER_BUCKET=self.settings['edx_s3_endpoint_bucket'], clean=False, ACCESS_KEY_ID=self.settings['edx_access_key_id'], SECRET_ACCESS_KEY=self.settings['edx_secret_access_key']) else: hls_chunk_instance = Chunkey( mezz_file=os.path.join(self.workdir, self.source_file), DELIVER_BUCKET=self.settings['edx_s3_endpoint_bucket'], clean=False, ) if hls_chunk_instance.complete: self.endpoint_url = hls_chunk_instance.manifest_url def _engine_intake(self): """ Copy file down from AWS S3 storage bucket """ if not self.VideoObject.valid: logger.error(': {id} Invalid Video'.format( id=self.VideoObject.veda_id, )) return if self.source_file is None: if self.settings['onsite_worker'] is True: conn = S3Connection(self.settings['veda_access_key_id'], self.settings['veda_secret_access_key']) else: conn = S3Connection() try: bucket = conn.get_bucket( self.settings['veda_s3_hotstore_bucket']) except S3ResponseError: logger.error('Invalid hotstore S3 bucket') return if self.VideoObject.mezz_extension is not None and len( self.VideoObject.mezz_extension) > 0: self.source_file = '.'.join((self.VideoObject.veda_id, self.VideoObject.mezz_extension)) else: self.source_file = self.VideoObject.veda_id source_key = bucket.get_key(self.source_file) if source_key is None: logger.error(': {id} S3 Intake object not found'.format( id=self.VideoObject.val_id)) return source_key.get_contents_to_filename( os.path.join(self.workdir, self.source_file)) if not os.path.exists(os.path.join(self.workdir, self.source_file)): logger.error(': {id} engine intake download error'.format( id=self.VideoObject.val_id)) return self.VideoObject.valid = ValidateVideo( filepath=os.path.join(self.workdir, self.source_file)).valid def _update_api(self): UpdateAPIStatus( val_video_status=VAL_TRANSCODE_STATUS, veda_video_status=NODE_TRANSCODE_STATUS, send_val=self.update_val_status, VideoObject=self.VideoObject, ).run() def _generate_encode(self): """ Generate the (shell) command / Encode Object """ encoding = Encode(video_object=self.VideoObject, profile_name=self.encode_profile) encoding.pull_data() if encoding.filetype is None: return self.ffcommand = CommandGenerate(VideoObject=self.VideoObject, EncodeObject=encoding, jobid=self.jobid, workdir=self.workdir, settings=self.settings).generate() def _execute_encode(self): """ if this is just a filepath, this should just work --no need to move the source-- """ if not os.path.exists(os.path.join(self.workdir, self.source_file)): logger.error(': {id} Encode input file not found'.format( id=self.VideoObject.veda_id)) return process = subprocess.Popen(self.ffcommand, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, universal_newlines=True) Output.status_bar(process=process) self.output_file = self.ffcommand.split('/')[-1] if not os.path.exists(os.path.join(self.workdir, self.output_file)): logger.error(': {id} Encode output file not found'.format( id=self.VideoObject.veda_id)) def _validate_encode(self): """ Validate encode by matching (w/in 5 sec) encode duration, as well as standard validation tests """ if self.output_file is None: self.encoded = False return else: self.encoded = ValidateVideo(filepath=os.path.join( self.workdir, self.output_file), product_file=True, VideoObject=self.VideoObject).valid def _deliver_file(self): """ Deliver Here """ if not os.path.exists(os.path.join(self.workdir, self.output_file)): return D1 = Deliverable(VideoObject=self.VideoObject, encode_profile=self.encode_profile, output_file=self.output_file, jobid=self.jobid, workdir=self.workdir) D1.run() self.delivered = D1.delivered self.endpoint_url = D1.endpoint_url