Repository: bluenviron/mediamtx

Issue:
Allow support for MPEG-TS over unix sockets
### Describe the feature

#### **Background**
We are operating a high-scale video ingestion pipeline where **hundreds of MPEG-TS feeds** are streamed into **MediaMTX** for real-time processing and distribution. Currently, we rely on **UDP ports** to deliver these streams.

However, this approach has become increasingly **difficult to manage and maintain** due to:

- **Port exhaustion and conflicts**, especially in environments with dynamically provisioned feeds.
- **Lack of backpressure or error signaling**, which is typical with UDP.
- **Fragility** in containerized or orchestrated environments where port mappings and host-level networking complicate deployment.

#### **Proposal**
We request **support for receiving MPEG-TS streams over Unix domain sockets** as a new input transport option in MediaMTX.

#### **Benefits**
- **Simplified Port Management**: Eliminate the need to manage hundreds of distinct UDP ports. Each feed can be associated with a well-named socket file.
- **Improved Reliability**: Unix sockets offer **connection-oriented semantics**, improved **error detection**, and **automatic cleanup** on disconnection.
- **Better Integration with Local Producers**: For tightly coupled pipelines (e.g., processes on the same host), Unix sockets provide **low-latency** and **secure** IPC.
- **Enhanced Deployment Flexibility**: Especially in **Docker/Kubernetes** environments, volume-mounted socket paths are more portable and easier to manage than host network bindings.

#### **Use Case**
We envision feed-producing applications piping MPEG-TS data to MediaMTX by connecting to a domain socket (e.g., `/var/run/mediafeeds/camera123.sock`). MediaMTX would treat this as a live input stream, similarly to a UDP listener, but initiated over a **stream-oriented** UDS.

# For testing

Settings:
```
paths:
  ffmpeg:
    source: unix:/tmp/ffmpeg.socket
```

Populate unix socket:
```
ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \
-f mpegts "unix:/tmp/ffmpeg.socket"
```

Play the video:
```
ffplay -i rtsp://localhost:8554/ffmpeg
```

Hints from issue comments:
@github-actions: This issue is mentioned in release v1.14.0 🚀
Check out the entire changelog by [clicking here](https://github.com/bluenviron/mediamtx/releases/tag/v1.14.0)@RedShift1: Was this feature request written by AI?@jean343: It's not an AI feature request, we actually do need this feature to reliably generate streams at runtime. The alternative is to use UDP ports, and this causes all sort of port conflicting issues.

I might have asked AI to polish my feature request :)@RedShift1: Suspected as much, it's a useful feature but parts of the rationale don't add up. UNIX sockets don't deal with backpressure and error signaling, just like UDP sockets. It also doesn't have any connection-oriented semantics, error detection or automatic cleanup. It's really just a dumb pipe like an UDP socket with the only difference it doesn't involve the network stack. But the semantics of it are completely determined by the applications reading and writing to the UNIX socket.

Solution patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3938344a..386dd8d6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,12 @@
 version: 2
 updates:
+
   - package-ecosystem: "gomod"
     directory: "/"
     schedule:
       interval: "daily"
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/code_lint.yml b/.github/workflows/code_lint.yml
index e7840630..98733038 100644
--- a/.github/workflows/code_lint.yml
+++ b/.github/workflows/code_lint.yml
@@ -15,7 +15,7 @@ jobs:
       with:
         fetch-depth: 0
 
-    - uses: actions/setup-go@v3
+    - uses: actions/setup-go@v5
       with:
         go-version: "1.24"
 
@@ -31,7 +31,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
-    - uses: actions/setup-go@v3
+    - uses: actions/setup-go@v5
       with:
         go-version: "1.24"
 
diff --git a/.github/workflows/code_test.yml b/.github/workflows/code_test.yml
index 618c89dc..dd86fc5f 100644
--- a/.github/workflows/code_test.yml
+++ b/.github/workflows/code_test.yml
@@ -17,7 +17,7 @@ jobs:
 
     - run: make test
 
-    - uses: codecov/codecov-action@v3
+    - uses: codecov/codecov-action@v5
       with:
         token: ${{ secrets.CODECOV_TOKEN }}
 
@@ -39,7 +39,7 @@ jobs:
       with:
         fetch-depth: 0
 
-    - uses: actions/setup-go@v3
+    - uses: actions/setup-go@v5
       with:
         go-version: "1.24"
 
diff --git a/.github/workflows/issue_lock.yml b/.github/workflows/issue_lock.yml
index db72729b..bacf4b02 100644
--- a/.github/workflows/issue_lock.yml
+++ b/.github/workflows/issue_lock.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-22.04
 
     steps:
-    - uses: actions/github-script@v6
+    - uses: actions/github-script@v7
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bedc05a3..094be25a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -38,12 +38,12 @@ jobs:
     runs-on: ubuntu-22.04
 
     steps:
-    - uses: actions/download-artifact@v4
+    - uses: actions/download-artifact@v5
       with:
         name: binaries
         path: binaries
 
-    - uses: actions/github-script@v6
+    - uses: actions/github-script@v7
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
@@ -102,7 +102,7 @@ jobs:
     runs-on: ubuntu-22.04
 
     steps:
-    - uses: actions/github-script@v6
+    - uses: actions/github-script@v7
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
@@ -151,7 +151,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
 
-    - uses: actions/download-artifact@v4
+    - uses: actions/download-artifact@v5
       with:
         name: binaries
         path: binaries
@@ -172,7 +172,7 @@ jobs:
 
     - run: mv apidocs/*.html apidocs/index.html
 
-    - uses: peaceiris/actions-gh-pages@v3
+    - uses: peaceiris/actions-gh-pages@v4
       with:
         github_token: ${{ secrets.GITHUB_TOKEN }}
         publish_dir: ./apidocs
diff --git a/README.md b/README.md
index 5acdaa34..ebbd48ab 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ Live streams can be published to the server with:
 |[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM|
 |[RTMP cameras and servers](#rtmp-cameras-and-servers)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM|
 |[HLS cameras and servers](#hls-cameras-and-servers)|Low-Latency HLS, MP4-based HLS, legacy HLS|AV1, VP9, [H265](#supported-browsers-1), H264|Opus, MPEG-4 Audio (AAC)|
-|[UDP/MPEG-TS](#udpmpeg-ts)|Unicast, broadcast, multicast|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
+|[MPEG-TS](#mpeg-ts)|MPEG-TS over UDP, MPEG-TS over Unix socket|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
 |[Raspberry Pi Cameras](#raspberry-pi-cameras)||H264||
 
 Live streams can be read from the server with:
@@ -54,7 +54,7 @@ Live streams be recorded and played back with:
 * Publish live streams to the server
 * Read live streams from the server
 * Streams are automatically converted from a protocol to another
-* Serve multiple streams at once in separate paths
+* Serve several streams at once in separate paths
 * Record streams to disk
 * Playback recorded streams
 * Authenticate users
@@ -101,7 +101,7 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
     * [RTMP clients](#rtmp-clients)
     * [RTMP cameras and servers](#rtmp-cameras-and-servers)
     * [HLS cameras and servers](#hls-cameras-and-servers)
-    * [UDP/MPEG-TS](#udpmpeg-ts)
+    * [MPEG-TS](#mpeg-ts)
 * [Read from the server](#read-from-the-server)
   * [By software](#by-software-1)
     * [FFmpeg](#ffmpeg-1)
@@ -275,13 +275,13 @@ Otherwise, [compile the server from source](#openwrt-1).
 
 #### FFmpeg
 
-FFmpeg can publish a stream to the server in multiple ways (SRT client, SRT server, RTSP client, RTMP client, UDP/MPEG-TS, WebRTC with WHIP). The recommended one consists in publishing as a [RTSP client](#rtsp-clients):
+FFmpeg can publish a stream to the server in several ways (SRT client, SRT server, RTSP client, RTMP client, MPEG-TS over UDP, MPEG-TS over Unix sockets, WebRTC with WHIP). The recommended one consists in publishing as a [RTSP client](#rtsp-clients):
 
 ```
 ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream
 ```
 
-The RTSP protocol supports multiple underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `rtsp_transport` flag, for instance, in order to use TCP:
+The RTSP protocol supports several underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `rtsp_transport` flag, for instance, in order to use TCP:
 
 ```sh
 ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp -rtsp_transport tcp rtsp://localhost:8554/mystream
@@ -291,7 +291,7 @@ The resulting stream is available in path `/mystream`.
 
 #### GStreamer
 
-GStreamer can publish a stream to the server in multiple ways (SRT client, SRT server, RTSP client, RTMP client, UDP/MPEG-TS, WebRTC with WHIP). The recommended one consists in publishing as a [RTSP client](#rtsp-clients):
+GStreamer can publish a stream to the server in several ways (SRT client, SRT server, RTSP client, RTMP client, MPEG-TS over UDP, WebRTC with WHIP). The recommended one consists in publishing as a [RTSP client](#rtsp-clients):
 
 ```sh
 gst-launch-1.0 rtspclientsink name=s location=rtsp://localhost:8554/mystream \
@@ -307,7 +307,7 @@ gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \
 d.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream
 ```
 
-The RTSP protocol supports multiple underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `protocols` flag:
+The RTSP protocol supports several underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `protocols` flag:
 
 ```sh
 gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \
@@ -335,7 +335,7 @@ gst-launch-1.0 videotestsrc \
 
 #### OBS Studio
 
-OBS Studio can publish to the server in multiple ways (SRT client, RTMP client, WebRTC client). The recommended one consists in publishing as a [RTMP client](#rtmp-clients). In `Settings -> Stream` (or in the Auto-configuration Wizard), use the following parameters:
+OBS Studio can publish to the server in several ways (SRT client, RTMP client, WebRTC client). The recommended one consists in publishing as a [RTMP client](#rtmp-clients). In `Settings -> Stream` (or in the Auto-configuration Wizard), use the following parameters:
 
 * Service: `Custom...`
 * Server: `rtmp://localhost/mystream`
@@ -819,7 +819,7 @@ Known clients that can publish with RTSP are [FFmpeg](#ffmpeg), [GStreamer](#gst
 
 #### RTSP cameras and servers
 
-Most IP cameras expose their video stream by using a RTSP server that is embedded into the camera itself. In particular, cameras that are compliant with ONVIF profile S or T meet this requirement. You can use _MediaMTX_ to connect to one or multiple existing RTSP servers and read their video streams:
+Most IP cameras expose their video stream by using a RTSP server that is embedded into the camera itself. In particular, cameras that are compliant with ONVIF profile S or T meet this requirement. You can use _MediaMTX_ to connect to one or several existing RTSP servers and read their video streams:
 
 ```yml
 paths:
@@ -861,7 +861,7 @@ Known clients that can publish with RTMP are [FFmpeg](#ffmpeg), [GStreamer](#gst
 
 #### RTMP cameras and servers
 
-You can use _MediaMTX_ to connect to one or multiple existing RTMP servers and read their video streams:
+You can use _MediaMTX_ to connect to one or several existing RTMP servers and read their video streams:
 
 ```yml
 paths:
@@ -874,7 +874,7 @@ The resulting stream is available in path `/proxied`.
 
 #### HLS cameras and servers
 
-HLS is a streaming protocol that works by splitting streams into segments, and by serving these segments and a playlist with the HTTP protocol. You can use _MediaMTX_ to connect to one or multiple existing HLS servers and read their video streams:
+HLS is a streaming protocol that works by splitting streams into segments, and by serving these segments and a playlist with the HTTP protocol. You can use _MediaMTX_ to connect to one or several existing HLS servers and read their video streams:
 
 ```yml
 paths:
@@ -885,9 +885,21 @@ paths:
 
 The resulting stream is available in path `/proxied`.
 
-#### UDP/MPEG-TS
+#### MPEG-TS
 
-The server supports ingesting UDP/MPEG-TS packets (i.e. MPEG-TS packets sent with UDP). Packets can be unicast, broadcast or multicast. For instance, you can generate a multicast UDP/MPEG-TS stream with GStreamer:
+The server supports ingesting MPEG-TS streams, shipped in several ways (UDP packets or Unix sockets).
+
+In order to read a UDP MPEG-TS stream, edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
+
+```yml
+paths:
+  mypath:
+    source: udp+mpegts://238.0.0.1:1234
+```
+
+Where `238.0.0.1` is the IP for listening packets, in this case a multicast IP.
+
+You can generate a UDP multicast MPEG-TS stream with GStreamer:
 
 ```sh
 gst-launch-1.0 -v mpegtsmux name=mux alignment=1 ! udpsink host=238.0.0.1 port=1234 \
@@ -903,33 +915,41 @@ ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
 -f mpegts udp://238.0.0.1:1234?pkt_size=1316
 ```
 
-Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
+The resulting stream is available in path `/mypath`.
+
+If the listening IP is a multicast IP, _MediaMTX_ will listen for incoming packets on the default multicast interface, picked by the operating system. It is possible to specify the interface manually by using the `interface` parameter:
 
 ```yml
 paths:
   mypath:
-    source: udp://238.0.0.1:1234
+    source: udp+mpegts://238.0.0.1:1234?interface=eth0
 ```
 
-The resulting stream is available in path `/mypath`.
-
-If the listening IP is a multicast IP, _MediaMTX_ listens for incoming multicast packets on the default interface picked by the operating system. It is possible to specify this interface manually by using the `interface` parameter:
+It is possible to restrict who can send packets by using the `source` parameter:
 
 ```yml
 paths:
   mypath:
-    source: udp://238.0.0.1:1234?interface=eth0
+    source: udp+mpegts://0.0.0.0:1234?source=192.168.3.5
 ```
 
-It is possible to restrict who can send packets by using the `source` parameter:
+Known clients that can publish with UDP and MPEG-TS are [FFmpeg](#ffmpeg) and [GStreamer](#gstreamer).
+
+Unix sockets are more efficient than UDP packets and can be used as transport by specifying the `unix+mpegts` scheme:
 
 ```yml
 paths:
   mypath:
-    source: udp://0.0.0.0:1234?source=192.168.3.5
+    source: unix+mpegts:///tmp/socket.sock
 ```
 
-Known clients that can publish with UDP/MPEG-TS are [FFmpeg](#ffmpeg) and [GStreamer](#gstreamer).
+FFmpeg can generate such streams:
+
+```sh
+ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \
+-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \
+-f mpegts unix:/tmp/socket.sock
+```
 
 ## Read from the server
 
@@ -937,13 +957,13 @@ Known clients that can publish with UDP/MPEG-TS are [FFmpeg](#ffmpeg) and [GStre
 
 #### FFmpeg
 
-FFmpeg can read a stream from the server in multiple ways (RTSP, RTMP, HLS, WebRTC with WHEP, SRT). The recommended one consists in reading with [RTSP](#rtsp):
+FFmpeg can read a stream from the server in several ways (RTSP, RTMP, HLS, WebRTC with WHEP, SRT). The recommended one consists in reading with [RTSP](#rtsp):
 
 ```sh
 ffmpeg -i rtsp://localhost:8554/mystream -c copy output.mp4
 ```
 
-The RTSP protocol supports multiple underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `rtsp_transport` flag:
+The RTSP protocol supports several underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can set the transport protocol by using the `rtsp_transport` flag:
 
 ```sh
 ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c copy output.mp4
@@ -951,13 +971,13 @@ ffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c copy output.mp4
 
 #### GStreamer
 
-GStreamer can read a stream from the server in multiple ways (RTSP, RTMP, HLS, WebRTC with WHEP, SRT). The recommended one consists in reading with [RTSP](#rtsp):
+GStreamer can read a stream from the server in several ways (RTSP, RTMP, HLS, WebRTC with WHEP, SRT). The recommended one consists in reading with [RTSP](#rtsp):
 
 ```sh
 gst-launch-1.0 rtspsrc location=rtsp://127.0.0.1:8554/mystream latency=0 ! decodebin ! autovideosink
 ```
 
-The RTSP protocol supports multiple underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can change the transport protocol by using the `protocols` flag:
+The RTSP protocol supports several underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)). You can change the transport protocol by using the `protocols` flag:
 
 ```sh
 gst-launch-1.0 rtspsrc protocols=tcp location=rtsp://127.0.0.1:8554/mystream latency=0 ! decodebin ! autovideosink
@@ -997,13 +1017,13 @@ audio-caps="application/x-rtp,media=audio,encoding-name=OPUS,payload=111,clock-r
 
 #### VLC
 
-VLC can read a stream from the server in multiple ways (RTSP, RTMP, HLS, SRT). The recommended one consists in reading with [RTSP](#rtsp):
+VLC can read a stream from the server in several ways (RTSP, RTMP, HLS, SRT). The recommended one consists in reading with [RTSP](#rtsp):
 
 ```sh
 vlc --network-caching=50 rtsp://localhost:8554/mystream
 ```
 
-The RTSP protocol supports multiple underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)).
+The RTSP protocol supports several underlying transport protocols, each with its own characteristics (see [RTSP-specific features](#rtsp-specific-features)).
 
 In order to use the TCP transport protocol, use the `--rtsp_tcp` flag:
 
@@ -1164,7 +1184,7 @@ In the _Hierarchy_ window, find or create a scene. Inside the scene, add a _Canv
 
 #### Web browsers
 
-Web browsers can read a stream from the server in multiple ways (WebRTC or HLS).
+Web browsers can read a stream from the server in several ways (WebRTC or HLS).
 
 You can read a stream by using the [WebRTC protocol](#webrtc-1) by visiting the web page:
 
@@ -2334,7 +2354,7 @@ When using WHIP or WHEP to establish a WebRTC connection, there are several ways
 
 If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection.
 
-Make sure that `webrtcAdditionalHosts` includes your public IPs, that are IPs that can be used by clients to reach the server. If clients are on the same LAN as the server, add the LAN address of the server. If clients are coming from the internet, add the public IP address of the server, or alternatively a DNS name, if you have one. You can add multiple values to support all scenarios:
+Make sure that `webrtcAdditionalHosts` includes your public IPs, that are IPs that can be used by clients to reach the server. If clients are on the same LAN as the server, add the LAN address of the server. If clients are coming from the internet, add the public IP address of the server, or alternatively a DNS name, if you have one. You can add several values to support all scenarios:
 
 ```yml
 webrtcAdditionalHosts: [192.168.x.x, 1.2.3.4, my-dns.example.org, ...]
@@ -2476,7 +2496,7 @@ rtsps://localhost:8322/mystream
 
 #### Corrupted frames
 
-In some scenarios, when publishing or reading from the server with RTSP, frames can get corrupted. This can be caused by multiple reasons:
+In some scenarios, when publishing or reading from the server with RTSP, frames can get corrupted. This can be caused by several reasons:
 
 * the write queue of the server is too small and can't keep up with the stream throughput. A solution consists in increasing its size:
 
diff --git a/go.mod b/go.mod
index 9846edc8..8b878b0f 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/gookit/color v1.5.4
 	github.com/gorilla/websocket v1.5.3
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
-	github.com/matthewhartstonge/argon2 v1.3.3
+	github.com/matthewhartstonge/argon2 v1.3.4
 	github.com/pion/ice/v4 v4.0.7
 	github.com/pion/interceptor v0.1.40
 	github.com/pion/logging v0.2.4
@@ -32,9 +32,9 @@ require (
 	github.com/pion/sdp/v3 v3.0.15
 	github.com/pion/webrtc/v4 v4.0.7
 	github.com/stretchr/testify v1.10.0
-	golang.org/x/crypto v0.40.0
-	golang.org/x/sys v0.34.0
-	golang.org/x/term v0.33.0
+	golang.org/x/crypto v0.41.0
+	golang.org/x/sys v0.35.0
+	golang.org/x/term v0.34.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -88,7 +88,7 @@ require (
 	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
 	golang.org/x/arch v0.16.0 // indirect
 	golang.org/x/net v0.42.0 // indirect
-	golang.org/x/text v0.27.0 // indirect
+	golang.org/x/text v0.28.0 // indirect
 	golang.org/x/time v0.9.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
diff --git a/go.sum b/go.sum
index 8159ffb4..a30ba1de 100644
--- a/go.sum
+++ b/go.sum
@@ -135,8 +135,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/matthewhartstonge/argon2 v1.3.3 h1:aLxMePclKDhOGjqZcwJrR419TYK1DlgqOg880k69N8A=
-github.com/matthewhartstonge/argon2 v1.3.3/go.mod h1:xPzyMXm1wTxUF6f6ZtEaMsQrVlp30Fgqocw6GKJKK1A=
+github.com/matthewhartstonge/argon2 v1.3.4 h1:GQb9404Z8++b+YTL2OBIAFOt+QHLld17NuGUZELK4uU=
+github.com/matthewhartstonge/argon2 v1.3.4/go.mod h1:0AUh12fJ3AvyV283ykNqvWcW1/Iw1laAZHFSsAap4Uc=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -225,8 +225,8 @@ go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwE
 golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
 golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
-golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
 golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
@@ -238,15 +238,15 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
-golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
 golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
 golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/internal/conf/path.go b/internal/conf/path.go
index 1da4c7af..e7d01f6a 100644
--- a/internal/conf/path.go
+++ b/internal/conf/path.go
@@ -422,6 +422,14 @@ func (pconf *Path) validate(
 			return fmt.Errorf("'%s' is not a valid UDP URL", pconf.Source)
 		}
 
+	case strings.HasPrefix(pconf.Source, "udp+mpegts://"):
+		_, _, err := net.SplitHostPort(pconf.Source[len("udp+mpegts://"):])
+		if err != nil {
+			return fmt.Errorf("'%s' is not a valid UDP+MPEGTS URL", pconf.Source)
+		}
+
+	case strings.HasPrefix(pconf.Source, "unix+mpegts://"):
+
 	case strings.HasPrefix(pconf.Source, "srt://"):
 		_, err := gourl.Parse(pconf.Source)
 		if err != nil {
diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH
index c7e982d8..7407b99e 100644
--- a/internal/servers/hls/hlsjsdownloader/HASH
+++ b/internal/servers/hls/hlsjsdownloader/HASH
@@ -1 +1 @@
-df282dc76256de0ad2819662ec5294e41daf3f71511631f642fb9c5c8f81b79f
+747b842f8868cc59783b2e2695ee56ccc248ff295035b49c2ad0fc7378a933cc
diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION
index e9d66c79..62f6ecd0 100644
--- a/internal/servers/hls/hlsjsdownloader/VERSION
+++ b/internal/servers/hls/hlsjsdownloader/VERSION
@@ -1 +1 @@
-v1.6.8
+v1.6.9
diff --git a/internal/staticsources/handler.go b/internal/staticsources/handler.go
index 7c768de3..b6272281 100644
--- a/internal/staticsources/handler.go
+++ b/internal/staticsources/handler.go
@@ -12,11 +12,11 @@ import (
 	"github.com/bluenviron/mediamtx/internal/defs"
 	"github.com/bluenviron/mediamtx/internal/logger"
 	sshls "github.com/bluenviron/mediamtx/internal/staticsources/hls"
+	ssmpegts "github.com/bluenviron/mediamtx/internal/staticsources/mpegts"
 	ssrpicamera "github.com/bluenviron/mediamtx/internal/staticsources/rpicamera"
 	ssrtmp "github.com/bluenviron/mediamtx/internal/staticsources/rtmp"
 	ssrtsp "github.com/bluenviron/mediamtx/internal/staticsources/rtsp"
 	sssrt "github.com/bluenviron/mediamtx/internal/staticsources/srt"
-	ssudp "github.com/bluenviron/mediamtx/internal/staticsources/udp"
 	sswebrtc "github.com/bluenviron/mediamtx/internal/staticsources/webrtc"
 	"github.com/bluenviron/mediamtx/internal/stream"
 )
@@ -117,8 +117,10 @@ func (s *Handler) Initialize() {
 			Parent:      s,
 		}
 
-	case strings.HasPrefix(s.Conf.Source, "udp://"):
-		s.instance = &ssudp.Source{
+	case strings.HasPrefix(s.Conf.Source, "udp://") ||
+		strings.HasPrefix(s.Conf.Source, "udp+mpegts://") ||
+		strings.HasPrefix(s.Conf.Source, "unix+mpegts://"):
+		s.instance = &ssmpegts.Source{
 			ReadTimeout: s.ReadTimeout,
 			Parent:      s,
 		}
diff --git a/internal/staticsources/mpegts/source.go b/internal/staticsources/mpegts/source.go
new file mode 100644
index 00000000..52837227
--- /dev/null
+++ b/internal/staticsources/mpegts/source.go
@@ -0,0 +1,142 @@
+// Package mpegts contains the MPEG-TS static source.
+package mpegts
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"time"
+
+	"github.com/bluenviron/gortsplib/v4/pkg/description"
+
+	"github.com/bluenviron/mediamtx/internal/conf"
+	"github.com/bluenviron/mediamtx/internal/counterdumper"
+	"github.com/bluenviron/mediamtx/internal/defs"
+	"github.com/bluenviron/mediamtx/internal/logger"
+	"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
+	"github.com/bluenviron/mediamtx/internal/stream"
+)
+
+type parent interface {
+	logger.Writer
+	SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes
+	SetNotReady(req defs.PathSourceStaticSetNotReadyReq)
+}
+
+// Source is a MPEG-TS static source.
+type Source struct {
+	ReadTimeout conf.Duration
+	Parent      parent
+}
+
+// Log implements logger.Writer.
+func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
+	s.Parent.Log(level, "[MPEG-TS source] "+format, args...)
+}
+
+// Run implements StaticSource.
+func (s *Source) Run(params defs.StaticSourceRunParams) error {
+	s.Log(logger.Debug, "connecting")
+
+	u, err := url.Parse(params.ResolvedSource)
+	if err != nil {
+		return err
+	}
+	q := u.Query()
+
+	var nc net.Conn
+
+	switch u.Scheme {
+	case "unix+mpegts":
+		nc, err = createUnix(u)
+		if err != nil {
+			return err
+		}
+
+	default:
+		nc, err = createUDP(u.Host, q)
+		if err != nil {
+			return err
+		}
+	}
+
+	readerErr := make(chan error)
+	go func() {
+		readerErr <- s.runReader(nc)
+	}()
+
+	select {
+	case err = <-readerErr:
+		nc.Close()
+		return err
+
+	case <-params.Context.Done():
+		nc.Close()
+		<-readerErr
+		return fmt.Errorf("terminated")
+	}
+}
+
+func (s *Source) runReader(nc net.Conn) error {
+	nc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
+	mr := &mpegts.EnhancedReader{R: nc}
+	err := mr.Initialize()
+	if err != nil {
+		return err
+	}
+
+	decodeErrors := &counterdumper.CounterDumper{
+		OnReport: func(val uint64) {
+			s.Log(logger.Warn, "%d decode %s",
+				val,
+				func() string {
+					if val == 1 {
+						return "error"
+					}
+					return "errors"
+				}())
+		},
+	}
+
+	decodeErrors.Start()
+	defer decodeErrors.Stop()
+
+	mr.OnDecodeError(func(_ error) {
+		decodeErrors.Increase()
+	})
+
+	var stream *stream.Stream
+
+	medias, err := mpegts.ToStream(mr, &stream, s)
+	if err != nil {
+		return err
+	}
+
+	res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
+		Desc:               &description.Session{Medias: medias},
+		GenerateRTPPackets: true,
+	})
+	if res.Err != nil {
+		return res.Err
+	}
+
+	defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
+
+	stream = res.Stream
+
+	for {
+		nc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
+		err = mr.Read()
+		if err != nil {
+			return err
+		}
+	}
+}
+
+// APISourceDescribe implements StaticSource.
+func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
+	return defs.APIPathSourceOrReader{
+		Type: "udpSource",
+		ID:   "",
+	}
+}
diff --git a/internal/staticsources/mpegts/udp.go b/internal/staticsources/mpegts/udp.go
new file mode 100644
index 00000000..bb891526
--- /dev/null
+++ b/internal/staticsources/mpegts/udp.go
@@ -0,0 +1,160 @@
+package mpegts
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"time"
+
+	"github.com/bluenviron/gortsplib/v4/pkg/multicast"
+	"github.com/bluenviron/mediamtx/internal/restrictnetwork"
+)
+
+const (
+	// same size as GStreamer's rtspsrc
+	udpKernelReadBufferSize = 0x80000
+)
+
+type udpConn struct {
+	pc       net.PacketConn
+	sourceIP net.IP
+}
+
+func (r *udpConn) Close() error {
+	return r.pc.Close()
+}
+
+func (r *udpConn) Read(p []byte) (int, error) {
+	for {
+		n, addr, err := r.pc.ReadFrom(p)
+
+		if r.sourceIP != nil && addr != nil && !addr.(*net.UDPAddr).IP.Equal(r.sourceIP) {
+			continue
+		}
+
+		return n, err
+	}
+}
+
+func (r *udpConn) Write(_ []byte) (int, error) {
+	panic("unimplemented")
+}
+
+func (r *udpConn) LocalAddr() net.Addr {
+	panic("unimplemented")
+}
+
+func (r *udpConn) RemoteAddr() net.Addr {
+	panic("unimplemented")
+}
+
+func (r *udpConn) SetDeadline(_ time.Time) error {
+	panic("unimplemented")
+}
+
+func (r *udpConn) SetReadDeadline(t time.Time) error {
+	return r.pc.SetReadDeadline(t)
+}
+
+func (r *udpConn) SetWriteDeadline(_ time.Time) error {
+	panic("unimplemented")
+}
+
+func defaultInterfaceForMulticast(multicastAddr *net.UDPAddr) (*net.Interface, error) {
+	conn, err := net.Dial("udp4", multicastAddr.String())
+	if err != nil {
+		return nil, err
+	}
+	localAddr := conn.LocalAddr().(*net.UDPAddr)
+	conn.Close()
+
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, iface := range interfaces {
+		var addrs []net.Addr
+		addrs, err = iface.Addrs()
+		if err != nil {
+			continue
+		}
+
+		for _, addr := range addrs {
+			var ip net.IP
+			switch v := addr.(type) {
+			case *net.IPNet:
+				ip = v.IP
+			case *net.IPAddr:
+				ip = v.IP
+			}
+
+			if ip != nil && ip.Equal(localAddr.IP) {
+				return &iface, nil
+			}
+		}
+	}
+
+	return nil, fmt.Errorf("could not find any interface for using multicast address %s", multicastAddr)
+}
+
+type packetConn interface {
+	net.PacketConn
+	SetReadBuffer(int) error
+}
+
+func createUDP(host string, q url.Values) (net.Conn, error) {
+	var sourceIP net.IP
+
+	if src := q.Get("source"); src != "" {
+		sourceIP = net.ParseIP(src)
+		if sourceIP == nil {
+			return nil, fmt.Errorf("invalid source IP")
+		}
+	}
+
+	addr, err := net.ResolveUDPAddr("udp", host)
+	if err != nil {
+		return nil, err
+	}
+
+	var pc packetConn
+
+	if ip4 := addr.IP.To4(); ip4 != nil && addr.IP.IsMulticast() {
+		var intf *net.Interface
+
+		if intfName := q.Get("interface"); intfName != "" {
+			intf, err = net.InterfaceByName(intfName)
+			if err != nil {
+				return nil, err
+			}
+		} else {
+			intf, err = defaultInterfaceForMulticast(addr)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		pc, err = multicast.NewSingleConn(intf, addr.String(), net.ListenPacket)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		var tmp net.PacketConn
+		tmp, err = net.ListenPacket(restrictnetwork.Restrict("udp", addr.String()))
+		if err != nil {
+			return nil, err
+		}
+		pc = tmp.(*net.UDPConn)
+	}
+
+	// defer pc.Close()
+
+	err = pc.SetReadBuffer(udpKernelReadBufferSize)
+	if err != nil {
+		pc.Close()
+		return nil, err
+	}
+
+	return &udpConn{pc: pc, sourceIP: sourceIP}, nil
+}
diff --git a/internal/staticsources/mpegts/unix.go b/internal/staticsources/mpegts/unix.go
new file mode 100644
index 00000000..8ae9b259
--- /dev/null
+++ b/internal/staticsources/mpegts/unix.go
@@ -0,0 +1,136 @@
+package mpegts
+
+import (
+	"fmt"
+	"net"
+	"net/url"
+	"os"
+	"sync"
+	"time"
+)
+
+type unixConn struct {
+	l        net.Listener
+	c        net.Conn
+	mutex    sync.Mutex
+	closed   bool
+	deadline time.Time
+}
+
+func (r *unixConn) Close() error {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	r.closed = true
+
+	r.l.Close()
+
+	if r.c != nil {
+		r.c.Close()
+	}
+
+	return nil
+}
+
+func (r *unixConn) acceptWithDeadline() (net.Conn, error) {
+	done := make(chan struct{})
+	defer func() { <-done }()
+
+	terminate := make(chan struct{})
+	defer close(terminate)
+
+	go func() {
+		defer close(done)
+		select {
+		case <-time.After(time.Until(r.deadline)):
+			r.l.Close()
+		case <-terminate:
+			return
+		}
+	}()
+
+	c, err := r.l.Accept()
+	if err != nil {
+		if time.Now().After(r.deadline) {
+			return nil, fmt.Errorf("deadline exceeded")
+		}
+		return nil, err
+	}
+	return c, nil
+}
+
+func (r *unixConn) setConn(c net.Conn) error {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+
+	if r.closed {
+		return fmt.Errorf("closed")
+	}
+
+	r.c = c
+	return nil
+}
+
+func (r *unixConn) Read(p []byte) (int, error) {
+	if r.c == nil {
+		c, err := r.acceptWithDeadline()
+		if err != nil {
+			return 0, err
+		}
+
+		err = r.setConn(c)
+		if err != nil {
+			return 0, err
+		}
+	}
+
+	r.c.SetReadDeadline(r.deadline)
+	return r.c.Read(p)
+}
+
+func (r *unixConn) Write(_ []byte) (int, error) {
+	panic("unimplemented")
+}
+
+func (r *unixConn) LocalAddr() net.Addr {
+	panic("unimplemented")
+}
+
+func (r *unixConn) RemoteAddr() net.Addr {
+	panic("unimplemented")
+}
+
+func (r *unixConn) SetDeadline(_ time.Time) error {
+	panic("unimplemented")
+}
+
+func (r *unixConn) SetReadDeadline(t time.Time) error {
+	r.deadline = t
+	return nil
+}
+
+func (r *unixConn) SetWriteDeadline(_ time.Time) error {
+	panic("unimplemented")
+}
+
+func createUnix(u *url.URL) (net.Conn, error) {
+	var pa string
+	if u.Path != "" {
+		pa = u.Path
+	} else {
+		pa = u.Host
+	}
+
+	if pa == "" {
+		return nil, fmt.Errorf("invalid unix path")
+	}
+
+	os.Remove(pa)
+
+	socket, err := net.Listen("unix", pa)
+	if err != nil {
+		return nil, err
+	}
+
+	return &unixConn{l: socket}, nil
+}
diff --git a/internal/staticsources/udp/source.go b/internal/staticsources/udp/source.go
deleted file mode 100644
index 40c0bf04..00000000
--- a/internal/staticsources/udp/source.go
+++ /dev/null
@@ -1,244 +0,0 @@
-// Package udp contains the UDP static source.
-package udp
-
-import (
-	"fmt"
-	"net"
-	"net/url"
-	"time"
-
-	"github.com/bluenviron/gortsplib/v4/pkg/description"
-	"github.com/bluenviron/gortsplib/v4/pkg/multicast"
-
-	"github.com/bluenviron/mediamtx/internal/conf"
-	"github.com/bluenviron/mediamtx/internal/counterdumper"
-	"github.com/bluenviron/mediamtx/internal/defs"
-	"github.com/bluenviron/mediamtx/internal/logger"
-	"github.com/bluenviron/mediamtx/internal/protocols/mpegts"
-	"github.com/bluenviron/mediamtx/internal/restrictnetwork"
-	"github.com/bluenviron/mediamtx/internal/stream"
-)
-
-const (
-	// same size as GStreamer's rtspsrc
-	udpKernelReadBufferSize = 0x80000
-)
-
-func defaultInterfaceForMulticast(multicastAddr *net.UDPAddr) (*net.Interface, error) {
-	conn, err := net.Dial("udp4", multicastAddr.String())
-	if err != nil {
-		return nil, err
-	}
-	localAddr := conn.LocalAddr().(*net.UDPAddr)
-	conn.Close()
-
-	interfaces, err := net.Interfaces()
-	if err != nil {
-		return nil, err
-	}
-
-	for _, iface := range interfaces {
-		var addrs []net.Addr
-		addrs, err = iface.Addrs()
-		if err != nil {
-			continue
-		}
-
-		for _, addr := range addrs {
-			var ip net.IP
-			switch v := addr.(type) {
-			case *net.IPNet:
-				ip = v.IP
-			case *net.IPAddr:
-				ip = v.IP
-			}
-
-			if ip != nil && ip.Equal(localAddr.IP) {
-				return &iface, nil
-			}
-		}
-	}
-
-	return nil, fmt.Errorf("could not find any interface for using multicast address %s", multicastAddr)
-}
-
-type packetConnReader struct {
-	pc       net.PacketConn
-	sourceIP net.IP
-}
-
-func (r *packetConnReader) Read(p []byte) (int, error) {
-	for {
-		n, addr, err := r.pc.ReadFrom(p)
-
-		if r.sourceIP != nil && addr != nil && !addr.(*net.UDPAddr).IP.Equal(r.sourceIP) {
-			continue
-		}
-
-		return n, err
-	}
-}
-
-type packetConn interface {
-	net.PacketConn
-	SetReadBuffer(int) error
-}
-
-type parent interface {
-	logger.Writer
-	SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes
-	SetNotReady(req defs.PathSourceStaticSetNotReadyReq)
-}
-
-// Source is a UDP static source.
-type Source struct {
-	ReadTimeout conf.Duration
-	Parent      parent
-}
-
-// Log implements logger.Writer.
-func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
-	s.Parent.Log(level, "[UDP source] "+format, args...)
-}
-
-// Run implements StaticSource.
-func (s *Source) Run(params defs.StaticSourceRunParams) error {
-	s.Log(logger.Debug, "connecting")
-
-	u, err := url.Parse(params.ResolvedSource)
-	if err != nil {
-		return err
-	}
-	q := u.Query()
-
-	var sourceIP net.IP
-
-	if src := q.Get("source"); src != "" {
-		sourceIP = net.ParseIP(src)
-		if sourceIP == nil {
-			return fmt.Errorf("invalid source IP")
-		}
-	}
-
-	addr, err := net.ResolveUDPAddr("udp", u.Host)
-	if err != nil {
-		return err
-	}
-
-	var pc packetConn
-
-	if ip4 := addr.IP.To4(); ip4 != nil && addr.IP.IsMulticast() {
-		var intf *net.Interface
-
-		if intfName := q.Get("interface"); intfName != "" {
-			intf, err = net.InterfaceByName(intfName)
-			if err != nil {
-				return err
-			}
-		} else {
-			intf, err = defaultInterfaceForMulticast(addr)
-			if err != nil {
-				return err
-			}
-		}
-
-		pc, err = multicast.NewSingleConn(intf, addr.String(), net.ListenPacket)
-		if err != nil {
-			return err
-		}
-	} else {
-		var tmp net.PacketConn
-		tmp, err = net.ListenPacket(restrictnetwork.Restrict("udp", addr.String()))
-		if err != nil {
-			return err
-		}
-		pc = tmp.(*net.UDPConn)
-	}
-
-	defer pc.Close()
-
-	err = pc.SetReadBuffer(udpKernelReadBufferSize)
-	if err != nil {
-		return err
-	}
-
-	readerErr := make(chan error)
-	go func() {
-		readerErr <- s.runReader(pc, sourceIP)
-	}()
-
-	select {
-	case err = <-readerErr:
-		return err
-
-	case <-params.Context.Done():
-		pc.Close()
-		<-readerErr
-		return fmt.Errorf("terminated")
-	}
-}
-
-func (s *Source) runReader(pc net.PacketConn, sourceIP net.IP) error {
-	pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
-	pcr := &packetConnReader{pc: pc, sourceIP: sourceIP}
-	r := &mpegts.EnhancedReader{R: pcr}
-	err := r.Initialize()
-	if err != nil {
-		return err
-	}
-
-	decodeErrors := &counterdumper.CounterDumper{
-		OnReport: func(val uint64) {
-			s.Log(logger.Warn, "%d decode %s",
-				val,
-				func() string {
-					if val == 1 {
-						return "error"
-					}
-					return "errors"
-				}())
-		},
-	}
-
-	decodeErrors.Start()
-	defer decodeErrors.Stop()
-
-	r.OnDecodeError(func(_ error) {
-		decodeErrors.Increase()
-	})
-
-	var stream *stream.Stream
-
-	medias, err := mpegts.ToStream(r, &stream, s)
-	if err != nil {
-		return err
-	}
-
-	res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
-		Desc:               &description.Session{Medias: medias},
-		GenerateRTPPackets: true,
-	})
-	if res.Err != nil {
-		return res.Err
-	}
-
-	defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
-
-	stream = res.Stream
-
-	for {
-		pc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
-		err = r.Read()
-		if err != nil {
-			return err
-		}
-	}
-}
-
-// APISourceDescribe implements StaticSource.
-func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
-	return defs.APIPathSourceOrReader{
-		Type: "udpSource",
-		ID:   "",
-	}
-}
diff --git a/mediamtx.yml b/mediamtx.yml
index d0d2f0e3..d674a1e9 100644
--- a/mediamtx.yml
+++ b/mediamtx.yml
@@ -438,7 +438,8 @@ pathDefaults:
   # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS
   # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera
   # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera with HTTPS
-  # * udp://ip:port -> the stream is pulled with UDP, by listening on the specified IP and port
+  # * udp+mpegts://ip:port -> the stream is pulled from MPEG-TS over UDP, by listening on the specified address
+  # * unix+mpegts://socket -> the stream is pulled from MPEG-TS over Unix socket, by using the socket
   # * srt://existing-url -> the stream is pulled from another SRT server / camera
   # * whep://existing-url -> the stream is pulled from another WebRTC server / camera
   # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS



Test patch:
diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/mpegts/source_test.go
similarity index 57%
rename from internal/staticsources/udp/source_test.go
rename to internal/staticsources/mpegts/source_test.go
index 6df1f2cd..e8052b04 100644
--- a/internal/staticsources/udp/source_test.go
+++ b/internal/staticsources/mpegts/source_test.go
@@ -1,9 +1,10 @@
-package udp
+package mpegts
 
 import (
-	"bufio"
 	"context"
 	"net"
+	"os"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -29,7 +30,7 @@ func multicastCapableInterface(t *testing.T) string {
 	return ""
 }
 
-func TestSource(t *testing.T) {
+func TestSourceUDP(t *testing.T) {
 	for _, ca := range []string{
 		"unicast",
 		"multicast",
@@ -41,16 +42,16 @@ func TestSource(t *testing.T) {
 
 			switch ca {
 			case "unicast":
-				src = "udp://127.0.0.1:9001"
+				src = "udp+mpegts://127.0.0.1:9001"
 
 			case "multicast":
-				src = "udp://238.0.0.1:9001"
+				src = "udp+mpegts://238.0.0.1:9001"
 
 			case "multicast with interface":
-				src = "udp://238.0.0.1:9001?interface=" + multicastCapableInterface(t)
+				src = "udp+mpegts://238.0.0.1:9001?interface=" + multicastCapableInterface(t)
 
 			case "unicast with source":
-				src = "udp://127.0.0.1:9001?source=127.0.1.1"
+				src = "udp+mpegts://127.0.0.1:9001?source=127.0.1.1"
 			}
 
 			p := &test.StaticSourceParent{}
@@ -112,8 +113,7 @@ func TestSource(t *testing.T) {
 				Codec: &mpegts.CodecH264{},
 			}
 
-			bw := bufio.NewWriter(conn)
-			w := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}
+			w := &mpegts.Writer{W: conn, Tracks: []*mpegts.Track{track}}
 			err = w.Initialize()
 			require.NoError(t, err)
 
@@ -127,10 +127,77 @@ func TestSource(t *testing.T) {
 			}})
 			require.NoError(t, err)
 
-			err = bw.Flush()
-			require.NoError(t, err)
-
 			<-p.Unit
 		})
 	}
 }
+
+func TestSourceUnixSocket(t *testing.T) {
+	for _, ca := range []string{
+		"relative",
+		"absolute",
+	} {
+		t.Run(ca, func(t *testing.T) {
+			var pa string
+			if ca == "relative" {
+				pa = "test.sock"
+			} else {
+				pa = filepath.Join(os.TempDir(), "test.sock")
+			}
+
+			func() {
+				p := &test.StaticSourceParent{}
+				p.Initialize()
+				defer p.Close()
+
+				so := &Source{
+					ReadTimeout: conf.Duration(10 * time.Second),
+					Parent:      p,
+				}
+
+				done := make(chan struct{})
+				defer func() { <-done }()
+
+				ctx, ctxCancel := context.WithCancel(context.Background())
+				defer ctxCancel()
+
+				go func() {
+					so.Run(defs.StaticSourceRunParams{ //nolint:errcheck
+						Context:        ctx,
+						ResolvedSource: "unix+mpegts://" + pa,
+						Conf:           &conf.Path{},
+					})
+					close(done)
+				}()
+
+				time.Sleep(50 * time.Millisecond)
+
+				_, err := os.Stat(pa)
+				require.NoError(t, err)
+
+				conn, err := net.Dial("unix", pa)
+				require.NoError(t, err)
+
+				track := &mpegts.Track{
+					Codec: &mpegts.CodecH264{},
+				}
+
+				w := &mpegts.Writer{W: conn, Tracks: []*mpegts.Track{track}}
+				err = w.Initialize()
+				require.NoError(t, err)
+
+				err = w.WriteH264(track, 0, 0, [][]byte{{ // IDR
+					5, 1,
+				}})
+				require.NoError(t, err)
+
+				conn.Close() // trigger a flush
+
+				<-p.Unit
+			}()
+
+			_, err := os.Stat(pa)
+			require.Error(t, err)
+		})
+	}
+}