Compare commits

...

3 Commits

Author SHA1 Message Date
langhuihui
649a5b558a 增加实例管理器 2020-02-11 14:53:29 +08:00
langhuihui
b3c8d35fad 修复集群传输bug 2020-02-05 17:29:55 +08:00
langhuihui
ab745145d9 集群采集信息功能完善 2020-02-04 16:09:55 +08:00
64 changed files with 13635 additions and 254 deletions

View File

@@ -7,8 +7,8 @@ ListenAddr = ":1935"
[Plugins.GateWay]
ListenAddr = ":8081"
[Plugins.Cluster]
Master = "203.60.1.23:2019"
#ListenAddr = ":2019"
#Master = "localhost:2019"
ListenAddr = ":2019"
#
#[Plugins.Auth]
#Key="www.monibuca.com"

View File

@@ -1 +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}.records[data-v-4eee1624]{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 15px}.records>[data-v-4eee1624]{width:200px}.log-container{overflow-y:auto;max-height:500px}@-webkit-keyframes recording-data-v-f6113870{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}@keyframes recording-data-v-f6113870{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}.recording[data-v-f6113870]{-webkit-animation:recording-data-v-f6113870 1s infinite;animation:recording-data-v-f6113870 1s infinite}.layout[data-v-f6113870]{padding-bottom:30px;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.room[data-v-f6113870]{width:250px;margin:10px;text-align:left}.empty[data-v-f6113870]{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-f6113870],.status[data-v-f6113870]{display:-webkit-box;display:-ms-flexbox;display:flex}.status[data-v-f6113870]{position:fixed;left:5px;bottom:10px}.status>div[data-v-f6113870]{margin:0 5px}
#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}.records[data-v-7d5ab110]{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0 15px}.records>[data-v-7d5ab110]{width:200px}.log-container{overflow-y:auto;max-height:500px}@-webkit-keyframes recording-data-v-65ac4b48{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}@keyframes recording-data-v-65ac4b48{0%{opacity:.2}50%{opacity:1}to{opacity:.2}}.recording[data-v-65ac4b48]{-webkit-animation:recording-data-v-65ac4b48 1s infinite;animation:recording-data-v-65ac4b48 1s infinite}.layout[data-v-65ac4b48]{padding-bottom:30px;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.room[data-v-65ac4b48]{width:250px;margin:10px;text-align:left}.empty[data-v-65ac4b48]{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-65ac4b48],.status[data-v-65ac4b48]{display:-webkit-box;display:-ms-flexbox;display:flex}.status[data-v-65ac4b48]{position:fixed;left:5px;bottom:10px}.status>div[data-v-65ac4b48]{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.ea4656d8.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.af5e5ef3.js rel=preload as=script><link href=/js/chunk-vendors.ebc28a73.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.ea4656d8.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.ebc28a73.js></script><script src=/js/app.af5e5ef3.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.ce470878.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.017fb959.js rel=preload as=script><link href=/js/chunk-vendors.ebc28a73.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.ce470878.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.ebc28a73.js></script><script src=/js/app.017fb959.js></script></body></html>

View File

@@ -9,7 +9,7 @@ function Jessibuca(opt) {
this.initBuffers();
this.initTextures();
};
this.decoderWorker = new Worker(opt.decoder || '264_mp3.js')
this.decoderWorker = new Worker(opt.decoder || 'ff.js')
var _this = this
function draw(output) {
_this.drawNextOutputPicture(_this.width, _this.height, null, output)
@@ -118,12 +118,15 @@ Jessibuca.prototype.playAudio = function (data) {
}
// setTimeout(playNextBuffer, buffer.duration * 1000)
}
var tryPlay = function (buffer) {
var decodeAudio = function () {
if (decodeQueue.length) {
context.decodeAudioData(decodeQueue.shift(), tryPlay, console.error);
context.decodeAudioData(decodeQueue.shift(), tryPlay, decodeAudio);
} else {
isDecoding = false
}
}
var tryPlay = function (buffer) {
decodeAudio()
if (isPlaying) {
audioBuffers.push(buffer);
} else {
@@ -134,7 +137,7 @@ Jessibuca.prototype.playAudio = function (data) {
decodeQueue.push(...data)
if (!isDecoding) {
isDecoding = true
context.decodeAudioData(decodeQueue.shift(), tryPlay, console.error);
decodeAudio()
}
}
this.playAudio = playAudio
@@ -452,7 +455,7 @@ Jessibuca.prototype.close = function () {
this.decoderWorker.postMessage({ cmd: "close" })
this.contextGL.clear(this.contextGL.COLOR_BUFFER_BIT);
}
Jessibuca.prototype.destroy = function(){
Jessibuca.prototype.destroy = function () {
this.decoderWorker.terminate()
}
Jessibuca.prototype.play = function (url) {

2
dashboard/dist/js/app.017fb959.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/dist/js/app.017fb959.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

@@ -27,7 +27,9 @@ func main() {
该配置文件主要是为了定制各个插件的配置,例如监听端口号等,具体还是要看各个插件的设计。
> 如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息
::: tip
如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息
:::
如果注释掉部分插件的配置,那么该插件就不会启用,典型的配置如下:
```toml

View File

@@ -9,7 +9,7 @@ function Jessibuca(opt) {
this.initBuffers();
this.initTextures();
};
this.decoderWorker = new Worker(opt.decoder || '264_mp3.js')
this.decoderWorker = new Worker(opt.decoder || 'ff.js')
var _this = this
function draw(output) {
_this.drawNextOutputPicture(_this.width, _this.height, null, output)

View File

@@ -1,5 +1,9 @@
<template>
<div id="mountNode"></div>
<div>
自动更新
<i-switch v-model="autoUpdate"></i-switch>
<div id="mountNode"></div>
</div>
</template>
<script>
@@ -7,24 +11,22 @@ import { mapState } from "vuex";
import G6 from "@antv/g6";
var graph = null;
export default {
data() {
return {
autoUpdate: true
};
},
computed: {
...mapState({
data(state) {
let summary = state.summary;
// 点集
let nodes = [];
// 边集
let edges = [];
this.addServer(summary, nodes, edges);
return {
nodes,
edges
};
let d = this.addServer(state.summary);
d.label = "🏠" + d.label;
return d;
}
})
},
methods: {
addServer(node, nodes, edges) {
addServer(node) {
let result = {
id: node.Address,
label: node.Address,
@@ -33,38 +35,35 @@ export default {
shape: "modelRect",
logoIcon: {
show: false
}
},
children: []
};
nodes.push(result);
if (node.Rooms) {
for (let i = 0; i < node.Rooms.length; i++) {
let room = node.Rooms[i];
let roomId = result.id + room.StreamPath;
nodes.push({
let roomId = room.StreamPath;
let roomData = {
id: roomId,
label: room.StreamPath,
shape: "rect"
});
edges.push({ source: result.id, target: roomId });
shape: "rect",
children: []
};
result.children.push(roomData);
if (room.SubscriberInfo) {
for (let j = 0; j < room.SubscriberInfo.length; j++) {
let subId = roomId + room.SubscriberInfo[j].ID;
nodes.push({
roomData.children.push({
id: subId,
label: room.SubscriberInfo[j].ID
});
edges.push({ source: roomId, target: subId });
}
}
}
}
if (node.Children && node.Children.length > 0) {
for (let i = 0; i < node.Children.length; i++) {
let child = this.addServer(node.Children[i], nodes, edges);
edges.push({
source: result.id,
target: child.id
});
if (node.Children) {
for (let childId in node.Children) {
result.children.push(this.addServer(node.Children[childId]));
}
}
return result;
@@ -72,23 +71,33 @@ export default {
},
watch: {
data(v) {
if (graph) {
graph.read(v); // 加载数据
if (graph && this.autoUpdate) {
//graph.updateChild(v, "");
graph.changeData(v); // 加载数据
graph.fitView();
//graph.read(v);
}
}
},
mounted() {
graph = new G6.Graph({
renderer: "svg",
graph = new G6.TreeGraph({
linkCenter: true,
// renderer: "svg",
container: "mountNode", // 指定挂载容器
width: 800, // 图的宽度
height: 500, // 图的高度
layout: {
type: "radial"
modes: {
default: ["drag-canvas", "zoom-canvas", "click-select", "drag-node"]
},
defaultNode: {}
animate: false,
layout: {
// type: "indeted",
direction: "H"
}
});
//graph.addChild(this.data, "");
graph.read(this.data); // 加载数据
graph.fitView();
}
};
</script>

View File

@@ -9,6 +9,8 @@
>
<canvas id="canvas" width="488" height="275" style="background: black" />
<div slot="footer">
音频缓冲
<InputNumber v-model="audioBuffer" size="small"></InputNumber>
<Button v-if="audioEnabled" @click="turnOff" icon="md-volume-off" />
<Button v-else @click="turnOn" icon="md-volume-up"></Button>
</div>
@@ -22,18 +24,23 @@ export default {
data() {
return {
audioEnabled: false,
audioBuffer: 12,
url: ""
};
},
watch: {
audioEnabled(value) {
h5lc.audioEnabled(value);
},
audioBuffer(v) {
h5lc.audioBuffer = v;
}
},
mounted() {
h5lc = new window.Jessibuca({
canvas: document.getElementById("canvas"),
decoder: "jessibuca/ff.js"
decoder: "jessibuca/ff.js",
audioBuffer: this.audioBuffer
});
},
destroyed() {

View File

@@ -33,7 +33,7 @@ export default {
x => {
if (x == "success") {
this.onVisible(true);
this.$Message.success("删除成功");
this.$Message.success("开始发布");
} else {
this.$Message.error(x);
}
@@ -50,7 +50,7 @@ export default {
{ streamPath: item.Path.replace(".flv", "") },
x => {
if (x == "success") {
this.$Message.success("开始发布");
this.$Message.success("删除成功");
} else {
this.$Message.error(x);
}

View File

@@ -118,6 +118,7 @@ export default {
currentTab: "",
currentStream: [],
typeMap: {
Receiver: "📡",
FlvFile: "🎥",
TS: "🎬",
HLS: "🍎",

270
main.go
View File

@@ -1,17 +1,273 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/BurntSushi/toml"
. "github.com/langhuihui/monibuca/monica"
_ "github.com/langhuihui/monibuca/plugins"
"github.com/langhuihui/monibuca/monica/util"
"io/ioutil"
"log"
"mime"
"net/http"
"os"
"os/exec"
"os/user"
"path"
"runtime"
"strings"
)
func main() {
log.SetOutput(os.Stdout)
configPath := flag.String("c", "config.toml", "configFile")
flag.Parse()
Run(*configPath)
select {}
type InstanceDesc struct {
Name string
Path string
Plugins []string
Config string
}
var instances map[string]*InstanceDesc
var instancesDir string
func main() {
// log.SetOutput(os.Stdout)
// configPath := flag.String("c", "config.toml", "configFile")
// flag.Parse()
// Run(*configPath)
// select {}
println("start monibuca instance manager version:", Version)
if MayBeError(readInstances()) {
return
}
addr := flag.String("port", "8000", "http server port")
flag.Parse()
http.HandleFunc("/list", listInstance)
http.HandleFunc("/create", initInstance)
http.HandleFunc("/upgrade/engine", upgradeEngine)
http.HandleFunc("/restart/instance", restartInstance)
http.HandleFunc("/", website)
fmt.Printf("start listen at %s", *addr)
if err := http.ListenAndServe(":"+*addr, nil); err != nil {
log.Fatal(err)
}
}
func readInstances() error {
if homeDir, err := Home(); err == nil {
instancesDir = path.Join(homeDir, ".monibuca")
if err = os.MkdirAll(instancesDir, os.FileMode(0666)); err == nil {
if f, err := os.Open(instancesDir); err != nil {
return err
} else if cs, err := f.Readdir(0); err != nil {
return err
} else {
for _, configFile := range cs {
des := new(InstanceDesc)
if _, err = toml.DecodeFile(path.Join(instancesDir, configFile.Name()), des); err == nil {
instances[des.Name] = des
} else {
log.Println(err)
}
}
return nil
}
} else {
return err
}
} else {
return err
}
}
func website(w http.ResponseWriter, r *http.Request) {
filePath := r.URL.Path
if filePath == "/" {
filePath = "/index.html"
}
if mime := mime.TypeByExtension(path.Ext(filePath)); mime != "" {
w.Header().Set("Content-Type", mime)
}
_, currentFilePath, _, _ := runtime.Caller(0)
if f, err := ioutil.ReadFile(path.Join(path.Dir(currentFilePath), "pm/dist", filePath)); err == nil {
if _, err = w.Write(f); err != nil {
w.WriteHeader(505)
}
} else {
w.Header().Set("Location", "/")
w.WriteHeader(302)
}
}
func listInstance(w http.ResponseWriter, r *http.Request) {
if bytes, err := json.Marshal(instances); err == nil {
_, err = w.Write(bytes)
} else {
w.Write([]byte(err.Error()))
}
}
func initInstance(w http.ResponseWriter, r *http.Request) {
instanceDesc := new(InstanceDesc)
sse := util.NewSSE(w, r.Context())
err := json.Unmarshal([]byte(r.URL.Query().Get("info")), instanceDesc)
defer func() {
if err != nil {
sse.WriteEvent("exception", []byte(err.Error()))
} else {
sse.Write([]byte("success"))
}
}()
if err != nil {
return
}
sse.WriteEvent("step", []byte("1:参数解析成功!"))
err = instanceDesc.createDir(sse)
if err != nil {
return
}
sse.WriteEvent("step", []byte("6:实例创建成功!"))
var file *os.File
file, err = os.OpenFile(path.Join(instancesDir, instanceDesc.Name+".toml"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
return
}
tomlEncoder := toml.NewEncoder(file)
err = tomlEncoder.Encode(&instanceDesc)
if err != nil {
return
}
instances[instanceDesc.Name] = instanceDesc
}
func upgradeEngine(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
cmd := exec.Command("go", "get", "-u", "github.com/langhuihui/monibuca/monica")
instanceName := r.URL.Query().Get("instance")
cmd.Dir = instances[instanceName].Path
err := sse.WriteExec(cmd)
if err != nil {
sse.Write([]byte(err.Error()))
}
}
func restartInstance(w http.ResponseWriter, r *http.Request) {
sse := util.NewSSE(w, r.Context())
instanceName := r.URL.Query().Get("instance")
cmd := exec.Command("sh", "restart.sh")
cmd.Dir = path.Join(instancesDir, instanceName)
cmd.Stderr = sse
cmd.Stdout = sse
err := cmd.Start()
if err != nil {
sse.Write([]byte(err.Error()))
}
}
func (p *InstanceDesc) writeExecSSE(sse *util.SSE, cmd *exec.Cmd) error {
cmd.Dir = p.Path
return sse.WriteExec(cmd)
}
func (p *InstanceDesc) createDir(sse *util.SSE) (err error) {
err = os.MkdirAll(p.Path, 0666)
if err != nil {
return
}
sse.WriteEvent("step", []byte("2:目录创建成功!"))
err = ioutil.WriteFile(path.Join(p.Path, "config.toml"), []byte(p.Config), 0666)
if err != nil {
return
}
var build bytes.Buffer
build.WriteString(`package main
import(
"github.com/langhuihui/monibuca/monica"`)
for _, plugin := range p.Plugins {
build.WriteString("\n_ \"")
build.WriteString(plugin)
build.WriteString("\"")
}
build.WriteString("\n)\n")
build.WriteString(`
func main(){
monica.Run("config.toml")
select{}
}
`)
err = ioutil.WriteFile(path.Join(p.Path, "main.go"), build.Bytes(), 0666)
if err != nil {
return
}
sse.WriteEvent("step", []byte("3:文件创建成功!"))
err = p.writeExecSSE(sse, exec.Command("go", "mod", "init", p.Name))
if err != nil {
return
}
sse.WriteEvent("step", []byte("4:go mod 初始化完成!"))
err = p.writeExecSSE(sse, exec.Command("go", "build"))
if err != nil {
return
}
sse.WriteEvent("step", []byte("5:go build 成功!"))
build.Reset()
build.WriteString("kill -9 `cat pid`\nnohup .")
build.WriteString(path.Dir(path.Join(p.Path, "main.go")))
build.WriteString(" > log.txt & echo $! > pid\n")
err = ioutil.WriteFile(path.Join(p.Path, "restart.sh"), build.Bytes(), 0777)
if err != nil {
return
}
cmd := exec.Command("sh", "restart.sh")
cmd.Dir = p.Path
cmd.Stderr = sse
cmd.Stdout = sse
err = cmd.Start()
return
}
func Home() (string, error) {
user, err := user.Current()
if nil == err {
return user.HomeDir, nil
}
// cross compile support
if "windows" == runtime.GOOS {
return homeWindows()
}
// Unix-like system, so just assume Unix
return homeUnix()
}
func homeUnix() (string, error) {
// First prefer the HOME environmental variable
if home := os.Getenv("HOME"); home != "" {
return home, nil
}
// If that fails, try the shell
var stdout bytes.Buffer
cmd := exec.Command("sh", "-c", "eval echo ~$USER")
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return "", err
}
result := strings.TrimSpace(stdout.String())
if result == "" {
return "", errors.New("blank output when reading home directory")
}
return result, nil
}
func homeWindows() (string, error) {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home := drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
if home == "" {
return "", errors.New("HOMEDRIVE, HOMEPATH, and USERPROFILE are blank")
}
return home, nil
}

BIN
monibuca.exe~ Normal file

Binary file not shown.

View File

@@ -1,6 +1,7 @@
package pool
package avformat
import (
"github.com/langhuihui/monibuca/monica/pool"
"sync"
)
@@ -33,13 +34,25 @@ type AVPacket struct {
func (av *AVPacket) IsKeyFrame() bool {
return av.VideoFrameType == 1 || av.VideoFrameType == 4
}
func (av *AVPacket) ADTS2ASC() (tagPacket *AVPacket) {
tagPacket = NewAVPacket(FLV_TAG_TYPE_AUDIO)
tagPacket.Payload = ADTSToAudioSpecificConfig(av.Payload)
tagPacket.IsAACSequence = true
ADTSLength := 7 + (int(av.Payload[1]&1) << 1)
if len(av.Payload) > ADTSLength {
av.Payload[0] = 0xAF
av.Payload[1] = 0x01 //raw AAC
copy(av.Payload[2:], av.Payload[ADTSLength:])
av.Payload = av.Payload[:(len(av.Payload) - ADTSLength + 2)]
}
return
}
func (av *AVPacket) Recycle() {
if av.RefCount == 0 {
return
} else if av.RefCount == 1 {
av.RefCount = 0
RecycleSlice(av.Payload)
pool.RecycleSlice(av.Payload)
AVPacketPool.Put(av)
} else {
av.RefCount--

View File

@@ -70,7 +70,7 @@ var (
var FLVHeader = []byte{0x46, 0x4c, 0x56, 0x01, 0x05, 0, 0, 0, 9, 0, 0, 0, 0}
func WriteFLVTag(w io.Writer, tag *pool.SendPacket) (err error) {
func WriteFLVTag(w io.Writer, tag *SendPacket) (err error) {
head := pool.GetSlice(11)
defer pool.RecycleSlice(head)
tail := pool.GetSlice(4)
@@ -93,13 +93,13 @@ func WriteFLVTag(w io.Writer, tag *pool.SendPacket) (err error) {
}
return
}
func ReadFLVTag(r io.Reader) (tag *pool.AVPacket, err error) {
func ReadFLVTag(r io.Reader) (tag *AVPacket, err error) {
head := pool.GetSlice(11)
defer pool.RecycleSlice(head)
if _, err = io.ReadFull(r, head); err != nil {
return
}
tag = pool.NewAVPacket(head[0])
tag = NewAVPacket(head[0])
dataSize := util.BigEndian.Uint24(head[1:])
tag.Timestamp = util.BigEndian.Uint24(head[4:])
body := pool.GetSlice(int(dataSize))

View File

@@ -8,8 +8,8 @@ func (h AuthHook) AddHook(hook func(string) error) {
AuthHooks = append(h, hook)
}
func (h AuthHook) Trigger(sign string) error {
for _, h := range h {
if err := h(sign); err != nil {
for _, f := range h {
if err := f(sign); err != nil {
return err
}
}
@@ -24,8 +24,8 @@ func (h OnPublishHook) AddHook(hook func(r *Room)) {
OnPublishHooks = append(h, hook)
}
func (h OnPublishHook) Trigger(r *Room) {
for _, h := range h {
h(r)
for _, f := range h {
f(r)
}
}
@@ -37,8 +37,8 @@ func (h OnSubscribeHook) AddHook(hook func(s *OutputStream)) {
OnSubscribeHooks = append(h, hook)
}
func (h OnSubscribeHook) Trigger(s *OutputStream) {
for _, h := range h {
h(s)
for _, f := range h {
f(s)
}
}
@@ -50,8 +50,8 @@ func (h OnDropHook) AddHook(hook func(s *OutputStream)) {
OnDropHooks = append(h, hook)
}
func (h OnDropHook) Trigger(s *OutputStream) {
for _, h := range h {
h(s)
for _, f := range h {
f(s)
}
}
@@ -63,7 +63,7 @@ func (h OnSummaryHook) AddHook(hook func(bool)) {
OnSummaryHooks = append(h, hook)
}
func (h OnSummaryHook) Trigger(v bool) {
for _, h := range h {
h(v)
for _, f := range h {
f(v)
}
}

View File

@@ -2,12 +2,14 @@ package monica
import (
"encoding/json"
"github.com/BurntSushi/toml"
"io/ioutil"
"log"
"github.com/BurntSushi/toml"
)
var ConfigRaw []byte
var Version = "0.1.2"
func Run(configFile string) (err error) {
if ConfigRaw, err = ioutil.ReadFile(configFile); err != nil {

View File

@@ -2,11 +2,11 @@ package monica
import (
"context"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"log"
"sync"
"time"
"github.com/langhuihui/monibuca/monica/avformat"
)
var (
@@ -22,8 +22,8 @@ func (c *Collection) Get(name string) (result *Room) {
item, loaded := AllRoom.LoadOrStore(name, &Room{
Subscribers: make(map[string]*OutputStream),
Control: make(chan interface{}),
VideoChan: make(chan *pool.AVPacket, 1),
AudioChan: make(chan *pool.AVPacket, 1),
VideoChan: make(chan *avformat.AVPacket, 1),
AudioChan: make(chan *avformat.AVPacket, 1),
})
result = item.(*Room)
if !loaded {
@@ -41,11 +41,11 @@ type Room struct {
Control chan interface{}
Cancel context.CancelFunc
Subscribers map[string]*OutputStream // 订阅者
VideoTag *pool.AVPacket // 每个视频包都是这样的结构,区别在于Payload的大小.FMS在发送AVC sequence header,需要加上 VideoTags,这个tag 1个字节(8bits)的数据
AudioTag *pool.AVPacket // 每个音频包都是这样的结构,区别在于Payload的大小.FMS在发送AAC sequence header,需要加上 AudioTags,这个tag 1个字节(8bits)的数据
FirstScreen []*pool.AVPacket
AudioChan chan *pool.AVPacket
VideoChan chan *pool.AVPacket
VideoTag *avformat.AVPacket // 每个视频包都是这样的结构,区别在于Payload的大小.FMS在发送AVC sequence header,需要加上 VideoTags,这个tag 1个字节(8bits)的数据
AudioTag *avformat.AVPacket // 每个音频包都是这样的结构,区别在于Payload的大小.FMS在发送AAC sequence header,需要加上 AudioTags,这个tag 1个字节(8bits)的数据
FirstScreen []*avformat.AVPacket
AudioChan chan *avformat.AVPacket
VideoChan chan *avformat.AVPacket
UseTimestamp bool //是否采用数据包中的时间戳
}
@@ -90,7 +90,7 @@ func (r *Room) Subscribe(s *OutputStream) {
if r.Err() == nil {
s.SubscribeTime = time.Now()
log.Printf("subscribe :%s %s,to room %s", s.Type, s.ID, r.StreamPath)
s.packetQueue = make(chan *pool.SendPacket, 1024)
s.packetQueue = make(chan *avformat.SendPacket, 1024)
s.Context, s.Cancel = context.WithCancel(r)
s.Control <- &SubscribeCmd{s}
}
@@ -153,12 +153,21 @@ func (r *Room) Run() {
}
}
}
func (r *Room) PushAudio(audio *pool.AVPacket) {
func (r *Room) PushAudio(audio *avformat.AVPacket) {
if len(audio.Payload) < 4 {
return
}
if audio.Payload[0] == 0xFF && (audio.Payload[1]&0xF0) == 0xF0 {
audio.IsADTS = true
r.AudioTag = audio
//audio.IsADTS = true
r.AudioInfo.SoundFormat = 10
r.AudioInfo.SoundRate = avformat.SamplingFrequencies[(audio.Payload[2]&0x3c)>>2]
r.AudioInfo.SoundType = ((audio.Payload[2] & 0x1) << 2) | ((audio.Payload[3] & 0xc0) >> 6)
r.AudioTag = audio.ADTS2ASC()
} else if r.AudioTag == nil {
audio.IsAACSequence = true
if len(audio.Payload) < 5 {
return
}
r.AudioTag = audio
tmp := audio.Payload[0] // 第一个字节保存着音频的相关信息
if r.AudioInfo.SoundFormat = tmp >> 4; r.AudioInfo.SoundFormat == 10 { //真的是AAC的话后面有一个字节的详细信息
@@ -191,7 +200,7 @@ func (r *Room) PushAudio(audio *pool.AVPacket) {
r.AudioInfo.PacketCount++
r.AudioChan <- audio
}
func (r *Room) setH264Info(video *pool.AVPacket) {
func (r *Room) setH264Info(video *avformat.AVPacket) {
r.VideoTag = video
info := avformat.AVCDecoderConfigurationRecord{}
//0:codec,1:IsAVCSequence,2~4:compositionTime
@@ -199,7 +208,10 @@ func (r *Room) setH264Info(video *pool.AVPacket) {
r.VideoInfo.SPSInfo, err = avformat.ParseSPS(info.SequenceParameterSetNALUnit)
}
}
func (r *Room) PushVideo(video *pool.AVPacket) {
func (r *Room) PushVideo(video *avformat.AVPacket) {
if len(video.Payload) < 3 {
return
}
video.VideoFrameType = video.Payload[0] >> 4 // 帧类型 4Bit, H264一般为1或者2
r.VideoInfo.CodecID = video.Payload[0] & 0x0f // 编码类型ID 4Bit, JPEG, H263, AVC...
video.IsAVCSequence = video.VideoFrameType == 1 && video.Payload[1] == 0

View File

@@ -3,12 +3,12 @@ package monica
import (
"context"
"fmt"
"github.com/langhuihui/monibuca/monica/pool"
"github.com/langhuihui/monibuca/monica/avformat"
"time"
)
type Subscriber interface {
Send(*pool.SendPacket) error
Send(*avformat.SendPacket) error
}
type SubscriberInfo struct {
@@ -23,14 +23,14 @@ type OutputStream struct {
context.Context
*Room
SubscriberInfo
SendHandler func(*pool.SendPacket) error
SendHandler func(*avformat.SendPacket) error
Cancel context.CancelFunc
Sign string
VTSent bool
ATSent bool
VSentTime uint32
ASentTime uint32
packetQueue chan *pool.SendPacket
packetQueue chan *avformat.SendPacket
dropCount int
OffsetTime uint32
firstScreenIndex int
@@ -61,7 +61,7 @@ func (s *OutputStream) Play(streamPath string) (err error) {
}
}
}
func (s *OutputStream) sendPacket(packet *pool.AVPacket, timestamp uint32) {
func (s *OutputStream) sendPacket(packet *avformat.AVPacket, timestamp uint32) {
if !packet.IsAVCSequence && timestamp == 0 {
timestamp = 1 //防止为0
}
@@ -82,11 +82,11 @@ func (s *OutputStream) sendPacket(packet *pool.AVPacket, timestamp uint32) {
s.TotalDrop++
packet.Recycle()
} else if !s.IsClosed() {
s.packetQueue <- pool.NewSendPacket(packet, timestamp)
s.packetQueue <- avformat.NewSendPacket(packet, timestamp)
}
}
func (s *OutputStream) sendVideo(video *pool.AVPacket) error {
func (s *OutputStream) sendVideo(video *avformat.AVPacket) error {
isKF := video.IsKeyFrame()
if s.VTSent {
if s.FirstScreen == nil || s.firstScreenIndex == -1 {
@@ -119,7 +119,7 @@ func (s *OutputStream) sendVideo(video *pool.AVPacket) error {
s.VSentTime = video.Timestamp
return s.sendVideo(video)
}
func (s *OutputStream) sendAudio(audio *pool.AVPacket) error {
func (s *OutputStream) sendAudio(audio *avformat.AVPacket) error {
if s.ATSent {
if s.FirstScreen != nil && s.firstScreenIndex == -1 {
audio.Recycle()

73
monica/util/SSE.go Normal file
View File

@@ -0,0 +1,73 @@
package util
import (
"context"
"encoding/json"
"net/http"
"os/exec"
)
var (
sseEent = []byte("event: ")
sseBegin = []byte("data: ")
sseEnd = []byte("\n\n")
)
type SSE struct {
http.ResponseWriter
context.Context
}
func (sse *SSE) Write(data []byte) (n int, err error) {
if err = sse.Err(); err != nil {
return
}
_, err = sse.ResponseWriter.Write(sseBegin)
n, err = sse.ResponseWriter.Write(data)
_, err = sse.ResponseWriter.Write(sseEnd)
if err != nil {
return
}
sse.ResponseWriter.(http.Flusher).Flush()
return
}
func (sse *SSE) WriteEvent(event string, data []byte) (err error) {
if err = sse.Err(); err != nil {
return
}
_, err = sse.ResponseWriter.Write(sseEent)
_, err = sse.ResponseWriter.Write([]byte(event))
_, err = sse.ResponseWriter.Write([]byte("\n"))
_, err = sse.Write(data)
return
}
func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("X-Accel-Buffering", "no")
header.Set("Access-Control-Allow-Origin", "*")
return &SSE{
w,
ctx,
}
}
func (sse *SSE) WriteJSON(data interface{}) (err error) {
var jsonData []byte
if jsonData, err = json.Marshal(data); err == nil {
if _, err = sse.Write(jsonData); err != nil {
return
}
return
}
return
}
func (sse *SSE) WriteExec(cmd *exec.Cmd) error {
cmd.Stderr = sse
cmd.Stdout = sse
return cmd.Run()
}

View File

@@ -3,7 +3,6 @@ package HDL
import (
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"log"
"net/http"
"strings"
@@ -42,7 +41,7 @@ func HDLHandler(w http.ResponseWriter, r *http.Request) {
w.Write(avformat.FLVHeader)
p := OutputStream{
Sign: sign,
SendHandler: func(packet *pool.SendPacket) error {
SendHandler: func(packet *avformat.SendPacket) error {
return avformat.WriteFLVTag(w, packet)
},
SubscriberInfo: SubscriberInfo{

View File

@@ -5,7 +5,6 @@ import (
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/avformat/mpegts"
"github.com/langhuihui/monibuca/monica/pool"
"github.com/langhuihui/monibuca/monica/util"
"log"
"time"
@@ -46,12 +45,12 @@ func (ts *TS) run() {
ts.TotalPesCount++
switch tsPesPkt.PesPkt.Header.StreamID & 0xF0 {
case mpegts.STREAM_ID_AUDIO:
av := pool.NewAVPacket(avformat.FLV_TAG_TYPE_AUDIO)
av := avformat.NewAVPacket(avformat.FLV_TAG_TYPE_AUDIO)
av.Payload = tsPesPkt.PesPkt.Payload
ts.PushAudio(av)
case mpegts.STREAM_ID_VIDEO:
var err error
av := pool.NewAVPacket(avformat.FLV_TAG_TYPE_VIDEO)
av := avformat.NewAVPacket(avformat.FLV_TAG_TYPE_VIDEO)
ts.PTS = tsPesPkt.PesPkt.Header.Pts
ts.DTS = tsPesPkt.PesPkt.Header.Dts
lastDts := ts.lastDts
@@ -95,7 +94,7 @@ func (ts *TS) run() {
av.VideoFrameType = 1
av.Payload = r.Bytes()
ts.PushVideo(av)
av = pool.NewAVPacket(avformat.FLV_TAG_TYPE_VIDEO)
av = avformat.NewAVPacket(avformat.FLV_TAG_TYPE_VIDEO)
av.Timestamp = uint32(dts / 90)
r = bytes.NewBuffer([]byte{})
continue

View File

@@ -61,9 +61,10 @@ func (p *HLS) run(info *M3u8Info) {
log.Printf("hls %s exit:%v", p.StreamPath, err)
p.Cancel()
}()
errcount := 0
for ; err == nil && p.Err() == nil; resp, err = client.Do(info.Req) {
if playlist, err := readM3U8(resp); err == nil {
errcount = 0
info.LastM3u8 = playlist.String()
//if !playlist.Live {
// log.Println(p.LastM3u8)
@@ -129,7 +130,11 @@ func (p *HLS) run(info *M3u8Info) {
time.Sleep(time.Second * time.Duration(playlist.Target) * 2)
} else {
log.Printf("%s readM3u8:%v", p.StreamPath, err)
return
errcount++
if errcount > 10 {
return
}
//return
}
}
}

View File

@@ -3,6 +3,7 @@ package cluster
import (
"bufio"
"encoding/json"
"io"
"log"
"math/rand"
"net"
@@ -46,56 +47,52 @@ func run() {
if MayBeError(err) {
return
}
masterConn, err = net.DialTCP("tcp", nil, addr)
if MayBeError(err) {
return
}
go readMaster()
go readMaster(addr)
}
if config.ListenAddr != "" {
Summary.Children = make(map[string]*ServerSummary)
OnSummaryHooks.AddHook(onSummary)
log.Printf("server bare start at %s", config.ListenAddr)
log.Fatal(ListenBare(config.ListenAddr))
}
}
func readMaster() {
func readMaster(addr *net.TCPAddr) {
var err error
defer func() {
for {
t := 5 + rand.Int63n(5)
log.Printf("reconnect to master %s after %d seconds", config.Master, t)
time.Sleep(time.Duration(t) * time.Second)
addr, _ := net.ResolveTCPAddr("tcp", config.Master)
if masterConn, err = net.DialTCP("tcp", nil, addr); err == nil {
go readMaster()
return
}
}
}()
brw := bufio.NewReadWriter(bufio.NewReader(masterConn), bufio.NewWriter(masterConn))
log.Printf("connect to master %s reporting", config.Master)
//首次报告
if b, err := json.Marshal(Summary); err == nil {
_, err = masterConn.Write(b)
}
var cmd byte
for {
cmd, err := brw.ReadByte()
if err != nil {
return
}
switch cmd {
case MSG_SUMMARY: //收到主服务器指令,进行采集和上报
log.Println("receive summary request from master")
if cmd, err = brw.ReadByte(); err != nil {
return
}
if cmd == 1 {
Summary.Add()
go onReport()
} else {
Summary.Done()
if masterConn, err = net.DialTCP("tcp", nil, addr); !MayBeError(err) {
reader := bufio.NewReader(masterConn)
log.Printf("connect to master %s reporting", config.Master)
for report(); err == nil; {
if cmd, err = reader.ReadByte(); !MayBeError(err) {
switch cmd {
case MSG_SUMMARY: //收到主服务器指令,进行采集和上报
log.Println("receive summary request from master")
if cmd, err = reader.ReadByte(); !MayBeError(err) {
if cmd == 1 {
Summary.Add()
go onReport()
} else {
Summary.Done()
}
}
}
}
}
}
t := 5 + rand.Int63n(5)
log.Printf("reconnect to master %s after %d seconds", config.Master, t)
time.Sleep(time.Duration(t) * time.Second)
}
}
func report() {
if b, err := json.Marshal(Summary); err == nil {
data := make([]byte, len(b)+2)
data[0] = MSG_SUMMARY
copy(data[1:], b)
data[len(data)-1] = 0
_, err = masterConn.Write(data)
}
}
@@ -103,28 +100,24 @@ func readMaster() {
func onReport() {
for range time.NewTicker(time.Second).C {
if Summary.Running() {
if b, err := json.Marshal(Summary); err == nil {
data := make([]byte, len(b)+2)
data[0] = MSG_SUMMARY
copy(data[1:], b)
data[len(data)-1] = 0
_, err = masterConn.Write(data)
}
report()
} else {
return
}
}
}
func orderReport(conn io.Writer, start bool) {
b := []byte{MSG_SUMMARY, 0}
if start {
b[1] = 1
}
conn.Write(b)
}
//通知从服务器需要上报或者关闭上报
func onSummary(start bool) {
slaves.Range(func(k, v interface{}) bool {
conn := v.(*net.TCPConn)
b := []byte{MSG_SUMMARY, 0}
if start {
b[1] = 1
}
conn.Write(b)
orderReport(v.(*net.TCPConn), start)
return true
})
}

View File

@@ -3,12 +3,14 @@ package cluster
import (
"bufio"
"encoding/binary"
"io"
"log"
"net"
"strings"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"io"
"net"
"strings"
)
type Receiver struct {
@@ -24,14 +26,15 @@ func (p *Receiver) Auth(authSub *OutputStream) {
p.Flush()
}
func (p *Receiver) readAVPacket(avType byte) (av *pool.AVPacket, err error) {
func (p *Receiver) readAVPacket(avType byte) (av *avformat.AVPacket, err error) {
buf := pool.GetSlice(4)
defer pool.RecycleSlice(buf)
_, err = io.ReadFull(p, buf)
if err != nil {
println(err.Error())
return
}
av = pool.NewAVPacket(avType)
av = avformat.NewAVPacket(avType)
av.Timestamp = binary.BigEndian.Uint32(buf)
_, err = io.ReadFull(p, buf)
if MayBeError(err) {
@@ -39,10 +42,7 @@ func (p *Receiver) readAVPacket(avType byte) (av *pool.AVPacket, err error) {
}
av.Payload = pool.GetSlice(int(binary.BigEndian.Uint32(buf)))
_, err = io.ReadFull(p, av.Payload)
if MayBeError(err) {
return
}
pool.RecycleSlice(buf)
MayBeError(err)
return
}
@@ -57,7 +57,7 @@ func PullUpStream(streamPath string) {
}
brw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
p := &Receiver{
Reader: conn,
Reader: brw.Reader,
Writer: brw.Writer,
}
if p.Publish(streamPath, p) {
@@ -72,11 +72,7 @@ func PullUpStream(streamPath string) {
return
}
defer p.Cancel()
for {
cmd, err := brw.ReadByte()
if MayBeError(err) {
return
}
for cmd, err := brw.ReadByte(); !MayBeError(err); cmd, err = brw.ReadByte() {
switch cmd {
case MSG_AUDIO:
if audio, err := p.readAVPacket(avformat.FLV_TAG_TYPE_AUDIO); err == nil {
@@ -103,6 +99,8 @@ func PullUpStream(streamPath string) {
v.Cancel()
}
}
default:
log.Printf("unknown cmd:%v", cmd)
}
}
}

View File

@@ -5,6 +5,8 @@ import (
"encoding/binary"
"encoding/json"
"fmt"
"github.com/langhuihui/monibuca/monica/avformat"
"io"
"net"
"strconv"
"strings"
@@ -53,7 +55,7 @@ func process(conn net.Conn) {
reader := bufio.NewReader(conn)
connAddr := conn.RemoteAddr().String()
stream := OutputStream{
SendHandler: func(p *pool.SendPacket) error {
SendHandler: func(p *avformat.SendPacket) error {
head := pool.GetSlice(9)
head[0] = p.Packet.Type - 7
binary.BigEndian.PutUint32(head[1:5], p.Timestamp)
@@ -80,29 +82,32 @@ func process(conn net.Conn) {
if err != nil {
return
}
bytes = bytes[0 : len(bytes)-1]
switch cmd {
case MSG_SUBSCRIBE:
if stream.Room != nil {
fmt.Printf("bare stream already exist from %s", conn.RemoteAddr())
return
}
streamName := string(bytes[0 : len(bytes)-1])
go stream.Play(streamName)
go stream.Play(string(bytes))
case MSG_AUTH:
sign := strings.Split(string(bytes[0:len(bytes)-1]), ",")
sign := strings.Split(string(bytes), ",")
head := []byte{MSG_AUTH, 2}
if len(sign) > 1 && AuthHooks.Trigger(sign[1]) == nil {
head[1] = 1
}
conn.Write(head)
conn.Write(bytes)
conn.Write(bytes[0 : len(bytes)+1])
case MSG_SUMMARY: //收到从服务器发来报告,加入摘要中
var summary *ServerSummary
summary := &ServerSummary{}
if err = json.Unmarshal(bytes, summary); err == nil {
summary.Address = connAddr
Summary.Report(summary)
if _, ok := slaves.Load(connAddr); !ok {
slaves.Store(connAddr, conn)
if Summary.Running() {
orderReport(io.Writer(conn), true)
}
defer slaves.Delete(connAddr)
}
}

View File

@@ -1,74 +1,23 @@
package gateway
import (
"context"
"encoding/json"
. "github.com/langhuihui/monibuca/monica"
. "github.com/langhuihui/monibuca/monica/util"
"io/ioutil"
"log"
"mime"
"net/http"
"os/exec"
"path"
"runtime"
"time"
. "github.com/langhuihui/monibuca/monica"
)
var (
config = new(ListenerConfig)
sseBegin = []byte("data: ")
sseEnd = []byte("\n\n")
config = new(ListenerConfig)
dashboardPath string
)
type SSE struct {
http.ResponseWriter
context.Context
}
func (sse *SSE) Write(data []byte) (n int, err error) {
if err = sse.Err(); err != nil {
return
}
_, err = sse.ResponseWriter.Write(sseBegin)
n, err = sse.ResponseWriter.Write(data)
_, err = sse.ResponseWriter.Write(sseEnd)
if err != nil {
return
}
sse.ResponseWriter.(http.Flusher).Flush()
return
}
func NewSSE(w http.ResponseWriter, ctx context.Context) *SSE {
header := w.Header()
header.Set("Content-Type", "text/event-stream")
header.Set("Cache-Control", "no-cache")
header.Set("Connection", "keep-alive")
header.Set("X-Accel-Buffering", "no")
header.Set("Access-Control-Allow-Origin", "*")
return &SSE{
w,
ctx,
}
}
func (sse *SSE) WriteJSON(data interface{}) (err error) {
var jsonData []byte
if jsonData, err = json.Marshal(data); err == nil {
if _, err = sse.Write(jsonData); err != nil {
return
}
return
}
return
}
func (sse *SSE) WriteExec(cmd *exec.Cmd) error {
cmd.Stderr = sse
cmd.Stdout = sse
return cmd.Run()
}
func init() {
_, currentFilePath, _, _ := runtime.Caller(0)
dashboardPath = path.Join(path.Dir(currentFilePath), "../../dashboard/dist")

View File

@@ -2,12 +2,13 @@ package jessica
import (
"encoding/binary"
"net/http"
"strings"
"github.com/gobwas/ws"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"net/http"
"strings"
)
func WsHandler(w http.ResponseWriter, r *http.Request) {
@@ -31,7 +32,7 @@ func WsHandler(w http.ResponseWriter, r *http.Request) {
defer conn.Close()
if isFlv {
baseStream.Type = "JessicaFlv"
baseStream.SendHandler = func(packet *pool.SendPacket) error {
baseStream.SendHandler = func(packet *avformat.SendPacket) error {
return avformat.WriteFLVTag(conn, packet)
}
if err := ws.WriteHeader(conn, ws.Header{
@@ -46,7 +47,7 @@ func WsHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
baseStream.Type = "Jessica"
baseStream.SendHandler = func(packet *pool.SendPacket) error {
baseStream.SendHandler = func(packet *avformat.SendPacket) error {
err := ws.WriteHeader(conn, ws.Header{
Fin: true,
OpCode: ws.OpBinary,

View File

@@ -3,7 +3,6 @@ package record
import (
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"github.com/langhuihui/monibuca/monica/util"
"io"
"os"
@@ -17,7 +16,7 @@ func getDuration(file *os.File) uint32 {
if tagSize, err = util.ReadByteToUint32(file, true); err == nil {
_, err = file.Seek(-int64(tagSize)-4, io.SeekEnd)
if err == nil {
var tag *pool.AVPacket
var tag *avformat.AVPacket
tag, err = avformat.ReadFLVTag(file)
if err == nil {
return tag.Timestamp
@@ -40,7 +39,7 @@ func SaveFlv(streamPath string, append bool) error {
if err != nil {
return err
}
p := OutputStream{SendHandler: func(packet *pool.SendPacket) error {
p := OutputStream{SendHandler: func(packet *avformat.SendPacket) error {
return avformat.WriteFLVTag(file, packet)
}}
p.ID = filePath

View File

@@ -3,6 +3,7 @@ package rtmp
import (
"bufio"
"errors"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"github.com/langhuihui/monibuca/monica/util"
"io"
@@ -312,21 +313,21 @@ func (conn *NetConnection) SendMessage(message string, args interface{}) error {
return conn.writeMessage(RTMP_MSG_AMF0_COMMAND, m)
case SEND_UNPUBLISH_RESPONSE_MESSAGE:
case SEND_FULL_AUDIO_MESSAGE:
audio, ok := args.(*pool.SendPacket)
audio, ok := args.(*avformat.SendPacket)
if !ok {
errors.New(message + ", The parameter is AVPacket")
}
return conn.sendAVMessage(audio, true, true)
case SEND_AUDIO_MESSAGE:
audio, ok := args.(*pool.SendPacket)
audio, ok := args.(*avformat.SendPacket)
if !ok {
errors.New(message + ", The parameter is AVPacket")
}
return conn.sendAVMessage(audio, true, false)
case SEND_FULL_VDIEO_MESSAGE:
video, ok := args.(*pool.SendPacket)
video, ok := args.(*avformat.SendPacket)
if !ok {
errors.New(message + ", The parameter is AVPacket")
}
@@ -334,7 +335,7 @@ func (conn *NetConnection) SendMessage(message string, args interface{}) error {
return conn.sendAVMessage(video, false, true)
case SEND_VIDEO_MESSAGE:
{
video, ok := args.(*pool.SendPacket)
video, ok := args.(*avformat.SendPacket)
if !ok {
errors.New(message + ", The parameter is AVPacket")
}
@@ -349,7 +350,7 @@ func (conn *NetConnection) SendMessage(message string, args interface{}) error {
// 当发送音视频数据的时候,当块类型为12的时候,Chunk Message Header有一个字段TimeStamp,指明一个时间
// 当块类型为4,8的时候,Chunk Message Header有一个字段TimeStamp Delta,记录与上一个Chunk的时间差值
// 当块类型为0的时候,Chunk Message Header没有时间字段,与上一个Chunk时间值相同
func (conn *NetConnection) sendAVMessage(av *pool.SendPacket, isAudio bool, isFirst bool) error {
func (conn *NetConnection) sendAVMessage(av *avformat.SendPacket, isAudio bool, isFirst bool) error {
if conn.writeSeqNum > conn.bandwidth {
conn.totalWrite += conn.writeSeqNum
conn.writeSeqNum = 0

View File

@@ -5,7 +5,6 @@ import (
"fmt"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/avformat"
"github.com/langhuihui/monibuca/monica/pool"
"log"
"net"
"strings"
@@ -103,7 +102,7 @@ func processRtmp(conn net.Conn) {
streamPath := nc.appName + "/" + strings.Split(pm.PublishingName, "?")[0]
pub := new(RTMP)
if pub.Publish(streamPath, pub) {
pub.FirstScreen = make([]*pool.AVPacket, 0)
pub.FirstScreen = make([]*avformat.AVPacket, 0)
room = pub.Room
err = nc.SendMessage(SEND_STREAM_BEGIN_MESSAGE, nil)
err = nc.SendMessage(SEND_PUBLISH_START_MESSAGE, newPublishResponseMessageData(nc.streamID, NetStream_Publish_Start, Level_Status))
@@ -114,15 +113,15 @@ func processRtmp(conn net.Conn) {
pm := msg.MsgData.(*PlayMessage)
streamPath := nc.appName + "/" + strings.Split(pm.StreamName, "?")[0]
nc.writeChunkSize = 512
stream := &OutputStream{SendHandler: func(packet *pool.SendPacket) (err error) {
stream := &OutputStream{SendHandler: func(packet *avformat.SendPacket) (err error) {
switch true {
case packet.Packet.IsADTS:
tagPacket := pool.NewAVPacket(RTMP_MSG_AUDIO)
tagPacket := avformat.NewAVPacket(RTMP_MSG_AUDIO)
tagPacket.Payload = avformat.ADTSToAudioSpecificConfig(packet.Packet.Payload)
err = nc.SendMessage(SEND_FULL_AUDIO_MESSAGE, tagPacket)
ADTSLength := 7 + (int(packet.Packet.Payload[1]&1) << 1)
if len(packet.Packet.Payload) > ADTSLength {
contentPacket := pool.NewAVPacket(RTMP_MSG_AUDIO)
contentPacket := avformat.NewAVPacket(RTMP_MSG_AUDIO)
contentPacket.Timestamp = packet.Timestamp
contentPacket.Payload = make([]byte, len(packet.Packet.Payload)-ADTSLength+2)
contentPacket.Payload[0] = 0xAF
@@ -162,7 +161,7 @@ func processRtmp(conn net.Conn) {
}
}
case RTMP_MSG_AUDIO:
pkt := pool.NewAVPacket(RTMP_MSG_AUDIO)
pkt := avformat.NewAVPacket(RTMP_MSG_AUDIO)
if msg.Timestamp == 0xffffff {
totalDuration += msg.ExtendTimestamp
} else {
@@ -172,7 +171,7 @@ func processRtmp(conn net.Conn) {
pkt.Payload = msg.Body
room.PushAudio(pkt)
case RTMP_MSG_VIDEO:
pkt := pool.NewAVPacket(RTMP_MSG_VIDEO)
pkt := avformat.NewAVPacket(RTMP_MSG_VIDEO)
if msg.Timestamp == 0xffffff {
totalDuration += msg.ExtendTimestamp
} else {

21
pm/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
.DS_Store
node_modules
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
pm/README.md Normal file
View File

@@ -0,0 +1,24 @@
# pm
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
pm/babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

1
pm/dist/css/app.74a1e2f4.css vendored Normal file
View File

@@ -0,0 +1 @@
.content{background:#fff}pre{white-space:pre-wrap;word-wrap:break-word}.ivu-tabs .ivu-tabs-tabpane{padding:20px}

File diff suppressed because one or more lines are too long

BIN
pm/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
pm/dist/fonts/ionicons.143146fa.woff2 vendored Normal file

Binary file not shown.

BIN
pm/dist/fonts/ionicons.99ac3308.woff vendored Normal file

Binary file not shown.

BIN
pm/dist/fonts/ionicons.d535a25a.ttf vendored Normal file

Binary file not shown.

870
pm/dist/img/ionicons.a2c4a261.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 542 KiB

1
pm/dist/index.html vendored Normal file
View File

@@ -0,0 +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>pm</title><link href=/css/app.74a1e2f4.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.4b08c1d1.js rel=preload as=script><link href=/js/chunk-vendors.6b87e1b5.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.74a1e2f4.css rel=stylesheet></head><body><noscript><strong>We're sorry but pm doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.6b87e1b5.js></script><script src=/js/app.4b08c1d1.js></script></body></html>

2
pm/dist/js/app.4b08c1d1.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pm/dist/js/app.4b08c1d1.js.map vendored Normal file

File diff suppressed because one or more lines are too long

44
pm/dist/js/chunk-vendors.6b87e1b5.js 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

11588
pm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
pm/package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "pm",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.4.4",
"view-design": "^4.0.0",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
"@vue/cli-plugin-router": "^4.1.0",
"@vue/cli-plugin-vuex": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"babel-eslint": "^10.0.3",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-cli-plugin-iview": "^2.0.0",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

BIN
pm/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
pm/public/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!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.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>pm</title>
</head>
<body>
<noscript>
<strong>We're sorry but pm doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

17
pm/src/App.vue Normal file
View File

@@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'app',
components: {
}
}
</script>
<style>
</style>

BIN
pm/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,57 @@
<template>
<Modal v-bind="$attrs" v-on="$listeners" :title="info.Path">
<Steps :current="currentStep" size="small" :status="status">
<Step title="解析请求"></Step>
<Step title="创建目录"></Step>
<Step title="写入文件"></Step>
<Step title="执行go mod init"></Step>
<Step title="执行go build"></Step>
<Step title="启动实例"></Step>
<Step title="完成"></Step>
</Steps>
<div>
<pre>{{log}}</pre>
</div>
</Modal>
</template>
<script>
let eventSource = null
export default {
name: "CreateInstance",
props: {
info: Object,
},
watch: {
info(v) {
if (v) {
eventSource = new EventSource("/create?info="+JSON.stringify(v))
eventSource.onmessage = evt => {
this.log += evt.data + "\n"
if (evt.data == "success") {
this.status = "finish"
eventSource.close()
}
}
eventSource.addEventListener("exception", evt => {
this.log += evt.data + "\n"
this.status = "error"
eventSource.close()
})
eventSource.addEventListener("step", evt => {
let [step,msg] = evt.data.split(":")
this.currentStep = step|0
this.log+=msg+"\n"
})
}
}
},
data() {
return {currentStep: 0, log: "", status: "process"}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

13
pm/src/main.js Normal file
View File

@@ -0,0 +1,13 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './plugins/iview.js'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

6
pm/src/plugins/iview.js Normal file
View File

@@ -0,0 +1,6 @@
import Vue from 'vue'
import ViewUI from 'view-design'
Vue.use(ViewUI)
import 'view-design/dist/styles/iview.css'

20
pm/src/router/index.js Normal file
View File

@@ -0,0 +1,20 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Instances from "../views/Instances"
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'instances',
component: Instances
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

15
pm/src/store/index.js Normal file
View File

@@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

18
pm/src/views/Home.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'home',
components: {
HelloWorld
}
}
</script>

206
pm/src/views/Instances.vue Normal file
View File

@@ -0,0 +1,206 @@
<template>
<Layout class="layout">
<Header style=" background:unset;text-align: center;">Monibuca 实例管理器</Header>
<Content class="content">
<Tabs value="name1">
<TabPane label="实例" name="name1">
<List border>
<ListItem v-for="item in instances" :key="item.Name">
<ListItemMeta :title="item.Name" :description="item.Path"></ListItemMeta>
</ListItem>
</List>
</TabPane>
<TabPane label="创建" name="name2">
<Steps :current="createStep">
<Step title="选择目录" content="选择创建实例的目录"></Step>
<Step title="选插件" content="选择要启用的插件"></Step>
<Step title="完成" content="完成实例创建"></Step>
</Steps>
<div style="margin:50px;width:auto">
<i-input v-model="createPath" v-if="createStep==0">
<Button slot="prepend" icon="md-arrow-round-up" @click="goUp"></Button>
</i-input>
<List v-else-if="createStep==1" border>
<ListItem v-for="(item,name) in plugins" :key="name">
<ListItemMeta :title="name" :description="item.Path"></ListItemMeta>
{{item.Config}}
<template slot="action">
<li @click="removePlugin(name)">
<Icon type="ios-trash"/>
移除
</li>
</template>
</ListItem>
</List>
<div v-else>
<h3>实例名称</h3>
<i-input v-model="instanceName" :placeholder="createPath.split('/').pop()"></i-input>
<h4>安装路径</h4>
<div>
<pre>{{createPath}}</pre>
</div>
<h4>启用的插件</h4>
<div>
<pre>{{pluginStr}}</pre>
</div>
<h4>配置文件</h4>
<div>
<pre>{{configStr}}</pre>
</div>
</div>
<ButtonGroup style="display:table;margin:50px auto;">
<Button size="large" type="primary" @click="createStep--" v-if="createStep!=0">
<Icon type="ios-arrow-back"></Icon>
上一步
</Button>
<Button size="large" type="success" @click="showAddPlugin=true" v-if="createStep==1">+
添加插件
</Button>
<Button size="large" type="primary" @click="createStep++" v-if="createStep!=2">下一步
<Icon type="ios-arrow-forward"></Icon>
</Button>
<Button size="large" type="success" @click="createInstance" v-else>开始创建</Button>
</ButtonGroup>
</div>
</TabPane>
<TabPane label="导入" name="name3">
</TabPane>
</Tabs>
</Content>
<Modal v-model="showAddPlugin" title="添加Plugin" @on-ok="addPlugin">
<Form :model="formPlugin" label-position="top">
<FormItem label="插件名称">
<i-input v-model="formPlugin.Name" placeholder="插件名称必须和插件注册时的名称一致"></i-input>
</FormItem>
<FormItem label="插件包地址">
<i-input v-model="formPlugin.Path">
<Button slot="append" @click="showBuiltinPlugin=true">内置插件</Button>
</i-input>
</FormItem>
<Alert type="show-icon" v-if="!Object.values(builtinPlugins).includes(formPlugin.Path)">
如果该插件是私有仓库请到服务器上输入echo "machine {{privateHost}} login 用户名 password 密码" >> ~/.netrc
并且添加环境变量GOPRIVATE={{privateHost}}
</Alert>
<FormItem label="插件配置信息">
<i-input type="textarea" v-model="formPlugin.Config" placeholder="请输入toml格式"></i-input>
</FormItem>
</Form>
</Modal>
<Modal v-model="showBuiltinPlugin">
<List>
<ListItem v-for="(item,name) in builtinPlugins" :key="name">
<ListItemMeta :title="name" :description="item"></ListItemMeta>
<template slot="action">
<li @click="addBuiltin(name,item)">
<Icon type="ios-add"/>
添加
</li>
</template>
</ListItem>
</List>
</Modal>
<CreateInstance v-model="showCreate" :info="createInfo"></CreateInstance>
</Layout>
</template>
<script>
import CreateInstance from "../components/CreateInstance"
export default {
components: {
CreateInstance
},
data() {
return {
instanceName: "",
createStep: 0,
showCreate: false,
createInfo: null,
createPath: "/opt/monibuca",
instances: [],
plugins: {},
showAddPlugin: false,
formPlugin: {},
showBuiltinPlugin: false,
builtinPlugins: {
Auth: "github.com/langhuihui/monibuca/plugins/auth",
Cluster: "github.com/langhuihui/monibuca/plugins/cluster",
GateWay: "github.com/langhuihui/monibuca/plugins/gateway",
HDL: "github.com/langhuihui/monibuca/plugins/HDL",
Jessica: "github.com/langhuihui/monibuca/plugins/jessica",
QoS: "github.com/langhuihui/monibuca/plugins/QoS",
RecordFlv: "github.com/langhuihui/monibuca/plugins/record",
RTMP: "github.com/langhuihui/monibuca/plugins/rtmp"
},
defaultConfig: {
Auth: 'Key = "www.monibuca.com"',
RecordFlv: 'Path="./resource"',
QoS: 'Suffix = ["high","medium","low"]',
Cluster: 'Master = "localhost:2019"\nListenAddr = ":2019"',
GateWay: 'ListenAddr = ":8081"',
RTMP: 'ListenAddr = ":1935"',
Jessica: 'ListenAddr = ":8080"',
HDL: 'ListenAddr = ":2020"',
}
}
},
computed: {
pluginStr() {
return Object.values(this.plugins).map(x => x.Path).join("\n")
},
configStr() {
return Object.values(this.plugins).map(x => `[Plugins.${x.Name}]
${x.Config || ""}`).join("\n")
},
privateHost(){
return (this.formPlugin.Path && this.formPlugin.Path.split("/")[0])||"仓库域名"
}
},
methods: {
goUp() {
let paths = this.createPath.split("/")
paths.pop()
this.createPath = paths.join("/")
},
createInstance() {
this.showCreate = true
this.createInfo = {
Name: this.instanceName || this.createPath.split('/').pop(),
Path: this.createPath,
Plugins: Object.values(this.plugins).map(x => x.Path),
Config: this.configStr
}
},
addPlugin() {
this.plugins[this.formPlugin.Name] = this.formPlugin
this.formPlugin = {}
},
removePlugin(name) {
delete this.plugins[name]
this.$forceUpdate()
},
addBuiltin(name, item) {
this.formPlugin.Name = name
this.formPlugin.Path = item
this.formPlugin.Config = this.defaultConfig[name]
this.showBuiltinPlugin = false
}
}
}
</script>
<style>
.content {
background: white
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.ivu-tabs .ivu-tabs-tabpane {
padding: 20px;
}
</style>

18
slave.toml Normal file
View File

@@ -0,0 +1,18 @@
# [Plugins.HDL]
# ListenAddr = ":2020"
[Plugins.Jessica]
ListenAddr = ":8082"
[Plugins.RTMP]
ListenAddr = ":1936"
[Plugins.GateWay]
ListenAddr = ":8083"
[Plugins.Cluster]
Master = "localhost:2019"
#ListenAddr = ":2019"
#
#[Plugins.Auth]
#Key="www.monibuca.com"
# [Plugins.RecordFlv]
# Path="./resource"
# [Plugins.QoS]
# Suffix = ["high","medium","low"]