주요글: 도커 시작하기

지금까지 한 번에 한 개의 변환 결과만 생성하는 기능을 구현했다. 이번엔 한 번에 여러 형식의 변환 결과를 생성해주는 기능을 구현해보도록 하자.


두 개 이상의 OutputFormat을 입력으로 받는 테스트 추가하기


TDD니까, 테스트를 먼저 작성해보도록 하자.


public class FfmpegTranscoderTest {

    ...

    private OutputFormat mp4Format;

    private OutputFormat mp4Format2;

    private OutputFormat aviFormat;


    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        mp4Format2 = new OutputFormat(80, 60, 80, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        aviFormat = new OutputFormat(160, 120, 150, Container.AVI,

                VideoCodec.MPEG4, AudioCodec.MP3);

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


        transcoder = new FfmpegTranscoder();

    }

    ...

    private void executeTranscoderAndAssert() {

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

                outputFormats);

        assertEquals(outputFormats.size(), transcodedFiles.size());

        for (int i = 0; i < outputFormats.size(); i++) {

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

            VideoFormatVerifier.verifyVideoFormat(outputFormats.get(i),

                    transcodedFiles.get(i));

        }

    }


    @Test

    public void transcodeWithTwoMp4OutputFormats() {

        outputFormats.add(mp4Format);

        outputFormats.add(mp4Format2);

        executeTranscoderAndAssert();

    }

}


새로운 크기를 갖는 OutputFormat 객체인 mp4Format2를 필드로 추가하였고, transcodeWithTwoMp4OutputFormats() 메서드를 이용해서 두 개의 변환 결과에 대한 테스트를 수행하도록 했다. executeTrancoderAndAssert() 메서드는 두 개 이상의 변환 요청을 검증할 수 있도록 수정하였다.


테스트를 실행해보자. 아래와 같은 메시지와 함께 테스트에 실패한다.


java.lang.AssertionError: expected:<160> but was:<80>

    at org.junit.Assert.fail(Assert.java:91)

    at org.junit.Assert.failNotEquals(Assert.java:645)

    at org.junit.Assert.assertEquals(Assert.java:126)

    at org.junit.Assert.assertEquals(Assert.java:470)

    at org.junit.Assert.assertEquals(Assert.java:454)

    at org.chimi....VideoFormatVerifier.assertVideoFile(VideoFormatVerifier.java:88)

    at org.chimi....VideoFormatVerifier.verify(VideoFormatVerifier.java:40)

    at org.chimi....VideoFormatVerifier.verifyVideoFormat(VideoFormatVerifier.java:18)

    at org.chimi.....executeTranscoderAndAssert(FfmpegTranscoderTest.java:64)

    at org.chimi....transcodeWithTwoMp4OutputFormats(FfmpegTranscoderTest.java:79)

    ... 


이유는 뭘까? FfmpegTranscoder 클래스는 다음과 같이 동일한 비디오 컨테이너에 대해 항상 동일한 파일을 생성한다. 따라서, 가장 마지막으로 요청한 OutputFormat의 결과만 최종적으로 남아있게 된다.


public class FfmpegTranscoder implements Transcoder {

    ...

    private String getFileName(OutputFormat format) {

        return "outputFile." + format.getFileExtension();

    }


}


예를 들어, 위 테스트의 경우 크기가 80*60인 비디오 파일이 최종적으로 남게 되고, 따라서 생성된 비디오를 검증하는 과정에서 첫 번째 변환 요청인 160*120 크기를 검사하는 과정에서 테스트를 통과하지 못하게 되었다.


이 테스트를 통과시키려면 FfmpegTranscoder의 getFileName() 메서드가 매번 서로 다른 파일 이름을 리턴해 주어야 한다. 변환된 결과 파일 이름의 경우 변환 시스템을 사용하는 곳에 따라 작명 규칙이 달라질 수 있을 것 같다. 그래서, 파일 이름을 생성하는 규칙을 NamingRule이라는 별도 인터페이스로 분리하고 FfmpegTranscoder는 NamingRule에 위임하도록 코드를 구성하는 게 좋을 것 같다. 아래는 그렇게 코딩한 결과이다.


public class FfmpegTranscoder implements Transcoder {


    private NamingRule namingRule;


    public FfmpegTranscoder(NamingRule namingRule) {

        this.namingRule = namingRule;

    }

    ...

    private String getFileName(OutputFormat format) {

        return namingRule.createName(format);

    }


}


NamingRule 인터페이스를 다음과 같이 만들었다.


public interface NamingRule {


    String createName(OutputFormat format);


}


FfmpegTranscoder의 생성자가 변경되었으므로, FfmpegTranscoderTest 클래스가 컴파일 에러가 난다. 컴파일 에러가 나지 않도록 NamingRule을 전달해주자.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;

    ...

    private NamingRule namingRule;

    ...

    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        ...

        transcoder = new FfmpegTranscoder(namingRule); // 아직 namingRule은 null

    }


NamingRule의 Mock 객체를 만들어서 테스트를 통과시킬까 고민하다가, DefaultNamingRule이라는 걸 만드는 게 좋을 것 같다라는 결론에 도달했다. 다음과 같이 NamingRule 인터페이스에 이너 클래스로 DefaultNamingRule 클래스를 정의했고, DEFAULT 상수가 값으로 DefaultNameRule 객체를 갖도록 했다.


public interface NamingRule {


    String createName(OutputFormat format);


    public static final NamingRule DEFAULT = new DefaultNamingRule();


    public static class DefaultNamingRule implements NamingRule {


        private Random random = new Random();

        private String baseDir;


        public DefaultNamingRule() {

            baseDir = System.getProperty("java.io.tmpdir");

        }


        public void setBaseDir(String baseDir) {

            this.baseDir = baseDir;

        }


        @Override

        public String createName(OutputFormat format) {

            String fileName = getFileNameFromTime();

            File file = createFileFromFileNameAndFormat(format, baseDir,

                    fileName);

            return file.getPath();

        }


        private String getFileNameFromTime() {

            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");

            String time = dateFormat.format(new Date());

            int num = random.nextInt(1000);

            String fileName = time + "_" + num;

            return fileName;

        }


        private File createFileFromFileNameAndFormat(OutputFormat format,

                String tempDir, String fileName) {

            File file = new File(tempDir, fileName + "."

                    + format.getFileExtension());

            return file;

        }

    }

}


이제 FfmpegTranscoderTest가 NamingRule로 NamingRule.DEFAULT를 사용하도록 수정할 차례이다.


public class FfmpegTranscoderTest {


    private Transcoder transcoder;

    private File multimediaFile;

    private List<OutputFormat> outputFormats;


    private NamingRule namingRule; // 필요 없으므로 삭제


    private OutputFormat mp4Format;

    private OutputFormat mp4Format2;

    private OutputFormat aviFormat;


    @Before

    public void setup() {

        outputFormats = new ArrayList<OutputFormat>();

        mp4Format = new OutputFormat(160, 120, 150, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        mp4Format2 = new OutputFormat(80, 60, 80, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        aviFormat = new OutputFormat(160, 120, 150, Container.AVI,

                VideoCodec.MPEG4, AudioCodec.MP3);

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


        transcoder = new FfmpegTranscoder(NamingRule.DEFAULT);

    }


테스트를 실행한다. 녹색! 통과다. 서로 다른 포맷을 갖는 OutputFormat을 생성하는 기능 구현이 완료되었다.


OutputFormat: 컨테이너와 코덱 옵션


OutputFormat의 생성자는 컨테이너 정보와 코덱 정보를 함께 받고 있다. 하지만, 컨테이너의 기본 코덱을 사용할 수 있기 때문에 코덱 정보를 전달하지 않을 경우 컨테이너의 기본 코덱을 사용하도록 하는 것이 편리할 것 같다. 이를 위해 OutputFormat 생성자에 코덱 정보를 전달하지 않는 테스트 코드부터 작성해보자. OutputFormat를 생성할 때 컨테이너 정보만 전달해도 정상적으로 실행되는 지 확인하면 되므로  VideoConverterTest를 이용해보도록 하겠다.


public class VideoConverterTest {

    ...

    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI); // 일치하는 생성자 없으므로 컴파일 에러

        VideoConverter writer = new VideoConverter("target/sample.avi", reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);

        

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(

                "target/sample.avi"));

    }

}


컴파일 에러를 없애야 하므로 OutputFormat에 생성자를 추가해주고, 다음과 같이 구현해 주었다.


public class OutputFormat {


    private int width;

    private int height;

    private int bitrate;

    private Container container;

    private VideoCodec videoCodec;

    private AudioCodec audioCodec;


    public OutputFormat(int width, int height, int bitrate,

            Container container, VideoCodec videoCodec, AudioCodec audioCodec) {

        this.width = width;

        this.height = height;

        this.bitrate = bitrate;

        this.container = container;

        this.videoCodec = videoCodec;

        this.audioCodec = audioCodec;

    }


    public OutputFormat(int width, int height, int bitrate, Container container) {

        this(width, height, bitrate, container, null, null);

    }


컴파일 에러를 없앴으므로, VideoConverterTest를 실행해보자. transcodeWithOnlyContainer() 테스트 메서드는 통과하지 못했다. 실패 메시지 및 관련 위치는 다음과 같다.


-- 실패 메시지

java.lang.AssertionError: expected:<null> but was:<MPEG4>

    at org.junit.Assert.fail(Assert.java:91)

    at org.junit.Assert.failNotEquals(Assert.java:645)

    at org.junit.Assert.assertEquals(Assert.java:126)

    at org.junit.Assert.assertEquals(Assert.java:145)

    at org....VideoFormatVerifier.assertVideoFile(VideoFormatVerifier.java:90)

    at org....VideoFormatVerifier.verify(VideoFormatVerifier.java:40)

    at org....VideoFormatVerifier.verifyVideoFormat(VideoFormatVerifier.java:18)

    at org....VideoConverterTest.transcodeWithOnlyContainer(VideoConverterTest.java:52)


-- 실패 위치

    private void assertVideoFile() {

        assertEquals(expectedFormat.getWidth(), width);

        assertEquals(expectedFormat.getHeight(), height);

        assertEquals(expectedFormat.getVideoCodec(),

                CodecValueConverter.toDomainVideoCodec(videoCodec)); // 테스트 실패

        assertEquals(expectedFormat.getAudioCodec(),

                CodecValueConverter.toDomainAudioCodec(audioCodec));

    }


OutputFormat을 생성하는 과정에서 videoCodec의 값으로 null을 전달했기 때문에, expectedFormat.getVideoCodec()가 null을 리턴했다. 동일하게 audioCodec도 null 값을 갖게 된다. 이 문제는 간단하게 처리할 수 있다. 다음과 같이 코덱을 전달받지 않는 OutputFormat 생성자에서 null 대신 Container의 기본 코덱을 전달하도록 코드를 수정해 주면 될 것 같다.


public class OutputFormat {

    ...

    public OutputFormat(int width, int height, int bitrate,

            Container container, VideoCodec videoCodec, AudioCodec audioCodec) {

        ...

        this.videoCodec = videoCodec;

        this.audioCodec = audioCodec;

    }


    public OutputFormat(int width, int height, int bitrate, Container container) {

        this(width, height, bitrate, container, 

                container.getDefaultVideoCodec(), container.getDefaultAudioCodec());

    }


다시 VideoConverterTest를 실행해 보자. 녹색! 통과다.


VideoConverterTest 작은 손질


VideoConverterTest를 정리할 타이밍이 왔다. 현재까지 만들어진 코드는 다음과 같다.


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,

                Container.MP4, 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));

    }

    

    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        VideoConverter writer = new VideoConverter("target/sample.avi", reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);

        

        VideoFormatVerifier.verifyVideoFormat(outputFormat, new File(

                "target/sample.avi"));

    }

}


두 테스트 메서드가 상당히 비슷하다. 우선, 두 메서드에서 사용되는 값들을 메서드의 앞 쪽에 변수로 선언해서 모으자.


public class VideoConverterTest {

    ...

    @Test

    public void transcode() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        String outputFile = TRANSCODED_FILE;

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        IMediaReader reader = ToolFactory.makeReader(SOURCE_FILE);

        OutputFormat outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        String outputFile = "target/sample.avi";

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


두 메서드에서 reader, outputFormat, outputFile을 필드로 정의한다. 이클립스와 같은 IDE는 메서드 로컬 변수를 필드로 빼주는 기능을 제공하고 있으니 그 기능을 사용하면 편리하다. 아 그 전에 테스트 하는 것 잊지 말기 바란다.


public class VideoConverterTest {

    ...

    private IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Test

    public void transcode() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.MP4, VideoCodec.H264, AudioCodec.AAC);

        outputFile = TRANSCODED_FILE;

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                Container.AVI);

        outputFile = "target/sample.avi";

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


이제 두 메서드에서 코드가 완전히 동일한 부분을 별도의 메서드로 분리해내자. 다음의 두 부분이 완전히 동일하다.

  • reader를 생성하는 부분 -> @Before를 붙인 setup 메서드로 이동
  • VideoConverter를 생성하고 변환하는 부분 -> testVideoConverter 메서드로 이동

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";

    private IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Before

    public void setup() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

    }


    @Test

    public void transcode() {

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE, Container.MP4,

                VideoCodec.H264, AudioCodec.AAC);

        outputFile = TRANSCODED_FILE;

        testVideoConverter();

    }


    private void testVideoConverter() {

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }


    @Test

    public void transcodeWithOnlyContainer() {

        outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE, Container.AVI);

        outputFile = "target/sample.avi";

        testVideoConverter();

    }

}


outputFormat과 outputFile을 초기화하는 부분도 비슷하다. 이 부분도 한 번 별도 메서드로 분리해보자.


public class VideoConverterTest {

    ...

    @Test

    public void transcode() {

        initOutput(Container.MP4, VideoCodec.H264, AudioCodec.AAC,

                TRANSCODED_FILE);

        testVideoConverter();

    }


    private void initOutput(Container outputContainer, VideoCodec videoCodec,

            AudioCodec audioCodec, String outputFileName) {

        if (videoCodec == null && audioCodec == null) {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer);

        } else {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer, videoCodec, audioCodec);

        }

        outputFile = outputFileName;

    }


    private void initOutput(Container outputContainer, String outputFileName) {

        initOutput(outputContainer, null, null, outputFileName);

        outputFile = outputFileName;

    }


    private void testVideoConverter() {

        ...

    }


    @Test

    public void transcodeWithOnlyContainer() {

        initOutput(Container.AVI, "target/sample.avi");

        testVideoConverter();

    }

}


이제 TRANSCODED_FILE 상수는 더 이상 의미가 없다. 이 상수를 사용하지 않도록 돌려 놓자.


지금까지의 작업으로 바뀐 VideoConverterTest는 다음과 같다.


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 IMediaReader reader;

    private OutputFormat outputFormat;

    private String outputFile;


    @Before

    public void setup() {

        reader = ToolFactory.makeReader(SOURCE_FILE);

    }


    @Test

    public void transcode() {

        initOutput(Container.MP4, VideoCodec.H264, AudioCodec.AAC,

                "target/sample.mp4");

        testVideoConverter();

    }


    @Test

    public void transcodeWithOnlyContainer() {

        initOutput(Container.AVI, "target/sample.avi");

        testVideoConverter();

    }


    private void initOutput(Container outputContainer, String outputFileName) {

        initOutput(outputContainer, null, null, outputFileName);

        outputFile = outputFileName;

    }


    private void initOutput(Container outputContainer, VideoCodec videoCodec,

            AudioCodec audioCodec, String outputFileName) {

        if (videoCodec == null && audioCodec == null) {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer);

        } else {

            outputFormat = new OutputFormat(WIDTH, HEIGHT, BITRATE,

                    outputContainer, videoCodec, audioCodec);

        }

        outputFile = outputFileName;

    }


    private void testVideoConverter() {

        VideoConverter writer = new VideoConverter(outputFile, reader,

                outputFormat);

        reader.addListener(writer);

        while (reader.readPacket() == null)

            do {

            } while (false);


        VideoFormatVerifier.verifyVideoFormat(outputFormat,

                new File(outputFile));

    }

}


@Test 메서드가 단순해졌다. 새로운 컨테이너의 추가라든가, 코덱 변환 등을 테스트하고 싶을 때에 간단하게 테스트를 추가할 수 있게 되었다. testVideoConverter() 메서드와 initOutput() 메서드를 조금 더 정리하고 싶은 마음은 있지만, 일단 이 정도까지 코드를 정리하자.




  1. 전홍석 2015.12.04 11:08

    안녕하세요
    스프링 4 프로그래밍 입문을 구입하여 열공중인 중년 초보개발자 입니다.
    대용량의 동영상 converter작업할일이 있어 찾아보니 저자님 글이 있어
    학습하여 익히려 합니다.
    염치 불구하고 위 전체 소스를 받아서 도움받을수 있도록 부탁드립니다.


    • 최범균 madvirus 2015.12.04 14:31 신고

      아,,, 이게 그냥 막 연습삼아서 하던거라서,,,, 코드가 있는지 모르겠네요. 업무용 장비에는 코드가 없네요. 아무래도 내용 보시면 관련 코드가 나오니까, 그 코드를 참조하셔야 할 것 같아요.

+ Recent posts