diff --git a/build/mediamtx.Dockerfile b/build/mediamtx.Dockerfile index 50193ed..ef7a3ec 100644 --- a/build/mediamtx.Dockerfile +++ b/build/mediamtx.Dockerfile @@ -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"] diff --git a/go.mod b/go.mod index 6961f88..c3bb887 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9b56a60..bf598ef 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/app/app.go b/internal/app/app.go index d07304f..51832c8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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"), }) diff --git a/internal/app/integration_test.go b/internal/app/integration_test.go index 950b4a6..ff860ff 100644 --- a/internal/app/integration_test.go +++ b/internal/app/integration_test.go @@ -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, ) diff --git a/internal/config/config.go b/internal/config/config.go index b1170c5..72f375b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` } diff --git a/internal/config/service_test.go b/internal/config/service_test.go index 039a287..ffcded5 100644 --- a/internal/config/service_test.go +++ b/internal/config/service_test.go @@ -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", diff --git a/internal/config/testdata/complete.yml b/internal/config/testdata/complete.yml index 6426993..bb95a11 100644 --- a/internal/config/testdata/complete.yml +++ b/internal/config/testdata/complete.yml @@ -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 diff --git a/internal/container/container.go b/internal/container/container.go index 8fd4b06..6106820 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -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"` diff --git a/internal/container/container_test.go b/internal/container/container_test.go index a318a2e..aa4aa7a 100644 --- a/internal/container/container_test.go +++ b/internal/container/container_test.go @@ -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{}) diff --git a/internal/container/mocks/dockerclient_mock.go b/internal/container/mocks/dockerclient_mock.go index edf889a..454394f 100644 --- a/internal/container/mocks/dockerclient_mock.go +++ b/internal/container/mocks/dockerclient_mock.go @@ -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) diff --git a/internal/mediaserver/actor.go b/internal/mediaserver/actor.go index a2fb165..78c2219 100644 --- a/internal/mediaserver/actor.go +++ b/internal/mediaserver/actor.go @@ -1,6 +1,7 @@ package mediaserver import ( + "bytes" "cmp" "context" "fmt" @@ -11,20 +12,28 @@ 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 - componentName = "mediaserver" // component name, mostly used for Docker labels - httpClientTimeout = time.Second // timeout for outgoing HTTP client requests + 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 + 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 ) // action is an action to be performed by the actor. @@ -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 diff --git a/internal/mediaserver/api.go b/internal/mediaserver/api.go index 24b631b..e216ea1 100644 --- a/internal/mediaserver/api.go +++ b/internal/mediaserver/api.go @@ -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 } } diff --git a/internal/mediaserver/api_test.go b/internal/mediaserver/api_test.go index f7e7e14..1c0951b 100644 --- a/internal/mediaserver/api_test.go +++ b/internal/mediaserver/api_test.go @@ -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 { diff --git a/internal/mediaserver/config.go b/internal/mediaserver/config.go new file mode 100644 index 0000000..61a5a8a --- /dev/null +++ b/internal/mediaserver/config.go @@ -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"` +} diff --git a/internal/mediaserver/doc.go b/internal/mediaserver/doc.go new file mode 100644 index 0000000..9cf99bf --- /dev/null +++ b/internal/mediaserver/doc.go @@ -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