Springboot AWS MediaConvert 통합

공부는 혼자하는 거·2023년 3월 13일
0

업무

목록 보기
10/17
post-custom-banner

AWS의 MediaConvert API를 활용하여 오디오 파일을 hls 포멧으로 바꾸어서 서빙해오는 작업이 있었다. 기존에는 별도의 AWS 람다와 트리거를 활용하여 변환시켜 주고 있던 것을 스프링부트 서버 내로 통합하기로 결정했다. 기존에는 파일을 컨버팅하는 시작시점과 완료시점을 따로 파악해야 한다는 애로사항이 있었던 점을 하나의 서버 내로 통합시키면서 자동화하는 데 이점이 있다고 판단했기 때문이다.

환경은 스프링부트 + Kotlin + gradle 이다.

작업 셋팅

    implementation("software.amazon.awssdk:mediaconvert:2.20.20")

언제나처럼 별도의 yml 파일에 환경변수를 저장했다. 한가지 에로사항은 미디어컨버팅하는데 필요한 roleArn은 별도의 IAM 역할을 생성해줘야 된다는 것이다. 왜인지는 모르겠으나, 사용자에 직접 정책을 연결한 것으로는 권한 거부가 뜨더라.. 이것 때문에 한 4시간은 헤멘 듯 한다..

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "mediaconvert.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
        @ConstructorBinding
        @ConfigurationProperties(prefix = "cloud")
        data class MediaConvert(
            val input_bucket_path:String,
            val outBucket: String,
            val inputBucket: String,
            val output_bucket_path:String,
            val accessKey: String,
            val secretKey: String,
            val roleArn:String,
        )
        
        
        
@Configuration
@PropertySource(value = ["classpath:sercet.yml"], factory = YamlPropertySourceFactory::class)
class MediaConvertConfig(
    private val cloudProperties: CloudProperties
) {

    private val log = KotlinLogging.logger {  }


    @Bean
    fun credentialsProvider(): AwsCredentialsProvider {
        return StaticCredentialsProvider.create(
            AwsBasicCredentials.create(
                cloudProperties.aws.mediaConvert.accessKey,
                cloudProperties.aws.mediaConvert.secretKey
            )
        )
    }

    @Bean
    fun mediaConvertClient(credentialsProvider: AwsCredentialsProvider): MediaConvertClient {
        // media convert에 대한 endpoint를 취득한다. (필수!)
        val describeEndpoints = MediaConvertClient.builder()
            .credentialsProvider(credentialsProvider)
            .region(Region.AP_NORTHEAST_2)
            .build()
            .describeEndpoints(DescribeEndpointsRequest.builder().maxResults(20).build())


        return MediaConvertClient.builder()
            .credentialsProvider(credentialsProvider)
            .endpointOverride(URI.create(describeEndpoints.endpoints().get(0).url()))
            .region(Region.AP_NORTHEAST_2)
            .build()
    }


}
        

컨버팅 서비스 작성

@Service
class AWSMediaConverter(
    private val mediaConvertClient: MediaConvertClient,
    private val mediaConvertProperties: MediaConvert,
) {

    private val log = KotlinLogging.logger {  }


    fun getConvertJob(jobId: String): GetJobResponse {
        val jobRequest = GetJobRequest.builder()
            .id(jobId)
            .build()
        return mediaConvertClient.getJob(jobRequest)
    }


    fun getConvertJobList(jobStatus: JobStatus, limit: Int): ListJobsResponse? {
        val jobsRequest = ListJobsRequest.builder()
            .maxResults(limit)
            .status(jobStatus)
            .build()
        return mediaConvertClient.listJobs(jobsRequest)
    }


 fun createConvertJob(fileName: String): CreateJobResponse {

        val fileInput =
            (mediaConvertProperties.inputBucket + "/" + mediaConvertProperties.input_bucket_path) + "/" + fileName

        val fileNamePath = fileName.split("_").firstOrNull() ?: throw IllegalArgumentException("cant find first name of file")
        //val convertedFilename = fileName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]
        val fileOutput: String =
            (mediaConvertProperties.outBucket + File.separator + mediaConvertProperties.output_bucket_path) + File.separator +
                    fileNamePath + File.separator + fileNamePath

        log.info { "fileOutput==>$fileOutput" }

        val appleHLS = OutputGroup.builder().name("Apple HLS").customName("HLS")
            .outputGroupSettings(
                OutputGroupSettings.builder().type(OutputGroupType.HLS_GROUP_SETTINGS)
                    .hlsGroupSettings(
                        HlsGroupSettings.builder()
                            .outputSelection(HlsOutputSelection.MANIFESTS_AND_SEGMENTS)
                            .directoryStructure(HlsDirectoryStructure.SINGLE_DIRECTORY)
                            .destination(fileOutput)
                            .segmentLength(10)
                            .minSegmentLength(0)
                            .build()
                    )
                    .build()
            )
            .outputs( createOutput()).build()

        val audioSelectors: MutableMap<String, AudioSelector> = HashMap()
        audioSelectors["Audio Selector 1"] =
            AudioSelector.builder().defaultSelection(AudioDefaultSelection.DEFAULT).offset(0).build()
        val jobSettings = JobSettings.builder().inputs(
            Input.builder().audioSelectors(audioSelectors)
                .filterEnable(InputFilterEnable.AUTO).filterStrength(0).deblockFilter(InputDeblockFilter.DISABLED)
                .denoiseFilter(InputDenoiseFilter.DISABLED).psiControl(InputPsiControl.USE_PSI)
                .timecodeSource(InputTimecodeSource.EMBEDDED).fileInput(fileInput).build()
        )
            .outputGroups(appleHLS).build()
        val createJobRequest = CreateJobRequest.builder()
            .role(mediaConvertProperties.roleArn)
            .settings(jobSettings).build()

        return mediaConvertClient.createJob(createJobRequest)
    }



    private fun createOutput(): Output {

        val output = Output.builder().nameModifier("_convert").outputSettings(
            OutputSettings.builder()
                .hlsSettings(
                    HlsSettings.builder().audioGroupId("program_audio")
                        .iFrameOnlyManifest(HlsIFrameOnlyManifest.EXCLUDE).build()
                )
                .build()
        )
            .containerSettings(
                ContainerSettings.builder().container(ContainerType.M3_U8)
                    .m3u8Settings(
                        M3u8Settings.builder().audioFramesPerPes(4)
                            .pcrControl(M3u8PcrControl.PCR_EVERY_PES_PACKET).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(482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492).build()
                    )
                    .build()
            )
            .audioDescriptions(
                AudioDescription.builder()

                    .codecSettings(
                        AudioCodecSettings.builder()
                            .codec(AudioCodec.AAC)
                            .aacSettings(AacSettings.builder()
                                .sampleRate(44100)
                                .bitrate(128000)
                                .codingMode(AacCodingMode.CODING_MODE_2_0)
                                .build()
                            )
                            .build()
                    )
                    .languageCode(LanguageCode.ENG)
                    .audioTypeControl(AudioTypeControl.FOLLOW_INPUT)
                    .build()
            )
            .extension("m3u8")



        return output.build()
    }


    @Async
    fun getJobUntilComplete(jobResponse: CreateJobResponse){

        var job = jobResponse.job()

        System.out.println("convertJob.job().id() = " + job.id())

        while (job.status() == null || job.status() != JobStatus.COMPLETE){

            job = getConvertJob(job.id()).job()
            System.out.println("convertJob.job().status() = " + job.status())
            try {
                Thread.sleep(1000)
            }catch (e:Exception){
                log.error { e.stackTraceToString() }
            }
        }

        System.out.println("job is complete = " + job.status())

        //todo complete 한 상태를 DB에 반영한다.


    }



    @Test
    fun mediaConvertTest(@RequestParam filename:String){

        val convertJob = awsMediaConverter.createConvertJob(filename)
        awsMediaConverter.getJobUntilComplete(convertJob) 	//비동기 
    }

간단히 테스트할 수 있는 서비스 코드를 작성해봤다.

참고

https://realwater87.tistory.com/m/14

https://docs.aws.amazon.com/ko_kr/mediaconvert/latest/ug/creating-the-iam-role-in-iam.html

profile
시간대비효율
post-custom-banner

0개의 댓글