DirectShow source/splitter filter for MMT/TLV .mmts files and .mmtsedit
edit decision lists.
This filter was built for playback/debugging with MPC-BE and MPC-HC. It uses
the dantto4k demuxer sources for MMT/TLV parsing and exposes HEVC video, AAC
audio, and ARIB B24 subtitle streams as DirectShow output pins.
- Loads
.mmtsand.mmtseditfiles throughIFileSourceFilter. - Registers
.mmtsand.mmtseditas DirectShow source filter extensions. - Exposes HEVC video output.
- Exposes each MPT
mp4aaudio stream as a separate audio output pin. - Outputs ordinary AAC streams as ADTS/
MEDIASUBTYPE_RAW_AAC1. - Converts AAC LATM 22.2ch audio streams to stereo PCM output for playback.
- Preserves ordinary AAC 2ch/5.1ch streams as separate ADTS output pins when they coexist with a 22.2ch stream.
- Supports DirectShow seeking through
IMediaSeeking. - Queues downstream delivery to avoid renderer/decoder blocking the demuxer.
- Starts playback and seek recovery from HEVC RAP/IRAP frames.
- Converts ARIB B24 TTML subtitles to ASS with per-character cell layout.
- Renders cell backgrounds as merged rectangles (no edge artifacts).
- Renders DRCS (broadcaster-defined) glyphs as ASS vector drawings.
src/ Filter implementation
msvc/ Visual Studio project and module definition
settings/ Sample INI configuration file
tools/ Local probe/debug helper sources
scripts/ Registration helper scripts
baseclasses/ DirectShow BaseClasses copy from Microsoft samples
thirdparty/ Static FFmpeg libraries and headers (see thirdparty/ffmpeg/NOTICE.txt)
Build outputs are intentionally ignored by Git.
- Windows
- Visual Studio 2022 with C++ desktop workload
- Windows SDK
dantto4kchecked out next to this repository:
<workspace>\
dantto4k\
mmts-dsfilter\
The Visual Studio project currently references ..\..\dantto4k from
msvc/mmts-dsfilter.vcxproj.
FFmpeg (AAC LATM decoding for 22.2ch audio) is linked statically from
thirdparty/ffmpeg, so neither vcpkg nor FFmpeg DLLs are needed. To rebuild
those libraries, run thirdparty\ffmpeg\build_static_msvc.cmd <ffmpeg-source>
with an FFmpeg n9.0.1 checkout; see thirdparty/ffmpeg/NOTICE.txt.
From the repository root:
.\build.ps1 -Configuration Debugor:
.\build.ps1 -Configuration ReleaseThe filter is produced as:
msvc\x64\<Configuration>\mmts-dsfilter.ax
Before building, tools/generate-version-header.ps1 writes the version this build comes from (git describe --tags --always --dirty, e.g. v20261015-6-gc8a6086) to src/MmtsDsfilterVersion.h, which is not committed (like TVTest's TVTestVersionHashGen.bat). It shows in the file properties of mmts-dsfilter.ax (Details tab) and in the debug log. Without git it is unknown.
tools/mmts_audio_split.exe scans MPT audio layout changes and reports whether
ordinary AAC streams can be converted to ADTS:
tools\mmts_audio_split.exe <input.mmts> [--split] [--max-mb N] [--progress-mb N]--max-mb Nlimits scanning to the firstNMB for quick investigation.--progress-mb Nprints progress everyNMB. The default is 512 MB.--splitwrites one file per detected audio-layout segment. It cannot be combined with--max-mb, because a partial scan may miss later layout changes.
tools/subtitle_ttml_dump.exe writes every subtitle TTML sample out as XML
and prints the B24 cell grid each paragraph asks for:
tools\subtitle_ttml_dump.exe <input.mmts> [outdir] [--verbose]outdirreceivessub_NNN.xmlper sample. Omit it to only print.--verbosealso prints the X position of every character.- A cell is one font width plus one
arib-tt:letter-spacing, and a region's extent is exactly the cell count times that, so the printedlaidOutandextentcolumns must agree. They are what the splitter positions text from, so aMISMATCH(also the exit code) is a caption it will lay out wrong - ruby drifting off the character it annotates, for instance.
tools/mmts_pin_probe.exe plays a file through the real filter in a
DirectShow graph and prints how many samples each output pin delivered:
tools\mmts_pin_probe.exe <input.mmts> [--seek SEC] [--run SEC] [--ax PATH] [--expect-main-subtitle] [--expect-pin NAME]...--run SECis real playback time (default 60). Cover an MPT change - a commentary track starting, a channel boundary - to check that captions stay on the main subtitle pin and every audio pin keeps receiving its stream.--ax PATHloads an unregistered build, e.g.msvc\x64\Release\mmts-dsfilter.ax; it reads the.ininext to it, so setDebugLogPaththere to log the same run. Without it the registered filter is used.--expect-main-subtitlefails the run unless the main caption pin received samples and no other subtitle pin did;--expect-pin NAME(repeatable) fails it unless the pin whose name starts withNAMEreceived samples. The exit code is 1 on a failed expectation, so runs can be scripted as a regression check.- Demuxer-only probes miss routing bugs: the filter locks its stream lists once pins exist, which only a real graph reproduces.
The filter keeps the selected MMT package when another service's MPT arrives. A PLT-confirmed removal of that package selects the first remaining local package, so recordings with a channel change can continue without mixing simultaneously multiplexed services. Without a PLT, the first MPT chooses the package; an unrelated MPT alone does not switch it. Seeking preserves the selection, and opening a new file resets it.
Build Release first, then run from the repository root:
./tools/run_subtitle_state_test.cmd
./tools/run_subtitle_file_index_test.ps1
./tools/run_subtitle_file_index_test.ps1 -Test subtitle_render_test
./tools/run_subtitle_file_index_test.ps1 -Test playback_state_test
./tools/run_subtitle_file_index_test.ps1 -Test transport_state_testThe transport state test checks long package IDs, isolation of video/audio/ subtitle/event callbacks across services, PLT transitions, seek selection and all defined rational video frame rates. Playback tests also verify negotiated video aspect/rate, sample durations and dynamic format changes on queued samples.
The playback state test exercises production EDL seek logic, video callbacks,
queued DirectShow output samples, and video/audio track changes. It checks
normalized RAP-map lookups, decoder preroll before cuts and seek targets,
reference/B-picture handling at cut ends, cumulative timestamps across cuts,
and worker joins with every connected output branch flushed. Fixtures are
written under work/playback-state.
The capture sink inspects timestamps and sample flags; it does not decode HEVC or AAC and does not replace playback/seek checks in MPC-BE or MPC-HC with real recordings. The runners require Visual Studio C++ tools and the Release build's v143 toolset and object files.
The filter supports a non-destructive edit decision list in a .mmtsedit JSON
file placed next to the media (recording.mmts -> recording.mmtsedit).
The edited program is the concatenation of the listed source segments, played
without rewriting the media file:
{
"version": 1,
"source": "recording.mmts",
"sourceSize": 123456789,
"map": "recording.mmtsmap",
"timeline": [
{ "sourceStartMs": 25000, "sourceEndMs": 1800000 },
{ "sourceStartMs": 1860000, "sourceEndMs": 3600000 }
]
}Opening recording.mmts plays the original file and does not look for an edit
file. To apply edits, open recording.mmtsedit directly. The filter then opens
the same-named recording.mmts, verifies the recorded source size, and exposes
a virtual timeline whose duration is the sum of the segment durations. A single
segment behaves as a simple in/out trim; multiple segments are concatenated,
with the demuxer jumping across the cut gaps (each segment starts at the nearest
RAP). Create and edit these files with the mmts-edit-gui tool.
The earlier start-only
.mmtsidxformat has been removed; use.mmtsedit(a single segment is equivalent to the old start offset plus an end).
If a dantto4k-generated .mmtsmap exists next to the MMTS file
(recording.mmtsmap), the filter also loads it during prescan. The map is used
to supplement audio/subtitle track candidates and duration information, so
tracks that appear after the first prescan window can still get output pins
when playback starts. Seek/RAP points from the map are also used as preferred
byte offsets before falling back to duration-ratio seeking.
MMTSMAP3 files generated by dantto4k v20260615 or later include audio
audioMode and channel-count metadata. The filter uses this metadata to expose
later-appearing 5.1ch and 22.2ch tracks with clearer pin names. When a stream
changes format across MPT sections, the pin name shows the source timeline, for
example Audio 1 AAC 2.0 -> LATM 22.2 -> AAC 5.1. A LATM 22.2ch input is
decoded and downmixed to PCM 48000Hz 2.0; the pin name keeps the original
source format visible while the DirectShow media type reports the actual PCM
output format.
The Build Release GitHub Actions workflow builds the Release configuration
and creates a ZIP package containing:
mmts-dsfilter.ax
mmts-dsfilter.ini
Install_mmts-dsfilter_64.cmd
Uninstall_mmts-dsfilter_64.cmd
README.md
FFmpeg-NOTICE.txt
FFmpeg-LICENSE.txt
FFmpeg is linked into mmts-dsfilter.ax, so the package carries no FFmpeg
DLLs, only its LGPL notice and license text.
The workflow can be run manually from GitHub Actions to create an artifact. It
also publishes the ZIP as a GitHub Release asset when a v* tag is pushed, for
example:
git tag v0.1.0
git push origin v0.1.0The workflow checks out dantto4k from the same GitHub owner as this
repository, so forks should also provide a sibling <owner>/dantto4k
repository. The checkout is pinned to a dantto4k tag (ref: in
.github/workflows/release.yml) because this project compiles dantto4k
sources directly; bump it deliberately after verifying the build against the
newer dantto4k release. Local builds are not pinned -- they use whatever is
checked out in the sibling dantto4k directory.
Administrator privileges are required.
From a release package, right-click each script and choose Run as administrator:
Install_mmts-dsfilter_64.cmd— registers the filterUninstall_mmts-dsfilter_64.cmd— unregisters the filter
The scripts call regsvr32 silently and then show a success/failure message.
Do not delete mmts-dsfilter.ax, the bundled FFmpeg DLLs, or
mmts-dsfilter.ini after installation. The installer does not copy files
anywhere; the filter runs from the release-package folder.
For a development build, use an elevated command prompt:
regsvr32 msvc\x64\Release\mmts-dsfilter.axTo unregister:
regsvr32 /u msvc\x64\Release\mmts-dsfilter.axPlace mmts-dsfilter.ini next to mmts-dsfilter.ax. The release package
includes a sample INI with all options documented.
[MMTS]
; Font family used in generated ASS subtitles.
;FontName=MS Gothic
CaptionTransparency=0
BackgroundTransparency=50
ShowBackground=1
ShowRubyBackground=1
OutlineWidth=0
DelayMs=0
DebugLogPath=
VerboseLog=0
DumpSubtitleData=0
DumpSubtitleDir=
DumpSubtitleMaxFiles=200| Key | Default | Description |
|---|---|---|
FontName |
MS Gothic |
Font family for ASS subtitles. Cell positions are fixed per character so changing the font does not shift later characters. |
CaptionTransparency |
0 |
Caption text transparency: 0 = opaque, 100 = fully transparent. |
BackgroundTransparency |
50 |
Cell background transparency: 0 = opaque, 100 = fully transparent. |
ShowBackground |
1 |
Show cell background rectangles (1) or hide them (0). Background height follows TTML lineHeight, then region extent, then font size. If TTML provides no background color a dark gray cell background is used. |
ShowRubyBackground |
1 |
Show cell background rectangles behind ruby (furigana) text. |
OutlineWidth |
0 |
Text outline width: 0 = none, 1–10 = ASS \bord value. |
DelayMs |
0 |
Subtitle display offset in milliseconds. Negative values show earlier. |
DebugLogPath |
(empty) | File path for debug logs. Empty disables file logging. |
VerboseLog |
0 |
Set to 1 to emit verbose debug logs in Release builds. Debug builds always emit verbose logs. |
DumpSubtitleData |
0 |
Set to 1 to save raw TTML samples and referenced subtitle resources for investigation. |
DumpSubtitleDir |
(empty) | Directory for subtitle dumps. Empty writes to subtitle_dump next to the filter. |
DumpSubtitleMaxFiles |
200 |
Maximum number of subtitle samples/resources to dump. |
When a broadcaster transmits a custom glyph (DRCS), the filter registers the
subtitle glyph resource and renders the glyph as an ASS vector drawing scaled
to fit the subtitle cell. Set DumpSubtitleData=1 to capture raw TTML samples
and subtitle resources for investigation.
OutputDebugString output is quiet by default in Release builds. Set
VerboseLog=1 in mmts-dsfilter.ini to emit lifecycle and verbose debug logs
to DebugView or an attached debugger. Debug builds always emit these logs.
File logging is disabled by default; set DebugLogPath to a log file path only
for temporary investigations. With DebugLogPath set and VerboseLog=0,
lifecycle logs are written to the file without also going to OutputDebugString.
The splitter keeps one DirectShow audio output pin per MPT mp4a audio stream
found during pre-scan so MPC-BE can switch tracks normally. Ordinary AAC streams
are delivered only after LATM/LOAS to ADTS conversion succeeds; candidate pins
are still created before that conversion is confirmed so late or fragmented
initial audio samples do not hide the track. For 8K broadcasts, 22.2ch audio is
carried as AAC LATM/LOAS rather than the ADTS form used for ordinary AAC tracks.
These LATM streams are passed through without ADTS conversion and are advertised
as:
majortype: MEDIATYPE_Audio
subtype: MEDIASUBTYPE_MPEG_LOAS {00001602-0000-0010-8000-00AA00389B71}
formattype: FORMAT_WaveFormatEx
wFormatTag: WAVE_FORMAT_MPEG_LOAS (0x1602)
Some downstream filters return failures on non-selected audio branches while another audio track is active. The splitter treats those failures as non-fatal and continues delivering samples to all connected audio pins.
ARIB-TTML paragraph and span times are retained as separate display intervals.
TMD 0010 uses the subtitle descriptor's NTP reference_start_time in the
same absolute clock as the video MPU timestamps; zero begin remains a timed
start. TMD 1111 ignores TTML time attributes and displays from arrival.
Without a usable descriptor, arrival time is used as a compatibility fallback.
Before submitting ASS events, a second incremental file reader determines the end of the current document from its declared end, the next document, a clear packet, or a change/removal of the subtitle descriptor. This also clips events that were previously sent with long fixed durations and prevents an older glyph-waiting document from returning after a clear. Future display intervals are held until shortly before their start, rather than filling the output queue with all future characters at once.
The reader starts at the beginning to preserve MPT/fragment context, reads each byte only once, and caches timing records across seeks. It can therefore add read latency on the first subtitle or after a forward seek; an open-ended last caption may require scanning to EOF. Stop/seek cancels the scan. This path is for fixed recording files; its cached EOF is not a growing-file monitor.
Regression checks (VS v143 and the adjacent dantto4k checkout are required):
./build.ps1 -Configuration Release
./tools/run_subtitle_state_test.cmd
./tools/run_subtitle_file_index_test.ps1
./tools/run_subtitle_file_index_test.ps1 -Test subtitle_render_testThese cover element timing, binary MMT/TLV update/clear/descriptor sequences, and production ASS cell layout. They do not replace playback validation in the target subtitle renderer or certify all ARIB-TTML styling/monomedia features.
This project includes a copy of Microsoft DirectShow BaseClasses under
baseclasses/; see baseclasses/LICENSE for that component.