feat: stream key

This commit is contained in:
Rob Watson 2025-03-22 16:31:05 +01:00 committed by Rob Watson
parent 2468111369
commit 117ed7562c
16 changed files with 380 additions and 27 deletions

View File

@ -7,6 +7,5 @@ RUN apk add --no-cache \
curl
COPY --from=mediamtx /mediamtx /usr/bin/mediamtx
COPY build/mediamtx.yml /mediamtx.yml
CMD ["/usr/bin/mediamtx"]

24
go.mod
View File

@ -9,14 +9,21 @@ require (
github.com/opencontainers/image-spec v1.1.1
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.35.0
golang.design/x/clipboard v0.7.0
gopkg.in/yaml.v3 v3.0.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/chigopher/pathlib v0.19.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
@ -25,13 +32,17 @@ require (
github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -39,16 +50,24 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
@ -57,7 +76,10 @@ require (
github.com/spf13/viper v1.19.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/vektra/mockery/v2 v2.52.2 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
@ -65,6 +87,7 @@ require (
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/image v0.25.0 // indirect
@ -77,7 +100,6 @@ require (
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
)
tool github.com/vektra/mockery/v2

60
go.sum
View File

@ -1,3 +1,7 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@ -8,8 +12,15 @@ github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGzn
github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -36,9 +47,13 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@ -58,12 +73,16 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -81,6 +100,14 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@ -93,8 +120,11 @@ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNH
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE=
github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -111,6 +141,12 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@ -126,17 +162,32 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/vektra/mockery/v2 v2.52.2 h1:8QfPKUIrq8P3Cs7G79Iu4Byd5wdhGCE0quIS27x7rQo=
github.com/vektra/mockery/v2 v2.52.2/go.mod h1:zGDY/f6bip0Yh13GQ5j7xa43fuEoYBa4ICHEaihisHw=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
@ -168,6 +219,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8=
@ -211,16 +264,22 @@ golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@ -278,6 +337,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=

View File

@ -2,6 +2,7 @@ package app
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
@ -68,7 +69,13 @@ func Run(ctx context.Context, params RunParams) error {
}
ui.AllowQuit()
// While RTMP is the only source, it doesn't make sense to disable it.
if !params.Config.Sources.RTMP.Enabled {
return errors.New("config: sources.rtmp.enabled must be set to true")
}
srv := mediaserver.StartActor(ctx, mediaserver.StartActorParams{
StreamKey: mediaserver.StreamKey(params.Config.Sources.RTMP.StreamKey),
ContainerClient: containerClient,
Logger: logger.With("component", "mediaserver"),
})

View File

@ -5,6 +5,7 @@ package app_test
import (
"context"
"fmt"
"runtime"
"sync"
"testing"
"time"
@ -18,12 +19,28 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestIntegration(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
defer cancel()
destServer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "bluenviron/mediamtx:latest",
Env: map[string]string{"MTX_RTMPADDRESS": ":1936"},
ExposedPorts: []string{"1936/tcp"},
WaitingFor: wait.ForListeningPort("1936/tcp"),
},
Started: true,
})
testcontainers.CleanupContainer(t, destServer)
require.NoError(t, err)
destServerPort, err := destServer.MappedPort(ctx, "1936/tcp")
require.NoError(t, err)
logger := testhelpers.NewTestLogger().With("component", "integration")
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
require.NoError(t, err)
@ -79,18 +96,28 @@ func TestIntegration(t *testing.T) {
done := make(chan struct{})
go func() {
// https://stackoverflow.com/a/60740997/62871
if runtime.GOOS != "linux" {
panic("TODO: try host.docker.internal or Mac equivalent here")
}
const destHost = "172.17.0.1"
err := app.Run(ctx, app.RunParams{
Config: config.Config{
// We use the mediaserver as the destination server, just because it is
// reachable from the docker network via mediaserver:1935.
Sources: config.Sources{
RTMP: config.RTMPSource{
Enabled: true,
StreamKey: "live",
},
},
Destinations: []config.Destination{
{
Name: "Local server 1",
URL: "rtmp://mediaserver:1935/live/dest1",
URL: fmt.Sprintf("rtmp://%s:%d/live/dest1", destHost, destServerPort.Int()),
},
{
Name: "Local server 2",
URL: "rtmp://mediaserver:1935/live/dest2",
URL: fmt.Sprintf("rtmp://%s:%d/live/dest2", destHost, destServerPort.Int()),
},
},
},
@ -126,7 +153,6 @@ func TestIntegration(t *testing.T) {
t,
func(t *assert.CollectT) {
contents := getContents()
fmt.Println("test")
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
assert.Contains(t, contents[2], "Status receiving", "expected mediaserver status to be receiving")
@ -142,7 +168,7 @@ func TestIntegration(t *testing.T) {
assert.Contains(t, contents[3], "healthy", "expected local server 2 to be healthy")
},
time.Minute,
2*time.Minute,
time.Second,
)
@ -165,7 +191,7 @@ func TestIntegration(t *testing.T) {
require.Contains(t, contents[3], "Local server 2", "expected local server 2 to be present")
assert.Contains(t, contents[3], "exited", "expected local server 2 to have exited")
},
time.Minute,
2*time.Minute,
time.Second,
)

View File

@ -12,8 +12,20 @@ type LogFile struct {
Path string `yaml:"path"`
}
// RTMPSource holds the configuration for the RTMP source.
type RTMPSource struct {
Enabled bool `yaml:"enabled"`
StreamKey string `yaml:"streamkey"`
}
// Sources holds the configuration for the sources.
type Sources struct {
RTMP RTMPSource `yaml:"rtmp"`
}
// Config holds the configuration for the application.
type Config struct {
LogFile LogFile `yaml:"logfile"`
Sources Sources `yaml:"sources"`
Destinations []Destination `yaml:"destinations"`
}

View File

@ -63,6 +63,12 @@ func TestConfigServiceReadConfig(t *testing.T) {
Enabled: true,
Path: "test.log",
},
Sources: config.Sources{
RTMP: config.RTMPSource{
Enabled: true,
StreamKey: "s3cr3t",
},
},
Destinations: []config.Destination{
{
Name: "my stream",

View File

@ -2,6 +2,10 @@
logfile:
enabled: true
path: test.log
sources:
rtmp:
enabled: true
streamkey: s3cr3t
destinations:
- name: my stream
url: rtmp://rtmp.example.com:1935/live

View File

@ -1,6 +1,8 @@
package container
import (
"archive/tar"
"bytes"
"cmp"
"context"
"fmt"
@ -41,6 +43,7 @@ type DockerClient interface {
ContainerStart(context.Context, string, container.StartOptions) error
ContainerStats(context.Context, string, bool) (container.StatsResponseReader, error)
ContainerStop(context.Context, string, container.StopOptions) error
CopyToContainer(context.Context, string, string, io.Reader, container.CopyToContainerOptions) error
ContainerWait(context.Context, string, container.WaitCondition) (<-chan container.WaitResponse, <-chan error)
Events(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
ImagePull(context.Context, string, image.PullOptions) (io.ReadCloser, error)
@ -137,6 +140,12 @@ type NetworkCountConfig struct {
Tx string // the network name to count the Tx bytes
}
type CopyFileConfig struct {
Path string
Payload io.Reader
Mode int64
}
// RunContainerParams are the parameters for running a container.
type RunContainerParams struct {
Name string
@ -145,6 +154,7 @@ type RunContainerParams struct {
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
NetworkCountConfig NetworkCountConfig
CopyFileConfigs []CopyFileConfig
}
// RunContainer runs a container with the given parameters.
@ -202,6 +212,11 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
return
}
if err = a.copyFilesToContainer(ctx, createResp.ID, params.CopyFileConfigs); err != nil {
sendError(fmt.Errorf("copy files to container: %w", err))
return
}
if err = a.apiClient.ContainerStart(ctx, createResp.ID, container.StartOptions{}); err != nil {
sendError(fmt.Errorf("container start: %w", err))
return
@ -223,6 +238,44 @@ func (a *Client) RunContainer(ctx context.Context, params RunContainerParams) (<
return containerStateC, errC
}
func (a *Client) copyFilesToContainer(ctx context.Context, containerID string, fileConfigs []CopyFileConfig) error {
if len(fileConfigs) == 0 {
return nil
}
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
for _, fileConfig := range fileConfigs {
payload, err := io.ReadAll(fileConfig.Payload)
if err != nil {
return fmt.Errorf("read payload: %w", err)
}
hdr := tar.Header{
Name: fileConfig.Path,
Mode: fileConfig.Mode,
Size: int64(len(payload)),
}
if err := tw.WriteHeader(&hdr); err != nil {
return fmt.Errorf("write tar header: %w", err)
}
if _, err := tw.Write(payload); err != nil {
return fmt.Errorf("write tar payload: %w", err)
}
}
if err := tw.Close(); err != nil {
return fmt.Errorf("close tar writer: %w", err)
}
if err := a.apiClient.CopyToContainer(ctx, containerID, "/", &buf, container.CopyToContainerOptions{}); err != nil {
return fmt.Errorf("copy to container: %w", err)
}
return nil
}
type pullProgressDetail struct {
Curr int64 `json:"current"`
Total int64 `json:"total"`

View File

@ -53,6 +53,10 @@ func TestClientRunContainer(t *testing.T) {
EXPECT().
NetworkConnect(mock.Anything, "test-network", "123", (*network.EndpointSettings)(nil)).
Return(nil)
dockerClient.
EXPECT().
CopyToContainer(mock.Anything, "123", "/", mock.Anything, dockercontainer.CopyToContainerOptions{}).
Return(nil)
dockerClient.
EXPECT().
ContainerStart(mock.Anything, "123", dockercontainer.StartOptions{}).
@ -82,6 +86,18 @@ func TestClientRunContainer(t *testing.T) {
ChanSize: 1,
ContainerConfig: &dockercontainer.Config{Image: "alpine"},
HostConfig: &dockercontainer.HostConfig{},
CopyFileConfigs: []container.CopyFileConfig{
{
Path: "/hello",
Payload: bytes.NewReader([]byte("world")),
Mode: 0755,
},
{
Path: "/foo/bar",
Payload: bytes.NewReader([]byte("baz")),
Mode: 0755,
},
},
})
done := make(chan struct{})

View File

@ -518,6 +518,56 @@ func (_c *DockerClient_ContainerWait_Call) RunAndReturn(run func(context.Context
return _c
}
// CopyToContainer provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4
func (_m *DockerClient) CopyToContainer(_a0 context.Context, _a1 string, _a2 string, _a3 io.Reader, _a4 typescontainer.CopyToContainerOptions) error {
ret := _m.Called(_a0, _a1, _a2, _a3, _a4)
if len(ret) == 0 {
panic("no return value specified for CopyToContainer")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, io.Reader, typescontainer.CopyToContainerOptions) error); ok {
r0 = rf(_a0, _a1, _a2, _a3, _a4)
} else {
r0 = ret.Error(0)
}
return r0
}
// DockerClient_CopyToContainer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CopyToContainer'
type DockerClient_CopyToContainer_Call struct {
*mock.Call
}
// CopyToContainer is a helper method to define mock.On call
// - _a0 context.Context
// - _a1 string
// - _a2 string
// - _a3 io.Reader
// - _a4 typescontainer.CopyToContainerOptions
func (_e *DockerClient_Expecter) CopyToContainer(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}, _a4 interface{}) *DockerClient_CopyToContainer_Call {
return &DockerClient_CopyToContainer_Call{Call: _e.mock.On("CopyToContainer", _a0, _a1, _a2, _a3, _a4)}
}
func (_c *DockerClient_CopyToContainer_Call) Run(run func(_a0 context.Context, _a1 string, _a2 string, _a3 io.Reader, _a4 typescontainer.CopyToContainerOptions)) *DockerClient_CopyToContainer_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(io.Reader), args[4].(typescontainer.CopyToContainerOptions))
})
return _c
}
func (_c *DockerClient_CopyToContainer_Call) Return(_a0 error) *DockerClient_CopyToContainer_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *DockerClient_CopyToContainer_Call) RunAndReturn(run func(context.Context, string, string, io.Reader, typescontainer.CopyToContainerOptions) error) *DockerClient_CopyToContainer_Call {
_c.Call.Return(run)
return _c
}
// Events provides a mock function with given fields: _a0, _a1
func (_m *DockerClient) Events(_a0 context.Context, _a1 events.ListOptions) (<-chan events.Message, <-chan error) {
ret := _m.Called(_a0, _a1)

View File

@ -1,6 +1,7 @@
package mediaserver
import (
"bytes"
"cmp"
"context"
"fmt"
@ -11,18 +12,26 @@ import (
typescontainer "github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"gopkg.in/yaml.v3"
"git.netflux.io/rob/octoplex/internal/container"
"git.netflux.io/rob/octoplex/internal/domain"
)
// StreamKey is the stream key for the media server, which forms the RTMP path
// component and can be used as a basic form of authentication.
//
// It defaults to "live", in which case the full RTMP URL would be:
// `rtmp://localhost:1935/live`.
type StreamKey string
const (
defaultFetchIngressStateInterval = 5 * time.Second // default interval to fetch the state of the media server
defaultAPIPort = 9997 // default API host port for the media server
defaultRTMPPort = 1935 // default RTMP host port for the media server
defaultChanSize = 64 // default channel size for asynchronous non-error channels
imageNameMediaMTX = "netfluxio/mediamtx-alpine:latest" // image name for mediamtx
rtmpPath = "live" // RTMP path for the media server
defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey].
componentName = "mediaserver" // component name, mostly used for Docker labels
httpClientTimeout = time.Second // timeout for outgoing HTTP client requests
)
@ -39,6 +48,7 @@ type Actor struct {
containerClient *container.Client
apiPort int
rtmpPort int
streamKey StreamKey
fetchIngressStateInterval time.Duration
logger *slog.Logger
httpClient *http.Client
@ -52,6 +62,7 @@ type Actor struct {
type StartActorParams struct {
APIPort int // defaults to 9997
RTMPPort int // defaults to 1935
StreamKey StreamKey // defaults to "live"
ChanSize int // defaults to 64
FetchIngressStateInterval time.Duration // defaults to 5 seconds
ContainerClient *container.Client
@ -70,6 +81,7 @@ func StartActor(ctx context.Context, params StartActorParams) *Actor {
cancel: cancel,
apiPort: cmp.Or(params.APIPort, defaultAPIPort),
rtmpPort: cmp.Or(params.RTMPPort, defaultRTMPPort),
streamKey: cmp.Or(params.StreamKey, defaultStreamKey),
fetchIngressStateInterval: cmp.Or(params.FetchIngressStateInterval, defaultFetchIngressStateInterval),
actorC: make(chan action, chanSize),
state: new(domain.Source),
@ -83,6 +95,39 @@ func StartActor(ctx context.Context, params StartActorParams) *Actor {
rtmpPortSpec := nat.Port(strconv.Itoa(actor.rtmpPort) + ":1935")
exposedPorts, portBindings, _ := nat.ParsePortSpecs([]string{string(apiPortSpec), string(rtmpPortSpec)})
cfg, err := yaml.Marshal(
Config{
LogLevel: "debug",
LogDestinations: []string{"stdout"},
AuthMethod: "internal",
AuthInternalUsers: []User{
// TODO: tighten permissions
{
User: "any",
IPs: []string{}, // any IP
Permissions: []UserPermission{
{Action: "publish"},
{Action: "read"},
},
},
{
User: "any",
IPs: []string{"127.0.0.1", "::1", "172.17.0.0/16"},
Permissions: []UserPermission{{Action: "api"}},
},
},
API: true,
Paths: map[string]Path{
string(actor.streamKey): {
Source: "publisher",
},
},
},
)
if err != nil { // should never happen
panic(fmt.Sprintf("failed to marshal config: %v", err))
}
containerStateC, errC := params.ContainerClient.RunContainer(
ctx,
container.RunContainerParams{
@ -112,6 +157,13 @@ func StartActor(ctx context.Context, params StartActorParams) *Actor {
PortBindings: portBindings,
},
NetworkCountConfig: container.NetworkCountConfig{Rx: "eth0", Tx: "eth1"},
CopyFileConfigs: []container.CopyFileConfig{
{
Path: "/mediamtx.yml",
Payload: bytes.NewReader(cfg),
Mode: 0600,
},
},
},
)
@ -197,7 +249,7 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
sendState()
case <-fetchStateT.C:
ingressState, err := fetchIngressState(s.rtmpConnsURL(), s.httpClient)
ingressState, err := fetchIngressState(s.rtmpConnsURL(), s.streamKey, s.httpClient)
if err != nil {
s.logger.Error("Error fetching server state", "err", err)
continue
@ -222,7 +274,7 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
continue
}
if tracks, err := fetchTracks(s.pathsURL(), s.httpClient); err != nil {
if tracks, err := fetchTracks(s.pathsURL(), s.streamKey, s.httpClient); err != nil {
s.logger.Error("Error fetching tracks", "err", err)
resetFetchTracksT(3 * time.Second)
} else if len(tracks) == 0 {
@ -258,14 +310,14 @@ func (s *Actor) handleContainerExit(err error) {
// rtmpURL returns the RTMP URL for the media server, accessible from the host.
func (s *Actor) rtmpURL() string {
return fmt.Sprintf("rtmp://localhost:%d/%s", s.rtmpPort, rtmpPath)
return fmt.Sprintf("rtmp://localhost:%d/%s", s.rtmpPort, s.streamKey)
}
// rtmpInternalURL returns the RTMP URL for the media server, accessible from
// the app network.
func (s *Actor) rtmpInternalURL() string {
// Container port, not host port:
return fmt.Sprintf("rtmp://mediaserver:1935/%s", rtmpPath)
return fmt.Sprintf("rtmp://mediaserver:1935/%s", s.streamKey)
}
// rtmpConnsURL returns the URL for fetching RTMP connections, accessible from

View File

@ -32,7 +32,7 @@ type ingressStreamState struct {
}
// TODO: handle pagination
func fetchIngressState(apiURL string, httpClient httpClient) (state ingressStreamState, _ error) {
func fetchIngressState(apiURL string, streamKey StreamKey, httpClient httpClient) (state ingressStreamState, _ error) {
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return state, fmt.Errorf("new request: %w", err)
@ -58,7 +58,7 @@ func fetchIngressState(apiURL string, httpClient httpClient) (state ingressStrea
}
for _, conn := range resp.Items {
if conn.Path != rtmpPath {
if conn.Path != string(streamKey) {
continue
}
@ -82,7 +82,7 @@ type path struct {
}
// TODO: handle pagination
func fetchTracks(apiURL string, httpClient httpClient) ([]string, error) {
func fetchTracks(apiURL string, streamKey StreamKey, httpClient httpClient) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
if err != nil {
return nil, fmt.Errorf("new request: %w", err)
@ -109,7 +109,7 @@ func fetchTracks(apiURL string, httpClient httpClient) ([]string, error) {
var tracks []string
for _, path := range resp.Items {
if path.Name == rtmpPath {
if path.Name == string(streamKey) {
tracks = path.Tracks
}
}

View File

@ -79,7 +79,7 @@ func TestFetchIngressState(t *testing.T) {
})).
Return(tc.httpResponse, tc.httpError)
state, err := fetchIngressState(url, &httpClient)
state, err := fetchIngressState(url, StreamKey("live"), &httpClient)
if tc.wantErr != nil {
require.EqualError(t, err, tc.wantErr.Error())
} else {
@ -141,7 +141,7 @@ func TestFetchTracks(t *testing.T) {
})).
Return(tc.httpResponse, tc.httpError)
tracks, err := fetchTracks(url, &httpClient)
tracks, err := fetchTracks(url, StreamKey("live"), &httpClient)
if tc.wantErr != nil {
require.EqualError(t, err, tc.wantErr.Error())
} else {

View File

@ -0,0 +1,41 @@
package mediaserver
// Config represents the MediaMTX configuration file.
type Config struct {
LogLevel string `yaml:"logLevel,omitempty"`
LogDestinations []string `yaml:"logDestinations,omitempty"`
ReadTimeout string `yaml:"readTimeout,omitempty"`
WriteTimeout string `yaml:"writeTimeout,omitempty"`
WriteQueueSize int `yaml:"writeQueueSize,omitempty"`
UDPMaxPayloadSize int `yaml:"udpMaxPayloadSize,omitempty"`
AuthMethod string `yaml:"authMethod,omitempty"`
AuthInternalUsers []User `yaml:"authInternalUsers,omitempty"`
Metrics bool `yaml:"metrics,omitempty"`
MetricsAddress string `yaml:"metricsAddress,omitempty"`
API bool `yaml:"api,omitempty"`
APIAddr bool `yaml:"apiAddress,omitempty"`
RTMP bool `yaml:"rtmp,omitempty"`
RTMPAddress string `yaml:"rtmpAddress,omitempty"`
HLS bool `yaml:"hls"`
RTSP bool `yaml:"rtsp"`
WebRTC bool `yaml:"webrtc"`
SRT bool `yaml:"srt"`
Paths map[string]Path `yaml:"paths,omitempty"`
}
// Path represents a path configuration in MediaMTX.
type Path struct {
Source string `yaml:"source,omitempty"`
}
// UserPermission represents a user permission in MediaMTX.
type UserPermission struct {
Action string `yaml:"action,omitempty"`
}
// User represents a user configuration in MediaMTX.
type User struct {
User string `yaml:"user,omitempty"`
IPs []string `yaml:"ips,omitempty"`
Permissions []UserPermission `yaml:"permissions,omitempty"`
}

View File

@ -0,0 +1,5 @@
// package mediaserver is responsible for managing the media server, which is
// currently an instance of MediaMTX.
//
// https://github.com/bluenviron/mediamtx
package mediaserver