Skip to content

Commit 4d3402b

Browse files
committed
Add mov and webm probe validation
Add MOV and WebM container support to probing and strict validation. Detect both formats by header signatures, return video stream metadata, and map MIME/media types for extension fallback. Include facade tests with minimal MOV/WebM fixtures for probe and strict validation coverage, plus a guard test for unsupported mov->webm video-to-video conversion routing. Update README and Maven metadata to document the new format support and refresh project/logo URLs, and bump the project version to 1.0.2
1 parent ea82926 commit 4d3402b

File tree

11 files changed

+1053
-9
lines changed

11 files changed

+1053
-9
lines changed

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [1.0.2] - 2026-03-02
9+
10+
### Added
11+
- Added MOV parser/probe support via [`MovParser`](src/main/java/me/tamkungz/codecmedia/internal/video/mov/MovParser.java).
12+
- Added WebM parser/probe support via [`WebmParser`](src/main/java/me/tamkungz/codecmedia/internal/video/webm/WebmParser.java).
13+
- Added MOV/WebM codec wrappers ([`MovCodec`](src/main/java/me/tamkungz/codecmedia/internal/video/mov/MovCodec.java), [`WebmCodec`](src/main/java/me/tamkungz/codecmedia/internal/video/webm/WebmCodec.java)) and probe info records.
14+
- Added facade tests for MOV/WebM probe and strict validation in [`CodecMediaFacadeTest`](src/test/java/me/tamkungz/codecmedia/CodecMediaFacadeTest.java).
15+
16+
### Changed
17+
- Extended probing and strict validation routing in [`StubCodecMediaEngine`](src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java) to cover MOV and WebM.
18+
- Updated project metadata URLs and branding references in [`pom.xml`](pom.xml) and [`README.md`](README.md).
19+
20+
### Fixed
21+
- Fixed EBML element decoding in [`WebmParser`](src/main/java/me/tamkungz/codecmedia/internal/video/webm/WebmParser.java) so multi-byte element IDs (including `DefaultDuration`) are parsed with correct ID/size boundaries.
22+
- Fixed MOV frame-rate extraction in [`MovParser`](src/main/java/me/tamkungz/codecmedia/internal/video/mov/MovParser.java) to use `mdhd` track timescale when interpreting `stts` sample delta.
23+
24+
## [1.0.1] - 2026-03-02
25+
26+
### Added
27+
- Initial public release with probing, validation, metadata sidecar persistence, extraction workflow, playback simulation, and conversion routing.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
CodecMedia is a Java library for media probing, validation, metadata sidecar persistence, audio extraction, playback workflow simulation, and conversion routing.
66

77
<p align="center">
8-
<img src="https://www.tamkungz.me/assets-image/CodecMedia_Full_Logo.png" width="70%" alt="CodecMedia Logo">
8+
<img src="https://codecmedia.tamkungz.me/CodecMedia_Full_Logo.png" width="70%" alt="CodecMedia Logo">
99
</p>
1010

1111
## Features
@@ -21,8 +21,10 @@ CodecMedia is a Java library for media probing, validation, metadata sidecar per
2121
- BMP
2222
- TIFF
2323
- HEIC/HEIF/AVIF (basic BMFF parsing)
24+
- MOV (QuickTime container parsing)
2425
- MP4 (basic ISO BMFF parsing)
25-
- Validation with size limits and strict parser checks for MP3/OGG/WAV/PNG/JPEG/WebP/BMP/TIFF/HEIC/HEIF/AVIF/MP4
26+
- WebM (EBML container parsing)
27+
- Validation with size limits and strict parser checks for MP3/OGG/WAV/PNG/JPEG/WebP/BMP/TIFF/HEIC/HEIF/AVIF/MOV/MP4/WebM
2628
- Metadata read/write with sidecar persistence (`.codecmedia.properties`)
2729
- In-Java extraction and conversion file operations
2830
- Image-to-image conversion in Java for: `png`, `jpg`/`jpeg`, `webp`, `bmp`, `tif`/`tiff`, `heic`/`heif`
@@ -94,4 +96,4 @@ This project is licensed under the Apache License 2.0.
9496

9597
---
9698

97-
*by TamKungZ_*
99+
*by TamKungZ_*

pom.xml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77

88
<groupId>me.tamkungz.codecmedia</groupId>
99
<artifactId>codecmedia</artifactId>
10-
<version>1.0.1</version>
10+
<version>1.0.2</version>
1111
<packaging>jar</packaging>
1212

1313
<name>CodecMedia</name>
1414
<description>
1515
Universal media processing library for Java.
16-
Supports probing, metadata reading/writing, container parsing,
17-
audio/video extraction and format conversion.
16+
CodecMedia is a Java library for media probing, validation, metadata sidecar persistence,
17+
audio extraction, playback workflow simulation, and conversion routing. (Image/Video/Audio)
1818
</description>
19-
<url>https://github.com/TamKungZ/codecmedia-java</url>
19+
<url>https://codecmedia.tamkungz.me/</url>
2020

2121
<organization>
2222
<name>TamKungZ_</name>
@@ -78,7 +78,8 @@
7878

7979
<gpg.keyname>7311416EAA9848E4</gpg.keyname>
8080

81-
<project.logo.url>https://www.tamkungz.me/assets-image/CodecMedia_Logo.png</project.logo.url>
81+
<project.logo.url>https://codecmedia.tamkungz.me/CodecMedia_Icon_Logo.png</project.logo.url>
82+
<project.full.logo.url>https://codecmedia.tamkungz.me/CodecMedia_Full_Logo.png</project.full.logo.url>
8283
</properties>
8384

8485
<dependencies>

src/main/java/me/tamkungz/codecmedia/internal/StubCodecMediaEngine.java

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
import me.tamkungz.codecmedia.internal.video.mp4.Mp4Codec;
4444
import me.tamkungz.codecmedia.internal.video.mp4.Mp4Parser;
4545
import me.tamkungz.codecmedia.internal.video.mp4.Mp4ProbeInfo;
46+
import me.tamkungz.codecmedia.internal.video.mov.MovCodec;
47+
import me.tamkungz.codecmedia.internal.video.mov.MovParser;
48+
import me.tamkungz.codecmedia.internal.video.mov.MovProbeInfo;
49+
import me.tamkungz.codecmedia.internal.video.webm.WebmCodec;
50+
import me.tamkungz.codecmedia.internal.video.webm.WebmParser;
51+
import me.tamkungz.codecmedia.internal.video.webm.WebmProbeInfo;
4652
import me.tamkungz.codecmedia.model.ConversionResult;
4753
import me.tamkungz.codecmedia.model.ExtractionResult;
4854
import me.tamkungz.codecmedia.model.MediaType;
@@ -287,6 +293,54 @@ public ProbeResult probe(Path input) throws CodecMediaException {
287293
return new ProbeResult(input, mimeType, outputExt, MediaType.IMAGE, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
288294
}
289295

296+
if ("mov".equals(extension) || MovParser.isLikelyMov(bytes)) {
297+
try {
298+
MovProbeInfo info = MovCodec.decode(bytes, input);
299+
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
300+
tags.put("sizeBytes", String.valueOf(size));
301+
if (info.majorBrand() != null && !info.majorBrand().isBlank()) {
302+
tags.put("majorBrand", info.majorBrand());
303+
}
304+
if (info.videoCodec() != null && !info.videoCodec().isBlank()) {
305+
tags.put("videoCodec", info.videoCodec());
306+
}
307+
if (info.audioCodec() != null && !info.audioCodec().isBlank()) {
308+
tags.put("audioCodec", info.audioCodec());
309+
}
310+
311+
java.util.ArrayList<StreamInfo> streams = new java.util.ArrayList<>();
312+
if (info.width() != null && info.height() != null && info.width() > 0 && info.height() > 0) {
313+
streams.add(new StreamInfo(0, StreamKind.VIDEO, info.videoCodec() != null ? info.videoCodec() : "unknown", null, null, null, info.width(), info.height(), info.frameRate()));
314+
}
315+
if (info.sampleRate() != null && info.channels() != null && info.sampleRate() > 0 && info.channels() > 0) {
316+
streams.add(new StreamInfo(
317+
streams.size(),
318+
StreamKind.AUDIO,
319+
info.audioCodec() != null ? info.audioCodec() : "unknown",
320+
null,
321+
info.sampleRate(),
322+
info.channels(),
323+
null,
324+
null,
325+
null
326+
));
327+
}
328+
329+
return new ProbeResult(
330+
input,
331+
"video/quicktime",
332+
"mov",
333+
MediaType.VIDEO,
334+
info.durationMillis(),
335+
List.copyOf(streams),
336+
tags
337+
);
338+
} catch (CodecMediaException ignored) {
339+
// Fall back to extension-only probe for malformed/partial files.
340+
}
341+
return new ProbeResult(input, "video/quicktime", "mov", MediaType.VIDEO, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
342+
}
343+
290344
if ("mp4".equals(extension) || "m4a".equals(extension) || Mp4Parser.isLikelyMp4(bytes)) {
291345
String outputExt = "m4a".equals(extension) ? "m4a" : "mp4";
292346
String mimeType = "m4a".equals(outputExt) ? "audio/mp4" : "video/mp4";
@@ -321,8 +375,55 @@ public ProbeResult probe(Path input) throws CodecMediaException {
321375
return new ProbeResult(input, mimeType, outputExt, mediaType, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
322376
}
323377

378+
if ("webm".equals(extension) || WebmParser.isLikelyWebm(bytes)) {
379+
try {
380+
WebmProbeInfo info = WebmCodec.decode(bytes, input);
381+
java.util.LinkedHashMap<String, String> tags = new java.util.LinkedHashMap<>();
382+
tags.put("sizeBytes", String.valueOf(size));
383+
if (info.videoCodec() != null && !info.videoCodec().isBlank()) {
384+
tags.put("videoCodec", info.videoCodec());
385+
}
386+
if (info.audioCodec() != null && !info.audioCodec().isBlank()) {
387+
tags.put("audioCodec", info.audioCodec());
388+
}
389+
390+
java.util.ArrayList<StreamInfo> streams = new java.util.ArrayList<>();
391+
if (info.width() != null && info.height() != null && info.width() > 0 && info.height() > 0) {
392+
streams.add(new StreamInfo(0, StreamKind.VIDEO, info.videoCodec() != null ? info.videoCodec() : "unknown", null, null, null, info.width(), info.height(), info.frameRate()));
393+
}
394+
if (info.sampleRate() != null && info.channels() != null && info.sampleRate() > 0 && info.channels() > 0) {
395+
streams.add(new StreamInfo(
396+
streams.size(),
397+
StreamKind.AUDIO,
398+
info.audioCodec() != null ? info.audioCodec() : "unknown",
399+
null,
400+
info.sampleRate(),
401+
info.channels(),
402+
null,
403+
null,
404+
null
405+
));
406+
}
407+
408+
return new ProbeResult(
409+
input,
410+
"video/webm",
411+
"webm",
412+
MediaType.VIDEO,
413+
info.durationMillis(),
414+
List.copyOf(streams),
415+
tags
416+
);
417+
} catch (CodecMediaException ignored) {
418+
// Fall back to extension-only probe for malformed/partial files.
419+
}
420+
return new ProbeResult(input, "video/webm", "webm", MediaType.VIDEO, null, List.of(), Map.of("sizeBytes", String.valueOf(size)));
421+
}
422+
324423
String mimeType = switch (extension) {
325424
case "mp4" -> "video/mp4";
425+
case "mov" -> "video/quicktime";
426+
case "webm" -> "video/webm";
326427
case "m4a" -> "audio/mp4";
327428
case "mp3" -> "audio/mpeg";
328429
case "ogg" -> "audio/ogg";
@@ -338,7 +439,7 @@ public ProbeResult probe(Path input) throws CodecMediaException {
338439
default -> "application/octet-stream";
339440
};
340441
MediaType mediaType = switch (extension) {
341-
case "mp4" -> MediaType.VIDEO;
442+
case "mp4", "mov", "webm" -> MediaType.VIDEO;
342443
case "m4a", "mp3", "ogg", "wav" -> MediaType.AUDIO;
343444
case "png", "jpg", "jpeg", "webp", "bmp", "tif", "tiff", "heic", "heif", "avif" -> MediaType.IMAGE;
344445
default -> MediaType.UNKNOWN;
@@ -559,12 +660,24 @@ public ValidationResult validate(Path input, ValidationOptions options) {
559660
} catch (CodecMediaException e) {
560661
return new ValidationResult(false, List.of(), List.of("Strict validation failed for jpg/jpeg: " + e.getMessage()));
561662
}
663+
} else if ("mov".equals(extension)) {
664+
try {
665+
MovParser.parse(bytes);
666+
} catch (CodecMediaException e) {
667+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for mov: " + e.getMessage()));
668+
}
562669
} else if ("mp4".equals(extension) || "m4a".equals(extension)) {
563670
try {
564671
Mp4Parser.parse(bytes);
565672
} catch (CodecMediaException e) {
566673
return new ValidationResult(false, List.of(), List.of("Strict validation failed for " + extension + ": " + e.getMessage()));
567674
}
675+
} else if ("webm".equals(extension)) {
676+
try {
677+
WebmParser.parse(bytes);
678+
} catch (CodecMediaException e) {
679+
return new ValidationResult(false, List.of(), List.of("Strict validation failed for webm: " + e.getMessage()));
680+
}
568681
} else if ("webp".equals(extension)) {
569682
try {
570683
WebpParser.parse(bytes);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package me.tamkungz.codecmedia.internal.video.mov;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
7+
import me.tamkungz.codecmedia.CodecMediaException;
8+
9+
public final class MovCodec {
10+
11+
private MovCodec() {
12+
}
13+
14+
public static MovProbeInfo decode(Path input) throws CodecMediaException {
15+
try {
16+
byte[] bytes = Files.readAllBytes(input);
17+
return decode(bytes, input);
18+
} catch (IOException e) {
19+
throw new CodecMediaException("Failed to decode MOV: " + input, e);
20+
}
21+
}
22+
23+
public static MovProbeInfo decode(byte[] bytes, Path sourceRef) throws CodecMediaException {
24+
MovProbeInfo info = MovParser.parse(bytes);
25+
validateDecodedProbe(info, sourceRef);
26+
return info;
27+
}
28+
29+
public static void encode(byte[] encodedMovData, Path output) throws CodecMediaException {
30+
if (encodedMovData == null || encodedMovData.length == 0) {
31+
throw new CodecMediaException("MOV encoded data is empty");
32+
}
33+
try {
34+
Files.write(output, encodedMovData);
35+
} catch (IOException e) {
36+
throw new CodecMediaException("Failed to encode MOV: " + output, e);
37+
}
38+
}
39+
40+
private static void validateDecodedProbe(MovProbeInfo info, Path input) throws CodecMediaException {
41+
if (info.majorBrand() == null || info.majorBrand().isBlank()) {
42+
throw new CodecMediaException("Decoded MOV has invalid major brand: " + input);
43+
}
44+
if (info.width() != null && info.width() <= 0) {
45+
throw new CodecMediaException("Decoded MOV has invalid width: " + input);
46+
}
47+
if (info.height() != null && info.height() <= 0) {
48+
throw new CodecMediaException("Decoded MOV has invalid height: " + input);
49+
}
50+
if (info.sampleRate() != null && info.sampleRate() <= 0) {
51+
throw new CodecMediaException("Decoded MOV has invalid sample rate: " + input);
52+
}
53+
if (info.channels() != null && info.channels() <= 0) {
54+
throw new CodecMediaException("Decoded MOV has invalid channel count: " + input);
55+
}
56+
}
57+
}
58+

0 commit comments

Comments
 (0)