Transcode Reolink streams for smaller recordings

While my video surveillance system covered all my home’s ingress areas, I did not have full coverage over my outside property. In order to remedy this situation, I installed some Reolink RLC-520A cameras since they allowed me to power them using power over ethernet, provided high resolution video streams and had decent online reviews. However, little did I know that their recordings would end up eating up most of my Frigate NVR’s allotted disk space. I needed to figure out a way to transcode these Reolink streams into smaller recordings.

About my Frigate setup

I have setup Frigate to perform continuous recording for every camera stream, even though it’s capable of saving recordings only when motion/objects are detected. I just enjoy being able to scrub through recordings to see the full timeline. Besides, I am more than happy to have just a few days worth of recordings.

Thus, I have allocated 500 GB of storage to store the recordings. This worked well prior to the installation of the Reolink cameras since I was able to retain all the 1080p recordings until they expired. Furthermore, the disk partition hosting the recordings is backed up on a daily basis using Bacula.

The problem with the new cameras

These cameras produce 2560×1920 video streams and their recordings were using around 2.5GB per hour, each! Some quick napkin math will tell you that such figures drastically reduced my data retention capabilities. Fortunately, Frigate will delete the oldest recordings to make room for new ones, but this ended up using way too much space for backups!

I had to remedy this situation.

Failed attempts to reduce storage usage

My first reflex was to ensure the cameras had audio recording disabled and I tried lowering the cameras’ stream resolution. Unfortunately, doing so seemingly reduced the field of view so this was out of the question.

I then attempted to setup Frigate’s go2rtc capabilities in order to transcode the video streams into a lower resolution. This appeared to do the trick, however the cameras suffered from connectivity issues after a few hours of streaming. Also, when looking at the birdseye view in Frigate, the streams were lagging by around 30 seconds. This resulted in significant gaps in my recording timelines and inadequate live-monitoring capabilities, so I reverted back to consuming the cameras’ RTSP streams.

Transcoding using MediaMTX

I came across MediaMTX while searching for other options for transcoding video streams. It checked a few boxes for me as it’s available as a container image, it is actively maintained and it is well documented. While MediaMTX is a full-featured media server/proxy, I really don’t need all of its bells and whistles. All I need it to do is transcode the Reolink camera streams and output them into another RTSP stream, so that Frigate can consume them and ultimately generate smaller recordings.

I went ahead and deployed it to my Kubernetes cluster as a single replica Deployment. I’m mounting the /mediamtx.yml configuration file as a ConfigMap object and I’m exposing the tcp/8554 RTSP service using a LoadBalancer, since Frigate is hosted outside of the cluster. The MTX_PROTOCOLS=tcp environment variable is needed because using UDP does not work when using the LoadBalancer. Make sure you use the bluenviron/mediamtx image having the -ffmpeg suffix as we will need it to capture input streams.

For MediaMTX’s configuration, I went ahead and disabled all services I didn’t need such as hls, rtmp, srt, etc. I next forced using the TCP protocol for RTSP. Then, I created paths for each camera I wanted to transcode and re-stream. You can see on line 267 that I’m defining the ext-front path, which causes MediaMTX to expose the following RTSP stream: rtsp://10.150.0.170:8554/ext-front – this is the URI Frigate will use to capture the transcoded camera stream.

Then, on line 268, I am defining the ext-front-original input stream. All the magic is happening on line 270, where ffmpeg captures one of my camera’s RTSP stream and performs the following notable tasks:

  • -an will skip the inclusion of the audio stream.
  • scale=1920:-1 will resize the video stream to a width of 1920 and -1 is used to adjust the height to keep the same aspect ratio as the input stream.
  • -f rtsp rtsp://localhost:8554/ext-front will output the transcoded stream as an RTSP stream to the provided URI.

Here is the Kubernetes manifest containing all the objects:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mediamtx-deployment
  namespace: mediamtx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mediamtx
  template:
    metadata:
      labels:
        app: mediamtx
    spec:
      volumes:
        - name: mediamtx-configmap
          configMap:
            name: mediamtx-configmap
      containers:
        - name: mediamtx
          env:
            - name: TZ
              value: "America/New_York"
            - name: MTX_PROTOCOLS
              value: "tcp"
          resources:
            requests:
              cpu: "1"
              memory: "1Gi"
          image: bluenviron/mediamtx:1.8.3-ffmpeg
          ports:
            - containerPort: 8554
              protocol: TCP
              name: rtsp
          volumeMounts:
            - mountPath: /mediamtx.yml
              name: mediamtx-configmap
              subPath: mediamtx.yml
---
apiVersion: v1
kind: Service
metadata:
  name: mediamtx
  namespace: mediamtx
spec:
  ports:
  - name: mediamtx-rtsp
    port: 8554
    targetPort: rtsp
  selector:
    app: mediamtx
---
apiVersion: v1
kind: Service
metadata:
  name: mediamtx-lb
  namespace: mediamtx
  annotations:
    io.cilium/lb-ipam-ips: 10.150.0.170
spec:
  ports:
  - name: mediamtx-rtsp
    port: 8554
    targetPort: rtsp
  selector:
    app: mediamtx
  type: LoadBalancer
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: mediamtx-configmap
  namespace: mediamtx
  labels:
    app.kubernetes.io/name: mediamtx
data:
  mediamtx.yml: |
    logLevel: info
    logDestinations: [stdout]
    logFile: mediamtx.log
    readTimeout: 10s
    writeTimeout: 10s
    writeQueueSize: 512
    udpMaxPayloadSize: 1472
    runOnConnect:
    runOnConnectRestart: no
    runOnDisconnect:
    authMethod: internal
    authInternalUsers:
    - user: any
      pass:
      ips: []
      permissions:
      - action: publish
        path:
      - action: read
        path:
      - action: playback
        path:
    - user: any
      pass:
      ips: ['127.0.0.1', '::1']
      permissions:
      - action: api
      - action: metrics
      - action: pprof
    authHTTPAddress:
    authHTTPExclude:
    - action: api
    - action: metrics
    - action: pprof
    authJWTJWKS:
    api: no
    apiAddress: :9997
    apiEncryption: no
    apiServerKey: server.key
    apiServerCert: server.crt
    apiAllowOrigin: '*'
    apiTrustedProxies: []
    metrics: no
    metricsAddress: :9998
    metricsEncryption: no
    metricsServerKey: server.key
    metricsServerCert: server.crt
    metricsAllowOrigin: '*'
    metricsTrustedProxies: []
    pprof: no
    pprofAddress: :9999
    pprofEncryption: no
    pprofServerKey: server.key
    pprofServerCert: server.crt
    pprofAllowOrigin: '*'
    pprofTrustedProxies: []
    playback: no
    playbackAddress: :9996
    playbackEncryption: no
    playbackServerKey: server.key
    playbackServerCert: server.crt
    playbackAllowOrigin: '*'
    playbackTrustedProxies: []
    rtsp: yes
    protocols: [tcp]
    encryption: "no"
    rtspAddress: :8554
    rtspsAddress: :8322
    rtpAddress: :8000
    rtcpAddress: :8001
    multicastIPRange: 224.1.0.0/16
    multicastRTPPort: 8002
    multicastRTCPPort: 8003
    serverKey: server.key
    serverCert: server.crt
    rtspAuthMethods: [basic]
    rtmp: no
    rtmpAddress: :1935
    rtmpEncryption: "no"
    rtmpsAddress: :1936
    rtmpServerKey: server.key
    rtmpServerCert: server.crt
    hls: no
    hlsAddress: :8888
    hlsEncryption: no
    hlsServerKey: server.key
    hlsServerCert: server.crt
    hlsAllowOrigin: '*'
    hlsTrustedProxies: []
    hlsAlwaysRemux: no
    hlsVariant: lowLatency
    hlsSegmentCount: 7
    hlsSegmentDuration: 1s
    hlsPartDuration: 200ms
    hlsSegmentMaxSize: 50M
    hlsDirectory: ''
    hlsMuxerCloseAfter: 60s
    webrtc: no
    webrtcAddress: :8889
    webrtcEncryption: no
    webrtcServerKey: server.key
    webrtcServerCert: server.crt
    webrtcAllowOrigin: '*'
    webrtcTrustedProxies: []
    webrtcLocalUDPAddress: :8189
    webrtcLocalTCPAddress: ''
    webrtcIPsFromInterfaces: yes
    webrtcIPsFromInterfacesList: []
    webrtcAdditionalHosts: []
    webrtcICEServers2: []
    webrtcHandshakeTimeout: 10s
    webrtcTrackGatherTimeout: 2s
    srt: no
    srtAddress: :8890
    pathDefaults:
      source: publisher
      sourceFingerprint:
      sourceOnDemand: no
      sourceOnDemandStartTimeout: 10s
      sourceOnDemandCloseAfter: 10s
      maxReaders: 0
      srtReadPassphrase:
      fallback:
      record: no
      recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f
      recordFormat: fmp4
      recordPartDuration: 1s
      recordSegmentDuration: 1h
      recordDeleteAfter: 24h
      overridePublisher: yes
      srtPublishPassphrase:
      rtspTransport: automatic
      rtspAnyPort: no
      rtspRangeType:
      rtspRangeStart:
      sourceRedirect:
      rpiCameraCamID: 0
      rpiCameraWidth: 1920
      rpiCameraHeight: 1080
      rpiCameraHFlip: false
      rpiCameraVFlip: false
      rpiCameraBrightness: 0
      rpiCameraContrast: 1
      rpiCameraSaturation: 1
      rpiCameraSharpness: 1
      rpiCameraExposure: normal
      rpiCameraAWB: auto
      rpiCameraAWBGains: [0, 0]
      rpiCameraDenoise: "off"
      rpiCameraShutter: 0
      rpiCameraMetering: centre
      rpiCameraGain: 0
      rpiCameraEV: 0
      rpiCameraROI:
      rpiCameraHDR: false
      rpiCameraTuningFile:
      rpiCameraMode:
      rpiCameraFPS: 30
      rpiCameraIDRPeriod: 60
      rpiCameraBitrate: 1000000
      rpiCameraProfile: main
      rpiCameraLevel: '4.1'
      rpiCameraAfMode: continuous
      rpiCameraAfRange: normal
      rpiCameraAfSpeed: normal
      rpiCameraLensPosition: 0.0
      rpiCameraAfWindow:
      rpiCameraTextOverlayEnable: false
      rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX'
      runOnInit:
      runOnInitRestart: no
      runOnDemand:
      runOnDemandRestart: no
      runOnDemandStartTimeout: 10s
      runOnDemandCloseAfter: 10s
      runOnUnDemand:
      runOnReady:
      runOnReadyRestart: no
      runOnNotReady:
      runOnRead:
      runOnReadRestart: no
      runOnUnread:
      runOnRecordSegmentCreate:
      runOnRecordSegmentComplete:
    paths:
      all_others:
    paths:
      ext-front:
      ext-front-original:
        runOnInit: >
          ffmpeg -rtsp_transport tcp -i rtsp://redacteduser:[email protected]/h264Preview_01_main -pix_fmt yuv420p -c:v libx264 -preset ultrafast -b:v 600k -max_muxing_queue_size 9999 -an -filter:v scale=1920:-1 -f rtsp rtsp://localhost:8554/ext-front
        runOnInitRestart: yes

Stability, performance and storage usage

By incorporating MediaMTX between Frigate and my cameras, video recordings are now using around 300 MB of disk space per hour for the Reolink cameras when using the transcoded video streams. This way, I can easily fit several days worth of recordings on Frigate’s allotted storage space!

As for stability, there are seemingly no gaps in the camera timelines and both Frigate and MediaMTX logs show no signs of connectivity issues.

With regards to performance, MediaMTX is using around half a CPU core and 500 MB of RAM to transcode 2 camera streams.

The mediamtx pod used 506 mCPU and 478Mi of memory.

While MediaMTX is not using GPU acceleration to perform the decoding and encoding, the Kubernetes nodes are hosted in Proxmox using the x86-64-v3 emulated processor type. I suspect that this processor type may allow for some form hardware offloading when decoding/encoding video streams, but still, this is very decent resource usage.

When watching the Birdseye view in Frigate, I can still see a ~10 second lag when comparing the camera’s timestamp with the actual time, but this is acceptable.

Conclusion

Introducing MediaMTX as a middleman between Frigate and my Reolink cameras to transcode their video streams proved to be an excellent solution to produce smaller recordings. I can now retain around two weeks of continuous video recordings on Frigate’s 500 GB drive.

I suspect there may be a way to replicate what MediaMTX does within Frigate’s go2rtc configuration but considering how little resources the former uses and how easy it is to configure, I’m happy with the outcome.

Leave a Reply

Related Posts