Browse Source

Merge branch 'master' into fix-issue-202

pull/219/head
Jannik Volkland 5 years ago
parent
commit
a17e507664
  1. 1
      .gitattributes
  2. 2
      .gitignore
  3. 3
      .gitmodules
  4. 2
      README.md
  5. 10
      build/javascript/README.md
  6. 3225
      build/javascript/package-lock.json
  7. 40
      build/javascript/package.json
  8. 23
      config/config.go
  9. 12
      config/constants.go
  10. 2
      config/defaults.go
  11. 4
      controllers/admin/changeStreamKey.go
  12. 6
      controllers/admin/disconnect.go
  13. 16
      controllers/admin/hardware.go
  14. 9
      controllers/admin/inboundBroadcasterDetails.go
  15. 2
      controllers/admin/serverConfig.go
  16. 2
      controllers/admin/viewers.go
  17. 6
      controllers/chat.go
  18. 4
      controllers/config.go
  19. 2
      controllers/controllers.go
  20. 5
      controllers/emoji.go
  21. 20
      controllers/index.go
  22. 4
      controllers/status.go
  23. 2
      core/chat/chat.go
  24. 4
      core/chat/client.go
  25. 6
      core/chat/persistence.go
  26. 4
      core/chat/server.go
  27. 4
      core/chatListener.go
  28. 35
      core/core.go
  29. 2
      core/ffmpeg/ffmpeg.go
  30. 6
      core/ffmpeg/thumbnailGenerator.go
  31. 10
      core/ffmpeg/transcoder.go
  32. 18
      core/playlist/monitor.go
  33. 10
      core/rtmp/rtmp.go
  34. 2
      core/rtmp/utils.go
  35. 6
      core/stats.go
  36. 12
      core/status.go
  37. 6
      core/storage.go
  38. 8
      core/storageproviders/s3Storage.go
  39. 1
      doc
  40. 3
      go.mod
  41. 2
      go.sum
  42. 8
      main.go
  43. 20
      metrics/alerting.go
  44. 13
      metrics/hardware.go
  45. 21
      metrics/metrics.go
  46. 2
      metrics/viewers.go
  47. 2
      models/stats.go
  48. 2
      models/status.go
  49. 513
      openapi.yaml
  50. 17
      router/middleware/auth.go
  51. 4
      router/middleware/caching.go
  52. 43
      router/router.go
  53. 8
      scripts/build.sh
  54. 11
      webroot/index-standalone-chat.html
  55. 15
      webroot/index-video-only.html
  56. 31
      webroot/index.html
  57. 4
      webroot/js/app-standalone-chat.js
  58. 4
      webroot/js/app-video-only.js
  59. 5
      webroot/js/app.js
  60. 14
      webroot/js/components/chat/chat-input.js
  61. 4
      webroot/js/components/chat/chat.js
  62. 2
      webroot/js/components/chat/content-editable.js
  63. 10
      webroot/js/components/chat/message.js
  64. 4
      webroot/js/components/chat/username.js
  65. 30
      webroot/js/components/player.js
  66. 16
      webroot/js/utils/chat.js
  67. 1
      webroot/js/utils/constants.js
  68. 3
      webroot/js/web_modules/@joeattardi/emoji-button.js
  69. 301
      webroot/js/web_modules/@justinribeiro/lite-youtube.js
  70. 58119
      webroot/js/web_modules/@videojs/http-streaming/dist/videojs-http-streaming.min.js
  71. 116
      webroot/js/web_modules/@videojs/themes/fantasy/index.css
  72. 25
      webroot/js/web_modules/common/_commonjsHelpers-37fa8da4.js
  73. 44
      webroot/js/web_modules/common/window-2f8a9a85.js
  74. 3
      webroot/js/web_modules/htm.js
  75. 14
      webroot/js/web_modules/import-map.json
  76. 3
      webroot/js/web_modules/preact.js
  77. 5044
      webroot/js/web_modules/showdown.js
  78. 1
      webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
  79. 1
      webroot/js/web_modules/videojs/dist/video-js.min.css
  80. 32
      webroot/js/web_modules/videojs/dist/video.min.js
  81. 4
      webroot/styles/app.css
  82. 9
      webroot/styles/chat.css
  83. 4
      yp/api.go
  84. 4
      yp/yp.go

1
.gitattributes vendored

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

2
.gitignore vendored

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

3
.gitmodules vendored

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

2
README.md

@ -14,7 +14,7 @@ @@ -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>

10
build/javascript/README.md

@ -0,0 +1,10 @@ @@ -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

File diff suppressed because it is too large Load Diff

40
build/javascript/package.json

@ -0,0 +1,40 @@ @@ -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"
}

23
config/config.go

@ -4,7 +4,7 @@ import ( @@ -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 @@ -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 { @@ -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

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

2
config/defaults.go

@ -12,8 +12,6 @@ func getDefaults() config { @@ -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

4
controllers/admin/changeStreamKey.go

@ -4,8 +4,8 @@ import ( @@ -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"
)

6
controllers/admin/disconnect.go

@ -3,10 +3,10 @@ package admin @@ -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

16
controllers/admin/hardware.go

@ -0,0 +1,16 @@ @@ -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)
}

9
controllers/admin/inboundBroadcasterDetails.go

@ -4,16 +4,13 @@ import ( @@ -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")

2
controllers/admin/serverConfig.go

@ -4,7 +4,7 @@ import ( @@ -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

2
controllers/admin/viewers.go

@ -4,7 +4,7 @@ import ( @@ -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

6
controllers/chat.go

@ -4,9 +4,9 @@ import ( @@ -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

4
controllers/config.go

@ -4,8 +4,8 @@ import ( @@ -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

2
controllers/controllers.go

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

5
controllers/emoji.go

@ -8,7 +8,8 @@ import ( @@ -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 @@ -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)

20
controllers/index.go

@ -5,15 +5,16 @@ import ( @@ -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) { @@ -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) { @@ -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) { @@ -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)

4
controllers/status.go

@ -4,8 +4,8 @@ import ( @@ -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

2
core/chat/chat.go

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

4
core/chat/client.go

@ -9,8 +9,8 @@ import ( @@ -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"
)

6
core/chat/persistence.go

@ -5,9 +5,9 @@ import ( @@ -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"
)

4
core/chat/server.go

@ -8,8 +8,8 @@ import ( @@ -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 (

4
core/chatListener.go

@ -3,8 +3,8 @@ package core @@ -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

35
core/core.go

@ -3,17 +3,18 @@ package core @@ -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 { @@ -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() { @@ -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)
}
}

2
core/ffmpeg/ffmpeg.go

@ -1,7 +1,7 @@ @@ -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

6
core/ffmpeg/thumbnailGenerator.go

@ -10,7 +10,7 @@ import ( @@ -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) { @@ -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)

10
core/ffmpeg/transcoder.go

@ -10,8 +10,8 @@ import ( @@ -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 { @@ -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()

18
core/playlist/monitor.go

@ -12,9 +12,9 @@ import ( @@ -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 ( @@ -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 { @@ -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 { @@ -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 { @@ -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))
}

10
core/rtmp/rtmp.go

@ -14,11 +14,11 @@ import ( @@ -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"
)

2
core/rtmp/utils.go

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

6
core/stats.go

@ -10,9 +10,9 @@ import ( @@ -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 (

12
core/status.go

@ -3,10 +3,10 @@ package core @@ -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() { @@ -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 {

6
core/storage.go

@ -1,9 +1,9 @@ @@ -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 (

8
core/storageproviders/s3Storage.go

@ -13,8 +13,8 @@ import ( @@ -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 { @@ -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 @@ -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 @@ @@ -1 +0,0 @@
Subproject commit 54a0ee13964c70585c24a9b5869604373faaa926

3
go.mod

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
module github.com/gabek/owncast
module github.com/owncast/owncast
go 1.14
@ -11,7 +11,6 @@ require ( @@ -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

@ -31,8 +31,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -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=

8
main.go

@ -7,10 +7,10 @@ import ( @@ -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

20
metrics/alerting.go

@ -6,12 +6,14 @@ import ( @@ -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() { @@ -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() { @@ -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)
}
}

13
metrics/hardware.go

@ -4,6 +4,7 @@ import ( @@ -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() { @@ -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)
}

21
metrics/metrics.go

@ -5,21 +5,25 @@ import ( @@ -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() { @@ -30,6 +34,7 @@ func handlePolling() {
// Collect hardware stats
collectCPUUtilization()
collectRAMUtilization()
collectDiskUtilization()
// Alerting
handleAlerting()

2
metrics/viewers.go

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

2
models/stats.go

@ -3,7 +3,7 @@ package models @@ -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

2
models/status.go

@ -1,6 +1,6 @@ @@ -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

@ -0,0 +1,513 @@ @@ -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

17
router/middleware/auth.go

@ -4,7 +4,7 @@ import ( @@ -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 ( @@ -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 {

4
router/middleware/caching.go

@ -57,8 +57,10 @@ func getCacheDurationSecondsForPath(filePath string) int { @@ -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
}

43
router/router.go

@ -6,14 +6,14 @@ import ( @@ -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/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/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
"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 { @@ -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 { @@ -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)

8
scripts/build.sh

@ -52,7 +52,7 @@ build() { @@ -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}" @@ -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) @@ -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.

11
webroot/index-standalone-chat.html

@ -3,20 +3,19 @@ @@ -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")

15
webroot/index-video-only.html

@ -3,17 +3,13 @@ @@ -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 @@ @@ -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>

31
webroot/index.html

@ -1,6 +1,7 @@ @@ -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 @@ @@ -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 @@ @@ -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>

4
webroot/js/app-standalone-chat.js

@ -1,5 +1,5 @@ @@ -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';

4
webroot/js/app-video-only.js

@ -1,5 +1,5 @@ @@ -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';

5
webroot/js/app.js

@ -1,6 +1,7 @@ @@ -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';

14
webroot/js/components/chat/chat-input.js

@ -1,8 +1,9 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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;

4
webroot/js/components/chat/chat.js

@ -1,5 +1,5 @@ @@ -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';

2
webroot/js/components/chat/content-editable.js

@ -6,7 +6,7 @@ and here: @@ -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

10
webroot/js/components/chat/message.js

@ -1,9 +1,9 @@ @@ -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 { @@ -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 { @@ -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 }
}

4
webroot/js/components/chat/username.js

@ -1,5 +1,5 @@ @@ -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';

30
webroot/js/components/player.js

@ -1,5 +1,9 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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');
}
});
}

16
webroot/js/utils/chat.js

@ -4,6 +4,7 @@ import { @@ -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() {} }) { @@ -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()}`;
}

1
webroot/js/utils/constants.js

@ -18,6 +18,7 @@ export const MESSAGE_OFFLINE = 'Stream is offline.'; @@ -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';

3
webroot/js/web_modules/@joeattardi/emoji-button.js

File diff suppressed because one or more lines are too long

301
webroot/js/web_modules/@justinribeiro/lite-youtube.js

@ -0,0 +1,301 @@ @@ -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

File diff suppressed because one or more lines are too long

116
webroot/js/web_modules/@videojs/themes/fantasy/index.css

@ -0,0 +1,116 @@ @@ -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

@ -0,0 +1,25 @@ @@ -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

@ -0,0 +1,44 @@ @@ -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

@ -0,0 +1,3 @@ @@ -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

@ -0,0 +1,14 @@ @@ -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

File diff suppressed because one or more lines are too long

5044
webroot/js/web_modules/showdown.js vendored

File diff suppressed because it is too large Load Diff

1
webroot/js/web_modules/tailwindcss/dist/tailwind.min.css vendored

File diff suppressed because one or more lines are too long

1
webroot/js/web_modules/videojs/dist/video-js.min.css vendored

File diff suppressed because one or more lines are too long

32
webroot/js/web_modules/videojs/dist/video.min.js vendored

File diff suppressed because one or more lines are too long

4
webroot/styles/app.css

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

9
webroot/styles/chat.css

@ -80,6 +80,7 @@ @@ -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 @@ @@ -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;
}
/******************************/

4
yp/api.go

@ -4,8 +4,8 @@ import ( @@ -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 {

4
yp/yp.go

@ -9,8 +9,8 @@ import ( @@ -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"
)

Loading…
Cancel
Save