Source code for media_nommer.ec2nommerd.nommers.ffmpeg

"""
Contains a class used for nomming media on EC2 via FFmpeg_. This is used in
conjunction with the ec2nommerd Twisted_ plugin.
"""
import os
import tempfile
import subprocess
from media_nommer.utils import logger
from media_nommer.ec2nommerd.nommers.base_nommer import BaseNommer
from media_nommer.conf import settings

[docs]class FFmpegNommer(BaseNommer): """ This :ref:`Nommer <nommers>` is used to encode media with the excellent FFmpeg_ utility. **Example API request** Below is an example API request. The ``job_options`` dict that is passed to the :doc:`feederd` JSON API is the important part that is specific to this nommer.:: { 'source_path': 'some_video.mp4', 'dest_path': 'some_video_hqual.mp4', 'notify_url': 'http://somewhere.com:8000/job_state_pingback', 'job_options': { 'nommer': 'media_nommer.ec2nommerd.nommers.ffmpeg.FFmpegNommer', # This options key has ffmpeg command line arguments for a 2-pass # encoding. If you're doing single pass, you'd only have one # dict in this list. 'options': [ # First pass command specification. { # Just documenting this here so its existence is known. 'infile_options': [], # Fed to ffmpeg as command line flags. A None key means # that just the flag name is provided with no arg. 'outfile_options': [ ('threads', 0), ('vcodec', 'libx264'), ('preset', 'medium'), ('profile', 'baseline'), ('b', '400k'), ('vf', 'yadif,scale=640:-1'), # This denotes the first pass in ffmpeg. ('pass', '1'), ('f', 'mp4'), ('an', None), ], }, # Second pass command specification. { 'outfile_options': [ ('threads', 0), ('vcodec', 'libx264'), ('preset', 'medium'), ('profile', 'baseline'), ('b', '400k'), ('vf', 'yadif,scale=640:-1'), # Notice that this is now 2, for the second pass. ('pass', '2'), ('acodec', 'libfaac'), ('ab', '128k'), ('ar', '48000'), ('ac', '2'), ('f', 'mp4'), ], }, ], # end options list, max of 2 passes. }, # end job_options } To show how this would be put together, here is the command that would be ran for the first pass:: ffmpeg -y -i some_video.mp4 -threads 0 -vcodec libx264 -preset medium -profile baseline -b 400k -vf yadif,scale=640:-1 -pass 1 -f mp4 -an /dev/null Note that the ``an`` key in the ``outfile_options`` list of the first pass above has a ``None`` value. You'll need to do this for flags or options that don't require a value. """ def _onomnom(self): """ Best thought of as a ``main()`` method for the Nommer. This is the main bit of logic that directs the encoding process. """ logger.info("Starting to encode job %s" % self.job.unique_id) fobj = self.download_source_file() # Encode the file. The return value is a tempfile with the output. self.wrapped_set_job_state('ENCODING') out_fobj = self.__run_ffmpeg(fobj) if not out_fobj: # Failure! We're going nowhere. fobj.close() return False # Upload the encoding output file to its final destination. self.upload_to_destination(out_fobj) self.wrapped_set_job_state('FINISHED') logger.info("FFmpegNommer: Job %s has been successfully encoded." % self.job.unique_id) # Clean these up explicitly, just in case. fobj.close() out_fobj.close() return True def __append_inout_opts_to_cmd_list(self, option_dict, cmd_list): """ Takes user or preset options and adds them as arguments to the command list that will be ran with ``Popen()``. :param dict option_dict: The options to add to the command list. :param list cmd_list: The list being formed to pass to ``Popen()`` in :py:meth:`__run_ffmpeg`. """ for key, val in option_dict: cmd_list.append('-%s' % key) if val or val == 0: # None values are not used. cmd_list.append(str(val)) def __assemble_ffmpeg_cmd_list(self, encoding_pass_options, infile_obj, outfile_obj, is_two_pass=False, is_second_pass=False): """ Assembles a command list that subprocess.Popen() will use within self.__run_ffmpeg() to run ffmpeg. :param file infile_obj: A file-like object for input. :param file outfile_obj: A file-like object to store the output. :rtype: list :returns: A list to be passed to subprocess.Popen(). """ #ffmpeg [[infile options][-i infile]]... {[outfile options] outfile}... ffmpeg_cmd = ['ffmpeg', '-y'] # Form the ffmpeg infile and outfile options from the options # stored in the SimpleDB domain. if encoding_pass_options.has_key('infile_options'): infile_opts = encoding_pass_options['infile_options'] self.__append_inout_opts_to_cmd_list(infile_opts, ffmpeg_cmd) # Specify infile ffmpeg_cmd += ['-i', infile_obj.name] if encoding_pass_options.has_key('outfile_options'): outfile_opts = encoding_pass_options['outfile_options'] self.__append_inout_opts_to_cmd_list(outfile_opts, ffmpeg_cmd) if is_two_pass and not is_second_pass: # First pass of a 2-pass encoding. ffmpeg_cmd.append('/dev/null') else: # Second pass of a 2-pass encoding, or one-pass. ffmpeg_cmd.append(outfile_obj.name) logger.debug( "FFmpegNommer.__run_ffmpeg(): Command to run: %s" % ' '.join( ffmpeg_cmd ) ) return ffmpeg_cmd def __assemble_qtfaststart_cmd_list(self, outfile_obj): """ Assembles a command list that subprocess.Popen() will use within self.__run_ffmpeg() to run qtfaststart. :param file outfile_obj: A file-like object to store the output. :rtype: list :returns: A list to be passed to subprocess.Popen(). """ qtf_cmd = [ settings.NOMMERD_QTFASTSTART_BIN_PATH, outfile_obj.name ] logger.debug( "FFmpegNommer.__run_ffmpeg(): Command to run: %s" % ' '.join( qtf_cmd ) ) return qtf_cmd def __run_ffmpeg(self, fobj): """ Fire up ffmpeg and toss the results into a temporary file. :rtype: file-like object or ``None`` :returns: If the encoding succeeds, a file-like object is returned. If an error happens, ``None`` is returned, and the ERROR job state is set. """ is_two_pass = len(self.job.job_options) > 1 out_fobj = tempfile.NamedTemporaryFile(mode='w+b', delete=True) pass_counter = 1 for encoding_pass_options in self.job.job_options: is_second_pass = pass_counter == 2 # Based on the given options, assemble the command list to # pass on to Popen. ffmpeg_cmd = self.__assemble_ffmpeg_cmd_list( encoding_pass_options, fobj, out_fobj, is_two_pass=is_two_pass, is_second_pass=is_second_pass) # Do this for ffmpeg's sake. Allows more than one concurrent # encoding job per EC2 instance. os.chdir(self.temp_cwd) # Fire up ffmpeg. process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Get back to home dir. Not sure this is necessary, but meh. os.chdir(os.path.expanduser('~')) # Block here while waiting for output cmd_output = process.communicate() # 0 is success, so anything but that is bad. error_happened = process.returncode != 0 if error_happened: # Error found, return nothing so the nommer can die. logger.error( message_or_obj="Error encountered while running ffmpeg.") logger.error(message_or_obj=cmd_output[0]) logger.error(message_or_obj=cmd_output[1]) self.wrapped_set_job_state('ERROR', details=cmd_output[1]) return None move_atom = encoding_pass_options.get('move_atom_to_front', False) if move_atom: qtfaststart_cmd = self.__assemble_qtfaststart_cmd_list(out_fobj) process = subprocess.Popen(qtfaststart_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd_output = process.communicate() error_happened = process.returncode != 0 if error_happened: # Error found, return nothing so the nommer can die. logger.error( message_or_obj="Error encountered while running qtfaststart.") logger.error(message_or_obj=cmd_output[0]) logger.error(message_or_obj=cmd_output[1]) self.wrapped_set_job_state('ERROR', details=cmd_output[1]) return None if is_second_pass or not is_two_pass: # No errors, return the file object for uploading. out_fobj.seek(0) return out_fobj pass_counter += 1