feat: stream key
This commit is contained in:
parent
2468111369
commit
117ed7562c
@ -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
24
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
|
||||
|
60
go.sum
60
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=
|
||||
|
@ -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"),
|
||||
})
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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",
|
||||
|
4
internal/config/testdata/complete.yml
vendored
4
internal/config/testdata/complete.yml
vendored
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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{})
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
41
internal/mediaserver/config.go
Normal file
41
internal/mediaserver/config.go
Normal 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"`
|
||||
}
|
5
internal/mediaserver/doc.go
Normal file
5
internal/mediaserver/doc.go
Normal 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
|
Loading…
x
Reference in New Issue
Block a user