Merge branch 'master' into gek/current-stream-duration

This commit is contained in:
Ginger Wong 2020-07-19 15:17:03 -07:00
commit a3613612eb
12 changed files with 239 additions and 205 deletions

View file

@ -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()

40
config/defaults.go Normal file
View file

@ -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
}

View file

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

View file

@ -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{

View file

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

View file

@ -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)
}()
}

View file

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

View file

@ -16,7 +16,7 @@
<body class="bg-gray-300 text-gray-800">
<div id="app-container" class="flex no-chat">
<div id="app-container" class="flex chat">
<div id="top-content">
<header class="flex border-b border-gray-900 border-solid shadow-md">
<h1 v-cloak class="flex text-gray-400">
@ -84,7 +84,7 @@
</section>
</main>
<section id="user-content" v-if="layout === 'desktop'" aria-label="User information">
<section id="user-content" aria-label="User information">
<user-details
v-bind:logo="logo"
v-bind:platforms="socialHandles"
@ -96,13 +96,13 @@
</section>
<owncast-footer v-if="layout === 'desktop'" v-bind:app-version="appVersion"></owncast-footer>
<owncast-footer v-bind:app-version="appVersion"></owncast-footer>
</div>
<section id="chat-container-wrap" class="flex">
<div v-if="layout !== 'desktop'" id="user-content-touch">
<!-- <div v-if="layout !== 'desktop'" id="user-content-touch">
<user-details
v-bind:logo="logo"
v-bind:platforms="socialHandles"
@ -114,7 +114,7 @@
<owncast-footer v-bind:app-version="appVersion"></owncast-footer>
</div>
</div> -->
<div id="chat-container" class="bg-gray-800">
<div id="messages-container">
@ -165,7 +165,6 @@
</div>
<script src="js/usercolors.js"></script>
<script src="js/utils.js"></script>
<script src="js/message.js"></script>
@ -179,5 +178,32 @@
app.init();
})();
</script>
<noscript>
<style>
[v-cloak] { display: none; }
.noscript {
text-align: center;
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.noscript a {
display: inline;
color: blue;
text-decoration: underline;
}
</style>
<div class="noscript">
<img src="https://github.com/gabek/owncast/raw/master/doc/logo.png">
<br/>
<p>
This <a href="https://github.com/gabek/owncast" target="_blank">Owncast</a> stream requires Javascript to play.
</p>
</div>
</noscript>
</body>
</html>

View file

@ -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,16 +133,20 @@ class Owncast {
return;
}
const message = new Message(model);
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];
}
};
this.websocket = ws;
this.messagingInterface.setWebsocket(this.websocket);
};
}
// fetch /config data
getConfig() {
@ -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);
}
};

View file

@ -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) {

View file

@ -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
@ -155,3 +159,10 @@ function secondsToHMMSS(seconds = 0) {
return hoursString + minString + secsString;
}
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)
}

View file

@ -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;
--right-col-width: 0;
--video-container-height: 40vh;
}
.desktop {
--video-container-height: 50vh;
}
.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%;
}
#chat-container {
width: 100%;
position: static;
/* min-height: calc(100vh - var(--header-height)); */
height: calc(100vh - var(--header-height) - var(--video-container-height) - 3vh)
}
#messages-container {
min-height: unset;
}
#user-content {
width: 100%;
}
#stream-info {
width: 100%;
}
#video-container {
width: 100%;
}
.chat #video-container {
width: 100%;
}
.chat #user-content {
display: none;
}
.chat footer {
display: none;
}
}
@media screen and (orientation: landscape) and (min-width: 1024px) {
/* 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: 65vh;
}
}
@media screen and (orientation: landscape) and (max-width: 1024px) {
:root .landscape {
--video-container-height: 75vh;
}
.touch-screen.landscape #chat-container-wrap {
margin-top: calc(var(--header-height) + var(--video-container-height));
}
.touch-screen.landscape .user-content {
display: block;
}
.touch-screen.landscape #chat-container {
display: none;
}
.touch-screen.landscape #chat-toggle {
display: none;
--video-container-height: 40vh;
}
.user-content {
flex-direction: column;
}
}