<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.Filter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterGroup

class FilterGroupInitializer(private val filterGroup: FilterGroup) : BaseFiltersInitializer() {

    override fun addFilter(filter: Filter) {
        filterGroup.addFilter(filter)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.annotation.FFmpegInitializerMarker
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.*
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter.PanFilter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter.PanFilterFactory

@FFmpegInitializerMarker
abstract class BaseFiltersInitializer {
    var audioSampleRate: Number? = null
        set(value) {
            field = value
            value?.let(::addAudioSampleRateFilter)
        }
    var audioResampleRate: Number? = null
        set(value) {
            field = value
            value?.let(::addAudioResampleRateFilter)
        }
    var audioTempo: Double? = null
        set(value) {
            field = value
            value?.let(::addAudioTempoFilter)
        }
    var volume: Number? = null
        set(value) {
            field = value
            value?.let(::addVolumeFilter)
        }


    private fun addAudioSampleRateFilter(sampleRate: Number) {
        addFilter(AudioSampleRateFilter(sampleRate))
    }

    private fun addAudioResampleRateFilter(resampleRate: Number) {
        addFilter(AudioResampleFilter(resampleRate))
    }

    private fun addAudioTempoFilter(tempo: Double) {
        addFilter(AudioTempoFilter(tempo))
    }

    private fun addVolumeFilter(volume: Number) {
        addFilter(VolumeFilter(volume))
    }

    fun pan(initializer: PanInitializer.() -> Unit): PanFilter {
        val panInitializer = PanInitializer().apply(initializer)
        val panFilter = PanFilterFactory.create(panInitializer.channelType, panInitializer.gains)

        addFilter(panFilter)

        return panFilter
    }

    protected abstract fun addFilter(filter: Filter)
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.command


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface KeyCommandArgument {
    abstract val key: String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.command


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface ValueCommandArgument {
    abstract val value: String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier


/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 *
 * @see https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1
 */
class StreamSpecifier private constructor() {

    private var streamIndex: Int? = null
    private var streamType: StreamType? = null
    private var additionalStreamSpecifier: Int? = null
    private var customAdditionalStreamSpecifier: String = ""

    constructor(
        streamIndex: Int,
        streamType: StreamType,
        additionalStreamSpecifier: Int? = null
    ) : this() {
        this.streamIndex = streamIndex
        this.streamType = streamType
        this.additionalStreamSpecifier = additionalStreamSpecifier
    }

    constructor(streamType: StreamType, additionalStreamSpecifier: Int? = null) : this() {
        this.streamType = streamType
        this.additionalStreamSpecifier = additionalStreamSpecifier
    }

    constructor(streamType: StreamType, customAdditionalStreamSpecifier: String) : this() {
        this.streamType = streamType
        this.customAdditionalStreamSpecifier = customAdditionalStreamSpecifier
    }


    fun parseToString(): String {
        val resultString = StringBuilder()

        streamIndex?.let {
            resultString.append(it).append(":")
        }

        streamType?.let {
            resultString.append(it.id)
        }

        additionalStreamSpecifier?.let {
            resultString.append(":").append(it)
        }

        resultString.append(customAdditionalStreamSpecifier)

        return resultString.toString()
    }

    override fun toString(): String = parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.annotation.FFmpegInitializerMarker
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.Filter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.AudioMixFilter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption.DropoutTransitionOption
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.Duration
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption.DurationOption
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption.InputsOption

@FFmpegInitializerMarker
class AudioMixInitializer(private val audioMixFilter: AudioMixFilter) : BaseFiltersInitializer() {
    var inputs: Int? = null
        set(value) {
            field = value
            value?.let(::addInputsOption)
        }
    var duration: Duration? = null
        set(value) {
            field = value
            value?.let(::addDurationOption)
        }
    var dropoutTransition: Int? = null
        set(value) {
            field = value
            value?.let(::addDropoutTransitionOption)
        }

    private fun addInputsOption(inputsSize: Int) {
        audioMixFilter.addOption(InputsOption(inputsSize))
    }

    private fun addDurationOption(duration: Duration) {
        audioMixFilter.addOption(DurationOption(duration))
    }

    private fun addDropoutTransitionOption(dropoutTransition: Int) {
        audioMixFilter.addOption(DropoutTransitionOption(dropoutTransition))
    }

    override fun addFilter(filter: Filter) {
        audioMixFilter.addFilter(filter)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.KeyAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.ValueAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.KeyCommandArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.ValueCommandArgument

abstract class Filter: KeyCommandArgument, ValueCommandArgument, CommandArgumentParser {
    override fun parseToString(): String {
        val trimmedKey = key.trim()
        val trimmedValue = value.trim()

        KeyAssertion.assert(trimmedKey)
        ValueAssertion.assert(trimmedValue)

        return "${trimmedKey}=${trimmedValue}"
    }

    override fun toString(): String =
        parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.CodecNotSupportedException
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamType
import org.junit.Test

/**
 * Created by <NAME> on 21/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class CodecArgumentTest {

    @Test
    fun should_HaveBasicKey_when_StreamSpecifierIsNull() {
        val codecArgument = CodecArgument("libcodec")
        val codecKey = codecArgument.key

        assertThat(codecKey).isEqualTo("-c")
    }

    @Test
    fun should_ContainsStreamSpecifierAtTheEndOfTheKey_when_StreamSpecifierIsNotNull() {
        val codecArgument = CodecArgument("libcodec", StreamSpecifier(StreamType.Audio, 1))

        val codecKey = codecArgument.key

        assertThat(codecKey).isEqualTo("-c:a:1")
    }

    @Test(expected = CodecNotSupportedException::class)
    fun should_FailToConstruct_when_CodecIsBlank() {
        CodecArgument("")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_CodecAndStreamSpecifierAreNotBlank() {
        val codecArgument = CodecArgument("libcodec", StreamSpecifier(StreamType.Audio))

        val codecArgumentParsed = codecArgument.parseToString()

        assertThat(codecArgumentParsed).isEqualTo("-c:a \"libcodec\"")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.StreamSpecifierAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier


/**
 * Created by <NAME> on 25/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class MapArgument private constructor(): Argument() {
    override val key: String = "-map"
    override val value: String
        get() = valueString

    private var valueString: String = ""

    constructor(streamSpecifier: StreamSpecifier): this() {
        valueString = streamSpecifier.parseToString()
    }

    constructor(specifier: String): this() {
        StreamSpecifierAssertion.assert(specifier)

        valueString = specifier
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.GainAssertion

class MonoPanFilter(gains: FloatArray) : PanFilter(ChannelType.Mono) {
    init {
        GainAssertion.assert(gains)
        setupChannels(gains)
    }

    private fun setupChannels(gains: FloatArray) {
        val channelsWithGain = gains.mapIndexed { index, value ->
            "$value*c$index"
        }
        val channels = channelsWithGain.joinToString("+")
        val channelsConfig = "c0<$channels"

        addChannelConfig(channelsConfig)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 30/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class NanosecondTimeDurationUnitTest {
    private val nanosecondTimeDurationUnit = NanosecondTimeDurationUnit()

    @Test
    fun should_CorrectlyFormatTheValueToNanosecondsInString_when_ValueIsAPositiveValue() {
        val secondsInString = nanosecondTimeDurationUnit.format(2)

        assertThat(secondsInString).matches("2us")
    }

    @Test
    fun should_CorrectlyFormatTheValueToNanosecondsInString_when_ValueIsANegativeValue() {
        val secondsInString = nanosecondTimeDurationUnit.format(-10)

        assertThat(secondsInString).matches("-10us")
    }

    @Test
    fun should_CorrectlyFormatTheValueToNanosecondsInString_when_ValueIsADecimalNumber() {
        val secondsInString = nanosecondTimeDurationUnit.format(20.5f)

        assertThat(secondsInString).matches("20.5us")
    }

    @Test
    fun should_KeepAllDecimalPartOnFormattedString_when_ValueIsALongDecimalNumber() {
        val secondsInString = nanosecondTimeDurationUnit.format(0.5123123123123)

        assertThat(secondsInString).matches("0.5123123123123us")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.MillisecondTimeDurationUnit
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.NanosecondTimeDurationUnit
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.SecondTimeDurationUnit
import org.junit.Test

/**
 * Created by <NAME> on 30/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class SimpleTimeDurationTest {

    @Test
    fun should_FormatTimeDurationValueBasedOnTimeDurationUnit_when_TimeDurationUnitIsInSeconds() {
        val simpleTimeDuration = SimpleTimeDuration(2f, SecondTimeDurationUnit())

        val formattedTimeDuration = simpleTimeDuration.format()

        assertThat(formattedTimeDuration).matches("2.0")
    }

    @Test
    fun should_FormatTimeDurationValueBasedOnTimeDurationUnit_when_TimeDurationUnitIsInMilliseconds() {
        val simpleTimeDuration = SimpleTimeDuration(200, MillisecondTimeDurationUnit())

        val formattedTimeDuration = simpleTimeDuration.format()

        assertThat(formattedTimeDuration).matches("200ms")
    }

    @Test
    fun should_FormatTimeDurationValueBasedOnTimeDurationUnit_when_TimeDurationUnitIsInNanoseconds() {
        val simpleTimeDuration = SimpleTimeDuration(200, NanosecondTimeDurationUnit())

        val formattedTimeDuration = simpleTimeDuration.format()

        assertThat(formattedTimeDuration).matches("200us")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class StreamSpecifierTest {

    @Test
    fun should_CorrectlyPartStreamSpecifierToString_when_ConstructWithIndexAndStreamType() {
        val streamSpecifier = StreamSpecifier(0, StreamType.Audio)

        val parsedStreamSpecifier = streamSpecifier.parseToString()

        assertThat(parsedStreamSpecifier).isEqualTo("0:a")
    }

    @Test
    fun should_CorrectlyPartStreamSpecifierToString_when_ConstructWithIndexStreamTypeAndAdditionalStreamSpecifier() {
        val streamSpecifier = StreamSpecifier(0, StreamType.Video, 1)

        val parsedStreamSpecifier = streamSpecifier.parseToString()

        assertThat(parsedStreamSpecifier).isEqualTo("0:v:1")
    }

    @Test
    fun should_CorrectlyPartStreamSpecifierToString_when_ConstructOnlyWithStreamType() {
        val streamSpecifier = StreamSpecifier(StreamType.Subtitle)

        val parsedStreamSpecifier = streamSpecifier.parseToString()

        assertThat(parsedStreamSpecifier).isEqualTo("s")
    }

    @Test
    fun should_CorrectlyPartStreamSpecifierToString_when_ConstructWithStreamTypeAndAdditionalStreamSpecifier() {
        val streamSpecifier = StreamSpecifier(StreamType.Audio, 0)

        val parsedStreamSpecifier = streamSpecifier.parseToString()

        assertThat(parsedStreamSpecifier).isEqualTo("a:0")
    }

    @Test
    fun should_CorrectlyPartStreamSpecifierToString_when_ConstructWithStreamTypeAndCustomAdditionalStreamSpecifier() {
        val streamSpecifier = StreamSpecifier(StreamType.Audio, "1")

        val parsedStreamSpecifier = streamSpecifier.parseToString()

        assertThat(parsedStreamSpecifier).isEqualTo("a1")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 01/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class AudioTempoFilterTest {
    @Test
    fun should_CorrectlyParseTheFilter_when_SetAValue() {
        val audioTempoFilter = AudioTempoFilter(2.0)

        val parsedFilter = audioTempoFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("atempo=2.0")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.CodecAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier


/**
 * Created by <NAME> on 21/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class CodecArgument(codec: String, private val streamSpecifier: StreamSpecifier? = null): Argument() {
    override val key: String
        get() = generateKey()
    override val value: String = codec

    init {
        CodecAssertion.assert(codec)
    }

    private fun generateKey(): String {
        val specifier = streamSpecifier?.parseToString()?.let {
            ":$it"
        } ?: ""

        return BASE_KEY + specifier
    }

    companion object {
        const val BASE_KEY = "-c"
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.value

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.ValueIsBlankException
import org.junit.Test

/**
 * Created by <NAME> on 01/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class OutputPathValueTest {

    @Test
    fun should_CorrectlyParseTheValue_when_PathIsNotEmpty() {
        val outputPathValue = OutputPathValue("abc")

        val parsedPath = outputPathValue.parseToString()

        assertThat(parsedPath).isEqualTo("\"abc\"")
    }

    @Test(expected = ValueIsBlankException::class)
    fun should_Fails_when_ParsePathWithBlankValue() {
        val outputPathValue = OutputPathValue("")

        outputPathValue.parseToString()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.command


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface CommandArgumentParser {
    fun parseToString(): String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier

class FilterGroup(
    val inputStreamSpecifier: StreamSpecifier? = null,
    val outputStreamSpecifier: StreamSpecifier? = null
) :
    CommandArgumentParser, TaggedFilter {
    private val filters: MutableList<Filter> = mutableListOf()
    private val filterTagGenerator = FilterTagGenerator()

    override val inputTag: String =
        filterTagGenerator.generateTagFromStreamSpecifier(inputStreamSpecifier)
    override val outputTag: String =
        filterTagGenerator.generateTagFromStreamSpecifier(outputStreamSpecifier)

    fun addFilter(filter: Filter) {
        filters.add(filter)
    }

    override fun parseToString(): String {
        val filtersString = filters.joinToString(",") { it.parseToString() }

        return "$inputTag$filtersString$outputTag;"
    }

    override fun toString(): String =
        parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 *
 * @see https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
 */
interface TimeDuration {
    fun format(): String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.TimeDurationUnit


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class SimpleTimeDuration private constructor(): TimeDuration {

    private var timeDurationString: String = ""

    constructor(value: Number, timeDurationUnit: TimeDurationUnit) : this() {
        timeDurationString = timeDurationUnit.format(value)
    }

    override fun format(): String = timeDurationString
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration


abstract class TimeDurationArgument(override val key: String) : Argument() {
    abstract val timeDuration: TimeDuration
    override val value: String
        get() = timeDuration.format()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidGainListSize
import org.junit.Test

/**
 * Created by <NAME> on 06/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class MonoPanFilterTest {

    @Test
    fun should_CorrectlyParseMonoPanToString_when_GainHasASingleItem() {
        val monoPanFilter = MonoPanFilter(floatArrayOf(1f))

        val parsedString = monoPanFilter.parseToString()

        assertThat(parsedString).isEqualTo("pan=1c|c0<1.0*c0")
    }

    @Test
    fun should_CorrectlyParseMonoPanToString_when_GainHasMultipleItems() {
        val monoPanFilter = MonoPanFilter(floatArrayOf(1f, 2f, .5f))

        val parsedString = monoPanFilter.parseToString()

        assertThat(parsedString).isEqualTo("pan=1c|c0<1.0*c0+2.0*c1+0.5*c2")
    }

    @Test(expected = InvalidGainListSize::class)
    fun should_Fail_when_GainListIsEmpty() {
        MonoPanFilter(floatArrayOf())
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 06/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class InvalidGainListSize: Exception("The `gain` list can not be empty") {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidTimeException
import org.junit.Test

/**
 * Created by <NAME> on 30/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class TimePositionTest {

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsSecondsLessThanTen() {
        val timePosition = TimePosition(2.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("00:02")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsSecondsGreaterThanTen() {
        val timePosition = TimePosition(22.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("00:22")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionContainsSecondsWithLongDecimalValue() {
        val timePosition = TimePosition(12.1412412323)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("00:12.1412412323")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsMinutesLessThanTen() {
        val timePosition = TimePosition(2,0.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("02:00")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsMinutesGreaterThanTen() {
        val timePosition = TimePosition(11,0.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("11:00")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsHoursLessThanTen() {
        val timePosition = TimePosition(8, 0,0.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("08:00:00")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionOnlyContainsHoursGreaterThanTen() {
        val timePosition = TimePosition(90, 0,0.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("90:00:00")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionContainsMinutesAndSeconds() {
        val timePosition = TimePosition(0, 22,5.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("22:05")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionContainsNegativeHours() {
        val timePosition = TimePosition(-1, 45,5.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("-01:45:05")
    }

    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionContainsNegativeSeconds() {
        val timePosition = TimePosition(0, 0,-5.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("-00:05")
    }


    @Test
    fun should_CorrectlyFormatTimePosition_when_TimePositionContainsNegativeMinutes() {
        val timePosition = TimePosition(-2,12.0)

        val formattedTimePosition = timePosition.format()

        assertThat(formattedTimePosition).matches("-02:12")
    }

    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsNegativeMinutesAndHours() {
        TimePosition(12, -2,12.0)
    }

    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsNegativeSecondsAndMinutes() {
        TimePosition(2,-12.0)
    }

    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsNegativeSecondsAndHours() {
        TimePosition(2, 0,-12.0)
    }

    // The minutes should be a value between -59 and +59
    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsMinutesOutOfNegativeValueBoundary() {
        TimePosition(-60, 0.0)
    }

    // The minutes should be a value between -59 and +59
    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsMinutesOutOfPositiveValueBoundary() {
        TimePosition(60, 0.0)
    }

    // The seconds should be a value between -59 and +59
    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsSecondsOutOfNegativeValueBoundary() {
        TimePosition(-100.0)
    }

    // The seconds should be a value between -59 and +59
    @Test(expected = InvalidTimeException::class)
    fun should_Fails_when_TimePositionContainsSecondsOutOfPositiveValueBoundary() {
        TimePosition(100.0)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface TimeDurationUnit {
    fun format(value: Number): String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class InvalidVolumeException: Exception("The 'volume' can not be a negative value") {
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration

class TimeArgument(override val timeDuration: TimeDuration) : TimeDurationArgument("-t") {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 01/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class AudioResampleFilterTest {
    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnIntegerValue() {
        val audioResampleFilter = AudioResampleFilter(44100)

        val parsedFilter = audioResampleFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("aresample=44100")
    }

    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnFloatValue() {
        val audioResampleFilter = AudioResampleFilter(100f)

        val parsedFilter = audioResampleFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("aresample=100.0")
    }

    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnDoubleValue() {
        val audioResampleFilter = AudioResampleFilter(200.0)

        val parsedFilter = audioResampleFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("aresample=200.0")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

class AudioSampleRateFilter(sampleRate: Number) : Filter() {
    override val key: String = "asetrate"
    override val value: String = sampleRate.toString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidVolumeException


/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 */
object VolumeAssertion: Assertion<Number> {
    override fun assert(value: Number) {
        if (value.toInt() < 0) throw InvalidVolumeException()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDurationBuilder
import org.junit.Test

/**
 * Created by <NAME> on 29/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class InputTimeOffsetArgumentTest {

    private val timeDurationBuilder = TimeDurationBuilder()

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsInSeconds() {
        val timedDurationSeconds = timeDurationBuilder.toSecond(2.0)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"2.0\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsInMilliseconds() {
        val timedDurationSeconds = timeDurationBuilder.toMillisecond(200)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"200ms\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsInNanoseconds() {
        val timedDurationSeconds = timeDurationBuilder.toNanosecond(200)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"200us\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsTimePositionOnlyWithSeconds() {
        val timedDurationSeconds = timeDurationBuilder.toTimePosition(0, 0, 2.5)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"00:02.5\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsTimePositionWithPositiveMinutes() {
        val timedDurationSeconds = timeDurationBuilder.toTimePosition(0, 10, 10.55)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"10:10.55\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsTimePositionWithNegativeMinutes() {
        val timedDurationSeconds = timeDurationBuilder.toTimePosition(0, -9, 10.55)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"-09:10.55\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsTimePositionWithPositiveHours() {
        val timedDurationSeconds = timeDurationBuilder.toTimePosition(10, 50, 10.55)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"10:50:10.55\"")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_TimeDurationIsTimePositionWithNegativeHours() {
        val timedDurationSeconds = timeDurationBuilder.toTimePosition(-10, 5, 0.0)
        val inputTimeOffsetArgument = InputTimeOffsetArgument(timedDurationSeconds)

        val parsedArgument = inputTimeOffsetArgument.parseToString()

        assertThat(parsedArgument).matches("-itsoffset \"-10:05:00\"")
    }
}<file_sep>// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply plugin: 'maven-publish'

def githubProperties = new Properties()

try {
    githubProperties.load(new FileInputStream(rootProject.file("github.properties")))
} catch(FileNotFoundException e) {
    // ignore
}

def user = githubProperties.getProperty('gpr.usr', System.getenv("GPR_USER"))
def key = githubProperties.getProperty('gpr.key', System.getenv("GPR_API_KEY"))
def versionName = "0.0.1"
def artificatId = "ffmpeg-dsl"

publishing {
    publications {
        bar(MavenPublication) {
            groupId 'ai.moises'
            artifactId artificatId
            version versionName
            artifact("$projectDir/ffmpeg-dsl/build/outputs/aar/$artificatId-release.aar")
        }
    }

    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/moises-ai/ffmpeg-dsl-android")
            credentials {
                username = user
                password = key
            }
        }
    }
}

buildscript {
    ext.kotlin_version = "1.5.0"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter() // Warning: this repository is going to shut down soon
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDurationBuilder


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */

fun seconds(second: Double): TimeDuration = TimeDurationBuilder().toSecond(second)

fun milliseconds(millisecond: Long): TimeDuration = TimeDurationBuilder().toMillisecond(millisecond)

fun nanoseconds(nanosecond: Long): TimeDuration = TimeDurationBuilder().toNanosecond(nanosecond)

fun hours(hour: Int): TimeDuration =
    TimeDurationBuilder().toTimePosition(hour, 0, 0.0)

fun minutes(minute: Int): TimeDuration =
    TimeDurationBuilder().toTimePosition(0, minute, 0.0)

fun timePosition(second: Double): TimeDuration =
    TimeDurationBuilder().toTimePosition(second)

fun timePosition(minute: Int, second: Double): TimeDuration =
    TimeDurationBuilder().toTimePosition(minute, second)

fun timePosition(hour: Int, minute: Int, second: Double): TimeDuration =
    TimeDurationBuilder().toTimePosition(hour, minute, second)<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.flag

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.KeyAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.KeyCommandArgument

abstract class Flag : KeyCommandArgument, CommandArgumentParser {
    override fun parseToString(): String {
        val trimmedKey = key.trim()

        KeyAssertion.assert(key)

        return trimmedKey
    }

    override fun toString(): String =
        parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.value

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.ValueAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.ValueCommandArgument

abstract class Value: ValueCommandArgument, CommandArgumentParser {
    override fun parseToString(): String {
        val trimmedValue = value.trim()

        ValueAssertion.assert(trimmedValue)

        return "\"${trimmedValue}\""
    }

    override fun toString(): String =
        parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier


/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 *
 * @see https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1
 */
enum class StreamType(val id: String) {
    Video("v"),
    Audio("a"),
    Subtitle("s"),
    Data("d"),
    Attachment("t")
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception

import java.util.concurrent.TimeUnit


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class InvalidTimeException(private val timeUnit: TimeUnit, val value: Number) :
    Exception("The value $value to \"${timeUnit.name}\" is not valid")<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit

import ai.moises.ffmpegdslandroid.ffmpegcommand.extension.compress


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class NanosecondTimeDurationUnit: TimeDurationUnit {
    override fun format(value: Number): String {
        return "${value.compress()}us"
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamType
import org.junit.Test

/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class FilterTagGeneratorTest {
    @Test
    fun should_GenerateAStringInTagFormat_when_TheEnterIsAStreamSpecifier() {
        val filterTagGenerator = FilterTagGenerator()

        val tagString = filterTagGenerator.generateTagFromStreamSpecifier(StreamSpecifier(1, StreamType.Audio))

        assertThat(tagString).isEqualTo("[1:a]")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.StreamSpecifierIsBlankException


/**
 * Created by <NAME> on 29/06/21.
 * github: @vnicius
 * <EMAIL>
 */
object StreamSpecifierAssertion: Assertion<String> {
    override fun assert(value: String) {
        if (value.isBlank()) throw StreamSpecifierIsBlankException()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption

import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.KeyCommandArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.ValueCommandArgument


/**
 * Created by <NAME> on 23/06/21.
 * github: @vnicius
 * <EMAIL>
 */
abstract class MixOption : KeyCommandArgument, ValueCommandArgument, CommandArgumentParser {
    override fun parseToString(): String =
        "${key.trim()}=${value.trim()}"

    override fun toString(): String =
        parseToString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration


/**
 * Created by <NAME> on 29/06/21.
 * github: @vnicius
 * <EMAIL>
 *
 * @see https://ffmpeg.org/ffmpeg.html#Main-options
 */
class InputTimeOffsetArgument(override val timeDuration: TimeDuration) :
    TimeDurationArgument("-itsoffset") {
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface Assertion<T> {
    fun assert(value: T)
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.annotation.FFmpegInitializerMarker
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter.ChannelType

@FFmpegInitializerMarker
class PanInitializer {
    var channelType: ChannelType = ChannelType.Mono
    var gains: FloatArray = floatArrayOf()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter

import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.Filter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterTagGenerator
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.TaggedFilter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption.MixOption
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier

abstract class MixFilter(
    private val filterKey: String,
    private val inputsStreamsSpecifiers: List<StreamSpecifier>? = null,
    outputStreamSpecifier: StreamSpecifier? = null
) : Filter(), TaggedFilter {
    override val key: String
        get() = generateKey()
    override val value: String
        get() = generateMixFilterAsString()
    private val mixFilters: MutableList<Filter> = mutableListOf()
    private val mixOptions: MutableList<MixOption> = mutableListOf()
    private val filterTagGenerator = FilterTagGenerator()

    override val inputTag: String = generateInputSpecifier()
    override val outputTag: String =
        filterTagGenerator.generateTagFromStreamSpecifier(outputStreamSpecifier)

    private fun generateMixFilterAsString(): String {
        val filters = mixFilters.joinToString(",") { it.parseToString() }.takeIf { it.isNotBlank() }
            ?.let { ",$it" }
        val options = mixOptions.joinToString(":") { it.parseToString() }

        return "$options$filters$outputTag"
    }

    private fun generateInputSpecifier(): String {
        return inputsStreamsSpecifiers?.joinToString("") {
            filterTagGenerator.generateTagFromStreamSpecifier(
                it
            )
        } ?: ""
    }

    private fun generateKey(): String = "$inputTag$filterKey"

    fun addFilter(filter: Filter) {
        mixFilters.add(filter)
    }

    fun addOption(mixOption: MixOption) {
        mixOptions.add(mixOption)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.annotation


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
@DslMarker
annotation class FFmpegInitializerMarker {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.annotation.FFmpegInitializerMarker
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier


/**
 * Created by <NAME> on 21/06/21.
 * github: @vnicius
 * <EMAIL>
 */
@FFmpegInitializerMarker
class CodecInitializer {
    var streamSpecifier: StreamSpecifier? = null
    var codec: String = ""
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.extension

import kotlin.math.abs


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */

fun Number.compress(): Number {
    val intPart = this.toInt()
    val compressedValue = when {
        (this is Float && this - intPart == 0f) -> intPart
        (this is Double && this - intPart == 0.0) -> intPart
        else -> this
    }

    return compressedValue
}

fun Number.formatToTime(): String {
    val absLongValue = abs(this.toLong())

    if (absLongValue >= 10) {
        return this.toString()
    }

    val absValue = when(this) {
        is Double -> abs(this)
        is Float -> abs(this)
        else -> absLongValue
    }
    val stringValue = "0$absValue"
    val signal = if (this.toInt() < 0) {
        "-"
    } else {
        ""
    }

    return signal + stringValue
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.MillisecondTimeDurationUnit
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.NanosecondTimeDurationUnit
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit.SecondTimeDurationUnit


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class TimeDurationBuilder {
    fun toSecond(second: Double): TimeDuration = SimpleTimeDuration(second, SecondTimeDurationUnit())

    fun toMillisecond(millisecond: Long): TimeDuration =
        SimpleTimeDuration(millisecond, MillisecondTimeDurationUnit())

    fun toNanosecond(nanosecond: Long): TimeDuration =
        SimpleTimeDuration(nanosecond, NanosecondTimeDurationUnit())

    fun toTimePosition(hour: Int, minute: Int, second: Double): TimeDuration =
        TimePosition(hour, minute, second)

    fun toTimePosition(minute: Int, second: Double): TimeDuration =
        TimePosition(minute, second)

    fun toTimePosition(second: Double): TimeDuration =
        TimePosition(second)
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier


/**
 * Created by <NAME> on 23/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class FilterTagGenerator {
    fun generateTagFromStreamSpecifier(streamSpecifier: StreamSpecifier?): String =
        streamSpecifier?.let { "[${it.parseToString()}]" } ?: ""
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 30/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class SecondTimeDurationUnitTest {

    private val secondTimeDurationUnit = SecondTimeDurationUnit()

    @Test
    fun should_CorrectlyFormatTheValueToSecondsInString_when_ValueIsAPositiveValue() {
        val secondsInString = secondTimeDurationUnit.format(2f)

        assertThat(secondsInString).matches("2.0")
    }

    @Test
    fun should_CorrectlyFormatTheValueToSecondsInString_when_ValueIsANegativeValue() {
        val secondsInString = secondTimeDurationUnit.format(-10.5f)

        assertThat(secondsInString).matches("-10.5")
    }

    @Test
    fun should_KeepAllDecimalPartOnFormattedString_when_ValueIsALongDecimalValue() {
        val secondsInString = secondTimeDurationUnit.format(0.5123123123123)

        assertThat(secondsInString).matches("0.5123123123123")
    }
}<file_sep>package ai.moises.ffmpegdslandroidexample

import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import ai.moises.ffmpegdslandroid.ffmpegcommand.ffmpegCommand
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterGroup
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter.ChannelType
import ai.moises.ffmpegdslandroid.ffmpegcommand.initializer.minutes
import ai.moises.ffmpegdslandroid.ffmpegcommand.initializer.seconds
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamType
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.Duration

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val command = ffmpegCommand {
            isOverrideAllowed = true

            input {
                seekStart = minutes(2)
                path = "test.mp3"
                inputTimeOffset = seconds(0.3102834467120186)
            }

            input {
                seekStart = minutes(2)
                path = "test_2.mp3"
            }

            filterComplex {
                val filterGroups = mutableListOf<FilterGroup>()

                filterGroup(
                    StreamSpecifier(0, StreamType.Audio),
                    StreamSpecifier(StreamType.Audio, 0)
                ) {
                    volume = 1.2f
                    pan {
                        channelType = ChannelType.Stereo
                        gains = floatArrayOf(1f, 0f)
                    }
                }.also(filterGroups::add)

                filterGroup(
                    StreamSpecifier(1, StreamType.Audio),
                    StreamSpecifier(StreamType.Audio, 1)
                ) {
                    volume = 1.5f
                    pan {
                        channelType = ChannelType.Stereo
                        gains = floatArrayOf(0f, 1f)
                    }
                }.also(filterGroups::add)

                amix(
                    filterGroups.mapNotNull { it.outputStreamSpecifier },
                    StreamSpecifier(StreamType.Audio, 3)
                ) {
                    inputs = 2
                    duration = Duration.Longest
                    dropoutTransition = 2
                    volume = 2
                }
            }

            map(tag(StreamSpecifier(StreamType.Audio, 3)))

            outputPath = "out.mp3"
        }

        findViewById<TextView>(R.id.text_view).text = command

        Log.d("command", command)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.annotation.FFmpegInitializerMarker
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.FilterComplexArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterGroup
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.AudioMixFilter
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier

@FFmpegInitializerMarker
class FilterComplexInitializer(private val filterComplexArgument: FilterComplexArgument) {

    fun filterGroup(
        inputStreamSpecifier: StreamSpecifier? = null,
        outputStreamSpecifier: StreamSpecifier? = null,
        initializer: FilterGroupInitializer.() -> Unit
    ): FilterGroup {
        val filterGroup = FilterGroup(inputStreamSpecifier, outputStreamSpecifier)

        FilterGroupInitializer(filterGroup).apply(initializer)

        filterComplexArgument.addFilterGroup(filterGroup)

        return filterGroup
    }

    fun amix(
        inputsStreamsSpecifiers: List<StreamSpecifier>? = null,
        outputStreamSpecifier: StreamSpecifier? = null,
        initializer: AudioMixInitializer.() -> Unit
    ): AudioMixFilter {
        val audioMixFilter = AudioMixFilter(inputsStreamsSpecifiers, outputStreamSpecifier)

        AudioMixInitializer(audioMixFilter).apply(initializer)

        filterComplexArgument.addFilter(audioMixFilter)

        return audioMixFilter
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.PathAssertion


class InputArgument(path: String): Argument() {
    override val key: String = "-i"
    override val value: String = path

    init {
        PathAssertion.assert(path)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.CodecArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.FilterComplexArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.MapArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandBuilder
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterTagGenerator
import ai.moises.ffmpegdslandroid.ffmpegcommand.flag.OverrideAllowedFlag
import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier
import ai.moises.ffmpegdslandroid.ffmpegcommand.value.OutputPathValue

class FFmpegCommandInitializer(private val commandBuilder: CommandBuilder) {
    var isOverrideAllowed: Boolean? = null
        set(value) {
            field = value
            value?.let(::addIsOverrideAllowed)
        }
    var outputPath: String? = null
        set(value) {
            field = value
            value?.let(::addOutputPathValue)
        }

    private fun addIsOverrideAllowed(isAllowed: Boolean) {
        commandBuilder.addArgument(OverrideAllowedFlag(isAllowed))
    }

    private fun addOutputPathValue(path: String) {
        commandBuilder.addArgument(OutputPathValue(path))
    }

    private fun addMapArgument(streamSpecifier: StreamSpecifier) {
        commandBuilder.addArgument(MapArgument(streamSpecifier))
    }

    fun input(initializer: InputCommandInitializer.() -> Unit): List<CommandArgumentParser> {
        val destinationList = mutableListOf<CommandArgumentParser>()

        InputCommandInitializer(destinationList).apply(initializer)

        destinationList.forEach {
            commandBuilder.addArgument(it)
        }

        return destinationList
    }

    fun filterComplex(initializer: FilterComplexInitializer.() -> Unit): FilterComplexArgument {
        val filterComplexArgument = FilterComplexArgument()

        FilterComplexInitializer(filterComplexArgument).apply(initializer)

        commandBuilder.addArgument(filterComplexArgument)

        return filterComplexArgument
    }

    fun codec(initializer: CodecInitializer.() -> Unit): CodecArgument {
        val codecInitializer = CodecInitializer().apply(initializer)
        val codecArgument = CodecArgument(codecInitializer.codec, codecInitializer.streamSpecifier)

        commandBuilder.addArgument(codecArgument)

        return codecArgument
    }

    fun tag(streamSpecifier: StreamSpecifier): String =
        FilterTagGenerator().generateTagFromStreamSpecifier(streamSpecifier)

    fun map(streamSpecifier: StreamSpecifier): MapArgument {
        val mapArgument = MapArgument(streamSpecifier)

        commandBuilder.addArgument(mapArgument)

        return mapArgument;
    }

    fun map(streamSpecifier: String): MapArgument {
        val mapArgument = MapArgument(streamSpecifier)

        commandBuilder.addArgument(mapArgument)

        return mapArgument;
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidGainListSize


/**
 * Created by <NAME> on 06/07/21.
 * github: @vnicius
 * <EMAIL>
 */
object GainAssertion: Assertion<FloatArray> {
    override fun assert(value: FloatArray) {
        if (value.isEmpty()) throw InvalidGainListSize()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 21/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class CodecNotSupportedException(codec: String): Exception("The codec '$codec' is not supported")<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.KeyAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.ValueAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.KeyCommandArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.ValueCommandArgument

abstract class Argument : KeyCommandArgument, ValueCommandArgument, CommandArgumentParser {
    override fun parseToString(): String {
        KeyAssertion.assert(key)
        ValueAssertion.assert(value)

        return "$key \"$value\"".trim()
    }

    override fun toString(): String {
        return parseToString()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidVolumeException
import org.junit.Test

/**
 * Created by <NAME> on 05/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class VolumeFilterTest {

    @Test
    fun should_CorrectlyParseTheVolumeFilterToString_when_TheValueIsAPositiveNumber() {
        val volumeFilter = VolumeFilter(10)

        val parsedString = volumeFilter.toString()

        assertThat(parsedString).isEqualTo("volume=10")
    }

    @Test(expected = InvalidVolumeException::class)
    fun should_Fail_when_ValueIsANegativeNumber() {
        VolumeFilter(-5)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand

import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandBuilder
import ai.moises.ffmpegdslandroid.ffmpegcommand.initializer.FFmpegCommandInitializer

fun ffmpegCommand(initializer: FFmpegCommandInitializer.() -> Unit): String {
    val commandBuilder = CommandBuilder()

    FFmpegCommandInitializer(commandBuilder).apply(initializer)

    return commandBuilder.build()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 29/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class StreamSpecifierIsBlankException: Exception("The 'stream specifier' cannot be blank") {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class ValueIsBlankException : Exception("The 'value' cannot be blank")<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidGainListSize
import org.junit.Test

/**
 * Created by <NAME> on 06/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class StereoPanFilterTest {

    @Test
    fun should_CorrectlyParseStereoPanToString_when_GainHasASingleItem() {
        val monoPanFilter = StereoPanFilter(floatArrayOf(1f))

        val parsedString = monoPanFilter.parseToString()

        assertThat(parsedString).isEqualTo("pan=stereo|c0<1.0*c0")
    }

    @Test
    fun should_CorrectlyParseStereoPanToString_when_GainHasMultipleItems() {
        val monoPanFilter = StereoPanFilter(floatArrayOf(1f, 2f))

        val parsedString = monoPanFilter.parseToString()

        assertThat(parsedString).isEqualTo("pan=stereo|c0<1.0*c0|c1<2.0*c1")
    }

    @Test(expected = InvalidGainListSize::class)
    fun should_Fail_when_GainListIsEmpty() {
        StereoPanFilter(floatArrayOf())
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter

import ai.moises.ffmpegdslandroid.ffmpegcommand.streamspecifier.StreamSpecifier

class AudioMixFilter(
    inputsStreamsSpecifiers: List<StreamSpecifier>? = null,
    outputStreamSpecifier: StreamSpecifier? = null
) : MixFilter("amix", inputsStreamsSpecifiers, outputStreamSpecifier) {
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption

class DropoutTransitionOption(dropoutTransition: Int): MixOption() {
    override val key: String = "dropout_transition"
    override val value: String = dropoutTransition.toString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 01/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class AudioSampleRateFilterTest {
    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnIntegerValue() {
        val audioSampleRateFilter = AudioSampleRateFilter(44100)

        val parsedFilter = audioSampleRateFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("asetrate=44100")
    }

    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnFloatValue() {
        val audioSampleRateFilter = AudioSampleRateFilter(100f)

        val parsedFilter = audioSampleRateFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("asetrate=100.0")
    }

    @Test
    fun should_CorrectlyParseTheArgument_when_DefinedASampleRateIsAnDoubleValue() {
        val audioSampleRateFilter = AudioSampleRateFilter(200.0)

        val parsedFilter = audioSampleRateFilter.parseToString()

        assertThat(parsedFilter).isEqualTo("asetrate=200.0")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 30/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class MillisecondTimeDurationUnitTest {
    private val millisecondTimeDurationUnit = MillisecondTimeDurationUnit()

    @Test
    fun should_CorrectlyFormatTheValueToMillisecondsInString_when_ValueIsAPositiveValue() {
        val secondsInString = millisecondTimeDurationUnit.format(2)

        assertThat(secondsInString).matches("2ms")
    }

    @Test
    fun should_CorrectlyFormatTheValueToMillisecondsInString_when_ValueIsANegativeValue() {
        val secondsInString = millisecondTimeDurationUnit.format(-10)

        assertThat(secondsInString).matches("-10ms")
    }

    @Test
    fun should_CorrectlyFormatTheValueToMillisecondsInString_when_ValueIsADecimalNumber() {
        val secondsInString = millisecondTimeDurationUnit.format(20.5f)

        assertThat(secondsInString).matches("20.5ms")
    }

    @Test
    fun should_KeepAllDecimalPartOnFormattedString_when_ValueIsALongDecimalNumber() {
        val secondsInString = millisecondTimeDurationUnit.format(0.5123123123123)

        assertThat(secondsInString).matches("0.5123123123123ms")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.timedurationunit


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class SecondTimeDurationUnit: TimeDurationUnit {
    override fun format(value: Number): String {
        return value.toString()
    }
}<file_sep><div align="center">
    <h1>FFmpeg DSL Android</h1>
</div>

Pure Kotlin DSL library to build FFmpeg CLI commands in a more descriptive and comprehensive way

[![Latest Version](https://img.shields.io/badge/Latest_Version-0.1-green.svg)](https://github.com/orgs/moises-ai/packages?repo_name=ffmpeg-dsl-android)

---

## Summary

## Getting Start

Steps explain how the add the `FFmpeg DSL` library to your Android project.

Take [this post](https://ppulikal.medium.com/publishing-android-libraries-to-the-github-package-registry-part-2-3c5aab31f477)
as a reference to the following steps.

### 1 - Create the `github.properties` file on your root project directory

```
// github.properties

gpr.usr=00000 // Your Github User ID
gpr.key=xxxx // Your Github Personal Access Token with the permission to read packages
```

- **Alternative**: Set some environment variables with the credentials, something like `GPR_USER` and `GPR_API_KEY`.

\*_see the [github.properties.example](./github.properties.example) file_

\*_[how to get the Github User Id](https://stackoverflow.com/questions/17308954/where-can-i-find-the-github-id-in-my-account#:~:text=Up%20vote%202-,It%20can%20be%20easily%20retrieved%20using%20Github%20API%20.,.noreply.github.com%20.)_

### 2 - Add the repository to the `bundle.gradle` file

At the top of your `bundle.gradle` file, add the code to read the `github.properties`
content

```gradle
def githubProperties = new Properties()

try {
    githubProperties.load(new FileInputStream(rootProject.file("github.properties")))
} catch(FileNotFoundException e) {
    // ignore
}

def user = githubProperties.getProperty('gpr.usr', System.getenv("GPR_USER"))
def key = githubProperties.getProperty('gpr.key', System.getenv("GPR_API_KEY"))
```

At the `allprojects` block, add a custom Maven repository

```gradle
allprojects {
    repositories {
        ...
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/moises-ai/ffmpeg-dsl-android")
            credentials {
                username = user
                password = key
            }
        }
    }
}
```

### 3 - Add the library dependency to your `app/bundle.gradle`

```gradle
implementation 'ai.moises:ffmpeg-dsl:[LATEST_VERSION]'
```

## Publishing a new version

Steps explain how to manually publish a new version of the `FFmpeg DSL` library
to the Github Package Registry.

Take [this post](https://proandroiddev.com/publishing-android-libraries-to-the-github-package-registry-part-1-7997be54ea5a)
as a reference to the following steps.

### 1 - Create the `github.properties` file on your root project directory

```
// github.properties

gpr.usr=00000 // Your Github User ID
gpr.key=xxxx // Your Github Personal Access Token with the permission to read packages
```

- **Alternative**: Set some environment variables with the credentials, something like `GPR_USER` and `GPR_API_KEY`.

\*_see the [github.properties.example](./github.properties.example) file_

\*_[how to get the Github User Id](https://stackoverflow.com/questions/17308954/where-can-i-find-the-github-id-in-my-account#:~:text=Up%20vote%202-,It%20can%20be%20easily%20retrieved%20using%20Github%20API%20.,.noreply.github.com%20.)_

### 2 - Update the version name

On the `build.gradle` file, update the `versionName` variable with the new version.

### 3 - Publishing

For Windows and Linux, run the flowing scripts under the root directory of the project.

```sh
$ gradle clean
$ gradle assemble
$ gradle publish
```

For MacOS, run the flowing scripts under the root directory of the project.

```sh
$ ./gradlew clean
$ ./gradlew assemble
$ ./gradlew publish
```

## Basic Usage

```kotlin
    ffmpegCommand {
        isOverrideAllowed = true

        input {
            seekStart = minutes(2)
            inputTimeOffset = seconds(0.3102834467120186)
            path = "test.mp3"
        }

        outputPath = "out.mp3"
    }

    // result: -y -ss "02:00" -itoffset "0.3102834467120186" -i "test.mp3" "out.mp3"
```

See the Example app for more usage examples.

## 👀 Check the Wiki for available commands and DSL syntax
<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption

class InputsOption(inputsSize: Int): MixOption() {
    override val key: String = "inputs"
    override val value: String = inputsSize.toString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidTimeException
import java.util.concurrent.TimeUnit
import kotlin.math.abs


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
abstract class TimeAssertion<T> : Assertion<T> {
    object MinutesAssertion: TimeAssertion<Int>() {
        override fun assert(value: Int) {
            if (value !in -59..59) {
                throw InvalidTimeException(TimeUnit.MINUTES, value)
            }
        }
    }

    object SecondsAssertion: TimeAssertion<Double>() {
        override fun assert(value: Double) {
            if(abs(value.toInt()) >= 60) {
                throw InvalidTimeException(TimeUnit.SECONDS, value)
            }
        }
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.initializer

import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.InputArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.InputTimeOffsetArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.SeekStartArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.argument.TimeArgument
import ai.moises.ffmpegdslandroid.ffmpegcommand.command.CommandArgumentParser
import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration

class InputCommandInitializer(private val destination: MutableList<CommandArgumentParser>) {
    var path: String = ""
        set(value) {
            field = value
            addInputArgument(path)
        }
    var seekStart: TimeDuration? = null
        set(value) {
            field = value
            value?.let(::addSeekStartArgument)
        }
    var duration: TimeDuration? = null
        set(value) {
            field = value
            value?.let(::addTimeArgument)
        }
    var inputTimeOffset: TimeDuration? = null
        set(value) {
            field = value
            value?.let(::addInputTimeOffsetArgument)
        }

    private fun addInputArgument(path: String) {
        destination.add(InputArgument(path))
    }

    private fun addSeekStartArgument(timeDuration: TimeDuration) {
        destination.add(SeekStartArgument(timeDuration))
    }

    private fun addTimeArgument(timeDuration: TimeDuration) {
        destination.add(TimeArgument(timeDuration))
    }

    private fun addInputTimeOffsetArgument(timeDuration: TimeDuration) {
        destination.add(InputTimeOffsetArgument(timeDuration))
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.panfilter

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.GainAssertion

class StereoPanFilter(gains: FloatArray) : PanFilter(ChannelType.Stereo) {
    init {
        GainAssertion.assert(gains)
        setupChannelsGain(gains)
    }

    private fun setupChannelsGain(gains: FloatArray) {
        val channelsConfigs = gains.mapIndexed { index, value ->
            "c$index<$value*c$index"
        }

        channelsConfigs.forEach(::addChannelConfig)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.KeyIsBlankException


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
object KeyAssertion: Assertion<String> {
    override fun assert(value: String) {
        if (value.isBlank()) throw KeyIsBlankException()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.value

class OutputPathValue(path: String) : Value() {
    override val value: String = path
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter


/**
 * Created by <NAME> on 23/06/21.
 * github: @vnicius
 * <EMAIL>
 */
interface TaggedFilter {
    val inputTag: String
    val outputTag: String
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.PathIsBlankException
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
@RunWith(JUnit4::class)
class InputArgumentTest {

    @Test
    fun should_MatchesTheInputPattern_when_ParseTheContentToString() {
        val inputArgument = InputArgument("a")

        val parsedArgument = inputArgument.parseToString()

        assertThat(parsedArgument).matches("-i \"a\"")
    }

    @Test(expected = PathIsBlankException::class)
    fun should_Fail_when_PathIsEmpty() {
        InputArgument("")
    }

    @Test(expected = PathIsBlankException::class)
    fun should_Fail_when_PathIsBlank() {
        InputArgument("     ")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.PathIsBlankException


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
object PathAssertion: Assertion<String> {
    override fun assert(value: String) {
        if (value.isBlank()) throw PathIsBlankException()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.exception


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class PathIsBlankException : Exception("The 'path' cannot be blank") {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.TimeAssertion
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.InvalidTimeException
import ai.moises.ffmpegdslandroid.ffmpegcommand.extension.compress
import ai.moises.ffmpegdslandroid.ffmpegcommand.extension.formatToTime
import java.util.concurrent.TimeUnit
import kotlin.math.abs


/**
 * Created by <NAME> on 28/06/21.
 * github: @vnicius
 * <EMAIL>
 */
class TimePosition private constructor(): TimeDuration {

    private var timeDurationString: String = ""

    constructor(hour: Int, minute: Int, second: Double) : this() {
        timeDurationString = parseTime(hour, minute, second)
    }

    constructor(minute: Int, second: Double): this() {
        timeDurationString = parseTime(0, minute, second)
    }

    constructor(second: Double): this() {
        timeDurationString = parseTime(0, 0, second)
    }

    private fun parseTime(hour: Int, minute: Int, second: Double): String {
        val timeStringBuilder = StringBuilder()

        appendHour(timeStringBuilder, hour)
        assertMinute(hour, minute)
        appendMinute(timeStringBuilder, minute)
        assertSecond(hour, minute, second)
        appendSecond(timeStringBuilder, second)

        return timeStringBuilder.toString()
    }

    private fun assertMinute(hour: Int, minute: Int) {
        if (hour != 0 && minute < 0) {
            throw InvalidTimeException(TimeUnit.MINUTES, minute)
        }

        TimeAssertion.MinutesAssertion.assert(minute)
    }

    private fun assertSecond(hour: Int, minute: Int, second: Double) {
        if ((hour != 0 || minute != 0) && second < 0) {
            throw InvalidTimeException(TimeUnit.SECONDS, second)
        }

        TimeAssertion.SecondsAssertion.assert(second)
    }

    private fun appendHour(stringBuilder: StringBuilder, hour: Int) {
        if (hour != 0) {
            stringBuilder.append(hour.formatToTime()).append(":")
        }
    }

    private fun appendMinute(stringBuilder: StringBuilder, minute: Int) {
        stringBuilder.append(minute.formatToTime()).append(":")
    }

    private fun appendSecond(stringBuilder: StringBuilder, second: Double) {
        val absSecond = abs(second)

        if (second < 0) {
            stringBuilder.insert(0, "-")
        }

        stringBuilder.append(absSecond.compress().formatToTime())
    }

    override fun format(): String = timeDurationString
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.Filter
import ai.moises.ffmpegdslandroid.ffmpegcommand.filter.FilterGroup

class FilterComplexArgument: Argument() {
    override val key: String = "-filter_complex"
    override val value: String
        get() = getContentAsString()

    private var filtersGroups: MutableList<FilterGroup> = mutableListOf()
    private val filters: MutableList<Filter> = mutableListOf()

    private fun getFilterGroupsAsString(): String {
        return filtersGroups.joinToString("") { filterGroup ->
            filterGroup.parseToString()
        }
    }

    private fun getFiltersAsString(): String {
        return filters.joinToString(",") { it.toString() }
    }

    private fun getContentAsString(): String {
        return getFilterGroupsAsString() + getFiltersAsString()
    }

    fun addFilterGroup(filterGroup: FilterGroup) {
        filtersGroups.add(filterGroup)
    }

    fun addFilter(filter: Filter) {
        filters.add(filter)
    }

    fun getFilterGroups(): List<FilterGroup> = filtersGroups
}<file_sep>rootProject.name = "FFmpegDSLAndroidExample"
include ':app'
include ':ffmpeg-dsl'
<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.CodecNotSupportedException


/**
 * Created by <NAME> on 21/06/21.
 * github: @vnicius
 * <EMAIL>
 */
object CodecAssertion: Assertion<String> {
    override fun assert(value: String) {
        if (value.isBlank()) throw CodecNotSupportedException(value)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

class AudioResampleFilter(sampleRate: Number) : Filter() {
    override val key: String = "aresample"
    override val value: String = sampleRate.toString()
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.TimeDuration

class SeekStartArgument(override val timeDuration: TimeDuration) : TimeDurationArgument("-ss") {}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter.mixfilter.mixoption

import ai.moises.ffmpegdslandroid.ffmpegcommand.timeduration.Duration

class DurationOption(duration: Duration): MixOption() {
    override val key: String = "duration"
    override val value: String = duration.value
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.assertion

import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.ValueIsBlankException


/**
 * Created by <NAME> on 19/06/21.
 * github: @vnicius
 * <EMAIL>
 */
object ValueAssertion: Assertion<String> {
    override fun assert(value: String) {
        if (value.isBlank()) throw ValueIsBlankException()
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.argument

import com.google.common.truth.Truth.assertThat
import ai.moises.ffmpegdslandroid.ffmpegcommand.exception.StreamSpecifierIsBlankException
import org.junit.Test

/**
 * Created by <NAME> on 29/06/21.
 * github: @vnicius
 * <EMAIL>
 */

class MapArgumentTest {

    @Test(expected = StreamSpecifierIsBlankException::class)
    fun should_Fail_when_StreamSpecifierIsBlank() {
        MapArgument("")
    }

    @Test
    fun should_ParseArgumentCorrectly_when_StreamSpecifierIsNotBlank() {
        val mapArgument = MapArgument("a")

        val parsedArgument = mapArgument.parseToString()

        assertThat(parsedArgument).matches("-map \"a\"")
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.filter

import ai.moises.ffmpegdslandroid.ffmpegcommand.assertion.VolumeAssertion

class VolumeFilter(volume: Number): Filter() {
    override val key: String = "volume"
    override val value: String = volume.toString()

    init {
        VolumeAssertion.assert(volume)
    }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.command


open class CommandBuilder {
    private val arguments: MutableList<CommandArgumentParser> = mutableListOf()

    fun addArgument(argument: CommandArgumentParser): CommandBuilder {
        arguments.add(argument)
        return this
    }

    fun build(): String =
        arguments.joinToString(" ") {
            it.parseToString()
        }
}<file_sep>package ai.moises.ffmpegdslandroid.ffmpegcommand.flag

import com.google.common.truth.Truth.assertThat
import org.junit.Test

/**
 * Created by <NAME> on 01/07/21.
 * github: @vnicius
 * <EMAIL>
 */
class OverrideAllowedFlagTest {

    @Test
    fun should_ParsePositiveFlag_when_OverrideValueIsTrue() {
        val overrideAllowedFlag = OverrideAllowedFlag(true)

        val parsedFlag = overrideAllowedFlag.parseToString()

        assertThat(parsedFlag).isEqualTo("-y")
    }

    @Test
    fun should_ParseNegativeFlag_when_OverrideValueIsFalse() {
        val overrideAllowedFlag = OverrideAllowedFlag(false)

        val parsedFlag = overrideAllowedFlag.parseToString()

        assertThat(parsedFlag).isEqualTo("-n")
    }
}