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