Merge branch 'master' into fix-issue-202

This commit is contained in:
Jannik Volkland 2020-10-07 08:49:10 +02:00
commit a17e507664
84 changed files with 67819 additions and 251 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
webroot/js/web_modules/* linguist-vendored

2
.gitignore vendored
View file

@ -27,3 +27,5 @@ dist/
transcoder.log
chat.db
.yp.key
!webroot/js/web_modules/**/dist

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "doc"]
path = doc
url = https://github.com/owncast/owncast.github.io/

View file

@ -14,7 +14,7 @@
<a href="https://goth.land/">View Demo</a>
·
<a href="https://owncast.online/docs/faq/">FAQ</a>
.
·
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
</p>
</p>

View 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

File diff suppressed because it is too large Load diff

View 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"
}

View file

@ -4,7 +4,7 @@ import (
"errors"
"io/ioutil"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@ -15,15 +15,12 @@ var _default config
type config struct {
ChatDatabaseFilePath string `yaml:"chatDatabaseFile"`
DisableWebFeatures bool `yaml:"disableWebFeatures"`
EnableDebugFeatures bool `yaml:"-"`
FFMpegPath string `yaml:"ffmpegPath"`
Files files `yaml:"files"`
InstanceDetails InstanceDetails `yaml:"instanceDetails"`
PrivateHLSPath string `yaml:"privateHLSPath"`
PublicHLSPath string `yaml:"publicHLSPath"`
S3 S3 `yaml:"s3"`
VersionInfo string `yaml:"-"`
VersionInfo string `yaml:"-"` // For storing the version/build number
VideoSettings videoSettings `yaml:"videoSettings"`
WebServerPort int `yaml:"webServerPort"`
YP yp `yaml:"yp"`
@ -158,22 +155,6 @@ func (c *config) GetVideoSegmentSecondsLength() int {
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 {
if c.WebServerPort != 0 {
return c.WebServerPort

12
config/constants.go Normal file
View file

@ -0,0 +1,12 @@
package config
import "path/filepath"
const (
WebRoot = "webroot"
PrivateHLSStoragePath = "hls"
)
var (
PublicHLSStoragePath = filepath.Join(WebRoot, "hls")
)

View file

@ -12,8 +12,6 @@ func getDefaults() config {
defaults.FFMpegPath = getDefaultFFMpegPath()
defaults.VideoSettings.ChunkLengthInSeconds = 4
defaults.Files.MaxNumberInPlaylist = 5
defaults.PublicHLSPath = "webroot/hls"
defaults.PrivateHLSPath = "hls"
defaults.VideoSettings.OfflineContent = "static/offline.m4v"
defaults.InstanceDetails.ExtraInfoFile = "/static/content.md"
defaults.YP.Enabled = false

View file

@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/controllers"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
log "github.com/sirupsen/logrus"
)

View file

@ -3,10 +3,10 @@ package admin
import (
"net/http"
"github.com/gabek/owncast/controllers"
"github.com/gabek/owncast/core"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/gabek/owncast/core/rtmp"
"github.com/owncast/owncast/core/rtmp"
)
// DisconnectInboundConnection will force-disconnect an inbound stream

View 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)
}

View file

@ -4,16 +4,13 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/controllers"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/router/middleware"
"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) {
middleware.EnableCors(&w)
broadcaster := core.GetBroadcaster()
if broadcaster == nil {
controllers.WriteSimpleResponse(w, false, "no broadcaster connected")

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/config"
"github.com/owncast/owncast/config"
)
// GetServerConfig gets the config details of the server

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/metrics"
"github.com/owncast/owncast/metrics"
)
// GetViewersOverTime will return the number of viewers at points in time

View file

@ -4,9 +4,9 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/router/middleware"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware"
)
//GetChatMessages gets all of the chat messages

View file

@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/router/middleware"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/router/middleware"
)
//GetWebConfig gets the status of the server

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/models"
)
type j map[string]interface{}

View file

@ -8,7 +8,8 @@ import (
"path/filepath"
"strings"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
@ -20,7 +21,7 @@ const emojiPath = "/img/emoji" // Relative to webroot
func GetCustomEmoji(w http.ResponseWriter, r *http.Request) {
emojiList := make([]models.CustomEmoji, 0)
fullPath := filepath.Join("webroot", emojiPath)
fullPath := filepath.Join(config.WebRoot, emojiPath)
files, err := ioutil.ReadDir(fullPath)
if err != nil {
log.Errorln(err)

View file

@ -5,15 +5,16 @@ import (
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"text/template"
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/router/middleware"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
)
type MetadataPage struct {
@ -30,13 +31,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
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
// server-rendered page.
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
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
@ -75,7 +69,7 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
// If the thumbnail does not exist or we're offline then just use the logo image
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"))
if err != nil {
log.Errorln(err)

View file

@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/router/middleware"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/router/middleware"
)
//GetStatus gets the status of the server

View file

@ -4,7 +4,7 @@ import (
"errors"
"time"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/models"
)
//Setup sets up the chat server

View file

@ -9,8 +9,8 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
"github.com/teris-io/shortid"
)

View file

@ -5,9 +5,9 @@ import (
"os"
"time"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
_ "github.com/mattn/go-sqlite3"
log "github.com/sirupsen/logrus"
)

View file

@ -8,8 +8,8 @@ import (
log "github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
)
var (

View file

@ -3,8 +3,8 @@ package core
import (
"errors"
"github.com/gabek/owncast/core/chat"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/models"
)
//ChatListenerImpl the implementation of the chat client

View file

@ -3,17 +3,18 @@ package core
import (
"os"
"path"
"path/filepath"
"strconv"
"time"
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core/chat"
"github.com/gabek/owncast/core/ffmpeg"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/gabek/owncast/yp"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/ffmpeg"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/yp"
)
var (
@ -56,8 +57,8 @@ func Start() error {
func createInitialOfflineState() error {
// Provide default files
if !utils.DoesFileExists("webroot/thumbnail.jpg") {
if err := utils.Copy("static/logo.png", "webroot/thumbnail.jpg"); err != nil {
if !utils.DoesFileExists(filepath.Join(config.WebRoot, "thumbnail.jpg")) {
if err := utils.Copy("static/logo.png", filepath.Join(config.WebRoot, "thumbnail.jpg")); err != nil {
return err
}
}
@ -93,22 +94,22 @@ func resetDirectories() {
log.Trace("Resetting file directories to a clean slate.")
// Wipe the public, web-accessible hls data directory
os.RemoveAll(config.Config.GetPublicHLSSavePath())
os.RemoveAll(config.Config.GetPrivateHLSSavePath())
os.MkdirAll(config.Config.GetPublicHLSSavePath(), 0777)
os.MkdirAll(config.Config.GetPrivateHLSSavePath(), 0777)
os.RemoveAll(config.PublicHLSStoragePath)
os.RemoveAll(config.PrivateHLSStoragePath)
os.MkdirAll(config.PublicHLSStoragePath, 0777)
os.MkdirAll(config.PrivateHLSStoragePath, 0777)
// Remove the previous thumbnail
os.Remove("webroot/thumbnail.jpg")
os.Remove(filepath.Join(config.WebRoot, "thumbnail.jpg"))
// Create private hls data dirs
if len(config.Config.VideoSettings.StreamQualities) != 0 {
for index := range config.Config.VideoSettings.StreamQualities {
os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(index)), 0777)
os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(index)), 0777)
os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(index)), 0777)
os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(index)), 0777)
}
} else {
os.MkdirAll(path.Join(config.Config.GetPrivateHLSSavePath(), strconv.Itoa(0)), 0777)
os.MkdirAll(path.Join(config.Config.GetPublicHLSSavePath(), strconv.Itoa(0)), 0777)
os.MkdirAll(path.Join(config.PrivateHLSStoragePath, strconv.Itoa(0)), 0777)
os.MkdirAll(path.Join(config.PublicHLSStoragePath, strconv.Itoa(0)), 0777)
}
}

View file

@ -1,7 +1,7 @@
package ffmpeg
import (
"github.com/gabek/owncast/config"
"github.com/owncast/owncast/config"
)
//ShowStreamOfflineState generates and shows the stream's offline state

View file

@ -10,7 +10,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/owncast/owncast/config"
)
//StartThumbnailGenerator starts generating thumbnails
@ -39,8 +39,8 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int) {
func fireThumbnailGenerator(chunkPath string, variantIndex int) error {
// JPG takes less time to encode than PNG
outputFile := path.Join("webroot", "thumbnail.jpg")
previewGifFile := path.Join("webroot", "preview.gif")
outputFile := path.Join(config.WebRoot, "thumbnail.jpg")
previewGifFile := path.Join(config.WebRoot, "preview.gif")
framePath := path.Join(chunkPath, strconv.Itoa(variantIndex))
files, err := ioutil.ReadDir(framePath)

View file

@ -10,8 +10,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/teris-io/shortid"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/utils"
)
var _commandExec *exec.Cmd
@ -188,15 +188,15 @@ func NewTranscoder() Transcoder {
var outputPath string
if config.Config.S3.Enabled {
// Segments are not available via the local HTTP server
outputPath = config.Config.GetPrivateHLSSavePath()
outputPath = config.PrivateHLSStoragePath
} else {
// Segments are available via the local HTTP server
outputPath = config.Config.GetPublicHLSSavePath()
outputPath = config.PublicHLSStoragePath
}
transcoder.segmentOutputPath = outputPath
// Playlists are available via the local HTTP server
transcoder.playlistOutputPath = config.Config.GetPublicHLSSavePath()
transcoder.playlistOutputPath = config.PublicHLSStoragePath
transcoder.input = utils.GetTemporaryPipePath()
transcoder.segmentLengthSeconds = config.Config.GetVideoSegmentSecondsLength()

View file

@ -12,9 +12,9 @@ import (
"github.com/radovskyb/watcher"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
)
var (
@ -26,7 +26,7 @@ var (
func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
_storage = storage
pathToMonitor := config.Config.GetPrivateHLSSavePath()
pathToMonitor := config.PrivateHLSStoragePath
// Create at least one structure to store the segments for the different stream variants
variants = make([]models.Variant, len(config.Config.VideoSettings.StreamQualities))
@ -63,11 +63,9 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
continue
}
// fmt.Println(event.Op, relativePath)
// Handle updates to the master playlist by copying it to webroot
if relativePath == path.Join(config.Config.GetPrivateHLSSavePath(), "stream.m3u8") {
utils.Copy(event.Path, path.Join(config.Config.GetPublicHLSSavePath(), "stream.m3u8"))
if relativePath == path.Join(config.PrivateHLSStoragePath, "stream.m3u8") {
utils.Copy(event.Path, path.Join(config.PublicHLSStoragePath, "stream.m3u8"))
} else if filepath.Ext(event.Path) == ".m3u8" {
// Handle updates to playlists, but not the master playlist
@ -82,7 +80,7 @@ func StartVideoContentMonitor(storage models.ChunkStorageProvider) error {
newObjectPathChannel := make(chan string, 1)
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 {
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 = _storage.GenerateRemotePlaylist(playlistString, variant)
return WritePlaylist(playlistString, path.Join(config.Config.GetPublicHLSSavePath(), relativePath))
return WritePlaylist(playlistString, path.Join(config.PublicHLSStoragePath, relativePath))
}

View file

@ -14,11 +14,11 @@ import (
"github.com/nareix/joy5/format/flv/flvio"
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/core/ffmpeg"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/ffmpeg"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
"github.com/nareix/joy5/format/rtmp"
)

View file

@ -6,7 +6,7 @@ import (
"fmt"
"regexp"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/models"
"github.com/nareix/joy5/format/flv/flvio"
)

View file

@ -10,9 +10,9 @@ import (
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
)
const (

View file

@ -3,10 +3,10 @@ package core
import (
"time"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core/ffmpeg"
"github.com/gabek/owncast/models"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/ffmpeg"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
)
//GetStatus gets the status of the system
@ -33,9 +33,9 @@ func SetStreamAsConnected() {
_stats.LastConnectTime = utils.NullTime{time.Now(), true}
_stats.LastDisconnectTime = utils.NullTime{time.Now(), false}
chunkPath := config.Config.GetPublicHLSSavePath()
chunkPath := config.PublicHLSStoragePath
if usingExternalStorage {
chunkPath = config.Config.GetPrivateHLSSavePath()
chunkPath = config.PrivateHLSStoragePath
}
if _yp != nil {

View file

@ -1,9 +1,9 @@
package core
import (
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core/playlist"
"github.com/gabek/owncast/core/storageproviders"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/playlist"
"github.com/owncast/owncast/core/storageproviders"
)
var (

View file

@ -13,8 +13,8 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
)
//S3Storage is the s3 implementation of the ChunkStorageProvider
@ -28,7 +28,7 @@ type S3Storage struct {
s3Bucket string
s3AccessKey string
s3Secret string
s3ACL string
s3ACL string
}
//Setup sets up the s3 storage for saving the video to s3
@ -94,7 +94,7 @@ func (s *S3Storage) GenerateRemotePlaylist(playlist string, variant models.Varia
if fullRemotePath == nil {
line = ""
} 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 {
line = fullRemotePath.RemoteID
}

1
doc

@ -1 +0,0 @@
Subproject commit 54a0ee13964c70585c24a9b5869604373faaa926

3
go.mod
View file

@ -1,4 +1,4 @@
module github.com/gabek/owncast
module github.com/owncast/owncast
go 1.14
@ -11,7 +11,6 @@ require (
github.com/mssola/user_agent v0.5.2
github.com/nareix/joy5 v0.0.0-20200712071056-a55089207c88
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/shirou/gopsutil v2.20.7+incompatible
github.com/sirupsen/logrus v1.6.0

2
go.sum
View file

@ -31,8 +31,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=

View file

@ -7,10 +7,10 @@ import (
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/core"
"github.com/gabek/owncast/metrics"
"github.com/gabek/owncast/router"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/router"
)
// the following are injected at build-time

View file

@ -6,12 +6,14 @@ import (
const maxCPUAlertingThresholdPCT = 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() {
handleCPUAlerting()
handleRAMAlerting()
handleDiskAlerting()
}
func handleCPUAlerting() {
@ -21,7 +23,7 @@ func handleCPUAlerting() {
avg := recentAverage(Metrics.CPUUtilizations)
if avg > maxCPUAlertingThresholdPCT {
log.Errorf(alertingError, "CPU", avg, maxCPUAlertingThresholdPCT)
log.Errorf(alertingError, "CPU", maxCPUAlertingThresholdPCT)
}
}
@ -32,7 +34,19 @@ func handleRAMAlerting() {
avg := recentAverage(Metrics.RAMUtilizations)
if avg > maxRAMAlertingThresholdPCT {
log.Errorf(alertingError, "memory", avg, maxRAMAlertingThresholdPCT)
log.Errorf(alertingError, "memory", maxRAMAlertingThresholdPCT)
}
}
func handleDiskAlerting() {
if len(Metrics.DiskUtilizations) < 2 {
return
}
avg := recentAverage(Metrics.DiskUtilizations)
if avg > maxDiskAlertingThresholdPCT {
log.Errorf(alertingError, "disk", maxRAMAlertingThresholdPCT)
}
}

View file

@ -4,6 +4,7 @@ import (
"time"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
)
@ -33,3 +34,15 @@ func collectRAMUtilization() {
metricValue := timestampedValue{time.Now(), int(memoryUsage.UsedPercent)}
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)
}

View file

@ -5,21 +5,25 @@ import (
)
// How often we poll for updates
const metricsPollingInterval = 15 * time.Second
const metricsPollingInterval = 1 * time.Minute
type metrics struct {
CPUUtilizations []timestampedValue
RAMUtilizations []timestampedValue
Viewers []timestampedValue
// CollectedMetrics stores different collected + timestamped values
type CollectedMetrics struct {
CPUUtilizations []timestampedValue `json:"cpu"`
RAMUtilizations []timestampedValue `json:"memory"`
DiskUtilizations []timestampedValue `json:"disk"`
Viewers []timestampedValue `json:"-"`
}
// Metrics is the shared Metrics instance
var Metrics *metrics
var Metrics *CollectedMetrics
// Start will begin the metrics collection and alerting
func Start() {
Metrics = new(metrics)
startViewerCollectionMetrics()
Metrics = new(CollectedMetrics)
go startViewerCollectionMetrics()
handlePolling()
for range time.Tick(metricsPollingInterval) {
handlePolling()
@ -30,6 +34,7 @@ func handlePolling() {
// Collect hardware stats
collectCPUUtilization()
collectRAMUtilization()
collectDiskUtilization()
// Alerting
handleAlerting()

View file

@ -3,7 +3,7 @@ package metrics
import (
"time"
"github.com/gabek/owncast/core"
"github.com/owncast/owncast/core"
)
// How often we poll for updates

View file

@ -3,7 +3,7 @@ package models
import (
"time"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/utils"
)
//Stats holds the stats for the system

View file

@ -1,6 +1,6 @@
package models
import "github.com/gabek/owncast/utils"
import "github.com/owncast/owncast/utils"
//Status represents the status of the system
type Status struct {

513
openapi.yaml Normal file
View 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

View file

@ -4,7 +4,7 @@ import (
"crypto/subtle"
"net/http"
"github.com/gabek/owncast/config"
"github.com/owncast/owncast/config"
log "github.com/sirupsen/logrus"
)
@ -13,11 +13,24 @@ import (
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()
realm := "Owncast Authenticated Request"
// Failed
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {

View file

@ -57,8 +57,10 @@ func getCacheDurationSecondsForPath(filePath string) int {
// This matters most for local hosting of segments for recordings
// and not for live or 3rd party storage.
return 31557600
} else if path.Ext(filePath) == ".m3u8" {
return 0
}
// Default cache length in seconds
return 30 * 60
return 30
}

View file

@ -6,14 +6,14 @@ import (
log "github.com/sirupsen/logrus"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/controllers"
"github.com/gabek/owncast/controllers/admin"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
"github.com/gabek/owncast/core/chat"
"github.com/gabek/owncast/core/rtmp"
"github.com/gabek/owncast/router/middleware"
"github.com/gabek/owncast/yp"
"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
@ -30,24 +30,22 @@ func Start() error {
// custom emoji supported in the chat
http.HandleFunc("/api/emoji", controllers.GetCustomEmoji)
if !config.Config.DisableWebFeatures {
// websocket chat server
go chat.Start()
// websocket chat server
go chat.Start()
// chat rest api
http.HandleFunc("/api/chat", controllers.GetChatMessages)
// chat rest api
http.HandleFunc("/api/chat", controllers.GetChatMessages)
// web config api
http.HandleFunc("/api/config", controllers.GetWebConfig)
// web config api
http.HandleFunc("/api/config", controllers.GetWebConfig)
// chat embed
http.HandleFunc("/embed/chat", controllers.GetChatEmbed)
// chat embed
http.HandleFunc("/embed/chat", controllers.GetChatEmbed)
// video embed
http.HandleFunc("/embed/video", controllers.GetVideoEmbed)
// video embed
http.HandleFunc("/embed/video", controllers.GetVideoEmbed)
http.HandleFunc("/api/yp", yp.GetYPResponse)
}
http.HandleFunc("/api/yp", yp.GetYPResponse)
// Authenticated admin requests
@ -66,6 +64,9 @@ func Start() error {
// 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()
log.Infof("Web server running on port: %d", port)

View file

@ -52,7 +52,7 @@ build() {
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
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
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
fi
@ -90,8 +90,8 @@ cd $(git rev-parse --show-toplevel)
# 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 tag $DOCKER_IMAGE docker.pkg.github.com/gabek/owncast/$DOCKER_IMAGE:$VERSION
docker push 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/owncast/owncast/$DOCKER_IMAGE:$VERSION
#
# Dockerhub
# You must be authenticated via `docker login` with your Dockerhub credentials first.

View file

@ -3,20 +3,19 @@
<head>
<meta charset="UTF-8" />
<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/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>
<body>
<div id="messages-only"></div>
<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';
render(
html`<${StandaloneChat} messagesOnly />`, document.getElementById("messages-only")

View file

@ -3,17 +3,13 @@
<head>
<meta charset="UTF-8" />
<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="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"></script>
<script src="//unpkg.com/@videojs/http-streaming@2.1.0/dist/videojs-http-streaming.min.js"></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-only.css" rel="stylesheet" />
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
</head>
<body>
@ -21,7 +17,10 @@
<div id="video-only"></div>
<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';
render(html`<${VideoOnly} />`, document.getElementById("video-only"));
</script>

View file

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Owncast</title>
<meta charset="UTF-8" />
<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="theme-color" content="#ffffff">
<link href="https://unpkg.com/tailwindcss@^1.0/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/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
<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/chat.css" rel="stylesheet" />
<link href="./styles/user-content.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>
<body class="scrollbar-hidden bg-gray-300 text-gray-800">
<div id="app"></div>
<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';
render(html`<${App} />`, document.getElementById("app"));
</script>
@ -77,7 +68,7 @@
<img src="https://owncast.online/images/logo.png" />
<br/>
<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>
</div>
</noscript>

View file

@ -1,5 +1,5 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import Chat from './components/chat/chat.js';

View file

@ -1,5 +1,5 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { OwncastPlayer } from './components/player.js';

View file

@ -1,6 +1,7 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import showdown from '/js/web_modules/showdown.js';
import { OwncastPlayer } from './components/player.js';
import SocialIconsList from './components/social-icons-list.js';

View file

@ -1,8 +1,9 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
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 { generatePlaceholderText, getCaretPosition, convertToText, convertOnPaste } from '../../utils/chat.js';
import { getLocalStorage, setLocalStorage, classNames } from '../../utils/helpers.js';
@ -147,8 +148,7 @@ export default class ChatInput extends Component {
handleMessageInputKeydown(event) {
const formField = this.formMessageInput.current;
let textValue = formField.innerText.trim(); // get this only to count chars
let textValue = formField.textContent; // get this only to count chars
const newStates = {};
let numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
const key = event && event.key;
@ -173,7 +173,7 @@ export default class ChatInput extends Component {
event.preventDefault();
// value could have been changed, update char count
textValue = formField.innerText.trim();
textValue = formField.textContent;
numCharsLeft = CHAT_MAX_MESSAGE_LENGTH - textValue.length;
}
}
@ -192,7 +192,7 @@ export default class ChatInput extends Component {
handleMessageInputKeyup(event) {
const formField = this.formMessageInput.current;
const textValue = formField.innerText.trim(); // get this only to count chars
const textValue = formField.textContent; // get this only to count chars
const { key } = event;

View file

@ -1,5 +1,5 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import Message from './message.js';

View file

@ -6,7 +6,7 @@ and here:
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) {
// Place the caret at the end of the element

View file

@ -1,9 +1,9 @@
import { h, Component } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
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 { SOCKET_MESSAGE_TYPES } from '../../utils/websocket.js';
@ -13,9 +13,10 @@ export default class Message extends Component {
const { type } = message;
if (type === SOCKET_MESSAGE_TYPES.CHAT) {
const { image, author, body } = message;
const { image, author, body, timestamp } = message;
const formattedMessage = formatMessageText(body, username);
const avatar = image || generateAvatar(author);
const formattedTimestamp = formatTimestamp(timestamp);
const authorColor = messageBubbleColorForString(author);
const avatarBgColor = { backgroundColor: authorColor };
@ -35,6 +36,7 @@ export default class Message extends Component {
</div>
<div
class="message-text text-gray-300 font-normal overflow-y-hidden"
title=${formattedTimestamp}
dangerouslySetInnerHTML=${
{ __html: formattedMessage }
}

View file

@ -1,5 +1,5 @@
import { h, Component, createRef } from 'https://unpkg.com/preact?module';
import htm from 'https://unpkg.com/htm?module';
import { h, Component, createRef } from '/js/web_modules/preact.js';
import htm from '/js/web_modules/htm.js';
const html = htm.bind(h);
import { generateAvatar, setLocalStorage } from '../../utils/helpers.js';

View file

@ -1,5 +1,9 @@
// https://docs.videojs.com/player
import videojs from '/js/web_modules/videojs/dist/video.min.js';
import { getLocalStorage, setLocalStorage } from '../utils/helpers.js';
import { PLAYER_VOLUME } from '../utils/constants.js';
const VIDEO_ID = 'video';
// TODO: This directory is customizable in the config. So we should expose this via the config API.
const URL_STREAM = `/hls/stream.m3u8`;
@ -47,18 +51,20 @@ class OwncastPlayer {
this.startPlayer = this.startPlayer.bind(this);
this.handleReady = this.handleReady.bind(this);
this.handlePlaying = this.handlePlaying.bind(this);
this.handleVolume = this.handleVolume.bind(this);
this.handleEnded = this.handleEnded.bind(this);
this.handleError = this.handleError.bind(this);
}
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);
options.uri = `${options.uri}?cachebust=${cachebuster}`;
return options;
};
this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
this.addAirplay();
this.vjsPlayer.ready(this.handleReady);
}
@ -75,15 +81,18 @@ class OwncastPlayer {
// play
startPlayer() {
this.log('Start playing');
const source = { ...VIDEO_SRC }
const source = { ...VIDEO_SRC };
this.vjsPlayer.volume(getLocalStorage(PLAYER_VOLUME) || 1);
this.vjsPlayer.src(source);
// this.vjsPlayer.play();
};
}
handleReady() {
this.log('on Ready');
this.vjsPlayer.on('error', this.handleError);
this.vjsPlayer.on('playing', this.handlePlaying);
this.vjsPlayer.on('volumechange', this.handleVolume);
this.vjsPlayer.on('ended', this.handleEnded);
if (this.appPlayerReadyCallback) {
@ -92,6 +101,10 @@ class OwncastPlayer {
}
}
handleVolume() {
setLocalStorage(PLAYER_VOLUME, this.vjsPlayer.muted() ? 0 : this.vjsPlayer.volume());
}
handlePlaying() {
this.log('on Playing');
if (this.appPlayerPlayingCallback) {
@ -117,7 +130,7 @@ class OwncastPlayer {
setPoster() {
const cachebuster = Math.round(new Date().getTime() / 1000);
const poster = POSTER_THUMB + "?okhi=" + cachebuster;
const poster = POSTER_THUMB + '?okhi=' + cachebuster;
this.vjsPlayer.poster(poster);
}
@ -131,7 +144,6 @@ class OwncastPlayer {
if (window.WebKitPlaybackTargetAvailabilityEvent) {
var videoJsButtonClass = videojs.getComponent('Button');
var concreteButtonClass = videojs.extend(videoJsButtonClass, {
// The `init()` method will also work for constructor logic here, but it is
// deprecated. If you provide an `init()` method, it will override the
// `constructor()` method!
@ -145,8 +157,10 @@ class OwncastPlayer {
},
});
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(new concreteButtonClass());
concreteButtonInstance.addClass("vjs-airplay");
var concreteButtonInstance = this.vjsPlayer.controlBar.addChild(
new concreteButtonClass()
);
concreteButtonInstance.addClass('vjs-airplay');
}
});
}

View file

@ -4,6 +4,7 @@ import {
CHAT_PLACEHOLDER_OFFLINE,
} from './constants.js';
import showdown from '/js/web_modules/showdown.js';
export function formatMessageText(message, username) {
showdown.setFlavor('github');
let formattedText = new showdown.Converter({
@ -278,3 +279,18 @@ export function convertOnPaste( event = { preventDefault() {} }) {
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()}`;
}

View file

@ -18,6 +18,7 @@ export const MESSAGE_OFFLINE = 'Stream is offline.';
export const MESSAGE_ONLINE = 'Stream is online.';
export const URL_OWNCAST = 'https://owncast.online'; // used in footer
export const PLAYER_VOLUME = 'owncast_volume';
export const KEY_USERNAME = 'owncast_username';

File diff suppressed because one or more lines are too long

View 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 };

File diff suppressed because one or more lines are too long

View 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;
}

View 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 };

View 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
View 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
View 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

File diff suppressed because one or more lines are too long

5044
webroot/js/web_modules/showdown.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -14,6 +14,7 @@ May have overrides for other components with own stylesheets.
html {
font-size: 14px;
scrollbar-width: none;
}
a:hover {
@ -24,6 +25,9 @@ a:hover {
width: 0px;
background: transparent;
}
.scrollbar-hidden {
scrollbar-width: none; /* moz only */
}
#app-container * {

View file

@ -80,6 +80,7 @@
.emoji-picker.owncast {
--secondary-text-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 */
color: rgba(226,232,240,1); /* tailwind text-gray-300 */
@ -101,15 +102,19 @@
}
.emoji-picker__emojis::-webkit-scrollbar-track {
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);
}
.emoji-picker__emojis::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,.45);
background-color: var(--category-button-color);
border-radius: 8px;
}
.emoji-picker__emojis {
scrollbar-color: var(--category-button-color) black;
}
/******************************/

View file

@ -4,8 +4,8 @@ import (
"encoding/json"
"net/http"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/utils"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/utils"
)
type ypDetailsResponse struct {

View file

@ -9,8 +9,8 @@ import (
"encoding/json"
"github.com/gabek/owncast/config"
"github.com/gabek/owncast/models"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)