mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-10-03 16:07:22 +08:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
649a5b558a | ||
![]() |
b3c8d35fad | ||
![]() |
ab745145d9 |
@@ -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"
|
||||
|
@@ -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}
|
2
dashboard/dist/index.html
vendored
2
dashboard/dist/index.html
vendored
@@ -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>
|
13
dashboard/dist/jessibuca/renderer.js
vendored
13
dashboard/dist/jessibuca/renderer.js
vendored
@@ -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
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
1
dashboard/dist/js/app.017fb959.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dashboard/dist/js/app.af5e5ef3.js
vendored
2
dashboard/dist/js/app.af5e5ef3.js
vendored
File diff suppressed because one or more lines are too long
1
dashboard/dist/js/app.af5e5ef3.js.map
vendored
1
dashboard/dist/js/app.af5e5ef3.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -27,7 +27,9 @@ func main() {
|
||||
|
||||
该配置文件主要是为了定制各个插件的配置,例如监听端口号等,具体还是要看各个插件的设计。
|
||||
|
||||
> 如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息
|
||||
::: tip
|
||||
如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息
|
||||
:::
|
||||
|
||||
如果注释掉部分插件的配置,那么该插件就不会启用,典型的配置如下:
|
||||
```toml
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -118,6 +118,7 @@ export default {
|
||||
currentTab: "",
|
||||
currentStream: [],
|
||||
typeMap: {
|
||||
Receiver: "📡",
|
||||
FlvFile: "🎥",
|
||||
TS: "🎬",
|
||||
HLS: "🍎",
|
||||
|
270
main.go
270
main.go
@@ -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
BIN
monibuca.exe~
Normal file
Binary file not shown.
@@ -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--
|
@@ -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))
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
73
monica/util/SSE.go
Normal 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()
|
||||
}
|
@@ -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{
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
21
pm/.gitignore
vendored
Normal 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
24
pm/README.md
Normal 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
5
pm/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
1
pm/dist/css/app.74a1e2f4.css
vendored
Normal file
1
pm/dist/css/app.74a1e2f4.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.content{background:#fff}pre{white-space:pre-wrap;word-wrap:break-word}.ivu-tabs .ivu-tabs-tabpane{padding:20px}
|
1
pm/dist/css/chunk-vendors.22ebf426.css
vendored
Normal file
1
pm/dist/css/chunk-vendors.22ebf426.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
pm/dist/favicon.ico
vendored
Normal file
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
BIN
pm/dist/fonts/ionicons.143146fa.woff2
vendored
Normal file
Binary file not shown.
BIN
pm/dist/fonts/ionicons.99ac3308.woff
vendored
Normal file
BIN
pm/dist/fonts/ionicons.99ac3308.woff
vendored
Normal file
Binary file not shown.
BIN
pm/dist/fonts/ionicons.d535a25a.ttf
vendored
Normal file
BIN
pm/dist/fonts/ionicons.d535a25a.ttf
vendored
Normal file
Binary file not shown.
870
pm/dist/img/ionicons.a2c4a261.svg
vendored
Normal file
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
1
pm/dist/index.html
vendored
Normal 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
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
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
44
pm/dist/js/chunk-vendors.6b87e1b5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pm/dist/js/chunk-vendors.6b87e1b5.js.map
vendored
Normal file
1
pm/dist/js/chunk-vendors.6b87e1b5.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
11588
pm/package-lock.json
generated
Normal file
11588
pm/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
pm/package.json
Normal file
49
pm/package.json
Normal 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
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
17
pm/public/index.html
Normal 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
17
pm/src/App.vue
Normal 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
BIN
pm/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
57
pm/src/components/CreateInstance.vue
Normal file
57
pm/src/components/CreateInstance.vue
Normal 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>
|
60
pm/src/components/HelloWorld.vue
Normal file
60
pm/src/components/HelloWorld.vue
Normal 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
13
pm/src/main.js
Normal 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
6
pm/src/plugins/iview.js
Normal 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
20
pm/src/router/index.js
Normal 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
15
pm/src/store/index.js
Normal 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
18
pm/src/views/Home.vue
Normal 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
206
pm/src/views/Instances.vue
Normal 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
18
slave.toml
Normal 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"]
|
Reference in New Issue
Block a user