Compare commits

..

1 Commits

Author SHA1 Message Date
langhuihui
87776b9540 增加log查看和config查看 2020-01-31 14:22:21 +08:00
14 changed files with 410 additions and 289 deletions

1
dashboard/dist/css/app.93f68a59.css vendored Normal file
View File

@@ -0,0 +1 @@
#app,body,html{height:100%}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#184c18;position:relative}#app>div:first-child{position:absolute;top:10px;left:30px;font-size:x-large}.content{padding-top:60px}.feature-title[data-v-54efad41]{color:#eb5e46;font-weight:700;font-size:larger}p[data-v-54efad41]{margin:30px;font-size:20px}img[data-v-54efad41]{margin:20px}.root[data-v-e34eab40]{background:#d3d3d3}.root>img[data-v-e34eab40]{width:300px;margin:30px}.log-container{overflow-y:auto;max-height:360px}@-webkit-keyframes recording-data-v-1ed98600{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}@keyframes recording-data-v-1ed98600{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}.recording[data-v-1ed98600]{-webkit-animation:recording-data-v-1ed98600 1s infinite;animation:recording-data-v-1ed98600 1s infinite}.layout[data-v-1ed98600]{padding-bottom:30px;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.room[data-v-1ed98600]{width:250px;margin:10px;text-align:left}.empty[data-v-1ed98600]{color:#eb5e46;width:100%;min-height:500px;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.empty[data-v-1ed98600],.status[data-v-1ed98600]{display:-webkit-box;display:-ms-flexbox;display:flex}.status[data-v-1ed98600]{position:fixed;left:5px;bottom:10px}.status>div[data-v-1ed98600]{margin:0 5px}

View File

@@ -1 +0,0 @@
#app,body,html{height:100%}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#184c18;position:relative}#app>div:first-child{position:absolute;top:10px;left:30px;font-size:x-large}.content{padding-top:60px}.feature-title[data-v-54efad41]{color:#eb5e46;font-weight:700;font-size:larger}p[data-v-54efad41]{margin:30px;font-size:20px}img[data-v-54efad41]{margin:20px}.root[data-v-e34eab40]{background:#d3d3d3}.root>img[data-v-e34eab40]{width:300px;margin:30px}@-webkit-keyframes recording-data-v-65ce2c22{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}@keyframes recording-data-v-65ce2c22{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}.recording[data-v-65ce2c22]{-webkit-animation:recording-data-v-65ce2c22 1s infinite;animation:recording-data-v-65ce2c22 1s infinite}.layout[data-v-65ce2c22]{padding-bottom:30px;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.room[data-v-65ce2c22]{width:250px;margin:10px;text-align:left}.empty[data-v-65ce2c22]{color:#eb5e46;width:100%;min-height:500px;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.empty[data-v-65ce2c22],.status[data-v-65ce2c22]{display:-webkit-box;display:-ms-flexbox;display:flex}.status[data-v-65ce2c22]{position:fixed;left:5px;bottom:10px}.status>div[data-v-65ce2c22]{margin:0 5px}

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Monibuca</title><script src=jessibuca/ajax.js></script><script src=jessibuca/renderer.js></script><link href=/css/app.b2f5eee5.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.2d98a4c2.js rel=preload as=script><link href=/js/chunk-vendors.ae8ac63d.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.b2f5eee5.css rel=stylesheet></head><body><noscript><strong>We're sorry but dashboard doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.ae8ac63d.js></script><script src=/js/app.2d98a4c2.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>Monibuca</title><script src=jessibuca/ajax.js></script><script src=jessibuca/renderer.js></script><link href=/css/app.93f68a59.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.00b4a97a.js rel=preload as=script><link href=/js/chunk-vendors.ae8ac63d.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.93f68a59.css rel=stylesheet></head><body><noscript><strong>We're sorry but dashboard doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.ae8ac63d.js></script><script src=/js/app.00b4a97a.js></script></body></html>

2
dashboard/dist/js/app.00b4a97a.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/dist/js/app.00b4a97a.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
<template>
<Modal v-bind="$attrs" draggable v-on="$listeners" @on-visible-change="onVisible" title="配置">
<div>
<pre>{{config}}</pre>
</div>
</Modal>
</template>
<script>
export default {
data() {
return {
config: ""
};
},
methods: {
onVisible(visible) {
if (visible) {
window.ajax.get(
"//" + location.host + "/api/config",
{},
x => (this.config = x)
);
}
}
}
};
</script>
<style>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<Modal v-bind="$attrs" draggable v-on="$listeners" @on-visible-change="onVisible" title="日志跟踪">
<div ref="logContainer" class="log-container">
<pre><template v-for="item in $store.state.logs">{{item+"\n"}}</template></pre>
</div>
<div slot="footer">
自动滚动
<Switch v-model="autoScroll" />
</div>
</Modal>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
autoScroll: true
};
},
methods: {
...mapActions(["fetchLogs", "stopFetchLogs"]),
onVisible(visible) {
if (visible) {
this.fetchLogs();
} else {
this.stopFetchLogs();
}
}
},
updated() {
if (this.autoScroll) {
this.$refs.logContainer.scrollTop = this.$refs.logContainer.offsetHeight;
}
}
};
</script>
<style>
.log-container {
overflow-y: auto;
max-height: 360px;
}
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<Modal v-bind="$attrs" draggable v-on="$listeners" title="录制的视频"> <Modal v-bind="$attrs" draggable v-on="$listeners" title="录制的视频" @on-visible-change="onVisible" :z-index="900">
<List> <List>
<ListItem v-for="item in data" :key="item"> <ListItem v-for="item in data" :key="item">
<ListItemMeta :title="item.Path"> <ListItemMeta :title="item.Path">
@@ -39,7 +39,8 @@ export default {
{ streamPath: item.Path.replace(".flv", "") }, { streamPath: item.Path.replace(".flv", "") },
x => { x => {
if (x == "success") { if (x == "success") {
this.$Message.success("开始发布"); this.onVisible(true)
this.$Message.success("删除成功");
} else { } else {
this.$Message.error(x); this.$Message.error(x);
} }
@@ -90,9 +91,9 @@ export default {
} else { } else {
return value + "ms"; return value + "ms";
} }
}
}, },
mounted() { onVisible(visible){
if(visible){
window.ajax.getJSON( window.ajax.getJSON(
"//" + location.host + "/api/record/flv/list", "//" + location.host + "/api/record/flv/list",
{}, {},
@@ -101,6 +102,8 @@ export default {
} }
); );
} }
}
}
}; };
</script> </script>

View File

@@ -3,40 +3,55 @@ import Vuex from 'vuex'
Vue.use(Vuex) Vue.use(Vuex)
let summaryES = null let summaryES = null
let logsES = null
export default new Vuex.Store({ export default new Vuex.Store({
state: { state: {
summary:{ summary: {
NetWork:[], NetWork: [],
Rooms:[], Rooms: [],
Memory:{ Memory: {
Used: 0, Used: 0,
Usage: 0 Usage: 0
}, },
CPUUsage:0, CPUUsage: 0,
HardDisk:{ HardDisk: {
Used: 0, Used: 0,
Usage: 0 Usage: 0
} }
} }, logs: []
}, },
mutations: { mutations: {
update(state,payload){ update(state, payload) {
Object.assign(state,payload) Object.assign(state, payload)
},
addLog(state, payload) {
state.logs.push(payload)
} }
}, },
actions: { actions: {
fetchSummary({commit}){ fetchSummary({ commit }) {
summaryES = new EventSource( summaryES = new EventSource(
"//" + location.host + "/api/summary" "//" + location.host + "/api/summary"
); );
summaryES.onmessage = evt=>{ summaryES.onmessage = evt => {
if (!evt.data) return if (!evt.data) return
let summary = JSON.parse(evt.data) let summary = JSON.parse(evt.data)
commit("update",{summary}) commit("update", { summary })
} }
}, },
stopFetchSummary(){ fetchLogs({ commit }) {
logsES = new EventSource(
"//" + location.host + "/api/logs"
)
logsES.onmessage = evt => {
if (!evt.data) return
commit("addLog", evt.data)
}
},
stopFetchLogs() {
logsES.close()
},
stopFetchSummary() {
summaryES.close() summaryES.close()
} }
}, },

View File

@@ -2,8 +2,8 @@
<div class="layout"> <div class="layout">
<ButtonGroup vertical> <ButtonGroup vertical>
<Button icon="ios-folder" @click="showRecords=true"></Button> <Button icon="ios-folder" @click="showRecords=true"></Button>
<Button icon="md-bug"></Button> <Button icon="md-bug" @click="showLogs=true"></Button>
<Button icon="md-settings"></Button> <Button icon="md-settings" @click="showConfig=true"></Button>
</ButtonGroup> </ButtonGroup>
<Card v-for="item in Rooms" :key="item.StreamPath" class="room"> <Card v-for="item in Rooms" :key="item.StreamPath" class="room">
<p slot="title">{{typeMap[item.Type]||item.Type}}{{item.StreamPath}}</p> <p slot="title">{{typeMap[item.Type]||item.Type}}{{item.StreamPath}}</p>
@@ -20,7 +20,8 @@
<Button <Button
@click="onShowDetail(item)" @click="onShowDetail(item)"
icon="ios-people" icon="ios-people"
>{{item.SubscriberInfo?item.SubscriberInfo.length:0}}</Button> >{{item.SubscriberInfo?item.SubscriberInfo.length:0}}
</Button>
<Button v-if="item.Type" @click="preview(item)" icon="md-eye"></Button> <Button v-if="item.Type" @click="preview(item)" icon="md-eye"></Button>
<Button <Button
@click="stopRecord(item)" @click="stopRecord(item)"
@@ -32,35 +33,43 @@
</ButtonGroup> </ButtonGroup>
</Card> </Card>
<div v-if="Rooms.length==0" class="empty"> <div v-if="Rooms.length==0" class="empty">
<Icon type="md-wine" size="50" />没有任何房间 <Icon type="md-wine" size="50"/>
没有任何房间
</div> </div>
<div class="status"> <div class="status">
<Alert>带宽消耗 📥{{totalInNetSpeed}} 📤{{totalOutNetSpeed}}</Alert> <Alert>带宽消耗 📥{{totalInNetSpeed}} 📤{{totalOutNetSpeed}}</Alert>
<Alert <Alert
:type="memoryStatus" :type="memoryStatus"
>内存使用{{networkFormat(Memory.Used,"M")}} 占比{{Memory.Usage.toFixed(2)}}%</Alert> >内存使用{{networkFormat(Memory.Used,"M")}} 占比{{Memory.Usage.toFixed(2)}}%
</Alert>
<Alert :type="cpuStatus">CPU使用{{CPUUsage.toFixed(2)}}%</Alert> <Alert :type="cpuStatus">CPU使用{{CPUUsage.toFixed(2)}}%</Alert>
<Alert <Alert
:type="hardDiskStatus" :type="hardDiskStatus"
>磁盘使用{{networkFormat(HardDisk.Used,"M")}} 占比{{HardDisk.Usage.toFixed(2)}}%</Alert> >磁盘使用{{networkFormat(HardDisk.Used,"M")}} 占比{{HardDisk.Usage.toFixed(2)}}%
</Alert>
</div> </div>
<Jessibuca ref="jessibuca" v-model="showPreview"></Jessibuca> <Jessibuca ref="jessibuca" v-model="showPreview"></Jessibuca>
<Records v-model="showRecords" /> <Records v-model="showRecords"/>
<Logs v-model="showLogs"/>
<Config v-model="showConfig"/>
</div> </div>
</template> </template>
<script> <script>
import { mapActions, mapState } from "vuex"; import {mapActions, mapState} from "vuex";
import Jessibuca from "../components/Jessibuca"; import Jessibuca from "../components/Jessibuca";
import StartTime from "../components/StartTime"; import StartTime from "../components/StartTime";
import Records from "../components/Records"; import Records from "../components/Records";
const uintInc = { import Logs from "../components/Logs";
import Config from "../components/Config"
const uintInc = {
"": "K", "": "K",
K: "M", K: "M",
M: "G", M: "G",
G: null G: null
}; };
const SoundFormat = { const SoundFormat = {
0: "Linear PCM, platform endian", 0: "Linear PCM, platform endian",
1: "ADPCM", 1: "ADPCM",
2: "MP3", 2: "MP3",
@@ -75,8 +84,8 @@ const SoundFormat = {
11: "Speex", 11: "Speex",
14: "MP3 8Khz", 14: "MP3 8Khz",
15: "Device-specific sound" 15: "Device-specific sound"
}; };
const CodecID = { const CodecID = {
1: "JPEG (currently unused)", 1: "JPEG (currently unused)",
2: "Sorenson H.263", 2: "Sorenson H.263",
3: "Screen video", 3: "Screen video",
@@ -85,20 +94,23 @@ const CodecID = {
6: "Screen video version 2", 6: "Screen video version 2",
7: "AVC", 7: "AVC",
12: "H265" 12: "H265"
}; };
export default { export default {
name: "Console", name: "Console",
components: { components: {
Jessibuca, Jessibuca,
StartTime, StartTime,
Records Records,
Logs, Config
}, },
data() { data() {
return { return {
showPreview: false, showPreview: false,
showRecords: false, showRecords: false,
showLogs: false,
showConfig: false,
typeMap: { typeMap: {
FlvFile:"🎥", FlvFile: "🎥",
TS: "🎬", TS: "🎬",
HLS: "🍎", HLS: "🍎",
"": "⏳", "": "⏳",
@@ -171,7 +183,7 @@ export default {
record(item) { record(item) {
window.ajax.get( window.ajax.get(
"//" + location.host + "/api/record/flv", "//" + location.host + "/api/record/flv",
{ streamPath: item.StreamPath }, {streamPath: item.StreamPath},
x => { x => {
if (x == "success") { if (x == "success") {
this.$Message.success("开始录制"); this.$Message.success("开始录制");
@@ -184,7 +196,7 @@ export default {
stopRecord(item) { stopRecord(item) {
window.ajax.get( window.ajax.get(
"//" + location.host + "/api/record/flv/stop", "//" + location.host + "/api/record/flv/stop",
{ streamPath: item.StreamPath }, {streamPath: item.StreamPath},
x => { x => {
if (x == "success") { if (x == "success") {
this.$Message.success("停止录制"); this.$Message.success("停止录制");
@@ -207,11 +219,11 @@ export default {
destroyed() { destroyed() {
this.stopFetchSummary(); this.stopFetchSummary();
} }
}; };
</script> </script>
<style scoped> <style scoped>
@keyframes recording { @keyframes recording {
0% { 0% {
opacity: 0.2; opacity: 0.2;
} }
@@ -221,41 +233,41 @@ export default {
100% { 100% {
opacity: 0.2; opacity: 0.2;
} }
} }
.recording { .recording {
animation: recording 1s infinite; animation: recording 1s infinite;
} }
.layout { .layout {
padding-bottom: 30px; padding-bottom: 30px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.room { .room {
width: 250px; width: 250px;
margin: 10px; margin: 10px;
text-align: left; text-align: left;
} }
.empty { .empty {
color: #eb5e46; color: #eb5e46;
width: 100%; width: 100%;
min-height: 500px; min-height: 500px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.status { .status {
position: fixed; position: fixed;
display: flex; display: flex;
left: 5px; left: 5px;
bottom: 10px; bottom: 10px;
} }
.status > div { .status > div {
margin: 0 5px; margin: 0 5px;
} }
</style> </style>

View File

@@ -3,11 +3,17 @@ package monica
import ( import (
"encoding/json" "encoding/json"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"io/ioutil"
"log" "log"
) )
var ConfigRaw []byte
func Run(configFile string) (err error) { func Run(configFile string) (err error) {
if _, err = toml.DecodeFile(configFile, cg); err == nil { if ConfigRaw, err = ioutil.ReadFile(configFile); err != nil {
return
}
if _, err = toml.Decode(string(ConfigRaw), cg); err == nil {
for name, config := range plugins { for name, config := range plugins {
if cfg, ok := cg.Plugins[name]; ok { if cfg, ok := cg.Plugins[name]; ok {
b, _ := json.Marshal(cfg) b, _ := json.Marshal(cfg)

View File

@@ -84,7 +84,22 @@ func init() {
}) })
} }
func run() { func run() {
http.HandleFunc("/api/stop", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/api/stop", stopPublish)
http.HandleFunc("/api/summary", summary)
http.HandleFunc("/api/logs", watchLogs)
http.HandleFunc("/api/config", getConfig)
http.HandleFunc("/", website)
log.Printf("server gateway start at %s", config.ListenAddr)
log.Fatal(http.ListenAndServe(config.ListenAddr, nil))
}
func getConfig(w http.ResponseWriter, r *http.Request) {
w.Write(ConfigRaw)
}
func watchLogs(w http.ResponseWriter, r *http.Request) {
AddWriter(NewSSE(w, r.Context()))
<-r.Context().Done()
}
func stopPublish(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
if streamPath := r.URL.Query().Get("stream"); streamPath != "" { if streamPath := r.URL.Query().Get("stream"); streamPath != "" {
if b, ok := AllRoom.Load(streamPath); ok { if b, ok := AllRoom.Load(streamPath); ok {
@@ -96,11 +111,6 @@ func run() {
} else { } else {
w.Write([]byte("no such stream")) w.Write([]byte("no such stream"))
} }
})
http.HandleFunc("/api/summary", summary)
http.HandleFunc("/", website)
log.Printf("server gateway start at %s", config.ListenAddr)
log.Fatal(http.ListenAndServe(config.ListenAddr, nil))
} }
func website(w http.ResponseWriter, r *http.Request) { func website(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path filePath := r.URL.Path