Browse Source

0.0.6 -> Master (#731)

* Implement webhook events for external integrations (#574)

* Implement webhook events for external integrations

Reference #556

* move message type to models and remove duplicate

* add json header so content type can be determined

* Pass at migrating webhooks to datastore + management apis (#589)

* Pass at migrating webhooks to datastore + management apis

* Support nil lastUsed timestamps and return back the new webhook on create

* Cleanup from review feedback

* Simplify a bit

Co-authored-by: Aaron Ogle <aaron@geekgonecrazy.com>

Co-authored-by: Gabe Kangas <gabek@real-ity.com>

* Webhook query cleanup

* Access tokens + Send system message external API (#585)

* New add, get and delete access token APIs

* Create auth token middleware

* Update last_used timestamp when using an access token

* Add auth'ed endpoint for sending system messages

* Cleanup

* Update api spec for new apis

* Commit updated API documentation

* Add auth'ed endpoint for sending user chat messages

* Return access token string

* Commit updated API documentation

* Fix route

* Support nil lastUsed time

* Commit updated Javascript packages

* Remove duplicate function post rebase

* Fix msg id generation

* Update controllers/admin/chat.go

Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com>

* Webhook query cleanup

* Add SystemMessageSent to EventType

Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com>

* Set webhook as used on completion. Closes #610

* Display webhook errors as errors

* Commit updated API documentation

* Add user joined chat event

* Change integration API paths. Update API spec

* Update development version of admin that supports integration apis

* Commit updated API documentation

* Add automated tests for external integration APIs

* check error

* quiet this test for now

* Route up some additional 3rd party apis. #638

* Commit updated API documentation

* Save username on user joined event

* Add missing scope to valid scopes list

* Add generic chat action event API for 3rd parties. Closes #666

* Commit updated API documentation

* First pass at moving WIP config framework into project for #234

* Only support exported fields in custom types

* Using YP get/set key as a first pass at using the data layer. Fixes + integration.

* Ignore test db

* Start adding getters and setters for config values

* More get/set config work. Starting to populate api with data

* Wire up some config edit endpoints

* More endpoints

* Disable cors middleware

* Add more endpoints and add test to test them

* Remove the in-memory change APIs

* Add endpoint for changing tags

* Add more config endpoints

* Starting to point more things away from config file and to the datastore

* Populate YP with db data

* Create new util method for parsing page body markdown and return it in api

* Verify proposed path to ffmpeg

* For development purposes show the config key in logs

* Move stats values to datastore

* Moving over more values to the datastore

* Move S3 config to datastore

* First pass the config -> db migrator

* Add the start of the video config apis

* It builds pointing everything away from the config

* Tweak ffmpeg path error message

* Backup database every hour. Closes #549

* Config + defaults + migration work for db

* Cleanup logging

* Remove all the old config structs

* Add descriptive info about migration

* Tweak ffmpeg validation logic

* Fix db backup path. backup on db version migration

* Set video and s3 configurations

* Update api spec with new config endpoints

* Add migrator for stats file

* Commit updated API documentation

* Use a dynamic system port for internal HLS writes. Closes #577 (#626)

* Use a dynamic system port for internal HLS writes. Closes #577

* Cleanup

* YP key migration to datastore

* Create a backup directory if needed before migrations

* Remove config test that no longer makes sense. Cleanup.

* Change number types from float32 to float64

* Update automated test suite

* Allow restoring a database backup via command line flags. Closes #549

* Add new hls segment config api

* Commit updated API documentation

* Update apis to require a value container property

* add socialHandles api

* Commit updated API documentation

* Add new latancy level setting to replace segment settings

* Commit updated API documentation

* Fix spelling

* Commit updated API documentation

* hardcode a json api of available social platforms

* Add additional icons

* Return social handles in server config api

* Add socialhandles validation to test

* Move list of hard coded social platforms to an api

* Remove audio only code from transcoder since we do not use it

* Add latency levels api + snapshot of video settings as current broadcast

* Add config/serverurl endpoint

* Return 404 on YP api if disabled

* Surface stream title in YP response

* Add stream title to web ui

* Cleanup log message. Closes #520

* Rename ffmpeg package to transcoder

* Add ws package for testing

* Reduce chat backlog to past 5hrs, max 50. Closes #548

* Fix error formatting

* Add endpoint for resetting yp registration

* Add yp/reset to api spec. return status in response

* Return zero viewer count if stream is offline. Closes #422

* Post-rebase fixes

* Fix merge conflict in openapi file

* Commit updated API documentation

* Standardize controller names

* Support setting the stream key via the command line. Closes #665

* Return social handles with YP data. First half of https://github.com/owncast/owncast-yp/issues/28

* Give the YP package access to server status regardless if enabled or not

* Change delay in automated tests

* Add stream title integration API. For #638

* Commit updated API documentation

* Add storage to the migrator

* Missing returning NSFW value in server config

* Add flag to ignore websocket client. Closes #537

* Add error for parsing broadcaster metadata

* Add support for a cli specified http server port. Closes #674

* Add cpu usage levels and a temporary mapping between it and libx264 presets

* Test for valid url endpoint when saving s3 config

* Re-configure storage on every stream to allow changing storage providers

* After 5 minutes of a stream being stopped clear the stream title

* Hide viewer count once stream goes offline instead of when player stops

* Pull steamTitle from the status that gets updated instead of the config

* Commit updated API documentation

* Optionally show stream title in the header

* Reset stream title when server starts

* Show chat action when stream title is updated

* Allow system messages to come back in persistence

* Split out getting chat history for moderation + fix tests

* Remove server title and standardize on name only

* Commit updated API documentation

* Bump github.com/aws/aws-sdk-go from 1.37.1 to 1.37.2 (#680)

Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.1 to 1.37.2.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.1...v1.37.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add video variant and stream latency config file migrator

* Remove mostly unused disable upgrade check bool

* Commit updated API documentation

* Allow bundling the admin from the 0.0.6 branch

* Fix saving port numbers

* Use name instead of old title on window focus

* Work on latency levels. Fix test to use levels. Clean up transcoder to only reference levels

* Another place where title -> name

* Fix test

* Bump github.com/aws/aws-sdk-go from 1.37.2 to 1.37.3 (#690)

Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.2 to 1.37.3.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.2...v1.37.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update dependabot config

* Bump github.com/aws/aws-sdk-go from 1.37.3 to 1.37.5 (#693)

Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.3 to 1.37.5.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.3...v1.37.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump video.js from 7.10.2 to 7.11.4 in /build/javascript (#694)

* Bump video.js from 7.10.2 to 7.11.4 in /build/javascript

Bumps [video.js](https://github.com/videojs/video.js) from 7.10.2 to 7.11.4.
- [Release notes](https://github.com/videojs/video.js/releases)
- [Changelog](https://github.com/videojs/video.js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/videojs/video.js/compare/v7.10.2...v7.11.4)

Signed-off-by: dependabot[bot] <support@github.com>

* Commit updated Javascript packages

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>

* Make the latency migrator dynamic so I can tweak values easier

* Split out fetching ffmpeg path from validating the path so it can be changed in the admin

* Some commenting and linter cleanup

* Validate the path for a logo change and throw an error if it does not exist

* Logo change requests have to be a real file now

* Cleanup, making linter happy

* Format javascript on push

* Only format js in master

* Tweak latency level values

* Remove unused config file examples

* Fix thumbnail generation after messing with the ffmpeg path getter

* Reduce how often we report high hardware utilization warnings

* Bundle the 0.0.6 branch version of the admin

* Return validated ffmpeg path in admin server config

* Change the logo to be stored in the data directory instead of webroot

* Bump postcss from 8.2.4 to 8.2.5 in /build/javascript (#702)

Bumps [postcss](https://github.com/postcss/postcss) from 8.2.4 to 8.2.5.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.2.4...8.2.5)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Default config file no longer used

* don't show stream title when offline

addresses https://github.com/owncast/owncast/issues/677

* Remove auto-clearing stream title. #677

* webroot -> data when using logo as thumbnail

* Do not list websocket/access token create/delete as integration APIs

* Commit updated API documentation

* Bundle updated admin

* Remove pointing to the 0.0.6 admin branch

* Linter cleanup

* Linter cleanup

* Add donations and follow links to show up under social handles

* Prettified Code!

* More linter cleanup

* Update admin bundle

* Remove use of platforms.js and return icons with social handles. Closes #732

* Update admin bundle

* Support custom config path for use in migration

* Remove unused platform-logos.gif

* Reduce log level of message

* Remove unused logo files in static dir

* Handle dev vs. release build info

* Restore logo.png for initial thumbnail

* Cleanup some files from the build process that are not needed

* Fix incorrect build-time injection var

* Fix missing file getting copied to the build

* Remove console directory message.

* Update admin bundle

* Fix comment

* Report storage setup error

* add some value set error checking

* Use validated dynamic ffmpeg path for animated gif preview

* Make chat message links be white so they don't hide in the bg. Closes #599

* Restore conditional that was accidentally removed

Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Ginger Wong <omqmail@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: nebunez <uoj2y7wak869@opayq.net>
Co-authored-by: gabek <gabek@users.noreply.github.com>
master
Gabe Kangas 5 months ago
committed by GitHub
parent
commit
bc2caadb74
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
125 changed files with 5578 additions and 1544 deletions
  1. +2
    -2
      .github/workflows/javascript-formatting.yml
  2. +2
    -1
      .gitignore
  3. +22
    -0
      .vscode/settings.json
  4. +3
    -1
      build/admin/bundleAdmin.sh
  5. +4
    -8
      build/release/build.sh
  6. +24
    -283
      config/config.go
  7. +0
    -48
      config/configUtils.go
  8. +0
    -19
      config/config_test.go
  9. +14
    -5
      config/constants.go
  10. +55
    -18
      config/defaults.go
  11. +5
    -5
      config/verifyInstall.go
  12. +100
    -0
      controllers/admin/accessToken.go
  13. +0
    -36
      controllers/admin/changePageContent.go
  14. +0
    -35
      controllers/admin/changeStreamKey.go
  15. +0
    -35
      controllers/admin/changeStreamName.go
  16. +0
    -35
      controllers/admin/changeStreamTags.go
  17. +0
    -35
      controllers/admin/changeStreamTitle.go
  18. +97
    -10
      controllers/admin/chat.go
  19. +475
    -0
      controllers/admin/config.go
  20. +57
    -24
      controllers/admin/serverConfig.go
  21. +12
    -10
      controllers/admin/status.go
  22. +84
    -0
      controllers/admin/webhooks.go
  23. +20
    -0
      controllers/admin/yp.go
  24. +2
    -19
      controllers/chat.go
  25. +49
    -3
      controllers/config.go
  26. +1
    -1
      controllers/connectedClients.go
  27. +7
    -0
      controllers/constants.go
  28. +31
    -5
      controllers/controllers.go
  29. +1
    -1
      controllers/emoji.go
  30. +22
    -8
      controllers/index.go
  31. +65
    -0
      controllers/logo.go
  32. +1
    -1
      controllers/status.go
  33. +6
    -2
      core/chat/chat.go
  34. +40
    -17
      core/chat/client.go
  35. +5
    -1
      core/chat/messages.go
  36. +14
    -10
      core/chat/persistence.go
  37. +23
    -6
      core/chat/server.go
  38. +6
    -8
      core/chatListener.go
  39. +29
    -21
      core/core.go
  40. +199
    -0
      core/data/accessTokens.go
  41. +18
    -0
      core/data/cache.go
  42. +450
    -0
      core/data/config.go
  43. +46
    -0
      core/data/configEntry.go
  44. +25
    -3
      core/data/data.go
  45. +115
    -0
      core/data/data_test.go
  46. +43
    -0
      core/data/defaults.go
  47. +266
    -0
      core/data/migrator.go
  48. +152
    -0
      core/data/persistence.go
  49. +46
    -0
      core/data/types.go
  50. +220
    -0
      core/data/webhooks.go
  51. +1
    -1
      core/rtmp/broadcaster.go
  52. +4
    -5
      core/rtmp/rtmp.go
  53. +19
    -40
      core/stats.go
  54. +13
    -2
      core/status.go
  55. +5
    -3
      core/storage.go
  56. +2
    -2
      core/storageproviders/local.go
  57. +13
    -11
      core/storageproviders/s3Storage.go
  58. +31
    -12
      core/streamState.go
  59. +15
    -8
      core/transcoder/fileWriterReceiverService.go
  60. +3
    -3
      core/transcoder/hlsFilesystemCleanup.go
  61. +1
    -1
      core/transcoder/hlsHandler.go
  62. +9
    -4
      core/transcoder/thumbnailGenerator.go
  63. +27
    -52
      core/transcoder/transcoder.go
  64. +8
    -5
      core/transcoder/transcoder_test.go
  65. +39
    -0
      core/webhooks/chat.go
  66. +7
    -0
      core/webhooks/stream.go
  67. +67
    -0
      core/webhooks/webhooks.go
  68. +0
    -4
      data/content-example.md
  69. +123
    -48
      doc/api/index.html
  70. +0
    -50
      examples/config-example.yaml
  71. +2
    -2
      geoip/geoip.go
  72. +1
    -0
      go.mod
  73. +3
    -0
      go.sum
  74. +63
    -23
      main.go
  75. +35
    -6
      metrics/alerting.go
  76. +53
    -0
      models/accessToken.go
  77. +1
    -0
      models/broadcaster.go
  78. +12
    -0
      models/chatActionEvent.go
  79. +14
    -3
      models/chatMessage.go
  80. +3
    -0
      models/client.go
  81. +7
    -0
      models/currentBroadcast.go
  82. +27
    -0
      models/eventType.go
  83. +25
    -0
      models/latencyLevels.go
  84. +5
    -5
      models/nameChangeEvent.go
  85. +1
    -1
      models/pingMessage.go
  86. +13
    -0
      models/s3Storage.go
  87. +110
    -0
      models/socialHandle.go
  88. +1
    -0
      models/status.go
  89. +89
    -0
      models/streamOutputVariant.go
  90. +11
    -0
      models/userJoinedEvent.go
  91. +33
    -0
      models/webhook.go
  92. +850
    -264
      openapi.yaml
  93. +1
    -1
      pkged.go
  94. +34
    -2
      router/middleware/auth.go
  95. +103
    -17
      router/router.go
  96. BIN
      static/logo-900x720.png
  97. +13
    -13
      static/metadata.html
  98. +1
    -37
      test/automated/admin.test.js
  99. +11
    -7
      test/automated/chat.test.js
  100. +1
    -1
      test/automated/chatmoderation.test.js

+ 2
- 2
.github/workflows/javascript-formatting.yml View File

@ -4,8 +4,8 @@ name: Format Javascript
on:
pull_request:
push:
# branches:
# - master
branches:
- master
jobs:
prettier:


+ 2
- 1
.gitignore View File

@ -32,6 +32,7 @@ data/
transcoder.log
chat.db
.yp.key
backup/
!webroot/js/web_modules/**/dist
!core/data
test/test.db

+ 22
- 0
.vscode/settings.json View File

@ -0,0 +1,22 @@
{
"cSpell.words": [
"Debugln",
"Errorln",
"Ffmpeg",
"Mbps",
"Owncast",
"RTMP",
"Tracef",
"Traceln",
"Warnf",
"Warnln",
"ffmpegpath",
"ffmpg",
"mattn",
"nolint",
"preact",
"rtmpserverport",
"sqlite",
"videojs"
]
}

+ 3
- 1
build/admin/bundleAdmin.sh View File

@ -16,9 +16,11 @@ shutdown () {
trap shutdown INT TERM ABRT EXIT
echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..."
git clone --depth 1 https://github.com/owncast/owncast-admin 2> /dev/null
git clone https://github.com/owncast/owncast-admin 2> /dev/null
cd owncast-admin
git checkout 0.0.6
echo "Installing npm modules for the owncast admin..."
npm --silent install 2> /dev/null


+ 4
- 8
build/release/build.sh View File

@ -24,7 +24,7 @@ BUILD_TEMP_DIRECTORY="$(mktemp -d)"
cd $BUILD_TEMP_DIRECTORY
echo "Cloning owncast into $BUILD_TEMP_DIRECTORY..."
git clone --depth 1 https://github.com/owncast/owncast 2> /dev/null
git clone https://github.com/owncast/owncast 2> /dev/null
cd owncast
echo "Changing to branch: $GIT_BRANCH"
@ -58,26 +58,22 @@ build() {
VERSION=$4
GIT_COMMIT=$5
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH}..."
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH} ${GIT_COMMIT}..."
mkdir -p dist/${NAME}
mkdir -p dist/${NAME}/webroot/static
mkdir -p dist/${NAME}/data
# Default files
cp config-default.yaml dist/${NAME}/config.yaml
cp data/content-example.md dist/${NAME}/data/content.md
cp -R webroot/ dist/${NAME}/webroot/
# Copy the production pruned+minified css to the build's directory.
cp "${TMPDIR}tailwind.min.css" ./dist/${NAME}/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
cp -R static/ dist/${NAME}/static
cp README.md dist/${NAME}
cp webroot/img/logo.svg dist/${NAME}/data/logo.svg
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/owncast/owncast
CGO_ENABLED=1 ~/go/bin/xgo --branch ${GIT_BRANCH} -ldflags "-s -w -X main.GitCommit=${GIT_COMMIT} -X main.BuildVersion=${VERSION} -X main.BuildPlatform=${NAME}" -targets "${OS}/${ARCH}" github.com/owncast/owncast
mv owncast-*-${ARCH} owncast
zip -r -q -8 ../owncast-$VERSION-$NAME.zip .


+ 24
- 283
config/config.go View File

@ -1,299 +1,40 @@
package config
import (
"errors"
"io/ioutil"
"os/exec"
"strings"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"fmt"
)
// Config contains a reference to the configuration.
var Config *config
var _default config
type config struct {
DatabaseFilePath string `yaml:"databaseFile"`
EnableDebugFeatures bool `yaml:"-"`
FFMpegPath string `yaml:"ffmpegPath"`
Files files `yaml:"files"`
InstanceDetails InstanceDetails `yaml:"instanceDetails"`
S3 S3 `yaml:"s3"`
VersionInfo string `yaml:"-"` // For storing the version/build number
VersionNumber string `yaml:"-"`
VideoSettings videoSettings `yaml:"videoSettings"`
WebServerPort int `yaml:"webServerPort"`
RTMPServerPort int `yaml:"rtmpServerPort"`
DisableUpgradeChecks bool `yaml:"disableUpgradeChecks"`
YP YP `yaml:"yp"`
}
// InstanceDetails defines the user-visible information about this particular instance.
type InstanceDetails struct {
Name string `yaml:"name" json:"name"`
Title string `yaml:"title" json:"title"`
Summary string `yaml:"summary" json:"summary"`
// Logo logo `yaml:"logo" json:"logo"`
Logo string `yaml:"logo" json:"logo"`
Tags []string `yaml:"tags" json:"tags"`
SocialHandles []socialHandle `yaml:"socialHandles" json:"socialHandles"`
Version string `json:"version"`
NSFW bool `yaml:"nsfw" json:"nsfw"`
ExtraPageContent string `json:"extraPageContent"`
}
// type logo struct {
// Large string `yaml:"large" json:"large"`
// Small string `yaml:"small" json:"small"`
// }
type socialHandle struct {
Platform string `yaml:"platform" json:"platform"`
URL string `yaml:"url" json:"url"`
Icon string `yaml:"icon" json:"icon"`
}
type videoSettings struct {
ChunkLengthInSeconds int `yaml:"chunkLengthInSeconds"`
StreamingKey string `yaml:"streamingKey"`
StreamQualities []StreamQuality `yaml:"streamQualities"`
HighestQualityStreamIndex int `yaml:"-"`
}
// YP allows registration to the central Owncast YP (Yellow pages) service operating as a directory.
type YP struct {
Enabled bool `yaml:"enabled" json:"enabled"`
InstanceURL string `yaml:"instanceURL" json:"instanceUrl"` // The public URL the directory should link to
YPServiceURL string `yaml:"ypServiceURL" json:"-"` // The base URL to the YP API to register with (optional)
}
// StreamQuality defines the specifics of a single HLS stream variant.
type StreamQuality struct {
// Enable passthrough to copy the video and/or audio directly from the
// incoming stream and disable any transcoding. It will ignore any of
// the below settings.
IsVideoPassthrough bool `yaml:"videoPassthrough" json:"videoPassthrough"`
IsAudioPassthrough bool `yaml:"audioPassthrough" json:"audioPassthrough"`
VideoBitrate int `yaml:"videoBitrate" json:"videoBitrate"`
AudioBitrate int `yaml:"audioBitrate" json:"audioBitrate"`
// Set only one of these in order to keep your current aspect ratio.
// Or set neither to not scale the video.
ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"`
ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"`
Framerate int `yaml:"framerate" json:"framerate"`
EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"`
}
type files struct {
MaxNumberInPlaylist int `yaml:"maxNumberInPlaylist"`
}
// S3 is for configuring the S3 integration.
type S3 struct {
Enabled bool `yaml:"enabled" json:"enabled"`
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"`
ServingEndpoint string `yaml:"servingEndpoint" json:"servingEndpoint,omitempty"`
AccessKey string `yaml:"accessKey" json:"accessKey,omitempty"`
Secret string `yaml:"secret" json:"secret,omitempty"`
Bucket string `yaml:"bucket" json:"bucket,omitempty"`
Region string `yaml:"region" json:"region,omitempty"`
ACL string `yaml:"acl" json:"acl,omitempty"`
}
func (c *config) load(filePath string) error {
if !utils.DoesFileExists(filePath) {
log.Fatal("ERROR: valid config.yaml is required. Copy config-default.yaml to config.yaml and edit")
}
yamlFile, err := ioutil.ReadFile(filePath)
if err != nil {
log.Printf("yamlFile.Get err #%v ", err)
return err
}
if err := yaml.Unmarshal(yamlFile, c); err != nil {
log.Fatalf("Error reading the config file.\nHave you recently updated your version of Owncast?\nIf so there may be changes to the config.\nPlease read the change log for your version at https://owncast.online/posts/\n%v", err)
return err
}
c.VideoSettings.HighestQualityStreamIndex = findHighestQuality(c.VideoSettings.StreamQualities)
// Add custom page content to the instance details.
customContentMarkdownData, err := ioutil.ReadFile(ExtraInfoFile)
if err == nil {
customContentMarkdownString := string(customContentMarkdownData)
c.InstanceDetails.ExtraPageContent = utils.RenderSimpleMarkdown(customContentMarkdownString)
}
return nil
}
func (c *config) verifySettings() error {
if c.VideoSettings.StreamingKey == "" {
return errors.New("No stream key set. Please set one in your config file.")
}
if c.S3.Enabled {
if c.S3.AccessKey == "" || c.S3.Secret == "" {
return errors.New("s3 support requires an access key and secret")
}
if c.S3.Region == "" || c.S3.Endpoint == "" {
return errors.New("s3 support requires a region and endpoint")
}
if c.S3.Bucket == "" {
return errors.New("s3 support requires a bucket created for storing public video segments")
}
}
if c.YP.Enabled && c.YP.InstanceURL == "" {
return errors.New("YP is enabled but instance url is not set")
}
// These are runtime-set values used for configuration.
return nil
}
func (c *config) GetVideoSegmentSecondsLength() int {
if c.VideoSettings.ChunkLengthInSeconds != 0 {
return c.VideoSettings.ChunkLengthInSeconds
}
// DatabaseFilePath is the path to the file ot be used as the global database for this run of the application.
var DatabaseFilePath = "data/owncast.db"
return _default.GetVideoSegmentSecondsLength()
}
// EnableDebugFeatures will print additional data to help in debugging.
var EnableDebugFeatures = false
func (c *config) GetPublicWebServerPort() int {
if c.WebServerPort != 0 {
return c.WebServerPort
}
return _default.WebServerPort
}
// VersionNumber is the current version string.
var VersionNumber = StaticVersionNumber
func (c *config) GetRTMPServerPort() int {
if c.RTMPServerPort != 0 {
return c.RTMPServerPort
}
// WebServerPort is the port for Owncast's webserver that is used for this execution of the service.
var WebServerPort = 8080
return _default.RTMPServerPort
}
func (c *config) GetMaxNumberOfReferencedSegmentsInPlaylist() int {
if c.Files.MaxNumberInPlaylist > 0 {
return c.Files.MaxNumberInPlaylist
}
return _default.GetMaxNumberOfReferencedSegmentsInPlaylist()
}
func (c *config) GetFFMpegPath() string {
if c.FFMpegPath != "" {
if err := verifyFFMpegPath(c.FFMpegPath); err == nil {
return c.FFMpegPath
} else {
log.Errorln(c.FFMpegPath, "is an invalid path to ffmpeg. Will try to use a copy in your path, if possible.")
}
}
// First look to see if ffmpeg is in the current working directory
localCopy := "./ffmpeg"
hasLocalCopyError := verifyFFMpegPath(localCopy)
if hasLocalCopyError == nil {
// No error, so all is good. Use the local copy.
return localCopy
}
cmd := exec.Command("which", "ffmpeg")
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatalln("Unable to determine path to ffmpeg. Please specify it in the config file.")
}
path := strings.TrimSpace(string(out))
if err := verifyFFMpegPath(path); err != nil {
log.Warnln(err)
}
return path
}
func (c *config) GetYPServiceHost() string {
if c.YP.YPServiceURL != "" {
return c.YP.YPServiceURL
}
return _default.YP.YPServiceURL
}
// InternalHLSListenerPort is the port for HLS writes that is used for this execution of the service.
var InternalHLSListenerPort = "8927"
func (c *config) GetDataFilePath() string {
if c.DatabaseFilePath != "" {
return c.DatabaseFilePath
}
// ConfigFilePath is the path to the config file for migration.
var ConfigFilePath = "config.yaml"
return _default.DatabaseFilePath
}
func (c *config) GetVideoStreamQualities() []StreamQuality {
if len(c.VideoSettings.StreamQualities) > 0 {
return c.VideoSettings.StreamQualities
}
return _default.VideoSettings.StreamQualities
}
// GetFramerate returns the framerate or default.
func (q *StreamQuality) GetFramerate() int {
if q.IsVideoPassthrough {
return 0
}
if q.Framerate > 0 {
return q.Framerate
}
return _default.VideoSettings.StreamQualities[0].Framerate
}
// GetEncoderPreset returns the preset or default.
func (q *StreamQuality) GetEncoderPreset() string {
if q.IsVideoPassthrough {
return ""
}
if q.EncoderPreset != "" {
return q.EncoderPreset
}
return _default.VideoSettings.StreamQualities[0].EncoderPreset
}
func (q *StreamQuality) GetIsAudioPassthrough() bool {
if q.IsAudioPassthrough {
return true
}
if q.AudioBitrate == 0 {
return true
}
return false
}
// GitCommit is an optional commit this build was made from.
var GitCommit = ""
// Load tries to load the configuration file.
func Load(filePath string, versionInfo string, versionNumber string) error {
Config = new(config)
_default = getDefaults()
// BuildPlatform is the optional platform this release was built for.
var BuildPlatform = "local"
if err := Config.load(filePath); err != nil {
return err
}
// GetReleaseString gets the version string.
func GetReleaseString() string {
var versionNumber = VersionNumber
var buildPlatform = BuildPlatform
var gitCommit = GitCommit
Config.VersionInfo = versionInfo
Config.VersionNumber = versionNumber
return Config.verifySettings()
return fmt.Sprintf("Owncast v%s-%s (%s)", versionNumber, buildPlatform, gitCommit)
}

+ 0
- 48
config/configUtils.go View File

@ -1,49 +1 @@
package config
import (
"encoding/json"
"sort"
)
func findHighestQuality(qualities []StreamQuality) int {
type IndexedQuality struct {
index int
quality StreamQuality
}
if len(qualities) < 2 {
return 0
}
indexedQualities := make([]IndexedQuality, 0)
for index, quality := range qualities {
indexedQuality := IndexedQuality{index, quality}
indexedQualities = append(indexedQualities, indexedQuality)
}
sort.Slice(indexedQualities, func(a, b int) bool {
if indexedQualities[a].quality.IsVideoPassthrough && !indexedQualities[b].quality.IsVideoPassthrough {
return true
}
if !indexedQualities[a].quality.IsVideoPassthrough && indexedQualities[b].quality.IsVideoPassthrough {
return false
}
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
})
return indexedQualities[0].index
}
// MarshalJSON is a custom JSON marshal function for video stream qualities.
func (q *StreamQuality) MarshalJSON() ([]byte, error) {
type Alias StreamQuality
return json.Marshal(&struct {
Framerate int `json:"framerate"`
*Alias
}{
Framerate: q.GetFramerate(),
Alias: (*Alias)(q),
})
}

+ 0
- 19
config/config_test.go View File

@ -1,19 +0,0 @@
package config
import "testing"
func TestDefaults(t *testing.T) {
_default = getDefaults()
encoderPreset := "veryfast"
framerate := 24
quality := StreamQuality{}
if quality.GetEncoderPreset() != encoderPreset {
t.Errorf("default encoder preset does not match expected. Got %s, want: %s", quality.GetEncoderPreset(), encoderPreset)
}
if quality.GetFramerate() != framerate {
t.Errorf("default framerate does not match expected. Got %d, want: %d", quality.GetFramerate(), framerate)
}
}

+ 14
- 5
config/constants.go View File

@ -3,14 +3,23 @@ package config
import "path/filepath"
const (
WebRoot = "webroot"
PrivateHLSStoragePath = "hls"
GeoIPDatabasePath = "data/GeoLite2-City.mmdb"
ExtraInfoFile = "data/content.md"
StatsFile = "data/stats.json"
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
StaticVersionNumber = "0.0.6" // Shown when you build from master
// WebRoot is the web server root directory.
WebRoot = "webroot"
// PrivateHLSStoragePath is the HLS write directory.
PrivateHLSStoragePath = "hls"
// ExtraInfoFile is the markdown file for page content. Remove this after the migrator is removed.
ExtraInfoFile = "data/content.md"
// StatsFile is the json file we used to save stats in. Remove this after the migrator is removed.
StatsFile = "data/stats.json"
// FfmpegSuggestedVersion is the version of ffmpeg we suggest.
FfmpegSuggestedVersion = "v4.1.5" // Requires the v
// BackupDirectory is the directory we write backup files to.
BackupDirectory = "backup"
)
var (
// PublicHLSStoragePath is the directory we write public HLS files to for distribution.
PublicHLSStoragePath = filepath.Join(WebRoot, "hls")
)

+ 55
- 18
config/defaults.go View File

@ -1,22 +1,59 @@
package config
func getDefaults() config {
defaults := config{}
defaults.WebServerPort = 8080
defaults.RTMPServerPort = 1935
defaults.VideoSettings.ChunkLengthInSeconds = 4
defaults.Files.MaxNumberInPlaylist = 5
defaults.YP.Enabled = false
defaults.YP.YPServiceURL = "https://yp.owncast.online"
defaults.DatabaseFilePath = "data/owncast.db"
defaultQuality := StreamQuality{
IsAudioPassthrough: true,
VideoBitrate: 1200,
EncoderPreset: "veryfast",
Framerate: 24,
}
defaults.VideoSettings.StreamQualities = []StreamQuality{defaultQuality}
import "github.com/owncast/owncast/models"
// Defaults will hold default configuration values.
type Defaults struct {
Name string
Title string
Summary string
Logo string
Tags []string
PageBodyContent string
DatabaseFilePath string
WebServerPort int
RTMPServerPort int
StreamKey string
YPEnabled bool
YPServer string
SegmentLengthSeconds int
SegmentsInPlaylist int
StreamVariants []models.StreamOutputVariant
}
return defaults
// GetDefaults will return default configuration values.
func GetDefaults() Defaults {
return Defaults{
Name: "Owncast",
Title: "My Owncast Server",
Summary: "This is brief summary of whom you are or what your stream is. You can edit this description in the admin.",
Logo: "logo.svg",
Tags: []string{
"owncast",
"streaming",
},
PageBodyContent: "# This is your page content that can be edited from the admin.",
DatabaseFilePath: "data/owncast.db",
YPEnabled: false,
YPServer: "https://yp.owncast.online",
WebServerPort: 8080,
RTMPServerPort: 1935,
StreamKey: "abc123",
StreamVariants: []models.StreamOutputVariant{
{
IsAudioPassthrough: true,
VideoBitrate: 1200,
EncoderPreset: "veryfast",
Framerate: 24,
},
},
}
}

+ 5
- 5
config/verifyInstall.go View File

@ -12,8 +12,8 @@ import (
"golang.org/x/mod/semver"
)
// verifyFFMpegPath verifies that the path exists, is a file, and is executable.
func verifyFFMpegPath(path string) error {
// VerifyFFMpegPath verifies that the path exists, is a file, and is executable.
func VerifyFFMpegPath(path string) error {
stat, err := os.Stat(path)
if os.IsNotExist(err) {
@ -39,12 +39,12 @@ func verifyFFMpegPath(path string) error {
response := string(out)
if response == "" {
return fmt.Errorf("unable to determine the version of your ffmpeg installation at %s. you may experience issues with video.", path)
return fmt.Errorf("unable to determine the version of your ffmpeg installation at %s you may experience issues with video", path)
}
responseComponents := strings.Split(response, " ")
if len(responseComponents) < 3 {
log.Debugf("unable to determine the version of your ffmpeg installation at %s. you may experience issues with video.", path)
log.Debugf("unable to determine the version of your ffmpeg installation at %s you may experience issues with video", path)
return nil
}
@ -59,7 +59,7 @@ func verifyFFMpegPath(path string) error {
}
if semver.Compare(versionString, FfmpegSuggestedVersion) == -1 {
return fmt.Errorf("your %s version of ffmpeg at %s may be older than the suggested version of %s. you may experience issues with video.", versionString, path, FfmpegSuggestedVersion)
return fmt.Errorf("your %s version of ffmpeg at %s may be older than the suggested version of %s you may experience issues with video", versionString, path, FfmpegSuggestedVersion)
}
return nil


+ 100
- 0
controllers/admin/accessToken.go View File

@ -0,0 +1,100 @@
package admin
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
)
type deleteTokenRequest struct {
Token string `json:"token"`
}
type createTokenRequest struct {
Name string `json:"name"`
Scopes []string `json:"scopes"`
}
// CreateAccessToken will generate a 3rd party access token.
func CreateAccessToken(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var request createTokenRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
// Verify all the scopes provided are valid
if !models.HasValidScopes(request.Scopes) {
controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
return
}
token, err := utils.GenerateAccessToken()
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
if err := data.InsertToken(token, request.Name, request.Scopes); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
controllers.WriteResponse(w, models.AccessToken{
Token: token,
Name: request.Name,
Scopes: request.Scopes,
Timestamp: time.Now(),
LastUsed: nil,
})
}
// GetAccessTokens will return all 3rd party access tokens.
func GetAccessTokens(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
tokens, err := data.GetAccessTokens()
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteResponse(w, tokens)
}
// DeleteAccessToken will return a single 3rd party access token.
func DeleteAccessToken(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request deleteTokenRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
if request.Token == "" {
controllers.BadRequestHandler(w, errors.New("must provide a token"))
return
}
if err := data.DeleteToken(request.Token); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "deleted token")
}

+ 0
- 36
controllers/admin/changePageContent.go View File

@ -1,36 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// ChangeExtraPageContent will change the optional page content.
func ChangeExtraPageContent(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request changeExtraPageContentRequest
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
config.Config.InstanceDetails.ExtraPageContent = utils.RenderSimpleMarkdown(request.Key)
controllers.WriteSimpleResponse(w, true, "changed")
}
type changeExtraPageContentRequest struct {
Key string `json:"content"`
}

+ 0
- 35
controllers/admin/changeStreamKey.go View File

@ -1,35 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
log "github.com/sirupsen/logrus"
)
// ChangeStreamKey will change the stream key (in memory).
func ChangeStreamKey(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request changeStreamKeyRequest
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
config.Config.VideoSettings.StreamingKey = request.Key
controllers.WriteSimpleResponse(w, true, "changed")
}
type changeStreamKeyRequest struct {
Key string `json:"key"`
}

+ 0
- 35
controllers/admin/changeStreamName.go View File

@ -1,35 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
log "github.com/sirupsen/logrus"
)
// ChangeStreamName will change the stream key (in memory).
func ChangeStreamName(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request changeStreamNameRequest
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
config.Config.InstanceDetails.Name = request.Name
controllers.WriteSimpleResponse(w, true, "changed")
}
type changeStreamNameRequest struct {
Name string `json:"name"`
}

+ 0
- 35
controllers/admin/changeStreamTags.go View File

@ -1,35 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
log "github.com/sirupsen/logrus"
)
// ChangeStreamTags will change the stream key (in memory).
func ChangeStreamTags(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request changeStreamTagsRequest
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
config.Config.InstanceDetails.Tags = request.Tags
controllers.WriteSimpleResponse(w, true, "changed")
}
type changeStreamTagsRequest struct {
Tags []string `json:"tags"`
}

+ 0
- 35
controllers/admin/changeStreamTitle.go View File

@ -1,35 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
log "github.com/sirupsen/logrus"
)
// ChangeStreamTitle will change the stream key (in memory).
func ChangeStreamTitle(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request changeStreamTitleRequest
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
config.Config.InstanceDetails.Title = request.Title
controllers.WriteSimpleResponse(w, true, "changed")
}
type changeStreamTitleRequest struct {
Title string `json:"title"`
}

+ 97
- 10
controllers/admin/chat.go View File

@ -4,36 +4,36 @@ package admin
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
"github.com/teris-io/shortid"
)
// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request messageVisibilityUpdateRequest // creates an empty struc
var request messageVisibilityUpdateRequest
err := decoder.Decode(&request) // decode the json into `request`
err := decoder.Decode(&request)
if err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
// // make sql update call here.
// // := means create a new var
// _db := data.GetDatabase()
// updateMessageVisibility(_db, request)
if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
@ -49,12 +49,99 @@ type messageVisibilityUpdateRequest struct {
// GetChatMessages returns all of the chat messages, unfiltered.
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
// middleware.EnableCors(&w)
w.Header().Set("Content-Type", "application/json")
messages := core.GetAllChatMessages(false)
messages := core.GetModerationChatMessages()
if err := json.NewEncoder(w).Encode(messages); err != nil {
log.Errorln(err)
}
}
// SendSystemMessage will send an official "SYSTEM" message
// to chat on behalf of your server.
func SendSystemMessage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var message models.ChatEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
message.MessageType = models.SystemMessageSent
message.Author = data.GetServerName()
message.ClientID = "owncast-server"
message.ID = shortid.MustGenerate()
message.Visible = true
message.SetDefaults()
message.RenderAndSanitizeMessageBody()
if err := core.SendMessageToChat(message); err != nil {
controllers.BadRequestHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "sent")
}
// SendUserMessage will send a message to chat on behalf of a user.
func SendUserMessage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var message models.ChatEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
if !message.Valid() {
controllers.BadRequestHandler(w, errors.New("invalid chat message; id, author, and body are required"))
return
}
message.MessageType = models.MessageSent
message.ClientID = "external-request"
message.ID = shortid.MustGenerate()
message.Visible = true
message.SetDefaults()
message.RenderAndSanitizeMessageBody()
if err := core.SendMessageToChat(message); err != nil {
controllers.BadRequestHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "sent")
}
// SendChatAction will send a generic chat action.
func SendChatAction(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var message models.ChatEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
message.MessageType = models.ChatActionSent
message.ClientID = "external-request"
message.ID = shortid.MustGenerate()
message.Visible = true
if message.Author != "" {
message.Body = fmt.Sprintf("%s %s", message.Author, message.Body)
}
message.SetDefaults()
if err := core.SendMessageToChat(message); err != nil {
controllers.BadRequestHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "sent")
}

+ 475
- 0
controllers/admin/config.go View File

@ -0,0 +1,475 @@
package admin
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"reflect"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// ConfigValue is a container object that holds a value, is encoded, and saved to the database.
type ConfigValue struct {
Value interface{} `json:"value"`
}
// SetTags will handle the web config request to set tags.
func SetTags(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValues, success := getValuesFromRequest(w, r)
if !success {
return
}
var tagStrings []string
for _, tag := range configValues {
tagStrings = append(tagStrings, tag.Value.(string))
}
if err := data.SetServerMetadataTags(tagStrings); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetStreamTitle will handle the web config request to set the current stream title.
func SetStreamTitle(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
value := configValue.Value.(string)
if err := data.SetStreamTitle(value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if value != "" {
sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value))
}
controllers.WriteSimpleResponse(w, true, "changed")
}
func sendSystemChatAction(messageText string) {
message := models.ChatEvent{}
message.Body = messageText
message.MessageType = models.ChatActionSent
message.ClientID = "internal-server"
message.SetDefaults()
if err := core.SendMessageToChat(message); err != nil {
log.Errorln(err)
}
}
// SetServerName will handle the web config request to set the server's name.
func SetServerName(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerName(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetServerSummary will handle the web config request to set the about/summary text.
func SetServerSummary(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerSummary(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetExtraPageContent will handle the web config request to set the page markdown content.
func SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetExtraPageBodyContent(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetStreamKey will handle the web config request to set the server stream key.
func SetStreamKey(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetStreamKey(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetLogoPath will handle the web config request to validate and set the logo path.
func SetLogoPath(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
imgPath := configValue.Value.(string)
fullPath := filepath.Join("data", imgPath)
if !utils.DoesFileExists(fullPath) {
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("%s does not exist", fullPath))
return
}
if err := data.SetLogoPath(imgPath); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetNSFW will handle the web config request to set the NSFW flag.
func SetNSFW(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetNSFW(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetFfmpegPath will handle the web config request to validate and set an updated copy of ffmpg.
func SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
path := configValue.Value.(string)
if err := utils.VerifyFFMpegPath(path); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetWebServerPort will handle the web config request to set the server's HTTP port.
func SetWebServerPort(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetHTTPPortNumber(configValue.Value.(float64)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "http port set")
}
// SetRTMPServerPort will handle the web config request to set the inbound RTMP port.
func SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetRTMPPortNumber(configValue.Value.(float64)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "rtmp port set")
}
// SetServerURL will handle the web config request to set the full server URL.
func SetServerURL(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerURL(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "server url set")
}
// SetDirectoryEnabled will handle the web config request to enable or disable directory registration.
func SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetDirectoryEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "directory state changed")
}
// SetStreamLatencyLevel will handle the web config request to set the stream latency level.
func SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil {
controllers.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "set stream latency")
}
// SetS3Configuration will handle the web config request to set the storage configuration.
func SetS3Configuration(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type s3ConfigurationRequest struct {
Value models.S3 `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var newS3Config s3ConfigurationRequest
if err := decoder.Decode(&newS3Config); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update s3 config with provided values")
return
}
if newS3Config.Value.Enabled {
if newS3Config.Value.Endpoint == "" || !utils.IsValidUrl((newS3Config.Value.Endpoint)) {
controllers.WriteSimpleResponse(w, false, "s3 support requires an endpoint")
return
}
if newS3Config.Value.AccessKey == "" || newS3Config.Value.Secret == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires an access key and secret")
return
}
if newS3Config.Value.Region == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires a region and endpoint")
return
}
if newS3Config.Value.Bucket == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires a bucket created for storing public video segments")
return
}
}
data.SetS3Config(newS3Config.Value)
controllers.WriteSimpleResponse(w, true, "storage configuration changed")
}
// SetStreamOutputVariants will handle the web config request to set the video output stream variants.
func SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type streamOutputVariantRequest struct {
Value []models.StreamOutputVariant `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var videoVariants streamOutputVariantRequest
if err := decoder.Decode(&videoVariants); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values")
return
}
// Temporary: Convert the cpuUsageLevel to a preset. In the future we will have
// different codec models that will handle this for us and we won't
// be keeping track of presets at all. But for now...
presetMapping := []string{
"ultrafast",
"superfast",
"veryfast",
"faster",
"fast",
}
for i, variant := range videoVariants.Value {
preset := "superfast"
if variant.CPUUsageLevel > 0 && variant.CPUUsageLevel <= len(presetMapping) {
preset = presetMapping[variant.CPUUsageLevel-1]
}
variant.EncoderPreset = preset
videoVariants.Value[i] = variant
}
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "stream output variants updated")
}
// SetSocialHandles will handle the web config request to set the external social profile links.
func SetSocialHandles(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type socialHandlesRequest struct {
Value []models.SocialHandle `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var socialHandles socialHandlesRequest
if err := decoder.Decode(&socialHandles); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
if err := data.SetSocialHandles(socialHandles.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "social handles updated")
}
func requirePOST(w http.ResponseWriter, r *http.Request) bool {
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return false
}
return true
}
func getValueFromRequest(w http.ResponseWriter, r *http.Request) (ConfigValue, bool) {
decoder := json.NewDecoder(r.Body)
var configValue ConfigValue
if err := decoder.Decode(&configValue); err != nil {
log.Warnln(err)
controllers.WriteSimpleResponse(w, false, "unable to parse new value")
return configValue, false
}
return configValue, true
}
func getValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue, bool) {
var values []ConfigValue
decoder := json.NewDecoder(r.Body)
var configValue ConfigValue
if err := decoder.Decode(&configValue); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to parse array of values")
return values, false
}
object := reflect.ValueOf(configValue.Value)
for i := 0; i < object.Len(); i++ {
values = append(values, ConfigValue{Value: object.Index(i).Interface()})
}
return values, true
}

+ 57
- 24
controllers/admin/serverConfig.go View File

@ -5,35 +5,50 @@ import (
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// GetServerConfig gets the config details of the server.
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
var videoQualityVariants = make([]config.StreamQuality, 0)
for _, variant := range config.Config.GetVideoStreamQualities() {