package com.tcm.training.admin.upload;
|
|
import org.junit.jupiter.api.Test;
|
|
import java.io.ByteArrayOutputStream;
|
import java.nio.charset.StandardCharsets;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
/**
|
* 远程视频时长识别服务测试。
|
*/
|
class RemoteVideoDurationServiceTest {
|
|
private final RemoteVideoDurationService remoteVideoDurationService = new RemoteVideoDurationService();
|
|
/**
|
* 应支持解析 mvhd version 0 的 MP4 时长。
|
*/
|
@Test
|
void parseDurationSecondsShouldSupportVersionZeroMovieHeader() {
|
byte[] mvhdPayload = new byte[20];
|
mvhdPayload[12] = 0x00;
|
mvhdPayload[13] = 0x00;
|
mvhdPayload[14] = 0x03;
|
mvhdPayload[15] = (byte) 0xE8;
|
mvhdPayload[16] = 0x00;
|
mvhdPayload[17] = 0x00;
|
mvhdPayload[18] = 0x1D;
|
mvhdPayload[19] = 0x4C;
|
|
byte[] mp4Bytes = wrapMoovBox(wrapBox("mvhd", mvhdPayload));
|
|
assertEquals(8, remoteVideoDurationService.parseDurationSeconds(mp4Bytes));
|
}
|
|
/**
|
* 应支持解析 mvhd version 1 的 MP4 时长。
|
*/
|
@Test
|
void parseDurationSecondsShouldSupportVersionOneMovieHeader() {
|
byte[] mvhdPayload = new byte[32];
|
mvhdPayload[0] = 0x01;
|
mvhdPayload[20] = 0x00;
|
mvhdPayload[21] = 0x00;
|
mvhdPayload[22] = 0x03;
|
mvhdPayload[23] = (byte) 0xE8;
|
mvhdPayload[24] = 0x00;
|
mvhdPayload[25] = 0x00;
|
mvhdPayload[26] = 0x00;
|
mvhdPayload[27] = 0x00;
|
mvhdPayload[28] = 0x00;
|
mvhdPayload[29] = 0x00;
|
mvhdPayload[30] = 0x1D;
|
mvhdPayload[31] = 0x4C;
|
|
byte[] mp4Bytes = wrapMoovBox(wrapBox("mvhd", mvhdPayload));
|
|
assertEquals(8, remoteVideoDurationService.parseDurationSeconds(mp4Bytes));
|
}
|
|
/**
|
* 缺少 mvhd 盒子时应返回统一失败。
|
*/
|
@Test
|
void parseDurationSecondsShouldFailWhenMovieHeaderMissing() {
|
byte[] mp4Bytes = wrapMoovBox(wrapBox("trak", new byte[12]));
|
|
assertThrows(IllegalArgumentException.class, () -> remoteVideoDurationService.parseDurationSeconds(mp4Bytes));
|
}
|
|
/**
|
* 尾部分段中 moov 不在起始位置时也应可识别时长。
|
*/
|
@Test
|
void parseDurationSecondsFromTailChunkShouldSupportOffsetMoovBox() {
|
byte[] mvhdPayload = new byte[20];
|
mvhdPayload[12] = 0x00;
|
mvhdPayload[13] = 0x00;
|
mvhdPayload[14] = 0x03;
|
mvhdPayload[15] = (byte) 0xE8;
|
mvhdPayload[16] = 0x00;
|
mvhdPayload[17] = 0x00;
|
mvhdPayload[18] = 0x1D;
|
mvhdPayload[19] = 0x4C;
|
byte[] moovBytes = wrapMoovBox(wrapBox("mvhd", mvhdPayload));
|
byte[] chunkBytes = concatBytes(new byte[27], moovBytes);
|
|
assertEquals(8, remoteVideoDurationService.parseDurationSecondsFromTailChunk(chunkBytes));
|
}
|
|
/**
|
* 尾部分段中缺少完整 moov 盒子时应返回失败标记。
|
*/
|
@Test
|
void parseDurationSecondsFromTailChunkShouldFailWhenMoovBoxIncomplete() {
|
byte[] truncatedChunk = new byte[]{0x00, 0x00, 0x00, 0x20, 'm', 'o', 'o', 'v', 0x00, 0x00};
|
|
assertEquals(-1, remoteVideoDurationService.parseDurationSecondsFromTailChunk(truncatedChunk));
|
}
|
|
/**
|
* 应可从尾部分段中解析普通 size 的 moov 绝对范围。
|
*/
|
@Test
|
void resolveMoovRangeFromTailChunkShouldReturnSizedRange() {
|
byte[] moovBytes = wrapMoovBox(new byte[24]);
|
byte[] tailChunk = concatBytes(new byte[13], moovBytes);
|
long chunkStart = 1000L;
|
long totalLength = chunkStart + tailChunk.length;
|
|
RemoteVideoDurationService.ByteRange moovRange = remoteVideoDurationService.resolveMoovRangeFromTailChunk(
|
tailChunk,
|
chunkStart,
|
totalLength
|
);
|
|
assertEquals(chunkStart + 13L, moovRange.getStart());
|
assertEquals(chunkStart + 13L + moovBytes.length - 1L, moovRange.getEnd());
|
}
|
|
/**
|
* 应可解析 size=0 的 moov 范围,并将结束位置定位到文件末尾。
|
*/
|
@Test
|
void resolveMoovRangeFromTailChunkShouldSupportOpenEndedMoov() {
|
byte[] openEndedMoov = new byte[]{0x00, 0x00, 0x00, 0x00, 'm', 'o', 'o', 'v', 0x00, 0x00};
|
byte[] tailChunk = concatBytes(new byte[9], openEndedMoov);
|
long chunkStart = 2048L;
|
long totalLength = chunkStart + tailChunk.length + 321L;
|
|
RemoteVideoDurationService.ByteRange moovRange = remoteVideoDurationService.resolveMoovRangeFromTailChunk(
|
tailChunk,
|
chunkStart,
|
totalLength
|
);
|
|
assertEquals(chunkStart + 9L, moovRange.getStart());
|
assertEquals(totalLength - 1L, moovRange.getEnd());
|
}
|
|
/**
|
* 包装 moov 盒子。
|
*/
|
private byte[] wrapMoovBox(byte[] childBytes) {
|
return wrapBox("moov", childBytes);
|
}
|
|
/**
|
* 构造一个标准 MP4 盒子。
|
*/
|
private byte[] wrapBox(String type, byte[] payload) {
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
int boxSize = 8 + payload.length;
|
outputStream.write((byte) (boxSize >> 24));
|
outputStream.write((byte) (boxSize >> 16));
|
outputStream.write((byte) (boxSize >> 8));
|
outputStream.write((byte) boxSize);
|
outputStream.write(type.getBytes(StandardCharsets.US_ASCII), 0, 4);
|
outputStream.write(payload, 0, payload.length);
|
return outputStream.toByteArray();
|
}
|
|
/**
|
* 拼接多段字节数组。
|
*/
|
private byte[] concatBytes(byte[] left, byte[] right) {
|
byte[] merged = new byte[left.length + right.length];
|
System.arraycopy(left, 0, merged, 0, left.length);
|
System.arraycopy(right, 0, merged, left.length, right.length);
|
return merged;
|
}
|
}
|