TMD Server 2026.1 - Windows

TMD Server shares the Windows physical console with TMD Viewer or the HTTPS
browser viewer. Virtual sessions and attachment to RDP desktops are not
supported on Windows.

1. Install the server
---------------------
Run TMDServerInstaller.msi for the desired architecture (x64 or Win32/x86).
Both installers deploy the dynamic build. In the wizard:

  a. Select the installation folder.
  b. Create firewall exceptions for TCP 4815 and 8191, or choose "Configure it
     later" to install without creating those exceptions.
  c. Choose automatic console creation and verify the operating-system owner.
     The initial name is the account running the installer. Alternatively,
     select "No, I will create the session manually."
  d. Complete installation; the TMDServer service starts automatically.

2. Check the service and session
--------------------------------
Open an elevated Command Prompt. Change to the installed Server folder, normally
C:\Program Files\TextureMind\TMD\Server for x64, or the corresponding
Program Files (x86) folder for Win32:

  sc query TMDServer
  tmdtool.exe list-sessions

If you selected manual creation:
  tmdtool.exe create-session console --owner alice --type console

Replace alice with the intended Windows account (DOMAIN\alice when applicable).
There can be only one console definition. A session definition does not switch
Windows users or log anyone out. If Windows is in an RDP session, TMD continues
to target the physical console, which may show the lock/login screen.
The session must report active before connecting.

Connect from another computer
----------------------------
1. Make the server reachable on TCP 8191 for HTTPS/WebSocket (WSS).
   TCP 4815 is needed only for viewers using the native TCP/OpenSSL transport.
   Configure the host firewall and, where applicable, router/cloud access rules.
2. Check that the intended session is active with tmdtool list-sessions.
3. In a web browser, open https://server.example.com:8191/#console.
   Replace server.example.com with the host name/IP and console with the session
   ID. The server package must contain the browser viewer bundle. A page saying
   only "The TLS certificate has been accepted" means that bundle is missing;
   install a package with a populated browser directory.
4. Alternatively, launch TMD Viewer and enter server.example.com:8191#console
   in its host field. WSS is the default transport. For native TCP instead, run:

   tmdviewer --use-tcp --host "server.example.com:4815#console"

   On Windows use tmdviewer.exe; on macOS use the executable inside TmdViewer.app.
5. Trust the server certificate after verifying it belongs to the expected host.
   TMD creates a self-signed certificate by default. The browser viewer needs
   HTTPS. Then authenticate with the session owner's operating-system username
   and password (the default system authentication mode).
6. Choose the desired monitor in the viewer's display controls. Closing a viewer
   disconnects that viewer; it does not close the desktop session.

A session ID is not a username. Specifying #console or #work selects that exact
session. Without an ID, the server selects an accessible active session; specify
an ID when several sessions exist. Unknown, inactive or inaccessible IDs fail
instead of silently selecting a different desktop.

Service control and agentless operation
--------------------------------------
Stop/start TMDServer in Task Manager > Services, or in an elevated prompt:
  sc stop TMDServer
  sc start TMDServer

For a foreground test, stop the service first, then run from the Server folder:
  tmdserver.exe --agentless

This performs capture and server work in one process. It does not provide the
service's full UAC, Winlogon, lock/logout or Secure Attention Sequence support.
Do not run it concurrently with the installed service.

To uninstall, use Windows Installed apps or the unattended command below.

- Unattended installation
-----------------------

Both the x86 and x64 MSI packages support the standard Windows Installer
unattended modes. Run these commands from an elevated Command Prompt, or from
an administrator deployment service. Select the MSI for the desired architecture.
No extra TMD-specific switch or response file is required.

Install without displaying a wizard and without restarting the computer:

> msiexec.exe /i "C:\Packages\TMDServerInstaller.msi" /qn /norestart /L*v "%TEMP%\tmd-install.log"

The installation installs and starts the TMDServer service. On a fresh install,
it also creates the TCP 4815 and 8191 firewall exceptions by default.
The interactive wizard offers these choices after selecting the install folder:

- Create firewall exceptions for TCP ports 4815 and 8191 (default).
- Configure it later: neither firewall exception is created by the installer.

For unattended installation, use TMD_CONFIGURE_FIREWALL=0 to configure later:

> msiexec.exe /i "C:\Packages\TMDServerInstaller.msi" TMD_CONFIGURE_FIREWALL=0 /qn /norestart /L*v "%TEMP%\tmd-install.log"

Use TMD_CONFIGURE_FIREWALL=1 to explicitly create both exceptions. The selected
choice is saved and reused during repairs and upgrades; an explicitly supplied
property overrides the saved choice. Values other than 0 or 1 are rejected.
The firewall choice does not prevent installation or startup of the service.
To show a progress bar without requesting input, replace /qn with /passive.

Uninstall without displaying a wizard:

> msiexec.exe /x "C:\Packages\TMDServerInstaller.msi" /qn /norestart /L*v "%TEMP%\tmd-uninstall.log"

Use the MSI corresponding to the installed product when uninstalling.
For deployment scripts, wait for msiexec to exit and inspect its exit code:
0 means success; 3010 means success with a reboot required (deferred by
/norestart). Other codes indicate a failed or incomplete installation; inspect
the log. The log's parent directory must already exist.

Microsoft command reference:
https://learn.microsoft.com/windows-server/administration/windows-commands/msiexec
Installer exit codes:
https://learn.microsoft.com/windows/win32/msi/error-codes

- Console session at startup
--------------------------

The console-session page follows the firewall page. On a fresh installation,
automatic creation is enabled and the owner textbox is filled with the account
running the installer (Windows Installer's LogonUser property). You can edit
that name, including using DOMAIN\username for a domain account.

Select "No, I will create the session manually." to disable automatic creation.
The owner textbox is then disabled. This sets create-session to 0; it does not
remove stored session definitions. After installation, an administrator can run:

> tmdtool.exe create-session console --owner gingegneri --type console

For unattended installation with automatic creation and an explicit owner:

> msiexec.exe /i "C:\Packages\TMDServerInstaller.msi" TMD_CREATE_CONSOLE_SESSION=1 TMD_CONSOLE_SESSION_OWNER="gingegneri" /qn /norestart /L*v "%TEMP%\tmd-install.log"

For manual session creation:

> msiexec.exe /i "C:\Packages\TMDServerInstaller.msi" TMD_CREATE_CONSOLE_SESSION=0 /qn /norestart /L*v "%TEMP%\tmd-install.log"

If deploying as LocalSystem, supply the intended desktop owner's account
explicitly when enabling automatic creation. Existing service settings are
loaded for repairs and upgrades; explicit MSI properties override them.
The installer writes the create-session DWORD and owner string before starting
the service, under:

HKEY_USERS\S-1-5-18\Software\TextureMind\tmd\server\session-management

An existing console definition must match the configured owner; installation
does not replace or delete session definitions to change their ownership.

Microsoft LogonUser reference:
https://learn.microsoft.com/windows/win32/msi/logonuser


Documentation and support
-------------------------
https://www.texturemind.com/tmd/documentation/
support@texturemind.com

Created by Gianpaolo Ingegneri
2026-2030 (c) All rights reserved
https://www.texturemind.com/

Third-party notices
-------------------
See THIRD-PARTY-LICENSES.txt for third-party license texts.
This software is based in part on the work of the Independent JPEG Group.

FFmpeg source and build information
----------------------------------
This software uses libraries from the FFmpeg project (https://ffmpeg.org/)
under the GNU Lesser General Public License, version 2.1 or later (LGPL-2.1+).
The license text is included in THIRD-PARTY-LICENSES.txt. FFmpeg source:
https://github.com/FFmpeg/FFmpeg.git
Licensing information: https://ffmpeg.org/legal.html
Recorded Windows source revision:

SHA-1: 6400860b9d4bec5ace91ec3a5e43ad76f9660579

* avformat/flvdec: initialize ret in flv_read_packet() to AVERROR_BUG

Recorded Windows build commands (Visual Studio 2022 environment, MSYS2 shell):

- 64-bit build

  CC=cl PKG_CONFIG_PATH="../../libaom/install2022x64/lib/pkgconfig:../../libvpx/install2022x64/lib/pkgconfig:../../libdav1d/installdir64/lib/pkgconfig" \
  ../configure --toolchain=msvc --prefix=../installdir_x64 --arch=x86_64 --enable-asm --enable-shared --disable-everything --enable-swscale --enable-avcodec \
  --enable-decoder=mpeg1video --enable-encoder=mpeg1video --enable-decoder=av1 --enable-decoder=h264 --enable-decoder=vp9 --enable-libaom --enable-encoder=libaom_av1 \
  --enable-libvpx --enable-decoder=libvpx_vp9 --enable-encoder=libvpx_vp9 --enable-libdav1d --enable-decoder=libdav1d --enable-dxva2 --enable-hwaccel=av1_dxva2 \
  --enable-hwaccel=h264_dxva2 --enable-hwaccel=vp9_dxva2 --enable-d3d11va --enable-hwaccel=av1_d3d11va --enable-hwaccel=av1_d3d11va2 --enable-hwaccel=h264_d3d11va \
  --enable-hwaccel=h264_d3d11va2 --enable-hwaccel=vp9_d3d11va --enable-hwaccel=vp9_d3d11va2 --enable-d3d12va --enable-hwaccel=av1_d3d12va --enable-hwaccel=h264_d3d12va \
  --enable-hwaccel=vp9_d3d12va --disable-avformat --disable-avfilter --disable-avdevice --disable-swresample --disable-postproc --extra-cflags="-MT"

- 32-bit build

  CC=cl PKG_CONFIG_PATH="../../libaom/install2022x86/lib/pkgconfig:../../libvpx/install2022x86/lib/pkgconfig:../../libdav1d/installdir32/lib/pkgconfig" \
  ../configure --toolchain=msvc --prefix=../installdir_x86 --arch=x86 --enable-asm --enable-shared --disable-everything --enable-swscale --enable-avcodec \
  --enable-decoder=mpeg1video --enable-encoder=mpeg1video --enable-decoder=av1 --enable-decoder=h264 --enable-decoder=vp9 --enable-libaom --enable-encoder=libaom_av1 \
  --enable-libvpx --enable-decoder=libvpx_vp9 --enable-encoder=libvpx_vp9 --enable-libdav1d --enable-decoder=libdav1d --enable-dxva2 --enable-hwaccel=av1_dxva2 \
  --enable-hwaccel=h264_dxva2 --enable-hwaccel=vp9_dxva2 --enable-d3d11va --enable-hwaccel=av1_d3d11va --enable-hwaccel=av1_d3d11va2 --enable-hwaccel=h264_d3d11va \
  --enable-hwaccel=h264_d3d11va2 --enable-hwaccel=vp9_d3d11va --enable-hwaccel=vp9_d3d11va2 --enable-d3d12va --enable-hwaccel=av1_d3d12va --enable-hwaccel=h264_d3d12va \
  --enable-hwaccel=vp9_d3d12va --disable-avformat --disable-avfilter --disable-avdevice --disable-swresample --disable-postproc --extra-cflags="-MT"

After each configure command, build and install from its separate build folder:
  make -j4
  make install

The H.264 patch below also applies to the recorded Windows source revision
6400860b9d4bec5ace91ec3a5e43ad76f9660579. Apply it before configuring either
architecture, then use these recorded configure arguments. Run the hardware-only
API regression against the resulting DLLs before distributing a new build.

FFmpeg H.264 modification
------------------------
TMD uses a modified FFmpeg build. The patch is:
  utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff
Target: FFmpeg n7.1.5, commit 3a0867c2bfda4a4d4309ca1a8cbdc6175e67f587.
The complete diff is reproduced at the end of this README.

The shared h264 decoder registration and bitstream parsing are retained for
hardware decoding. The patch offers only hardware pixel formats, removes CPU
slice reconstruction dispatch, disables CPU error concealment, and rejects
software output, including delayed/error-recovery frames. Software H.264 cannot
be selected through the patched FFmpeg decoder API. Some software helpers may
remain in the binary. VAAPI and D3D11VA hardware implementations are retained.
TMD also rejects software H.264 decoder selection and fallback.

This decoder patch does not change hardware encoders. h264_vaapi is a hardware
encoder. libx264 and libopenh264 software H.264 encoders are not enabled in the
builds documented here. Software MPEG-1 and VP9 remain available.

For a manual build, run from a clean FFmpeg n7.1.5 source checkout, replacing
/path/to/tmf-trunk with the actual source tree location:

  git apply --check /path/to/tmf-trunk/utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff
  git apply /path/to/tmf-trunk/utils/ffmpeg/patches/ffmpeg-7.1.5-h264-hardware-only.diff

Apply once before configuring any architecture. --enable-decoder=h264 alone in
an upstream, unpatched build DOES enable software H.264; it is not a substitute
for this patch. Rebase and verify the patch when changing the FFmpeg revision.
Use fresh build/install directories to avoid retaining unpatched libraries.

The source tree includes utils/ffmpeg/test-h264-hardware-only.c and its embedded
test stream. The Linux builder runs this public-API regression automatically;
manual Windows verification is documented in docs/build-FFmpeg-Windows.txt.
The checks reject software output without needing a GPU. Successful hardware
decoding must also be tested on the intended GPU/driver.


Appendix: complete FFmpeg H.264 hardware-only patch
-------------------------------------------------

diff --git a/libavcodec/h264_slice.c b/libavcodec/h264_slice.c
index aa51a588fabb8be9a65f4997dc9bbc051cd350d3..63a7b23cbe2059a581e7857f159c22115a4080fa 100644
--- a/libavcodec/h264_slice.c
+++ b/libavcodec/h264_slice.c
@@ -915,8 +915,23 @@ static enum AVPixelFormat get_pixel_format(H264Context *h, int force_callback)
 
     *fmt = AV_PIX_FMT_NONE;
 
+    /* TMF: the registered H.264 decoder is hardware-only, including for
+     * callers using the default get_format callback or explicitly requesting
+     * a software format. Never offer software fallback to ff_get_format(). */
+    fmt = pix_fmts;
+    for (int i = 0; pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
+        const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmts[i]);
+        if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
+            *fmt++ = pix_fmts[i];
+    }
+    *fmt = AV_PIX_FMT_NONE;
+    if (fmt == pix_fmts) {
+        av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 requires hardware decoding; no supported hardware format.\n");
+        return AV_PIX_FMT_NONE;
+    }
+
     for (int i = 0; pix_fmts[i] != AV_PIX_FMT_NONE; i++)
-        if (pix_fmts[i] == h->avctx->pix_fmt && !force_callback)
+        if (pix_fmts[i] == h->avctx->pix_fmt && !force_callback && h->avctx->hwaccel)
             return pix_fmts[i];
     return ff_get_format(h->avctx, pix_fmts);
 }
@@ -2775,76 +2790,13 @@ finish:
  */
 int ff_h264_execute_decode_slices(H264Context *h)
 {
-    AVCodecContext *const avctx = h->avctx;
-    H264SliceContext *sl;
-    int context_count = h->nb_slice_ctx_queued;
-    int ret = 0;
-    int i, j;
-
-    h->slice_ctx[0].next_slice_idx = INT_MAX;
-
-    if (h->avctx->hwaccel || context_count < 1)
-        return 0;
-
-    av_assert0(context_count && h->slice_ctx[context_count - 1].mb_y < h->mb_height);
-
-    if (context_count == 1) {
-
-        h->slice_ctx[0].next_slice_idx = h->mb_width * h->mb_height;
-        h->postpone_filter = 0;
-
-        ret = decode_slice(avctx, &h->slice_ctx[0]);
-        h->mb_y = h->slice_ctx[0].mb_y;
-        if (ret < 0)
-            goto finish;
-    } else {
-        av_assert0(context_count > 0);
-        for (i = 0; i < context_count; i++) {
-            int next_slice_idx = h->mb_width * h->mb_height;
-            int slice_idx;
-
-            sl                 = &h->slice_ctx[i];
-
-            /* make sure none of those slices overlap */
-            slice_idx = sl->mb_y * h->mb_width + sl->mb_x;
-            for (j = 0; j < context_count; j++) {
-                H264SliceContext *sl2 = &h->slice_ctx[j];
-                int        slice_idx2 = sl2->mb_y * h->mb_width + sl2->mb_x;
-
-                if (i == j || slice_idx2 < slice_idx)
-                    continue;
-                next_slice_idx = FFMIN(next_slice_idx, slice_idx2);
-            }
-            sl->next_slice_idx = next_slice_idx;
-        }
-
-        avctx->execute(avctx, decode_slice, h->slice_ctx,
-                       NULL, context_count, sizeof(h->slice_ctx[0]));
-
-        /* pull back stuff from slices to master context */
-        sl                   = &h->slice_ctx[context_count - 1];
-        h->mb_y              = sl->mb_y;
-
-        if (h->postpone_filter) {
-            h->postpone_filter = 0;
-
-            for (i = 0; i < context_count; i++) {
-                int y_end, x_end;
-
-                sl = &h->slice_ctx[i];
-                y_end = FFMIN(sl->mb_y + 1, h->mb_height);
-                x_end = (sl->mb_y >= h->mb_height) ? h->mb_width : sl->mb_x;
-
-                for (j = sl->resync_mb_y; j < y_end; j += 1 + FIELD_OR_MBAFF_PICTURE(h)) {
-                    sl->mb_y = j;
-                    loop_filter(h, sl, j > sl->resync_mb_y ? 0 : sl->resync_mb_x,
-                                j == y_end - 1 ? x_end : h->mb_width);
-                }
-            }
-        }
+    /* TMF: hardware slices are submitted through FF_HW_CALL(decode_slice).
+     * Refuse the CPU slice decoder even if format selection is bypassed or
+     * a hardware decoder is lost while processing a packet. */
+    if (h->nb_slice_ctx_queued > 0 && !h->avctx->hwaccel) {
+        av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 software slice decoding is disabled.\n");
+        h->nb_slice_ctx_queued = 0;
+        return AVERROR(ENOSYS);
     }
-
-finish:
-    h->nb_slice_ctx_queued = 0;
-    return ret;
+    return 0;
 }
diff --git a/libavcodec/h264dec.c b/libavcodec/h264dec.c
index 0154fe17b665842284c5d782ca5c895be098fbec..cb944352e6267ca59ed9e0b26ae34903cbc6bef2 100644
--- a/libavcodec/h264dec.c
+++ b/libavcodec/h264dec.c
@@ -69,35 +69,8 @@ static void h264_er_decode_mb(void *opaque, int ref, int mv_dir, int mv_type,
                               int mb_x, int mb_y, int mb_intra, int mb_skipped)
 {
     const H264Context *h = opaque;
-    H264SliceContext *sl = &h->slice_ctx[0];
-
-    sl->mb_x = mb_x;
-    sl->mb_y = mb_y;
-    sl->mb_xy = mb_x + mb_y * h->mb_stride;
-    memset(sl->non_zero_count_cache, 0, sizeof(sl->non_zero_count_cache));
-    av_assert1(ref >= 0);
-    /* FIXME: It is possible albeit uncommon that slice references
-     * differ between slices. We take the easy approach and ignore
-     * it for now. If this turns out to have any relevance in
-     * practice then correct remapping should be added. */
-    if (ref >= sl->ref_count[0])
-        ref = 0;
-    if (!sl->ref_list[0][ref].data[0]) {
-        av_log(h->avctx, AV_LOG_DEBUG, "Reference not available for error concealing\n");
-        ref = 0;
-    }
-    if ((sl->ref_list[0][ref].reference&3) != 3) {
-        av_log(h->avctx, AV_LOG_DEBUG, "Reference invalid\n");
-        return;
-    }
-    fill_rectangle(&h->cur_pic.ref_index[0][4 * sl->mb_xy],
-                   2, 2, 2, ref, 1);
-    fill_rectangle(&sl->ref_cache[0][scan8[0]], 4, 4, 8, ref, 1);
-    fill_rectangle(sl->mv_cache[0][scan8[0]], 4, 4, 8,
-                   pack16to32((*mv)[0][0][0], (*mv)[0][0][1]), 4);
-    sl->mb_mbaff =
-    sl->mb_field_decoding_flag = 0;
-    ff_h264_hl_decode_mb(h, &h->slice_ctx[0]);
+    /* TMF: error concealment must never reconstruct macroblocks on the CPU. */
+    av_log(h->avctx, AV_LOG_ERROR, "TMF H.264 software error concealment is disabled.\n");
 }
 
 void ff_h264_draw_horiz_band(const H264Context *h, H264SliceContext *sl,
@@ -427,6 +400,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
 
     ff_h264_flush_change(h);
 
+    /* TMF: do not enter software error recovery, including for damaged input. */
+    h->enable_er = 0;
+
     if (h->enable_er < 0 && (avctx->active_thread_type & FF_THREAD_SLICE))
         h->enable_er = 0;
 
@@ -938,6 +914,11 @@ static int is_avcc_extradata(const uint8_t *buf, int buf_size)
 static int finalize_frame(H264Context *h, AVFrame *dst, H264Picture *out, int *got_frame)
 {
     int ret;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(out->f->format);
+
+    /* TMF: reject software output, including delayed/corrupt field recovery. */
+    if (!h->avctx->hwaccel || !desc || !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
+        return AVERROR(ENOSYS);
 
     if (((h->avctx->flags & AV_CODEC_FLAG_OUTPUT_CORRUPT) ||
          (h->avctx->flags2 & AV_CODEC_FLAG2_SHOW_ALL) ||
@@ -1107,7 +1088,6 @@ static int h264_decode_frame(AVCodecContext *avctx, AVFrame *pict,
 static const AVOption h264_options[] = {
     { "is_avc", "is avc", OFFSET(is_avc), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, VDX },
     { "nal_length_size", "nal_length_size", OFFSET(nal_length_size), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 4, VDX },
-    { "enable_er", "Enable error resilience on damaged frames (unsafe)", OFFSET(enable_er), AV_OPT_TYPE_BOOL, { .i64 = -1 }, -1, 1, VD },
     { "x264_build", "Assume this x264 version if no x264 version found in any SEI", OFFSET(x264_build), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, VD },
     { "skip_gray", "Do not return gray gap frames", OFFSET(skip_gray), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, VD },
     { "noref_gray", "Avoid using gray gap frames as references", OFFSET(noref_gray), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, VD },
