diff --git a/README.md b/README.md
index 33008ada..338b05fd 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ And can be recorded with:
 
 |format|video codecs|audio codecs|
 |------|------------|------------|
-|[fMP4](#record-streams-to-disk)|AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, LPCM|
+|[fMP4](#record-streams-to-disk)|AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711, LPCM|
 |[MPEG-TS](#record-streams-to-disk)|H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3|
 
 **Features**
diff --git a/go.mod b/go.mod
index ffbf6a1f..971afdb7 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/aler9/writerseeker v1.1.0
 	github.com/bluenviron/gohlslib v1.1.0
 	github.com/bluenviron/gortsplib/v4 v4.6.2
-	github.com/bluenviron/mediacommon v1.6.0
+	github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1
 	github.com/datarhei/gosrt v0.5.5
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/gin-gonic/gin v1.9.1
diff --git a/go.sum b/go.sum
index 14b185f9..630a98f4 100644
--- a/go.sum
+++ b/go.sum
@@ -24,8 +24,8 @@ github.com/bluenviron/gohlslib v1.1.0 h1:NL8CYg1BqHpy9tUugd/SbXH5p5vEi7Im/zaw0dp
 github.com/bluenviron/gohlslib v1.1.0/go.mod h1:kG/Sjebsxnf5asMGaGcQ0aSvtFGNChJPgctds2wDHOI=
 github.com/bluenviron/gortsplib/v4 v4.6.2 h1:CGIsxpnUFvSlIxnSFS0oFSSfwsHMmBCmYcrGAtIcwXc=
 github.com/bluenviron/gortsplib/v4 v4.6.2/go.mod h1:dN1YjyPNMfy/NwC17Ga6MiIMiUoQfg5GL7LGsVHa0Jo=
-github.com/bluenviron/mediacommon v1.6.0 h1:xJgwbOKKwxyyrEONnSb5J5Sq7NLjNhVQoR/5gs2IDdQ=
-github.com/bluenviron/mediacommon v1.6.0/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w=
+github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1 h1:f8XDAHvgPbT+n5qf73REdUo9kLmGpP4HNgphKI/8fGE=
+github.com/bluenviron/mediacommon v1.6.1-0.20231228213201-7bb211dba7e1/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
diff --git a/internal/formatprocessor/g711.go b/internal/formatprocessor/g711.go
new file mode 100644
index 00000000..ffa39d8c
--- /dev/null
+++ b/internal/formatprocessor/g711.go
@@ -0,0 +1,107 @@
+package formatprocessor //nolint:dupl
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/bluenviron/gortsplib/v4/pkg/format"
+	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio"
+	"github.com/pion/rtp"
+
+	"github.com/bluenviron/mediamtx/internal/unit"
+)
+
+type formatProcessorG711 struct {
+	udpMaxPayloadSize int
+	format            *format.G711
+	encoder           *rtpsimpleaudio.Encoder
+	decoder           *rtpsimpleaudio.Decoder
+}
+
+func newG711(
+	udpMaxPayloadSize int,
+	forma *format.G711,
+	generateRTPPackets bool,
+) (*formatProcessorG711, error) {
+	t := &formatProcessorG711{
+		udpMaxPayloadSize: udpMaxPayloadSize,
+		format:            forma,
+	}
+
+	if generateRTPPackets {
+		err := t.createEncoder()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return t, nil
+}
+
+func (t *formatProcessorG711) createEncoder() error {
+	t.encoder = &rtpsimpleaudio.Encoder{
+		PayloadMaxSize: t.udpMaxPayloadSize - 12,
+	}
+	return t.encoder.Init()
+}
+
+func (t *formatProcessorG711) ProcessUnit(uu unit.Unit) error { //nolint:dupl
+	u := uu.(*unit.G711)
+
+	pkt, err := t.encoder.Encode(u.Samples)
+	if err != nil {
+		return err
+	}
+
+	ts := uint32(multiplyAndDivide(u.PTS, time.Duration(t.format.ClockRate()), time.Second))
+	pkt.Timestamp += ts
+
+	u.RTPPackets = []*rtp.Packet{pkt}
+
+	return nil
+}
+
+func (t *formatProcessorG711) ProcessRTPPacket( //nolint:dupl
+	pkt *rtp.Packet,
+	ntp time.Time,
+	pts time.Duration,
+	hasNonRTSPReaders bool,
+) (Unit, error) {
+	u := &unit.G711{
+		Base: unit.Base{
+			RTPPackets: []*rtp.Packet{pkt},
+			NTP:        ntp,
+			PTS:        pts,
+		},
+	}
+
+	// remove padding
+	pkt.Header.Padding = false
+	pkt.PaddingSize = 0
+
+	if pkt.MarshalSize() > t.udpMaxPayloadSize {
+		return nil, fmt.Errorf("payload size (%d) is greater than maximum allowed (%d)",
+			pkt.MarshalSize(), t.udpMaxPayloadSize)
+	}
+
+	// decode from RTP
+	if hasNonRTSPReaders || t.decoder != nil {
+		if t.decoder == nil {
+			var err error
+			t.decoder, err = t.format.CreateDecoder()
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		samples, err := t.decoder.Decode(pkt)
+		if err != nil {
+			return nil, err
+		}
+
+		u.Samples = samples
+	}
+
+	// route packet as is
+	return u, nil
+}
diff --git a/internal/formatprocessor/processor.go b/internal/formatprocessor/processor.go
index d603b7ad..76713a3f 100644
--- a/internal/formatprocessor/processor.go
+++ b/internal/formatprocessor/processor.go
@@ -75,6 +75,9 @@ func New(
 	case *format.AC3:
 		return newAC3(udpMaxPayloadSize, forma, generateRTPPackets)
 
+	case *format.G711:
+		return newG711(udpMaxPayloadSize, forma, generateRTPPackets)
+
 	case *format.LPCM:
 		return newLPCM(udpMaxPayloadSize, forma, generateRTPPackets)
 
diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go
index a2ee4ea7..eae58d87 100644
--- a/internal/record/agent_test.go
+++ b/internal/record/agent_test.go
@@ -53,6 +53,18 @@ func TestAgent(t *testing.T) {
 				IndexDeltaLength: 3,
 			}},
 		},
+		{
+			Type: description.MediaTypeAudio,
+			Formats: []rtspformat.Format{&rtspformat.G711{
+				MULaw: false,
+			}},
+		},
+		{
+			Type: description.MediaTypeAudio,
+			Formats: []rtspformat.Format{&rtspformat.G711{
+				MULaw: true,
+			}},
+		},
 	}}
 
 	writeToStream := func(stream *stream.Stream) {
@@ -107,6 +119,20 @@ func TestAgent(t *testing.T) {
 				},
 				AUs: [][]byte{{1, 2, 3, 4}},
 			})
+
+			stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{
+				Base: unit.Base{
+					PTS: (50 + time.Duration(i)) * time.Second,
+				},
+				Samples: []byte{1, 2, 3, 4},
+			})
+
+			stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.G711{
+				Base: unit.Base{
+					PTS: (50 + time.Duration(i)) * time.Second,
+				},
+				Samples: []byte{1, 2, 3, 4},
+			})
 		}
 	}
 
diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go
index 83ff16d1..57db81ea 100644
--- a/internal/record/format_fmp4.go
+++ b/internal/record/format_fmp4.go
@@ -8,6 +8,7 @@ import (
 	rtspformat "github.com/bluenviron/gortsplib/v4/pkg/format"
 	"github.com/bluenviron/mediacommon/pkg/codecs/ac3"
 	"github.com/bluenviron/mediacommon/pkg/codecs/av1"
+	"github.com/bluenviron/mediacommon/pkg/codecs/g711"
 	"github.com/bluenviron/mediacommon/pkg/codecs/h264"
 	"github.com/bluenviron/mediacommon/pkg/codecs/h265"
 	"github.com/bluenviron/mediacommon/pkg/codecs/jpeg"
@@ -771,7 +772,34 @@ func (f *formatFMP4) initialize() {
 				// TODO
 
 			case *rtspformat.G711:
-				// TODO
+				codec := &fmp4.CodecLPCM{
+					LittleEndian: false,
+					BitDepth:     16,
+					SampleRate:   8000,
+					ChannelCount: 1,
+				}
+				track := addTrack(codec)
+
+				f.a.agent.Stream.AddReader(f.a.writer, media, forma, func(u unit.Unit) error {
+					tunit := u.(*unit.G711)
+					if tunit.Samples == nil {
+						return nil
+					}
+
+					var out []byte
+					if forma.MULaw {
+						out = g711.DecodeMulaw(tunit.Samples)
+					} else {
+						out = g711.DecodeAlaw(tunit.Samples)
+					}
+
+					return track.record(&sample{
+						PartSample: &fmp4.PartSample{
+							Payload: out,
+						},
+						dts: tunit.PTS,
+					})
+				})
 
 			case *rtspformat.LPCM:
 				codec := &fmp4.CodecLPCM{
diff --git a/internal/unit/g711.go b/internal/unit/g711.go
new file mode 100644
index 00000000..2169baff
--- /dev/null
+++ b/internal/unit/g711.go
@@ -0,0 +1,7 @@
+package unit
+
+// G711 is a G711 data unit.
+type G711 struct {
+	Base
+	Samples []byte
+}