Esempio n. 1
0
    def strip(self, file):
        try:
            probe_data = _probe(file)
        except CalledProcessError:
            raise UnsupportedFormatError('Unsupported file format.')

        decoder_and_stream_type = _get_decoder_and_stream_type(probe_data)
        mime_type = self.__decoder_and_stream_type_to_mime_type.get(decoder_and_stream_type)
        if not mime_type:
            raise UnsupportedFormatError('Unsupported metadata source.')

        # Strip metadata
        result = io.BytesIO()
        with _FFmpegContext(file, result) as ctx:
            encoder_name = self.__mime_type_to_encoder[mime_type]
            command = ['ffmpeg', '-loglevel', 'error',
                       '-i', ctx.input_path,
                       '-map_metadata', '-1', '-codec', 'copy',
                       '-y', '-f', encoder_name, ctx.output_path]
            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not strip metadata: %s' % error_message)

        return result
Esempio n. 2
0
def mp4_video_asset(tmpdir_factory):
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
        subtitle_path='tests/resources/subtitle.vtt',
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-f webvtt -i %(subtitle_path)s '
        '-strict -2 -c:v h264 -preset ultrafast -qp 0 -c:a aac -c:s mov_text '
        '-f mp4' % ffmpeg_params).split()
    tmpfile = tmpdir_factory.mktemp('mp4_video_asset').join(
        'h264-aac-mov_text.mp4')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='video/quicktime',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION,
                            video=dict(codec='h264', depth=8),
                            audio=dict(codec='aac'),
                            subtitle=dict(codec='mov_text'))
Esempio n. 3
0
    def rotate(self, asset, angle, expand=False):
        """
        Creates an asset whose essence is rotated by the specified angle in
        degrees.

        :param asset: Asset whose contents will be rotated
        :type asset: Asset
        :param angle: Angle in degrees, counter clockwise
        :type angle: float
        :param expand: If true, changes the dimensions of the new asset so it
            can hold the entire rotated essence, otherwise the dimensions of
            the original asset will be used.
        :type expand: bool
        :return: New asset with rotated essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if angle % 360.0 == 0.0:
            return asset

        angle_rad = radians(angle)
        width = asset.width
        height = asset.height

        if expand:
            if angle % 180 < 90:
                width_ = asset.width
                height_ = asset.height
                angle_rad_ = angle_rad % pi
            else:
                width_ = asset.height
                height_ = asset.width
                angle_rad_ = angle_rad % pi - pi/2
            cos_a = cos(angle_rad_)
            sin_a = sin(angle_rad_)
            width = ceil(round(width_ * cos_a + height_ * sin_a, 7))
            height = ceil(round(width_ * sin_a + height_ * cos_a, 7))

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f:v', 'rotate=a=%(a)f:ow=%(w)d:oh=%(h)d)' % dict(a=angle_rad, w=width, h=height),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not rotate asset: %s' % error_message)

        metadata = _combine_metadata(asset,
                                     'mime_type', 'duration', 'video', 'audio', 'subtitle',
                                     width=width, height=height)

        return Asset(essence=result, **metadata)
Esempio n. 4
0
def mkv_video_asset(tmpdir_factory):
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
        subtitle_path='tests/resources/subtitle.vtt',
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-f webvtt -i %(subtitle_path)s '
        '-c:v vp9 -c:a libopus -c:s webvtt '
        '-f matroska' % ffmpeg_params).split()
    tmpfile = tmpdir_factory.mktemp('mkv_video_asset').join(
        'vp9-opus-webvtt.mkv')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='video/x-matroska',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION,
                            video=dict(codec='vp9', depth=8),
                            audio=dict(codec='libopus'),
                            subtitle=dict(codec='webvtt'))
Esempio n. 5
0
def ogg_video_asset(tmpdir_factory):
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-strict -2 -c:v theora -c:a vorbis -ac 2 -sn '
        '-f ogg' % ffmpeg_params).split()
    tmpfile = tmpdir_factory.mktemp('ogg_video_asset').join(
        'theora-vorbis.ogg')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='video/ogg',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION,
                            video=dict(codec='theora', depth=8),
                            audio=dict(codec='vorbis'))
Esempio n. 6
0
def mp2_video_asset(tmpdir_factory):
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
        subtitle_path='tests/resources/subtitle.vtt',
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-f webvtt -i %(subtitle_path)s '
        '-c:v mpeg2video -c:a mp2 -c:s dvbsub '
        '-f mpegts' % ffmpeg_params).split()
    tmpfile = tmpdir_factory.mktemp('mp2_video_asset').join(
        'mpeg2-mp2-dvbsub.ts')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='video/mp2t',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION)
Esempio n. 7
0
    def strip(self, file):
        try:
            probe_data = _probe(file)
        except CalledProcessError:
            raise UnsupportedFormatError('Unsupported file format.')

        decoder_and_stream_type = _get_decoder_and_stream_type(probe_data)
        mime_type = self.__decoder_and_stream_type_to_mime_type.get(decoder_and_stream_type)
        if not mime_type:
            raise UnsupportedFormatError('Unsupported metadata source.')

        # Strip metadata
        result = io.BytesIO()
        with _FFmpegContext(file, result) as ctx:
            encoder_name = self.__mime_type_to_encoder[mime_type]
            command = ['ffmpeg', '-loglevel', 'error',
                       '-i', ctx.input_path,
                       '-map_metadata', '-1', '-codec', 'copy',
                       '-y', '-f', encoder_name, ctx.output_path]
            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not strip metadata: %s' % error_message)

        return result
Esempio n. 8
0
    def rotate(self, asset, angle, expand=False):
        """
        Creates an asset whose essence is rotated by the specified angle in
        degrees.

        :param asset: Asset whose contents will be rotated
        :type asset: Asset
        :param angle: Angle in degrees, counter clockwise
        :type angle: float
        :param expand: If true, changes the dimensions of the new asset so it
            can hold the entire rotated essence, otherwise the dimensions of
            the original asset will be used.
        :type expand: bool
        :return: New asset with rotated essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if angle % 360.0 == 0.0:
            return asset

        angle_rad = radians(angle)
        width = asset.width
        height = asset.height

        if expand:
            if angle % 180 < 90:
                width_ = asset.width
                height_ = asset.height
                angle_rad_ = angle_rad % pi
            else:
                width_ = asset.height
                height_ = asset.width
                angle_rad_ = angle_rad % pi - pi/2
            cos_a = cos(angle_rad_)
            sin_a = sin(angle_rad_)
            width = ceil(round(width_ * cos_a + height_ * sin_a, 7))
            height = ceil(round(width_ * sin_a + height_ * cos_a, 7))

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f:v', 'rotate=a=%(a)f:ow=%(w)d:oh=%(h)d)' % dict(a=angle_rad, w=width, h=height),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert video asset: %s' % error_message)

        return Asset(essence=result, mime_type=mime_type,
                     width=width, height=height)
Esempio n. 9
0
    def crop(self, asset, x, y, width, height):
        """
        Creates a cropped video asset whose essence is cropped to the specified
        rectangular area.

        :param asset: Video asset whose contents will be cropped
        :type asset: Asset
        :param x: Horizontal offset of the cropping area from left
        :type x: int
        :param y: Vertical offset of the cropping area from top
        :type y: int
        :param width: Width of the cropping area
        :type width: int
        :param height: Height of the cropping area
        :type height: int
        :return: New asset with cropped essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if x == 0 and y == 0 and width == asset.width and height == asset.height:
            return asset

        max_x = max(0, min(asset.width, width + x))
        max_y = max(0, min(asset.height, height + y))
        min_x = max(0, min(asset.width, x))
        min_y = max(0, min(asset.height, y))

        if min_x == asset.width or min_y == asset.height or max_x <= min_x or max_y <= min_y:
            raise OperatorError('Invalid cropping area: <x=%r, y=%r, width=%r, height=%r>' % (x, y, width, height))

        width = max_x - min_x
        height = max_y - min_y

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f:v', 'crop=w=%d:h=%d:x=%d:y=%d' % (width, height, x, y),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not crop asset: %s' % error_message)

        metadata = _combine_metadata(asset,
                                     'mime_type', 'duration', 'video', 'audio', 'subtitle',
                                     width=width, height=height)

        return Asset(essence=result, **metadata)
Esempio n. 10
0
    def crop(self, asset, x, y, width, height):
        """
        Creates a cropped video asset whose essence is cropped to the specified
        rectangular area.

        :param asset: Video asset whose contents will be cropped
        :type asset: Asset
        :param x: Horizontal offset of the cropping area from left
        :type x: int
        :param y: Vertical offset of the cropping area from top
        :type y: int
        :param width: Width of the cropping area
        :type width: int
        :param height: Height of the cropping area
        :type height: int
        :return: New asset with cropped essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if x == 0 and y == 0 and width == asset.width and height == asset.height:
            return asset

        max_x = max(0, min(asset.width, width + x))
        max_y = max(0, min(asset.height, height + y))
        min_x = max(0, min(asset.width, x))
        min_y = max(0, min(asset.height, y))

        if min_x == asset.width or min_y == asset.height or max_x <= min_x or max_y <= min_y:
            raise OperatorError('Invalid cropping area: <x=%r, y=%r, width=%r, height=%r>' % (x, y, width, height))

        width = max_x - min_x
        height = max_y - min_y

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f:v', 'crop=w=%d:h=%d:x=%d:y=%d' % (width, height, x, y),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert video asset: %s' % error_message)

        return Asset(essence=result, mime_type=mime_type,
                     width=width, height=height)
Esempio n. 11
0
def nut_audio_asset(tmpdir_factory):
    duration = DEFAULT_DURATION
    command = (
        'ffmpeg -loglevel error -f lavfi -i sine=frequency=440:duration=%.1f '
        '-vn -sn -c:a pcm_s16le -f nut' % duration).split()
    tmpfile = tmpdir_factory.mktemp('nut_asset').join('without_metadata.nut')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='audio/x-nut',
                            duration=duration)
Esempio n. 12
0
def mp3_audio_asset(tmpdir_factory):
    duration = DEFAULT_DURATION
    command = (
        'ffmpeg -loglevel error -f lavfi -i sine=frequency=440:duration=%.1f '
        '-write_xing 0 -id3v2_version 0 -write_id3v1 0 '
        '-vn -f mp3' % duration).split()
    tmpfile = tmpdir_factory.mktemp('mp3_asset').join('without_metadata.mp3')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='audio/mpeg',
                            duration=duration)
Esempio n. 13
0
    def resize(self, asset, width, height):
        """
        Creates a new image or video asset of the specified width and height
        from the essence of the specified image or video asset.

        Width and height must be positive numbers.

        :param asset: Video asset that will serve as the source for the frame
        :type asset: Asset
        :param width: Width of the resized asset
        :type width: int
        :param height: Height of the resized asset
        :type height: int
        :return: New asset with specified width and height
        :rtype: Asset
        """
        if width < 1 or height < 1:
            raise ValueError('Invalid dimensions: %dx%d' % (width, height))

        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name:
            raise UnsupportedFormatError('Unsupported asset type: %s' % mime_type)
        if mime_type.type not in ('image', 'video'):
            raise OperatorError('Cannot resize asset of type %s')

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            with open(ctx.input_path, 'wb') as temp_in:
                shutil.copyfileobj(asset.essence, temp_in)
                temp_in.flush()

            command = ['ffmpeg', '-loglevel', 'error',
                       '-f', encoder_name, '-i', ctx.input_path,
                       '-filter:v', 'scale=%d:%d' % (width, height),
                       '-threads', str(self.__threads),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not resize asset: %s' % error_message)

        metadata = _combine_metadata(asset,
                                     'mime_type', 'duration', 'video', 'audio', 'subtitle',
                                     width=width, height=height)

        return Asset(essence=result, **metadata)
Esempio n. 14
0
    def combine(self, file, metadata_by_type):
        try:
            probe_data = _probe(file)
        except CalledProcessError:
            raise UnsupportedFormatError('Unsupported file format.')

        decoder_and_stream_type = _get_decoder_and_stream_type(probe_data)
        mime_type = self.__decoder_and_stream_type_to_mime_type.get(decoder_and_stream_type)
        if not mime_type:
            raise UnsupportedFormatError('Unsupported metadata source.')

        # Validate provided metadata
        if not metadata_by_type:
            raise ValueError('No metadata provided')
        if 'ffmetadata' not in metadata_by_type:
            raise UnsupportedFormatError('Invalid metadata to be combined with essence: %r' %
                                         (metadata_by_type.keys(),))
        if not metadata_by_type['ffmetadata']:
            raise ValueError('No metadata provided')

        # Add metadata to file
        result = io.BytesIO()
        with _FFmpegContext(file, result) as ctx:
            encoder_name = self.__mime_type_to_encoder[mime_type]
            command = ['ffmpeg', '-loglevel', 'error',
                       '-f', encoder_name, '-i', ctx.input_path]

            ffmetadata = metadata_by_type['ffmetadata']
            metadata_keys = self.metadata_keys_by_mime_type[mime_type]
            for metadata_key, value in ffmetadata.items():
                ffmetadata_key = metadata_keys.get(metadata_key)
                if ffmetadata_key is None:
                    raise ValueError('Unsupported metadata key: %r' % metadata_key)
                command.append('-metadata')
                command.append('%s=%s' % (ffmetadata_key, value))

            command.extend(['-codec', 'copy',
                            '-y', '-f', encoder_name, ctx.output_path])

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not add metadata: %s' % error_message)

        return result
Esempio n. 15
0
    def combine(self, file, metadata_by_type):
        try:
            probe_data = _probe(file)
        except CalledProcessError:
            raise UnsupportedFormatError('Unsupported file format.')

        decoder_and_stream_type = _get_decoder_and_stream_type(probe_data)
        mime_type = self.__decoder_and_stream_type_to_mime_type.get(decoder_and_stream_type)
        if not mime_type:
            raise UnsupportedFormatError('Unsupported metadata source.')

        # Validate provided metadata
        if not metadata_by_type:
            raise ValueError('No metadata provided')
        if 'ffmetadata' not in metadata_by_type:
            raise UnsupportedFormatError('Invalid metadata to be combined with essence: %r' %
                                         (metadata_by_type.keys(),))
        if not metadata_by_type['ffmetadata']:
            raise ValueError('No metadata provided')

        # Add metadata to file
        result = io.BytesIO()
        with _FFmpegContext(file, result) as ctx:
            encoder_name = self.__mime_type_to_encoder[mime_type]
            command = ['ffmpeg', '-loglevel', 'error',
                       '-f', encoder_name, '-i', ctx.input_path]

            ffmetadata = metadata_by_type['ffmetadata']
            metadata_keys = self.metadata_keys_by_mime_type[mime_type]
            for metadata_key, value in ffmetadata.items():
                ffmetadata_key = metadata_keys.get(metadata_key)
                if ffmetadata_key is None:
                    raise ValueError('Unsupported metadata key: %r' % metadata_key)
                command.append('-metadata')
                command.append('%s=%s' % (ffmetadata_key, value))

            command.extend(['-codec', 'copy',
                            '-y', '-f', encoder_name, ctx.output_path])

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not add metadata: %s' % error_message)

        return result
Esempio n. 16
0
    def resize(self, asset, width, height):
        """
        Creates a new image or video asset of the specified width and height
        from the essence of the specified image or video asset.

        Width and height must be positive numbers.

        :param asset: Video asset that will serve as the source for the frame
        :type asset: Asset
        :param width: Width of the resized asset
        :type width: int
        :param height: Height of the resized asset
        :type height: int
        :return: New asset with specified width and height
        :rtype: Asset
        """
        if width < 1 or height < 1:
            raise ValueError('Invalid dimensions: %dx%d' % (width, height))

        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name:
            raise UnsupportedFormatError('Unsupported asset type: %s' % mime_type)
        if mime_type.type not in ('image', 'video'):
            raise OperatorError('Cannot resize asset of type %s')

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            with open(ctx.input_path, 'wb') as temp_in:
                shutil.copyfileobj(asset.essence, temp_in)
                temp_in.flush()

            command = ['ffmpeg', '-loglevel', 'error',
                       '-f', encoder_name, '-i', ctx.input_path,
                       '-filter:v', 'scale=%d:%d' % (width, height),
                       '-threads', str(self.__threads),
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not resize video asset: %s' % error_message)

        return Asset(essence=result, mime_type=mime_type,
                     width=width, height=height, duration=asset.duration)
Esempio n. 17
0
    def trim(self, asset, from_seconds=0, to_seconds=0):
        """
        Creates a trimmed audio or video asset that only contains the data
        between from_seconds and to_seconds.

        :param asset: Audio or video asset, which will serve as the source
        :type asset: Asset
        :param from_seconds: Start time of the clip in seconds
        :type from_seconds: float
        :param to_seconds: End time of the clip in seconds
        :type to_seconds: float
        :return: New asset with trimmed essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type not in ('audio', 'video'):
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if to_seconds <= 0:
            to_seconds = asset.duration + to_seconds

        duration = float(to_seconds) - float(from_seconds)

        if duration <= 0:
            raise ValueError('Start time must be before end time')

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-ss', str(float(from_seconds)), '-t', str(duration),
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not trim asset: %s' % error_message)

        metadata = _combine_metadata(asset,
                                     'mime_type', 'width', 'height', 'video', 'audio', 'subtitle',
                                     duration=duration)

        return Asset(essence=result, **metadata)
Esempio n. 18
0
    def extract_frame(self, asset, mime_type, seconds=0):
        """
        Creates a new image asset of the specified MIME type from the essence
        of the specified video asset.

        :param asset: Video asset which will serve as the source for the frame
        :type asset: Asset
        :param mime_type: MIME type of the destination image
        :type mime_type: MimeType or str
        :param seconds: Offset of the frame in seconds
        :type seconds: float
        :return: New image asset with converted essence
        :rtype: Asset
        """
        source_mime_type = MimeType(asset.mime_type)
        if source_mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % source_mime_type)

        mime_type = MimeType(mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        codec_name = self.__mime_type_to_codec.get(mime_type)
        if not (encoder_name and codec_name):
            raise UnsupportedFormatError('Unsupported target asset type: %s' % mime_type)

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path,
                       '-ss', str(float(seconds)),
                       '-codec:v', codec_name, '-vframes', '1',
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not extract frame from asset: %s' % error_message)

        metadata = _combine_metadata(asset,
                                     'width', 'height',
                                     mime_type=mime_type)
        if 'video' in asset.metadata:
            metadata['depth'] = asset.metadata['video']['depth']

        return Asset(essence=result, **metadata)
Esempio n. 19
0
    def test_resize_returns_essence_with_same_format(self, processor, mkv_video_asset):
        resize = processor.resize(width=12, height=34)

        resized_asset = resize(mkv_video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split()
        result = subprocess_run(command, input=resized_asset.essence.read(), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        assert video_info.get('format', {}).get('format_name') == 'matroska,webm'
Esempio n. 20
0
 def test_converted_essence_is_of_specified_type(self, converted_asset):
     command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split(
     )
     result = subprocess_run(command,
                             input=converted_asset.essence.read(),
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             check=True)
     video_info = json.loads(result.stdout.decode('utf-8'))
     assert video_info.get('format', {}).get('format_name') == 'mp3'
Esempio n. 21
0
    def test_trimmed_asset_contains_valid_essence(self, processor, video_asset):
        trim_operator = processor.trim(from_seconds=0, to_seconds=0.1)

        trimmed_asset = trim_operator(video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split()
        result = subprocess_run(command, input=trimmed_asset.essence.read(), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        assert bool(video_info.get('format'))
Esempio n. 22
0
    def trim(self, asset, from_seconds=0, to_seconds=0):
        """
        Creates a trimmed audio or video asset that only contains the data
        between from_seconds and to_seconds.

        :param asset: Audio or video asset, which will serve as the source
        :type asset: Asset
        :param from_seconds: Start time of the clip in seconds
        :type from_seconds: float
        :param to_seconds: End time of the clip in seconds
        :type to_seconds: float
        :return: New asset with trimmed essence
        :rtype: Asset
        """
        mime_type = MimeType(asset.mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name or mime_type.type not in ('audio', 'video'):
            raise UnsupportedFormatError('Unsupported source asset type: %s' % mime_type)

        if to_seconds <= 0:
            to_seconds = asset.duration + to_seconds

        duration = float(to_seconds) - float(from_seconds)

        if duration <= 0:
            raise ValueError('Start time must be before end time')

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-ss', str(float(from_seconds)), '-t', str(duration),
                       '-i', ctx.input_path, '-codec', 'copy',
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert video asset: %s' % error_message)

        return Asset(essence=result, mime_type=asset.mime_type,
                     width=asset.width, height=asset.height, duration=duration)
Esempio n. 23
0
 def test_converted_essence_stream_has_specified_codec(
         self, converted_asset):
     command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split(
     )
     result = subprocess_run(command,
                             input=converted_asset.essence.read(),
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             check=True)
     video_info = json.loads(result.stdout.decode('utf-8'))
     assert video_info.get('streams', [{}])[0].get('codec_name') == 'mp3'
Esempio n. 24
0
    def __probe_streams_by_type(self, converted_asset):
        command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split()
        result = subprocess_run(command, input=converted_asset.essence.read(), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)
        ffprobe_info = json.loads(result.stdout.decode('utf-8'))

        streams_by_type = defaultdict(list)
        for stream in ffprobe_info.get('streams', []):
            streams_by_type[stream['codec_type']].append(stream)

        return streams_by_type
Esempio n. 25
0
def avi_video_asset(tmpdir_factory):
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-c:v h264 -c:a mp3 -f avi' % ffmpeg_params).split()
    tmpfile = tmpdir_factory.mktemp('mkv_video_asset').join('h264-mp3.avi')
    command.append(str(tmpfile))
    subprocess_run(command, check=True, stderr=subprocess.PIPE)
    with tmpfile.open('rb') as file:
        essence = file.read()
    return madam.core.Asset(essence=io.BytesIO(essence),
                            mime_type='video/x-msvideo',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION)
Esempio n. 26
0
    def test_resize_returns_essence_with_correct_dimensions(self, processor, video_asset):
        resize_operator = processor.resize(width=12, height=34)

        resized_asset = resize_operator(video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split()
        result = subprocess_run(command, input=resized_asset.essence.read(), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        first_stream = video_info.get('streams', [{}])[0]
        assert first_stream.get('width') == 12
        assert first_stream.get('height') == 34
Esempio n. 27
0
    def extract_frame(self, asset, mime_type, seconds=0):
        """
        Creates a new image asset of the specified MIME type from the essence
        of the specified video asset.

        :param asset: Video asset which will serve as the source for the frame
        :type asset: Asset
        :param mime_type: MIME type of the destination image
        :type mime_type: MimeType or str
        :param seconds: Offset of the frame in seconds
        :type seconds: float
        :return: New image asset with converted essence
        :rtype: Asset
        """
        source_mime_type = MimeType(asset.mime_type)
        if source_mime_type.type != 'video':
            raise UnsupportedFormatError('Unsupported source asset type: %s' % source_mime_type)

        mime_type = MimeType(mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        codec_name = self.__mime_type_to_codec.get(mime_type)
        if not (encoder_name and codec_name):
            raise UnsupportedFormatError('Unsupported target asset type: %s' % mime_type)

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-v', 'error',
                       '-i', ctx.input_path,
                       '-ss', str(float(seconds)),
                       '-codec:v', codec_name, '-vframes', '1',
                       '-f', encoder_name, '-y', ctx.output_path]

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert video asset: %s' % error_message)

        return Asset(essence=result, mime_type=mime_type,
                     width=asset.width, height=asset.height)
Esempio n. 28
0
def _probe(file):
    with tempfile.NamedTemporaryFile(mode='wb') as temp_in:
        shutil.copyfileobj(file, temp_in.file)
        temp_in.flush()
        file.seek(0)

        command = 'ffprobe -loglevel error -print_format json -show_format -show_streams'.split()
        command.append(temp_in.name)
        result = subprocess_run(command, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)

    string_result = result.stdout.decode('utf-8')
    json_obj = json.loads(string_result)

    return json_obj
Esempio n. 29
0
    def test_trimmed_asset_contains_valid_essence(self, processor,
                                                  video_asset):
        trim_operator = processor.trim(from_seconds=0, to_seconds=0.1)

        trimmed_asset = trim_operator(video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split(
        )
        result = subprocess_run(command,
                                input=trimmed_asset.essence.read(),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        assert bool(video_info.get('format'))
Esempio n. 30
0
    def __probe_streams_by_type(self, converted_asset):
        command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split(
        )
        result = subprocess_run(command,
                                input=converted_asset.essence.read(),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                check=True)
        ffprobe_info = json.loads(result.stdout.decode('utf-8'))

        streams_by_type = defaultdict(list)
        for stream in ffprobe_info.get('streams', []):
            streams_by_type[stream['codec_type']].append(stream)

        return streams_by_type
Esempio n. 31
0
def _probe(file):
    with tempfile.NamedTemporaryFile(mode='wb') as temp_in:
        shutil.copyfileobj(file, temp_in.file)
        temp_in.flush()
        file.seek(0)

        command = 'ffprobe -loglevel error -print_format json -show_format -show_streams'.split()
        command.append(temp_in.name)
        result = subprocess_run(command, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, check=True)

    string_result = result.stdout.decode('utf-8')
    json_obj = json.loads(string_result)

    return json_obj
Esempio n. 32
0
    def test_resize_returns_essence_with_same_format(self, processor,
                                                     mkv_video_asset):
        resize = processor.resize(width=12, height=34)

        resized_asset = resize(mkv_video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split(
        )
        result = subprocess_run(command,
                                input=resized_asset.essence.read(),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        assert video_info.get('format',
                              {}).get('format_name') == 'matroska,webm'
Esempio n. 33
0
    def test_resize_returns_essence_with_correct_dimensions(
            self, processor, video_asset):
        resize_operator = processor.resize(width=12, height=34)

        resized_asset = resize_operator(video_asset)

        command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split(
        )
        result = subprocess_run(command,
                                input=resized_asset.essence.read(),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                check=True)
        video_info = json.loads(result.stdout.decode('utf-8'))
        first_stream = video_info.get('streams', [{}])[0]
        assert first_stream.get('width') == 12
        assert first_stream.get('height') == 34
Esempio n. 34
0
    def __init__(self):
        """
        Initializes a new `FFmpegProcessor`.

        :raises EnvironmentError: if the installed version of ffprobe does not match the minimum version requirement
        """
        super().__init__()

        self._min_version = '0.9'
        command = 'ffprobe -version'.split()
        result = subprocess_run(command, stdout=subprocess.PIPE)
        string_result = result.stdout.decode('utf-8')
        version_string = string_result.split()[2]
        if version_string < self._min_version:
            raise EnvironmentError('Found ffprobe version %s. Requiring at least version %s.'
                                   % (version_string, self._min_version))

        self.__threads = multiprocessing.cpu_count()
Esempio n. 35
0
    def __init__(self):
        """
        Initializes a new `FFmpegProcessor`.

        :raises EnvironmentError: if the installed version of ffprobe does not match the minimum version requirement
        """
        super().__init__()

        self._min_version = '0.9'
        command = 'ffprobe -version'.split()
        result = subprocess_run(command, stdout=subprocess.PIPE)
        string_result = result.stdout.decode('utf-8')
        version_string = string_result.split()[2]
        if version_string < self._min_version:
            raise EnvironmentError('Found ffprobe version %s. Requiring at least version %s.'
                                   % (version_string, self._min_version))

        self.__threads = multiprocessing.cpu_count()
Esempio n. 36
0
def nut_video_asset():
    ffmpeg_params = dict(
        width=DEFAULT_WIDTH,
        height=DEFAULT_HEIGHT,
        duration=DEFAULT_DURATION,
    )
    command = (
        'ffmpeg -loglevel error '
        '-f lavfi -i color=color=red:size=%(width)dx%(height)d:duration=%(duration).1f:rate=15 '
        '-f lavfi -i sine=frequency=440:duration=%(duration).1f '
        '-c:v ffv1 -level 3 -a:c pcm_s16le -sn '
        '-f nut pipe:' % ffmpeg_params).split()
    ffmpeg = subprocess_run(command,
                            check=True,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
    return madam.core.Asset(essence=io.BytesIO(ffmpeg.stdout),
                            mime_type='video/x-nut',
                            width=DEFAULT_WIDTH,
                            height=DEFAULT_HEIGHT,
                            duration=DEFAULT_DURATION)
Esempio n. 37
0
 def test_converted_essence_stream_has_specified_codec(self, converted_asset):
     command = 'ffprobe -print_format json -loglevel error -show_streams -i pipe:'.split()
     result = subprocess_run(command, input=converted_asset.essence.read(), stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE, check=True)
     video_info = json.loads(result.stdout.decode('utf-8'))
     assert video_info.get('streams', [{}])[0].get('codec_name') == 'mp3'
Esempio n. 38
0
    def convert(self, asset, mime_type, video=None, audio=None, subtitle=None):
        """
        Creates a new asset of the specified MIME type from the essence of the
        specified asset.

        Additional options can be specified for video, audio, and subtitle streams.
        Options are passed as dictionary instances and can contain various keys for
        each stream type.

        **Options for video streams:**

        - **codec** – Processor-specific name of the video codec as string
        - **bitrate** – Target bitrate in kBit/s as float number

        **Options for audio streams:**

        - **codec** – Processor-specific name of the audio codec as string
        - **bitrate** – Target bitrate in kBit/s as float number

        **Options for subtitle streams:**

        - **codec** – Processor-specific name of the subtitle format as string

        :param asset: Asset whose contents will be converted
        :type asset: Asset
        :param mime_type: MIME type of the video container
        :type mime_type: MimeType or str
        :param video: Dictionary with options for video streams.
        :type video: dict or None
        :param audio: Dictionary with options for audio streams.
        :type audio: dict or None
        :param subtitle: Dictionary with the options for subtitle streams.
        :type subtitle: dict or None
        :return: New asset with converted essence
        :rtype: Asset
        """
        mime_type = MimeType(mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name:
            raise UnsupportedFormatError('Unsupported asset type: %s' % mime_type)

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-loglevel', 'error',
                       '-i', ctx.input_path]
            if video:
                if 'codec' in video:
                    if video['codec']:
                        command.extend(['-c:v', video['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('video', {})
                        command.extend(codec_options.get(video['codec'], []))
                    else:
                        command.extend(['-vn'])
                if video.get('bitrate'):
                    # Set minimum at 50% of bitrate and maximum at 145% of bitrate
                    # (see https://developers.google.com/media/vp9/settings/vod/)
                    command.extend(['-minrate', '%dk' % round(0.5*video['bitrate']),
                                    '-b:v', '%dk' % video['bitrate'],
                                    '-maxrate', '%dk' % round(1.45*video['bitrate'])])
                if video.get('color_space') or video.get('depth') or video.get('data_type'):
                    color_mode = (
                        video.get('color_space', asset.video.get('color_space', 'YUV')),
                        video.get('depth', asset.video.get('depth', 8)),
                        video.get('data_type', asset.video.get('data_type', 'uint')),
                    )
                    ffmpeg_pix_fmt = FFmpegProcessor.__color_mode_to_ffmpeg_pix_fmt.get(color_mode)
                    if ffmpeg_pix_fmt:
                        command.extend(['-pix_fmt', ffmpeg_pix_fmt])
            if audio:
                if 'codec' in audio:
                    if audio['codec']:
                        command.extend(['-c:a', audio['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('audio', {})
                        command.extend(codec_options.get(audio['codec'], []))
                    else:
                        command.extend(['-an'])
                if audio.get('bitrate'):
                    command.extend(['-b:a', '%dk' % audio['bitrate']])
            if subtitle:
                if 'codec' in subtitle:
                    if subtitle['codec']:
                        command.extend(['-c:s', subtitle['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('subtitles', {})
                        command.extend(codec_options.get(subtitle['codec'], []))
                    else:
                        command.extend(['-sn'])

            container_options = FFmpegProcessor.__container_options.get(mime_type, [])
            command.extend(container_options)

            command.extend(['-threads', str(self.__threads),
                            '-f', encoder_name, '-y', ctx.output_path])

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert asset: %s' % error_message)

        return self.read(result)
Esempio n. 39
0
 def test_converted_essence_is_of_specified_type(self, converted_asset):
     command = 'ffprobe -print_format json -loglevel error -show_format -i pipe:'.split()
     result = subprocess_run(command, input=converted_asset.essence.read(), stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE, check=True)
     video_info = json.loads(result.stdout.decode('utf-8'))
     assert video_info.get('format', {}).get('format_name') == 'matroska,webm'
Esempio n. 40
0
    def convert(self, asset, mime_type, video=None, audio=None, subtitle=None):
        """
        Creates a new asset of the specified MIME type from the essence of the
        specified asset.

        Additional options can be specified for video, audio, and subtitle streams.
        Options are passed as dictionary instances and can contain various keys for
        each stream type.

        **Options for video streams:**

        - **codec** – Processor-specific name of the video codec as string
        - **bitrate** – Target bitrate in kBit/s as float number

        **Options for audio streams:**

        - **codec** – Processor-specific name of the audio codec as string
        - **bitrate** – Target bitrate in kBit/s as float number

        **Options for subtitle streams:**

        - **codec** – Processor-specific name of the subtitle format as string

        :param asset: Asset whose contents will be converted
        :type asset: Asset
        :param mime_type: MIME type of the video container
        :type mime_type: MimeType or str
        :param video: Dictionary with options for video streams.
        :type video: dict or None
        :param audio: Dictionary with options for audio streams.
        :type audio: dict or None
        :param subtitle: Dictionary with the options for subtitle streams.
        :type subtitle: dict or None
        :return: New asset with converted essence
        :rtype: Asset
        """
        mime_type = MimeType(mime_type)
        encoder_name = self.__mime_type_to_encoder.get(mime_type)
        if not encoder_name:
            raise UnsupportedFormatError('Unsupported asset type: %s' % mime_type)

        result = io.BytesIO()
        with _FFmpegContext(asset.essence, result) as ctx:
            command = ['ffmpeg', '-loglevel', 'error',
                       '-i', ctx.input_path]
            if video:
                if 'codec' in video:
                    if video['codec']:
                        command.extend(['-c:v', video['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('video', {})
                        command.extend(codec_options.get(video['codec'], []))
                    else:
                        command.extend(['-vn'])
                if video.get('bitrate'):
                    # Set minimum at 50% of bitrate and maximum at 145% of bitrate
                    # (see https://developers.google.com/media/vp9/settings/vod/)
                    command.extend(['-minrate', '%dk' % round(0.5*video['bitrate']),
                                    '-b:v', '%dk' % video['bitrate'],
                                    '-maxrate', '%dk' % round(1.45*video['bitrate'])])
            if audio:
                if 'codec' in audio:
                    if audio['codec']:
                        command.extend(['-c:a', audio['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('audio', {})
                        command.extend(codec_options.get(audio['codec'], []))
                    else:
                        command.extend(['-an'])
                if audio.get('bitrate'):
                    command.extend(['-b:a', '%dk' % audio['bitrate']])
            if subtitle:
                if 'codec' in subtitle:
                    if subtitle['codec']:
                        command.extend(['-c:s', subtitle['codec']])
                        codec_options = FFmpegProcessor.__codec_options.get('subtitles', {})
                        command.extend(codec_options.get(subtitle['codec'], []))
                    else:
                        command.extend(['-sn'])

            container_options = FFmpegProcessor.__container_options.get(mime_type, [])
            command.extend(container_options)

            command.extend(['-threads', str(self.__threads),
                            '-f', encoder_name, '-y', ctx.output_path])

            try:
                subprocess_run(command, stderr=subprocess.PIPE, check=True)
            except CalledProcessError as ffmpeg_error:
                error_message = ffmpeg_error.stderr.decode('utf-8')
                raise OperatorError('Could not convert video asset: %s' % error_message)

        metadata = {
            'mime_type': str(mime_type)
        }
        if mime_type.type in ('image', 'video'):
            metadata['width'] = asset.width
            metadata['height'] = asset.height
        if mime_type.type in ('audio', 'video'):
            metadata['duration'] = asset.duration

        return Asset(essence=result, **metadata)