diff --git a/controllers/admin/config.go b/controllers/admin/config.go index be0192d20..76b452fe2 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -771,6 +771,28 @@ func SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) { controllers.WriteSimpleResponse(w, true, "search indexing support updated") } +// SetVideoServingEndpoint will save the video serving endpoint. +func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) { + endpoint, success := getValueFromRequest(w, r) + if !success { + controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") + return + } + + value, ok := endpoint.Value.(string) + if !ok { + controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") + return + } + + if err := data.SetVideoServingEndpoint(value); err != nil { + controllers.WriteSimpleResponse(w, false, err.Error()) + return + } + + controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated") +} + func requirePOST(w http.ResponseWriter, r *http.Request) bool { if r.Method != controllers.POST { controllers.WriteSimpleResponse(w, false, r.Method+" not supported") diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index d069a5479..0a5269c63 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -59,6 +59,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { ChatDisabled: data.GetChatDisabled(), ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(), SocketHostOverride: data.GetWebsocketOverrideHost(), + VideoServingEndpoint: data.GetVideoServingEndpoint(), ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), HideViewerCount: data.GetHideViewerCount(), DisableSearchIndexing: data.GetDisableSearchIndexing(), @@ -107,6 +108,7 @@ type serverConfigAdminResponse struct { SocketHostOverride string `json:"socketHostOverride,omitempty"` WebServerIP string `json:"webServerIP"` VideoCodec string `json:"videoCodec"` + VideoServingEndpoint string `json:"videoServingEndpoint"` S3 models.S3 `json:"s3"` Federation federationConfigResponse `json:"federation"` SupportedCodecs []string `json:"supportedCodecs"` @@ -120,9 +122,9 @@ type serverConfigAdminResponse struct { ChatDisabled bool `json:"chatDisabled"` ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` + DisableSearchIndexing bool `json:"disableSearchIndexing"` StreamKeyOverridden bool `json:"streamKeyOverridden"` HideViewerCount bool `json:"hideViewerCount"` - DisableSearchIndexing bool `json:"disableSearchIndexing"` } type videoSettings struct { diff --git a/core/data/config.go b/core/data/config.go index c7dd3c53b..d76c9e718 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -70,6 +70,7 @@ const ( customColorVariableValuesKey = "custom_color_variable_values" streamKeysKey = "stream_keys" disableSearchIndexingKey = "disable_search_indexing" + videoServingEndpointKey = "video_serving_endpoint" ) // GetExtraPageBodyContent will return the user-supplied body content. @@ -974,3 +975,14 @@ func GetDisableSearchIndexing() bool { } return disableSearchIndexing } + +// GetVideoServingEndpoint returns the custom video endpont. +func GetVideoServingEndpoint() string { + message, _ := _datastore.GetString(videoServingEndpointKey) + return message +} + +// SetVideoServingEndpoint sets the custom video endpoint. +func SetVideoServingEndpoint(message string) error { + return _datastore.SetString(videoServingEndpointKey, message) +} diff --git a/core/data/datastoreMigrations.go b/core/data/datastoreMigrations.go index b63df7a61..af7a22326 100644 --- a/core/data/datastoreMigrations.go +++ b/core/data/datastoreMigrations.go @@ -8,7 +8,7 @@ import ( ) const ( - datastoreValuesVersion = 2 + datastoreValuesVersion = 3 datastoreValueVersionKey = "DATA_STORE_VERSION" ) @@ -25,6 +25,8 @@ func migrateDatastoreValues(datastore *Datastore) { migrateToDatastoreValues1(datastore) case 1: migrateToDatastoreValues2(datastore) + case 2: + migrateToDatastoreValues3ServingEndpoint3(datastore) default: log.Fatalln("missing datastore values migration step") } @@ -61,3 +63,13 @@ func migrateToDatastoreValues2(datastore *Datastore) { {Key: oldAdminPassword, Comment: "Default stream key"}, }) } + +func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) { + s3Config := GetS3Config() + + if !s3Config.Enabled { + return + } + + _ = SetVideoServingEndpoint(s3Config.ServingEndpoint) +} diff --git a/core/storageproviders/local.go b/core/storageproviders/local.go index 05203cfea..11d5c3b31 100644 --- a/core/storageproviders/local.go +++ b/core/storageproviders/local.go @@ -6,13 +6,15 @@ import ( log "github.com/sirupsen/logrus" "github.com/owncast/owncast/config" + "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/transcoder" ) // LocalStorage represents an instance of the local storage provider for HLS video. type LocalStorage struct { // Cleanup old public HLS content every N min from the webroot. - onlineCleanupTicker *time.Ticker + onlineCleanupTicker *time.Ticker + customVideoServingEndpoint string } // NewLocalStorage returns a new LocalStorage instance. @@ -22,6 +24,10 @@ func NewLocalStorage() *LocalStorage { // Setup configures this storage provider. func (s *LocalStorage) Setup() error { + if data.GetVideoServingEndpoint() != "" { + s.customVideoServingEndpoint = data.GetVideoServingEndpoint() + } + // NOTE: This cleanup timer will have to be disabled to support recordings in the future // as all HLS segments have to be publicly available on disk to keep a recording of them. s.onlineCleanupTicker = time.NewTicker(1 * time.Minute) @@ -50,7 +56,12 @@ func (s *LocalStorage) VariantPlaylistWritten(localFilePath string) { // MasterPlaylistWritten is called when the master hls playlist is written. func (s *LocalStorage) MasterPlaylistWritten(localFilePath string) { - if _, err := s.Save(localFilePath, 0); err != nil { + if s.customVideoServingEndpoint != "" { + // Rewrite the playlist to use custom absolute remote URLs + if err := rewriteRemotePlaylist(localFilePath, s.customVideoServingEndpoint); err != nil { + log.Warnln(err) + } + } else if _, err := s.Save(localFilePath, 0); err != nil { log.Warnln(err) } } diff --git a/core/storageproviders/rewriteLocalPlaylist.go b/core/storageproviders/rewriteLocalPlaylist.go new file mode 100644 index 000000000..db346d21c --- /dev/null +++ b/core/storageproviders/rewriteLocalPlaylist.go @@ -0,0 +1,36 @@ +package storageproviders + +import ( + "bufio" + "os" + "path/filepath" + + "github.com/grafov/m3u8" + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/core/playlist" + + log "github.com/sirupsen/logrus" +) + +// rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations. +func rewriteRemotePlaylist(localFilePath, remoteServingEndpoint string) error { + f, err := os.Open(localFilePath) // nolint + if err != nil { + log.Fatalln(err) + } + + p := m3u8.NewMasterPlaylist() + if err := p.DecodeFrom(bufio.NewReader(f), false); err != nil { + log.Warnln(err) + } + + for _, item := range p.Variants { + item.URI = remoteServingEndpoint + filepath.Join("/hls", item.URI) + } + + publicPath := filepath.Join(config.HLSStoragePath, filepath.Base(localFilePath)) + + newPlaylist := p.String() + + return playlist.WritePlaylist(newPlaylist, publicPath) +} diff --git a/core/storageproviders/s3Storage.go b/core/storageproviders/s3Storage.go index 5c99ea7e8..4b6b26f28 100644 --- a/core/storageproviders/s3Storage.go +++ b/core/storageproviders/s3Storage.go @@ -1,7 +1,6 @@ package storageproviders import ( - "bufio" "fmt" "net/http" "os" @@ -11,7 +10,6 @@ import ( "time" "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/core/playlist" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" @@ -21,8 +19,6 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/owncast/owncast/config" - - "github.com/grafov/m3u8" ) // S3Storage is the s3 implementation of a storage provider. @@ -58,8 +54,9 @@ func (s *S3Storage) Setup() error { log.Trace("Setting up S3 for external storage of video...") s3Config := data.GetS3Config() - if s3Config.ServingEndpoint != "" { - s.host = s3Config.ServingEndpoint + customVideoServingEndpoint := data.GetVideoServingEndpoint() + if customVideoServingEndpoint != "" { + s.host = customVideoServingEndpoint } else { s.host = fmt.Sprintf("%s/%s", s3Config.Endpoint, s3Config.Bucket) } @@ -130,7 +127,7 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) { // MasterPlaylistWritten is called when the master hls playlist is written. func (s *S3Storage) MasterPlaylistWritten(localFilePath string) { // Rewrite the playlist to use absolute remote S3 URLs - if err := s.rewriteRemotePlaylist(localFilePath); err != nil { + if err := rewriteRemotePlaylist(localFilePath, s.host); err != nil { log.Warnln(err) } } @@ -216,26 +213,3 @@ func (s *S3Storage) connectAWS() *session.Session { } return sess } - -// rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations. -func (s *S3Storage) rewriteRemotePlaylist(filePath string) error { - f, err := os.Open(filePath) // nolint - if err != nil { - log.Fatalln(err) - } - - p := m3u8.NewMasterPlaylist() - if err := p.DecodeFrom(bufio.NewReader(f), false); err != nil { - log.Warnln(err) - } - - for _, item := range p.Variants { - item.URI = s.host + filepath.Join("/hls", item.URI) - } - - publicPath := filepath.Join(config.HLSStoragePath, filepath.Base(filePath)) - - newPlaylist := p.String() - - return playlist.WritePlaylist(newPlaylist, publicPath) -} diff --git a/models/s3Storage.go b/models/s3Storage.go index 26d01a38c..5eca8c063 100644 --- a/models/s3Storage.go +++ b/models/s3Storage.go @@ -2,13 +2,17 @@ package models // S3 is the storage configuration. type S3 struct { - Endpoint string `json:"endpoint,omitempty"` - ServingEndpoint string `json:"servingEndpoint,omitempty"` - AccessKey string `json:"accessKey,omitempty"` - Secret string `json:"secret,omitempty"` - Bucket string `json:"bucket,omitempty"` - Region string `json:"region,omitempty"` - ACL string `json:"acl,omitempty"` - Enabled bool `json:"enabled"` - ForcePathStyle bool `json:"forcePathStyle"` + Enabled bool `json:"enabled"` + Endpoint string `json:"endpoint,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + Secret string `json:"secret,omitempty"` + Bucket string `json:"bucket,omitempty"` + Region string `json:"region,omitempty"` + ACL string `json:"acl,omitempty"` + ForcePathStyle bool `json:"forcePathStyle"` + + // This property is no longer used as of v0.1.1. See the standalone + // property that was pulled out of here instead. It's only left here + // to allow the migration to take place without data loss. + ServingEndpoint string `json:"-"` } diff --git a/router/router.go b/router/router.go index fd9583172..f8f181093 100644 --- a/router/router.go +++ b/router/router.go @@ -291,6 +291,9 @@ func Start() error { // Websocket host override http.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride)) + // Custom video serving endpoint + http.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)) + // Is server marked as NSFW http.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW)) diff --git a/web/components/admin/EditInstanceDetails2.tsx b/web/components/admin/EditInstanceDetails2.tsx index 89a0a4c0b..4e0eb0683 100644 --- a/web/components/admin/EditInstanceDetails2.tsx +++ b/web/components/admin/EditInstanceDetails2.tsx @@ -10,6 +10,7 @@ import { TEXTFIELD_PROPS_SOCKET_HOST_OVERRIDE, TEXTFIELD_PROPS_ADMIN_PASSWORD, TEXTFIELD_PROPS_WEB_PORT, + TEXTFIELD_PROPS_VIDEO_SERVING_ENDPOINT, } from '../../utils/config-constants'; import { UpdateArgs } from '../../types/config-section'; import { ResetYP } from './ResetYP'; @@ -24,8 +25,15 @@ export default function EditInstanceDetails() { const { serverConfig } = serverStatusData || {}; - const { adminPassword, ffmpegPath, rtmpServerPort, webServerPort, yp, socketHostOverride } = - serverConfig; + const { + adminPassword, + ffmpegPath, + rtmpServerPort, + webServerPort, + yp, + socketHostOverride, + videoServingEndpoint, + } = serverConfig; useEffect(() => { setFormDataValues({ @@ -34,6 +42,7 @@ export default function EditInstanceDetails() { rtmpServerPort, webServerPort, socketHostOverride, + videoServingEndpoint, }); }, [serverConfig]); @@ -119,6 +128,15 @@ export default function EditInstanceDetails() { type={TEXTFIELD_TYPE_URL} onChange={handleFieldChange} /> + + {yp.enabled && } diff --git a/web/components/admin/config/server/EditStorage.tsx b/web/components/admin/config/server/EditStorage.tsx index ea2e807b2..fc5767728 100644 --- a/web/components/admin/config/server/EditStorage.tsx +++ b/web/components/admin/config/server/EditStorage.tsx @@ -28,17 +28,7 @@ const { Panel } = Collapse; // we could probably add more detailed checks here // `currentValues` is what's currently in the global store and in the db function checkSaveable(formValues: any, currentValues: any) { - const { - endpoint, - accessKey, - secret, - bucket, - region, - enabled, - servingEndpoint, - acl, - forcePathStyle, - } = formValues; + const { endpoint, accessKey, secret, bucket, region, enabled, acl, forcePathStyle } = formValues; // if fields are filled out and different from what's in store, then return true if (enabled) { if (!!endpoint && isValidUrl(endpoint) && !!accessKey && !!secret && !!bucket && !!region) { @@ -49,8 +39,6 @@ function checkSaveable(formValues: any, currentValues: any) { secret !== currentValues.secret || bucket !== currentValues.bucket || region !== currentValues.region || - (!currentValues.servingEndpoint && servingEndpoint !== '') || - (!!currentValues.servingEndpoint && servingEndpoint !== currentValues.servingEndpoint) || (!currentValues.acl && acl !== '') || (!!currentValues.acl && acl !== currentValues.acl) || forcePathStyle !== currentValues.forcePathStyle @@ -84,7 +72,6 @@ export default function EditStorage() { endpoint = '', region = '', secret = '', - servingEndpoint = '', forcePathStyle = false, } = s3; @@ -97,7 +84,6 @@ export default function EditStorage() { endpoint, region, secret, - servingEndpoint, forcePathStyle, }); setShouldDisplayForm(enabled); @@ -232,13 +218,7 @@ export default function EditStorage() { onChange={handleFieldChange} /> -
- -
+