Compare commits

...

11 Commits

Author SHA1 Message Date
langhuihui
66a396df0b 重启脚本修改 2020-02-21 16:01:46 +08:00
langhuihui
029abe3187 加入设计原理文档 2020-02-21 10:09:37 +08:00
langhuihui
40fba0d348 支持PES中包含多个AAC帧 2020-02-19 14:58:21 +08:00
langhuihui
73941d1e0b ADTS转RTMP协议方式传输音频包 2020-02-18 15:19:21 +08:00
langhuihui
709c2c6ac7 增加目录候选功能 2020-02-17 11:48:58 +08:00
langhuihui
f96bc11ddb 增强对实例的控制 2020-02-15 21:07:47 +08:00
langhuihui
5563ddc0d2 增强对实例的控制 2020-02-14 09:54:53 +08:00
langhuihui
95657bd6df 增强对实例的控制 2020-02-13 17:41:39 +08:00
langhuihui
b9e19e75c8 增强对实例的控制 2020-02-13 10:43:32 +08:00
langhuihui
eac623639d 界面增加重启和升级 2020-02-11 21:59:31 +08:00
langhuihui
fea6e98ca7 小功能增加 2020-02-11 17:27:05 +08:00
68 changed files with 2315 additions and 510 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
*.exe
.vscode
.idea
resource
resource
*.log
/monibuca

View File

@@ -7,11 +7,11 @@
<meta name="description" content="">
<link rel="preload" href="/docs/assets/css/styles.d51c815b.css" as="style"><link rel="preload" href="/docs/assets/js/app.d51c815b.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.6babbc1d.js"><link rel="prefetch" href="/docs/assets/js/2.190ec46a.js"><link rel="prefetch" href="/docs/assets/js/3.7646e76c.js"><link rel="prefetch" href="/docs/assets/js/4.08fbc0d9.js">
<link rel="stylesheet" href="/docs/assets/css/styles.d51c815b.css">
<link rel="preload" href="/docs/assets/css/styles.1fc3f87c.css" as="style"><link rel="preload" href="/docs/assets/js/app.1fc3f87c.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.05f88c5b.js"><link rel="prefetch" href="/docs/assets/js/2.69b20946.js"><link rel="prefetch" href="/docs/assets/js/3.197b5253.js"><link rel="prefetch" href="/docs/assets/js/4.2a48a234.js"><link rel="prefetch" href="/docs/assets/js/5.bd73b45e.js">
<link rel="stylesheet" href="/docs/assets/css/styles.1fc3f87c.css">
</head>
<body>
<div id="app" data-server-rendered="true"><div class="theme-container"><div class="content"><h1>404</h1><blockquote>Looks like we've got some broken links.</blockquote><a href="/docs/" class="router-link-active">Take me home.</a></div></div></div>
<script src="/docs/assets/js/app.d51c815b.js" defer></script>
<div id="app" data-server-rendered="true"><div class="theme-container"><div class="content"><h1>404</h1><blockquote>There's nothing here.</blockquote><a href="/docs/" class="router-link-active">Take me home.</a></div></div></div>
<script src="/docs/assets/js/app.1fc3f87c.js" defer></script>
</body>
</html>

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

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[3],{165:function(t,e,s){"use strict";s.r(e);var n=s(0),i=Object(n.a)({},(function(){var t=this.$createElement;this._self._c;return this._m(0)}),[function(){var t=this.$createElement,e=this._self._c||t;return e("div",{staticClass:"content"},[e("h1",{attrs:{id:"更新历史"}},[e("a",{staticClass:"header-anchor",attrs:{href:"#更新历史","aria-hidden":"true"}},[this._v("#")]),this._v(" 更新历史")]),e("ul",[e("li",[this._v("2020/1/27\n初步完成")])])])}],!1,null,null,null);e.default=i.exports}}]);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[4],{162:function(t,s,e){"use strict";e.r(s);var i=e(0),n=Object(i.a)({},(function(){var t=this.$createElement;this._self._c;return this._m(0)}),[function(){var t=this.$createElement,s=this._self._c||t;return s("div",{staticClass:"content"},[s("h1",{attrs:{id:"更新历史"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#更新历史","aria-hidden":"true"}},[this._v("#")]),this._v(" 更新历史")]),s("ul",[s("li",[this._v("2020/2/20\n完成实例管理器")]),s("li",[this._v("2020/1/27\n完成核心架构")])])])}],!1,null,null,null);s.default=n.exports}}]);

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

30
dashboard/dist/docs/design.html vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,13 +7,13 @@
<meta name="description" content="">
<link rel="preload" href="/docs/assets/css/styles.d51c815b.css" as="style"><link rel="preload" href="/docs/assets/js/app.d51c815b.js" as="script"><link rel="preload" href="/docs/assets/js/2.190ec46a.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.6babbc1d.js"><link rel="prefetch" href="/docs/assets/js/3.7646e76c.js"><link rel="prefetch" href="/docs/assets/js/4.08fbc0d9.js">
<link rel="stylesheet" href="/docs/assets/css/styles.d51c815b.css">
<link rel="preload" href="/docs/assets/css/styles.1fc3f87c.css" as="style"><link rel="preload" href="/docs/assets/js/app.1fc3f87c.js" as="script"><link rel="preload" href="/docs/assets/js/3.197b5253.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.05f88c5b.js"><link rel="prefetch" href="/docs/assets/js/2.69b20946.js"><link rel="prefetch" href="/docs/assets/js/4.2a48a234.js"><link rel="prefetch" href="/docs/assets/js/5.bd73b45e.js">
<link rel="stylesheet" href="/docs/assets/css/styles.1fc3f87c.css">
</head>
<body>
<div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div><a href="/docs/" class="home-link router-link-active"><!----><span class="site-name">
Monibuca
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="active sidebar-link">插件开发</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/develop.html#插件的定义" class="sidebar-link">插件的定义</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#插件的安装" class="sidebar-link">插件的安装</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发订阅者插件" class="sidebar-link">开发订阅者插件</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发发布者插件" class="sidebar-link">开发发布者插件</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发钩子插件" class="sidebar-link">开发钩子插件</a></li></ul></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li></ul></div><div class="page"><div class="content"><h1 id="插件开发"><a href="#插件开发" aria-hidden="true" class="header-anchor">#</a> 插件开发</h1><h2 id="插件的定义"><a href="#插件的定义" aria-hidden="true" class="header-anchor">#</a> 插件的定义</h2><p>所谓的插件,没有什么固定的规则,只需要完成<code>安装</code>操作即可。插件可以实现任意的功能扩展,最常见的是实现某种传输协议用来推流或者拉流</p><h2 id="插件的安装"><a href="#插件的安装" aria-hidden="true" class="header-anchor">#</a> 插件的安装</h2><p>下面是内置插件jessica的源码代表了典型的插件安装</p><div class="language-go extra-class"><pre class="language-go"><code><span class="token keyword">package</span> jessica
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="active sidebar-link">插件开发</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/develop.html#插件的定义" class="sidebar-link">插件的定义</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#插件的安装" class="sidebar-link">插件的安装</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发订阅者插件" class="sidebar-link">开发订阅者插件</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发发布者插件" class="sidebar-link">开发发布者插件</a></li><li class="sidebar-sub-header"><a href="/docs/develop.html#开发钩子插件" class="sidebar-link">开发钩子插件</a></li></ul></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li><li><a href="/docs/design.html" class="sidebar-link">设计原理</a></li></ul></div><div class="page"><div class="content"><h1 id="插件开发"><a href="#插件开发" aria-hidden="true" class="header-anchor">#</a> 插件开发</h1><h2 id="插件的定义"><a href="#插件的定义" aria-hidden="true" class="header-anchor">#</a> 插件的定义</h2><p>所谓的插件,没有什么固定的规则,只需要完成<code>安装</code>操作即可。插件可以实现任意的功能扩展,最常见的是实现某种传输协议用来推流或者拉流</p><h2 id="插件的安装"><a href="#插件的安装" aria-hidden="true" class="header-anchor">#</a> 插件的安装</h2><p>下面是内置插件jessica的源码代表了典型的插件安装</p><div class="language-go extra-class"><pre class="language-go"><code><span class="token keyword">package</span> jessica
<span class="token keyword">import</span> <span class="token punctuation">(</span>
<span class="token punctuation">.</span> <span class="token string">&quot;github.com/langhuihui/monibuca/monica&quot;</span>
@@ -184,6 +184,6 @@
更新日志
</a>
</span></p></div></div></div></div>
<script src="/docs/assets/js/app.d51c815b.js" defer></script><script src="/docs/assets/js/2.190ec46a.js" defer></script>
<script src="/docs/assets/js/app.1fc3f87c.js" defer></script><script src="/docs/assets/js/3.197b5253.js" defer></script>
</body>
</html>

View File

@@ -7,20 +7,21 @@
<meta name="description" content="">
<link rel="preload" href="/docs/assets/css/styles.d51c815b.css" as="style"><link rel="preload" href="/docs/assets/js/app.d51c815b.js" as="script"><link rel="preload" href="/docs/assets/js/3.7646e76c.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.6babbc1d.js"><link rel="prefetch" href="/docs/assets/js/2.190ec46a.js"><link rel="prefetch" href="/docs/assets/js/4.08fbc0d9.js">
<link rel="stylesheet" href="/docs/assets/css/styles.d51c815b.css">
<link rel="preload" href="/docs/assets/css/styles.1fc3f87c.css" as="style"><link rel="preload" href="/docs/assets/js/app.1fc3f87c.js" as="script"><link rel="preload" href="/docs/assets/js/4.2a48a234.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.05f88c5b.js"><link rel="prefetch" href="/docs/assets/js/2.69b20946.js"><link rel="prefetch" href="/docs/assets/js/3.197b5253.js"><link rel="prefetch" href="/docs/assets/js/5.bd73b45e.js">
<link rel="stylesheet" href="/docs/assets/css/styles.1fc3f87c.css">
</head>
<body>
<div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div><a href="/docs/" class="home-link router-link-active"><!----><span class="site-name">
Monibuca
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="active sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li></ul></div><div class="page"><div class="content"><h1 id="更新历史"><a href="#更新历史" aria-hidden="true" class="header-anchor">#</a> 更新历史</h1><ul><li>2020/1/27
初步完成</li></ul></div><div class="page-edit"><!----><!----></div><div class="page-nav"><p class="inner"><span class="prev">
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="active sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li><li><a href="/docs/design.html" class="sidebar-link">设计原理</a></li></ul></div><div class="page"><div class="content"><h1 id="更新历史"><a href="#更新历史" aria-hidden="true" class="header-anchor">#</a> 更新历史</h1><ul><li>2020/2/20
完成实例管理器</li><li>2020/1/27
完成核心架构</li></ul></div><div class="page-edit"><!----><!----></div><div class="page-nav"><p class="inner"><span class="prev">
<a href="/docs/develop.html" class="prev">
插件开发
</a></span><span class="next"><a href="/docs/plugins.html">
内置插件
</a>
</span></p></div></div></div></div>
<script src="/docs/assets/js/app.d51c815b.js" defer></script><script src="/docs/assets/js/3.7646e76c.js" defer></script>
<script src="/docs/assets/js/app.1fc3f87c.js" defer></script><script src="/docs/assets/js/4.2a48a234.js" defer></script>
</body>
</html>

View File

@@ -7,16 +7,20 @@
<meta name="description" content="">
<link rel="preload" href="/docs/assets/css/styles.d51c815b.css" as="style"><link rel="preload" href="/docs/assets/js/app.d51c815b.js" as="script"><link rel="preload" href="/docs/assets/js/1.6babbc1d.js" as="script"><link rel="prefetch" href="/docs/assets/js/2.190ec46a.js"><link rel="prefetch" href="/docs/assets/js/3.7646e76c.js"><link rel="prefetch" href="/docs/assets/js/4.08fbc0d9.js">
<link rel="stylesheet" href="/docs/assets/css/styles.d51c815b.css">
<link rel="preload" href="/docs/assets/css/styles.1fc3f87c.css" as="style"><link rel="preload" href="/docs/assets/js/app.1fc3f87c.js" as="script"><link rel="preload" href="/docs/assets/js/1.05f88c5b.js" as="script"><link rel="prefetch" href="/docs/assets/js/2.69b20946.js"><link rel="prefetch" href="/docs/assets/js/3.197b5253.js"><link rel="prefetch" href="/docs/assets/js/4.2a48a234.js"><link rel="prefetch" href="/docs/assets/js/5.bd73b45e.js">
<link rel="stylesheet" href="/docs/assets/css/styles.1fc3f87c.css">
</head>
<body>
<div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div><a href="/docs/" class="home-link router-link-exact-active router-link-active"><!----><span class="site-name">
Monibuca
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="active sidebar-link">起步</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/#介绍" class="sidebar-link">介绍</a></li><li class="sidebar-sub-header"><a href="/docs/#启动" class="sidebar-link">启动</a></li><li class="sidebar-sub-header"><a href="/docs/#配置" class="sidebar-link">配置</a></li></ul></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li></ul></div><div class="page"><div class="content"><h1 id="monibuca快速起步"><a href="#monibuca快速起步" aria-hidden="true" class="header-anchor">#</a> Monibuca快速起步</h1><h2 id="介绍"><a href="#介绍" aria-hidden="true" class="header-anchor">#</a> 介绍</h2><p>Monibuca 是一个开源的流媒体服务器开发框架适用于快速定制化开发流媒体服务器可以对接CDN厂商作为回源服务器也可以自己搭建集群部署环境。
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="active sidebar-link">起步</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/#介绍" class="sidebar-link">介绍</a></li><li class="sidebar-sub-header"><a href="/docs/#使用实例管理器启动实例" class="sidebar-link">使用实例管理器启动实例</a></li><li class="sidebar-sub-header"><a href="/docs/#实例目录说明" class="sidebar-link">实例目录说明</a></li></ul></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="sidebar-link">内置插件</a></li><li><a href="/docs/design.html" class="sidebar-link">设计原理</a></li></ul></div><div class="page"><div class="content"><h1 id="monibuca快速起步"><a href="#monibuca快速起步" aria-hidden="true" class="header-anchor">#</a> Monibuca快速起步</h1><h2 id="介绍"><a href="#介绍" aria-hidden="true" class="header-anchor">#</a> 介绍</h2><p>Monibuca 是一个开源的流媒体服务器开发框架适用于快速定制化开发流媒体服务器可以对接CDN厂商作为回源服务器也可以自己搭建集群部署环境。
丰富的内置插件提供了流媒体服务器的常见功能例如rtmp server、http-flv、视频录制、QoS等。除此以外还内置了后台web界面方便观察服务器运行的状态。
也可以自己开发后台管理界面通过api方式获取服务器的运行信息。
Monibuca 提供了可供定制化开发的插件机制,可以任意扩展其功能。</p><h2 id="启动"><a href="#启动" aria-hidden="true" class="header-anchor">#</a> 启动</h2><p>启用所有内置插件</p><div class="language-go extra-class"><pre class="language-go"><code><span class="token keyword">package</span> main
Monibuca 提供了可供定制化开发的插件机制,可以任意扩展其功能。</p><h2 id="使用实例管理器启动实例"><a href="#使用实例管理器启动实例" aria-hidden="true" class="header-anchor">#</a> 使用实例管理器启动实例</h2><h3 id="step0-配置golang环境"><a href="#step0-配置golang环境" aria-hidden="true" class="header-anchor">#</a> step0 配置golang环境</h3><p>将GOPATH的bin目录加入环境变量PATH中这样可以快速启动Monibuca实例管理器</p><h3 id="step1-安装monibuca"><a href="#step1-安装monibuca" aria-hidden="true" class="header-anchor">#</a> step1 安装Monibuca</h3><div class="language-bash extra-class"><pre class="language-bash"><code>go get github.com/langhuihui/monibuca
</code></pre></div><p>安装完成后会在GOPATH的bin目录下生成monibuca可执行文件</p><h3 id="step2-启动monibuca实例管理器"><a href="#step2-启动monibuca实例管理器" aria-hidden="true" class="header-anchor">#</a> step2 启动monibuca实例管理器</h3><p>如果GOPATH的bin目录已经加入PATH环境变量则可以直接执行</p><div class="language-bash extra-class"><pre class="language-bash"><code>monibuca
</code></pre></div><p>程序默认监听8000端口你也可以带上参数指定启动的端口</p><div class="language-bash extra-class"><pre class="language-bash"><code>monibuca -port <span class="token number">8001</span>
</code></pre></div><h3 id="step3-创建实例"><a href="#step3-创建实例" aria-hidden="true" class="header-anchor">#</a> step3 创建实例</h3><p>浏览器打开上面的端口地址,出现实例管理器页面,点击创建标签页,按照提示选择实例放置的目录和插件,进行创建。
完成后会在所在目录创建若干文件并运行该golang项目如果选择了网关插件则可以在该插件配置的端口下看到控制台页面。</p><h2 id="实例目录说明"><a href="#实例目录说明" aria-hidden="true" class="header-anchor">#</a> 实例目录说明</h2><ol><li>main.go</li><li>config.toml</li><li>restart.sh</li></ol><h3 id="main-go"><a href="#main-go" aria-hidden="true" class="header-anchor">#</a> main.go</h3><p>实例启动的主文件,初始化各类插件,然后调用配置文件启动引擎</p><div class="language-go extra-class"><pre class="language-go"><code><span class="token keyword">package</span> main
<span class="token keyword">import</span> <span class="token punctuation">(</span>
<span class="token punctuation">.</span> <span class="token string">&quot;github.com/langhuihui/monibuca/monica&quot;</span>
@@ -27,7 +31,7 @@ Monibuca 提供了可供定制化开发的插件机制,可以任意扩展其
<span class="token function">Run</span><span class="token punctuation">(</span><span class="token string">&quot;config.toml&quot;</span><span class="token punctuation">)</span>
<span class="token keyword">select</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><h2 id="配置"><a href="#配置" aria-hidden="true" class="header-anchor">#</a> 配置</h2><p>要使用<code>Monibuca</code>,需要编写一个<code>toml</code>格式的配置文件,通常可以放在程序的同级目录下例如:<code>config.toml</code>(名称不是必须为<code>config</code>)</p><p>该配置文件主要是为了定制各个插件的配置,例如监听端口号等,具体还是要看各个插件的设计。</p><blockquote><p>如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息</p></blockquote><p>如果注释掉部分插件的配置,那么该插件就不会启用,典型的配置如下:</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.HDL</span><span class="token punctuation">]</span>
</code></pre></div><p>可以修改该主文件,添加任意功能</p><h3 id="config-toml"><a href="#config-toml" aria-hidden="true" class="header-anchor">#</a> config.toml</h3><p>该配置文件主要是为了定制各个插件的配置,例如监听端口号等,具体还是要看各个插件的设计。</p><div class="tip custom-block"><p class="custom-block-title">TIP</p><p>如果你编写了自己的插件,就必须在该配置文件中写入对自己插件的配置信息</p></div><p>如果注释掉部分插件的配置,那么该插件就不会启用,典型的配置如下:</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.HDL</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:2020&quot;</span>
<span class="token punctuation">[</span><span class="token table class-name">Plugins.Jessica</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:8080&quot;</span>
@@ -45,10 +49,10 @@ Monibuca 提供了可供定制化开发的插件机制,可以任意扩展其
<span class="token comment">#Path=&quot;./resouce&quot;</span>
<span class="token punctuation">[</span><span class="token table class-name">Plugins.QoS</span><span class="token punctuation">]</span>
<span class="token key property">Suffix</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">&quot;high&quot;</span><span class="token punctuation">,</span><span class="token string">&quot;medium&quot;</span><span class="token punctuation">,</span><span class="token string">&quot;low&quot;</span><span class="token punctuation">]</span>
</code></pre></div><p>具体配置的含义,可以参考每个插件的说明</p></div><div class="page-edit"><!----><!----></div><div class="page-nav"><p class="inner"><!----><span class="next"><a href="/docs/develop.html">
</code></pre></div><p>具体配置的含义,可以参考每个插件的说明</p><h3 id="restart-sh"><a href="#restart-sh" aria-hidden="true" class="header-anchor">#</a> restart.sh</h3><p>该文件是一个用来重启实例的bash脚本方便通过实例管理器重启或者手工重启。</p></div><div class="page-edit"><!----><!----></div><div class="page-nav"><p class="inner"><!----><span class="next"><a href="/docs/develop.html">
插件开发
</a>
</span></p></div></div></div></div>
<script src="/docs/assets/js/app.d51c815b.js" defer></script><script src="/docs/assets/js/1.6babbc1d.js" defer></script>
<script src="/docs/assets/js/app.1fc3f87c.js" defer></script><script src="/docs/assets/js/1.05f88c5b.js" defer></script>
</body>
</html>

View File

@@ -7,38 +7,47 @@
<meta name="description" content="">
<link rel="preload" href="/docs/assets/css/styles.d51c815b.css" as="style"><link rel="preload" href="/docs/assets/js/app.d51c815b.js" as="script"><link rel="preload" href="/docs/assets/js/4.08fbc0d9.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.6babbc1d.js"><link rel="prefetch" href="/docs/assets/js/2.190ec46a.js"><link rel="prefetch" href="/docs/assets/js/3.7646e76c.js">
<link rel="stylesheet" href="/docs/assets/css/styles.d51c815b.css">
<link rel="preload" href="/docs/assets/css/styles.1fc3f87c.css" as="style"><link rel="preload" href="/docs/assets/js/app.1fc3f87c.js" as="script"><link rel="preload" href="/docs/assets/js/5.bd73b45e.js" as="script"><link rel="prefetch" href="/docs/assets/js/1.05f88c5b.js"><link rel="prefetch" href="/docs/assets/js/2.69b20946.js"><link rel="prefetch" href="/docs/assets/js/3.197b5253.js"><link rel="prefetch" href="/docs/assets/js/4.2a48a234.js">
<link rel="stylesheet" href="/docs/assets/css/styles.1fc3f87c.css">
</head>
<body>
<div id="app" data-server-rendered="true"><div class="theme-container"><header class="navbar"><div class="sidebar-button"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 448 512" class="icon"><path fill="currentColor" d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"></path></svg></div><a href="/docs/" class="home-link router-link-active"><!----><span class="site-name">
Monibuca
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="active sidebar-link">内置插件</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/plugins.html#jessica插件" class="sidebar-link">Jessica插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#rtmp插件" class="sidebar-link">Rtmp插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#recordflv插件" class="sidebar-link">RecordFlv插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#http-flv插件" class="sidebar-link">Http-Flv插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#cluster插件" class="sidebar-link">Cluster插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#hls插件" class="sidebar-link">HLS插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#网关插件" class="sidebar-link">网关插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#校验插件" class="sidebar-link">校验插件</a></li></ul></li></ul></div><div class="page"><div class="content"><h1 id="内置插件介绍"><a href="#内置插件介绍" aria-hidden="true" class="header-anchor">#</a> 内置插件介绍</h1><p>内置插件为Monibuca提供了许多基础功能当然你完全可以不采用内置插件而改用自己开发的插件也丝毫不会影响您使用Monibuca。</p><h2 id="jessica插件"><a href="#jessica插件" aria-hidden="true" class="header-anchor">#</a> Jessica插件</h2><blockquote><p>该插件源码位于plugins/jessica</p></blockquote><p>该插件为基于WebSocket协议传输音视频的订阅者音视频数据以裸数据的形式进行传输我们需要Jessibuca播放器来进行播放
</span></a><div class="links"><div class="search-box"><input aria-label="Search" autocomplete="off" spellcheck="false" value=""><!----></div><!----></div></header><div class="sidebar-mask"></div><div class="sidebar"><!----><ul class="sidebar-links"><li><a href="/docs/" class="sidebar-link">起步</a></li><li><a href="/docs/develop.html" class="sidebar-link">插件开发</a></li><li><a href="/docs/history.html" class="sidebar-link">更新日志</a></li><li><a href="/docs/plugins.html" class="active sidebar-link">内置插件</a><ul class="sidebar-sub-headers"><li class="sidebar-sub-header"><a href="/docs/plugins.html#网关插件" class="sidebar-link">网关插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#日志分割插件" class="sidebar-link">日志分割插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#jessica插件" class="sidebar-link">Jessica插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#rtmp插件" class="sidebar-link">Rtmp插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#recordflv插件" class="sidebar-link">RecordFlv插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#http-flv插件" class="sidebar-link">Http-Flv插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#cluster插件" class="sidebar-link">Cluster插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#hls插件" class="sidebar-link">HLS插件</a></li><li class="sidebar-sub-header"><a href="/docs/plugins.html#校验插件" class="sidebar-link">校验插件</a></li></ul></li><li><a href="/docs/design.html" class="sidebar-link">设计原理</a></li></ul></div><div class="page"><div class="content"><h1 id="内置插件介绍"><a href="#内置插件介绍" aria-hidden="true" class="header-anchor">#</a> 内置插件介绍</h1><p>内置插件为Monibuca提供了许多基础功能当然你完全可以不采用内置插件而改用自己开发的插件也丝毫不会影响您使用Monibuca。</p><h2 id="网关插件"><a href="#网关插件" aria-hidden="true" class="header-anchor">#</a> 网关插件</h2><div class="tip custom-block"><p class="custom-block-title">源码位置</p><p>该插件位于plugins/gateway</p></div><p>该插件是为web控制台界面提供api用来采集服务器的信息。</p><h3 id="配置"><a href="#配置" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.GateWay</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:80&quot;</span>
</code></pre></div><p>如果80端口有其他用途可以换成别的端口比如有nginx反向代理。</p><h2 id="日志分割插件"><a href="#日志分割插件" aria-hidden="true" class="header-anchor">#</a> 日志分割插件</h2><div class="tip custom-block"><p class="custom-block-title">源码位置</p><p>该插件源码位于plugins/logrotate下</p></div><h3 id="配置-2"><a href="#配置-2" aria-hidden="true" class="header-anchor">#</a> 配置</h3><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.LogRotate</span><span class="token punctuation">]</span>
<span class="token key property">Path</span> <span class="token punctuation">=</span> <span class="token string">&quot;log&quot;</span>
<span class="token key property">Size</span> <span class="token punctuation">=</span> <span class="token number">0</span>
<span class="token key property">Days</span> <span class="token punctuation">=</span> <span class="token number">1</span>
</code></pre></div><p>其中Path代表生成日志的目录
Size代表按大小分割单位是字节如果为0则按时间分割
Days代表按时间分割单位是天即24小时</p><h2 id="jessica插件"><a href="#jessica插件" aria-hidden="true" class="header-anchor">#</a> Jessica插件</h2><div class="tip custom-block"><p class="custom-block-title">源码位置</p><p>该插件源码位于plugins/jessica下</p></div><p>该插件为基于WebSocket协议传输音视频的订阅者音视频数据以裸数据的形式进行传输我们需要Jessibuca播放器来进行播放
Jessibua播放器已内置于源码中该播放器通过js解码H264/H265并用canvas进行渲染可以运行在几乎所有的终端浏览器上面。
在Monibuca的Web界面中预览功能就是使用的Jessibuca播放器。</p><h3 id="配置"><a href="#配置" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.Jessica</span><span class="token punctuation">]</span>
在Monibuca的Web界面中预览功能就是使用的Jessibuca播放器。</p><h3 id="配置-3"><a href="#配置-3" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.Jessica</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:8080&quot;</span>
</code></pre></div><h3 id="flv格式支持"><a href="#flv格式支持" aria-hidden="true" class="header-anchor">#</a> Flv格式支持</h3><p>Jessica以及Jessibuca也支持采用WebSocket中传输Flv格式的方式进行通讯目前有部分CDN厂商已经支持这种方式进行传输。</p><blockquote><p>私有协议以及Flv格式的判断是通过URL后缀是否带有.flv来进行判断</p></blockquote><h2 id="rtmp插件"><a href="#rtmp插件" aria-hidden="true" class="header-anchor">#</a> Rtmp插件</h2><blockquote><p>该插件源码位于plugins/rtmp下</p></blockquote><p>实现了基本的rtmp传输协议包括接收来自OBS、ffmpeg等软件的推流以及来在Flash Player播放器的拉流。</p><h3 id="配置-2"><a href="#配置-2" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.RTMP</span><span class="token punctuation">]</span>
</code></pre></div><h3 id="flv格式支持"><a href="#flv格式支持" aria-hidden="true" class="header-anchor">#</a> Flv格式支持</h3><p>Jessica以及Jessibuca也支持采用WebSocket中传输Flv格式的方式进行通讯目前有部分CDN厂商已经支持这种方式进行传输。</p><blockquote><p>私有协议以及Flv格式的判断是通过URL后缀是否带有.flv来进行判断</p></blockquote><h2 id="rtmp插件"><a href="#rtmp插件" aria-hidden="true" class="header-anchor">#</a> Rtmp插件</h2><blockquote><p>该插件源码位于plugins/rtmp下</p></blockquote><p>实现了基本的rtmp传输协议包括接收来自OBS、ffmpeg等软件的推流以及来在Flash Player播放器的拉流。</p><h3 id="配置-4"><a href="#配置-4" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.RTMP</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:1935&quot;</span>
</code></pre></div><h2 id="recordflv插件"><a href="#recordflv插件" aria-hidden="true" class="header-anchor">#</a> RecordFlv插件</h2><blockquote><p>该插件源码位于plugins/record下</p></blockquote><p>实现了录制Flv文件的功能并且支持再次使用录制好的Flv文件作为发布者进行发布。在Monibuca的web界面的控制台中提供了对房间进行录制的操作按钮以及列出所有已经录制的文件的界面。</p><h3 id="配置-3"><a href="#配置-3" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>配置中的Path 表示要保存的Flv文件的根路径可以使用相对路径或者绝对路径</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.RecordFlv</span><span class="token punctuation">]</span>
</code></pre></div><h2 id="recordflv插件"><a href="#recordflv插件" aria-hidden="true" class="header-anchor">#</a> RecordFlv插件</h2><blockquote><p>该插件源码位于plugins/record下</p></blockquote><p>实现了录制Flv文件的功能并且支持再次使用录制好的Flv文件作为发布者进行发布。在Monibuca的web界面的控制台中提供了对房间进行录制的操作按钮以及列出所有已经录制的文件的界面。</p><h3 id="配置-5"><a href="#配置-5" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>配置中的Path 表示要保存的Flv文件的根路径可以使用相对路径或者绝对路径</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.RecordFlv</span><span class="token punctuation">]</span>
<span class="token key property">Path</span><span class="token punctuation">=</span><span class="token string">&quot;./resource&quot;</span>
</code></pre></div><h2 id="http-flv插件"><a href="#http-flv插件" aria-hidden="true" class="header-anchor">#</a> Http-Flv插件</h2><blockquote><p>该插件位于plugins/HDL下</p></blockquote><p>实现了http-flv格式的拉流功能方便对接CDN厂商</p><h3 id="配置-4"><a href="#配置-4" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.HDL</span><span class="token punctuation">]</span>
</code></pre></div><h2 id="http-flv插件"><a href="#http-flv插件" aria-hidden="true" class="header-anchor">#</a> Http-Flv插件</h2><blockquote><p>该插件位于plugins/HDL下</p></blockquote><p>实现了http-flv格式的拉流功能方便对接CDN厂商</p><h3 id="配置-6"><a href="#配置-6" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.HDL</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:2020&quot;</span>
</code></pre></div><h2 id="cluster插件"><a href="#cluster插件" aria-hidden="true" class="header-anchor">#</a> Cluster插件</h2><blockquote><p>该插件源码位于plugins/cluster下</p></blockquote><p>实现了基本的集群功能,里面包含一对发布者和订阅者,分别在主从服务器中启用,进行连接。
起基本原理就是在主服务器启动端口监听从服务器收到播放请求时如果从服务器没有对应的发布者则向主服务器发起请求主服务器收到来自从服务器的请求时将该请求作为一个订阅者。从服务器则把tcp连接作为发布者实现视频流的传递过程。</p><h3 id="配置-5"><a href="#配置-5" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>主服务器的配置是ListenAddr用来监听从服务器的请求。
起基本原理就是在主服务器启动端口监听从服务器收到播放请求时如果从服务器没有对应的发布者则向主服务器发起请求主服务器收到来自从服务器的请求时将该请求作为一个订阅者。从服务器则把tcp连接作为发布者实现视频流的传递过程。</p><h3 id="配置-7"><a href="#配置-7" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>主服务器的配置是ListenAddr用来监听从服务器的请求。
从服务器的配置是Master,表示主服务器的地址。
当然服务器可以既是主也是从,即充当中转站。</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.Cluster</span><span class="token punctuation">]</span>
<span class="token key property">Master</span> <span class="token punctuation">=</span> <span class="token string">&quot;localhost:2019&quot;</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:2019&quot;</span>
</code></pre></div><h2 id="hls插件"><a href="#hls插件" aria-hidden="true" class="header-anchor">#</a> HLS插件</h2><blockquote><p>该插件源码位于plugins/HLS下</p></blockquote><p>该插件的作用是请求M3u8文件进行解码最终将TS视频流转码成裸的视频流进行发布。
注意该插件目前并没有实现生成HLS的功能。</p><h2 id="网关插件"><a href="#网关插件" aria-hidden="true" class="header-anchor">#</a> 网关插件</h2><blockquote><p>该插件位于plugins/gateway</p></blockquote><p>该插件是为web控制台界面提供api用来采集服务器的信息。</p><h3 id="配置-6"><a href="#配置-6" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>目前仅有的配置是监听的端口号</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.GateWay</span><span class="token punctuation">]</span>
<span class="token key property">ListenAddr</span> <span class="token punctuation">=</span> <span class="token string">&quot;:80&quot;</span>
</code></pre></div><p>如果80端口有其他用途可以换成别的端口比如有nginx反向代理。</p><h2 id="校验插件"><a href="#校验插件" aria-hidden="true" class="header-anchor">#</a> 校验插件</h2><blockquote><p>该插件位于plugins/auth下</p></blockquote><p>该插件提供了基本的验证功能,其原理是
订阅流提供一个签名签名只可以使用一次把签名进行AES CBC 解密如果得到的解密字符串的前面部分就是和Key相同则通过验证。</p><h3 id="配置-7"><a href="#配置-7" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>Key代表用来加密的Key</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.Auth</span><span class="token punctuation">]</span>
注意该插件目前并没有实现生成HLS的功能。</p><h2 id="校验插件"><a href="#校验插件" aria-hidden="true" class="header-anchor">#</a> 校验插件</h2><blockquote><p>该插件位于plugins/auth</p></blockquote><p>该插件提供了基本的验证功能,其原理是
订阅流提供一个签名签名只可以使用一次把签名进行AES CBC 解密如果得到的解密字符串的前面部分就是和Key相同则通过验证。</p><h3 id="配置-8"><a href="#配置-8" aria-hidden="true" class="header-anchor">#</a> 配置</h3><p>Key代表用来加密的Key</p><div class="language-toml extra-class"><pre class="language-toml"><code><span class="token punctuation">[</span><span class="token table class-name">Plugins.Auth</span><span class="token punctuation">]</span>
<span class="token key property">Key</span><span class="token punctuation">=</span><span class="token string">&quot;www.monibuca.com&quot;</span>
</code></pre></div></div><div class="page-edit"><!----><!----></div><div class="page-nav"><p class="inner"><span class="prev">
<a href="/docs/history.html" class="prev">
更新日志
</a></span><!----></p></div></div></div></div>
<script src="/docs/assets/js/app.d51c815b.js" defer></script><script src="/docs/assets/js/4.08fbc0d9.js" defer></script>
</a></span><span class="next"><a href="/docs/design.html">
设计原理
</a>
</span></p></div></div></div></div>
<script src="/docs/assets/js/app.1fc3f87c.js" defer></script><script src="/docs/assets/js/5.bd73b45e.js" defer></script>
</body>
</html>

View File

@@ -21,10 +21,10 @@ importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox
self.__precacheManifest = [
{
"url": "404.html",
"revision": "75925fb89348802ecd737cdad2d5d801"
"revision": "deb4e5a58824b45354e458242cfc5f68"
},
{
"url": "assets/css/styles.d51c815b.css",
"url": "assets/css/styles.1fc3f87c.css",
"revision": "4a6b650244e5b709f84a81ad0565b485"
},
{
@@ -32,40 +32,48 @@ self.__precacheManifest = [
"revision": "83621669651b9a3d4bf64d1a670ad856"
},
{
"url": "assets/js/1.6babbc1d.js",
"revision": "0142763a4e8630af56b66f5fa8320c5c"
"url": "assets/js/1.05f88c5b.js",
"revision": "afa91e5980d9ef9c164df65dfcc212f3"
},
{
"url": "assets/js/2.190ec46a.js",
"revision": "8dceb01ded85f36cc30e0e18371fe5d4"
"url": "assets/js/2.69b20946.js",
"revision": "e49c74ff572f38586aa8d2d0295f6d67"
},
{
"url": "assets/js/3.7646e76c.js",
"revision": "378c41587710afd31466293c83a6c738"
"url": "assets/js/3.197b5253.js",
"revision": "68528a936ba6abcb203ebf1e3b779f7b"
},
{
"url": "assets/js/4.08fbc0d9.js",
"revision": "0ea387538f5b25ef46237e3dcc1c1694"
"url": "assets/js/4.2a48a234.js",
"revision": "84b7fc95074e5673dc9cbac5e43ac54b"
},
{
"url": "assets/js/app.d51c815b.js",
"revision": "467373b055c3575c2082a1b9ab1769fd"
"url": "assets/js/5.bd73b45e.js",
"revision": "9e29fc1b0c76fdeee1a9b6073856ca62"
},
{
"url": "assets/js/app.1fc3f87c.js",
"revision": "3433613b6d3d61b3b5b1472588698b88"
},
{
"url": "design.html",
"revision": "d3141cee964d685ecdd1115018ebb5a5"
},
{
"url": "develop.html",
"revision": "92b13eb27581e4dc7008cf4205e5c215"
"revision": "41466536941a8fefa23255ec78f400e2"
},
{
"url": "history.html",
"revision": "ef202ac3bf63e1bdef479a05d49d5a8d"
"revision": "a30a702553aeb8c3654a7b51a26591de"
},
{
"url": "index.html",
"revision": "306f1a0a8ceadb0c85dae2342f0bd637"
"revision": "e1a828d24054341e2439ac0a59d1f673"
},
{
"url": "plugins.html",
"revision": "ac2ce38679fb75f05e8e501f35d161bd"
"revision": "20cdd78205e1bd15b6f5f0ad1b203ab0"
}
].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();

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.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>
<!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.16c0d7c9.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.16c0d7c9.js></script></body></html>

View File

@@ -1,4 +1,6 @@
window.AudioContext = window.AudioContext || window.webkitAudioContext;
function Jessibuca(opt) {
this.audioContext = new window.AudioContext()
this.canvasElement = opt.canvas;
this.contextOptions = opt.contextOptions;
this.videoBuffer = opt.videoBuffer || 1
@@ -65,9 +67,7 @@ function Jessibuca(opt) {
}
}
};
window.AudioContext = window.AudioContext || window.webkitAudioContext;
function _unlock() {
var context = Jessibuca.prototype.audioContext = Jessibuca.prototype.audioContext || new window.AudioContext();
function _unlock(context) {
context.resume();
var source = context.createBufferSource();
source.buffer = context.createBuffer(1, 1, 22050);
@@ -81,7 +81,7 @@ function _unlock() {
// document.addEventListener("touchend", _unlock, true);
Jessibuca.prototype.audioEnabled = function (flag) {
if (flag) {
_unlock()
_unlock(this.audioContext)
this.audioEnabled = function (flag) {
if (flag) {
this.audioContext.resume();
@@ -89,6 +89,8 @@ Jessibuca.prototype.audioEnabled = function (flag) {
this.audioContext.suspend();
}
}
}else{
this.audioContext.suspend();
}
}
Jessibuca.prototype.playAudio = function (data) {
@@ -150,6 +152,7 @@ Jessibuca.prototype.initAudioPlay = function (frameCount, samplerate, channels)
if (!context) return false;
var resampled = samplerate < 22050;
var audioBuffer = resampled ? context.createBuffer(channels, frameCount << 1, samplerate << 1) : context.createBuffer(channels, frameCount, samplerate);
var _this = this
var playNextBuffer = function () {
isPlaying = false;
console.log("~", audioBuffers.length)
@@ -189,9 +192,12 @@ Jessibuca.prototype.initAudioPlay = function (frameCount, samplerate, channels)
var source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.onended = playNextBuffer;
//setTimeout(playNextBuffer, audioBufferTime-audioBuffers.length*200);
// source.onended = playNextBuffer;
// setTimeout(playNextBuffer, audioBufferTime-audioBuffers.length*200);
source.start();
if (!_this.audioInterval) {
_this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000 - 1);
}
};
this.playAudio = playAudio;
}
@@ -452,6 +458,7 @@ Jessibuca.prototype.close = function () {
if (this.audioInterval) {
clearInterval(this.audioInterval)
}
delete this.playAudio
this.decoderWorker.postMessage({ cmd: "close" })
this.contextGL.clear(this.contextGL.COLOR_BUFFER_BIT);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dashboard/dist/js/app.16c0d7c9.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dashboard/dist/js/app.16c0d7c9.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,8 @@ module.exports = {
['/', '起步'],
['/develop', '插件开发'],
['/history', '更新日志'],
['/plugins', '内置插件']
['/plugins', '内置插件'],
['/design', '设计原理']
]
},
title: 'Monibuca',

View File

@@ -10,7 +10,7 @@ import upperFirst from 'lodash/upperFirst'
const requireComponent = require.context(
// The relative path of the components folder
'../../src/components',
'../',
// Whether or not to look in subfolders
true,
// The regular expression used to match base component filenames

View File

@@ -5,8 +5,39 @@ Monibuca 是一个开源的流媒体服务器开发框架,适用于快速定
也可以自己开发后台管理界面通过api方式获取服务器的运行信息。
Monibuca 提供了可供定制化开发的插件机制,可以任意扩展其功能。
## 启动
启用所有内置插件
## 使用实例管理器启动实例
### step0 配置golang环境
将GOPATH的bin目录加入环境变量PATH中这样可以快速启动Monibuca实例管理器
### step1 安装Monibuca
```bash
go get github.com/langhuihui/monibuca
```
安装完成后会在GOPATH的bin目录下生成monibuca可执行文件
### step2 启动monibuca实例管理器
如果GOPATH的bin目录已经加入PATH环境变量则可以直接执行
```bash
monibuca
```
程序默认监听8000端口你也可以带上参数指定启动的端口
```bash
monibuca -port 8001
```
### step3 创建实例
浏览器打开上面的端口地址,出现实例管理器页面,点击创建标签页,按照提示选择实例放置的目录和插件,进行创建。
完成后会在所在目录创建若干文件并运行该golang项目如果选择了网关插件则可以在该插件配置的端口下看到控制台页面。
## 实例目录说明
1. main.go
2. config.toml
3. restart.sh
### main.go
实例启动的主文件,初始化各类插件,然后调用配置文件启动引擎
```go
package main
@@ -20,10 +51,9 @@ func main() {
select {}
}
```
可以修改该主文件,添加任意功能
## 配置
要使用`Monibuca`,需要编写一个`toml`格式的配置文件,通常可以放在程序的同级目录下例如:`config.toml`(名称不是必须为`config`)
### config.toml
该配置文件主要是为了定制各个插件的配置,例如监听端口号等,具体还是要看各个插件的设计。
@@ -52,4 +82,7 @@ ListenAddr = ":81"
[Plugins.QoS]
Suffix = ["high","medium","low"]
```
具体配置的含义,可以参考每个插件的说明
具体配置的含义,可以参考每个插件的说明
### restart.sh
该文件是一个用来重启实例的bash脚本方便通过实例管理器重启或者手工重启。

View File

@@ -1,4 +1,4 @@
# Monibuca设计思想
# Monibuca设计原理
## 背景
@@ -16,7 +16,47 @@
### 受到vue渐进式思想的影响
vue渐进式框架的设计思想非常棒那么是否可以用来设计流媒体服务器使得流媒体服务器不只是一个服务器而是一个开发框架让开发者可以定制化自己的流媒体服务器呢答案是肯定的。当然我们需要更多的抽象。
## 流媒体服务器的核心
## 如何实现可扩展——插件化
许多IDE和编辑器都依靠插件化技术得以拓展其功能并形成其生态例如vs、vs code、eclipse、jetbrains系列当然vue作为一个前端框架也是设计了很不错的插件机制。这些都可以作为借鉴。
要实现流媒体服务器的插件化,就需要把核心功能和拓展功能分离,进行足够的抽象。
### 三大抽象概念
1. 发布者Publisher
2. 订阅者Subscriber
3. 房间Room
#### 发布者Publisher
发布者本质上就是输入流,其抽象行为就是将音频和视频数据压入**房间**中,换句话说,就是在恰当的时候调用**房间**的PushVideo和PushAudio函数
::: tip 源码位置
发布者定义位于monica/publisher.go中
:::
在发布者的定义中有一个**InputStream**的结构体,用来和**房间**进行互操作。
所有具体的发布者都应该包含这个**InputStream**,以组合继承的方式成为发布者。
该**InputStream**包含最核心功能就是Publish函数这个函数的功能就是在**房间**里面设置发布者是自己,这个行为就是发布。形象的理解就是主播走进了房间。
引擎不关心是谁走进了房间,也不关心进来的人会发布什么内容。
::: tip 发布者插件
所有实现了发布者具体功能的插件就是发布者插件这样一来流媒体的媒体源可以是任意的形式比如RTMP协议提供的推流可以由FFMPEG、OBS发布。也可以是读取本地磁盘上的媒体文件也可以来自源服务器的私有协议传输的内容。
:::
#### 订阅者Subscriber
订阅者就是输出流,其抽象行为就是被动接收来自**房间**的音频和视频数据。
::: tip 源码位置
订阅者定义位于monica/subscriber.go中
:::
订阅者有两个函数sendVideo和sendAudio用于接收音频和视频数据。这个两个函数会对音视频做一些预处理主要是实现丢包机制、时间戳和首屏渲染。具体的视频数据会共享读取。
然后调用SendHandler将打包好的音视频数据发送到具体的订阅者那里。
::: tip 订阅者插件
订阅者插件本质上就是SendHandler函数。具体可以将打包的数据以何种协议输出还是写入文件由插件实现。
:::
#### 房间Room
房间就是一个连接发布者和订阅者的地方。可以形象的理解为主播的房间,发布者是主播,订阅者就是粉丝观众。房间是引擎的核心,其重要逻辑包括:
1. 房间的创建、查询、关闭
2. 订阅者的加入和移除
3. 发布者的进入和离开。
::: tip 源码位置
订阅者定义位于monica/room.go中
:::
流媒体服务器的核心是**转发**二字。当你去研究一款流媒体服务器的时候,会有海量的代码阻碍你看清其核心逻辑。包括:
@@ -31,13 +71,21 @@ for _, v := range r.Subscribers {
}
```
其他都是围绕这个for循环展开。所有的流媒体服务器代码里面都有这个for循环写法稍有不同但本质相同。
::: tip 源码位置
该核心逻辑位于monica/room.go中的Run函数内
:::
### 核心概念
## 如何实现高性能
流媒体服务器对性能要求极为苛刻。因为流媒体服务器属于高速系统会有并发的长连接请求协议封包解包和音视频格式的编解码都消耗着CPU以及内存如何尽可能的减少消耗是必须考虑的问题。
### 内存使用
池化是一个不错的选择所以尽量池化在Monibuca中对`[]byte`类型,采用了[github.com/funny/slab](https://github.com/funny/slab)包来管理。其他结构体就用系统自带的pool包来池化对象。
基于这个循环,我们需要思考两个问题:
1. 如何高效的循环(性能问题)
2. 如何对接不同的协议(扩展性)
第一个问题golang的性能算是很好的那么重点就在于减少内存的分配上池化是一个不错的选择所以尽量池化在Monibuca中对`[]byte`类型,采用了[github.com/funny/slab](https://github.com/funny/slab)包来管理。其他结构体就用系统自带的pool包来池化对象。对于协程的使用在多次迭代后已经使用了最少的协程来支持并发性。
### 协程的使用
golang自带的goroutine可以有效的减少线程的使用并可以支持各种异步并发的情况。合理的创建goroutine很重要这样才能尽可能高效利用CPU时间。
在monibuca中创建goroutine在如下场景中
1. 通讯协议建立的长连接对于一个goroutine
2. 每个房间拥有一个goroutine用于接收指令和转发音视频数据
3. 每一个插件会使用一个goroutine来执行插件的Run函数
由于引擎本身比较轻量化,更多的性能的优化需要插件提供者自由发挥了。

View File

@@ -1,4 +1,5 @@
# 更新历史
- 2020/2/20
完成实例管理器
- 2020/1/27
初步完成
完成核心架构

View File

@@ -1,7 +1,42 @@
# 内置插件介绍
内置插件为Monibuca提供了许多基础功能当然你完全可以不采用内置插件而改用自己开发的插件也丝毫不会影响您使用Monibuca。
## 网关插件
::: tip 源码位置
该插件位于plugins/gateway下
:::
该插件是为web控制台界面提供api用来采集服务器的信息。
### 配置
目前仅有的配置是监听的端口号
```toml
[Plugins.GateWay]
ListenAddr = ":80"
```
如果80端口有其他用途可以换成别的端口比如有nginx反向代理。
## 日志分割插件
::: tip 源码位置
该插件源码位于plugins/logrotate下
:::
### 配置
```toml
[Plugins.LogRotate]
Path = "log"
Size = 0
Days = 1
```
其中Path代表生成日志的目录
Size代表按大小分割单位是字节如果为0则按时间分割
Days代表按时间分割单位是天即24小时
## Jessica插件
> 该插件源码位于plugins/jessica下
::: tip 源码位置
该插件源码位于plugins/jessica下
:::
该插件为基于WebSocket协议传输音视频的订阅者音视频数据以裸数据的形式进行传输我们需要Jessibuca播放器来进行播放
Jessibua播放器已内置于源码中该播放器通过js解码H264/H265并用canvas进行渲染可以运行在几乎所有的终端浏览器上面。
@@ -76,19 +111,6 @@ ListenAddr = ":2019"
该插件的作用是请求M3u8文件进行解码最终将TS视频流转码成裸的视频流进行发布。
注意该插件目前并没有实现生成HLS的功能。
## 网关插件
> 该插件位于plugins/gateway下
该插件是为web控制台界面提供api用来采集服务器的信息。
### 配置
目前仅有的配置是监听的端口号
```toml
[Plugins.GateWay]
ListenAddr = ":80"
```
如果80端口有其他用途可以换成别的端口比如有nginx反向代理。
## 校验插件
> 该插件位于plugins/auth下

View File

@@ -1,4 +1,6 @@
window.AudioContext = window.AudioContext || window.webkitAudioContext;
function Jessibuca(opt) {
this.audioContext = new window.AudioContext()
this.canvasElement = opt.canvas;
this.contextOptions = opt.contextOptions;
this.videoBuffer = opt.videoBuffer || 1
@@ -65,9 +67,7 @@ function Jessibuca(opt) {
}
}
};
window.AudioContext = window.AudioContext || window.webkitAudioContext;
function _unlock() {
var context = Jessibuca.prototype.audioContext = Jessibuca.prototype.audioContext || new window.AudioContext();
function _unlock(context) {
context.resume();
var source = context.createBufferSource();
source.buffer = context.createBuffer(1, 1, 22050);
@@ -81,7 +81,7 @@ function _unlock() {
// document.addEventListener("touchend", _unlock, true);
Jessibuca.prototype.audioEnabled = function (flag) {
if (flag) {
_unlock()
_unlock(this.audioContext)
this.audioEnabled = function (flag) {
if (flag) {
this.audioContext.resume();
@@ -89,6 +89,8 @@ Jessibuca.prototype.audioEnabled = function (flag) {
this.audioContext.suspend();
}
}
}else{
this.audioContext.suspend();
}
}
Jessibuca.prototype.playAudio = function (data) {
@@ -150,6 +152,7 @@ Jessibuca.prototype.initAudioPlay = function (frameCount, samplerate, channels)
if (!context) return false;
var resampled = samplerate < 22050;
var audioBuffer = resampled ? context.createBuffer(channels, frameCount << 1, samplerate << 1) : context.createBuffer(channels, frameCount, samplerate);
var _this = this
var playNextBuffer = function () {
isPlaying = false;
console.log("~", audioBuffers.length)
@@ -189,9 +192,12 @@ Jessibuca.prototype.initAudioPlay = function (frameCount, samplerate, channels)
var source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.onended = playNextBuffer;
//setTimeout(playNextBuffer, audioBufferTime-audioBuffers.length*200);
// source.onended = playNextBuffer;
// setTimeout(playNextBuffer, audioBufferTime-audioBuffers.length*200);
source.start();
if (!_this.audioInterval) {
_this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000 - 1);
}
};
this.playAudio = playAudio;
}
@@ -452,6 +458,7 @@ Jessibuca.prototype.close = function () {
if (this.audioInterval) {
clearInterval(this.audioInterval)
}
delete this.playAudio
this.decoderWorker.postMessage({ cmd: "close" })
this.contextGL.clear(this.contextGL.COLOR_BUFFER_BIT);
}

View File

@@ -9,8 +9,8 @@
>
<canvas id="canvas" width="488" height="275" style="background: black" />
<div slot="footer">
音频缓冲
<InputNumber v-model="audioBuffer" size="small"></InputNumber>
<!-- 音频缓冲-->
<!-- <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>
@@ -24,7 +24,7 @@ export default {
data() {
return {
audioEnabled: false,
audioBuffer: 12,
// audioBuffer: 12,
url: ""
};
},
@@ -32,9 +32,9 @@ export default {
audioEnabled(value) {
h5lc.audioEnabled(value);
},
audioBuffer(v) {
h5lc.audioBuffer = v;
}
// audioBuffer(v) {
// h5lc.audioBuffer = v;
// }
},
mounted() {
h5lc = new window.Jessibuca({

308
main.go
View File

@@ -1,52 +1,47 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"github.com/BurntSushi/toml"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/util"
"io"
"io/ioutil"
"log"
"mime"
"net/http"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/BurntSushi/toml"
. "github.com/langhuihui/monibuca/monica"
"github.com/langhuihui/monibuca/monica/util"
"github.com/langhuihui/monibuca/pm"
)
type InstanceDesc struct {
Name string
Path string
Plugins []string
Config string
}
var instances map[string]*InstanceDesc
var instances = make(map[string]*pm.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("/instance/listDir", listDir)
http.HandleFunc("/instance/import", importInstance)
http.HandleFunc("/instance/updateConfig", updateConfig)
http.HandleFunc("/instance/list", listInstance)
http.HandleFunc("/instance/create", initInstance)
http.HandleFunc("/instance/restart", restartInstance)
http.HandleFunc("/instance/shutdown", shutdownInstance)
http.HandleFunc("/", website)
fmt.Printf("start listen at %s", *addr)
if err := http.ListenAndServe(":"+*addr, nil); err != nil {
@@ -54,6 +49,98 @@ func main() {
}
}
func listDir(w http.ResponseWriter, r *http.Request) {
if input := r.URL.Query().Get("input"); input != "" {
if dir, err := os.Open(filepath.Dir(input)); err == nil {
var dirs []string
if infos, err := dir.Readdir(0); err == nil {
for _, info := range infos {
if info.IsDir() {
dirs = append(dirs, info.Name())
}
}
if bytes, err := json.Marshal(dirs); err == nil {
w.Write(bytes)
}
}
}
}
}
func importInstance(w http.ResponseWriter, r *http.Request) {
var e error
defer func() {
result := "success"
if e != nil {
result = e.Error()
}
w.Write([]byte(result))
}()
name := r.URL.Query().Get("name")
if importPath := r.URL.Query().Get("path"); importPath != "" {
if strings.HasSuffix(importPath, "/") {
importPath = importPath[:len(importPath)-1]
}
f, err := os.Open(importPath)
if e = err; err != nil {
return
}
children, err := f.Readdir(0)
if e = err; err == nil {
var hasMain, hasConfig, hasMod, hasRestart bool
for _, child := range children {
switch child.Name() {
case "main.go":
hasMain = true
case "config.toml":
hasConfig = true
case "go.mod":
hasMod = true
case "restart.sh", "restart.bat":
hasRestart = true
}
}
if hasMain && hasConfig && hasMod && hasRestart {
if name == "" {
_, name = path.Split(importPath)
}
config, err := ioutil.ReadFile(path.Join(importPath, "config.toml"))
if e = err; err != nil {
return
}
mainGo, err := ioutil.ReadFile(path.Join(importPath, "main.go"))
if e = err; err != nil {
return
}
reg, err := regexp.Compile("_ \"(.+)\"")
if e = err; err != nil {
return
}
instances[name] = &pm.InstanceDesc{
Name: name,
Path: importPath,
Plugins: nil,
Config: string(config),
}
for _, m := range reg.FindAllStringSubmatch(string(mainGo), -1) {
instances[name].Plugins = append(instances[name].Plugins, m[1])
}
var file *os.File
file, e = os.OpenFile(path.Join(instancesDir, name+".toml"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
if err != nil {
return
}
tomlEncoder := toml.NewEncoder(file)
e = tomlEncoder.Encode(instances[name])
} else {
e = errors.New("路径中缺少文件")
}
}
} else {
w.Write([]byte("参数错误"))
}
}
func readInstances() error {
if homeDir, err := Home(); err == nil {
instancesDir = path.Join(homeDir, ".monibuca")
@@ -64,7 +151,7 @@ func readInstances() error {
return err
} else {
for _, configFile := range cs {
des := new(InstanceDesc)
des := new(pm.InstanceDesc)
if _, err = toml.DecodeFile(path.Join(instancesDir, configFile.Name()), des); err == nil {
instances[des.Name] = des
} else {
@@ -107,9 +194,10 @@ func listInstance(w http.ResponseWriter, r *http.Request) {
}
}
func initInstance(w http.ResponseWriter, r *http.Request) {
instanceDesc := new(InstanceDesc)
instanceDesc := new(pm.InstanceDesc)
sse := util.NewSSE(w, r.Context())
err := json.Unmarshal([]byte(r.URL.Query().Get("info")), instanceDesc)
clearDir := r.URL.Query().Get("clear") != ""
defer func() {
if err != nil {
sse.WriteEvent("exception", []byte(err.Error()))
@@ -121,8 +209,7 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
return
}
sse.WriteEvent("step", []byte("1:参数解析成功!"))
err = instanceDesc.createDir(sse)
if err != nil {
if err = instanceDesc.CreateDir(sse, clearDir); err != nil {
return
}
sse.WriteEvent("step", []byte("6:实例创建成功!"))
@@ -138,136 +225,67 @@ func initInstance(w http.ResponseWriter, r *http.Request) {
}
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")
func shutdownInstance(w http.ResponseWriter, r *http.Request) {
instanceName := r.URL.Query().Get("instance")
cmd.Dir = instances[instanceName].Path
err := sse.WriteExec(cmd)
if err != nil {
sse.Write([]byte(err.Error()))
if instance, ok := instances[instanceName]; ok {
if err := instance.ShutDownCmd().Run(); err == nil {
w.Write([]byte("success"))
} else {
w.Write([]byte(err.Error()))
}
} else {
w.Write([]byte("no such instance"))
}
}
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()))
needUpdate := r.URL.Query().Get("update") != ""
needBuild := r.URL.Query().Get("build") != ""
if instance, ok := instances[instanceName]; ok {
if needUpdate {
if err := sse.WriteExec(instance.Command("go", "get", "-u")); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
}
if needBuild {
if err := sse.WriteExec(instance.Command("go", "build")); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
}
if err := sse.WriteExec(instance.RestartCmd()); err != nil {
sse.WriteEvent("failed", []byte(err.Error()))
return
}
sse.Write([]byte("success"))
} else {
sse.WriteEvent("failed", []byte("no such instance"))
}
}
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
func updateConfig(w http.ResponseWriter, r *http.Request) {
instanceName := r.URL.Query().Get("instance")
if instance, ok := instances[instanceName]; ok {
f, err := os.OpenFile(path.Join(instance.Path, "config.toml"), os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
w.Write([]byte(err.Error()))
return
}
_, err = io.Copy(f, r.Body)
if err != nil {
w.Write([]byte(err.Error()))
return
}
w.Write([]byte("success"))
} else {
w.Write([]byte("no such instance"))
}
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 {
if user, err := user.Current(); 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
return pm.HomeDir()
}

Binary file not shown.

View File

@@ -38,7 +38,7 @@ 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)
ADTSLength := 7 + ((1 - int(av.Payload[1]&1)) << 1)
if len(av.Payload) > ADTSLength {
av.Payload[0] = 0xAF
av.Payload[1] = 0x01 //raw AAC

View File

@@ -2,17 +2,32 @@ package monica
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"runtime"
"time"
"github.com/BurntSushi/toml"
)
var ConfigRaw []byte
var Version = "0.1.2"
var Version = "0.3.1"
var EngineInfo = &struct {
Version string
StartTime time.Time
}{Version, time.Now()}
func Run(configFile string) (err error) {
if runtime.GOOS == "windows" {
ioutil.WriteFile("shutdown.bat", []byte(fmt.Sprintf("taskkill /pid %d -t -f", os.Getpid())), 0777)
} else {
ioutil.WriteFile("shutdown.sh", []byte(fmt.Sprintf("kill -9 %d", os.Getpid())), 0777)
}
log.Printf("start monibuca version:%s", Version)
if ConfigRaw, err = ioutil.ReadFile(configFile); err != nil {
log.Printf("read config file error: %v", err)
return
}
go Summary.StartSummary()
@@ -31,6 +46,8 @@ func Run(configFile string) (err error) {
go config.Run()
}
}
} else {
log.Printf("decode config file error: %v", err)
}
return
}

View File

@@ -35,6 +35,7 @@ func (ts *TS) run() {
spsHead := []byte{0xE1, 0, 0}
ppsHead := []byte{0x01, 0, 0}
nalLength := []byte{0, 0, 0, 0}
for {
select {
case <-ts.Done():
@@ -45,9 +46,21 @@ func (ts *TS) run() {
ts.TotalPesCount++
switch tsPesPkt.PesPkt.Header.StreamID & 0xF0 {
case mpegts.STREAM_ID_AUDIO:
av := avformat.NewAVPacket(avformat.FLV_TAG_TYPE_AUDIO)
av.Payload = tsPesPkt.PesPkt.Payload
ts.PushAudio(av)
data := tsPesPkt.PesPkt.Payload
for remainLen := len(data); remainLen > 0; {
// AACFrameLength(13)
// xx xxxxxxxx xxx
frameLen := (int(data[3]&3) << 11) | (int(data[4]) << 3) | (int(data[5]) >> 5)
if frameLen > remainLen {
break
}
av := avformat.NewAVPacket(avformat.FLV_TAG_TYPE_AUDIO)
av.Payload = data[:frameLen]
ts.PushAudio(av)
data = data[frameLen:remainLen]
remainLen = remainLen - frameLen
}
case mpegts.STREAM_ID_VIDEO:
var err error
av := avformat.NewAVPacket(avformat.FLV_TAG_TYPE_VIDEO)

View File

@@ -1,28 +1,27 @@
package QoS
import (
"strings"
. "github.com/langhuihui/monibuca/monica"
)
var (
selectMap = map[string][]string{
"low": {"low", "medium", "high"},
"medium": {"medium", "low", "high"},
"high": {"high", "medium", "low"},
}
)
// var (
// selectMap = map[string][]string{
// "low": {"low", "medium", "high"},
// "medium": {"medium", "low", "high"},
// "high": {"high", "medium", "low"},
// }
// )
func getQualityName(name string, qualityLevel string) string {
if qualityLevel == "" {
return name
}
for _, l := range selectMap[qualityLevel] {
if _, ok := AllRoom.Load(name + "/" + l); ok {
return name + "/" + l
}
}
return name + "/" + qualityLevel
}
// func getQualityName(name string, qualityLevel string) string {
// for _, l := range selectMap[qualityLevel] {
// if _, ok := AllRoom.Load(name + "/" + l); ok {
// return name + "/" + l
// }
// }
// return name + "/" + qualityLevel
// }
var config = struct {
Suffix []string
@@ -39,8 +38,23 @@ func init() {
func run() {
OnDropHooks.AddHook(func(s *OutputStream) {
if s.TotalDrop > s.TotalPacket>>2 {
//TODO
//s.Control<-&ChangeRoomCmd{s,AllRoom.Get()}
var newStreamPath = ""
for i, suf := range config.Suffix {
if strings.HasSuffix(s.StreamPath, suf) {
if i < len(config.Suffix)-1 {
newStreamPath = s.StreamPath + "/" + config.Suffix[i+1]
break
}
} else {
newStreamPath = s.StreamPath + "/" + suf
break
}
}
if newStreamPath != "" {
if _, ok := AllRoom.Load(newStreamPath); ok {
s.Control <- &ChangeRoomCmd{s, AllRoom.Get(newStreamPath)}
}
}
}
})
}

View File

@@ -8,6 +8,7 @@ import (
_ "github.com/langhuihui/monibuca/plugins/cluster"
_ "github.com/langhuihui/monibuca/plugins/gateway"
_ "github.com/langhuihui/monibuca/plugins/jessica"
_ "github.com/langhuihui/monibuca/plugins/logrotate"
_ "github.com/langhuihui/monibuca/plugins/record"
_ "github.com/langhuihui/monibuca/plugins/rtmp"
)

View File

@@ -1,8 +1,7 @@
package gateway
import (
. "github.com/langhuihui/monibuca/monica"
. "github.com/langhuihui/monibuca/monica/util"
"encoding/json"
"io/ioutil"
"log"
"mime"
@@ -10,11 +9,14 @@ import (
"path"
"runtime"
"time"
. "github.com/langhuihui/monibuca/monica"
. "github.com/langhuihui/monibuca/monica/util"
)
var (
config = new(ListenerConfig)
config = new(ListenerConfig)
startTime = time.Now()
dashboardPath string
)
@@ -30,6 +32,7 @@ func init() {
})
}
func run() {
http.HandleFunc("/api/sysInfo", sysInfo)
http.HandleFunc("/api/stop", stopPublish)
http.HandleFunc("/api/summary", summary)
http.HandleFunc("/api/logs", watchLogs)
@@ -95,3 +98,10 @@ func summary(w http.ResponseWriter, r *http.Request) {
}
}
}
func sysInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
bytes, err := json.Marshal(EngineInfo)
if err == nil {
_, err = w.Write(bytes)
}
}

View File

@@ -0,0 +1,78 @@
package logrotate
import (
"fmt"
. "github.com/langhuihui/monibuca/monica"
"log"
"os"
"path"
"time"
)
var config = new(LogRotate)
type LogRotate struct {
Path string
Size int64
Days int
file *os.File
currentSize int64
createTime time.Time
hours float64
splitFunc func() bool
}
func init() {
InstallPlugin(&PluginConfig{
Name: "LogRotate",
Type: PLUGIN_HOOK,
Config: config,
Run: run,
})
}
func run() {
if config.Size > 0 {
config.splitFunc = config.splitBySize
} else {
if config.Days == 0 {
config.Days = 1
}
config.hours = float64(config.Days) * 24
config.splitFunc = config.splitByTime
}
config.createTime = time.Now()
err := os.MkdirAll(config.Path, 0666)
config.file, err = os.OpenFile(path.Join(config.Path, fmt.Sprintf("%s.log", config.createTime.Format("2006-01-02T15:04:05"))), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666)
if err == nil {
stat, _ := config.file.Stat()
config.currentSize = stat.Size()
AddWriter(config)
} else {
log.Println(err)
}
}
func (l *LogRotate) splitBySize() bool {
return l.currentSize >= l.Size
}
func (l *LogRotate) splitByTime() bool {
return time.Since(l.createTime).Hours() > l.hours
}
func (l *LogRotate) Write(data []byte) (n int, err error) {
n, err = l.file.Write(data)
l.currentSize += int64(n)
if err == nil {
if l.splitFunc() {
l.createTime = time.Now()
if file, err := os.OpenFile(path.Join(l.Path, fmt.Sprintf("%s.log", l.createTime.Format("2006-01-02T15:04:05"))), os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666); err == nil {
l.file = file
l.currentSize = 0
}
}
}
return
}
//func (l *LogRotate) FindLog(grep string) string{
// cmd:=exec.Command("grep",fmt.Sprintf("\"%s\"",grep),l.Path)
// err:=cmd.Run()
//}

535
pm/dist/ajax.js vendored Normal file
View File

@@ -0,0 +1,535 @@
// a simple ajax
!(function () {
var jsonType = 'application/json';
var htmlType = 'text/html';
var xmlTypeRE = /^(?:text|application)\/xml/i;
var blankRE = /^\s*$/; // \s
/*
* default setting
* */
var _settings = {
type: "GET",
beforeSend: noop,
success: noop,
error: noop,
complete: noop,
context: null,
xhr: function () {
return new window.XMLHttpRequest();
},
accepts: {
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
crossDomain: false,
timeout: 0,
username: null,
password: null,
processData: true,
promise: noop
};
function noop() {
}
var ajax = function (options) {
//
var settings = extend({}, options || {});
//
for (var key in _settings) {
if (settings[key] === undefined) {
settings[key] = _settings[key];
}
}
//
try {
var q = {};
var promise = new Promise(function (resolve, reject) {
q.resolve = resolve;
q.reject = reject;
});
promise.resolve = q.resolve;
promise.reject = q.reject;
settings.promise = promise;
}
catch (e) {
//
settings.promise = {
resolve: noop,
reject: noop
};
}
//
if (!settings.crossDomain) {
settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && RegExp.$2 !== window.location.href;
}
var dataType = settings.dataType;
// jsonp
if (dataType === 'jsonp') {
//
var hasPlaceholder = /=\?/.test(settings.url);
if (!hasPlaceholder) {
var jsonpCallback = (settings.jsonp || 'callback') + '=?';
settings.url = appendQuery(settings.url, jsonpCallback)
}
return JSONP(settings);
}
// url
if (!settings.url) {
settings.url = window.location.toString();
}
//
serializeData(settings);
var mime = settings.accepts[dataType]; // mime
var baseHeader = {}; // header
var protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol; // protocol
var xhr = _settings.xhr();
var abortTimeout;
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if (!settings.crossDomain && !baseHeader['X-Requested-With']) {
baseHeader['X-Requested-With'] = 'XMLHttpRequest';
}
// mime
if (mime) {
//
baseHeader['Accept'] = mime;
if (mime.indexOf(',') > -1) {
mime = mime.split(',', 2)[0]
}
//
xhr.overrideMimeType && xhr.overrideMimeType(mime);
}
// contentType
if (settings.contentType || (settings.data && settings.type.toUpperCase() !== 'GET')) {
baseHeader['Content-Type'] = (settings.contentType || 'application/x-www-form-urlencoded; charset=UTF-8');
}
// headers
settings.headers = extend(baseHeader, settings.headers || {});
// on ready state change
xhr.onreadystatechange = function () {
// readystate
if (xhr.readyState === 4) {
clearTimeout(abortTimeout);
var result;
var error = false;
//
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
dataType = dataType || mimeToDataType(xhr.getResponseHeader('content-type'));
result = xhr.responseText;
try {
// xml
if (dataType === 'xml') {
result = xhr.responseXML;
}
// json
else if (dataType === 'json') {
result = blankRE.test(result) ? null : JSON.parse(result);
}
}
catch (e) {
error = e;
}
if (error) {
ajaxError(error, 'parseerror', xhr, settings);
}
else {
ajaxSuccess(result, xhr, settings);
}
}
else {
ajaxError(null, 'error', xhr, settings);
}
}
};
// async
var async = 'async' in settings ? settings.async : true;
// open
xhr.open(settings.type, settings.url, async, settings.username, settings.password);
// xhrFields
if (settings.xhrFields) {
for (var name in settings.xhrFields) {
xhr[name] = settings.xhrFields[name];
}
}
// Override mime type if needed
if (settings.mimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(settings.mimeType);
}
// set request header
for (var name in settings.headers) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if (settings.headers[name] !== undefined) {
xhr.setRequestHeader(name, settings.headers[name] + "");
}
}
// before send
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
return false;
}
// timeout
if (settings.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.onreadystatechange = noop;
xhr.abort();
ajaxError(null, 'timeout', xhr, settings);
}, settings.timeout);
}
// send
xhr.send(settings.data ? settings.data : null);
return settings.promise;
};
/*
* method get
* */
ajax.get = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: dataType
});
};
/*
* method post
*
* dataType:
* */
ajax.post = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
type: 'POST',
url: url,
data: data,
success: success,
dataType: dataType
})
};
/*
* method getJSON
* */
ajax.getJSON = function (url, data, success) {
if (isFunction(data)) {
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: 'json'
})
};
/*
* method ajaxSetup
* */
ajax.ajaxSetup = function (target, settings) {
return settings ? extend(extend(target, _settings), settings) : extend(_settings, target);
};
/*
* utils
*
* */
// triggers and extra global event ajaxBeforeSend that's like ajaxSend but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
//
if (settings.beforeSend.call(context, xhr, settings) === false) {
return false;
}
}
// ajax success
function ajaxSuccess(data, xhr, settings) {
var context = settings.context;
var status = 'success';
settings.success.call(context, data, status, xhr);
settings.promise.resolve(data, status, xhr);
ajaxComplete(status, xhr, settings);
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
settings.promise.reject(xhr, type, error);
ajaxComplete(type, xhr, settings);
}
// jsonp
/*
* tks: https://www.cnblogs.com/rubylouvre/archive/2011/02/13/1953087.html
* */
function JSONP(options) {
//
var callbackName = options.jsonpCallback || 'jsonp' + (new Date().getTime());
var script = window.document.createElement('script');
var abort = function () {
// 设置 window.xxx = noop
if (callbackName in window) {
window[callbackName] = noop;
}
};
var xhr = {abort: abort};
var abortTimeout;
var head = window.document.getElementsByTagName('head')[0] || window.document.documentElement;
// ie8+
script.onerror = function (error) {
_error(error);
};
function _error(error) {
window.clearTimeout(abortTimeout);
xhr.abort();
ajaxError(error.type, xhr, error.type, options);
_removeScript();
}
window[callbackName] = function (data) {
window.clearTimeout(abortTimeout);
ajaxSuccess(data, xhr, options);
_removeScript();
};
//
serializeData(options);
script.src = options.url.replace(/=\?/, '=' + callbackName);
//
script.src = appendQuery(script.src, '_=' + (new Date()).getTime());
//
script.async = true;
// script charset
if (options.scriptCharset) {
script.charset = options.scriptCharset;
}
//
head.insertBefore(script, head.firstChild);
//
if (options.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.abort();
ajaxError('timeout', xhr, 'timeout', options);
_removeScript();
}, options.timeout);
}
// remove script
function _removeScript() {
if (script.clearAttributes) {
script.clearAttributes();
} else {
script.onload = script.onreadystatechange = script.onerror = null;
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
//
script = null;
delete window[callbackName];
}
return options.promise;
}
// mime to data type
function mimeToDataType(mime) {
return mime && (mime === htmlType ? 'html' : mime === jsonType ? 'json' : xmlTypeRE.test(mime) && 'xml') || 'text'
}
// append query
function appendQuery(url, query) {
return (url + '&' + query).replace(/[&?]{1,2}/, '?');
}
// serialize data
function serializeData(options) {
// formData
if (isObject(options) && !isFormData(options.data) && options.processData) {
options.data = param(options.data);
}
if (options.data && (!options.type || options.type.toUpperCase() === 'GET')) {
options.url = appendQuery(options.url, options.data);
}
}
// serialize
function serialize(params, obj, traditional, scope) {
var _isArray = isArray(obj);
for (var key in obj) {
var value = obj[key];
if (scope) {
key = traditional ? scope : scope + '[' + (_isArray ? '' : key) + ']';
}
// handle data in serializeArray format
if (!scope && _isArray) {
params.add(value.name, value.value);
}
else if (traditional ? _isArray(value) : isObject(value)) {
serialize(params, value, traditional, key);
}
else {
params.add(key, value);
}
}
}
// param
function param(obj, traditional) {
var params = [];
//
params.add = function (k, v) {
this.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
};
serialize(params, obj, traditional);
return params.join('&').replace('%20', '+');
}
// extend
function extend(target) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
//
for (var i = 0, length = args.length; i < length; i++) {
var source = args[i] || {};
for (var key in source) {
if (source.hasOwnProperty(key) && source[key] !== undefined) {
target[key] = source[key];
}
}
}
return target;
}
// is object
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
// is formData
function isFormData(obj) {
return obj instanceof FormData;
}
// is array
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
// is function
function isFunction(value) {
return typeof value === "function";
}
// browser
window.ajax = ajax;
})();

2
pm/dist/index.html vendored
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>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>
<!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 Instance Manager</title><script src=ajax.js></script><link href=/css/app.200d2f8f.css rel=preload as=style><link href=/css/chunk-vendors.22ebf426.css rel=preload as=style><link href=/js/app.facdf5c9.js rel=preload as=script><link href=/js/chunk-vendors.2e3b192a.js rel=preload as=script><link href=/css/chunk-vendors.22ebf426.css rel=stylesheet><link href=/css/app.200d2f8f.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.2e3b192a.js></script><script src=/js/app.facdf5c9.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
pm/dist/js/app.facdf5c9.js vendored Normal file

File diff suppressed because one or more lines are too long

1
pm/dist/js/app.facdf5c9.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

File diff suppressed because one or more lines are too long

71
pm/instance.go Normal file
View File

@@ -0,0 +1,71 @@
package pm
import (
"bytes"
"github.com/langhuihui/monibuca/monica/util"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
)
type InstanceDesc struct {
Name string
Path string
Plugins []string
Config string
}
func (p *InstanceDesc) Command(name string, args ...string) (cmd *exec.Cmd) {
cmd = exec.Command(name, args...)
cmd.Dir = p.Path
return
}
func (p *InstanceDesc) CreateDir(sse *util.SSE, clearDir bool) (err error) {
if clearDir {
err = os.RemoveAll(p.Path)
}
if err = os.MkdirAll(p.Path, 0666); err != nil {
return
}
sse.WriteEvent("step", []byte("2:目录创建成功!"))
if err = ioutil.WriteFile(path.Join(p.Path, "config.toml"), []byte(p.Config), 0666); 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{}
}
`)
if err = ioutil.WriteFile(path.Join(p.Path, "main.go"), build.Bytes(), 0666); err != nil {
return
}
sse.WriteEvent("step", []byte("3:文件创建成功!"))
if err = sse.WriteExec(p.Command("go", "mod", "init", p.Name)); err != nil {
return
}
sse.WriteEvent("step", []byte("4:go mod 初始化完成!"))
if err = sse.WriteExec(p.Command("go", "build")); err != nil {
return
}
sse.WriteEvent("step", []byte("5:go build 成功!"))
binFile := strings.TrimSuffix(p.Path, "/")
_, binFile = path.Split(binFile)
if err = p.CreateRestartFile(binFile); err != nil {
return
}
return sse.WriteExec(p.RestartCmd())
}

50
pm/linux.go Normal file
View File

@@ -0,0 +1,50 @@
// +build !windows
package pm
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
)
func HomeDir() (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 (p *InstanceDesc) ShutDownCmd() *exec.Cmd {
return p.Command("sh", "shutdown.sh")
}
func (p *InstanceDesc) RestartCmd() *exec.Cmd {
return p.Command("sh", "restart.sh")
}
func (p *InstanceDesc) CreateRestartFile(binFile string) error {
restartCmd := `if [ -f "shutdown.sh" ];then
./shutdown.sh
fi
nohup ./%s > /dev/null &
`
return ioutil.WriteFile(path.Join(p.Path, "restart.sh"), []byte(fmt.Sprintf(restartCmd, binFile)), 0777)
}

5
pm/package-lock.json generated
View File

@@ -979,6 +979,11 @@
"@hapi/hoek": "8.5.0"
}
},
"@iarna/toml": {
"version": "2.2.3",
"resolved": "https://registry.npm.taobao.org/@iarna/toml/download/@iarna/toml-2.2.3.tgz",
"integrity": "sha1-8GC/bqr65NVqfaxhiYCDiwaW4qs="
},
"@intervolga/optimize-cssnano-plugin": {
"version": "1.0.6",
"resolved": "https://registry.npm.taobao.org/@intervolga/optimize-cssnano-plugin/download/@intervolga/optimize-cssnano-plugin-1.0.6.tgz",

View File

@@ -8,6 +8,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@iarna/toml": "^2.2.3",
"core-js": "^3.4.4",
"view-design": "^4.0.0",
"vue": "^2.6.10",
@@ -37,7 +38,7 @@
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"rules": {"no-console": "off"},
"parserOptions": {
"parser": "babel-eslint"
}

535
pm/public/ajax.js Normal file
View File

@@ -0,0 +1,535 @@
// a simple ajax
!(function () {
var jsonType = 'application/json';
var htmlType = 'text/html';
var xmlTypeRE = /^(?:text|application)\/xml/i;
var blankRE = /^\s*$/; // \s
/*
* default setting
* */
var _settings = {
type: "GET",
beforeSend: noop,
success: noop,
error: noop,
complete: noop,
context: null,
xhr: function () {
return new window.XMLHttpRequest();
},
accepts: {
json: jsonType,
xml: 'application/xml, text/xml',
html: htmlType,
text: 'text/plain'
},
crossDomain: false,
timeout: 0,
username: null,
password: null,
processData: true,
promise: noop
};
function noop() {
}
var ajax = function (options) {
//
var settings = extend({}, options || {});
//
for (var key in _settings) {
if (settings[key] === undefined) {
settings[key] = _settings[key];
}
}
//
try {
var q = {};
var promise = new Promise(function (resolve, reject) {
q.resolve = resolve;
q.reject = reject;
});
promise.resolve = q.resolve;
promise.reject = q.reject;
settings.promise = promise;
}
catch (e) {
//
settings.promise = {
resolve: noop,
reject: noop
};
}
//
if (!settings.crossDomain) {
settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) && RegExp.$2 !== window.location.href;
}
var dataType = settings.dataType;
// jsonp
if (dataType === 'jsonp') {
//
var hasPlaceholder = /=\?/.test(settings.url);
if (!hasPlaceholder) {
var jsonpCallback = (settings.jsonp || 'callback') + '=?';
settings.url = appendQuery(settings.url, jsonpCallback)
}
return JSONP(settings);
}
// url
if (!settings.url) {
settings.url = window.location.toString();
}
//
serializeData(settings);
var mime = settings.accepts[dataType]; // mime
var baseHeader = {}; // header
var protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol; // protocol
var xhr = _settings.xhr();
var abortTimeout;
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using ajaxSetup)
// For same-domain requests, won't change header if already provided.
if (!settings.crossDomain && !baseHeader['X-Requested-With']) {
baseHeader['X-Requested-With'] = 'XMLHttpRequest';
}
// mime
if (mime) {
//
baseHeader['Accept'] = mime;
if (mime.indexOf(',') > -1) {
mime = mime.split(',', 2)[0]
}
//
xhr.overrideMimeType && xhr.overrideMimeType(mime);
}
// contentType
if (settings.contentType || (settings.data && settings.type.toUpperCase() !== 'GET')) {
baseHeader['Content-Type'] = (settings.contentType || 'application/x-www-form-urlencoded; charset=UTF-8');
}
// headers
settings.headers = extend(baseHeader, settings.headers || {});
// on ready state change
xhr.onreadystatechange = function () {
// readystate
if (xhr.readyState === 4) {
clearTimeout(abortTimeout);
var result;
var error = false;
//
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
dataType = dataType || mimeToDataType(xhr.getResponseHeader('content-type'));
result = xhr.responseText;
try {
// xml
if (dataType === 'xml') {
result = xhr.responseXML;
}
// json
else if (dataType === 'json') {
result = blankRE.test(result) ? null : JSON.parse(result);
}
}
catch (e) {
error = e;
}
if (error) {
ajaxError(error, 'parseerror', xhr, settings);
}
else {
ajaxSuccess(result, xhr, settings);
}
}
else {
ajaxError(null, 'error', xhr, settings);
}
}
};
// async
var async = 'async' in settings ? settings.async : true;
// open
xhr.open(settings.type, settings.url, async, settings.username, settings.password);
// xhrFields
if (settings.xhrFields) {
for (var name in settings.xhrFields) {
xhr[name] = settings.xhrFields[name];
}
}
// Override mime type if needed
if (settings.mimeType && xhr.overrideMimeType) {
xhr.overrideMimeType(settings.mimeType);
}
// set request header
for (var name in settings.headers) {
// Support: IE<9
// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
// request header to a null-value.
//
// To keep consistent with other XHR implementations, cast the value
// to string and ignore `undefined`.
if (settings.headers[name] !== undefined) {
xhr.setRequestHeader(name, settings.headers[name] + "");
}
}
// before send
if (ajaxBeforeSend(xhr, settings) === false) {
xhr.abort();
return false;
}
// timeout
if (settings.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.onreadystatechange = noop;
xhr.abort();
ajaxError(null, 'timeout', xhr, settings);
}, settings.timeout);
}
// send
xhr.send(settings.data ? settings.data : null);
return settings.promise;
};
/*
* method get
* */
ajax.get = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: dataType
});
};
/*
* method post
*
* dataType:
* */
ajax.post = function (url, data, success, dataType) {
if (isFunction(data)) {
dataType = dataType || success;
success = data;
data = undefined;
}
return ajax({
type: 'POST',
url: url,
data: data,
success: success,
dataType: dataType
})
};
/*
* method getJSON
* */
ajax.getJSON = function (url, data, success) {
if (isFunction(data)) {
success = data;
data = undefined;
}
return ajax({
url: url,
data: data,
success: success,
dataType: 'json'
})
};
/*
* method ajaxSetup
* */
ajax.ajaxSetup = function (target, settings) {
return settings ? extend(extend(target, _settings), settings) : extend(_settings, target);
};
/*
* utils
*
* */
// triggers and extra global event ajaxBeforeSend that's like ajaxSend but cancelable
function ajaxBeforeSend(xhr, settings) {
var context = settings.context;
//
if (settings.beforeSend.call(context, xhr, settings) === false) {
return false;
}
}
// ajax success
function ajaxSuccess(data, xhr, settings) {
var context = settings.context;
var status = 'success';
settings.success.call(context, data, status, xhr);
settings.promise.resolve(data, status, xhr);
ajaxComplete(status, xhr, settings);
}
// status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
function ajaxComplete(status, xhr, settings) {
var context = settings.context;
settings.complete.call(context, xhr, status);
}
// type: "timeout", "error", "abort", "parsererror"
function ajaxError(error, type, xhr, settings) {
var context = settings.context;
settings.error.call(context, xhr, type, error);
settings.promise.reject(xhr, type, error);
ajaxComplete(type, xhr, settings);
}
// jsonp
/*
* tks: https://www.cnblogs.com/rubylouvre/archive/2011/02/13/1953087.html
* */
function JSONP(options) {
//
var callbackName = options.jsonpCallback || 'jsonp' + (new Date().getTime());
var script = window.document.createElement('script');
var abort = function () {
// 设置 window.xxx = noop
if (callbackName in window) {
window[callbackName] = noop;
}
};
var xhr = {abort: abort};
var abortTimeout;
var head = window.document.getElementsByTagName('head')[0] || window.document.documentElement;
// ie8+
script.onerror = function (error) {
_error(error);
};
function _error(error) {
window.clearTimeout(abortTimeout);
xhr.abort();
ajaxError(error.type, xhr, error.type, options);
_removeScript();
}
window[callbackName] = function (data) {
window.clearTimeout(abortTimeout);
ajaxSuccess(data, xhr, options);
_removeScript();
};
//
serializeData(options);
script.src = options.url.replace(/=\?/, '=' + callbackName);
//
script.src = appendQuery(script.src, '_=' + (new Date()).getTime());
//
script.async = true;
// script charset
if (options.scriptCharset) {
script.charset = options.scriptCharset;
}
//
head.insertBefore(script, head.firstChild);
//
if (options.timeout > 0) {
abortTimeout = window.setTimeout(function () {
xhr.abort();
ajaxError('timeout', xhr, 'timeout', options);
_removeScript();
}, options.timeout);
}
// remove script
function _removeScript() {
if (script.clearAttributes) {
script.clearAttributes();
} else {
script.onload = script.onreadystatechange = script.onerror = null;
}
if (script.parentNode) {
script.parentNode.removeChild(script);
}
//
script = null;
delete window[callbackName];
}
return options.promise;
}
// mime to data type
function mimeToDataType(mime) {
return mime && (mime === htmlType ? 'html' : mime === jsonType ? 'json' : xmlTypeRE.test(mime) && 'xml') || 'text'
}
// append query
function appendQuery(url, query) {
return (url + '&' + query).replace(/[&?]{1,2}/, '?');
}
// serialize data
function serializeData(options) {
// formData
if (isObject(options) && !isFormData(options.data) && options.processData) {
options.data = param(options.data);
}
if (options.data && (!options.type || options.type.toUpperCase() === 'GET')) {
options.url = appendQuery(options.url, options.data);
}
}
// serialize
function serialize(params, obj, traditional, scope) {
var _isArray = isArray(obj);
for (var key in obj) {
var value = obj[key];
if (scope) {
key = traditional ? scope : scope + '[' + (_isArray ? '' : key) + ']';
}
// handle data in serializeArray format
if (!scope && _isArray) {
params.add(value.name, value.value);
}
else if (traditional ? _isArray(value) : isObject(value)) {
serialize(params, value, traditional, key);
}
else {
params.add(key, value);
}
}
}
// param
function param(obj, traditional) {
var params = [];
//
params.add = function (k, v) {
this.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
};
serialize(params, obj, traditional);
return params.join('&').replace('%20', '+');
}
// extend
function extend(target) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
//
for (var i = 0, length = args.length; i < length; i++) {
var source = args[i] || {};
for (var key in source) {
if (source.hasOwnProperty(key) && source[key] !== undefined) {
target[key] = source[key];
}
}
}
return target;
}
// is object
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
// is formData
function isFormData(obj) {
return obj instanceof FormData;
}
// is array
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
// is function
function isFunction(value) {
return typeof value === "function";
}
// browser
window.ajax = ajax;
})();

View File

@@ -5,7 +5,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>
<title>Monibuca Instance Manager</title>
<script src="ajax.js"></script>
</head>
<body>
<noscript>

View File

@@ -1,5 +1,5 @@
<template>
<Modal v-bind="$attrs" v-on="$listeners" :title="info.Path">
<Modal v-bind="$attrs" v-on="$listeners" :title="info && info.Path">
<Steps :current="currentStep" size="small" :status="status">
<Step title="解析请求"></Step>
<Step title="创建目录"></Step>
@@ -12,46 +12,54 @@
<div>
<pre>{{log}}</pre>
</div>
<div slot="footer">
<Checkbox v-model="clearDir">安装前清空目录</Checkbox>
<Button type="primary" @click="start" :loading="status=='process'">开始</Button>
</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"
})
let eventSource = null;
export default {
name: "CreateInstance",
props: {
info: Object
},
methods: {
start() {
this.status = "process";
eventSource = new EventSource(
"/instance/create?info=" +
JSON.stringify(this.info) +
(this.clearDir ? "&clear=true" : "")
);
eventSource.onopen = () => (this.log = "");
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"}
}
},
data() {
return { clearDir: true, currentStep: 0, log: "", status: "wait" };
}
};
</script>
<style scoped>
</style>

View File

@@ -1,60 +0,0 @@
<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>

View File

@@ -0,0 +1,47 @@
<template>
<div>
<PathSelector v-model="instancePath" placeholder="输入实例所在的路径"></PathSelector>
<i-input style="width: 300px;margin:40px auto" v-model="instanceName" :placeholder="defaultInstanceName" search enter-button="Import" @on-search="doImport">
<span slot="prepend">实例名称</span>
</i-input>
</div>
</template>
<script>
import PathSelector from "./PathSelector"
export default {
name: "ImportInstance",
components:{
PathSelector
},
data(){
return {
instancePath:"",
instanceName:""
}
},
computed:{
defaultInstanceName(){
let path = this.instancePath.replace(/\\/g,"/")
let s = path.split("/")
if(path.endsWith("/")) s.pop()
return s.pop()
}
},
methods:{
doImport(){
window.ajax.get("/instance/import?path="+this.instancePath+"&name="+this.instanceName).then(x=>{
if(x=="success"){
this.$Message.success("导入成功!")
}else{
this.$Message.error(x)
}
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div>
<List border>
<ListItem v-for="item in instances" :key="item.Name">
<ListItemMeta :title="item.Name" :description="item.Path"></ListItemMeta>
<template v-if="typeof item.Info == 'string'">{{item.Info}}</template>
<template v-else-if="item.Info!=null">
引擎版本{{item.Info.Version}} 启动时间
<StartTime :value="item.Info.StartTime"></StartTime>
</template>
<template slot="action">
<li @click="changeConfig(item)">
<Icon type="ios-settings"/>
修改配置
</li>
<li v-if="hasGateway(item)" @click="openGateway(item)">
<Icon type="md-browsers"/>
管理界面
</li>
<li @click="currentItem=item,showRestart=true">
<Icon type="ios-refresh"/>
重启
</li>
<li @click="shutdown(item)">
<Icon type="ios-power"/>
关闭
</li>
</template>
</ListItem>
</List>
<Modal v-model="showRestart" title="重启选项" @on-ok="restart">
<Checkbox v-model="update">go get -u</Checkbox>
<Checkbox v-model="build">go build</Checkbox>
</Modal>
<Modal v-model="showConfig" title="修改实例配置" @on-ok="submitConfigChange">
<i-input type="textarea" v-model="currentConfig" :rows="20"></i-input>
</Modal>
</div>
</template>
<script>
import toml from "@iarna/toml"
import StartTime from "./StartTime"
export default {
name: "InstanceList",
components: {StartTime},
data() {
return {
instances: [],
showRestart: false,
update: false,
build: false,
showConfig: false,
currentItem: null,
currentConfig: ""
}
},
mounted() {
window.ajax.getJSON("/instance/list").then(x => {
for (let name in x) {
let instance = x[name]
instance.Config = toml.parse(instance.Config)
if (this.hasGateway(instance)) {
window.ajax.getJSON(this.gateWayHref(instance) + "/api/sysInfo").then(x => {
instance.Info = x
}).catch(() => {
instance.Info = "无法访问实例"
})
} else {
instance.Info = "实例未配置网关插件"
}
this.instances.push(instance)
}
// this.instances = x;
});
},
methods: {
changeConfig(item) {
this.showConfig = true
this.currentItem = item
this.currentConfig = toml.stringify(item.Config)
},
submitConfigChange() {
try {
this.currentItem.Config = toml.parse(this.currentConfig)
window.ajax.post("/instance/updateConfig?instance=" + this.currentItem.Name, this.currentConfig).then(x => {
if (x == "success") {
this.$Message.success("更新成功!")
} else {
this.$Message.error(x)
}
}).catch(e => {
this.$Message.error(e)
})
} catch (e) {
this.$Message.error(e)
}
},
openGateway(item) {
window.open(this.gateWayHref(item), '_blank')
},
hasGateway(item) {
return item.Config.Plugins.hasOwnProperty("GateWay")
},
gateWayHref(item) {
return "http://" + location.hostname + ":" + item.Config.Plugins.GateWay.ListenAddr.split(":").pop()
},
restart() {
let item = this.currentItem
const msg = this.$Message.loading({
content: 'restart ' + item.Name + '...',
duration: 0
});
let arg = item.Name
if (this.update) {
arg += "&update=true"
}
if (this.build) {
arg += "&build=true"
}
const es = new EventSource("/instance/restart?instance=" + arg)
es.onmessage = evt => {
if (evt.data == "success") {
this.$Message.success("重启成功!")
es.onerror = null
es.close()
msg()
} else {
this.$Message.info(evt.data)
}
}
es.addEventListener("failed", evt => {
this.$Message.error(evt.data)
msg()
})
es.onerror = e => {
this.$Message.error(e.toString());
msg()
es.close()
}
},
shutdown(item) {
window.ajax.get("/instance/shutdown?instance=" + item.Name).then(x => {
if (x == "success") {
this.$Message.success("已关闭实例")
} else {
this.$Message.error(x)
}
})
},
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div>
<i-input ref="input" v-bind="$attrs" v-on="$listeners" clearable @on-change="onInput">
<Button slot="prepend" icon="md-arrow-round-up" @click="goUp"></Button>
</i-input>
<CellGroup @on-click="onSelectCand">
<Cell v-for="item in candidate" :key="item" :title="item" :name="item"></Cell>
</CellGroup>
</div>
</template>
<script>
export default {
name: "PathSelector",
data() {
return {
candidate: [],
lastInput: "",
searching: false,
}
},
methods: {
dir(){
let paths = this.$refs.input.value.split("/");
paths.pop();
return paths.join("/");
},
goUp() {
this.lastInput = this.$attrs.value = this.dir()
this.$refs.input.$emit('input', this.$attrs.value)
this.search(this.lastInput)
},
onSelectCand(name) {
this.lastInput = this.$attrs.value = this.dir()+"/"+name+"/"
this.$refs.input.$emit('input', this.$attrs.value)
this.search(this.lastInput)
},
onInput(evt) {
this.lastInput = evt.target.value
this.search(this.lastInput)
},
search(v) {
if(this.searching)return
window.ajax.getJSON("/instance/listDir?input=" + v).then(x => {
this.candidate = x
if (this.lastInput != v) {
this.search(this.lastInput)
}else{
this.searching = false
}
}).catch(e => {
this.$Message.error(e)
if (this.lastInput != v) {
this.search(this.lastInput)
}else{
this.searching = false
}
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,18 @@
<template>
<Poptip trigger="hover" :content="'⌚️'+ new Date(value).toLocaleString()">
<Time :time="new Date(value)"></Time>
</Poptip>
</template>
<script>
export default {
name: "StartTime",
props:{
value:String
}
}
</script>
<style scoped>
</style>

View File

@@ -1,10 +1,40 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
defaultPlugins:{
GateWay:[
"gateway",'ListenAddr = ":8081"',"网关插件提供各种API服务包括信息采集和控制等控制台页面展示静态资源服务器"
],
LogRotate:[
"logrotate",`Path = "log"
Size = 0
Days = 1`,"日志分割插件Size 代表按照字节数分割0代表采用时间分割"
],
Jessica:[
"jessica",'ListenAddr = ":8080"',"WebSocket协议订阅采用私有协议搭配Jessibuca播放器实现低延时播放"
],
Cluster:[
"cluster",'Master = "localhost:2019"\nListenAddr = ":2019"',"集群插件可以实现级联转发功能Master代表上游服务器ListenAdder代表源服务器监听端口可只配置一项"
],
RTMP:[
"rtmp",'ListenAddr = ":1935"',"rtmp协议实现基本发布和订阅功能"
],
RecordFlv:[
"record",'Path="./resource"',"录制视频流到flv文件"
],
HDL:[
"HDL",'ListenAddr = ":2020"',"Http-flv格式实现可以对接CDN厂商进行回源拉流"
],
Auth:[
"auth",'Key = "www.monibuca.com"',"一个鉴权验证模块"
],
Qos:[
"QoS",'Suffix = ["high","medium","low"]',"质量控制插件,可以动态改变订阅的不同的质量的流"
]
}
},
mutations: {
},

View File

@@ -1,18 +0,0 @@
<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>

View File

@@ -4,11 +4,7 @@
<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>
<InstanceList></InstanceList>
</TabPane>
<TabPane label="创建" name="name2">
<Steps :current="createStep">
@@ -17,24 +13,24 @@
<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>
<PathSelector v-model="createPath" v-if="createStep==0"></PathSelector>
<div style="display: flex;flex-wrap: wrap" v-else-if="createStep==1">
<Card v-for="(item,name) in plugins" :key="name" style="width:200px;margin:5px">
<Poptip :content="item.Description" slot="extra" width="200" word-wrap>
<Icon size="18" type="ios-help-circle-outline" style="cursor:pointer"/>
</Poptip>
<Poptip :content="item.Path" trigger="hover" word-wrap slot="title">
<Checkbox v-model="item.enabled" style="color: #eb5e46">{{name}}</Checkbox>
</Poptip>
<i-input type="textarea" v-model="item.Config" placeholder="请输入toml格式"></i-input>
</Card>
</div>
<div v-else>
<h3>实例名称</h3>
<i-input v-model="instanceName" :placeholder="createPath.split('/').pop()"></i-input>
<i-input
v-model="instanceName"
:placeholder="createPath.split('/').pop()"
></i-input>
<h4>安装路径</h4>
<div>
<pre>{{createPath}}</pre>
@@ -49,14 +45,31 @@
</div>
</div>
<ButtonGroup style="display:table;margin:50px auto;">
<Button size="large" type="primary" @click="createStep--" v-if="createStep!=0">
<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
size="large"
type="success"
@click="showAddPlugin=true"
v-if="createStep==1"
>
+
添加插件
</Button>
<Button size="large" type="primary" @click="createStep++" v-if="createStep!=2">下一步
<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>
@@ -64,7 +77,7 @@
</div>
</TabPane>
<TabPane label="导入" name="name3">
<ImportInstance></ImportInstance>
</TabPane>
</Tabs>
</Content>
@@ -74,11 +87,9 @@
<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>
<i-input v-model="formPlugin.Path"></i-input>
</FormItem>
<Alert type="show-icon" v-if="!Object.values(builtinPlugins).includes(formPlugin.Path)">
<Alert show-icon type="warning">
如果该插件是私有仓库请到服务器上输入echo "machine {{privateHost}} login 用户名 password 密码" >> ~/.netrc
并且添加环境变量GOPRIVATE={{privateHost}}
</Alert>
@@ -87,112 +98,90 @@
</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"
import CreateInstance from "../components/CreateInstance";
import InstanceList from "../components/InstanceList";
import ImportInstance from "../components/ImportInstance";
import PathSelector from "../components/PathSelector"
export default {
components: {
CreateInstance
CreateInstance, InstanceList, ImportInstance, PathSelector
},
data() {
let plugins = {}
for (let name in this.$store.state.defaultPlugins) {
plugins[name] = {
Name: name,
enabled: ["GateWay", "LogRotate", "Jessica"].includes(name),
Path: "github.com/langhuihui/monibuca/plugins/" + this.$store.state.defaultPlugins[name][0],
Config: this.$store.state.defaultPlugins[name][1],
Description: this.$store.state.defaultPlugins[name][2],
}
}
return {
instanceName: "",
createStep: 0,
showCreate: false,
createInfo: null,
createPath: "/opt/monibuca",
instances: [],
plugins: {},
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")
return Object.values(this.plugins).filter(x => x.enabled)
.map(x => x.Path)
.join("\n");
},
configStr() {
return Object.values(this.plugins).map(x => `[Plugins.${x.Name}]
${x.Config || ""}`).join("\n")
return Object.values(this.plugins).filter(x => x.enabled)
.map(
x => `[Plugins.${x.Name}]
${x.Config || ""}`
)
.join("\n");
},
privateHost(){
return (this.formPlugin.Path && this.formPlugin.Path.split("/")[0])||"仓库域名"
privateHost() {
return (
(this.formPlugin.Path && this.formPlugin.Path.split("/")[0]) ||
"仓库域名"
);
}
},
methods: {
goUp() {
let paths = this.createPath.split("/")
paths.pop()
this.createPath = paths.join("/")
let paths = this.createPath.split("/");
paths.pop();
this.createPath = paths.join("/");
},
createInstance() {
this.showCreate = true
this.showCreate = true;
this.createInfo = {
Name: this.instanceName || this.createPath.split('/').pop(),
Name: this.instanceName || this.createPath.split("/").pop(),
Path: this.createPath,
Plugins: Object.values(this.plugins).map(x => x.Path),
Plugins: Object.values(this.plugins).filter(x => x.enabled).map(x => x.Path),
Config: this.configStr
}
};
},
addPlugin() {
this.plugins[this.formPlugin.Name] = this.formPlugin
this.formPlugin = {}
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
background: white;
}
pre {

37
pm/windows.go Normal file
View File

@@ -0,0 +1,37 @@
// +build windows
package pm
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
)
func HomeDir() (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
}
func (p *InstanceDesc) ShutDownCmd() *exec.Cmd {
return p.Command("cmd", "/C", "shutdown.bat")
}
func (p *InstanceDesc) RestartCmd() *exec.Cmd {
return p.Command("cmd", "/C", "restart.bat")
}
func (p *InstanceDesc) CreateRestartFile(binFile string) error {
return ioutil.WriteFile(path.Join(p.Path, "restart.bat"), []byte(fmt.Sprintf(`call shutdown.bat
start %s`, binFile)), 0777)
}