Merge remote-tracking branch 'upstream/master' into save-volume-settings
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
webroot/js/web_modules/* linguist-vendored
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,9 +19,13 @@ vendor/
|
|||||||
/stats.json
|
/stats.json
|
||||||
owncast
|
owncast
|
||||||
webroot/thumbnail.jpg
|
webroot/thumbnail.jpg
|
||||||
|
webroot/preview.gif
|
||||||
webroot/hls
|
webroot/hls
|
||||||
webroot/static/content.md
|
webroot/static/content.md
|
||||||
hls/
|
hls/
|
||||||
dist/
|
dist/
|
||||||
transcoder.log
|
transcoder.log
|
||||||
chat.db
|
chat.db
|
||||||
|
.yp.key
|
||||||
|
|
||||||
|
!webroot/js/web_modules/**/dist
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "doc"]
|
|
||||||
path = doc
|
|
||||||
url = https://github.com/owncast/owncast.github.io/
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<a href="https://goth.land/">View Demo</a>
|
<a href="https://goth.land/">View Demo</a>
|
||||||
·
|
·
|
||||||
<a href="https://owncast.online/docs/faq/">FAQ</a>
|
<a href="https://owncast.online/docs/faq/">FAQ</a>
|
||||||
.
|
·
|
||||||
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
|
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
10
build/javascript/README.md
Normal file
10
build/javascript/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## Third party web dependencies
|
||||||
|
|
||||||
|
Owncast's web frontend utilizes a few third party Javascript and CSS dependencies that we ship with the application.
|
||||||
|
|
||||||
|
To add, remove, or update one of these components:
|
||||||
|
|
||||||
|
1. Perform your `npm install/uninstall/etc`, or edit the `package.json` file to reflect the change you want to make.
|
||||||
|
2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`.
|
||||||
|
3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory.
|
||||||
|
4. Your new web dependency is now available for use in your web code.
|
||||||
3225
build/javascript/package-lock.json
generated
Normal file
3225
build/javascript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
build/javascript/package.json
Normal file
40
build/javascript/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "owncast-dependencies",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@joeattardi/emoji-button": "^4.2.0",
|
||||||
|
"@justinribeiro/lite-youtube": "^0.9.0",
|
||||||
|
"@videojs/http-streaming": "^2.2.0",
|
||||||
|
"@videojs/themes": "^1.0.0",
|
||||||
|
"htm": "^3.0.4",
|
||||||
|
"preact": "^10.5.3",
|
||||||
|
"showdown": "^1.9.1",
|
||||||
|
"tailwindcss": "^1.8.10",
|
||||||
|
"video.js": "^7.9.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"snowpack": "^2.12.1"
|
||||||
|
},
|
||||||
|
"snowpack": {
|
||||||
|
"install": [
|
||||||
|
"video.js/dist/video.min.js",
|
||||||
|
"@videojs/themes/fantasy/*",
|
||||||
|
"@videojs/http-streaming/dist/videojs-http-streaming.min.js",
|
||||||
|
"video.js/dist/video-js.min.css",
|
||||||
|
"@joeattardi/emoji-button",
|
||||||
|
"@justinribeiro/lite-youtube",
|
||||||
|
"htm",
|
||||||
|
"preact",
|
||||||
|
"showdown",
|
||||||
|
"tailwindcss/dist/tailwind.min.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "npm install && npx snowpack install && cp -R web_modules ../../webroot/js"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
@@ -25,3 +25,9 @@ instanceDetails:
|
|||||||
videoSettings:
|
videoSettings:
|
||||||
# Change this value and keep it secure. Treat it like a password to your live stream.
|
# Change this value and keep it secure. Treat it like a password to your live stream.
|
||||||
streamingKey: abc123
|
streamingKey: abc123
|
||||||
|
|
||||||
|
yp:
|
||||||
|
# Enable YP to be listed in the Owncast directory and let people discover your instance.
|
||||||
|
enabled: false
|
||||||
|
# You must specify the public URL of your host that you want the directory to link to.
|
||||||
|
instanceURL: http://yourhost.com
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
@@ -15,17 +15,15 @@ var _default config
|
|||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
ChatDatabaseFilePath string `yaml:"chatDatabaseFile"`
|
ChatDatabaseFilePath string `yaml:"chatDatabaseFile"`
|
||||||
DisableWebFeatures bool `yaml:"disableWebFeatures"`
|
|
||||||
EnableDebugFeatures bool `yaml:"-"`
|
EnableDebugFeatures bool `yaml:"-"`
|
||||||
FFMpegPath string `yaml:"ffmpegPath"`
|
FFMpegPath string `yaml:"ffmpegPath"`
|
||||||
Files files `yaml:"files"`
|
Files files `yaml:"files"`
|
||||||
InstanceDetails InstanceDetails `yaml:"instanceDetails"`
|
InstanceDetails InstanceDetails `yaml:"instanceDetails"`
|
||||||
PrivateHLSPath string `yaml:"privateHLSPath"`
|
S3 S3 `yaml:"s3"`
|
||||||
PublicHLSPath string `yaml:"publicHLSPath"`
|
VersionInfo string `yaml:"-"` // For storing the version/build number
|
||||||
S3 s3 `yaml:"s3"`
|
|
||||||
VersionInfo string `yaml:"-"`
|
|
||||||
VideoSettings videoSettings `yaml:"videoSettings"`
|
VideoSettings videoSettings `yaml:"videoSettings"`
|
||||||
WebServerPort int `yaml:"webServerPort"`
|
WebServerPort int `yaml:"webServerPort"`
|
||||||
|
YP yp `yaml:"yp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstanceDetails defines the user-visible information about this particular instance.
|
// InstanceDetails defines the user-visible information about this particular instance.
|
||||||
@@ -33,11 +31,17 @@ type InstanceDetails struct {
|
|||||||
Name string `yaml:"name" json:"name"`
|
Name string `yaml:"name" json:"name"`
|
||||||
Title string `yaml:"title" json:"title"`
|
Title string `yaml:"title" json:"title"`
|
||||||
Summary string `yaml:"summary" json:"summary"`
|
Summary string `yaml:"summary" json:"summary"`
|
||||||
Logo map[string]string `yaml:"logo" json:"logo"`
|
Logo logo `yaml:"logo" json:"logo"`
|
||||||
Tags []string `yaml:"tags" json:"tags"`
|
Tags []string `yaml:"tags" json:"tags"`
|
||||||
SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"`
|
SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"`
|
||||||
ExtraInfoFile string `yaml:"extraUserInfoFileName" json:"extraUserInfoFileName"`
|
ExtraInfoFile string `yaml:"extraUserInfoFileName" json:"extraUserInfoFileName"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
NSFW bool `yaml:"nsfw" json:"nsfw"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type logo struct {
|
||||||
|
Large string `yaml:"large" json:"large"`
|
||||||
|
Small string `yaml:"small" json:"small"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type socialHandle struct {
|
type socialHandle struct {
|
||||||
@@ -50,7 +54,14 @@ type videoSettings struct {
|
|||||||
StreamingKey string `yaml:"streamingKey"`
|
StreamingKey string `yaml:"streamingKey"`
|
||||||
StreamQualities []StreamQuality `yaml:"streamQualities"`
|
StreamQualities []StreamQuality `yaml:"streamQualities"`
|
||||||
OfflineContent string `yaml:"offlineContent"`
|
OfflineContent string `yaml:"offlineContent"`
|
||||||
HighestQualityStreamIndex int `yaml"-"`
|
HighestQualityStreamIndex int `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registration to the central Owncast YP (Yellow pages) service operating as a directory.
|
||||||
|
type yp struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
InstanceURL string `yaml:"instanceURL"` // The public URL the directory should link to
|
||||||
|
YPServiceURL string `yaml:"ypServiceURL"` // The base URL to the YP API to register with (optional)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamQuality defines the specifics of a single HLS stream variant.
|
// StreamQuality defines the specifics of a single HLS stream variant.
|
||||||
@@ -58,35 +69,35 @@ type StreamQuality struct {
|
|||||||
// Enable passthrough to copy the video and/or audio directly from the
|
// Enable passthrough to copy the video and/or audio directly from the
|
||||||
// incoming stream and disable any transcoding. It will ignore any of
|
// incoming stream and disable any transcoding. It will ignore any of
|
||||||
// the below settings.
|
// the below settings.
|
||||||
IsVideoPassthrough bool `yaml:"videoPassthrough"`
|
IsVideoPassthrough bool `yaml:"videoPassthrough" json:"videoPassthrough"`
|
||||||
IsAudioPassthrough bool `yaml:"audioPassthrough"`
|
IsAudioPassthrough bool `yaml:"audioPassthrough" json:"audioPassthrough"`
|
||||||
|
|
||||||
VideoBitrate int `yaml:"videoBitrate"`
|
VideoBitrate int `yaml:"videoBitrate" json:"videoBitrate"`
|
||||||
AudioBitrate int `yaml:"audioBitrate"`
|
AudioBitrate int `yaml:"audioBitrate" json:"audioBitrate"`
|
||||||
|
|
||||||
// Set only one of these in order to keep your current aspect ratio.
|
// Set only one of these in order to keep your current aspect ratio.
|
||||||
// Or set neither to not scale the video.
|
// Or set neither to not scale the video.
|
||||||
ScaledWidth int `yaml:"scaledWidth"`
|
ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"`
|
||||||
ScaledHeight int `yaml:"scaledHeight"`
|
ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"`
|
||||||
|
|
||||||
Framerate int `yaml:"framerate"`
|
Framerate int `yaml:"framerate" json:"framerate"`
|
||||||
EncoderPreset string `yaml:"encoderPreset"`
|
EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type files struct {
|
type files struct {
|
||||||
MaxNumberInPlaylist int `yaml:"maxNumberInPlaylist"`
|
MaxNumberInPlaylist int `yaml:"maxNumberInPlaylist"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//s3 is for configuring the s3 integration
|
//S3 is for configuring the S3 integration
|
||||||
type s3 struct {
|
type S3 struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||||
Endpoint string `yaml:"endpoint"`
|
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"`
|
||||||
ServingEndpoint string `yaml:"servingEndpoint"`
|
ServingEndpoint string `yaml:"servingEndpoint" json:"servingEndpoint,omitempty"`
|
||||||
AccessKey string `yaml:"accessKey"`
|
AccessKey string `yaml:"accessKey" json:"accessKey,omitempty"`
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret" json:"secret,omitempty"`
|
||||||
Bucket string `yaml:"bucket"`
|
Bucket string `yaml:"bucket" json:"bucket,omitempty"`
|
||||||
Region string `yaml:"region"`
|
Region string `yaml:"region" json:"region,omitempty"`
|
||||||
ACL string `yaml:"acl"`
|
ACL string `yaml:"acl" json:"acl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *config) load(filePath string) error {
|
func (c *config) load(filePath string) error {
|
||||||
@@ -129,6 +140,10 @@ func (c *config) verifySettings() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.YP.Enabled && c.YP.InstanceURL == "" {
|
||||||
|
return errors.New("YP is enabled but instance url is not set")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,22 +155,6 @@ func (c *config) GetVideoSegmentSecondsLength() int {
|
|||||||
return _default.GetVideoSegmentSecondsLength()
|
return _default.GetVideoSegmentSecondsLength()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *config) GetPublicHLSSavePath() string {
|
|
||||||
if c.PublicHLSPath != "" {
|
|
||||||
return c.PublicHLSPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return _default.PublicHLSPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *config) GetPrivateHLSSavePath() string {
|
|
||||||
if c.PrivateHLSPath != "" {
|
|
||||||
return c.PrivateHLSPath
|
|
||||||
}
|
|
||||||
|
|
||||||
return _default.PrivateHLSPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *config) GetPublicWebServerPort() int {
|
func (c *config) GetPublicWebServerPort() int {
|
||||||
if c.WebServerPort != 0 {
|
if c.WebServerPort != 0 {
|
||||||
return c.WebServerPort
|
return c.WebServerPort
|
||||||
@@ -189,6 +188,14 @@ func (c *config) GetFFMpegPath() string {
|
|||||||
return _default.FFMpegPath
|
return _default.FFMpegPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *config) GetYPServiceHost() string {
|
||||||
|
if c.YP.YPServiceURL != "" {
|
||||||
|
return c.YP.YPServiceURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return _default.YP.YPServiceURL
|
||||||
|
}
|
||||||
|
|
||||||
func (c *config) GetVideoStreamQualities() []StreamQuality {
|
func (c *config) GetVideoStreamQualities() []StreamQuality {
|
||||||
if len(c.VideoSettings.StreamQualities) > 0 {
|
if len(c.VideoSettings.StreamQualities) > 0 {
|
||||||
return c.VideoSettings.StreamQualities
|
return c.VideoSettings.StreamQualities
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "sort"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
func findHighestQuality(qualities []StreamQuality) int {
|
func findHighestQuality(qualities []StreamQuality) int {
|
||||||
type IndexedQuality struct {
|
type IndexedQuality struct {
|
||||||
@@ -32,3 +35,15 @@ func findHighestQuality(qualities []StreamQuality) int {
|
|||||||
|
|
||||||
return indexedQualities[0].index
|
return indexedQualities[0].index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is a custom JSON marshal function for video stream qualities
|
||||||
|
func (q *StreamQuality) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias StreamQuality
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Framerate int `json:"framerate"`
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Framerate: q.GetFramerate(),
|
||||||
|
Alias: (*Alias)(q),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
12
config/constants.go
Normal file
12
config/constants.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebRoot = "webroot"
|
||||||
|
PrivateHLSStoragePath = "hls"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
PublicHLSStoragePath = filepath.Join(WebRoot, "hls")
|
||||||
|
)
|
||||||
@@ -12,10 +12,10 @@ func getDefaults() config {
|
|||||||
defaults.FFMpegPath = getDefaultFFMpegPath()
|
defaults.FFMpegPath = getDefaultFFMpegPath()
|
||||||
defaults.VideoSettings.ChunkLengthInSeconds = 4
|
defaults.VideoSettings.ChunkLengthInSeconds = 4
|
||||||
defaults.Files.MaxNumberInPlaylist = 5
|
defaults.Files.MaxNumberInPlaylist = 5
|
||||||
defaults.PublicHLSPath = "webroot/hls"
|
|
||||||
defaults.PrivateHLSPath = "hls"
|
|
||||||
defaults.VideoSettings.OfflineContent = "static/offline.m4v"
|
defaults.VideoSettings.OfflineContent = "static/offline.m4v"
|
||||||
defaults.InstanceDetails.ExtraInfoFile = "/static/content.md"
|
defaults.InstanceDetails.ExtraInfoFile = "/static/content.md"
|
||||||
|
defaults.YP.Enabled = false
|
||||||
|
defaults.YP.YPServiceURL = "https://yp.owncast.online"
|
||||||
|
|
||||||
defaultQuality := StreamQuality{
|
defaultQuality := StreamQuality{
|
||||||
IsAudioPassthrough: true,
|
IsAudioPassthrough: true,
|
||||||
|
|||||||
35
controllers/admin/changeStreamKey.go
Normal file
35
controllers/admin/changeStreamKey.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/controllers"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChangeStreamKey will change the stream key (in memory)
|
||||||
|
func ChangeStreamKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
var request changeStreamKeyRequest
|
||||||
|
err := decoder.Decode(&request)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
controllers.WriteSimpleResponse(w, false, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Config.VideoSettings.StreamingKey = request.Key
|
||||||
|
controllers.WriteSimpleResponse(w, true, "changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
type changeStreamKeyRequest struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
21
controllers/admin/disconnect.go
Normal file
21
controllers/admin/disconnect.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/controllers"
|
||||||
|
"github.com/owncast/owncast/core"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/core/rtmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DisconnectInboundConnection will force-disconnect an inbound stream
|
||||||
|
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !core.GetStatus().Online {
|
||||||
|
controllers.WriteSimpleResponse(w, false, "no inbound stream connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rtmp.Disconnect()
|
||||||
|
controllers.WriteSimpleResponse(w, true, "inbound stream disconnected")
|
||||||
|
}
|
||||||
16
controllers/admin/hardware.go
Normal file
16
controllers/admin/hardware.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHardwareStats will return hardware utilization over time
|
||||||
|
func GetHardwareStats(w http.ResponseWriter, r *http.Request) {
|
||||||
|
metrics := metrics.Metrics
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(metrics)
|
||||||
|
}
|
||||||
35
controllers/admin/inboundBroadcasterDetails.go
Normal file
35
controllers/admin/inboundBroadcasterDetails.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/controllers"
|
||||||
|
"github.com/owncast/owncast/core"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInboundBroadasterDetails gets the details of the inbound broadcaster
|
||||||
|
func GetInboundBroadasterDetails(w http.ResponseWriter, r *http.Request) {
|
||||||
|
broadcaster := core.GetBroadcaster()
|
||||||
|
if broadcaster == nil {
|
||||||
|
controllers.WriteSimpleResponse(w, false, "no broadcaster connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := inboundBroadasterDetailsResponse{
|
||||||
|
models.BaseAPIResponse{
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
broadcaster,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type inboundBroadasterDetailsResponse struct {
|
||||||
|
models.BaseAPIResponse
|
||||||
|
Broadcaster *models.Broadcaster `json:"broadcaster"`
|
||||||
|
}
|
||||||
40
controllers/admin/serverConfig.go
Normal file
40
controllers/admin/serverConfig.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetServerConfig gets the config details of the server
|
||||||
|
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := serverConfigAdminResponse{
|
||||||
|
InstanceDetails: config.Config.InstanceDetails,
|
||||||
|
FFmpegPath: config.Config.GetFFMpegPath(),
|
||||||
|
WebServerPort: config.Config.GetPublicWebServerPort(),
|
||||||
|
VideoSettings: videoSettings{
|
||||||
|
VideoQualityVariants: config.Config.GetVideoStreamQualities(),
|
||||||
|
SegmentLengthSeconds: config.Config.GetVideoSegmentSecondsLength(),
|
||||||
|
NumberOfPlaylistItems: config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist(),
|
||||||
|
},
|
||||||
|
S3: config.Config.S3,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConfigAdminResponse struct {
|
||||||
|
InstanceDetails config.InstanceDetails `json:"instanceDetails"`
|
||||||
|
FFmpegPath string `json:"ffmpegPath"`
|
||||||
|
WebServerPort int `json:"webServerPort"`
|
||||||
|
S3 config.S3 `json:"s3"`
|
||||||
|
VideoSettings videoSettings `json:"videoSettings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type videoSettings struct {
|
||||||
|
VideoQualityVariants []config.StreamQuality `json:"videoQualityVariants"`
|
||||||
|
SegmentLengthSeconds int `json:"segmentLengthSeconds"`
|
||||||
|
NumberOfPlaylistItems int `json:"numberOfPlaylistItems"`
|
||||||
|
}
|
||||||
15
controllers/admin/viewers.go
Normal file
15
controllers/admin/viewers.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetViewersOverTime will return the number of viewers at points in time
|
||||||
|
func GetViewersOverTime(w http.ResponseWriter, r *http.Request) {
|
||||||
|
viewersOverTime := metrics.Metrics.Viewers
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(viewersOverTime)
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gabek/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/router/middleware"
|
"github.com/owncast/owncast/router/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//GetChatMessages gets all of the chat messages
|
//GetChatMessages gets all of the chat messages
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/router/middleware"
|
"github.com/owncast/owncast/router/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//GetWebConfig gets the status of the server
|
//GetWebConfig gets the status of the server
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package controllers
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type j map[string]interface{}
|
type j map[string]interface{}
|
||||||
@@ -24,3 +26,12 @@ func badRequestHandler(w http.ResponseWriter, err error) {
|
|||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
json.NewEncoder(w).Encode(j{"error": err.Error()})
|
json.NewEncoder(w).Encode(j{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteSimpleResponse(w http.ResponseWriter, success bool, message string) {
|
||||||
|
response := models.BaseAPIResponse{
|
||||||
|
Success: success,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ const emojiPath = "/img/emoji" // Relative to webroot
|
|||||||
func GetCustomEmoji(w http.ResponseWriter, r *http.Request) {
|
func GetCustomEmoji(w http.ResponseWriter, r *http.Request) {
|
||||||
emojiList := make([]models.CustomEmoji, 0)
|
emojiList := make([]models.CustomEmoji, 0)
|
||||||
|
|
||||||
fullPath := filepath.Join("webroot", emojiPath)
|
fullPath := filepath.Join(config.WebRoot, emojiPath)
|
||||||
files, err := ioutil.ReadDir(fullPath)
|
files, err := ioutil.ReadDir(fullPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/gabek/owncast/router/middleware"
|
"github.com/owncast/owncast/router/middleware"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetadataPage struct {
|
type MetadataPage struct {
|
||||||
@@ -30,13 +31,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
isIndexRequest := r.URL.Path == "/" || r.URL.Path == "/index.html"
|
isIndexRequest := r.URL.Path == "/" || r.URL.Path == "/index.html"
|
||||||
|
|
||||||
// Reject requests for the web UI if it's disabled.
|
|
||||||
if isIndexRequest && config.Config.DisableWebFeatures {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
w.Write([]byte("404 - y u ask 4 this? If this is an error let us know: https://github.com/owncast/owncast/issues"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For search engine bots and social scrapers return a special
|
// For search engine bots and social scrapers return a special
|
||||||
// server-rendered page.
|
// server-rendered page.
|
||||||
if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest {
|
if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest {
|
||||||
@@ -60,7 +54,7 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Set a cache control max-age header
|
// Set a cache control max-age header
|
||||||
middleware.SetCachingHeaders(w, r)
|
middleware.SetCachingHeaders(w, r)
|
||||||
|
|
||||||
http.ServeFile(w, r, path.Join("webroot", r.URL.Path))
|
http.ServeFile(w, r, path.Join(config.WebRoot, r.URL.Path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a basic HTML page with server-rendered metadata from the config file
|
// Return a basic HTML page with server-rendered metadata from the config file
|
||||||
@@ -69,13 +63,13 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
tmpl := template.Must(template.ParseFiles(path.Join("static", "metadata.html")))
|
tmpl := template.Must(template.ParseFiles(path.Join("static", "metadata.html")))
|
||||||
|
|
||||||
fullURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, r.URL.Path))
|
fullURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, r.URL.Path))
|
||||||
imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, config.Config.InstanceDetails.Logo["large"]))
|
imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, config.Config.InstanceDetails.Logo.Large))
|
||||||
|
|
||||||
status := core.GetStatus()
|
status := core.GetStatus()
|
||||||
|
|
||||||
// If the thumbnail does not exist or we're offline then just use the logo image
|
// If the thumbnail does not exist or we're offline then just use the logo image
|
||||||
var thumbnailURL string
|
var thumbnailURL string
|
||||||
if status.Online && utils.DoesFileExists("webroot/thumbnail.jpg") {
|
if status.Online && utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) {
|
||||||
thumbnail, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, "/thumbnail.jpg"))
|
thumbnail, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, "/thumbnail.jpg"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gabek/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/gabek/owncast/router/middleware"
|
"github.com/owncast/owncast/router/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//GetStatus gets the status of the server
|
//GetStatus gets the status of the server
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Setup sets up the chat server
|
//Setup sets up the chat server
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
|
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
|
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -133,7 +133,7 @@ func (s *server) sendWelcomeMessageToClient(c *Client) {
|
|||||||
time.Sleep(7 * time.Second)
|
time.Sleep(7 * time.Second)
|
||||||
|
|
||||||
initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary)
|
initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary)
|
||||||
initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, config.Config.InstanceDetails.Logo["small"], "initial-message-1", "CHAT", true, time.Now()}
|
initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, config.Config.InstanceDetails.Logo.Small, "initial-message-1", "CHAT", true, time.Now()}
|
||||||
c.Write(initialMessage)
|
c.Write(initialMessage)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package core
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/gabek/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
//ChatListenerImpl the implementation of the chat client
|
//ChatListenerImpl the implementation of the chat client
|
||||||
|
|||||||
42
core/core.go
42
core/core.go
@@ -3,22 +3,26 @@ package core
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/gabek/owncast/core/ffmpeg"
|
"github.com/owncast/owncast/core/ffmpeg"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/owncast/owncast/yp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_stats *models.Stats
|
_stats *models.Stats
|
||||||
_storage models.ChunkStorageProvider
|
_storage models.ChunkStorageProvider
|
||||||
_cleanupTimer *time.Timer
|
_cleanupTimer *time.Timer
|
||||||
|
_yp *yp.YP
|
||||||
|
_broadcaster *models.Broadcaster
|
||||||
)
|
)
|
||||||
|
|
||||||
//Start starts up the core processing
|
//Start starts up the core processing
|
||||||
@@ -40,6 +44,12 @@ func Start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.Config.YP.Enabled {
|
||||||
|
_yp = yp.NewYP(GetStatus)
|
||||||
|
} else {
|
||||||
|
yp.DisplayInstructions()
|
||||||
|
}
|
||||||
|
|
||||||
chat.Setup(ChatListenerImpl{})
|
chat.Setup(ChatListenerImpl{})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -47,8 +57,8 @@ func Start() error {
|
|||||||
|
|
||||||
func createInitialOfflineState() error {
|
func createInitialOfflineState() error {
|
||||||
// Provide default files
|
// Provide default files
|
||||||
if !utils.DoesFileExists("webroot/thumbnail.jpg") {
|
if !utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) {
|
||||||
if err := utils.Copy("static/logo.png", "webroot/thumbnail.jpg"); err != nil {
|
if err := utils.Copy("static/logo.png", filepath.Join(config.WebRoot, "thumbnail.jpg")); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,22 +94,22 @@ func resetDirectories() {
|
|||||||
log.Trace("Resetting file directories to a clean slate.")
|
log.Trace("Resetting file directories to a clean slate.")
|
||||||
|
|
||||||
// Wipe the public, web-accessible hls data directory
|
// Wipe the public, web-accessible hls data directory
|
||||||
os.RemoveAll(config.Config.GetPublicHLSSavePath())
|
os.RemoveAll(config.PublicHLSStoragePath)
|
||||||
os.RemoveAll(config.Config.GetPrivateHLSSavePath())
|
os.RemoveAll(config.PrivateHLSStoragePath)
|
||||||
os.MkdirAll(config.Config.GetPublicHLSSavePath(), 0777)
|
os.MkdirAll(config.PublicHLSStoragePath, 0777)
|
||||||
os.MkdirAll(config.Config.GetPrivateHLSSavePath(), 0777)
|
os.MkdirAll(config.PrivateHLSStoragePath, 0777)
|
||||||
|
|
||||||
// Remove the previous thumbnail
|
// Remove the previous thumbnail
|
||||||
os.Remove("webroot/thumbnail.jpg")
|
os.Remove(filepath.Join(config.WebRoot, "thumbnail.jpg"))
|
||||||
|
|
||||||
// Create private hls data dirs
|
// Create private hls data dirs
|
||||||
if len(config.Config.VideoSettings.StreamQualities) != 0 {
|
if len(config.Config.VideoSettings.StreamQualities) != 0 {
|
||||||
for index := range config.Config.VideoSettings.StreamQualities {
|
for index := range config.Config.VideoSettings.StreamQualities {
|
||||||
os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(index)), 0777)
|
os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(index)), 0777)
|
||||||
os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(index)), 0777)
|
os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(index)), 0777)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(0)), 0777)
|
os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(0)), 0777)
|
||||||
os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(0)), 0777)
|
os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(0)), 0777)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
//ShowStreamOfflineState generates and shows the stream's offline state
|
//ShowStreamOfflineState generates and shows the stream's offline state
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
//StartThumbnailGenerator starts generating thumbnails
|
//StartThumbnailGenerator starts generating thumbnails
|
||||||
@@ -39,7 +39,8 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int) {
|
|||||||
|
|
||||||
func fireThumbnailGenerator(chunkPath string, variantIndex int) error {
|
func fireThumbnailGenerator(chunkPath string, variantIndex int) error {
|
||||||
// JPG takes less time to encode than PNG
|
// JPG takes less time to encode than PNG
|
||||||
outputFile := path.Join("webroot", "thumbnail.jpg")
|
outputFile := path.Join(config.WebRoot, "thumbnail.jpg")
|
||||||
|
previewGifFile := path.Join(config.WebRoot, "preview.gif")
|
||||||
|
|
||||||
framePath := path.Join(chunkPath, strconv.Itoa(variantIndex))
|
framePath := path.Join(chunkPath, strconv.Itoa(variantIndex))
|
||||||
files, err := ioutil.ReadDir(framePath)
|
files, err := ioutil.ReadDir(framePath)
|
||||||
@@ -83,12 +84,32 @@ func fireThumbnailGenerator(chunkPath string, variantIndex int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ffmpegCmd := strings.Join(thumbnailCmdFlags, " ")
|
ffmpegCmd := strings.Join(thumbnailCmdFlags, " ")
|
||||||
|
|
||||||
// fmt.Println(ffmpegCmd)
|
|
||||||
|
|
||||||
if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
|
if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If YP support is enabled also create an animated GIF preview
|
||||||
|
if config.Config.YP.Enabled {
|
||||||
|
makeAnimatedGifPreview(mostRecentFile, previewGifFile)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeAnimatedGifPreview(sourceFile string, outputFile string) {
|
||||||
|
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
|
||||||
|
animatedGifFlags := []string{
|
||||||
|
config.Config.GetFFMpegPath(),
|
||||||
|
"-y", // Overwrite file
|
||||||
|
"-threads 1", // Low priority processing
|
||||||
|
"-i", sourceFile, // Input
|
||||||
|
"-t 1", // Output is one second in length
|
||||||
|
"-filter_complex", "\"[0:v] fps=8,scale=w=480:h=-1:flags=lanczos,split [a][b];[a] palettegen=stats_mode=full [p];[b][p] paletteuse=new=1\"",
|
||||||
|
outputFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
ffmpegCmd := strings.Join(animatedGifFlags, " ")
|
||||||
|
if _, err := exec.Command("sh", "-c", ffmpegCmd).Output(); err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/teris-io/shortid"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _commandExec *exec.Cmd
|
var _commandExec *exec.Cmd
|
||||||
@@ -25,6 +26,7 @@ type Transcoder struct {
|
|||||||
segmentLengthSeconds int
|
segmentLengthSeconds int
|
||||||
appendToStream bool
|
appendToStream bool
|
||||||
ffmpegPath string
|
ffmpegPath string
|
||||||
|
segmentIdentifier string
|
||||||
}
|
}
|
||||||
|
|
||||||
// HLSVariant is a combination of settings that results in a single HLS stream
|
// HLSVariant is a combination of settings that results in a single HLS stream
|
||||||
@@ -103,6 +105,10 @@ func (t *Transcoder) getString() string {
|
|||||||
hlsOptionFlags = append(hlsOptionFlags, "append_list")
|
hlsOptionFlags = append(hlsOptionFlags, "append_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.segmentIdentifier == "" {
|
||||||
|
t.segmentIdentifier = shortid.MustGenerate()
|
||||||
|
}
|
||||||
|
|
||||||
ffmpegFlags := []string{
|
ffmpegFlags := []string{
|
||||||
"cat", t.input, "|",
|
"cat", t.input, "|",
|
||||||
t.ffmpegPath,
|
t.ffmpegPath,
|
||||||
@@ -125,7 +131,7 @@ func (t *Transcoder) getString() string {
|
|||||||
// Filenames
|
// Filenames
|
||||||
"-master_pl_name", "stream.m3u8",
|
"-master_pl_name", "stream.m3u8",
|
||||||
"-strftime 1", // Support the use of strftime in filenames
|
"-strftime 1", // Support the use of strftime in filenames
|
||||||
"-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s.ts"), // Each segment's filename
|
"-hls_segment_filename", path.Join(t.segmentOutputPath, "/%v/stream-%s-"+t.segmentIdentifier+".ts"), // Each segment's filename
|
||||||
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
||||||
path.Join(t.segmentOutputPath, "/%v/stream.m3u8"), // Each variant's playlist
|
path.Join(t.segmentOutputPath, "/%v/stream.m3u8"), // Each variant's playlist
|
||||||
"2> transcoder.log",
|
"2> transcoder.log",
|
||||||
@@ -182,15 +188,15 @@ func NewTranscoder() Transcoder {
|
|||||||
var outputPath string
|
var outputPath string
|
||||||
if config.Config.S3.Enabled {
|
if config.Config.S3.Enabled {
|
||||||
// Segments are not available via the local HTTP server
|
// Segments are not available via the local HTTP server
|
||||||
outputPath = config.Config.GetPrivateHLSSavePath()
|
outputPath = config.PrivateHLSStoragePath
|
||||||
} else {
|
} else {
|
||||||
// Segments are available via the local HTTP server
|
// Segments are available via the local HTTP server
|
||||||
outputPath = config.Config.GetPublicHLSSavePath()
|
outputPath = config.PublicHLSStoragePath
|
||||||
}
|
}
|
||||||
|
|
||||||
transcoder.segmentOutputPath = outputPath
|
transcoder.segmentOutputPath = outputPath
|
||||||
// Playlists are available via the local HTTP server
|
// Playlists are available via the local HTTP server
|
||||||
transcoder.playlistOutputPath = config.Config.GetPublicHLSSavePath()
|
transcoder.playlistOutputPath = config.PublicHLSStoragePath
|
||||||
|
|
||||||
transcoder.input = utils.GetTemporaryPipePath()
|
transcoder.input = utils.GetTemporaryPipePath()
|
||||||
transcoder.segmentLengthSeconds = config.Config.GetVideoSegmentSecondsLength()
|
transcoder.segmentLengthSeconds = config.Config.GetVideoSegmentSecondsLength()
|
||||||
@@ -353,3 +359,8 @@ func (t *Transcoder) SetSegmentLength(seconds int) {
|
|||||||
func (t *Transcoder) SetAppendToStream(append bool) {
|
func (t *Transcoder) SetAppendToStream(append bool) {
|
||||||
t.appendToStream = append
|
t.appendToStream = append
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIdentifer enables appending a unique identifier to segment file name
|
||||||
|
func (t *Transcoder) SetIdentifier(output string) {
|
||||||
|
t.segmentIdentifier = output
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func TestFFmpegCommand(t *testing.T) {
|
|||||||
transcoder.SetInput("fakecontent.flv")
|
transcoder.SetInput("fakecontent.flv")
|
||||||
transcoder.SetOutputPath("fakeOutput")
|
transcoder.SetOutputPath("fakeOutput")
|
||||||
transcoder.SetHLSPlaylistLength(10)
|
transcoder.SetHLSPlaylistLength(10)
|
||||||
|
transcoder.SetIdentifier("jdofFGg")
|
||||||
|
|
||||||
variant := HLSVariant{}
|
variant := HLSVariant{}
|
||||||
variant.videoBitrate = 1200
|
variant.videoBitrate = 1200
|
||||||
@@ -22,7 +23,7 @@ func TestFFmpegCommand(t *testing.T) {
|
|||||||
|
|
||||||
cmd := transcoder.getString()
|
cmd := transcoder.getString()
|
||||||
|
|
||||||
expected := `cat fakecontent.flv | /fake/path/ffmpeg -hide_banner -i pipe: -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 119 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=119:keyint=119" -map a:0 -c:a:0 copy -r 30 -preset veryfast -var_stream_map "v:0,a:0 " -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -hls_flags delete_segments+program_date_time+temp_file -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename fakeOutput/%v/stream-%s.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log`
|
expected := `cat fakecontent.flv | /fake/path/ffmpeg -hide_banner -i pipe: -map v:0 -c:v:0 libx264 -b:v:0 1200k -maxrate:v:0 1272k -bufsize:v:0 1440k -g:v:0 119 -x264-params:v:0 "scenecut=0:open_gop=0:min-keyint=119:keyint=119" -map a:0 -c:a:0 copy -r 30 -preset veryfast -var_stream_map "v:0,a:0 " -f hls -hls_time 4 -hls_list_size 10 -hls_delete_threshold 10 -hls_flags delete_segments+program_date_time+temp_file -tune zerolatency -sc_threshold 0 -master_pl_name stream.m3u8 -strftime 1 -hls_segment_filename fakeOutput/%v/stream-%s-jdofFGg.ts -max_muxing_queue_size 400 fakeOutput/%v/stream.m3u8 2> transcoder.log`
|
||||||
|
|
||||||
if cmd != expected {
|
if cmd != expected {
|
||||||
t.Errorf("ffmpeg command does not match expected. Got %s, want: %s", cmd, expected)
|
t.Errorf("ffmpeg command does not match expected. Got %s, want: %s", cmd, expected)
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/radovskyb/watcher"
|
"github.com/radovskyb/watcher"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -26,7 +26,7 @@ var (
|
|||||||
func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
|
func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
|
||||||
_storage = storage
|
_storage = storage
|
||||||
|
|
||||||
pathToMonitor := config.Config.GetPrivateHLSSavePath()
|
pathToMonitor := config.PrivateHLSStoragePath
|
||||||
|
|
||||||
// Create at least one structure to store the segments for the different stream variants
|
// Create at least one structure to store the segments for the different stream variants
|
||||||
variants = make([]models.Variant, len(config.Config.VideoSettings.StreamQualities))
|
variants = make([]models.Variant, len(config.Config.VideoSettings.StreamQualities))
|
||||||
@@ -63,11 +63,9 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmt.Println(event.Op, relativePath)
|
|
||||||
|
|
||||||
// Handle updates to the master playlist by copying it to webroot
|
// Handle updates to the master playlist by copying it to webroot
|
||||||
if relativePath == path.Join(config.Config.GetPrivateHLSSavePath(), "stream.m3u8") {
|
if relativePath == path.Join(config.PrivateHLSStoragePath, "stream.m3u8") {
|
||||||
utils.Copy(event.Path, path.Join(config.Config.GetPublicHLSSavePath(), "stream.m3u8"))
|
utils.Copy(event.Path, path.Join(config.PublicHLSStoragePath, "stream.m3u8"))
|
||||||
|
|
||||||
} else if filepath.Ext(event.Path) == ".m3u8" {
|
} else if filepath.Ext(event.Path) == ".m3u8" {
|
||||||
// Handle updates to playlists, but not the master playlist
|
// Handle updates to playlists, but not the master playlist
|
||||||
@@ -82,7 +80,7 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
|
|||||||
|
|
||||||
newObjectPathChannel := make(chan string, 1)
|
newObjectPathChannel := make(chan string, 1)
|
||||||
go func() {
|
go func() {
|
||||||
newObjectPath, err := storage.Save(path.Join(config.Config.GetPrivateHLSSavePath(), segment.RelativeUploadPath), 0)
|
newObjectPath, err := storage.Save(path.Join(config.PrivateHLSStoragePath, segment.RelativeUploadPath), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("failed to save the file to the chunk storage.", err)
|
log.Errorln("failed to save the file to the chunk storage.", err)
|
||||||
}
|
}
|
||||||
@@ -155,5 +153,5 @@ func updateVariantPlaylist(fullPath string) error {
|
|||||||
playlistString := string(playlistBytes)
|
playlistString := string(playlistBytes)
|
||||||
playlistString = _storage.GenerateRemotePlaylist(playlistString, variant)
|
playlistString = _storage.GenerateRemotePlaylist(playlistString, variant)
|
||||||
|
|
||||||
return WritePlaylist(playlistString, path.Join(config.Config.GetPublicHLSSavePath(), relativePath))
|
return WritePlaylist(playlistString, path.Join(config.PublicHLSStoragePath, relativePath))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ import (
|
|||||||
"github.com/nareix/joy5/format/flv/flvio"
|
"github.com/nareix/joy5/format/flv/flvio"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/gabek/owncast/core/ffmpeg"
|
"github.com/owncast/owncast/core/ffmpeg"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/nareix/joy5/format/rtmp"
|
"github.com/nareix/joy5/format/rtmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ var (
|
|||||||
|
|
||||||
var _transcoder ffmpeg.Transcoder
|
var _transcoder ffmpeg.Transcoder
|
||||||
var _pipe *os.File
|
var _pipe *os.File
|
||||||
|
var _rtmpConnection net.Conn
|
||||||
|
|
||||||
//Start starts the rtmp service, listening on port 1935
|
//Start starts the rtmp service, listening on port 1935
|
||||||
func Start() {
|
func Start() {
|
||||||
@@ -61,10 +63,36 @@ func Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setCurrentBroadcasterInfo(t flvio.Tag, remoteAddr string) {
|
||||||
|
data, err := getInboundDetailsFromMetadata(t.DebugFields())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcaster := models.Broadcaster{
|
||||||
|
RemoteAddr: remoteAddr,
|
||||||
|
Time: time.Now(),
|
||||||
|
StreamDetails: models.InboundStreamDetails{
|
||||||
|
Width: data.Width,
|
||||||
|
Height: data.Height,
|
||||||
|
VideoBitrate: int(data.VideoBitrate),
|
||||||
|
VideoCodec: getVideoCodec(data.VideoCodec),
|
||||||
|
VideoFramerate: data.VideoFramerate,
|
||||||
|
AudioBitrate: int(data.AudioBitrate),
|
||||||
|
AudioCodec: getAudioCodec(data.AudioCodec),
|
||||||
|
Encoder: data.Encoder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
core.SetBroadcaster(broadcaster)
|
||||||
|
}
|
||||||
|
|
||||||
func HandleConn(c *rtmp.Conn, nc net.Conn) {
|
func HandleConn(c *rtmp.Conn, nc net.Conn) {
|
||||||
c.LogTagEvent = func(isRead bool, t flvio.Tag) {
|
c.LogTagEvent = func(isRead bool, t flvio.Tag) {
|
||||||
if t.Type == flvio.TAG_AMF0 {
|
if t.Type == flvio.TAG_AMF0 {
|
||||||
log.Tracef("%+v\n", t.DebugFields())
|
log.Tracef("%+v\n", t.DebugFields())
|
||||||
|
setCurrentBroadcasterInfo(t, nc.RemoteAddr().String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +120,7 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
|
|||||||
|
|
||||||
_isConnected = true
|
_isConnected = true
|
||||||
core.SetStreamAsConnected()
|
core.SetStreamAsConnected()
|
||||||
|
_rtmpConnection = nc
|
||||||
|
|
||||||
f, err := os.OpenFile(pipePath, os.O_RDWR, os.ModeNamedPipe)
|
f, err := os.OpenFile(pipePath, os.O_RDWR, os.ModeNamedPipe)
|
||||||
_pipe = f
|
_pipe = f
|
||||||
@@ -121,9 +150,20 @@ func handleDisconnect(conn net.Conn) {
|
|||||||
_pipe.Close()
|
_pipe.Close()
|
||||||
_isConnected = false
|
_isConnected = false
|
||||||
_transcoder.Stop()
|
_transcoder.Stop()
|
||||||
|
_rtmpConnection = nil
|
||||||
core.SetStreamAsDisconnected()
|
core.SetStreamAsDisconnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disconnect will force disconnect the current inbound RTMP connection.
|
||||||
|
func Disconnect() {
|
||||||
|
if _rtmpConnection == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infoln("Inbound stream disconnect requested.")
|
||||||
|
handleDisconnect(_rtmpConnection)
|
||||||
|
}
|
||||||
|
|
||||||
//IsConnected gets whether there is an rtmp connection or not
|
//IsConnected gets whether there is an rtmp connection or not
|
||||||
//this is only a getter since it is controlled by the rtmp handler
|
//this is only a getter since it is controlled by the rtmp handler
|
||||||
func IsConnected() bool {
|
func IsConnected() bool {
|
||||||
|
|||||||
64
core/rtmp/utils.go
Normal file
64
core/rtmp/utils.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package rtmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/nareix/joy5/format/flv/flvio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInboundDetailsFromMetadata(metadata []interface{}) (models.RTMPStreamMetadata, error) {
|
||||||
|
metadataComponentsString := fmt.Sprintf("%+v", metadata)
|
||||||
|
re := regexp.MustCompile(`\{(.*?)\}`)
|
||||||
|
submatchall := re.FindAllString(metadataComponentsString, 1)
|
||||||
|
|
||||||
|
if len(submatchall) == 0 {
|
||||||
|
return models.RTMPStreamMetadata{}, errors.New("unable to parse inbound metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataJSONString := submatchall[0]
|
||||||
|
var details models.RTMPStreamMetadata
|
||||||
|
json.Unmarshal([]byte(metadataJSONString), &details)
|
||||||
|
return details, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAudioCodec(codec interface{}) string {
|
||||||
|
var codecID float64
|
||||||
|
if assertedCodecID, ok := codec.(float64); ok {
|
||||||
|
codecID = assertedCodecID
|
||||||
|
} else {
|
||||||
|
return codec.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch codecID {
|
||||||
|
case flvio.SOUND_MP3:
|
||||||
|
return "MP3"
|
||||||
|
case flvio.SOUND_AAC:
|
||||||
|
return "AAC"
|
||||||
|
case flvio.SOUND_SPEEX:
|
||||||
|
return "Speex"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVideoCodec(codec interface{}) string {
|
||||||
|
var codecID float64
|
||||||
|
if assertedCodecID, ok := codec.(float64); ok {
|
||||||
|
codecID = assertedCodecID
|
||||||
|
} else {
|
||||||
|
return codec.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch codecID {
|
||||||
|
case flvio.VIDEO_H264:
|
||||||
|
return "H.264"
|
||||||
|
case flvio.VIDEO_H265:
|
||||||
|
return "H.265"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package core
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core/ffmpeg"
|
"github.com/owncast/owncast/core/ffmpeg"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//GetStatus gets the status of the system
|
//GetStatus gets the status of the system
|
||||||
@@ -33,9 +33,13 @@ func SetStreamAsConnected() {
|
|||||||
_stats.LastConnectTime = utils.NullTime{time.Now(), true}
|
_stats.LastConnectTime = utils.NullTime{time.Now(), true}
|
||||||
_stats.LastDisconnectTime = utils.NullTime{time.Now(), false}
|
_stats.LastDisconnectTime = utils.NullTime{time.Now(), false}
|
||||||
|
|
||||||
chunkPath := config.Config.GetPublicHLSSavePath()
|
chunkPath := config.PublicHLSStoragePath
|
||||||
if usingExternalStorage {
|
if usingExternalStorage {
|
||||||
chunkPath = config.Config.GetPrivateHLSSavePath()
|
chunkPath = config.PrivateHLSStoragePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if _yp != nil {
|
||||||
|
_yp.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
ffmpeg.StartThumbnailGenerator(chunkPath, config.Config.VideoSettings.HighestQualityStreamIndex)
|
ffmpeg.StartThumbnailGenerator(chunkPath, config.Config.VideoSettings.HighestQualityStreamIndex)
|
||||||
@@ -45,7 +49,21 @@ func SetStreamAsConnected() {
|
|||||||
func SetStreamAsDisconnected() {
|
func SetStreamAsDisconnected() {
|
||||||
_stats.StreamConnected = false
|
_stats.StreamConnected = false
|
||||||
_stats.LastDisconnectTime = utils.NullTime{time.Now(), true}
|
_stats.LastDisconnectTime = utils.NullTime{time.Now(), true}
|
||||||
|
_broadcaster = nil
|
||||||
|
|
||||||
|
if _yp != nil {
|
||||||
|
_yp.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
ffmpeg.ShowStreamOfflineState()
|
ffmpeg.ShowStreamOfflineState()
|
||||||
startCleanupTimer()
|
startCleanupTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetBroadcaster will store the current inbound broadcasting details
|
||||||
|
func SetBroadcaster(broadcaster models.Broadcaster) {
|
||||||
|
_broadcaster = &broadcaster
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBroadcaster() *models.Broadcaster {
|
||||||
|
return _broadcaster
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core/playlist"
|
"github.com/owncast/owncast/core/playlist"
|
||||||
"github.com/gabek/owncast/core/storageproviders"
|
"github.com/owncast/owncast/core/storageproviders"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
//S3Storage is the s3 implementation of the ChunkStorageProvider
|
//S3Storage is the s3 implementation of the ChunkStorageProvider
|
||||||
@@ -94,7 +94,7 @@ func (s *S3Storage) GenerateRemotePlaylist(playlist string, variant models.Varia
|
|||||||
if fullRemotePath == nil {
|
if fullRemotePath == nil {
|
||||||
line = ""
|
line = ""
|
||||||
} else if s.s3ServingEndpoint != "" {
|
} else if s.s3ServingEndpoint != "" {
|
||||||
line = fmt.Sprintf("%s/%s/%s", s.s3ServingEndpoint, config.Config.GetPrivateHLSSavePath(), fullRemotePath.RelativeUploadPath)
|
line = fmt.Sprintf("%s/%s/%s", s.s3ServingEndpoint, config.PrivateHLSStoragePath, fullRemotePath.RelativeUploadPath)
|
||||||
} else {
|
} else {
|
||||||
line = fullRemotePath.RemoteID
|
line = fullRemotePath.RemoteID
|
||||||
}
|
}
|
||||||
|
|||||||
1
doc
1
doc
Submodule doc deleted from 54a0ee1396
3
go.mod
3
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/gabek/owncast
|
module github.com/owncast/owncast
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/mssola/user_agent v0.5.2
|
github.com/mssola/user_agent v0.5.2
|
||||||
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88
|
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
|
github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1
|
||||||
github.com/radovskyb/watcher v1.0.7
|
github.com/radovskyb/watcher v1.0.7
|
||||||
github.com/shirou/gopsutil v2.20.7+incompatible
|
github.com/shirou/gopsutil v2.20.7+incompatible
|
||||||
github.com/sirupsen/logrus v1.6.0
|
github.com/sirupsen/logrus v1.6.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -31,6 +31,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1 h1:CskT+S6Ay54OwxBGB0R3Rsx4Muto6UnEYTyKJbyRIAI=
|
||||||
|
github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
|
||||||
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
|
||||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||||
github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=
|
github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -7,10 +7,10 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/gabek/owncast/metrics"
|
"github.com/owncast/owncast/metrics"
|
||||||
"github.com/gabek/owncast/router"
|
"github.com/owncast/owncast/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the following are injected at build-time
|
// the following are injected at build-time
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
|
|
||||||
const maxCPUAlertingThresholdPCT = 95
|
const maxCPUAlertingThresholdPCT = 95
|
||||||
const maxRAMAlertingThresholdPCT = 95
|
const maxRAMAlertingThresholdPCT = 95
|
||||||
|
const maxDiskAlertingThresholdPCT = 95
|
||||||
|
|
||||||
const alertingError = "The %s utilization of %d%% is higher than the alerting threshold of %d%%. This can cause issues with video generation and delivery. Please visit the documentation at http://owncast.online/docs/troubleshooting/ to help troubleshoot this issue."
|
const alertingError = "The %s utilization of %d%% can cause issues with video generation and delivery. Please visit the documentation at http://owncast.online/docs/troubleshooting/ to help troubleshoot this issue."
|
||||||
|
|
||||||
func handleAlerting() {
|
func handleAlerting() {
|
||||||
handleCPUAlerting()
|
handleCPUAlerting()
|
||||||
handleRAMAlerting()
|
handleRAMAlerting()
|
||||||
|
handleDiskAlerting()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCPUAlerting() {
|
func handleCPUAlerting() {
|
||||||
@@ -21,7 +23,7 @@ func handleCPUAlerting() {
|
|||||||
|
|
||||||
avg := recentAverage(Metrics.CPUUtilizations)
|
avg := recentAverage(Metrics.CPUUtilizations)
|
||||||
if avg > maxCPUAlertingThresholdPCT {
|
if avg > maxCPUAlertingThresholdPCT {
|
||||||
log.Errorf(alertingError, "CPU", avg, maxCPUAlertingThresholdPCT)
|
log.Errorf(alertingError, "CPU", maxCPUAlertingThresholdPCT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,10 +34,22 @@ func handleRAMAlerting() {
|
|||||||
|
|
||||||
avg := recentAverage(Metrics.RAMUtilizations)
|
avg := recentAverage(Metrics.RAMUtilizations)
|
||||||
if avg > maxRAMAlertingThresholdPCT {
|
if avg > maxRAMAlertingThresholdPCT {
|
||||||
log.Errorf(alertingError, "memory", avg, maxRAMAlertingThresholdPCT)
|
log.Errorf(alertingError, "memory", maxRAMAlertingThresholdPCT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recentAverage(values []value) int {
|
func handleDiskAlerting() {
|
||||||
|
if len(Metrics.DiskUtilizations) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avg := recentAverage(Metrics.DiskUtilizations)
|
||||||
|
|
||||||
|
if avg > maxDiskAlertingThresholdPCT {
|
||||||
|
log.Errorf(alertingError, "disk", maxRAMAlertingThresholdPCT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recentAverage(values []timestampedValue) int {
|
||||||
return int((values[len(values)-1].Value + values[len(values)-2].Value) / 2)
|
return int((values[len(values)-1].Value + values[len(values)-2].Value) / 2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/cpu"
|
"github.com/shirou/gopsutil/cpu"
|
||||||
|
"github.com/shirou/gopsutil/disk"
|
||||||
"github.com/shirou/gopsutil/mem"
|
"github.com/shirou/gopsutil/mem"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ func collectCPUUtilization() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metricValue := value{time.Now(), int(v[0])}
|
metricValue := timestampedValue{time.Now(), int(v[0])}
|
||||||
Metrics.CPUUtilizations = append(Metrics.CPUUtilizations, metricValue)
|
Metrics.CPUUtilizations = append(Metrics.CPUUtilizations, metricValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +31,18 @@ func collectRAMUtilization() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
memoryUsage, _ := mem.VirtualMemory()
|
memoryUsage, _ := mem.VirtualMemory()
|
||||||
metricValue := value{time.Now(), int(memoryUsage.UsedPercent)}
|
metricValue := timestampedValue{time.Now(), int(memoryUsage.UsedPercent)}
|
||||||
Metrics.RAMUtilizations = append(Metrics.RAMUtilizations, metricValue)
|
Metrics.RAMUtilizations = append(Metrics.RAMUtilizations, metricValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectDiskUtilization() {
|
||||||
|
path := "./"
|
||||||
|
diskUse, _ := disk.Usage(path)
|
||||||
|
|
||||||
|
if len(Metrics.DiskUtilizations) > maxCollectionValues {
|
||||||
|
Metrics.DiskUtilizations = Metrics.DiskUtilizations[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
metricValue := timestampedValue{time.Now(), int(diskUse.UsedPercent)}
|
||||||
|
Metrics.DiskUtilizations = append(Metrics.DiskUtilizations, metricValue)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// How often we poll for updates
|
// How often we poll for updates
|
||||||
const metricsPollingInterval = 15 * time.Second
|
const metricsPollingInterval = 1 * time.Minute
|
||||||
|
|
||||||
type value struct {
|
// CollectedMetrics stores different collected + timestamped values
|
||||||
Time time.Time
|
type CollectedMetrics struct {
|
||||||
Value int
|
CPUUtilizations []timestampedValue `json:"cpu"`
|
||||||
}
|
RAMUtilizations []timestampedValue `json:"memory"`
|
||||||
|
DiskUtilizations []timestampedValue `json:"disk"`
|
||||||
|
|
||||||
type metrics struct {
|
Viewers []timestampedValue `json:"-"`
|
||||||
CPUUtilizations []value
|
|
||||||
RAMUtilizations []value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics is the shared Metrics instance
|
// Metrics is the shared Metrics instance
|
||||||
var Metrics *metrics
|
var Metrics *CollectedMetrics
|
||||||
|
|
||||||
// Start will begin the metrics collection and alerting
|
// Start will begin the metrics collection and alerting
|
||||||
func Start() {
|
func Start() {
|
||||||
Metrics = new(metrics)
|
Metrics = new(CollectedMetrics)
|
||||||
|
go startViewerCollectionMetrics()
|
||||||
|
handlePolling()
|
||||||
|
|
||||||
for range time.Tick(metricsPollingInterval) {
|
for range time.Tick(metricsPollingInterval) {
|
||||||
handlePolling()
|
handlePolling()
|
||||||
@@ -33,6 +34,7 @@ func handlePolling() {
|
|||||||
// Collect hardware stats
|
// Collect hardware stats
|
||||||
collectCPUUtilization()
|
collectCPUUtilization()
|
||||||
collectRAMUtilization()
|
collectRAMUtilization()
|
||||||
|
collectDiskUtilization()
|
||||||
|
|
||||||
// Alerting
|
// Alerting
|
||||||
handleAlerting()
|
handleAlerting()
|
||||||
|
|||||||
8
metrics/timestampedValue.go
Normal file
8
metrics/timestampedValue.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type timestampedValue struct {
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
31
metrics/viewers.go
Normal file
31
metrics/viewers.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// How often we poll for updates
|
||||||
|
const viewerMetricsPollingInterval = 5 * time.Minute
|
||||||
|
|
||||||
|
func startViewerCollectionMetrics() {
|
||||||
|
collectViewerCount()
|
||||||
|
|
||||||
|
for range time.Tick(viewerMetricsPollingInterval) {
|
||||||
|
collectViewerCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectViewerCount() {
|
||||||
|
if len(Metrics.Viewers) > maxCollectionValues {
|
||||||
|
Metrics.Viewers = Metrics.Viewers[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
count := core.GetStatus().ViewerCount
|
||||||
|
value := timestampedValue{
|
||||||
|
Value: count,
|
||||||
|
Time: time.Now(),
|
||||||
|
}
|
||||||
|
Metrics.Viewers = append(Metrics.Viewers, value)
|
||||||
|
}
|
||||||
7
models/baseAPIResponse.go
Normal file
7
models/baseAPIResponse.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// BaseAPIResponse is a simple response to API requests.
|
||||||
|
type BaseAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
33
models/broadcaster.go
Normal file
33
models/broadcaster.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Broadcaster represents the details around the inbound broadcasting connection.
|
||||||
|
type Broadcaster struct {
|
||||||
|
RemoteAddr string `json:"remoteAddr"`
|
||||||
|
StreamDetails InboundStreamDetails `json:"streamDetails"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InboundStreamDetails struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
VideoFramerate int `json:"framerate"`
|
||||||
|
VideoBitrate int `json:"videoBitrate"`
|
||||||
|
VideoCodec string `json:"videoCodec"`
|
||||||
|
AudioBitrate int `json:"audioBitrate"`
|
||||||
|
AudioCodec string `json:"audioCodec"`
|
||||||
|
Encoder string `json:"encoder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTMPStreamMetadata is the raw metadata that comes in with a RTMP connection
|
||||||
|
type RTMPStreamMetadata struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
VideoBitrate float32 `json:"videodatarate"`
|
||||||
|
VideoCodec interface{} `json:"videocodecid"`
|
||||||
|
VideoFramerate int `json:"framerate"`
|
||||||
|
AudioBitrate float32 `json:"audiodatarate"`
|
||||||
|
AudioCodec interface{} `json:"audiocodecid"`
|
||||||
|
Encoder string `json:"encoder"`
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package models
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gabek/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Stats holds the stats for the system
|
//Stats holds the stats for the system
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import "github.com/gabek/owncast/utils"
|
import "github.com/owncast/owncast/utils"
|
||||||
|
|
||||||
//Status represents the status of the system
|
//Status represents the status of the system
|
||||||
type Status struct {
|
type Status struct {
|
||||||
|
|||||||
513
openapi.yaml
Normal file
513
openapi.yaml
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
openapi: 3.0.1
|
||||||
|
info:
|
||||||
|
title: Owncast
|
||||||
|
description: Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software.
|
||||||
|
version: '0.0.2'
|
||||||
|
servers: []
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Admin
|
||||||
|
description: Admin operations requiring authentication.
|
||||||
|
- name: Chat
|
||||||
|
description: Endpoints related to the chat interface.
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
BasicResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
InstanceDetails:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
summary:
|
||||||
|
type: string
|
||||||
|
description: This is brief summary of whom you are or what the stream is.
|
||||||
|
logo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
large:
|
||||||
|
type: string
|
||||||
|
small:
|
||||||
|
type: string
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
socialHandles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
platform:
|
||||||
|
type: string
|
||||||
|
example: github
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: http://github.com/owncast/owncast
|
||||||
|
extraUserInfoFileName:
|
||||||
|
type: string
|
||||||
|
description: Path to additional content about the server.
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
example: Owncast v0.0.2-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb)
|
||||||
|
S3:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
endpoint:
|
||||||
|
type: string
|
||||||
|
servingEndpoint:
|
||||||
|
type: string
|
||||||
|
accessKey:
|
||||||
|
type: string
|
||||||
|
secret:
|
||||||
|
type: string
|
||||||
|
bucket:
|
||||||
|
type: string
|
||||||
|
region:
|
||||||
|
type: string
|
||||||
|
acl:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- enabled
|
||||||
|
StreamQuality:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
videoPassthrough:
|
||||||
|
type: boolean
|
||||||
|
audioPassthrough:
|
||||||
|
type: boolean
|
||||||
|
videoBitrate:
|
||||||
|
type: integer
|
||||||
|
audioBitrate:
|
||||||
|
type: integer
|
||||||
|
scaledWidth:
|
||||||
|
type: integer
|
||||||
|
scaledHeight:
|
||||||
|
type: integer
|
||||||
|
framerate:
|
||||||
|
type: integer
|
||||||
|
encoderPreset:
|
||||||
|
type: string
|
||||||
|
TimestampedValue:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
value:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
AdminBasicAuth:
|
||||||
|
type: http
|
||||||
|
scheme: basic
|
||||||
|
description: The username for admin basic auth is `admin` and the password is the stream key.
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BasicResponse:
|
||||||
|
description: Operation Success/Failure Response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/BasicResponse"
|
||||||
|
examples:
|
||||||
|
success:
|
||||||
|
summary: Operation succeeded.
|
||||||
|
value: {"success": true, "message": "inbound stream disconnected"}
|
||||||
|
failure:
|
||||||
|
summary: Operation failed.
|
||||||
|
value: {"success": false, "message": "no inbound stream connected"}
|
||||||
|
|
||||||
|
paths:
|
||||||
|
|
||||||
|
/api/config:
|
||||||
|
get:
|
||||||
|
summary: Information
|
||||||
|
description: Get the public information about the server. Adds context to the server, as well as information useful for the user interface.
|
||||||
|
tags: ["Server"]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/InstanceDetails"
|
||||||
|
|
||||||
|
/api/status:
|
||||||
|
get:
|
||||||
|
summary: Current Status
|
||||||
|
description: This endpoint is used to discover when a server is broadcasting, the number of active viewers as well as other useful information for updating the user interface.
|
||||||
|
tags: ["Server"]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lastConnectTime:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
format: date-time
|
||||||
|
overallMaxViewerCount:
|
||||||
|
type: integer
|
||||||
|
sessionMaxViewerCount:
|
||||||
|
type: integer
|
||||||
|
online:
|
||||||
|
type: boolean
|
||||||
|
viewerCount:
|
||||||
|
type: integer
|
||||||
|
lastDisconnectTime:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
format: date-time
|
||||||
|
examples:
|
||||||
|
online:
|
||||||
|
value:
|
||||||
|
lastConnectTime: "2020-10-03T21:36:22-05:00"
|
||||||
|
lastDisconnectTime: null
|
||||||
|
online: true
|
||||||
|
overallMaxViewerCount: 420
|
||||||
|
sessionMaxViewerCount: 12
|
||||||
|
viewerCount: 7
|
||||||
|
|
||||||
|
/api/chat:
|
||||||
|
get:
|
||||||
|
summary: Historical Chat Messages
|
||||||
|
description: Used to get all chat messages prior to connecting to the websocket.
|
||||||
|
tags: ["Chat"]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
author:
|
||||||
|
type: string
|
||||||
|
description: Username of the chat message poster.
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
description: Escaped HTML of the chat message content.
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
description: URL of the chat user avatar.
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
description: Unique ID of the chat message.
|
||||||
|
visible:
|
||||||
|
type: boolean
|
||||||
|
description: "TODO"
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
/api/yp:
|
||||||
|
get:
|
||||||
|
summary: Yellow Pages Information
|
||||||
|
description: Information to be used in the Yellow Pages service, a global directory of Owncast servers.
|
||||||
|
tags: ["Server"]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
logo:
|
||||||
|
type: string
|
||||||
|
nsfw:
|
||||||
|
type: boolean
|
||||||
|
tags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
online:
|
||||||
|
type: boolean
|
||||||
|
viewerCount:
|
||||||
|
type: integer
|
||||||
|
overallMaxViewerCount:
|
||||||
|
type: integer
|
||||||
|
sessionMaxViewerCount:
|
||||||
|
type: integer
|
||||||
|
lastConnectTime:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
/api/emoji:
|
||||||
|
get:
|
||||||
|
summary: Get Custom Emoji
|
||||||
|
description: Get a list of custom emoji that are supported in chat.
|
||||||
|
tags: ["Chat"]
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the Emoji
|
||||||
|
emoji:
|
||||||
|
type: string
|
||||||
|
description: The relative path to the Emoji image file
|
||||||
|
examples:
|
||||||
|
default:
|
||||||
|
value:
|
||||||
|
items:
|
||||||
|
- name: nicolas_cage_party
|
||||||
|
emoji: /img/emoji/nicolas_cage_party.gif
|
||||||
|
- name: parrot
|
||||||
|
emoji: /img/emoji/parrot.gif
|
||||||
|
|
||||||
|
/api/admin/broadcaster:
|
||||||
|
get:
|
||||||
|
summary: "Broadcaster Details"
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Connected Broadcaster Details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
broadcaster:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
remoteAddr:
|
||||||
|
type: string
|
||||||
|
time:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
streamDetails:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
width:
|
||||||
|
type: integer
|
||||||
|
height:
|
||||||
|
type: integer
|
||||||
|
frameRate:
|
||||||
|
type: integer
|
||||||
|
videoBitrate:
|
||||||
|
type: integer
|
||||||
|
videoCodec:
|
||||||
|
type: string
|
||||||
|
audioBitrate:
|
||||||
|
type: integer
|
||||||
|
audioCodec:
|
||||||
|
type: string
|
||||||
|
encoder:
|
||||||
|
type: string
|
||||||
|
examples:
|
||||||
|
connected:
|
||||||
|
summary: "Broadcaster Connected"
|
||||||
|
value:
|
||||||
|
success: true
|
||||||
|
message: ""
|
||||||
|
broadcaster:
|
||||||
|
remoteAddr: 127.0.0.1
|
||||||
|
time: "TODO"
|
||||||
|
streamDetails:
|
||||||
|
width: 640
|
||||||
|
height: 480
|
||||||
|
frameRate: 24
|
||||||
|
videoBitrate: 1500
|
||||||
|
videoCodec: "todo"
|
||||||
|
audioBitrate: 256
|
||||||
|
audioCodec: "aac"
|
||||||
|
encoder: "todo"
|
||||||
|
not-connected:
|
||||||
|
summary: "Broadcaster Not Connected"
|
||||||
|
value:
|
||||||
|
success: false
|
||||||
|
message: "no broadcaster connected"
|
||||||
|
|
||||||
|
/api/admin/disconnect:
|
||||||
|
post:
|
||||||
|
summary: Disconnect Broadcaster
|
||||||
|
description: Disconnect the active inbound stream, if one exists, and terminate the broadcast.
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: "#/components/responses/BasicResponse"
|
||||||
|
|
||||||
|
|
||||||
|
/api/admin/changekey:
|
||||||
|
post:
|
||||||
|
summary: Update Stream Key
|
||||||
|
description: Change the stream key in memory, but not in the config file. This will require all broadcasters to be reconfigured to connect again.
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
requestBody:
|
||||||
|
description: ""
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Stream was disconnected.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: changed
|
||||||
|
|
||||||
|
/api/admin/serverconfig:
|
||||||
|
get:
|
||||||
|
summary: Server Configuration
|
||||||
|
description: Get the current configuration of the Owncast server.
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
instanceDetails:
|
||||||
|
$ref: "#/components/schemas/InstanceDetails"
|
||||||
|
ffmpegPath:
|
||||||
|
type: string
|
||||||
|
webServerPort:
|
||||||
|
type: integer
|
||||||
|
s3:
|
||||||
|
$ref: "#/components/schemas/S3"
|
||||||
|
videoSettings:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
videoQualityVariants:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/StreamQuality"
|
||||||
|
segmentLengthSeconds:
|
||||||
|
type: integer
|
||||||
|
numberOfPlaylistItems:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
/api/admin/viewersOverTime:
|
||||||
|
get:
|
||||||
|
summary: Viewers Over Time
|
||||||
|
description: Get the tracked viewer count over the collected period.
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TimestampedValue"
|
||||||
|
examples:
|
||||||
|
default:
|
||||||
|
value:
|
||||||
|
- time: "2020-10-03T21:41:00.381996-05:00"
|
||||||
|
value: 50
|
||||||
|
- time: "2020-10-03T21:42:00.381996-05:00"
|
||||||
|
value: 52
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/api/admin/hardwarestats:
|
||||||
|
get:
|
||||||
|
summary: Hardware Stats
|
||||||
|
description: "Get the CPU, Memory and Disk utilization levels over the collected period."
|
||||||
|
tags: ["Admin"]
|
||||||
|
security:
|
||||||
|
- AdminBasicAuth: []
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cpu:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TimestampedValue"
|
||||||
|
memory:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TimestampedValue"
|
||||||
|
disk:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/TimestampedValue"
|
||||||
|
examples:
|
||||||
|
default:
|
||||||
|
value:
|
||||||
|
cpu:
|
||||||
|
- time: "2020-10-03T21:41:00.381996-05:00"
|
||||||
|
value: 23
|
||||||
|
- time: "2020-10-03T21:42:00.381996-05:00"
|
||||||
|
value: 27
|
||||||
|
- time: "2020-10-03T21:43:00.381996-05:00"
|
||||||
|
value: 22
|
||||||
|
memory:
|
||||||
|
- time: "2020-10-03T21:41:00.381996-05:00"
|
||||||
|
value: 65
|
||||||
|
- time: "2020-10-03T21:42:00.381996-05:00"
|
||||||
|
value: 66
|
||||||
|
- time: "2020-10-03T21:43:00.381996-05:00"
|
||||||
|
value: 72
|
||||||
|
disk:
|
||||||
|
- time: "2020-10-03T21:41:00.381996-05:00"
|
||||||
|
value: 11
|
||||||
|
- time: "2020-10-03T21:42:00.381996-05:00"
|
||||||
|
value: 11
|
||||||
|
- time: "2020-10-03T21:43:00.381996-05:00"
|
||||||
|
value: 11
|
||||||
47
router/middleware/auth.go
Normal file
47
router/middleware/auth.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequireAdminAuth wraps a handler requiring HTTP basic auth for it using the given
|
||||||
|
// the stream key as the password and and a hardcoded "admin" for username.
|
||||||
|
func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
|
||||||
|
username := "admin"
|
||||||
|
password := config.Config.VideoSettings.StreamingKey
|
||||||
|
realm := "Owncast Authenticated Request"
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// The following line is kind of a work around.
|
||||||
|
// If you want HTTP Basic Auth + Cors it requires _explicit_ origins to be provided in the
|
||||||
|
// Access-Control-Allow-Origin header. So we just pull out the origin header and specify it.
|
||||||
|
// If we want to lock down admin APIs to not be CORS accessible for anywhere, this is where we would do that.
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||||
|
|
||||||
|
// For request needing CORS, send a 200.
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
|
||||||
|
// Failed
|
||||||
|
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
log.Warnln("Failed authentication for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
log.Traceln("Authenticated request OK for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent())
|
||||||
|
handler(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,5 +60,5 @@ func getCacheDurationSecondsForPath(filePath string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default cache length in seconds
|
// Default cache length in seconds
|
||||||
return 30 * 60
|
return 30
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gabek/owncast/config"
|
"github.com/owncast/owncast/config"
|
||||||
"github.com/gabek/owncast/controllers"
|
"github.com/owncast/owncast/controllers"
|
||||||
"github.com/gabek/owncast/core/chat"
|
"github.com/owncast/owncast/controllers/admin"
|
||||||
"github.com/gabek/owncast/core/rtmp"
|
|
||||||
|
"github.com/owncast/owncast/core/chat"
|
||||||
|
"github.com/owncast/owncast/core/rtmp"
|
||||||
|
"github.com/owncast/owncast/router/middleware"
|
||||||
|
"github.com/owncast/owncast/yp"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Start starts the router for the http, ws, and rtmp
|
//Start starts the router for the http, ws, and rtmp
|
||||||
@@ -26,7 +30,6 @@ func Start() error {
|
|||||||
// custom emoji supported in the chat
|
// custom emoji supported in the chat
|
||||||
http.HandleFunc("/api/emoji", controllers.GetCustomEmoji)
|
http.HandleFunc("/api/emoji", controllers.GetCustomEmoji)
|
||||||
|
|
||||||
if !config.Config.DisableWebFeatures {
|
|
||||||
// websocket chat server
|
// websocket chat server
|
||||||
go chat.Start()
|
go chat.Start()
|
||||||
|
|
||||||
@@ -41,7 +44,28 @@ func Start() error {
|
|||||||
|
|
||||||
// video embed
|
// video embed
|
||||||
http.HandleFunc("/embed/video", controllers.GetVideoEmbed)
|
http.HandleFunc("/embed/video", controllers.GetVideoEmbed)
|
||||||
}
|
|
||||||
|
http.HandleFunc("/api/yp", yp.GetYPResponse)
|
||||||
|
|
||||||
|
// Authenticated admin requests
|
||||||
|
|
||||||
|
// Current inbound broadcaster
|
||||||
|
http.HandleFunc("/api/admin/broadcaster", middleware.RequireAdminAuth(admin.GetInboundBroadasterDetails))
|
||||||
|
|
||||||
|
// Disconnect inbound stream
|
||||||
|
http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection))
|
||||||
|
|
||||||
|
// Change the current streaming key in memory
|
||||||
|
http.HandleFunc("/api/admin/changekey", middleware.RequireAdminAuth(admin.ChangeStreamKey))
|
||||||
|
|
||||||
|
// Server config
|
||||||
|
http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig))
|
||||||
|
|
||||||
|
// Get viewer count over time
|
||||||
|
http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime))
|
||||||
|
|
||||||
|
// Get hardware stats
|
||||||
|
http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats))
|
||||||
|
|
||||||
port := config.Config.GetPublicWebServerPort()
|
port := config.Config.GetPublicWebServerPort()
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ build() {
|
|||||||
|
|
||||||
pushd dist/${NAME} >> /dev/null
|
pushd dist/${NAME} >> /dev/null
|
||||||
|
|
||||||
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.BuildVersion=${VERSION} -X main.BuildType=${NAME}" -targets "${OS}/${ARCH}" github.com/gabek/owncast
|
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.BuildVersion=${VERSION} -X main.BuildType=${NAME}" -targets "${OS}/${ARCH}" github.com/owncast/owncast
|
||||||
mv owncast-*-${ARCH} owncast
|
mv owncast-*-${ARCH} owncast
|
||||||
|
|
||||||
zip -r -q -8 ../owncast-$NAME-$VERSION.zip .
|
zip -r -q -8 ../owncast-$NAME-$VERSION.zip .
|
||||||
@@ -76,7 +76,7 @@ git tag -a "v${VERSION}" -m "Release build v${VERSION}"
|
|||||||
|
|
||||||
# On macOS open the Github page for new releases so they can be uploaded
|
# On macOS open the Github page for new releases so they can be uploaded
|
||||||
if test -f "/usr/bin/open"; then
|
if test -f "/usr/bin/open"; then
|
||||||
open "https://github.com/gabek/owncast/releases/new"
|
open "https://github.com/owncast/owncast/releases/new"
|
||||||
open dist
|
open dist
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -90,8 +90,8 @@ cd $(git rev-parse --show-toplevel)
|
|||||||
|
|
||||||
# Github Packages
|
# Github Packages
|
||||||
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t owncast . -f scripts/Dockerfile-build
|
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t owncast . -f scripts/Dockerfile-build
|
||||||
docker tag $DOCKER_IMAGE docker.pkg.github.com/gabek/owncast/$DOCKER_IMAGE:$VERSION
|
docker tag $DOCKER_IMAGE docker.pkg.github.com/owncast/owncast/$DOCKER_IMAGE:$VERSION
|
||||||
docker push docker.pkg.github.com/gabek/owncast/$DOCKER_IMAGE:$VERSION
|
docker push docker.pkg.github.com/owncast/owncast/$DOCKER_IMAGE:$VERSION
|
||||||
#
|
#
|
||||||
# Dockerhub
|
# Dockerhub
|
||||||
# You must be authenticated via `docker login` with your Dockerhub credentials first.
|
# You must be authenticated via `docker login` with your Dockerhub credentials first.
|
||||||
|
|||||||
13
scripts/bundleAdmin.sh
Executable file
13
scripts/bundleAdmin.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
PROJECT_SOURCE_DIR=$(pwd)
|
||||||
|
mkdir $TMPDIR/admin 2> /dev/null
|
||||||
|
cd $TMPDIR/admin
|
||||||
|
git clone --depth 1 https://github.com/owncast/owncast-admin 2> /dev/null
|
||||||
|
cd owncast-admin
|
||||||
|
npm --silent install 2> /dev/null
|
||||||
|
(node_modules/.bin/next build && node_modules/.bin/next export) | grep info
|
||||||
|
ADMIN_BUILD_DIR=$(pwd)
|
||||||
|
cd $PROJECT_SOURCE_DIR
|
||||||
|
mkdir webroot/admin 2> /dev/null
|
||||||
|
cd webroot/admin
|
||||||
|
cp -R ${ADMIN_BUILD_DIR}/out/* .
|
||||||
|
rm -rf $TMPDIR/admin
|
||||||
@@ -3,20 +3,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
||||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
<link href="./styles/chat.css" rel="stylesheet" />
|
<link href="./styles/chat.css" rel="stylesheet" />
|
||||||
<link href="./styles/standalone-chat.css" rel="stylesheet" />
|
<link href="./styles/standalone-chat.css" rel="stylesheet" />
|
||||||
|
|
||||||
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
|
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js"></script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="messages-only"></div>
|
<div id="messages-only"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
|
import { h, render } from '/js/web_modules/preact.js';
|
||||||
|
import htm from '/js/web_modules/htm.js';
|
||||||
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import StandaloneChat from './js/app-standalone-chat.js';
|
import StandaloneChat from './js/app-standalone-chat.js';
|
||||||
render(
|
render(
|
||||||
html`<${StandaloneChat} messagesOnly />`, document.getElementById("messages-only")
|
html`<${StandaloneChat} messagesOnly />`, document.getElementById("messages-only")
|
||||||
|
|||||||
@@ -3,17 +3,13 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
||||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
|
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="//unpkg.com/video.js@7.9.3/dist/video-js.css" rel="stylesheet"/>
|
<link href="/js/web_modules/videojs/dist/video-js.min.css" rel="stylesheet"/>
|
||||||
<link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet" />
|
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
|
||||||
<script src="//unpkg.com/video.js@7.9.3/dist/alt/video.core.min.js"></script>
|
|
||||||
<script src="//unpkg.com/@videojs/http-streaming@2.1.0/dist/videojs-http-streaming.min.js"></script>
|
|
||||||
|
|
||||||
<link href="./styles/video.css" rel="stylesheet" />
|
<link href="./styles/video.css" rel="stylesheet" />
|
||||||
<link href="./styles/video-only.css" rel="stylesheet" />
|
<link href="./styles/video-only.css" rel="stylesheet" />
|
||||||
|
|
||||||
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -21,7 +17,10 @@
|
|||||||
<div id="video-only"></div>
|
<div id="video-only"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
|
import { h, render } from '/js/web_modules/preact.js';
|
||||||
|
import htm from '/js/web_modules/htm.js';
|
||||||
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import VideoOnly from './js/app-video-only.js';
|
import VideoOnly from './js/app-video-only.js';
|
||||||
render(html`<${VideoOnly} />`, document.getElementById("video-only"));
|
render(html`<${VideoOnly} />`, document.getElementById("video-only"));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<title>Owncast</title>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
||||||
|
|
||||||
@@ -23,35 +24,25 @@
|
|||||||
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
|
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"/>
|
<link href="/js/web_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="//unpkg.com/video.js@7.9.3/dist/video-js.css" rel="stylesheet" />
|
|
||||||
<link href="https://unpkg.com/@videojs/themes@1/dist/fantasy/index.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<script src="//unpkg.com/video.js@7.9.3/dist/alt/video.core.min.js" defer></script>
|
|
||||||
<script src="//unpkg.com/@videojs/http-streaming@2.1.0/dist/videojs-http-streaming.min.js" defer></script>
|
|
||||||
|
|
||||||
<!-- markdown renderer -->
|
|
||||||
<script src="//unpkg.com/showdown/dist/showdown.min.js" defer></script>
|
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@0.6.2/lite-youtube.js" defer></script>
|
|
||||||
|
|
||||||
|
<link href="/js/web_modules/videojs/dist/video-js.min.css" rel="stylesheet"/>
|
||||||
|
<link href="/js/web_modules/@videojs/themes/fantasy/index.css" rel="stylesheet" />
|
||||||
|
|
||||||
<link href="./styles/video.css" rel="stylesheet" />
|
<link href="./styles/video.css" rel="stylesheet" />
|
||||||
<link href="./styles/chat.css" rel="stylesheet" />
|
<link href="./styles/chat.css" rel="stylesheet" />
|
||||||
<link href="./styles/user-content.css" rel="stylesheet" />
|
<link href="./styles/user-content.css" rel="stylesheet" />
|
||||||
<link href="./styles/app.css" rel="stylesheet" />
|
<link href="./styles/app.css" rel="stylesheet" />
|
||||||
|
|
||||||
<!-- Preloads -->
|
|
||||||
<link rel="preconnect" href="https://unpkg.com/@joeattardi/emoji-button@4.2.0/dist/index.js" />
|
|
||||||
<link rel="preconnect" href="https://unpkg.com/preact?module" />
|
|
||||||
<link rel="preconnect" href="https://unpkg.com/htm?module" />
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
|
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { render, html } from 'https://unpkg.com/htm/preact/standalone.module.js';
|
import { h, render } from '/js/web_modules/preact.js';
|
||||||
|
import htm from '/js/web_modules/htm.js';
|
||||||
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import App from './js/app.js';
|
import App from './js/app.js';
|
||||||
render(html`<${App} />`, document.getElementById("app"));
|
render(html`<${App} />`, document.getElementById("app"));
|
||||||
</script>
|
</script>
|
||||||
@@ -77,7 +68,7 @@
|
|||||||
<img src="https://owncast.online/images/logo.png" />
|
<img src="https://owncast.online/images/logo.png" />
|
||||||
<br/>
|
<br/>
|
||||||
<p>
|
<p>
|
||||||
This <a href="https://owncast.online" target="_blank">Owncast</a> stream requires Javascript to play.
|
This <a href="https://owncast.online" rel="noopener noreferrer" target="_blank">Owncast</a> stream requires Javascript to play.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { h, Component } from 'https://unpkg.com/preact?module';
|
import { h, Component } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import Chat from './components/chat/chat.js';
|
import Chat from './components/chat/chat.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { h, Component } from 'https://unpkg.com/preact?module';
|
import { h, Component } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import { OwncastPlayer } from './components/player.js';
|
import { OwncastPlayer } from './components/player.js';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { h, Component } from 'https://unpkg.com/preact?module';
|
import { h, Component } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
import showdown from '/js/web_modules/showdown.js';
|
||||||
|
|
||||||
import { OwncastPlayer } from './components/player.js';
|
import { OwncastPlayer } from './components/player.js';
|
||||||
import SocialIcon from './components/social.js';
|
import SocialIcon from './components/social.js';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
|
import { h, Component, createRef } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import { EmojiButton } from 'https://unpkg.com/@joeattardi/emoji-button@4.2.0/dist/index.js';
|
import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js';
|
||||||
|
|
||||||
import ContentEditable, { replaceCaret } from './content-editable.js';
|
import ContentEditable, { replaceCaret } from './content-editable.js';
|
||||||
import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
|
import { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
|
||||||
import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js';
|
import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
|
import { h, Component, createRef } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import Message from './message.js';
|
import Message from './message.js';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ and here:
|
|||||||
https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103
|
https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103
|
||||||
|
|
||||||
*/
|
*/
|
||||||
import { Component, createRef, h } from 'https://unpkg.com/preact?module';
|
import { h, Component, createRef } from '/js/web_modules/preact.js';
|
||||||
|
|
||||||
export function replaceCaret(el) {
|
export function replaceCaret(el) {
|
||||||
// Place the caret at the end of the element
|
// Place the caret at the end of the element
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { h, Component } from 'https://unpkg.com/preact?module';
|
import { h, Component } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import { messageBubbleColorForString } from '../../utils/user-colors.js';
|
import { messageBubbleColorForString } from '../../utils/user-colors.js';
|
||||||
import { formatMessageText } from '../../utils/chat.js';
|
import { formatMessageText, formatTimestamp } from '../../utils/chat.js';
|
||||||
import { generateAvatar } from '../../utils/helpers.js';
|
import { generateAvatar } from '../../utils/helpers.js';
|
||||||
import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
|
import { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
|
||||||
|
|
||||||
@@ -13,9 +13,10 @@ export default class Message extends Component {
|
|||||||
const { type } = message;
|
const { type } = message;
|
||||||
|
|
||||||
if (type === SOCKET_MESSAGE_TYPES.CHAT) {
|
if (type === SOCKET_MESSAGE_TYPES.CHAT) {
|
||||||
const { image, author, body } = message;
|
const { image, author, body, timestamp } = message;
|
||||||
const formattedMessage = formatMessageText(body, username);
|
const formattedMessage = formatMessageText(body, username);
|
||||||
const avatar = image || generateAvatar(author);
|
const avatar = image || generateAvatar(author);
|
||||||
|
const formattedTimestamp = formatTimestamp(timestamp);
|
||||||
|
|
||||||
const authorColor = messageBubbleColorForString(author);
|
const authorColor = messageBubbleColorForString(author);
|
||||||
const avatarBgColor = { backgroundColor: authorColor };
|
const avatarBgColor = { backgroundColor: authorColor };
|
||||||
@@ -35,6 +36,7 @@ export default class Message extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="message-text text-gray-300 font-normal overflow-y-hidden"
|
class="message-text text-gray-300 font-normal overflow-y-hidden"
|
||||||
|
title=${formattedTimestamp}
|
||||||
dangerouslySetInnerHTML=${
|
dangerouslySetInnerHTML=${
|
||||||
{ __html: formattedMessage }
|
{ __html: formattedMessage }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
|
import { h, Component, createRef } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
import { generateAvatar, setLocalStorage } from '../../utils/helpers.js';
|
import { generateAvatar, setLocalStorage } from '../../utils/helpers.js';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// https://docs.videojs.com/player
|
// https://docs.videojs.com/player
|
||||||
|
|
||||||
|
import videojs from '/js/web_modules/videojs/dist/video.min.js';
|
||||||
|
|
||||||
const VIDEO_ID = 'video';
|
const VIDEO_ID = 'video';
|
||||||
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
||||||
const URL_STREAM = `/hls/stream.m3u8`;
|
const URL_STREAM = `/hls/stream.m3u8`;
|
||||||
@@ -53,13 +55,14 @@ class OwncastPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
videojs.Vhs.xhr.beforeRequest = function (options) {
|
this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
|
||||||
|
|
||||||
|
this.vjsPlayer.beforeRequest = function (options) {
|
||||||
const cachebuster = Math.round(new Date().getTime() / 1000);
|
const cachebuster = Math.round(new Date().getTime() / 1000);
|
||||||
options.uri = `${options.uri}?cachebust=${cachebuster}`;
|
options.uri = `${options.uri}?cachebust=${cachebuster}`;
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
|
|
||||||
this.addAirplay();
|
this.addAirplay();
|
||||||
this.vjsPlayer.ready(this.handleReady);
|
this.vjsPlayer.ready(this.handleReady);
|
||||||
}
|
}
|
||||||
@@ -76,11 +79,11 @@ class OwncastPlayer {
|
|||||||
// play
|
// play
|
||||||
startPlayer() {
|
startPlayer() {
|
||||||
this.log('Start playing');
|
this.log('Start playing');
|
||||||
const source = { ...VIDEO_SRC }
|
const source = { ...VIDEO_SRC };
|
||||||
this.vjsPlayer.volume(localStorage.getItem('owncastVolume'));
|
this.vjsPlayer.volume(localStorage.getItem('owncastVolume'));
|
||||||
this.vjsPlayer.src(source);
|
this.vjsPlayer.src(source);
|
||||||
// this.vjsPlayer.play();
|
// this.vjsPlayer.play();
|
||||||
};
|
}
|
||||||
|
|
||||||
handleReady() {
|
handleReady() {
|
||||||
this.log('on Ready');
|
this.log('on Ready');
|
||||||
@@ -124,7 +127,7 @@ class OwncastPlayer {
|
|||||||
|
|
||||||
setPoster() {
|
setPoster() {
|
||||||
const cachebuster = Math.round(new Date().getTime() / 1000);
|
const cachebuster = Math.round(new Date().getTime() / 1000);
|
||||||
const poster = POSTER_THUMB + "?okhi=" + cachebuster;
|
const poster = POSTER_THUMB + '?okhi=' + cachebuster;
|
||||||
|
|
||||||
this.vjsPlayer.poster(poster);
|
this.vjsPlayer.poster(poster);
|
||||||
}
|
}
|
||||||
@@ -138,7 +141,6 @@ class OwncastPlayer {
|
|||||||
if (window.WebKitPlaybackTargetAvailabilityEvent) {
|
if (window.WebKitPlaybackTargetAvailabilityEvent) {
|
||||||
var videoJsButtonClass = videojs.getComponent('Button');
|
var videoJsButtonClass = videojs.getComponent('Button');
|
||||||
var concreteButtonClass = videojs.extend(videoJsButtonClass, {
|
var concreteButtonClass = videojs.extend(videoJsButtonClass, {
|
||||||
|
|
||||||
// The `init()` method will also work for constructor logic here, but it is
|
// The `init()` method will also work for constructor logic here, but it is
|
||||||
// deprecated. If you provide an `init()` method, it will override the
|
// deprecated. If you provide an `init()` method, it will override the
|
||||||
// `constructor()` method!
|
// `constructor()` method!
|
||||||
@@ -152,8 +154,10 @@ class OwncastPlayer {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass());
|
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(
|
||||||
concreteButtonInstance.addClass("vjs-airplay");
|
new concreteButtonClass()
|
||||||
|
);
|
||||||
|
concreteButtonInstance.addClass('vjs-airplay');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { h } from 'https://unpkg.com/preact?module';
|
import { h } from '/js/web_modules/preact.js';
|
||||||
import htm from 'https://unpkg.com/htm?module';
|
import htm from '/js/web_modules/htm.js';
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
import { SOCIAL_PLATFORMS } from '../utils/social.js';
|
import { SOCIAL_PLATFORMS } from '../utils/social.js';
|
||||||
import { classNames } from '../utils/helpers.js';
|
import { classNames } from '../utils/helpers.js';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CHAT_PLACEHOLDER_OFFLINE,
|
CHAT_PLACEHOLDER_OFFLINE,
|
||||||
} from './constants.js';
|
} from './constants.js';
|
||||||
|
|
||||||
|
import showdown from '/js/web_modules/showdown.js';
|
||||||
export function formatMessageText(message, username) {
|
export function formatMessageText(message, username) {
|
||||||
showdown.setFlavor('github');
|
showdown.setFlavor('github');
|
||||||
let formattedText = new showdown.Converter({
|
let formattedText = new showdown.Converter({
|
||||||
@@ -278,3 +279,18 @@ export function convertOnPaste( event = { preventDefault() {} }) {
|
|||||||
document.execCommand('insertText', false, value);
|
document.execCommand('insertText', false, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatTimestamp(sentAt) {
|
||||||
|
sentAt = new Date(sentAt);
|
||||||
|
if (isNaN(sentAt)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffInDays = ((new Date()) - sentAt) / (24 * 3600 * 1000);
|
||||||
|
if (diffInDays >= 1) {
|
||||||
|
return `Sent at ${sentAt.toLocaleDateString('en-US', {dateStyle: 'medium'})} at ` +
|
||||||
|
sentAt.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Sent at ${sentAt.toLocaleTimeString()}`;
|
||||||
|
}
|
||||||
|
|||||||
3
webroot/js/web_modules/@joeattardi/emoji-button.js
Normal file
3
webroot/js/web_modules/@joeattardi/emoji-button.js
Normal file
File diff suppressed because one or more lines are too long
301
webroot/js/web_modules/@justinribeiro/lite-youtube.js
Normal file
301
webroot/js/web_modules/@justinribeiro/lite-youtube.js
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* The shadowDom / Intersection Observer version of Paul's concept:
|
||||||
|
* https://github.com/paulirish/lite-youtube-embed
|
||||||
|
*
|
||||||
|
* A lightweight YouTube embed. Still should feel the same to the user, just
|
||||||
|
* MUCH faster to initialize and paint.
|
||||||
|
*
|
||||||
|
* Thx to these as the inspiration
|
||||||
|
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
|
||||||
|
* https://autoplay-youtube-player.glitch.me/
|
||||||
|
*
|
||||||
|
* Once built it, I also found these (👍👍):
|
||||||
|
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube
|
||||||
|
* https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe
|
||||||
|
*/
|
||||||
|
class LiteYTEmbed extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.iframeLoaded = false;
|
||||||
|
this.setupDom();
|
||||||
|
}
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['videoid'];
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
this.addEventListener('click', () => this.addIframe());
|
||||||
|
}
|
||||||
|
get videoId() {
|
||||||
|
return encodeURIComponent(this.getAttribute('videoid') || '');
|
||||||
|
}
|
||||||
|
set videoId(id) {
|
||||||
|
this.setAttribute('videoid', id);
|
||||||
|
}
|
||||||
|
get videoTitle() {
|
||||||
|
return this.getAttribute('videotitle') || 'Video';
|
||||||
|
}
|
||||||
|
set videoTitle(title) {
|
||||||
|
this.setAttribute('videotitle', title);
|
||||||
|
}
|
||||||
|
get videoPlay() {
|
||||||
|
return this.getAttribute('videoPlay') || 'Play';
|
||||||
|
}
|
||||||
|
set videoPlay(name) {
|
||||||
|
this.setAttribute('videoPlay', name);
|
||||||
|
}
|
||||||
|
get videoStartAt() {
|
||||||
|
return Number(this.getAttribute('videoStartAt') || '0');
|
||||||
|
}
|
||||||
|
set videoStartAt(time) {
|
||||||
|
this.setAttribute('videoStartAt', String(time));
|
||||||
|
}
|
||||||
|
get autoLoad() {
|
||||||
|
return this.hasAttribute('autoload');
|
||||||
|
}
|
||||||
|
set autoLoad(value) {
|
||||||
|
if (value) {
|
||||||
|
this.setAttribute('autoload', '');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.removeAttribute('autoload');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get params() {
|
||||||
|
return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Define our shadowDOM for the component
|
||||||
|
*/
|
||||||
|
setupDom() {
|
||||||
|
const shadowDom = this.attachShadow({ mode: 'open' });
|
||||||
|
shadowDom.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
contain: content;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: calc(100% / (16 / 9));
|
||||||
|
}
|
||||||
|
|
||||||
|
#frame, #fallbackPlaceholder, iframe {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frame {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fallbackPlaceholder {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frame::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
|
||||||
|
background-position: top;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
height: 60px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
/* play button */
|
||||||
|
.lty-playbtn {
|
||||||
|
width: 70px;
|
||||||
|
height: 46px;
|
||||||
|
background-color: #212121;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */
|
||||||
|
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
#frame:hover .lty-playbtn {
|
||||||
|
background-color: #f00;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
/* play button triangle */
|
||||||
|
.lty-playbtn:before {
|
||||||
|
content: '';
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 11px 0 11px 19px;
|
||||||
|
border-color: transparent transparent transparent #fff;
|
||||||
|
}
|
||||||
|
.lty-playbtn,
|
||||||
|
.lty-playbtn:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate3d(-50%, -50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post-click styles */
|
||||||
|
.lyt-activated {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frame.lyt-activated::before,
|
||||||
|
.lyt-activated .lty-playbtn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="frame">
|
||||||
|
<picture>
|
||||||
|
<source id="webpPlaceholder" type="image/webp">
|
||||||
|
<source id="jpegPlaceholder" type="image/jpeg">
|
||||||
|
<img id="fallbackPlaceholder" referrerpolicy="origin">
|
||||||
|
</picture>
|
||||||
|
<button class="lty-playbtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.domRefFrame = this.shadowRoot.querySelector('#frame');
|
||||||
|
this.domRefImg = {
|
||||||
|
fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'),
|
||||||
|
webp: this.shadowRoot.querySelector('#webpPlaceholder'),
|
||||||
|
jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'),
|
||||||
|
};
|
||||||
|
this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Parse our attributes and fire up some placeholders
|
||||||
|
*/
|
||||||
|
setupComponent() {
|
||||||
|
this.initImagePlaceholder();
|
||||||
|
this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
|
||||||
|
this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);
|
||||||
|
if (this.autoLoad) {
|
||||||
|
this.initIntersectionObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Lifecycle method that we use to listen for attribute changes to period
|
||||||
|
* @param {*} name
|
||||||
|
* @param {*} oldVal
|
||||||
|
* @param {*} newVal
|
||||||
|
*/
|
||||||
|
attributeChangedCallback(name, oldVal, newVal) {
|
||||||
|
switch (name) {
|
||||||
|
case 'videoid': {
|
||||||
|
if (oldVal !== newVal) {
|
||||||
|
this.setupComponent();
|
||||||
|
// if we have a previous iframe, remove it and the activated class
|
||||||
|
if (this.domRefFrame.classList.contains('lyt-activated')) {
|
||||||
|
this.domRefFrame.classList.remove('lyt-activated');
|
||||||
|
this.shadowRoot.querySelector('iframe').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Inject the iframe into the component body
|
||||||
|
*/
|
||||||
|
addIframe() {
|
||||||
|
if (!this.iframeLoaded) {
|
||||||
|
const iframeHTML = `
|
||||||
|
<iframe frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
|
||||||
|
src="https://www.youtube.com/embed/${this.videoId}?autoplay=1&${this.params}"
|
||||||
|
></iframe>`;
|
||||||
|
this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
|
||||||
|
this.domRefFrame.classList.add('lyt-activated');
|
||||||
|
this.iframeLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Setup the placeholder image for the component
|
||||||
|
*/
|
||||||
|
initImagePlaceholder() {
|
||||||
|
// we don't know which image type to preload, so warm the connection
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');
|
||||||
|
const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`;
|
||||||
|
const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
|
||||||
|
this.domRefImg.webp.srcset = posterUrlWebp;
|
||||||
|
this.domRefImg.jpeg.srcset = posterUrlJpeg;
|
||||||
|
this.domRefImg.fallback.src = posterUrlJpeg;
|
||||||
|
this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
|
||||||
|
this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Setup the Intersection Observer to load the iframe when scrolled into view
|
||||||
|
*/
|
||||||
|
initIntersectionObserver() {
|
||||||
|
if ('IntersectionObserver' in window &&
|
||||||
|
'IntersectionObserverEntry' in window) {
|
||||||
|
const options = {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 0,
|
||||||
|
};
|
||||||
|
const observer = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting && !this.iframeLoaded) {
|
||||||
|
LiteYTEmbed.warmConnections();
|
||||||
|
this.addIframe();
|
||||||
|
observer.unobserve(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, options);
|
||||||
|
observer.observe(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a <link rel={preload | preconnect} ...> to the head
|
||||||
|
* @param {*} kind
|
||||||
|
* @param {*} url
|
||||||
|
* @param {*} as
|
||||||
|
*/
|
||||||
|
static addPrefetch(kind, url, as) {
|
||||||
|
const linkElem = document.createElement('link');
|
||||||
|
linkElem.rel = kind;
|
||||||
|
linkElem.href = url;
|
||||||
|
if (as) {
|
||||||
|
linkElem.as = as;
|
||||||
|
}
|
||||||
|
linkElem.crossOrigin = 'true';
|
||||||
|
document.head.append(linkElem);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Begin preconnecting to warm up the iframe load Since the embed's netwok
|
||||||
|
* requests load within its iframe, preload/prefetch'ing them outside the
|
||||||
|
* iframe will only cause double-downloads. So, the best we can do is warm up
|
||||||
|
* a few connections to origins that are in the critical path.
|
||||||
|
*
|
||||||
|
* Maybe `<link rel=preload as=document>` would work, but it's unsupported:
|
||||||
|
* http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site
|
||||||
|
* Isolation and split caches adding serious complexity.
|
||||||
|
*/
|
||||||
|
static warmConnections() {
|
||||||
|
if (LiteYTEmbed.preconnected)
|
||||||
|
return;
|
||||||
|
// Host that YT uses to serve JS needed by player, per amp-youtube
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');
|
||||||
|
// The iframe document and most of its subresources come right off
|
||||||
|
// youtube.com
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');
|
||||||
|
// The botguard script is fetched off from google.com
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
|
||||||
|
// TODO: Not certain if these ad related domains are in the critical path.
|
||||||
|
// Could verify with domain-specific throttling.
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
|
||||||
|
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
|
||||||
|
LiteYTEmbed.preconnected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LiteYTEmbed.preconnected = false;
|
||||||
|
// Register custom element
|
||||||
|
customElements.define('lite-youtube', LiteYTEmbed);
|
||||||
|
|
||||||
|
export { LiteYTEmbed };
|
||||||
58119
webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js
vendored
Normal file
58119
webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
116
webroot/js/web_modules/@videojs/themes/fantasy/index.css
Normal file
116
webroot/js/web_modules/@videojs/themes/fantasy/index.css
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
.vjs-theme-fantasy {
|
||||||
|
--vjs-theme-fantasy--primary: #9f44b4;
|
||||||
|
--vjs-theme-fantasy--secondary: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-big-play-button {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background: none;
|
||||||
|
line-height: 70px;
|
||||||
|
font-size: 80px;
|
||||||
|
border: none;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -35px;
|
||||||
|
margin-left: -35px;
|
||||||
|
color: var(--vjs-theme-fantasy--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy:hover .vjs-big-play-button,
|
||||||
|
.vjs-theme-fantasy.vjs-big-play-button:focus {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-control-bar {
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-button > .vjs-icon-placeholder::before {
|
||||||
|
line-height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-time-control {
|
||||||
|
line-height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Play Button */
|
||||||
|
.vjs-theme-fantasy .vjs-play-control {
|
||||||
|
font-size: 1.5em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-volume-panel {
|
||||||
|
order: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-volume-bar {
|
||||||
|
margin-top: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-city .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-progress-control .vjs-progress-holder {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-progress-control:hover .vjs-progress-holder {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-play-control .vjs-icon-placeholder::before {
|
||||||
|
height: 1.3em;
|
||||||
|
width: 1.3em;
|
||||||
|
margin-top: 0.2em;
|
||||||
|
border-radius: 1em;
|
||||||
|
border: 3px solid var(--vjs-theme-fantasy--secondary);
|
||||||
|
top: 2px;
|
||||||
|
left: 9px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-play-control:hover .vjs-icon-placeholder::before {
|
||||||
|
border: 3px solid var(--vjs-theme-fantasy--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-play-progress {
|
||||||
|
background-color: var(--vjs-theme-fantasy--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-play-progress::before {
|
||||||
|
height: 0.8em;
|
||||||
|
width: 0.8em;
|
||||||
|
content: '';
|
||||||
|
background-color: var(--vjs-theme-fantasy--primary);
|
||||||
|
border: 4px solid var(--vjs-theme-fantasy--secondary);
|
||||||
|
border-radius: 0.8em;
|
||||||
|
top: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-progress-control {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-fullscreen-control {
|
||||||
|
order: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy .vjs-remaining-time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nyan version */
|
||||||
|
.vjs-theme-fantasy.nyan .vjs-play-progress {
|
||||||
|
background: linear-gradient(to bottom, #fe0000 0%, #fe9a01 16.666666667%, #fe9a01 16.666666667%, #ffff00 33.332666667%, #ffff00 33.332666667%, #32ff00 49.999326667%, #32ff00 49.999326667%, #0099fe 66.6659926%, #0099fe 66.6659926%, #6633ff 83.33266%, #6633ff 83.33266%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-theme-fantasy.nyan .vjs-play-progress::before {
|
||||||
|
height: 1.3em;
|
||||||
|
width: 1.3em;
|
||||||
|
background: svg-load('icons/nyan-cat.svg', fill=#fff) no-repeat;
|
||||||
|
border: none;
|
||||||
|
top: -0.35em;
|
||||||
|
}
|
||||||
25
webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js
Normal file
25
webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||||||
|
|
||||||
|
function getDefaultExportFromCjs (x) {
|
||||||
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommonjsModule(fn, basedir, module) {
|
||||||
|
return module = {
|
||||||
|
path: basedir,
|
||||||
|
exports: {},
|
||||||
|
require: function (path, base) {
|
||||||
|
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base);
|
||||||
|
}
|
||||||
|
}, fn(module, module.exports), module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultExportFromNamespaceIfNotNamed (n) {
|
||||||
|
return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commonjsRequire () {
|
||||||
|
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { commonjsGlobal as a, getDefaultExportFromNamespaceIfNotNamed as b, createCommonjsModule as c, getDefaultExportFromCjs as g };
|
||||||
44
webroot/js/web_modules/common/window-2f8a9a85.js
Normal file
44
webroot/js/web_modules/common/window-2f8a9a85.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { b as getDefaultExportFromNamespaceIfNotNamed, a as commonjsGlobal } from './_commonjsHelpers-37fa8da4.js';
|
||||||
|
|
||||||
|
var _nodeResolve_empty = {};
|
||||||
|
|
||||||
|
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
|
||||||
|
__proto__: null,
|
||||||
|
'default': _nodeResolve_empty
|
||||||
|
});
|
||||||
|
|
||||||
|
var minDoc = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(_nodeResolve_empty$1);
|
||||||
|
|
||||||
|
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal :
|
||||||
|
typeof window !== 'undefined' ? window : {};
|
||||||
|
|
||||||
|
|
||||||
|
var doccy;
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
doccy = document;
|
||||||
|
} else {
|
||||||
|
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
|
||||||
|
|
||||||
|
if (!doccy) {
|
||||||
|
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var document_1 = doccy;
|
||||||
|
|
||||||
|
var win;
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
win = window;
|
||||||
|
} else if (typeof commonjsGlobal !== "undefined") {
|
||||||
|
win = commonjsGlobal;
|
||||||
|
} else if (typeof self !== "undefined"){
|
||||||
|
win = self;
|
||||||
|
} else {
|
||||||
|
win = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var window_1 = win;
|
||||||
|
|
||||||
|
export { document_1 as d, window_1 as w };
|
||||||
3
webroot/js/web_modules/htm.js
vendored
Normal file
3
webroot/js/web_modules/htm.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a);}return e},t=new Map;function htm_module(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
|
||||||
|
|
||||||
|
export default htm_module;
|
||||||
14
webroot/js/web_modules/import-map.json
vendored
Normal file
14
webroot/js/web_modules/import-map.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"@joeattardi/emoji-button": "./@joeattardi/emoji-button.js",
|
||||||
|
"@justinribeiro/lite-youtube": "./@justinribeiro/lite-youtube.js",
|
||||||
|
"@videojs/http-streaming/dist/videojs-http-streaming.min.js": "./@videojs/http-streaming/dist/videojs-http-streaming.min.js",
|
||||||
|
"@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css",
|
||||||
|
"htm": "./htm.js",
|
||||||
|
"preact": "./preact.js",
|
||||||
|
"showdown": "./showdown.js",
|
||||||
|
"tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css",
|
||||||
|
"video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css",
|
||||||
|
"video.js/dist/video.min.js": "./videojs/dist/video.min.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
webroot/js/web_modules/preact.js
vendored
Normal file
3
webroot/js/web_modules/preact.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5044
webroot/js/web_modules/showdown.js
vendored
Normal file
5044
webroot/js/web_modules/showdown.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
vendored
Normal file
1
webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webroot/js/web_modules/videojs/dist/video-js.min.css
vendored
Normal file
1
webroot/js/web_modules/videojs/dist/video-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
32
webroot/js/web_modules/videojs/dist/video.min.js
vendored
Normal file
32
webroot/js/web_modules/videojs/dist/video.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ May have overrides for other components with own stylesheets.
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
@@ -24,6 +25,9 @@ a:hover {
|
|||||||
width: 0px;
|
width: 0px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
.scrollbar-hidden {
|
||||||
|
scrollbar-width: none; /* moz only */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#app-container * {
|
#app-container * {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
.emoji-picker.owncast {
|
.emoji-picker.owncast {
|
||||||
--secondary-text-color: rgba(255,255,255,.5);
|
--secondary-text-color: rgba(255,255,255,.5);
|
||||||
--category-button-color: rgba(255,255,255,.5);
|
--category-button-color: rgba(255,255,255,.5);
|
||||||
|
--hover-color: rgba(255,255,255,.25);
|
||||||
|
|
||||||
background: rgba(26,32,44,1); /* tailwind bg-gray-900 */
|
background: rgba(26,32,44,1); /* tailwind bg-gray-900 */
|
||||||
color: rgba(226,232,240,1); /* tailwind text-gray-300 */
|
color: rgba(226,232,240,1); /* tailwind text-gray-300 */
|
||||||
@@ -101,15 +102,19 @@
|
|||||||
}
|
}
|
||||||
.emoji-picker__emojis::-webkit-scrollbar-track {
|
.emoji-picker__emojis::-webkit-scrollbar-track {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: rgba(0,0,0,.2);
|
background-color: black;
|
||||||
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
|
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-picker__emojis::-webkit-scrollbar-thumb {
|
.emoji-picker__emojis::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(0,0,0,.45);
|
background-color: var(--category-button-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-picker__emojis {
|
||||||
|
scrollbar-color: var(--category-button-color) black;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************************/
|
/******************************/
|
||||||
|
|
||||||
|
|||||||
0
yp/README.md
Normal file
0
yp/README.md
Normal file
46
yp/api.go
Normal file
46
yp/api.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package yp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ypDetailsResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Logo string `json:"logo"`
|
||||||
|
NSFW bool `json:"nsfw"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
ViewerCount int `json:"viewerCount"`
|
||||||
|
OverallMaxViewerCount int `json:"overallMaxViewerCount"`
|
||||||
|
SessionMaxViewerCount int `json:"sessionMaxViewerCount"`
|
||||||
|
|
||||||
|
LastConnectTime utils.NullTime `json:"lastConnectTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetYPResponse gets the status of the server for YP purposes
|
||||||
|
func GetYPResponse(w http.ResponseWriter, r *http.Request) {
|
||||||
|
status := getStatus()
|
||||||
|
|
||||||
|
response := ypDetailsResponse{
|
||||||
|
Name: config.Config.InstanceDetails.Name,
|
||||||
|
Description: config.Config.InstanceDetails.Summary,
|
||||||
|
Logo: config.Config.InstanceDetails.Logo.Large,
|
||||||
|
NSFW: config.Config.InstanceDetails.NSFW,
|
||||||
|
Tags: config.Config.InstanceDetails.Tags,
|
||||||
|
Online: status.Online,
|
||||||
|
ViewerCount: status.ViewerCount,
|
||||||
|
OverallMaxViewerCount: status.OverallMaxViewerCount,
|
||||||
|
SessionMaxViewerCount: status.SessionMaxViewerCount,
|
||||||
|
LastConnectTime: status.LastConnectTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
|
||||||
|
}
|
||||||
139
yp/yp.go
Normal file
139
yp/yp.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package yp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/config"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pingInterval = 4 * time.Minute
|
||||||
|
|
||||||
|
var getStatus func() models.Status
|
||||||
|
|
||||||
|
//YP is a service for handling listing in the Owncast directory.
|
||||||
|
type YP struct {
|
||||||
|
timer *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
type ypPingResponse struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorCode int `json:"errorCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ypPingRequest struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewYP creates a new instance of the YP service handler
|
||||||
|
func NewYP(getStatusFunc func() models.Status) *YP {
|
||||||
|
getStatus = getStatusFunc
|
||||||
|
return &YP{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start is run when a live stream begins to start pinging YP
|
||||||
|
func (yp *YP) Start() {
|
||||||
|
yp.timer = time.NewTicker(pingInterval)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-yp.timer.C:
|
||||||
|
yp.ping()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
yp.ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the pinging of YP
|
||||||
|
func (yp *YP) Stop() {
|
||||||
|
yp.timer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yp *YP) ping() {
|
||||||
|
myInstanceURL := config.Config.YP.InstanceURL
|
||||||
|
key := yp.getSavedKey()
|
||||||
|
|
||||||
|
log.Traceln("Pinging YP as: ", config.Config.InstanceDetails.Name)
|
||||||
|
|
||||||
|
request := ypPingRequest{
|
||||||
|
Key: key,
|
||||||
|
URL: myInstanceURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pingURL := config.Config.GetYPServiceHost() + "/ping"
|
||||||
|
resp, err := http.Post(pingURL, "application/json", bytes.NewBuffer(req))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pingResponse := ypPingResponse{}
|
||||||
|
json.Unmarshal(body, &pingResponse)
|
||||||
|
|
||||||
|
if !pingResponse.Success {
|
||||||
|
log.Debugln("YP Ping error returned from service:", pingResponse.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pingResponse.Key != key {
|
||||||
|
yp.writeSavedKey(pingResponse.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yp *YP) writeSavedKey(key string) {
|
||||||
|
f, err := os.Create(".yp.key")
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yp *YP) getSavedKey() string {
|
||||||
|
fileBytes, err := ioutil.ReadFile(".yp.key")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(fileBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayInstructions will let the user know they are not in the directory by default and
|
||||||
|
// how they can enable the feature.
|
||||||
|
func DisplayInstructions() {
|
||||||
|
text := "Your instance can be listed on the Owncast directory at http://something.something by enabling YP in your config. Learn more at http://something.something."
|
||||||
|
log.Debugln(text)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user