Mediamtx + repertory instance

This commit is contained in:
Ian Keane 2026-09-14 13:37:00 -04:00
parent 39420f524b
commit a0b28b7a01
13 changed files with 470 additions and 2 deletions

View file

@ -0,0 +1,5 @@
apiVersion: v2
name: mediamtx
description: MediaMTX streaming server + zoitestream landing page/API for stream.dumpnet.chat
type: application
version: 0.1.0

View file

@ -0,0 +1,69 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mediamtx-config
namespace: mediamtx
data:
mediamtx.yml: |
logLevel: info
authMethod: internal
authInternalUsers:
- user: {{ .Values.publishUser }}
pass: __PUBLISH_PASSWORD__
ips: []
permissions:
- action: publish
path: ""
- action: read
path: ""
- action: playback
path: ""
- user: any
pass: ""
ips: []
permissions:
- action: read
path: ""
- action: playback
path: ""
rtmp: true
rtmpAddress: :{{ .Values.mediamtx.rtmpPort }}
rtmpEncryption: "no"
hls: true
hlsAddress: :8888
hlsVariant: lowLatency
hlsAllowOrigin: "*"
hlsSegmentCount: 7
hlsSegmentDuration: 1s
hlsPartDuration: 200ms
webrtc: true
webrtcAddress: :{{ .Values.mediamtx.webrtcHttpPort }}
webrtcEncryption: false
webrtcAllowOrigin: "*"
webrtcLocalUDPAddress: :{{ .Values.mediamtx.webrtcUdpPort }}
webrtcAdditionalHosts:
- {{ .Values.publicIp }}
webrtcICEServers2:
- url: stun:stun.l.google.com:19302
- url: stun:stun1.l.google.com:19302
rtsp: false
srt: false
pathDefaults:
source: publisher
runOnReady: >
wget -q -O- --post-data "path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE"
"http://localhost:5000/hooks/ready?token=$HOOK_TOKEN"
runOnReadyRestart: false
runOnNotReady: >
wget -q -O- --post-data "path=$MTX_PATH"
"http://localhost:5000/hooks/not-ready?token=$HOOK_TOKEN"
runOnNotReadyRestart: false
paths:
all_others: {}

View file

@ -0,0 +1,27 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: zoitestream-secrets
namespace: mediamtx
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: zoitestream-secrets
data:
- secretKey: PUBLISH_PASSWORD
remoteRef:
key: dumpnet
property: mediamtx.publish_password
- secretKey: HOOK_TOKEN
remoteRef:
key: dumpnet
property: mediamtx.hook_token
- secretKey: CREDS_TOKEN
remoteRef:
key: dumpnet
property: mediamtx.creds_token

View file

@ -0,0 +1,125 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mediamtx
namespace: mediamtx
spec:
replicas: 1
selector:
matchLabels:
app: mediamtx
template:
metadata:
labels:
app: mediamtx
spec:
hostNetwork: true
imagePullSecrets:
- name: forgejo-registry
containers:
- name: mediamtx
image: {{ .Values.mediamtx.image }}
command: ["/bin/sh", "-c"]
args:
- |
sed "s|__PUBLISH_PASSWORD__|$PUBLISH_PASSWORD|g" /etc/mediamtx/mediamtx.yml > /tmp/mediamtx.yml
exec /mediamtx /tmp/mediamtx.yml
env:
- name: HOOK_TOKEN
valueFrom:
secretKeyRef:
name: zoitestream-secrets
key: HOOK_TOKEN
- name: PUBLISH_PASSWORD
valueFrom:
secretKeyRef:
name: zoitestream-secrets
key: PUBLISH_PASSWORD
ports:
- name: rtmp
containerPort: {{ .Values.mediamtx.rtmpPort }}
- name: webrtc-http
containerPort: {{ .Values.mediamtx.webrtcHttpPort }}
- name: webrtc-udp
containerPort: {{ .Values.mediamtx.webrtcUdpPort }}
protocol: UDP
volumeMounts:
- name: config
mountPath: /etc/mediamtx/mediamtx.yml
subPath: mediamtx.yml
- name: zoitestream
image: "{{ .Values.registry.host }}/{{ .Values.registry.user }}/zoitestream:latest"
env:
- name: DOMAIN
value: "stream.{{ .Values.domain }}"
- name: RTMP_PORT
value: "{{ .Values.mediamtx.rtmpPort }}"
- name: PUBLISH_USER
value: {{ .Values.publishUser }}
- name: STATE_DB_PATH
value: /data/zoitestream.db
envFrom:
- secretRef:
name: zoitestream-secrets
ports:
- name: http
containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 3
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: config
configMap:
name: mediamtx-config
- name: data
hostPath:
path: /var/local/appdata/zoitestream
type: DirectoryOrCreate
---
# HTTP-only Service (landing page + API), routed through ingress-nginx
apiVersion: v1
kind: Service
metadata:
name: zoitestream
namespace: mediamtx
spec:
selector:
app: mediamtx
ports:
- port: 80
targetPort: 5000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zoitestream
namespace: mediamtx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- stream.{{ .Values.domain }}
secretName: zoitestream-tls
rules:
- host: stream.{{ .Values.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: zoitestream
port:
number: 80

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: Namespace
metadata:
name: mediamtx
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged

View file

@ -0,0 +1,27 @@
{{- $host := .Values.registry.host }}
{{- $user := .Values.registry.user }}
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: forgejo-registry
namespace: mediamtx
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: forgejo-registry
template:
engineVersion: v2
mergePolicy: Replace
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: '{"auths":{"{{ $host }}":{"username":"{{ $user }}","password":"{{ "{{" }} .registry_token {{ "}}" }}","auth":"{{ "{{" }} printf "{{ $user }}:%s" .registry_token | b64enc {{ "}}" }}"}}}'
data:
- secretKey: registry_token
remoteRef:
key: dumpnet
property: forgejo.registry_token

View file

@ -0,0 +1,10 @@
domain: dumpnet.chat
registry:
host: forge.keane.sh
user: ian
mediamtx:
image: bluenviron/mediamtx:latest-ffmpeg
rtmpPort: 1935
webrtcHttpPort: 8889
webrtcUdpPort: 8189
publishUser: streamer

View file

@ -2,4 +2,4 @@ domain: dumpnet.chat
registry: registry:
host: forge.keane.sh host: forge.keane.sh
user: ian user: ian
corsOrigins: "*" corsOrigins: "https://repertory.dumpnet.chat"

View file

@ -0,0 +1,33 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: mediamtx
namespace: argocd
spec:
project: default
sources:
- repoURL: https://forge.keane.sh/ian/dumpnet-argo.git
targetRevision: HEAD
path: charts/mediamtx
helm:
valueFiles:
- $values/values.yaml
- repoURL: https://forge.keane.sh/ian/dumpnet-argo.git
targetRevision: HEAD
ref: values
destination:
server: https://kubernetes.default.svc
namespace: mediamtx
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ignoreDifferences:
- group: external-secrets.io
kind: ExternalSecret
jsonPointers:
- /spec/target/template/mergePolicy
- /spec/target/template/engineVersion
- /spec/target/type

View file

@ -0,0 +1,147 @@
# Static site hosting for the repertory frontend (tunes.dumpnet.chat)
# S3 static website hosting behind CloudFront (for HTTPS) + ACM cert.
# NOTE: bucket name currently matches the live hostname (repertory.dumpnet.chat)
# because it's also used as an S3-website CNAME target internally. When this
# moves to tunes.dumpnet.chat, rename the bucket + update the Route53 record
# + alt name below to match.
resource "aws_s3_bucket" "repertory_frontend" {
bucket = "repertory.dumpnet.chat"
force_destroy = true
}
resource "aws_s3_bucket_website_configuration" "repertory_frontend" {
bucket = aws_s3_bucket.repertory_frontend.id
index_document {
suffix = "index.html"
}
}
resource "aws_s3_bucket_public_access_block" "repertory_frontend" {
bucket = aws_s3_bucket.repertory_frontend.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "repertory_frontend" {
bucket = aws_s3_bucket.repertory_frontend.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.repertory_frontend.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.repertory_frontend]
}
# --- ACM cert for CloudFront (must be in us-east-1, which this provider already is) ---
resource "aws_acm_certificate" "repertory_frontend" {
domain_name = "repertory.${var.domain}"
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "repertory_frontend_cert_validation" {
for_each = {
for dvo in aws_acm_certificate.repertory_frontend.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.aws_route53_zone.dumpnet.zone_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 300
}
resource "aws_acm_certificate_validation" "repertory_frontend" {
certificate_arn = aws_acm_certificate.repertory_frontend.arn
validation_record_fqdns = [for r in aws_route53_record.repertory_frontend_cert_validation : r.fqdn]
}
# --- CloudFront distribution in front of the S3 website endpoint ---
resource "aws_cloudfront_distribution" "repertory_frontend" {
enabled = true
default_root_object = "index.html"
aliases = ["repertory.${var.domain}"]
origin {
domain_name = aws_s3_bucket_website_configuration.repertory_frontend.website_endpoint
origin_id = "repertory-frontend-s3-website"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only" # S3 website endpoints are HTTP-only
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "repertory-frontend-s3-website"
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.repertory_frontend.certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
}
resource "aws_route53_record" "repertory_frontend" {
zone_id = data.aws_route53_zone.dumpnet.zone_id
name = "repertory.${var.domain}"
type = "A"
alias {
name = aws_cloudfront_distribution.repertory_frontend.domain_name
zone_id = aws_cloudfront_distribution.repertory_frontend.hosted_zone_id
evaluate_target_health = false
}
}
output "repertory_frontend_bucket" {
value = aws_s3_bucket.repertory_frontend.id
}
output "repertory_frontend_website_endpoint" {
value = aws_s3_bucket_website_configuration.repertory_frontend.website_endpoint
}
output "repertory_frontend_cloudfront_domain" {
value = aws_cloudfront_distribution.repertory_frontend.domain_name
}

View file

@ -43,6 +43,22 @@ resource "aws_security_group" "talos" {
self = true self = true
} }
ingress {
description = "MediaMTX RTMP ingest"
from_port = 1935
to_port = 1935
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "MediaMTX WebRTC ICE"
from_port = 8189
to_port = 8189
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
egress { egress {
from_port = 0 from_port = 0
to_port = 0 to_port = 0

View file

@ -1,2 +1,2 @@
hosted_zone_id = "Z068835512G0ZQJ9SJGOI" hosted_zone_id = "Z068835512G0ZQJ9SJGOI"
dns_records = ["argocd", "todo", "git-mcp", "git-mcp-oauth", "repertory-api"] dns_records = ["argocd", "todo", "git-mcp", "git-mcp-oauth", "repertory-api", "stream"]

View file

@ -5,6 +5,7 @@
clusterName: dumpnet clusterName: dumpnet
domain: dumpnet.chat domain: dumpnet.chat
publicIp: "32.195.88.32"
repoURL: https://forge.keane.sh/ian/dumpnet-argo.git repoURL: https://forge.keane.sh/ian/dumpnet-argo.git
certEmail: dumpnetcerts@keane.sh certEmail: dumpnetcerts@keane.sh
awsRegion: us-east-1 awsRegion: us-east-1