ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AWS] MediaConvert createJob Kotlin SDK 적용
    AWS 2022. 12. 24. 00:01

    [1] [AWS] MediaConvert란? + 튜토리얼

    [2] [AWS] AWS MediaConvert createJob Kotlin SDK 적용

    [3] [AWS] AWS MediaConvert Jobtemplate Kotlin SDK 적용

    개요

    위의 예제에서 AWS Console을 통해 MediaConvert의 job을 생성하는 작업을 수행하였습니다.

    이번에는 Spring, Kotlin과 AWS MediaConvert SDK를 활용하여 job을 생성해보도록 하겠습니다.

     

    AWS Kotlin MediaConvert SDK 문서를 기반으로 작성해보겠습니다

    mediaconvert build.gradle.kts 링크

    mediaconvert CreateJob.kt 링크

     

     

    Dependency 추가

    dependencies {
        implementation("aws.sdk.kotlin:mediaconvert:0.17.1-beta")
        testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2")
    }

    문서에서는 3개의 의존성을 추가하고 있습니다.

     

    또한 CreateJob.kt 문서에는 suspend를 활용하기 때문에 coroutines의존성을 추가하였습니다.

     

    여기서 mediaconvert의 경우에는 0.17.1-beta 버전입니다.

     

    https://mvnrepository.com/artifact/aws.sdk.kotlin/mediaconvert

     

    메이븐 저장소에서 aws.sdk.kotlin:mediaconvert의 버전들을 살펴보았습니다.

    그림 1

    [그림 1]을 통해 aws.sdk.kotlin:mediaconvert의 다양한 버전들을 볼 수 있습니다.

    거의 다 beta가 붙어있고 0.16.0의 경우에는 붙어있지 않습니다.

    따라서 0.16.0 버전을 활용하고자 합니다.

     

    CreateJob.kt

    코드가 이미 작성되어 있고 suspend fun으로 함수 구성이 되어있습니다.

     

    코루틴은 기본적으로 일시 중단이 가능합니다.

     

    [그림 2]는 코루틴이 일시 중단 가능한 예시를 보여줍니다.

    그림 2

     

    코루틴이 일시 중단이 되려면 수행되는 위치 또한 코루틴 내부여야 하기 때문에 일시 중단 작업을 코루틴 내부로 옮기거나, suspend fun을 활용해야 합니다.

     

    따라서 Controller 코드에 suspend가 붙게 됩니다.

    @PostMapping("/job")
    suspend fun createJob(){
    	return jobService.createMediaConvertJob()
    }

    URL으로 /job 이 호출되면 job을 만들도록 구성하였습니다.

    suspend를 제거하려고 해도 내부적으로 코루틴을 사용하기 때문에 불가능합니다.

     

    과연 Controller에 suspend를 사용해도 될까?

    코루틴은 non-blocking 으로 동작하는 Spring WebFlux에서 지원됩니다.

    Spring WebFlux에서 컨트롤러 메소드는 suspend로 선언될 수 있습니다.

     

    example WebFlux with coroutines code 

    @GetMapping("/foo")
    suspend fun foo: X = coroutineScope{
        val x = async { restClient.getForObject<Int>("/bla1") }
        val y = async { restClient.getForObject<Int>("/bla2") }
        x.await() + y.await()
    }

     

    Spring Boot에서는 컨트롤러를 suspending으로 선언할 수 없지만 runBlocking을 사용할 수 있습니다.

     

    example Spring Boot with coroutines codre

    @GetMapping("/foo")
    fun foo(): Int = runBlocking {
        val x = async { restClient.getForObject<Int>("/bla1") }
        val y = async { restClient.getForObject<Int>("/bla2") }
        x.await() + y.await()
    }
    Spring Boot는 요청당 하나의 스레드 모델을 구현하므로 스레드는 어쨋든 차단됩니다.

    그래서 기존의 코드를 어떻게 처리하였는지는 아래에서 다루겠습니다

     

    다음은 Service 코드입니다.

    @Service
    class JobService {
        suspend fun createMediaConvertJob(){
            val usage = """
            Usage
                <mcRoleARN> <fileInput> 
    
            Where:
                mcRoleARN - the MediaConvert Role ARN.
                fileInput -  the URL of an Amazon S3 bucket where the input file is located.
            """
    
            val mcRoleARN = "미디어 컨버터의 iam role을 넣어주세요(arn:aws:iam::xxxxxxxxxx:role/MediaConvert-common)"
            val fileInput = "mediaconvert에서 사용될 s3 input file을 넣어주세요(s3://your_bucket/your_path/somefile.mov)"
            val mcClient = MediaConvertClient { region = "aws 리전을 넣어주세요(ap-northeast-2)"}
            val id = createMediaJob(mcClient, mcRoleARN, fileInput)
            println("MediaConvert job is $id")
        }
    }
    
    suspend fun createMediaJob(mcClient: MediaConvertClient, mcRoleARN: String, fileInputVal: String): String? {
        val s3path = fileInputVal.substring(0, fileInputVal.lastIndexOf('/') + 1) + "kotlin-sdk/out/"
        val fileOutput = s3path + "index"
        val thumbsOutput = s3path + "thumbs/"
        val mp4Output = s3path + "mp4/"
    
        try {
            val describeEndpoints = DescribeEndpointsRequest {
                maxResults = 20
            }
    
            val res = mcClient.describeEndpoints(describeEndpoints)
    
            if (res.endpoints?.size!! <= 0) {
                println("Cannot find MediaConvert service endpoint URL!")
                exitProcess(0)
            }
            val endpointURL = res.endpoints!![0].url!!
            val mediaConvertClient = MediaConvertClient {
                region = "ap-northeast-2"
                endpointResolver = AwsEndpointResolver { service, region ->
                    AwsEndpoint(endpointURL, CredentialScope(region = region))
                }
            }
    
            // output group Preset HLS low profile
            val hlsLow = createOutput("hls_low", "_low", "_\$dt$", 750000, 7, 1920, 1080, 640)
    
            // output group Preset HLS medium profile
            val hlsMedium = createOutput("hls_medium", "_medium", "_\$dt$", 1200000, 7, 1920, 1080, 1280)
    
            // output group Preset HLS high profole
            val hlsHigh = createOutput("hls_high", "_high", "_\$dt$", 3500000, 8, 1920, 1080, 1920)
    
            val outputSettings = OutputGroupSettings {
                type = OutputGroupType.HlsGroupSettings
            }
    
            val OutputObsList: MutableList<Output> = mutableListOf()
            if (hlsLow != null) {
                OutputObsList.add(hlsLow)
            }
            if (hlsMedium != null) {
                OutputObsList.add(hlsMedium)
            }
            if (hlsHigh != null) {
                OutputObsList.add(hlsHigh)
            }
    
            // Create an OutputGroup object.
            val appleHLS = OutputGroup {
                name = "Apple HLS"
                customName = "Example"
                outputGroupSettings = OutputGroupSettings {
                    type = OutputGroupType.HlsGroupSettings
                    this.hlsGroupSettings = HlsGroupSettings {
                        directoryStructure = HlsDirectoryStructure.SingleDirectory
                        manifestDurationFormat = HlsManifestDurationFormat.Integer
                        streamInfResolution = HlsStreamInfResolution.Include
                        clientCache = HlsClientCache.Enabled
                        captionLanguageSetting = HlsCaptionLanguageSetting.Omit
                        manifestCompression = HlsManifestCompression.None
                        codecSpecification = HlsCodecSpecification.Rfc4281
                        outputSelection = HlsOutputSelection.ManifestsAndSegments
                        programDateTime = HlsProgramDateTime.Exclude
                        programDateTimePeriod = 600
                        timedMetadataId3Frame = HlsTimedMetadataId3Frame.Priv
                        timedMetadataId3Period = 10
                        destination = fileOutput
                        segmentControl = HlsSegmentControl.SegmentedFiles
                        minFinalSegmentLength = 0.toDouble()
                        segmentLength = 4
                        minSegmentLength = 1
                    }
                }
                outputs = OutputObsList
            }
    
            val theOutput = Output {
                extension = "mp4"
                containerSettings = ContainerSettings {
                    container = ContainerType.fromValue("MP4")
                }
    
                videoDescription = VideoDescription {
                    width = 1280
                    height = 720
                    scalingBehavior = ScalingBehavior.Default
                    sharpness = 50
                    antiAlias = AntiAlias.Enabled
                    timecodeInsertion = VideoTimecodeInsertion.Disabled
                    colorMetadata = ColorMetadata.Insert
                    respondToAfd = RespondToAfd.None
                    afdSignaling = AfdSignaling.None
                    dropFrameTimecode = DropFrameTimecode.Enabled
                    codecSettings = VideoCodecSettings {
                        codec = VideoCodec.H264
                        h264Settings = H264Settings {
                            rateControlMode = H264RateControlMode.Qvbr
                            parControl = H264ParControl.InitializeFromSource
                            qualityTuningLevel = H264QualityTuningLevel.SinglePass
                            qvbrSettings = H264QvbrSettings { qvbrQualityLevel = 8 }
                            codecLevel = H264CodecLevel.Auto
                            codecProfile = H264CodecProfile.Main
                            maxBitrate = 2400000
                            framerateControl = H264FramerateControl.InitializeFromSource
                            gopSize = 2.0
                            gopSizeUnits = H264GopSizeUnits.Seconds
                            numberBFramesBetweenReferenceFrames = 2
                            gopClosedCadence = 1
                            gopBReference = H264GopBReference.Disabled
                            slowPal = H264SlowPal.Disabled
                            syntax = H264Syntax.Default
                            numberReferenceFrames = 3
                            dynamicSubGop = H264DynamicSubGop.Static
                            fieldEncoding = H264FieldEncoding.Paff
                            sceneChangeDetect = H264SceneChangeDetect.Enabled
                            minIInterval = 0
                            telecine = H264Telecine.None
                            framerateConversionAlgorithm = H264FramerateConversionAlgorithm.DuplicateDrop
                            entropyEncoding = H264EntropyEncoding.Cabac
                            slices = 1
                            unregisteredSeiTimecode = H264UnregisteredSeiTimecode.Disabled
                            repeatPps = H264RepeatPps.Disabled
                            adaptiveQuantization = H264AdaptiveQuantization.High
                            spatialAdaptiveQuantization = H264SpatialAdaptiveQuantization.Enabled
                            temporalAdaptiveQuantization = H264TemporalAdaptiveQuantization.Enabled
                            flickerAdaptiveQuantization = H264FlickerAdaptiveQuantization.Disabled
                            softness = 0
                            interlaceMode = H264InterlaceMode.Progressive
                        }
                    }
                }
    
                audioDescriptions = listOf(
                    AudioDescription {
                        audioTypeControl = AudioTypeControl.FollowInput
                        languageCodeControl = AudioLanguageCodeControl.FollowInput
                        codecSettings = AudioCodecSettings {
                            codec = AudioCodec.Aac
                            aacSettings = AacSettings {
                                codecProfile = AacCodecProfile.Lc
                                rateControlMode = AacRateControlMode.Cbr
                                codingMode = AacCodingMode.CodingMode2_0
                                sampleRate = 44100
                                bitrate = 160000
                                rawFormat = AacRawFormat.None
                                specification = AacSpecification.Mpeg4
                                audioDescriptionBroadcasterMix = AacAudioDescriptionBroadcasterMix.Normal
                            }
                        }
                    }
                )
            }
    
            // Create an OutputGroup
            val fileMp4 = OutputGroup {
                name = "File Group"
                customName = "mp4"
                outputGroupSettings = OutputGroupSettings {
                    type = OutputGroupType.FileGroupSettings
                    fileGroupSettings = FileGroupSettings {
                        destination = mp4Output
                    }
                }
                outputs = listOf(theOutput)
            }
    
            val containerSettings1 = ContainerSettings {
                container = ContainerType.Raw
            }
    
            val thumbs = OutputGroup {
                name = "File Group"
                customName = "thumbs"
                outputGroupSettings = OutputGroupSettings {
                    type = OutputGroupType.FileGroupSettings
                    fileGroupSettings = FileGroupSettings {
                        destination = thumbsOutput
                    }
                }
    
                outputs = listOf(
                    Output {
                        extension = "jpg"
    
                        this.containerSettings = containerSettings1
                        videoDescription = VideoDescription {
                            scalingBehavior = ScalingBehavior.Default
                            sharpness = 50
                            antiAlias = AntiAlias.Enabled
                            timecodeInsertion = VideoTimecodeInsertion.Disabled
                            colorMetadata = ColorMetadata.Insert
                            dropFrameTimecode = DropFrameTimecode.Enabled
                            codecSettings = VideoCodecSettings {
                                codec = VideoCodec.FrameCapture
                                frameCaptureSettings = FrameCaptureSettings {
                                    framerateNumerator = 1
                                    framerateDenominator = 1
                                    maxCaptures = 10000000
                                    quality = 80
                                }
                            }
                        }
                    }
                )
            }
    
            val audioSelectors1: MutableMap<String, AudioSelector> = HashMap()
            audioSelectors1["Audio Selector 1"] =
                AudioSelector {
                    defaultSelection = AudioDefaultSelection.Default
                    offset = 0
                }
    
            val jobSettings = JobSettings {
                inputs = listOf(
                    Input {
                        audioSelectors = audioSelectors1
                        videoSelector = VideoSelector {
                            colorSpace = ColorSpace.Follow
                            rotate = InputRotate.Degree0
                        }
                        filterEnable = InputFilterEnable.Auto
                        filterStrength = 0
                        deblockFilter = InputDeblockFilter.Disabled
                        denoiseFilter = InputDenoiseFilter.Disabled
                        psiControl = InputPsiControl.UsePsi
                        timecodeSource = InputTimecodeSource.Embedded
                        fileInput = fileInputVal
    
                        outputGroups = listOf(appleHLS, thumbs, fileMp4)
                    }
                )
            }
    
            val createJobRequest = CreateJobRequest {
                role = mcRoleARN
                settings = jobSettings
            }
    
            val createJobResponse = mediaConvertClient.createJob(createJobRequest)
            return createJobResponse.job?.id
        } catch (ex: MediaConvertException) {
            println(ex.message)
            mcClient.close()
            exitProcess(0)
        }
    }
    
    fun createOutput(
        customName: String,
        nameModifierVal: String,
        segmentModifierVal: String,
        qvbrMaxBitrate: Int,
        qvbrQualityLevelVal: Int,
        originWidth: Int,
        originHeight: Int,
        targetWidth: Int
    ): Output? {
    
        val targetHeight = (
                Math.round((originHeight * targetWidth / originWidth).toFloat()) -
                        Math.round((originHeight * targetWidth / originWidth).toFloat()) % 4
                )
    
        var output: Output? = null
    
        try {
    
            val audio1 = AudioDescription {
                audioTypeControl = AudioTypeControl.FollowInput
                languageCodeControl = AudioLanguageCodeControl.FollowInput
                codecSettings = AudioCodecSettings {
                    codec = AudioCodec.Aac
                    aacSettings = AacSettings {
                        codecProfile = AacCodecProfile.Lc
                        rateControlMode = AacRateControlMode.Cbr
                        codingMode = AacCodingMode.CodingMode2_0
                        sampleRate = 44100
                        bitrate = 96000
                        rawFormat = AacRawFormat.None
                        specification = AacSpecification.Mpeg4
                        audioDescriptionBroadcasterMix = AacAudioDescriptionBroadcasterMix.Normal
                    }
                }
            }
    
            output = Output {
                nameModifier = nameModifierVal
                outputSettings = OutputSettings {
                    hlsSettings = HlsSettings {
                        segmentModifier = segmentModifierVal
                        audioGroupId = "program_audio"
                        iFrameOnlyManifest = HlsIFrameOnlyManifest.Exclude
                    }
                }
                containerSettings = ContainerSettings {
                    container = ContainerType.M3U8
                    this.m3U8Settings = M3U8Settings {
                        audioFramesPerPes = 4
                        pcrControl = M3U8PcrControl.PcrEveryPesPacket
                        pmtPid = 480
                        privateMetadataPid = 503
                        programNumber = 1
                        patInterval = 0
                        pmtInterval = 0
                        scte35Source = M3U8Scte35Source.None
                        scte35Pid = 500
                        nielsenId3 = M3U8NielsenId3.None
                        timedMetadata = TimedMetadata.None
                        timedMetadataPid = 502
                        videoPid = 481
                        audioPids = listOf(482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492)
                    }
    
                    videoDescription = VideoDescription {
                        width = targetWidth
                        height = targetHeight
                        scalingBehavior = ScalingBehavior.Default
                        sharpness = 50
                        antiAlias = AntiAlias.Enabled
                        timecodeInsertion = VideoTimecodeInsertion.Disabled
                        colorMetadata = ColorMetadata.Insert
                        respondToAfd = RespondToAfd.None
                        afdSignaling = AfdSignaling.None
                        dropFrameTimecode = DropFrameTimecode.Enabled
                        codecSettings = VideoCodecSettings {
                            codec = VideoCodec.H264
                            h264Settings = H264Settings {
                                rateControlMode = H264RateControlMode.Qvbr
                                parControl = H264ParControl.InitializeFromSource
                                qualityTuningLevel = H264QualityTuningLevel.SinglePass
                                qvbrSettings = H264QvbrSettings {
                                    qvbrQualityLevel = qvbrQualityLevelVal
                                }
                                codecLevel = H264CodecLevel.Auto
                                codecProfile =
                                    if (targetHeight > 720 && targetWidth > 1280) H264CodecProfile.High else H264CodecProfile.Main
                                maxBitrate = qvbrMaxBitrate
                                framerateControl = H264FramerateControl.InitializeFromSource
                                gopSize = 2.0
                                gopSizeUnits = H264GopSizeUnits.Seconds
                                numberBFramesBetweenReferenceFrames = 2
                                gopClosedCadence = 1
                                gopBReference = H264GopBReference.Disabled
                                slowPal = H264SlowPal.Disabled
                                syntax = H264Syntax.Default
                                numberReferenceFrames = 3
                                dynamicSubGop = H264DynamicSubGop.Static
                                fieldEncoding = H264FieldEncoding.Paff
                                sceneChangeDetect = H264SceneChangeDetect.Enabled
                                minIInterval = 0
                                telecine = H264Telecine.None
                                framerateConversionAlgorithm = H264FramerateConversionAlgorithm.DuplicateDrop
                                entropyEncoding = H264EntropyEncoding.Cabac
                                slices = 1
                                unregisteredSeiTimecode = H264UnregisteredSeiTimecode.Disabled
                                repeatPps = H264RepeatPps.Disabled
                                adaptiveQuantization = H264AdaptiveQuantization.High
                                spatialAdaptiveQuantization = H264SpatialAdaptiveQuantization.Enabled
                                temporalAdaptiveQuantization = H264TemporalAdaptiveQuantization.Enabled
                                flickerAdaptiveQuantization = H264FlickerAdaptiveQuantization.Disabled
                                softness = 0
                                interlaceMode = H264InterlaceMode.Progressive
                            }
                        }
                        audioDescriptions = listOf(audio1)
                    }
                }
            }
        } catch (ex: MediaConvertException) {
            println(ex.toString())
            exitProcess(0)
        }
        return output
    }

    MediaConvert에서 job을 생성하기 위한 다양한 옵션들이 들어가기 때문에 코드가 좀 깁니다.

     

    수정해야 할 부분은 다음과 같습니다.

    - mcRoleARN

    - fileInput

    - region

    - suspend 제거하기

    val mcRoleARN = "미디어 컨버터의 iam role을 넣어주세요(arn:aws:iam::xxxxxxxxxx:role/MediaConvert-common)"
    val fileInput = "mediaconvert에서 사용될 s3 input file을 넣어주세요(s3://your_bucket/your_path/somefile.mov)"
    val mcClient = MediaConvertClient { region = "aws 리전을 넣어주세요(ap-northeast-2)"}

     

    suspend 제거하기

    Spring Boot에서는 컨트롤러를 suspending으로 선언하지 않고 runBlocking을 사용해야 하기 때문에 suspend를 모두 제거하고 runblokcing{}으로 코루틴함수를 감싸줍니다.

     

    runblocking{}을 사용하면 하위스코프가 완전히 끝나야 그다음 라인 실행이 가능합니다.

    val res = runBlocking { mcClient.describeEndpoints(describeEndpoints) }
    ...
    ...
    ...
    val createJobResponse = runBlocking { mediaConvertClient.createJob(createJobRequest) }

    describeEndpoints 메서드와 createJob메서드를 타고들어가보면 내부적으로 suspend fun으로 구현되어 있습니다.

     

     

    런타임 시 ClassNotFoundException

    실행 후 다음과 같은 Exception이 발생할 수 있습니다.

    java.lang.ClassNotFoundException: org.reactivestreams.Publisher
    java.lang.ClassNotFoundException: kotlinx.coroutines.reactor.MonoKt
    implementation("org.reactivestreams:reactive-streams:1.0.4")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.6.2")

    두 의존성을 implementation 하여 해결했습니다.

     

     

    이제 성공적으로 job이 생성되는것을 확인할 수 있습니다.

     

     

     

     

     

    참고자료

    https://kotlinworld.com/m/144

     

    [Coroutine] 5. suspend fun의 이해

    일시중단 가능한 코루틴 코루틴은 기본적으로 일시중단 가능하다. launch로 실행하든 async로 실행하든 내부에 해당 코루틴을 일시중단 해야하는 동작이 있으면 코루틴은 일시 중단된다. 예시로

    kotlinworld.com

    https://stackoverflow.com/questions/43231477/why-i-am-getting-noclassdeffounderror-org-reactivestreams-publisher

     

    Why I am getting NoClassDefFoundError: org/reactivestreams/Publisher

    Stream.java import io.reactivex.*; public class Stream { public static void main(String args[]) { Observable.just("Howdy!").subscribe(System.out::println); } } build.gradle:

    stackoverflow.com

     

    'AWS' 카테고리의 다른 글

    [AWS] CloudFront와 S3 연결하기[이론편]  (0) 2023.02.02
    [AWS] AWS MediaConvert Jobtemplate Kotlin SDK 적용  (0) 2022.12.25
    [AWS] IAM이란?  (0) 2022.12.18
    [AWS] MediaConvert란? + 튜토리얼  (0) 2022.12.17
    Amazon S3란?  (0) 2022.03.12

    댓글

Designed by Tistory.