지금까지 한 번에 한 개의 변환 결과만 생성하는 기능을 구현했다. 이번엔 한 번에 여러 형식의 변환 결과를 생성해주는 기능을 구현해보도록 하자.
두 개 이상의 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() 메서드를 조금 더 정리하고 싶은 마음은 있지만, 일단 이 정도까지 코드를 정리하자.