diff --git a/config/config.go b/config/config.go index af3747e21..d5b2ca144 100644 --- a/config/config.go +++ b/config/config.go @@ -3,8 +3,6 @@ package config import ( "errors" "io/ioutil" - "os/exec" - "strings" "github.com/gabek/owncast/utils" log "github.com/sirupsen/logrus" @@ -13,6 +11,7 @@ import ( //Config contains a reference to the configuration var Config *config +var _default config type config struct { ChatDatabaseFilePath string `yaml:"chatDatabaseFile"` @@ -116,6 +115,10 @@ func (c *config) load(filePath string) error { } 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 && c.IPFS.Enabled { return errors.New("s3 and IPFS support cannot be enabled at the same time; choose one") } @@ -137,32 +140,12 @@ func (c *config) verifySettings() error { return nil } -func (c *config) GetFFMpegPath() string { - if c.FFMpegPath != "" { - return c.FFMpegPath - } - - cmd := exec.Command("which", "ffmpeg") - out, err := cmd.CombinedOutput() - if err != nil { - log.Panicln("Unable to determine path to ffmpeg. Please specify it in the config file.") - } - - path := strings.TrimSpace(string(out)) - - // Memoize it for future access - c.FFMpegPath = path - - return path -} - func (c *config) GetVideoSegmentSecondsLength() int { if c.VideoSettings.ChunkLengthInSeconds != 0 { return c.VideoSettings.ChunkLengthInSeconds } - // Default - return 4 + return _default.GetVideoSegmentSecondsLength() } func (c *config) GetPublicHLSSavePath() string { @@ -170,7 +153,7 @@ func (c *config) GetPublicHLSSavePath() string { return c.PublicHLSPath } - return "webroot/hls" + return _default.PublicHLSPath } func (c *config) GetPrivateHLSSavePath() string { @@ -178,7 +161,7 @@ func (c *config) GetPrivateHLSSavePath() string { return c.PrivateHLSPath } - return "hls" + return _default.PrivateHLSPath } func (c *config) GetPublicWebServerPort() int { @@ -186,8 +169,7 @@ func (c *config) GetPublicWebServerPort() int { return c.WebServerPort } - // Default web server port - return 8080 + return _default.WebServerPort } func (c *config) GetMaxNumberOfReferencedSegmentsInPlaylist() int { @@ -195,7 +177,7 @@ func (c *config) GetMaxNumberOfReferencedSegmentsInPlaylist() int { return c.Files.MaxNumberInPlaylist } - return 20 + return _default.GetMaxNumberOfReferencedSegmentsInPlaylist() } func (c *config) GetOfflineContentPath() string { @@ -204,12 +186,29 @@ func (c *config) GetOfflineContentPath() string { } // This is relative to the webroot, not the project root. - return "static/offline.m4v" + return _default.VideoSettings.OfflineContent +} + +func (c *config) GetFFMpegPath() string { + if c.FFMpegPath != "" { + return c.FFMpegPath + } + + return _default.FFMpegPath +} + +func (c *config) GetVideoStreamQualities() []StreamQuality { + if len(c.VideoSettings.StreamQualities) > 0 { + return c.VideoSettings.StreamQualities + } + + return _default.VideoSettings.StreamQualities } //Load tries to load the configuration file func Load(filePath string, versionInfo string) error { Config = new(config) + _default = getDefaults() if err := Config.load(filePath); err != nil { return err @@ -220,8 +219,10 @@ func Load(filePath string, versionInfo string) error { // Defaults // This is relative to the webroot, not the project root. + // Has to be set here instead of pulled from a getter + // since it's serialized to JSON. if Config.InstanceDetails.ExtraInfoFile == "" { - Config.InstanceDetails.ExtraInfoFile = "/static/content.md" + Config.InstanceDetails.ExtraInfoFile = _default.InstanceDetails.ExtraInfoFile } return Config.verifySettings() diff --git a/config/defaults.go b/config/defaults.go new file mode 100644 index 000000000..ded829273 --- /dev/null +++ b/config/defaults.go @@ -0,0 +1,40 @@ +package config + +import ( + "log" + "os/exec" + "strings" +) + +func getDefaults() config { + defaults := config{} + defaults.WebServerPort = 8080 + 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" + + defaultQuality := StreamQuality{ + IsAudioPassthrough: true, + VideoBitrate: 1200, + EncoderPreset: "veryfast", + } + defaults.VideoSettings.StreamQualities = []StreamQuality{defaultQuality} + + return defaults +} + +func getDefaultFFMpegPath() string { + cmd := exec.Command("which", "ffmpeg") + out, err := cmd.CombinedOutput() + if err != nil { + log.Panicln("Unable to determine path to ffmpeg. Please specify it in the config file.") + } + + path := strings.TrimSpace(string(out)) + + return path +} diff --git a/controllers/status.go b/controllers/status.go index b372651a5..78e62f1dc 100644 --- a/controllers/status.go +++ b/controllers/status.go @@ -13,6 +13,7 @@ func GetStatus(w http.ResponseWriter, r *http.Request) { middleware.EnableCors(&w) status := core.GetStatus() + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(status) } diff --git a/core/chat/chat.go b/core/chat/chat.go index 50a7b9bc4..2a548d1cc 100644 --- a/core/chat/chat.go +++ b/core/chat/chat.go @@ -19,15 +19,6 @@ func Setup(listener models.ChatListener) { pingCh := make(chan models.PingMessage) doneCh := make(chan bool) errCh := make(chan error) - - // Demo messages only. Remove me eventually!!! - messages = append(messages, models.ChatMessage{"", "Tom Nook", "I'll be there with Bells on! Ho ho!", "https://gamepedia.cursecdn.com/animalcrossingpocketcamp_gamepedia_en/thumb/4/4f/Timmy_Icon.png/120px-Timmy_Icon.png?version=87b38d7d6130411d113486c2db151385", "demo-message-1", "CHAT", true, time.Now()}) - messages = append(messages, models.ChatMessage{"", "Redd", "Fool me once, shame on you. Fool me twice, stop foolin' me.", "https://vignette.wikia.nocookie.net/animalcrossing/images/3/3d/Redd2.gif/revision/latest?cb=20100710004252", "demo-message-2", "CHAT", true, time.Now()}) - messages = append(messages, models.ChatMessage{"", "Kevin", "You just caught me before I was about to go work out weeweewee!", "https://vignette.wikia.nocookie.net/animalcrossing/images/2/20/NH-Kevin_poster.png/revision/latest/scale-to-width-down/100?cb=20200410185817", "demo-message-3", "CHAT", true, time.Now()}) - messages = append(messages, models.ChatMessage{"", "Isabelle", " Isabelle is the mayor's highly capable secretary. She can be forgetful sometimes, but you can always count on her for information about the town. She wears her hair up in a bun that makes her look like a shih tzu. Mostly because she is one! She also has a twin brother named Digby.", "https://dodo.ac/np/images/thumb/7/7b/IsabelleTrophyWiiU.png/200px-IsabelleTrophyWiiU.png", "demo-message-4", "CHAT", true, time.Now()}) - messages = append(messages, models.ChatMessage{"", "Judy", "myohmy, I'm dancing my dreams away.", "https://vignette.wikia.nocookie.net/animalcrossing/images/5/50/NH-Judy_poster.png/revision/latest/scale-to-width-down/100?cb=20200522063219", "demo-message-5", "CHAT", true, time.Now()}) - messages = append(messages, models.ChatMessage{"", "Blathers", "Blathers is an owl with brown feathers. His face is white and he has a yellow beak. His arms are wing shaped and he has yellow talons. His eyes are very big with small black irises. He also has big pink cheek circles on his cheeks. His belly appears to be checkered in diamonds with light brown and white squares, similar to an argyle vest, which is traditionally associated with academia. His green bowtie further alludes to his academic nature.", "https://vignette.wikia.nocookie.net/animalcrossing/images/b/b3/NH-character-Blathers.png/revision/latest?cb=20200229053519", "demo-message-6", "CHAT", true, time.Now()}) - messages = append(messages, getChatHistory()...) _server = &server{ diff --git a/core/chat/client.go b/core/chat/client.go index 7436336fb..2c5553047 100644 --- a/core/chat/client.go +++ b/core/chat/client.go @@ -111,6 +111,7 @@ func (c *Client) listenRead() { msg.ID = id msg.MessageType = "CHAT" msg.Timestamp = time.Now() + msg.Visible = true if err := websocket.JSON.Receive(c.ws, &msg); err == io.EOF { c.doneCh <- true diff --git a/core/chat/server.go b/core/chat/server.go index 62e9f4bf6..4d8cabc12 100644 --- a/core/chat/server.go +++ b/core/chat/server.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/net/websocket" + "github.com/gabek/owncast/config" "github.com/gabek/owncast/models" ) @@ -56,12 +57,6 @@ func (s *server) err(err error) { s.errCh <- err } -func (s *server) sendPastMessages(c *Client) { - for _, msg := range s.Messages { - c.Write(msg) - } -} - func (s *server) sendAll(msg models.ChatMessage) { for _, c := range s.Clients { c.Write(msg) @@ -104,7 +99,7 @@ func (s *server) Listen() { s.Clients[c.id] = c s.listener.ClientAdded(c.id) - s.sendPastMessages(c) + s.sendWelcomeMessageToClient(c) // remove a client case c := <-s.delCh: @@ -128,3 +123,14 @@ func (s *server) Listen() { } } } + +func (s *server) sendWelcomeMessageToClient(c *Client) { + go func() { + time.Sleep(5 * time.Second) + + initialChatMessageText := fmt.Sprintf("Welcome to %s! %s", config.Config.InstanceDetails.Title, config.Config.InstanceDetails.Summary) + initialMessage := models.ChatMessage{"owncast-server", config.Config.InstanceDetails.Name, initialChatMessageText, config.Config.InstanceDetails.Logo["small"], "initial-message-1", "CHAT", true, time.Now()} + c.Write(initialMessage) + }() + +} diff --git a/doc/config-example-full.yaml b/doc/config-example-full.yaml index ac7bca6dc..9edcaf821 100644 --- a/doc/config-example-full.yaml +++ b/doc/config-example-full.yaml @@ -55,7 +55,8 @@ videoSettings: audioPassthrough: true # The slower the preset the higher quality the video is. # Select a preset from https://trac.ffmpeg.org/wiki/Encode/H.264 - encoderPreset: superfast + # "superfast" and "ultrafast" are generally not recommended since they look bad. + encoderPreset: veryfast - medium: videoBitrate: 800 diff --git a/webroot/index.html b/webroot/index.html index fb9726a81..4d0845695 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -16,7 +16,7 @@ -
+

@@ -84,7 +84,7 @@ -
+
- +

-
+
@@ -165,7 +165,6 @@
- @@ -179,5 +178,32 @@ app.init(); })(); + + \ No newline at end of file diff --git a/webroot/js/app.js b/webroot/js/app.js index 5da4e31ef..5d9d5dc6e 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -46,7 +46,6 @@ class Owncast { el: '#app-container', data: { isOnline: false, - layout: hasTouchScreen() ? 'touch' : 'desktop', messages: [], overallMaxViewerCount: 0, sessionMaxViewerCount: 0, @@ -86,6 +85,8 @@ class Owncast { onError: this.handlePlayerError, }); this.player.init(); + + this.getChatHistory(); }; setConfigData(data) { @@ -132,17 +133,21 @@ class Owncast { return; } const message = new Message(model); - const existing = this.vueApp.messages.filter(function (item) { - return item.id === message.id; - }) - if (existing.length === 0 || !existing) { - this.vueApp.messages = [...this.vueApp.messages, message]; - } + this.addMessage(message); }; this.websocket = ws; this.messagingInterface.setWebsocket(this.websocket); }; + addMessage(message) { + const existing = this.vueApp.messages.filter(function (item) { + return item.id === message.id; + }) + if (existing.length === 0 || !existing) { + this.vueApp.messages = [...this.vueApp.messages, message]; + } + } + // fetch /config data getConfig() { fetch(URL_CONFIG) @@ -296,4 +301,18 @@ class Owncast { this.handleOfflineMode(); // stop timers? }; + + async getChatHistory() { + const url = "/chat"; + const response = await fetch(url); + const data = await response.json(); + const messages = data.map(function (message) { + return new Message(message); + }) + this.setChatHistory(messages); + } + + setChatHistory(messages) { + this.vueApp.messages = messages.concat(this.vueApp.messages); + } }; diff --git a/webroot/js/message.js b/webroot/js/message.js index 698deb685..5744149b7 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -74,13 +74,11 @@ class MessagingInterface { this.initLocalStates(); if (hasTouchScreen()) { - this.scrollableMessagesContainer = document.body; + setVHvar(); + window.addEventListener("orientationchange", setVHvar); this.tagAppContainer.classList.add('touch-screen'); - window.onorientationchange = this.handleOrientationChange.bind(this); - this.handleOrientationChange(); - } else { - this.tagAppContainer.classList.add('desktop'); } + } setWebsocket(socket) { @@ -93,7 +91,7 @@ class MessagingInterface { getLocalStorage(KEY_AVATAR) || generateAvatar(`${this.username}${Date.now()}`); this.updateUsernameFields(this.username); - this.chatDisplayed = getLocalStorage(KEY_CHAT_DISPLAYED) || false; + this.chatDisplayed = getLocalStorage(KEY_CHAT_DISPLAYED) || true; this.displayChat(); } @@ -112,22 +110,9 @@ class MessagingInterface { this.tagAppContainer.classList.add('no-chat'); this.tagAppContainer.classList.remove('chat'); } + this.setChatPlaceholderText(); } - handleOrientationChange() { - var isPortrait = Math.abs(window.orientation % 180) === 0; - if(!isPortrait) { - if (document.body.clientWidth < 1024) { - this.tagAppContainer.classList.add('no-chat'); - this.tagAppContainer.classList.add('landscape'); - } - } else { - if (this.chatDisplayed) { - this.tagAppContainer.classList.remove('no-chat'); - } - this.tagAppContainer.classList.remove('landscape'); - } - } handleChatToggle() { this.chatDisplayed = !this.chatDisplayed; @@ -241,6 +226,12 @@ class MessagingInterface { // clear out things. this.formMessageInput.value = ''; this.tagMessageFormWarning.innerText = ''; + + const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); + if (!hasSentFirstChatMessage) { + setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); + this.setChatPlaceholderText(); + } } disableChat() { @@ -248,14 +239,22 @@ class MessagingInterface { this.formMessageInput.disabled = true; this.formMessageInput.placeholder = "Chat is offline." } - // also show "disabled" text/message somewhere. } enableChat() { if (this.formMessageInput) { this.formMessageInput.disabled = false; - this.formMessageInput.placeholder = "Message" + this.setChatPlaceholderText(); } } + + setChatPlaceholderText() { + const firstMessageChatPlacholderText = "Type here to chat, no account necessary."; + const chatPlaceholderText = "Message" + + const hasSentFirstChatMessage = getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT); + this.formMessageInput.placeholder = hasSentFirstChatMessage ? chatPlaceholderText : firstMessageChatPlacholderText + } + // handle Vue.js message display onReceivedMessages(newMessages, oldMessages) { if (newMessages.length !== oldMessages.length) { diff --git a/webroot/js/utils.js b/webroot/js/utils.js index 8114819db..3ecebe06c 100644 --- a/webroot/js/utils.js +++ b/webroot/js/utils.js @@ -32,6 +32,9 @@ const VIDEO_SRC = { const VIDEO_OPTIONS = { autoplay: false, liveui: true, // try this + liveTracker: { + trackingThreshold: 0, + }, sources: [VIDEO_SRC], }; @@ -39,6 +42,7 @@ const VIDEO_OPTIONS = { const KEY_USERNAME = 'owncast_username'; const KEY_AVATAR = 'owncast_avatar'; const KEY_CHAT_DISPLAYED = 'owncast_chat'; +const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent'; const TIMER_STATUS_UPDATE = 5000; // ms const TIMER_WEBSOCKET_RECONNECT = 5000; // ms @@ -154,4 +158,11 @@ function secondsToHMMSS(seconds = 0) { const secsString = secs < 10 ? `0${secs}` : `${secs}`; return hoursString + minString + secsString; -} \ No newline at end of file +} + +function setVHvar() { + var vh = window.innerHeight * 0.01; + // Then we set the value in the --vh custom property to the root of the document + document.documentElement.style.setProperty('--vh', `${vh}px`); + console.log("== new vh", vh) +} diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index 91c6bd63a..58a8df08a 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -11,10 +11,10 @@ body { font-size: 14px; } - a:hover { text-decoration: underline; } +/* vuejs attribute to hide things before content ready */ [v-cloak] { visibility: hidden; } ::-webkit-scrollbar { @@ -323,9 +323,11 @@ h2 { #video { transition: opacity .5s; opacity: 0; + pointer-events: none; } .online #video { opacity: 1; + pointer-events: auto; } @@ -364,6 +366,9 @@ h2 { flex-direction: column; justify-content: flex-end; } +.touch-screen #chat-container { + height: calc(100vh - var(--header-height) - 3vh); +} #messages-container { @@ -426,83 +431,6 @@ h2 { /* ************************************************8 */ -.landscape #chat-toggle { - display: none; -} - -/* ************************************************8 */ -/* ************************************************8 */ - -.touch-screen header { - position: relative; -} -.touch-screen #top-content { - position: fixed; - left: 0; - top: 0; - width: 100%; - z-index: 10; -} - - -.touch-screen .user-content { - flex-direction: column; - align-content: center; -} -.touch-screen .user-image { - margin: auto; -} - - -.touch-screen #stream-info { - height: 2.5em; - overflow: hidden; -} - -.touch-screen #chat-container-wrap { - display: flex; - align-items: flex-end; - width: 100%; - height: auto; - flex-direction: column; - margin-top: calc(var(--header-height) + var(--video-container-height) + 2.5em); -} -.touch-screen #chat-container { - height: auto; - position: relative; - right: unset; - top: unset; - width: 100%; - z-index: 1; -} -.touch-screen.chat #video-container, -.touch-screen.chat #stream-info, -.touch-screen.chat #user-content { - width: 100%; -} - - -.touch-screen #video-container { - margin-top: 0; -} -.touch-screen .owncast-video-container { - height: 100%; -} - -.touch-screen #user-content-touch { - display: none; -} -.touch-screen #chat-container { - display: block; -} -.touch-screen.no-chat #user-content-touch { - display: block; -} -.touch-screen.no-chat #chat-container { - display: none; -} - -/* ************************************************8 */ @media screen and (max-width: 860px) { :root { @@ -516,33 +444,12 @@ h2 { } +/* single col layout */ @media screen and (max-width: 640px ) { :root { - --video-container-height: 36vh; - } - - .desktop { - --video-container-height: 50vh; + --right-col-width: 0; + --video-container-height: 40vh; } - .desktop #chat-container { - height: auto; - position: relative; - right: unset; - top: unset; - width: 100%; - z-index: 1; - } - .desktop.chat #video-container, - .desktop.chat #stream-info, - .desktop.chat #user-content { - width: 100%; - } - .desktop #footer, - .desktop.chat #user-content { - display: none; - } - - #logo-container { display: none; } @@ -552,29 +459,60 @@ h2 { #user-options-container { max-width: 41%; } -} -@media screen and (orientation: landscape) and (min-width: 1024px) { - :root { - --video-container-height: 65vh; + #chat-container { + width: 100%; + position: static; + /* min-height: calc(100vh - var(--header-height)); */ + height: calc(100vh - var(--header-height) - var(--video-container-height) - 3vh) } -} - -@media screen and (orientation: landscape) and (max-width: 1024px) { - :root .landscape { - --video-container-height: 75vh; + #messages-container { + min-height: unset; + } + #user-content { + width: 100%; + } + #stream-info { + width: 100%; } - .touch-screen.landscape #chat-container-wrap { - margin-top: calc(var(--header-height) + var(--video-container-height)); + #video-container { + width: 100%; } - .touch-screen.landscape .user-content { - display: block; + .chat #video-container { + width: 100%; } - .touch-screen.landscape #chat-container { + .chat #user-content { display: none; } - .touch-screen.landscape #chat-toggle { + .chat footer { display: none; } } +/* try not making the video fixed position for now */ +@media (min-height: 861px) { + /* main { + position: fixed; + z-index: 9; + width: 100%; + } + #user-content { + margin-top: calc(var(--video-container-height) + var(--header-height) + 2em) + } */ +} + + + + + + + +@media screen and (max-height: 860px ) { + :root { + --video-container-height: 40vh; + } + .user-content { + flex-direction: column; + } +} +