“mitao”
2026-04-10 1e72a0b06e37d372c46522c85c6733edbcd5331d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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;
    }
}