Compare commits
No commits in common. "add511e3dd56dabb11e5427cd497bbd77cd49983" and "98d93ad28606b394ca859ac363bb690abf96790f" have entirely different histories.
add511e3dd
...
98d93ad286
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
Octoplex is a live video restreamer for the terminal.
|
Octoplex is a live video restreamer for the terminal.
|
||||||
|
|
||||||
* Restream RTMP/RTMPS to unlimited destinations
|
* Restream RTMP to unlimited destinations
|
||||||
* Broadcast using OBS and other standard tools
|
* Broadcast using OBS and other standard tools
|
||||||
* Add and remove destinations while streaming
|
* Add and remove destinations while streaming
|
||||||
* Automatic reconnections
|
* Automatic reconnections
|
||||||
@ -100,9 +100,6 @@ sources:
|
|||||||
mediaServer:
|
mediaServer:
|
||||||
streamKey: live # defaults to "live"
|
streamKey: live # defaults to "live"
|
||||||
host: rtmp.example.com # defaults to "localhost"
|
host: rtmp.example.com # defaults to "localhost"
|
||||||
tls: # optional. If RTMPS is enabled, defaults to a
|
|
||||||
cert: /etc/mycert.pem # self-signed keypair corresponding to the host
|
|
||||||
key: /etc/mykey.pem # key.
|
|
||||||
rtmp:
|
rtmp:
|
||||||
enabled: true # defaults to false
|
enabled: true # defaults to false
|
||||||
ip: 127.0.0.1 # defaults to 127.0.0.1
|
ip: 127.0.0.1 # defaults to 127.0.0.1
|
||||||
|
@ -88,18 +88,10 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
updateUI := func() { ui.SetState(*state) }
|
updateUI := func() { ui.SetState(*state) }
|
||||||
updateUI()
|
updateUI()
|
||||||
|
|
||||||
var tlsCertPath, tlsKeyPath string
|
|
||||||
if cfg.Sources.MediaServer.TLS != nil {
|
|
||||||
tlsCertPath = cfg.Sources.MediaServer.TLS.CertPath
|
|
||||||
tlsKeyPath = cfg.Sources.MediaServer.TLS.KeyPath
|
|
||||||
}
|
|
||||||
|
|
||||||
srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{
|
srv, err := mediaserver.NewActor(ctx, mediaserver.NewActorParams{
|
||||||
RTMPAddr: buildNetAddr(cfg.Sources.MediaServer.RTMP),
|
RTMPAddr: buildNetAddr(cfg.Sources.MediaServer.RTMP),
|
||||||
RTMPSAddr: buildNetAddr(cfg.Sources.MediaServer.RTMPS),
|
RTMPSAddr: buildNetAddr(cfg.Sources.MediaServer.RTMPS),
|
||||||
Host: cfg.Sources.MediaServer.Host,
|
Host: cfg.Sources.MediaServer.Host,
|
||||||
TLSCertPath: tlsCertPath,
|
|
||||||
TLSKeyPath: tlsKeyPath,
|
|
||||||
StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey),
|
StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey),
|
||||||
ContainerClient: containerClient,
|
ContainerClient: containerClient,
|
||||||
Logger: logger.With("component", "mediaserver"),
|
Logger: logger.With("component", "mediaserver"),
|
||||||
@ -155,7 +147,7 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
|
|
||||||
logger.Debug("Command received", "cmd", cmd.Name())
|
logger.Debug("Command received", "cmd", cmd.Name())
|
||||||
switch c := cmd.(type) {
|
switch c := cmd.(type) {
|
||||||
case domain.CommandAddDestination:
|
case terminal.CommandAddDestination:
|
||||||
newCfg := cfg
|
newCfg := cfg
|
||||||
newCfg.Destinations = append(newCfg.Destinations, config.Destination{
|
newCfg.Destinations = append(newCfg.Destinations, config.Destination{
|
||||||
Name: c.DestinationName,
|
Name: c.DestinationName,
|
||||||
@ -169,7 +161,7 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
cfg = newCfg
|
cfg = newCfg
|
||||||
handleConfigUpdate(cfg, state, ui)
|
handleConfigUpdate(cfg, state, ui)
|
||||||
ui.DestinationAdded()
|
ui.DestinationAdded()
|
||||||
case domain.CommandRemoveDestination:
|
case terminal.CommandRemoveDestination:
|
||||||
repl.StopDestination(c.URL) // no-op if not live
|
repl.StopDestination(c.URL) // no-op if not live
|
||||||
newCfg := cfg
|
newCfg := cfg
|
||||||
newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool {
|
newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool {
|
||||||
@ -183,16 +175,16 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
cfg = newCfg
|
cfg = newCfg
|
||||||
handleConfigUpdate(cfg, state, ui)
|
handleConfigUpdate(cfg, state, ui)
|
||||||
ui.DestinationRemoved()
|
ui.DestinationRemoved()
|
||||||
case domain.CommandStartDestination:
|
case terminal.CommandStartDestination:
|
||||||
if !state.Source.Live {
|
if !state.Source.Live {
|
||||||
ui.ShowSourceNotLiveModal()
|
ui.ShowSourceNotLiveModal()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
repl.StartDestination(c.URL)
|
repl.StartDestination(c.URL)
|
||||||
case domain.CommandStopDestination:
|
case terminal.CommandStopDestination:
|
||||||
repl.StopDestination(c.URL)
|
repl.StopDestination(c.URL)
|
||||||
case domain.CommandQuit:
|
case terminal.CommandQuit:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case <-uiUpdateT.C:
|
case <-uiUpdateT.C:
|
||||||
|
@ -5,13 +5,9 @@ package app_test
|
|||||||
import (
|
import (
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -296,7 +292,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationCustomHost(t *testing.T) {
|
func TestIntegrationCustomRTMPURL(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -307,7 +303,7 @@ func TestIntegrationCustomHost(t *testing.T) {
|
|||||||
configService := setupConfigService(t, config.Config{
|
configService := setupConfigService(t, config.Config{
|
||||||
Sources: config.Sources{
|
Sources: config.Sources{
|
||||||
MediaServer: config.MediaServerSource{
|
MediaServer: config.MediaServerSource{
|
||||||
Host: "rtmp.example.com",
|
Host: "rtmp.live.tv",
|
||||||
RTMP: config.RTMPSource{Enabled: true},
|
RTMP: config.RTMPSource{Enabled: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -329,7 +325,7 @@ func TestIntegrationCustomHost(t *testing.T) {
|
|||||||
require.EventuallyWithT(
|
require.EventuallyWithT(
|
||||||
t,
|
t,
|
||||||
func(t *assert.CollectT) {
|
func(t *assert.CollectT) {
|
||||||
assert.True(t, contentsIncludes(getContents(), "rtmp://rtmp.example.com:1935/live"), "expected to see custom host name")
|
assert.True(t, contentsIncludes(getContents(), "rtmp://rtmp.live.tv:1935/live"), "expected to see custom host name")
|
||||||
},
|
},
|
||||||
waitTime,
|
waitTime,
|
||||||
time.Second,
|
time.Second,
|
||||||
@ -337,94 +333,6 @@ func TestIntegrationCustomHost(t *testing.T) {
|
|||||||
)
|
)
|
||||||
printScreen(t, getContents, "Ater opening the app with a custom host name")
|
printScreen(t, getContents, "Ater opening the app with a custom host name")
|
||||||
|
|
||||||
require.EventuallyWithT(
|
|
||||||
t,
|
|
||||||
func(c *assert.CollectT) {
|
|
||||||
conn, err := tls.Dial("tcp", "localhost:9997", &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
require.NoError(c, err)
|
|
||||||
|
|
||||||
require.Nil(
|
|
||||||
c,
|
|
||||||
conn.
|
|
||||||
ConnectionState().
|
|
||||||
PeerCertificates[0].
|
|
||||||
VerifyHostname("rtmp.example.com"),
|
|
||||||
"expected to verify custom host name",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
waitTime,
|
|
||||||
time.Second,
|
|
||||||
"expected to connect to API using self-signed TLS cert with custom host name",
|
|
||||||
)
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIntegrationCustomTLSCerts(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger(t).With("component", "integration")
|
|
||||||
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
configService := setupConfigService(t, config.Config{
|
|
||||||
Sources: config.Sources{
|
|
||||||
MediaServer: config.MediaServerSource{
|
|
||||||
TLS: &config.TLS{
|
|
||||||
CertPath: "testdata/server.crt",
|
|
||||||
KeyPath: "testdata/server.key",
|
|
||||||
},
|
|
||||||
RTMPS: config.RTMPSource{Enabled: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
screen, screenCaptureC, getContents := setupSimulationScreen(t)
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)))
|
|
||||||
}()
|
|
||||||
|
|
||||||
require.EventuallyWithT(
|
|
||||||
t,
|
|
||||||
func(c *assert.CollectT) {
|
|
||||||
certPEM, err := os.ReadFile("testdata/server.crt")
|
|
||||||
require.NoError(c, err)
|
|
||||||
|
|
||||||
block, _ := pem.Decode(certPEM)
|
|
||||||
require.NotNil(c, block, "failed to decode PEM block containing certificate")
|
|
||||||
require.True(c, block.Type == "CERTIFICATE", "expected PEM block to be a certificate")
|
|
||||||
|
|
||||||
rootCAs := x509.NewCertPool()
|
|
||||||
require.True(c, rootCAs.AppendCertsFromPEM(certPEM), "failed to append cert to root CA pool")
|
|
||||||
|
|
||||||
conn, err := tls.Dial("tcp", "localhost:1936", &tls.Config{
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
ServerName: "localhost",
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
})
|
|
||||||
require.NoError(c, err)
|
|
||||||
|
|
||||||
peerCert := conn.ConnectionState().PeerCertificates[0]
|
|
||||||
wantCert, err := x509.ParseCertificate(block.Bytes)
|
|
||||||
require.NoError(c, err)
|
|
||||||
require.True(c, peerCert.Equal(wantCert), "expected peer certificate to match the expected certificate")
|
|
||||||
},
|
|
||||||
waitTime,
|
|
||||||
time.Second,
|
|
||||||
)
|
|
||||||
|
|
||||||
printScreen(t, getContents, "After starting the app with custom TLS certs")
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
|
17
internal/app/testdata/openssl.cnf
vendored
17
internal/app/testdata/openssl.cnf
vendored
@ -1,17 +0,0 @@
|
|||||||
# openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout server.key -out server.crt -config openssl.cnf
|
|
||||||
|
|
||||||
[req]
|
|
||||||
default_bits = 2048
|
|
||||||
prompt = no
|
|
||||||
default_md = sha256
|
|
||||||
distinguished_name = dn
|
|
||||||
x509_extensions = v3_req
|
|
||||||
|
|
||||||
[dn]
|
|
||||||
CN = localhost
|
|
||||||
|
|
||||||
[v3_req]
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
DNS.1 = localhost
|
|
18
internal/app/testdata/server.crt
vendored
18
internal/app/testdata/server.crt
vendored
@ -1,18 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIC7TCCAdWgAwIBAgIUTeqv46R19q+BS2e4DBkbIHuWyIIwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDQyMDA4NTMwN1oXDTM1MDQx
|
|
||||||
ODA4NTMwN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEA0v/KndfKfG8XItStHeMQ/3z1r8vhkH9KGpfSwDMp8MdH
|
|
||||||
Mox6vcAsIIr1RFKmalQQg+T+TK9v3XM6F4sJ+WPyb5/31xLUqG6zivitrMy1AZ8w
|
|
||||||
XLgAz/CTufXL3OBntDwg29QXWt9lOUJyjRa66AQqreTlItuLG65bswfPA4g35f+U
|
|
||||||
hyr49paukqnVHRr44GtyiNxlfYCEdQWdOR0EQmZ7y6WNQQhnR8odQyftR2lykf17
|
|
||||||
MSJ8us4JAgZ2fr1QR+DfX5bCSS/WJ2aO7xxeES40NizBx08qYFami1zXrGMMo35I
|
|
||||||
SfedCohcok8ZZ1oWL+MfSJ2OLVclDnznDPTx39pZPQIDAQABozcwNTAUBgNVHREE
|
|
||||||
DTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFCgZah+m2NXkI9biS2vnhNUrd3FiMA0G
|
|
||||||
CSqGSIb3DQEBCwUAA4IBAQAPbofZIKCm3DnudFnK+LRkdlpMNOyH2zn3g8h8vrfL
|
|
||||||
Tfi0oBgHb7EYxcHYDanZbcIKracWCfQVze2FRLgNFBWiyhDO4IXe/LpwSnbyLWCh
|
|
||||||
psbGuyVmEz9CuiyVdIi+CWQs5dBBRUCFg6NE2/r6Diw9LD0fVCVUwkvqopetfp1B
|
|
||||||
tvA74O0RduLWs+iXNs5XW4sODVkrOmhBbRrP9GRCVqiqVWJka6CzrNdBm0Y9zZMQ
|
|
||||||
GD/6fEgDaW8YlShoO+e4FwmD2IgIx+m4xamr/cQkWpbOHMxAwv7vP0stfkpyUacW
|
|
||||||
dh9eJmsDAmgGgdtMJvbIfyR9ilG8D6zwOmSlkF6fDJ3E
|
|
||||||
-----END CERTIFICATE-----
|
|
28
internal/app/testdata/server.key
vendored
28
internal/app/testdata/server.key
vendored
@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDS/8qd18p8bxci
|
|
||||||
1K0d4xD/fPWvy+GQf0oal9LAMynwx0cyjHq9wCwgivVEUqZqVBCD5P5Mr2/dczoX
|
|
||||||
iwn5Y/Jvn/fXEtSobrOK+K2szLUBnzBcuADP8JO59cvc4Ge0PCDb1Bda32U5QnKN
|
|
||||||
FrroBCqt5OUi24sbrluzB88DiDfl/5SHKvj2lq6SqdUdGvjga3KI3GV9gIR1BZ05
|
|
||||||
HQRCZnvLpY1BCGdHyh1DJ+1HaXKR/XsxIny6zgkCBnZ+vVBH4N9flsJJL9YnZo7v
|
|
||||||
HF4RLjQ2LMHHTypgVqaLXNesYwyjfkhJ950KiFyiTxlnWhYv4x9InY4tVyUOfOcM
|
|
||||||
9PHf2lk9AgMBAAECggEAC3E3qaukHW9gz9C8upwvtcsu/6OMzes5N4v4L9gWdCo6
|
|
||||||
YDFiDpw3SGSAvH3G7Ik2hBCNAdeZt2aiRdiSZ+XVpdwE8rLguWmXbvfhYzeOsVHS
|
|
||||||
q5SG5r/jIviDX60DsrB4D7PGuHTY5mwGDkSnSiG/tsJs8qD5QD0KWAEaZtSiQ2Sp
|
|
||||||
kcRbdq13/2tjHyx7nBxEYUFC4EJQjK3cNNV4G7nG2xcfT46uPvFV0+1CQtMpFYhi
|
|
||||||
IsGaSBhW9gOAheycYxCi+LRdUh1IAnLUyYUenu0o8PoXsHp6KD8eS5RXtfA6THd/
|
|
||||||
Jr614gdAB2Sffw+bFf6FIBNWa5Jwsg9UtbGtjNdo+QKBgQDrOJ2nj7El6MIqeDHs
|
|
||||||
1cCeGDKmjB1CYWALLHrwwiwmrvEoeBMiJuMN4epZdQw9hwExa7fNpERI7Ay8s5HD
|
|
||||||
cdppxgcW7CWChNncbVZ39P+YI9URWC2Q2Y8FBhc9FA0sKpDak0rf5UE63SGjU8/I
|
|
||||||
FGgwjd1Ln5wws00OsYXBZw1lzwKBgQDlo2kRy6xvrUNAbeggT9OQeg2SdkWqvS3v
|
|
||||||
NUhBzZkVhJNf1oApNRoAvRMQt+Xt+Euw1pQ+TvdOZQhhqxs/pD/wGdM7rhq9r0+G
|
|
||||||
itsQ5LvNCxCePbSkbFMLgC8JgNuM3aRqhtsU+Illk9xvCj2nKsd+UUN3NxYgjCqa
|
|
||||||
evTKSzUfMwKBgFapy1w7EteWxEMFec96ibc1zyORqA4W9l3ni3w87itqdSul4dbJ
|
|
||||||
YQpyW/eNqm7Y2NWujE/V39rGLYMw3dmWjxQ9g8ssQj2uWN5f4mXb/He/a/cx98fQ
|
|
||||||
gGMndVRpmNjW7fu6HPIU802Ov5//dySOcDzDZ+8+5TsENLXfLhqtrz/9AoGBALc+
|
|
||||||
/BQoTFTdlSHv0mEecjwDOZtbZ+KEjggpo5xm/TbPkW7T03eOmU5nkrQvm3qXPYdC
|
|
||||||
5A8Ioo5bTyHpEZhqcF8frJEeMNaW88XwPjmv3TEVGFC9+s2OZ4Jw6pgRzKEPKSmc
|
|
||||||
rWyBm9qD8E5nhKVGHOVu4YBbY/va/hBB998Jvr1DAoGBAK5nnswLyQZi0lgpkl1P
|
|
||||||
ITkmvnQlZBfuqvoD7wcQ3nx/K/mdacsxepRne+U/4+iNzRtd3gU0iccCWUTJl4aB
|
|
||||||
cFRW1eXWuff+4vmM4JToDevGPXrS0CHE20mATJRZPH+YjZFl0pFSc4/tnjxBnx4y
|
|
||||||
vgM382WU9N9jIHCCnM6DYsbK
|
|
||||||
-----END PRIVATE KEY-----
|
|
@ -35,19 +35,12 @@ type RTMPSource struct {
|
|||||||
NetAddr `yaml:",inline"`
|
NetAddr `yaml:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLS holds the TLS configuration.
|
|
||||||
type TLS struct {
|
|
||||||
CertPath string `yaml:"cert,omitempty"`
|
|
||||||
KeyPath string `yaml:"key,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MediaServerSource holds the configuration for the media server source.
|
// MediaServerSource holds the configuration for the media server source.
|
||||||
type MediaServerSource struct {
|
type MediaServerSource struct {
|
||||||
StreamKey string `yaml:"streamKey,omitempty"`
|
StreamKey string `yaml:"streamKey,omitempty"`
|
||||||
Host string `yaml:"host,omitempty"`
|
Host string `yaml:"host,omitempty"`
|
||||||
TLS *TLS `yaml:"tls,omitempty"`
|
RTMP RTMPSource `yaml:"rtmp,omitempty"`
|
||||||
RTMP RTMPSource `yaml:"rtmp"`
|
RTMPS RTMPSource `yaml:"rtmps,omitempty"`
|
||||||
RTMPS RTMPSource `yaml:"rtmps"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sources holds the configuration for the sources.
|
// Sources holds the configuration for the sources.
|
||||||
|
@ -100,10 +100,6 @@ func TestConfigServiceReadConfig(t *testing.T) {
|
|||||||
MediaServer: config.MediaServerSource{
|
MediaServer: config.MediaServerSource{
|
||||||
StreamKey: "s3cr3t",
|
StreamKey: "s3cr3t",
|
||||||
Host: "rtmp.example.com",
|
Host: "rtmp.example.com",
|
||||||
TLS: &config.TLS{
|
|
||||||
CertPath: "/etc/cert.pem",
|
|
||||||
KeyPath: "/etc/key.pem",
|
|
||||||
},
|
|
||||||
RTMP: config.RTMPSource{
|
RTMP: config.RTMPSource{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
NetAddr: config.NetAddr{
|
NetAddr: config.NetAddr{
|
||||||
|
3
internal/config/testdata/complete.yml
vendored
3
internal/config/testdata/complete.yml
vendored
@ -6,9 +6,6 @@ sources:
|
|||||||
mediaServer:
|
mediaServer:
|
||||||
streamKey: s3cr3t
|
streamKey: s3cr3t
|
||||||
host: rtmp.example.com
|
host: rtmp.example.com
|
||||||
tls:
|
|
||||||
cert: /etc/cert.pem
|
|
||||||
key: /etc/key.pem
|
|
||||||
rtmp:
|
rtmp:
|
||||||
enabled: true
|
enabled: true
|
||||||
ip: 0.0.0.0
|
ip: 0.0.0.0
|
||||||
|
@ -66,16 +66,6 @@ func (n NetAddr) IsZero() bool {
|
|||||||
return n.IP == "" && n.Port == 0
|
return n.IP == "" && n.Port == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyPair holds a TLS key pair.
|
|
||||||
type KeyPair struct {
|
|
||||||
Cert, Key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsZero returns true if the KeyPair is zero value.
|
|
||||||
func (k KeyPair) IsZero() bool {
|
|
||||||
return k.Cert == nil && k.Key == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container status strings.
|
// Container status strings.
|
||||||
//
|
//
|
||||||
// TODO: refactor to strictly reflect Docker status strings.
|
// TODO: refactor to strictly reflect Docker status strings.
|
||||||
|
@ -40,12 +40,3 @@ func TestNetAddr(t *testing.T) {
|
|||||||
addr.Port = 3000
|
addr.Port = 3000
|
||||||
assert.False(t, addr.IsZero())
|
assert.False(t, addr.IsZero())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeyPair(t *testing.T) {
|
|
||||||
var keyPair domain.KeyPair
|
|
||||||
assert.True(t, keyPair.IsZero())
|
|
||||||
|
|
||||||
keyPair.Cert = []byte("cert")
|
|
||||||
keyPair.Key = []byte("key")
|
|
||||||
assert.False(t, keyPair.IsZero())
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
typescontainer "github.com/docker/docker/api/types/container"
|
typescontainer "github.com/docker/docker/api/types/container"
|
||||||
@ -38,11 +37,6 @@ const (
|
|||||||
defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey].
|
defaultStreamKey StreamKey = "live" // Default stream key. See [StreamKey].
|
||||||
componentName = "mediaserver" // component name, mostly used for Docker labels
|
componentName = "mediaserver" // component name, mostly used for Docker labels
|
||||||
httpClientTimeout = time.Second // timeout for outgoing HTTP client requests
|
httpClientTimeout = time.Second // timeout for outgoing HTTP client requests
|
||||||
configPath = "/mediamtx.yml" // path to the media server config file
|
|
||||||
tlsInternalCertPath = "/etc/tls-internal.crt" // path to the internal TLS cert
|
|
||||||
tlsInternalKeyPath = "/etc/tls-internal.key" // path to the internal TLS key
|
|
||||||
tlsCertPath = "/etc/tls.crt" // path to the custom TLS cert
|
|
||||||
tlsKeyPath = "/etc/tls.key" // path to the custom TLS key
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// action is an action to be performed by the actor.
|
// action is an action to be performed by the actor.
|
||||||
@ -60,9 +54,8 @@ type Actor struct {
|
|||||||
host string
|
host string
|
||||||
streamKey StreamKey
|
streamKey StreamKey
|
||||||
updateStateInterval time.Duration
|
updateStateInterval time.Duration
|
||||||
pass string // password for the media server
|
pass string // password for the media server
|
||||||
keyPairInternal domain.KeyPair // TLS key pair for the media server
|
tlsCert, tlsKey []byte // TLS cert and key for the media server
|
||||||
keyPairCustom domain.KeyPair // TLS key pair for the media server
|
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
apiClient *http.Client
|
apiClient *http.Client
|
||||||
|
|
||||||
@ -77,8 +70,6 @@ type NewActorParams struct {
|
|||||||
RTMPSAddr OptionalNetAddr // defaults to disabled, or 127.0.0.1:1936
|
RTMPSAddr OptionalNetAddr // defaults to disabled, or 127.0.0.1:1936
|
||||||
APIPort int // defaults to 9997
|
APIPort int // defaults to 9997
|
||||||
Host string // defaults to "localhost"
|
Host string // defaults to "localhost"
|
||||||
TLSCertPath string // defaults to empty
|
|
||||||
TLSKeyPath string // defaults to empty
|
|
||||||
StreamKey StreamKey // defaults to "live"
|
StreamKey StreamKey // defaults to "live"
|
||||||
ChanSize int // defaults to 64
|
ChanSize int // defaults to 64
|
||||||
UpdateStateInterval time.Duration // defaults to 5 seconds
|
UpdateStateInterval time.Duration // defaults to 5 seconds
|
||||||
@ -98,30 +89,11 @@ type OptionalNetAddr struct {
|
|||||||
//
|
//
|
||||||
// Callers must consume the state channel exposed via [C].
|
// Callers must consume the state channel exposed via [C].
|
||||||
func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error) {
|
func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error) {
|
||||||
dnsNames := []string{"localhost"}
|
tlsCert, tlsKey, err := generateTLSCert()
|
||||||
if params.Host != "" {
|
|
||||||
dnsNames = append(dnsNames, params.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPairInternal, err := generateTLSCert(dnsNames...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("generate TLS cert: %w", err)
|
return nil, fmt.Errorf("generate TLS cert: %w", err)
|
||||||
}
|
}
|
||||||
|
apiClient, err := buildAPIClient(tlsCert)
|
||||||
var keyPairCustom domain.KeyPair
|
|
||||||
if params.TLSCertPath != "" {
|
|
||||||
keyPairCustom.Cert, err = os.ReadFile(params.TLSCertPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read TLS cert: %w", err)
|
|
||||||
}
|
|
||||||
keyPairCustom.Key, err = os.ReadFile(params.TLSKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("read TLS key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: custom cert for API?
|
|
||||||
apiClient, err := buildAPIClient(keyPairInternal.Cert)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("build API client: %w", err)
|
return nil, fmt.Errorf("build API client: %w", err)
|
||||||
}
|
}
|
||||||
@ -134,8 +106,8 @@ func NewActor(ctx context.Context, params NewActorParams) (_ *Actor, err error)
|
|||||||
host: cmp.Or(params.Host, defaultHost),
|
host: cmp.Or(params.Host, defaultHost),
|
||||||
streamKey: cmp.Or(params.StreamKey, defaultStreamKey),
|
streamKey: cmp.Or(params.StreamKey, defaultStreamKey),
|
||||||
updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval),
|
updateStateInterval: cmp.Or(params.UpdateStateInterval, defaultUpdateStateInterval),
|
||||||
keyPairInternal: keyPairInternal,
|
tlsCert: tlsCert,
|
||||||
keyPairCustom: keyPairCustom,
|
tlsKey: tlsKey,
|
||||||
pass: generatePassword(),
|
pass: generatePassword(),
|
||||||
actorC: make(chan action, chanSize),
|
actorC: make(chan action, chanSize),
|
||||||
state: new(domain.Source),
|
state: new(domain.Source),
|
||||||
@ -166,45 +138,6 @@ func (a *Actor) Start(ctx context.Context) error {
|
|||||||
return fmt.Errorf("build server config: %w", err)
|
return fmt.Errorf("build server config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
copyFiles := []container.CopyFileConfig{
|
|
||||||
{
|
|
||||||
Path: configPath,
|
|
||||||
Payload: bytes.NewReader(cfg),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Path: tlsInternalCertPath,
|
|
||||||
Payload: bytes.NewReader(a.keyPairInternal.Cert),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Path: tlsInternalKeyPath,
|
|
||||||
Payload: bytes.NewReader(a.keyPairInternal.Key),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Path: "/etc/healthcheckopts.txt",
|
|
||||||
Payload: bytes.NewReader([]byte(fmt.Sprintf("--user api:%s", a.pass))),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if !a.keyPairCustom.IsZero() {
|
|
||||||
copyFiles = append(
|
|
||||||
copyFiles,
|
|
||||||
container.CopyFileConfig{
|
|
||||||
Path: tlsCertPath,
|
|
||||||
Payload: bytes.NewReader(a.keyPairCustom.Cert),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
container.CopyFileConfig{
|
|
||||||
Path: tlsKeyPath,
|
|
||||||
Payload: bytes.NewReader(a.keyPairCustom.Key),
|
|
||||||
Mode: 0600,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []any{"host", a.host}
|
args := []any{"host", a.host}
|
||||||
if a.rtmpAddr.IsZero() {
|
if a.rtmpAddr.IsZero() {
|
||||||
args = append(args, "rtmp.enabled", false)
|
args = append(args, "rtmp.enabled", false)
|
||||||
@ -233,7 +166,7 @@ func (a *Actor) Start(ctx context.Context) error {
|
|||||||
"curl",
|
"curl",
|
||||||
"--fail",
|
"--fail",
|
||||||
"--silent",
|
"--silent",
|
||||||
"--cacert", "/etc/tls-internal.crt",
|
"--cacert", "/etc/tls.crt",
|
||||||
"--config", "/etc/healthcheckopts.txt",
|
"--config", "/etc/healthcheckopts.txt",
|
||||||
a.healthCheckURL(),
|
a.healthCheckURL(),
|
||||||
},
|
},
|
||||||
@ -250,7 +183,28 @@ func (a *Actor) Start(ctx context.Context) error {
|
|||||||
},
|
},
|
||||||
NetworkCountConfig: container.NetworkCountConfig{Rx: "eth0", Tx: "eth1"},
|
NetworkCountConfig: container.NetworkCountConfig{Rx: "eth0", Tx: "eth1"},
|
||||||
Logs: container.LogConfig{Stdout: true},
|
Logs: container.LogConfig{Stdout: true},
|
||||||
CopyFiles: copyFiles,
|
CopyFiles: []container.CopyFileConfig{
|
||||||
|
{
|
||||||
|
Path: "/mediamtx.yml",
|
||||||
|
Payload: bytes.NewReader(cfg),
|
||||||
|
Mode: 0600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/etc/tls.crt",
|
||||||
|
Payload: bytes.NewReader(a.tlsCert),
|
||||||
|
Mode: 0600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/etc/tls.key",
|
||||||
|
Payload: bytes.NewReader(a.tlsKey),
|
||||||
|
Mode: 0600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/etc/healthcheckopts.txt",
|
||||||
|
Payload: bytes.NewReader([]byte(fmt.Sprintf("--user api:%s", a.pass))),
|
||||||
|
Mode: 0600,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -270,15 +224,6 @@ func (a *Actor) buildServerConfig() ([]byte, error) {
|
|||||||
encryptionString = "optional"
|
encryptionString = "optional"
|
||||||
}
|
}
|
||||||
|
|
||||||
var certPath, keyPath string
|
|
||||||
if a.keyPairCustom.IsZero() {
|
|
||||||
certPath = tlsInternalCertPath
|
|
||||||
keyPath = tlsInternalKeyPath
|
|
||||||
} else {
|
|
||||||
certPath = tlsCertPath
|
|
||||||
keyPath = tlsKeyPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return yaml.Marshal(
|
return yaml.Marshal(
|
||||||
Config{
|
Config{
|
||||||
LogLevel: "debug",
|
LogLevel: "debug",
|
||||||
@ -311,12 +256,12 @@ func (a *Actor) buildServerConfig() ([]byte, error) {
|
|||||||
RTMPEncryption: encryptionString,
|
RTMPEncryption: encryptionString,
|
||||||
RTMPAddress: ":1935",
|
RTMPAddress: ":1935",
|
||||||
RTMPSAddress: ":1936",
|
RTMPSAddress: ":1936",
|
||||||
RTMPServerCert: certPath,
|
RTMPServerCert: "/etc/tls.crt", // TODO: custom certs
|
||||||
RTMPServerKey: keyPath,
|
RTMPServerKey: "/etc/tls.key", // TODO: custom certs
|
||||||
API: true,
|
API: true,
|
||||||
APIEncryption: true,
|
APIEncryption: true,
|
||||||
APIServerCert: tlsInternalCertPath,
|
APIServerCert: "/etc/tls.crt",
|
||||||
APIServerKey: tlsInternalKeyPath,
|
APIServerKey: "/etc/tls.key",
|
||||||
Paths: map[string]Path{
|
Paths: map[string]Path{
|
||||||
string(a.streamKey): {Source: "publisher"},
|
string(a.streamKey): {Source: "publisher"},
|
||||||
},
|
},
|
||||||
|
@ -10,20 +10,23 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
"git.netflux.io/rob/octoplex/internal/domain"
|
type (
|
||||||
|
tlsCert []byte
|
||||||
|
tlsKey []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
// generateTLSCert generates a self-signed TLS certificate and private key.
|
// generateTLSCert generates a self-signed TLS certificate and private key.
|
||||||
func generateTLSCert(dnsNames ...string) (domain.KeyPair, error) {
|
func generateTLSCert() (tlsCert, tlsKey, error) {
|
||||||
privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -37,31 +40,28 @@ func generateTLSCert(dnsNames ...string) (domain.KeyPair, error) {
|
|||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
DNSNames: dnsNames,
|
DNSNames: []string{"localhost"},
|
||||||
}
|
}
|
||||||
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var certPEM, keyPEM bytes.Buffer
|
var certPEM, keyPEM bytes.Buffer
|
||||||
|
|
||||||
if err = pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
if err = pem.Encode(&certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
privKeyDER, err := x509.MarshalECPrivateKey(privKey)
|
privKeyDER, err := x509.MarshalECPrivateKey(privKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pem.Encode(&keyPEM, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privKeyDER}); err != nil {
|
if err := pem.Encode(&keyPEM, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privKeyDER}); err != nil {
|
||||||
return domain.KeyPair{}, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain.KeyPair{
|
return certPEM.Bytes(), keyPEM.Bytes(), nil
|
||||||
Cert: certPEM.Bytes(),
|
|
||||||
Key: keyPEM.Bytes(),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateTLSCert(t *testing.T) {
|
func TestGenerateTLSCert(t *testing.T) {
|
||||||
keyPair, err := generateTLSCert("localhost", "rtmp.example.com")
|
certPEM, keyPEM, err := generateTLSCert()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, keyPair.Cert)
|
require.NotEmpty(t, certPEM)
|
||||||
require.NotEmpty(t, keyPair.Key)
|
require.NotEmpty(t, keyPEM)
|
||||||
|
|
||||||
block, _ := pem.Decode(keyPair.Cert)
|
block, _ := pem.Decode(certPEM)
|
||||||
require.NotNil(t, block, "failed to decode certificate PEM")
|
require.NotNil(t, block, "failed to decode certificate PEM")
|
||||||
|
|
||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
@ -33,10 +33,8 @@ func TestGenerateTLSCert(t *testing.T) {
|
|||||||
assert.True(t, cert.BasicConstraintsValid, "basic constraints should be valid")
|
assert.True(t, cert.BasicConstraintsValid, "basic constraints should be valid")
|
||||||
assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
|
assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
|
||||||
assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
|
assert.Contains(t, cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
|
||||||
assert.Contains(t, cert.DNSNames, "localhost", "DNS names should include localhost")
|
|
||||||
assert.Contains(t, cert.DNSNames, "rtmp.example.com", "DNS names should include rtmp.example.com")
|
|
||||||
|
|
||||||
block, _ = pem.Decode(keyPair.Key)
|
block, _ = pem.Decode(keyPEM)
|
||||||
require.NotNil(t, block, "failed to decode private key PEM")
|
require.NotNil(t, block, "failed to decode private key PEM")
|
||||||
|
|
||||||
privKey, err := x509.ParseECPrivateKey(block.Bytes)
|
privKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package domain
|
package terminal
|
||||||
|
|
||||||
// CommandAddDestination adds a destination.
|
// CommandAddDestination adds a destination.
|
||||||
type CommandAddDestination struct {
|
type CommandAddDestination struct {
|
@ -40,7 +40,7 @@ const (
|
|||||||
|
|
||||||
// UI is responsible for managing the terminal user interface.
|
// UI is responsible for managing the terminal user interface.
|
||||||
type UI struct {
|
type UI struct {
|
||||||
commandC chan domain.Command
|
commandC chan Command
|
||||||
clipboardAvailable bool
|
clipboardAvailable bool
|
||||||
configFilePath string
|
configFilePath string
|
||||||
rtmpURL, rtmpsURL string
|
rtmpURL, rtmpsURL string
|
||||||
@ -106,7 +106,7 @@ const defaultChanSize = 64
|
|||||||
// StartUI starts the terminal user interface.
|
// StartUI starts the terminal user interface.
|
||||||
func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
||||||
chanSize := cmp.Or(params.ChanSize, defaultChanSize)
|
chanSize := cmp.Or(params.ChanSize, defaultChanSize)
|
||||||
commandCh := make(chan domain.Command, chanSize)
|
commandCh := make(chan Command, chanSize)
|
||||||
|
|
||||||
app := tview.NewApplication()
|
app := tview.NewApplication()
|
||||||
|
|
||||||
@ -268,7 +268,7 @@ func (ui *UI) renderAboutView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// C returns a channel that receives commands from the user interface.
|
// C returns a channel that receives commands from the user interface.
|
||||||
func (ui *UI) C() <-chan domain.Command {
|
func (ui *UI) C() <-chan Command {
|
||||||
return ui.commandC
|
return ui.commandC
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,7 +444,7 @@ func (ui *UI) ShowFatalErrorModal(errString string) {
|
|||||||
[]string{"Quit"},
|
[]string{"Quit"},
|
||||||
false,
|
false,
|
||||||
func(int, string) {
|
func(int, string) {
|
||||||
ui.commandC <- domain.CommandQuit{}
|
ui.commandC <- CommandQuit{}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -697,7 +697,7 @@ func (ui *UI) handleMediaServerClosed(exitReason string) {
|
|||||||
SetBackgroundColor(tcell.ColorBlack).
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(int, string) {
|
SetDoneFunc(func(int, string) {
|
||||||
ui.commandC <- domain.CommandQuit{}
|
ui.commandC <- CommandQuit{}
|
||||||
})
|
})
|
||||||
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
@ -887,7 +887,7 @@ func (ui *UI) addDestination() {
|
|||||||
AddInputField(inputLabelName, "My stream", inputLen, nil, nil).
|
AddInputField(inputLabelName, "My stream", inputLen, nil, nil).
|
||||||
AddInputField(inputLabelURL, "rtmp://", inputLen, nil, nil).
|
AddInputField(inputLabelURL, "rtmp://", inputLen, nil, nil).
|
||||||
AddButton("Add", func() {
|
AddButton("Add", func() {
|
||||||
ui.commandC <- domain.CommandAddDestination{
|
ui.commandC <- CommandAddDestination{
|
||||||
DestinationName: form.GetFormItemByLabel(inputLabelName).(*tview.InputField).GetText(),
|
DestinationName: form.GetFormItemByLabel(inputLabelName).(*tview.InputField).GetText(),
|
||||||
URL: form.GetFormItemByLabel(inputLabelURL).(*tview.InputField).GetText(),
|
URL: form.GetFormItemByLabel(inputLabelURL).(*tview.InputField).GetText(),
|
||||||
}
|
}
|
||||||
@ -945,7 +945,7 @@ func (ui *UI) removeDestination() {
|
|||||||
false,
|
false,
|
||||||
func(buttonIndex int, _ string) {
|
func(buttonIndex int, _ string) {
|
||||||
if buttonIndex == 0 {
|
if buttonIndex == 0 {
|
||||||
ui.commandC <- domain.CommandRemoveDestination{URL: url}
|
ui.commandC <- CommandRemoveDestination{URL: url}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1009,12 +1009,12 @@ func (ui *UI) toggleDestination() {
|
|||||||
switch ss {
|
switch ss {
|
||||||
case startStateNotStarted:
|
case startStateNotStarted:
|
||||||
ui.urlsToStartState[url] = startStateStarting
|
ui.urlsToStartState[url] = startStateStarting
|
||||||
ui.commandC <- domain.CommandStartDestination{URL: url}
|
ui.commandC <- CommandStartDestination{URL: url}
|
||||||
case startStateStarting:
|
case startStateStarting:
|
||||||
// do nothing
|
// do nothing
|
||||||
return
|
return
|
||||||
case startStateStarted:
|
case startStateStarted:
|
||||||
ui.commandC <- domain.CommandStopDestination{URL: url}
|
ui.commandC <- CommandStopDestination{URL: url}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1067,7 +1067,7 @@ func (ui *UI) confirmQuit() {
|
|||||||
false,
|
false,
|
||||||
func(buttonIndex int, _ string) {
|
func(buttonIndex int, _ string) {
|
||||||
if buttonIndex == 0 {
|
if buttonIndex == 0 {
|
||||||
ui.commandC <- domain.CommandQuit{}
|
ui.commandC <- CommandQuit{}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user