본문 바로가기

기타

[AWS MediaConvert] SpringBoot 연동하여 미디어 파일 DASH 포맷으로 변경하기

미디어 스트리밍에 대한 지식이 전무한 상태에서, 관련 업무 때문에 미디어 파일을 DASH 포맷으로 변경해야했다. 변경 방법은 여러가지가 있지만 나는 AWS에서 제공하는 MediaConvert라는 서비스를 사용하여 변환해보았다.

 

변환 과정에서 알게된 지식들이나, 특히 AWS MediaConvert 서비스를 콘솔이 아닌 Api로 구현하는 과정에서 겪었던 시행착오들을 공유하고자 한다. 후에 해당 서비스를 이용하고자 하는 분들에게 도움이 되었으면 좋겠다.

 

 

스트리밍이란?

유튜브나 멜론과 같은 서비스를 평소에 자주 접하고 있을 것이다. 직접 사용해보면 영상이나 음악을 거의 대기하지 않고 바로바로 재생할 수 있어 매우 편리하다.

 

 

스트리밍은 어떤 파일을 매우 작은 단위로 쪼개어 압축을 풀고, 내려받아 다운로드 받은 만큼만 재생한다. 작은 단위를 재생시간보다 빠르게 다운로드 할 수 있다면 영상이 재생되는 동안 다음 부분이 재생될 준비를 마칠 수 있다. 결국, 사용자는 영상이나 음악을 로딩 시간 없이도 막힘없이 이용할 수 있다. 

 

 

 

Adaptive HTTP Streaming

스트리밍 서비스를 사용하기 위해서, 통신을 위한 여러가지 프로토콜이 존재한다. 

Adaptive HTTP Streaming은 사용자가 동영상을 플레이할 때 네트워크 상황에 따라서 적절하게 컨텐츠 해상도를 조절하여 최적의 스트리밍 서비스를 제공하며, 클라이언트의 bandwidth의 상황에 맞게 그에 맞는 화질을 전송하는 방식이다.

 

공급사 마다 프로토콜이 여럿 존재하지만, 가장 대중적인 것은 HLS 이다.

 

이번 프로젝트에서는 DASH 를 사용했으므로 HLS는 넘어가도록 하겠다.

 

 

MPEG-DASH

HLS와 원리는 비슷하다. 미디어 파일을 세그먼트(보통 10초 이하) 단위로 분할하고, 각 세그먼트는 프레그넌트로 분할한다. 분할되면 각 세그먼트는 순서대로 정렬되어 재생되는데, 재생에 필요한 세그먼트의 URL과 같은 모든 설정 값이 mpd 파일에 들어있다. 즉, mpd는 미디어 파일을 스트리밍 서비스로 이용하기위한 재생 목록 이라고 보면 된다. 

 

 

 

결과적으로 스트리밍 서비스를 구현하기 위해서, mp4 확장자로 된 미디어 파일을 쪼개어 mpd 파일과 세그먼트 파일로 만들어야 한다.

 

AWS에서는 변환을 위한 서비스를 제공하고 있다. 

 

대략적인 흐름은 s3에 변환된 파일을 올려놓고, 클라이언트 단에서 s3 url로 접근하도록 구현하는 것이다.

 

API로 바로 변환해보기 보다, AWS MediaConvert 콘솔을 이용해서 직접 영상을 변환해보길 바란다.

아래 블로그를 따라하면 쉽게 영상을 변환할 수 있다.

 

 

AWS 미디어 서비스를 사용하여 파일 스트리밍 - BESPIN Tech Blog

이번 글에서는 스토리지 및 미디어 트랜스코딩을 위해 AWS 서비스를 사용하여 웹 스트리밍용 비디오를 처리하는 방법에 대해서 알아보겠습니다. [실습 순서] S3 버킷 생성 IAM 역할 생성 MediaConvert

blog.bespinglobal.com

 

직접 변환해본 후에, 직접 콘솔 화면을 띄워두고 화면을 보면서 코드를 보면 이해가 빠를 것이다.

 

 

API 만들기

build.gradle

plugins {
id 'java'
id 'org.springframework.boot' version '2.7.13'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '1.8'
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'com.amazonaws:aws-java-sdk-mediaconvert:1.11.960'
}

tasks.named('test') {
useJUnitPlatform()
}

의존성은 Spring Web, 롬복, 그리고 aws java sdk mediaconvert 1.11.960 버전으로 넣어주었다.

 

application.properties

# Server
server.port=8080

# Cloud AWS Credentials
cloud.aws.credentials.accessKey=계정 액세스 키
cloud.aws.credentials.secretKey=계정 시크릿 키
cloud.aws.s3.bucket=s3 버킷 url
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false

# Application Bucket
application.bucket.name=버킷 이름

# AWS Role ARN
aws.roleARN=IAM 역할 ARN

# MediaConvert
mediaconvert.endpoint=미디어 컨버트 계정 엔드포인트

해당 프로퍼티의 내용은 위의 블로그를 따라하면서 얻을 수 있는 값들이다. 해당 정보는 내 AWS 계정에 접근할 수 있는 개인 정보를 담고 있으므로, 공유되지 않도록 반드시 .gitIgnore에 추가하여 올라가는 일이 없도록 하자.

 

 

API로 MediaConvert 서비스를 사용하는 것만을 목적으로 만든 프로젝트 이므로, DB 설정이나 별다른 Entity 설정은 하지 않았다.

 

S3의 origin 폴더에 미디어 소스파일이 있고, dash 폴더에 변환 결과물을 업로드한다고 가정했다.

 

 

 

Controller

package com.example.awsmediaconvert.controller;

import com.amazonaws.services.mediaconvert.model.CreateJobResult;
import com.example.awsmediaconvert.service.AwsMediaConvertService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/convert")
public class AwsMediaConvertController {
    private final AwsMediaConvertService awsMediaConvertConnect;

    @GetMapping("")
    public ResponseEntity beginJob(){
        CreateJobResult result = awsMediaConvertConnect.beginJob();
        return ResponseEntity.ok().body(result);
    }
}

Contoller에서는 Get 요청을 받아 beginJob()을 호출하게 될 것이다.

 

 

Service

package com.example.awsmediaconvert.service;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.mediaconvert.AWSMediaConvert;
import com.amazonaws.services.mediaconvert.AWSMediaConvertClientBuilder;
import com.amazonaws.services.mediaconvert.model.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;

@Service
public class AwsMediaConvertService {

    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Value("${mediaconvert.endpoint}")
    private String mcEndPoint ;

    @Value("${aws.roleARN}")
    private String roleARN ;

    AWSMediaConvert awsMediaConvert;

    @PostConstruct
    public void setAwsMediaConvert() { // Aws 미디어 컨버트에 연결. 로컬에서 사용하려면 아마 키방식을 활용해야할듯.
        //아래 주석은 키방식을 활용할 때 사용(일반적)
        AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
        AwsClientBuilder.EndpointConfiguration eConf = new AwsClientBuilder.EndpointConfiguration( mcEndPoint, region ) ;
        awsMediaConvert = AWSMediaConvertClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withEndpointConfiguration(eConf)
                .build();

    }

    // 작업 시작 beginJob > createDashJobSetting > genarateDashOutput > createDashOutput
    public CreateJobResult beginJob(){
        return convert(createDashJobSetting());
    }

    // 생성한 Job으로 변환 작업 수행
    public CreateJobResult convert(JobSettings settings){
        CreateJobRequest jobParam = new CreateJobRequest()
                .withRole(roleARN)
                .withSettings(settings);
        CreateJobResult mcResponse = awsMediaConvert.createJob(jobParam);
        return mcResponse;
    }

    public JobSettings createDashJobSetting(){
        //JobSettings
        // 각 입력과 출력 그룹에 대한 세팅 오디오 + 비디오 설정이 들어가야함.
        JobSettings settings = new JobSettings();

        // 입력 생성
        Input inputSource = new Input();
        inputSource.withFileInput("https://streamingtest-bucket.s3.ap-northeast-2.amazonaws.com/origin/BigBuckBunny.mp4");
        AudioSelector as = new AudioSelector();
        // 오디오 재생 시작시간 설정
        as.withOffset(0);
        // 다중 오디오 프로그램 선택자 설정. 여러 개일 경우에 첫 번째 것 사용
        as.withProgramSelection(1);
        // 오디오 선택자가 특정 스트림을 선택하지 못할 때 기본으로 선택할 오디오 스트림 지정.
        as.withDefaultSelection("DEFAULT");

        // Input 값에 오디오 선택자 추가함
        inputSource.addAudioSelectorsEntry("Audio Selector 1", as);
        // JobSetting에 Input 값 추가함
        settings.withInputs(inputSource);

        // 출력 그룹 추가
        settings.withOutputGroups(generateDashOutput());

        return settings;
    }

    // 출력 그룹 생성
    public OutputGroup generateDashOutput(){
        //출력 그룹
        OutputGroup og = new OutputGroup();

        //출력 그룹 설정
        OutputGroupSettings ogs = new OutputGroupSettings();
        //DASH iso 출력 그룹 설정
        DashIsoGroupSettings dgs = new DashIsoGroupSettings();
        dgs.withDestination(bucket) // 결과 파일이 업로드될 S3 버킷 경로 s3://~~
                .withMpdProfile(DashIsoMpdProfile.MAIN_PROFILE) // MPD 프로필 설정
                .withSegmentLength(10) // 세그먼트 길이 10초
//                .withMinFinalSegmentLength((double) 1) // 세그먼트 최소 길이
                .withFragmentLength(2) // 프레그먼트 길이 5초
                .withSegmentControl(DashIsoSegmentControl.SEGMENTED_FILES); // 세그먼트 제어 >> 세그먼트 파일

        //출력 그룹의 유형을 DASH 그룹 설정으로 설정하고 위에서 설정한 값을 넣는다.
        ogs.withType(OutputGroupType.DASH_ISO_GROUP_SETTINGS).withDashIsoGroupSettings(dgs);

        //출력 그룹의 이름을 OTT로 사용자 지정 그룹이름을 dash로, 위의 출력 그룹 설정을 적용한다.
        og.withName("OTT").withCustomName("dash").withOutputGroupSettings(ogs);
        og.withOutputs(createDashOutput()); // 출력 그룹의 출력 객체를 설정한다.

        return og;
    }

    // 출력 생성
    public Output createDashOutput(){
        // H264 비디오 코덱으로 설정하기
        H264Settings h264Settings = new H264Settings();

        // AWS에서 제공하는 Enum을 사용하여 비디오 인코딩 설정 (문서의 FHD 기준으로 작성함)
        h264Settings
                .withRateControlMode(H264RateControlMode.VBR) // 속도 제어 모드
                .withQualityTuningLevel(H264QualityTuningLevel.SINGLE_PASS) // 품질 조정 수준
                .withCodecLevel(H264CodecLevel.AUTO) // MediaConvert가 자동으로 코덱 레벨을 조정한다.
                .withCodecProfile(H264CodecProfile.HIGH) // 프로필 High
//                .withFramerateControl(H264FramerateControl.SPECIFIED) // 프레임 속도제어 사용자 설정
//                .withFramerateDenominator(1) // 초당 프레임 속도 분모 1
//                .withFramerateNumerator(30) // 프레임 속도 30
//                .withFramerateConversionAlgorithm(H264FramerateConversionAlgorithm.DUPLICATE_DROP) // 프레임 속도 변환 알고리즘
                .withBitrate(8192000) // 비트레이트 설정
                .withMaxBitrate(12288000); // 최대 비트레이트 설정

        // 비디오코덱 세팅 설정
        VideoCodecSettings vcs = new VideoCodecSettings();
        vcs.withCodec(VideoCodec.H_264)
                .withH264Settings(h264Settings);

        // 비디오 트랙 속성 설정
        VideoDescription vds = new VideoDescription();
        vds
//                .withScalingBehavior(ScalingBehavior.DEFAULT) // 스케일링 동작 설정. 입출력 크기가 같은 때 기본 스케일링 동작
//                .withSharpness(50) // 비디오 선명도 설정
//                .withAntiAlias(AntiAlias.ENABLED) // 안티앨리어싱 활성화
//                .withTimecodeInsertion(VideoTimecodeInsertion.DISABLED) // 비디오 타임코드 삽입 비활성화
//                .withColorMetadata(ColorMetadata.INSERT) // 컬러 메타데이터 삽입
//                .withRespondToAfd(RespondToAfd.NONE) // AFD에 대한 응답 설정
//                .withAfdSignaling(AfdSignaling.NONE) // AFD 시그널링 None
//                .withDropFrameTimecode(DropFrameTimecode.ENABLED) // 드랍 프레임 타임코드 활성화
                .withCodecSettings(vcs)
                .withHeight(1080) // FHD 해상도 설정 세로
                .withWidth(1920); // FHD 해상도 설정 가로
        // 가로 세로 비율 설정이 없음..


        // 오디오 트랙 속성 설정
        AudioDescription ads = new AudioDescription();

        // 오디오 코덱 세팅
        AudioCodecSettings acs = new AudioCodecSettings();
        acs.withCodec(AudioCodec.AAC) // 오디오 코덱 AAC 설정.
                // AAC 코덱 설정
                .withAacSettings(new AacSettings()
//                        .withCodecProfile(AacCodecProfile.LC) // 코덱 프로필 Low Complexity 설정
//                        .withRateControlMode(AacRateControlMode.CBR) // 비트레이드 제어모드 고정 비트레이트
                        .withCodingMode(AacCodingMode.CODING_MODE_2_0) // 코딩 모드 2.0 채널 스테레오
                        .withSampleRate(48000) // 샘플속도 48.000Hz
                        .withBitrate(256000) // 비트레이트 256.000bps
                        .withRawFormat(AacRawFormat.NONE) // 원시형식 없음
                        .withSpecification(AacSpecification.MPEG4) // AAC 스펙 MPEG-4
//                        .withAudioDescriptionBroadcasterMix(AacAudioDescriptionBroadcasterMix.NORMAL) // 일반적인 믹스형식 사용
                );

        ads.withCodecSettings(acs);
//        .withAudioTypeControl(AudioTypeControl.FOLLOW_INPUT) // 입력 소스의 오디오 유형 사용
//                .withLanguageCodeControl(AudioLanguageCodeControl.FOLLOW_INPUT) // 입력 소스의 언어 코드 사용
//                .withCodecSettings(acs);

        // 작업 출력 설정
        Output op = new Output();

        //출력 컨테이너 설정 >> MPD 기본값으로 사용함.
        op.withContainerSettings(new ContainerSettings().withContainer(ContainerType.MPD).withMpdSettings(new MpdSettings()))
                .withVideoDescription(vds) // 작업 출력에 비디오 트랙 설정 추가
                .withAudioDescriptions(ads) // 작업 출력에 오디오 트랙 설정 추가
                .withNameModifier("FHD2"); // 출력 이름에 접미사 "FHD" 추가

        return op;
    }
}

서비스 단이 조금 방대한데, 순서대로 이해하면 좋을 것이다. 

 

JobSettings - 생성할 작업에 대한 설정 값을 담는다.

 

Input - 입력 탭에 들어갈 설정 값을 모두 담는다.

콘솔 - 입력 값 설정

 

Output - 출력 탭에 들어갈 설정 값을 모두 담는다.

 

OutputGroup - 한 개 출력그룹에 해당하는 값을 담는다.

콘솔 - 출력 값 설정

 

 

 

VideoDescription - 한 개 비디오 트랙 전반에 대한 설정을 담는다.

VideoCodecSettings - 비디오 코덱에 대한 설정을 담는다. 해당 설정을 VideoDescription에 넣어준다.

콘솔 - 비디오 트랙 설정

 

 

AudioDescription - 한 개 오디오트랙 전반에 대한 설정을 담는다.

AudioCodecSettings - 한 개 오디오 코덱에 대한 설정을 담는다. 해당 설정을 AudioDescription에 넣어준다.

콘솔 - 오디오 트랙 설정

 

정리하면 다음 과정과 같다.

 

1. 입력에 대한 설정을 Input에 담고, 출력에 대한 설정을 Output에 담는다.
2. Input과 Output을 JobSettings(작업 설정)에 담는다.
3. JobSettingsCreateJobRequest에 넣어 convert 작업을 요청한다.
4. AWS MediaConvert 작업이 생성되어 변환 작업을 수행한다.
5. S3에 변환된 파일이 업로드 된다.

 

 

실행 결과

요청이 보내지면, 콘솔 작업 탭에서 상태를 확인할 수 있다. 파일의 용량과 설정 값에 따라 소요되는 시간이 천차만별인데, 내가 했던 설정으로는 5분안에는 변환이 완료되었다.

 

mpd파일과 변환된 세그먼트 파일이 잘 업로드 된 것을 확인할 수 있다.

 

mpd파일의 URL로 영상이 잘 재생되는지 확인해보자.

 

 

변환한 스트리밍 프로토콜에 해당하는 곳에 URL을 적어넣고, Test Stream을 클릭하면...

영상이 잘 나오는 것을 확인할 수 있다.

 

아래의 사이트에서 테스트가 가능하다.

 

https://developer-tools.jwplayer.com/stream-tester

 

Welcome

Our documentation hub is here to help you create and deliver beautiful video experiences with the most powerful video APIs on the web. Get startedBefore you can begin using JWP's services, you need a JWP user account.Use one of the following options to obt

docs.jwplayer.com

 

 

아직 동영상에 대한 원리나, 스트리밍에 대한 지식이 부족하여 설정을 세분화하는 것까지는 공부가 더 필요해보인다. mpd 파일 (매니페스트)를 세분화 하는 방법도 추가로 공부해봐야겠다.

 

 

추가적으로, AWS MediaConvert 를 많이 사용하면.. 요금이 부과된다.

요금 생각을 못하고 막 변환해보았는데 (13건 정도) 생각보다 요금이 많이 부과되었다. 사용할 때 주의하길 바란다..

'기타' 카테고리의 다른 글

[Git] CLI 명령어  (0) 2023.10.10
[Kafka] SpringBoot 에서 Kafka 연동하기  (0) 2023.08.08
JPA 면접 질문 정리  (0) 2023.06.02
Spring Boot 면접 질문 정리  (0) 2023.06.02
Git (3) - merge와 branch  (0) 2023.01.05