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}
/>
+
+