주요글: 도커 시작하기
반응형

구현 진도를 확 빼고 싶지만, 글을 남기면서 진행하다보면 코드가 더디게 나간다. 답답함이 좀 있지만 몇 가지 영역에 대해서는 글로 남기는 게 좋을 것 같다.


Ffmpeg을 이용한 Transcoder 구현체 만들기! 테스트부터 작성


지금까지 TranscodingService 부터 시작해서 Job까지 점진적으로 구현을 만들어왔다. 다음 작업 대상은 Job 객체에서 호출할 Transcoder의 구현체이다. ffmpeg를 사용할 것이므로 구현체의 이름을 FfmpegTranscoder로 정리해 봤다. 구현의 시작은 테스트부터이다. 테스트부터 만들어보자.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;


    @Before

    public void setup() {

        transcoder = new FfmpegTranscoder();

    }


    @Test

    public void transcodeWithOnfOutputFormat() {

        File multimediaFile = new File(".");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(640, 480, 300, "h264", "aac"));

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(1, transcodedFiles.size());

        assertTrue(transcodedFiles.get(0).exists());

    }

}


위 테스트는 한 개의 비디오 파일에 대해 한 개의 변환 포맷을 입력으로 지정하고, 변환 결과로 생성된 파일이 존재하는 지 확인한다. 마음 같아서 생성된 파일을 분석해서 그 파일이 지정한 형식으로 올바르게 변환되었는지 확인하고 싶지만, 지금은 일단 위 수준에서 넘어가보자. (이 부분은 나중에 한 번 시도해 볼 것이다.)


FFmpegTranscoder 클래스가 없으니 컴파일 에러가 발생한다. 일단 이 클래스를 만들어서 컴파일 에러부터 없애자. 


다음으로 할 작업은 테스트를 통과시키는 것이다. 테스트 통과는 어렵지 않다. 빠르게 테스트를 통과시키기 위해 아래와 같이 코드를 작성한다.


public class FfmpegTranscoder implements Transcoder {


    @Override

    public List<File> transcode(File multimediaFile,

            List<OutputFormat> outputFormats) {

        List<File> results = new ArrayList<File>();

        for (OutputFormat format : outputFormats) {

            results.add(new File("."));

        }

        return results;

    }


}


이제 FfmpegTranscoderTest를 실행하자. 녹색, 통과다.


다음으로 할 작업은 테스트 코드를 좀 더 의미있게 변경하는 것이다. 지금은 단순히 파일이 존재하는지 여부로 검증하고 있는데, 더 정확하게 검증하려면 생성된 파일이 지정한 비디오 포맷과 크기를 갖는지의 여부로 검증해야 한다.


사용법을 익히기 위한 테스트 코드


생성된 비디오 결과물을 확인하기 위한 용도로 xuggler를 사용해 보기로 했다. xuggler는 ffmpeg을 자바에서 사용할 수 있도록 래핑해 놓은 라이브러리이다. 관련 네이티브 모듈을 모두 포함하고 있어서 jar 파일이 다소 크지만, ffmpeg를 로컬에 설치하고 네이티브를 만들거나 별도 프로세스로 호출해서는 사용하는 것 보다 편리하다가 판단되어 xuggler를 사용하기로 결정했다.


이 프로젝트는 Maven을 사용하고 있는데, 다음과 같은 설정으로 xuggler를 사용할 수 있다.


    <repositories>

        <repository>

            <id>xuggle repo</id>

            <url>http://xuggle.googlecode.com/svn/trunk/repo/share/java/</url>

        </repository>

    </repositories>


    <dependencies>

        <!-- ffmpeg -->

        <dependency>

            <groupId>xuggle</groupId>

            <artifactId>xuggle-xuggler</artifactId>

            <version>5.3</version>

        </dependency>

        ...


Xuggler를 클래스패스에 추가했으므로, xuggler의 이용방법을 익힐 차례이다. 이를 위해 Xuggler 관련 샘플 코드를 좀 검색한 뒤에 그 코드를 복사해서 다음과 같은 테스트를 만들었다.


public class XugglerTest {


    @Test

    public void getMetadataOfExistingAVIFile() {

        IContainer container = IContainer.make();

        int openResult = container.open("src/test/resources/sample.avi",

                IContainer.Type.READ, null);

        if (openResult < 0) {

            throw new RuntimeException("Xuggler file open failed: "

                    + openResult);

        }

        int numStreams = container.getNumStreams();

        System.out.printf("file \"%s\": %d stream%s; ",

                "src/test/resources/sample.avi", numStreams,

                numStreams == 1 ? "" : "s");

        System.out.printf("bit rate: %d; ", container.getBitRate());

        System.out.printf("\n");


        for (int i = 0; i < numStreams; i++) {

            IStream stream = container.getStream(i);

            IStreamCoder coder = stream.getStreamCoder();


            System.out.printf("stream %d: ", i);

            System.out.printf("type: %s; ", coder.getCodecType());

            System.out.printf("codec: %s; ", coder.getCodecID());

            System.out.printf("duration: %s; ",

                    stream.getDuration() == Global.NO_PTS ? "unknown" : ""

                            + stream.getDuration());

            System.out.printf("start time: %s; ",

                    container.getStartTime() == Global.NO_PTS ? "unknown" : ""

                            + stream.getStartTime());

            System.out.printf("timebase: %d/%d; ", stream.getTimeBase()

                    .getNumerator(), stream.getTimeBase().getDenominator());

            System.out.printf("coder tb: %d/%d; ", coder.getTimeBase()

                    .getNumerator(), coder.getTimeBase().getDenominator());


            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {

                System.out.printf("sample rate: %d; ", coder.getSampleRate());

                System.out.printf("channels: %d; ", coder.getChannels());

                System.out.printf("format: %s", coder.getSampleFormat());

            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {

                System.out.printf("width: %d; ", coder.getWidth());

                System.out.printf("height: %d; ", coder.getHeight());

                System.out.printf("format: %s; ", coder.getPixelType());

                System.out.printf("frame-rate: %5.2f; ", coder.getFrameRate()

                        .getDouble());

            }

            System.out.printf("\n");

        }

        container.close();

    }

}


위 코드를 이용해서 테스트를 돌려보고 어떤 결과값들이 출력되는지 확인한다. 몇 차례 코드를 수정하면서 테스트를 더 해 보면서 Xuggler를 이용해서 정보를 읽어오는 방법을 숙지해 나간다.


다시 FfmpegTranscoderTest로, 결과값 검증하는 코드 만들기


앞의 FfmpegTranscoderTest 코드에 변환 결과로 생성된 비디오 파일을 검증하는 코드를 추가해 보자.


public class FfmpegTranscoderTest {

    ...

    @Test

    public void transcodeWithOnfOutputFormat() {

        File multimediaFile = new File("src/test/resources/sample.avi");

        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();

        outputFormats.add(new OutputFormat(640, 480, 300, "h264", "aac"));

        List<File> transcodedFiles = transcoder.transcode(multimediaFile,

                outputFormats);

        assertEquals(1, transcodedFiles.size());

        assertTrue(transcodedFiles.get(0).exists());

        verifyTranscodedFile(outputFormats.get(0), transcodedFiles.get(0));

    }


    private void verifyTranscodedFile(OutputFormat outputFormat, File file) {

        IContainer container = IContainer.make();

        int openResult = container.open(file.getAbsolutePath(),

                IContainer.Type.READ, null);

        if (openResult < 0) {

            throw new RuntimeException("Xuggler file open failed: "

                    + openResult);

        }

        int numStreams = container.getNumStreams();


        int width = 0;

        int height = 0;

        ICodec.ID videoCodec = null;

        ICodec.ID audioCodec = null;


        for (int i = 0; i < numStreams; i++) {

            IStream stream = container.getStream(i);

            IStreamCoder coder = stream.getStreamCoder();


            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {

                audioCodec = coder.getCodecID();

            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {

                videoCodec = coder.getCodecID();

                width = coder.getWidth();

                height = coder.getHeight();

            }

        }

        container.close();


        assertEquals(outputFormat.getWidth(), width);

        assertEquals(outputFormat.getHeight(), height);

        assertEquals(outputFormat.getVideoFormat(), videoCodec.toString()); // ???

        assertEquals(outputFormat.getAudioFormat(), audioCodec.toString()); // ???

    }

}


verifyTranscodedFile() 메서드는 Xuggler를 이용해서 지정한 비디오 파일의 메타 정보가 OutputFormat에 지정된 값과 일치하는 지 여부를 검증한다. 테스트 코드의 마지막에서는 코덱을 비교하는 부분이 있는데, 다음의 두 가지가 걸린다.
  • OutputFormat의 메서드 이름이 getVideoFormat()/getAudioFormat()이다. 이건 getVideoCodec()과 같이 코덱의 의미로 바꿔줘야 할 것 같다.
  • OutputFormat은 String으로 값을 갖고 있는 반면에 비교하려는 값은 ICodec.ID 열거 타입이다. 이 둘 간에 변환 방법이 필요할 것 같다.
위 두 가지 중에서 첫 번째 것은 비교적 쉽다. 일단 먼저 처리하자. getVideoFormat과 getAudioFormat 이름을  getVideoCodec, getAutidoCodec 으로 변경한다.

두 번째는 Xuggler의 코덱 정보 표현 방법과 OutputFormat의 코덱 정보 표현 방법의 불일치로부터 발생하는 것이다. 이 부분은 뒤에서 다시 논의해 보자.

테스트 코드를 만들었으니 테스트를 실행한다. 빨간색. 통과에 실패했다. 실패를 발생시킨 코드는 아래와 같다.

// FfmpegTranscoderTest.java
    private void verifyTranscodedFile(OutputFormat outputFormat, File file) {
        IContainer container = IContainer.make();
        int openResult = container.open(file.getAbsolutePath(),
                IContainer.Type.READ, null);
        if (openResult < 0) { // 파일이 없으므로, 익셉션 발생됨
            throw new RuntimeException("Xuggler file open failed: "
                    + openResult);
        }


// FfmpegTranscoder.java
public class FfmpegTranscoder implements Transcoder {

    @Override
    public List<File> transcode(File multimediaFile,
            List<OutputFormat> outputFormats) {
        List<File> results = new ArrayList<File>();
        for (OutputFormat format : outputFormats) {
            results.add(new File(".")); // 디렉터리 정보임
        }
        return results;
    }

}

실패가 발생된 이유는 검증을 하기 위해 생성된 파일을 여는 과정에서 파일이 없어 발생한 것이다. 그 에러를 통과하려면 FfmpegTranscoder#transcode() 메서드에서 실제 파일을 리턴해 주어야 한다. 뿐만 아니라 폭, 높이 등에 대한 검증을 통과하려면 실제 비디오 파일을 생성해서 리턴해 주어야 한다.

아,, 비디오 변환 기능이 필요한 시점이 왔다. 음,,, 다시 Xuggler 테스트로 돌아가서 비디오 파일 변환 기능을 테스트 해 보자.

테스트 코드에서 Xuggler로 비디오 포맷 변환 기능 실험하기

Xuggler로 비디오 포맷을 변환하는 작업은 간단하다. 아래는 최초 테스트 코드이다.

public class XugglerTest {

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory
                .makeReader("src/test/resources/sample.avi");

        IMediaWriter writer = ToolFactory.makeWriter("target/sample.mp4",
                reader);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);
    }

아주 간단하게 비디오 변환을 할 수 있기에, 처음에는 '우~와' 했다. 그런데, 우리는 동영상의 크기도 변경시켜야 하고 비트레이트도 변경시킬 수 있어야 하고, MP4 컨테이너 정보 외에 인코딩할 때 사용할 비디오와 오디오의 코덱을 지정할 수 있어야 한다. 앗, 잠깐 컨테이너? 이런, 컨테이너를 설정하는 정보가 없다. 음, 일단 컨테이너에 대한 정보는 추후에 추가하자.

Xuggler가 다행히 오픈소스다. Xuggler의 소스 코드들을 탐색하고 테스트를 진행하면서, Xuggler를 이용한 VideoConverter를 완성했다. 더불어 VideoConverter를 만들어나가는 과정에서 다음을 함께 진행했다.

  • 테스트를 위한 VideoConverterTest 작성
  • OutputFormat에서 코덱 정보를 표현하기 위해 VideoCodec과 AudioCodec 타입이 도메인 모델에 추가
VideoConverterTest를 작성하면서 생성된 비디오의 내용물이 올바른지 확인하는 기능이 필요하다. 앞서 살펴봤던 FfmpegTranscoderTest의 verifyTranscodedFile() 메서드와 동일하다. 그러니, 이 메서드를 별도 헬퍼 클래스로 빼서 두 개의 테스트 코드에서 함께 사용할 수 있도록 하자.

헬러 클래스를 다음과 같이 작성해 보았다. 

public class VideoFormatVerifier {

    public static void verifyVideoFormat(OutputFormat expectedFormat,
            File videoFile) {
        new VideoFormatVerifier(expectedFormat, videoFile).verify();
    }

    private IContainer container;
    private int width;
    private int height;
    private ICodec.ID videoCodec;
    private ICodec.ID audioCodec;

    private OutputFormat expectedFormat;
    private File videoFile;

    public VideoFormatVerifier(OutputFormat expectedFormat, File videoFile) {
        this.expectedFormat = expectedFormat;
        this.videoFile = videoFile;
    }

    public void verify() {
        try {
            makeContainer();
            extractMetaInfoOfVideo();
            assertVideoFile();
        } finally {
            closeContainer();
        }
    }

    private void makeContainer() {
        container = IContainer.make();
        int openResult = container.open(videoFile.getAbsolutePath(),
                IContainer.Type.READ, null);
        if (openResult < 0) {
            throw new RuntimeException("Xuggler file open failed: "
                    + openResult);
        }
    }

    private void extractMetaInfoOfVideo() {
        int numStreams = container.getNumStreams();
        for (int i = 0; i < numStreams; i++) {
            IStream stream = container.getStream(i);
            IStreamCoder coder = stream.getStreamCoder();
            if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {
                audioCodec = coder.getCodecID();
            } else if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {
                videoCodec = coder.getCodecID();
                width = coder.getWidth();
                height = coder.getHeight();
            }
        }
    }

    private void closeContainer() {
        if (container != null)
            container.close();
    }

    private void assertVideoFile() {
        assertEquals(expectedFormat.getWidth(), width);
        assertEquals(expectedFormat.getHeight(), height);
        assertEquals(expectedFormat.getVideoCodec(),
                CodecValueConverter.toDomainVideoCodec(videoCodec));
        assertEquals(expectedFormat.getAudioCodec(),
                CodecValueConverter.toDomainAudioCodec(audioCodec));
    }

}

VideoConverterTest 클래스는 다음과 같이 VideoFormatVerifier를 이용해서 변환 결과를 검증하도록 했다.

public class VideoConverterTest {

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory
                .makeReader("src/test/resources/sample.avi");

        OutputFormat outputFormat = new OutputFormat(160, 120, 150,
                VideoCodec.H264, AudioCodec.AAC);
        VideoConverter writer = new VideoConverter("target/sample.mp4", reader,
                outputFormat);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File("target/sample.mp4"));
    }
}

위 테스트가 통과할 때 까지 VideoConverter를 지속적으로 수정해 나갔고, 최종적으로 녹색이 들어왔다.

테스트 코드의 몇 가지 값들을 상수로 바꾸면서 의미를 명확하게 바꾸자.

public class VideoConverterTest {

    private static final int WIDTH = 160;
    private static final int HEIGHT = 120;
    private static final int BITRATE = 150;
    private static final String SOURCE_FILE = "src/test/resources/sample.avi";
    private static final String TRANSCODED_FILE = "target/sample.mp4";

    @Test
    public void transcode() {
        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,
                VideoCodec.H264, AudioCodec.AAC);
        VideoConverter writer = new VideoConverter(TRANSCODED_FILE, reader,
                outputFormat);
        reader.addListener(writer);
        while (reader.readPacket() == null)
            do {
            } while (false);

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(TRANSCODED_FILE));
    }
}

다시 테스트. 녹색! 통과다.

FfmpegTranscoderTest로 돌아와 FfmpegTranscoder 구현하기

앞에서 작성한 VideoConverterTest 클래스의 transcode() 메서드는 FfmpegTranscoder#transcode() 메서드에서 비디오를 변환하기 위한 코드로 그대로 사용된다. 알맞게 FfmpegTranscoder#transcode()에 넣어보자.

public class FfmpegTranscoder implements Transcoder {

    @Override
    public List<File> transcode(File multimediaFile,
            List<OutputFormat> outputFormats) {
        List<File> results = new ArrayList<File>();
        for (OutputFormat format : outputFormats) {
            results.add(transcode(multimediaFile, format)); // 기존 new File(".")로 처리
        }
        return results;
    }

    private File transcode(File sourceFile, OutputFormat format) {
        IMediaReader reader = ToolFactory.makeReader(sourceFile
                .getAbsolutePath());

        String outputFile = "outputFile.mp4"; // 음, 일단 테스트 통과 위한 코딩
        VideoConverter converter = new VideoConverter(outputFile, reader,
                format);
        reader.addListener(converter);
        while (reader.readPacket() == null)
            do {
            } while (false);
        return new File(outputFile);
    }

}

FfmpegTranscoder 클래스를 작성했다. 음, 일단 테스트를 통과시키기 위해 결과 파일 이름을 지정했다. 마음에 안 드는 부분이다.

FfmpegTranscoder 클래스가 실제로 비디오 파일 변환을 처리하도록 구현했으니, 테스트를 실행해 보자. FfmpegTranscoderTest 클래스는 앞서 작성했던 VideoFormatVerifier를 이용해서 변환된 결과를 확인하도록 했다.

public class FfmpegTranscoderTest {

    private Transcoder transcoder;

    @Before
    public void setup() {
        transcoder = new FfmpegTranscoder();
    }

    @Test
    public void transcodeWithOnfOutputFormat() {
        File multimediaFile = new File("src/test/resources/sample.avi");
        List<OutputFormat> outputFormats = new ArrayList<OutputFormat>();
        outputFormats.add(new OutputFormat(160, 120, 150, VideoCodec.H264,
                AudioCodec.AAC));
        List<File> transcodedFiles = transcoder.transcode(multimediaFile,
                outputFormats);
        assertEquals(1, transcodedFiles.size());
        assertTrue(transcodedFiles.get(0).exists());
        VideoFormatVerifier.verifyVideoFormat(outputFormats.get(0),
                transcodedFiles.get(0));
    }
}

테스트 실행... 녹색이다. 야호!

다음에 할 것
  • 변환 결과로 생성되는 파일의 이름을 만들 정책이 필요하다.
  • MP4, AVI 등 컨테이너에 대한 정보 추가가 필요하다.
FfmpegTranscoder 클래스는 변환 내용에 상관없이 항상 outputFile.mp4라는 파일을 생성한다. AVI나 MP4, WMV 등 컨테이너에 종류를 지정하는 방법이 필요할 것 같고, 컨테이너 종류에 따라 생성되는 파일의 확장자가 변경되도록 하는 기능도 필요할 것 같다.

다음 TDD 연습 7에서는 이 두가지를 진행해보자.


+ Recent posts