monaco initial
This commit is contained in:
+410
-35
@@ -4,11 +4,17 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>config - go2rtc</title>
|
<title>config - go2rtc</title>
|
||||||
<script src="https://unpkg.com/ace-builds@1.33.1/src-min/ace.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
html, body, #config {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#config {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -22,44 +28,413 @@
|
|||||||
</main>
|
</main>
|
||||||
<div id="config"></div>
|
<div id="config"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
/* global ace */
|
/* global require, monaco */
|
||||||
ace.config.set('basePath', 'https://unpkg.com/ace-builds@1.33.1/src-min/');
|
const monacoVersion = '0.52.2';
|
||||||
const editor = ace.edit('config', {
|
const monacoRoot = `https://cdn.jsdelivr.net/npm/monaco-editor@${monacoVersion}/min`;
|
||||||
mode: 'ace/mode/yaml',
|
const monacoBase = `${monacoRoot}/vs`;
|
||||||
});
|
|
||||||
|
|
||||||
let dump;
|
window.MonacoEnvironment = {
|
||||||
|
getWorkerUrl: function () {
|
||||||
document.getElementById('save').addEventListener('click', async () => {
|
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
|
||||||
let r = await fetch('api/config', {cache: 'no-cache'});
|
self.MonacoEnvironment = { baseUrl: '${monacoRoot}/' };
|
||||||
if (r.ok && dump !== await r.text()) {
|
importScripts('${monacoBase}/base/worker/workerMain.js');
|
||||||
alert('Config was changed from another place. Refresh the page and make changes again');
|
`)}`;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
require.config({ paths: { vs: monacoBase } });
|
||||||
if (r.ok) {
|
|
||||||
alert('OK');
|
|
||||||
dump = editor.getValue();
|
|
||||||
await fetch('api/restart', {method: 'POST'});
|
|
||||||
} else {
|
|
||||||
alert(await r.text());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
require(['vs/editor/editor.main'], () => {
|
||||||
const r = await fetch('api/config', {cache: 'no-cache'});
|
const container = document.getElementById('config');
|
||||||
if (r.status === 410) {
|
container.textContent = '';
|
||||||
alert('Config file is not set');
|
|
||||||
} else if (r.status === 404) {
|
const ensureYamlLanguage = () => {
|
||||||
editor.setValue(''); // config file not exist
|
const hasYaml = (monaco.languages.getLanguages?.() || []).some((l) => l.id === 'yaml');
|
||||||
} else if (r.ok) {
|
if (hasYaml) return;
|
||||||
dump = await r.text();
|
|
||||||
editor.setValue(dump);
|
monaco.languages.register({
|
||||||
} else {
|
id: 'yaml',
|
||||||
alert(`Unknown error: ${r.statusText} (${r.status})`);
|
extensions: ['.yaml', '.yml'],
|
||||||
}
|
aliases: ['YAML', 'yaml'],
|
||||||
|
mimetypes: ['application/x-yaml', 'text/yaml'],
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.languages.setLanguageConfiguration('yaml', {
|
||||||
|
comments: {lineComment: '#'},
|
||||||
|
brackets: [['{', '}'], ['[', ']'], ['(', ')']],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{open: '{', close: '}'},
|
||||||
|
{open: '[', close: ']'},
|
||||||
|
{open: '(', close: ')'},
|
||||||
|
{open: '"', close: '"'},
|
||||||
|
{open: '\'', close: '\''},
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{open: '{', close: '}'},
|
||||||
|
{open: '[', close: ']'},
|
||||||
|
{open: '(', close: ')'},
|
||||||
|
{open: '"', close: '"'},
|
||||||
|
{open: '\'', close: '\''},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.languages.setMonarchTokensProvider('yaml', {
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/^\s*(---|\.\.\.)\s*$/, 'delimiter'],
|
||||||
|
[/#.*$/, 'comment'],
|
||||||
|
[/^\s*-\s+/, 'delimiter'],
|
||||||
|
[/[A-Za-z0-9_-]+(?=\s*:)/, 'key'],
|
||||||
|
[/:/, 'delimiter'],
|
||||||
|
[/[{}\[\](),]/, 'delimiter'],
|
||||||
|
[/\b(true|false|null|~)\b/, 'keyword'],
|
||||||
|
[/-?\d+(\.\d+)?\b/, 'number'],
|
||||||
|
[/"/, 'string', '@string_double'],
|
||||||
|
[/'/, 'string', '@string_single'],
|
||||||
|
[/[^#\s{}\[\](),]+/, 'string'],
|
||||||
|
[/\s+/, ''],
|
||||||
|
],
|
||||||
|
string_double: [
|
||||||
|
[/[^\\"]+/, 'string'],
|
||||||
|
[/\\./, 'string.escape'],
|
||||||
|
[/"/, 'string', '@pop'],
|
||||||
|
],
|
||||||
|
string_single: [
|
||||||
|
[/[^']+/, 'string'],
|
||||||
|
[/'/, 'string', '@pop'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureYamlLanguage();
|
||||||
|
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
monaco.editor.setTheme(prefersDark ? 'vs-dark' : 'vs');
|
||||||
|
|
||||||
|
const editor = monaco.editor.create(container, {
|
||||||
|
language: 'yaml',
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true,
|
||||||
|
tabSize: 2,
|
||||||
|
insertSpaces: true,
|
||||||
|
quickSuggestions: { other: true, comments: false, strings: true },
|
||||||
|
suggestOnTriggerCharacters: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = () => {
|
||||||
|
const top = container.getBoundingClientRect().top;
|
||||||
|
container.style.height = `${Math.max(200, window.innerHeight - top)}px`;
|
||||||
|
editor.layout();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', layout);
|
||||||
|
layout();
|
||||||
|
|
||||||
|
const stripInlineComment = (line) => {
|
||||||
|
let inSingle = false;
|
||||||
|
let inDouble = false;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const ch = line[i];
|
||||||
|
if (ch === '\'' && !inDouble) {
|
||||||
|
inSingle = !inSingle;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"' && !inSingle) {
|
||||||
|
inDouble = !inDouble;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '#' && !inSingle && !inDouble) {
|
||||||
|
if (i === 0 || /\s/.test(line[i - 1])) return line.slice(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
};
|
||||||
|
|
||||||
|
const countIndent = (line) => {
|
||||||
|
let indent = 0;
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
if (line[i] === ' ') {
|
||||||
|
indent++;
|
||||||
|
} else if (line[i] === '\t') {
|
||||||
|
indent += 2;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseListItem = (line) => {
|
||||||
|
const m = line.match(/^(\s*)-\s*(.*)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
return {indent: countIndent(m[1]), rest: m[2]};
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseKey = (line) => {
|
||||||
|
const m = line.match(/^(\s*)([A-Za-z0-9_-]+)\s*:(.*)$/);
|
||||||
|
if (!m) return null;
|
||||||
|
const after = m[3] ?? '';
|
||||||
|
const isContainer = after.trim() === '' || after.trim().startsWith('#');
|
||||||
|
return {indent: countIndent(m[1]), key: m[2], isContainer, after};
|
||||||
|
};
|
||||||
|
|
||||||
|
const unique = (arr) => [...new Set(arr)];
|
||||||
|
|
||||||
|
const toYamlScalar = (v) => {
|
||||||
|
if (v === '') return '\'\'';
|
||||||
|
if (typeof v === 'string') return v;
|
||||||
|
if (typeof v === 'number') return String(v);
|
||||||
|
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
||||||
|
return JSON.stringify(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupYamlHints = (schemaRoot) => {
|
||||||
|
const resolveRef = (schema, seen = new Set()) => {
|
||||||
|
if (!schema || typeof schema !== 'object') return schema;
|
||||||
|
if (typeof schema.$ref === 'string') {
|
||||||
|
const ref = schema.$ref;
|
||||||
|
if (ref.startsWith('#/definitions/')) {
|
||||||
|
if (seen.has(ref)) return schema;
|
||||||
|
seen.add(ref);
|
||||||
|
const name = ref.slice('#/definitions/'.length);
|
||||||
|
const def = schemaRoot.definitions?.[name];
|
||||||
|
if (!def) return schema;
|
||||||
|
const resolved = resolveRef(def, seen);
|
||||||
|
const {$ref, ...rest} = schema;
|
||||||
|
return {...resolved, ...rest};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeProps = (schemas) => {
|
||||||
|
const props = {};
|
||||||
|
for (const s of schemas) {
|
||||||
|
const schema = resolveRef(s);
|
||||||
|
if (schema && schema.properties && typeof schema.properties === 'object') {
|
||||||
|
Object.assign(props, schema.properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getObjectProperties = (schema) => {
|
||||||
|
schema = resolveRef(schema);
|
||||||
|
if (!schema) return {};
|
||||||
|
if (schema.properties && typeof schema.properties === 'object') return schema.properties;
|
||||||
|
if (Array.isArray(schema.anyOf)) return mergeProps(schema.anyOf);
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPropertySchema = (schema, key) => {
|
||||||
|
schema = resolveRef(schema);
|
||||||
|
if (!schema) return null;
|
||||||
|
|
||||||
|
if (schema.properties && schema.properties[key]) return resolveRef(schema.properties[key]);
|
||||||
|
|
||||||
|
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
||||||
|
return resolveRef(schema.additionalProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(schema.anyOf)) {
|
||||||
|
for (const alt of schema.anyOf) {
|
||||||
|
const res = getPropertySchema(alt, key);
|
||||||
|
if (res) return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValueSuggestions = (schema) => {
|
||||||
|
schema = resolveRef(schema);
|
||||||
|
if (!schema) return [];
|
||||||
|
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
const addFrom = (s) => {
|
||||||
|
s = resolveRef(s);
|
||||||
|
if (!s) return;
|
||||||
|
if (Array.isArray(s.enum)) values.push(...s.enum);
|
||||||
|
if ('const' in s) values.push(s.const);
|
||||||
|
if (Array.isArray(s.examples)) values.push(...s.examples);
|
||||||
|
if ('default' in s) values.push(s.default);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(schema.anyOf)) {
|
||||||
|
for (const alt of schema.anyOf) addFrom(alt);
|
||||||
|
} else {
|
||||||
|
addFrom(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unique(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildContextStack = (model, upToLineNumber) => {
|
||||||
|
const stack = [{indent: -1, schema: schemaRoot}];
|
||||||
|
|
||||||
|
for (let lineNumber = 1; lineNumber <= upToLineNumber; lineNumber++) {
|
||||||
|
let line = model.getLineContent(lineNumber);
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
line = stripInlineComment(line).trimEnd();
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
|
const listItem = parseListItem(line);
|
||||||
|
if (listItem) {
|
||||||
|
while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();
|
||||||
|
const parent = resolveRef(stack[stack.length - 1].schema);
|
||||||
|
if (parent && parent.type === 'array' && parent.items) {
|
||||||
|
stack.push({indent: listItem.indent, schema: resolveRef(parent.items)});
|
||||||
|
} else {
|
||||||
|
stack.push({indent: listItem.indent, schema: null});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inline = listItem.rest ? parseKey(' '.repeat(listItem.indent + 2) + listItem.rest) : null;
|
||||||
|
if (inline && inline.isContainer) {
|
||||||
|
while (stack.length > 1 && inline.indent <= stack[stack.length - 1].indent) stack.pop();
|
||||||
|
const ctx = resolveRef(stack[stack.length - 1].schema);
|
||||||
|
const next = ctx ? getPropertySchema(ctx, inline.key) : null;
|
||||||
|
stack.push({indent: inline.indent, schema: next});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kv = parseKey(line);
|
||||||
|
if (!kv) continue;
|
||||||
|
while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();
|
||||||
|
if (!kv.isContainer) continue;
|
||||||
|
|
||||||
|
const ctx = resolveRef(stack[stack.length - 1].schema);
|
||||||
|
const next = ctx ? getPropertySchema(ctx, kv.key) : null;
|
||||||
|
stack.push({indent: kv.indent, schema: next});
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.languages.registerCompletionItemProvider('yaml', {
|
||||||
|
triggerCharacters: [':', ' '],
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const line = model.getLineContent(position.lineNumber);
|
||||||
|
const lineNoComment = stripInlineComment(line).trimEnd();
|
||||||
|
const listItem = parseListItem(lineNoComment);
|
||||||
|
|
||||||
|
const {word, startColumn, endColumn} = model.getWordUntilPosition(position);
|
||||||
|
const range = new monaco.Range(position.lineNumber, startColumn, position.lineNumber, endColumn);
|
||||||
|
|
||||||
|
const cursorIndex = position.column - 1;
|
||||||
|
let contentStartIndex = 0;
|
||||||
|
if (listItem) {
|
||||||
|
contentStartIndex = listItem.indent;
|
||||||
|
if (lineNoComment.slice(contentStartIndex, contentStartIndex + 1) === '-') contentStartIndex += 1;
|
||||||
|
if (lineNoComment.slice(contentStartIndex, contentStartIndex + 1) === ' ') contentStartIndex += 1;
|
||||||
|
} else {
|
||||||
|
contentStartIndex = countIndent(lineNoComment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorIndex < contentStartIndex) return {suggestions: []};
|
||||||
|
|
||||||
|
const text = lineNoComment.slice(contentStartIndex);
|
||||||
|
const cursorInText = cursorIndex - contentStartIndex;
|
||||||
|
const colonIndex = text.indexOf(':');
|
||||||
|
const isValueContext = colonIndex >= 0 && cursorInText > colonIndex;
|
||||||
|
|
||||||
|
const stack = buildContextStack(model, position.lineNumber - 1);
|
||||||
|
|
||||||
|
const effectiveIndent = listItem ? listItem.indent + 2 : countIndent(lineNoComment);
|
||||||
|
while (stack.length > 1 && effectiveIndent <= stack[stack.length - 1].indent) stack.pop();
|
||||||
|
|
||||||
|
let contextSchema = resolveRef(stack[stack.length - 1].schema);
|
||||||
|
|
||||||
|
if (listItem && cursorIndex >= listItem.indent + 2 && contextSchema && contextSchema.type === 'array') {
|
||||||
|
contextSchema = resolveRef(contextSchema.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextSchema) return {suggestions: []};
|
||||||
|
|
||||||
|
if (!isValueContext) {
|
||||||
|
const props = getObjectProperties(contextSchema);
|
||||||
|
const suggestions = Object.keys(props).map((key) => {
|
||||||
|
const s = resolveRef(props[key]);
|
||||||
|
const wantsBlock = s && (s.type === 'object' || s.type === 'array' || s.properties);
|
||||||
|
const indent = listItem ? ' '.repeat(listItem.indent + 2) : ' '.repeat(countIndent(lineNoComment));
|
||||||
|
const innerIndent = indent + ' ';
|
||||||
|
|
||||||
|
const insertText = wantsBlock ? `${key}:\n${innerIndent}` : `${key}: `;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: key,
|
||||||
|
kind: monaco.languages.CompletionItemKind.Property,
|
||||||
|
insertText,
|
||||||
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||||
|
documentation: s?.description,
|
||||||
|
range,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {suggestions};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyName = text.slice(0, colonIndex).trim();
|
||||||
|
const keySchema = getPropertySchema(contextSchema, keyName);
|
||||||
|
if (!keySchema) return {suggestions: []};
|
||||||
|
|
||||||
|
const values = getValueSuggestions(keySchema);
|
||||||
|
const suggestions = values.map((v) => ({
|
||||||
|
label: toYamlScalar(v),
|
||||||
|
kind: monaco.languages.CompletionItemKind.Value,
|
||||||
|
insertText: toYamlScalar(v),
|
||||||
|
range,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {suggestions};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let dump;
|
||||||
|
|
||||||
|
document.getElementById('save').addEventListener('click', async () => {
|
||||||
|
let r = await fetch('api/config', {cache: 'no-cache'});
|
||||||
|
if (r.ok && dump !== await r.text()) {
|
||||||
|
alert('Config was changed from another place. Refresh the page and make changes again');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
r = await fetch('api/config', {method: 'POST', body: editor.getValue()});
|
||||||
|
if (r.ok) {
|
||||||
|
alert('OK');
|
||||||
|
dump = editor.getValue();
|
||||||
|
await fetch('api/restart', {method: 'POST'});
|
||||||
|
} else {
|
||||||
|
alert(await r.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const schemaRes = await fetch('schema.json', {cache: 'no-cache'});
|
||||||
|
if (schemaRes.ok) setupYamlHints(await schemaRes.json());
|
||||||
|
} catch (e) {
|
||||||
|
// ignore schema load errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await fetch('api/config', {cache: 'no-cache'});
|
||||||
|
if (r.status === 410) {
|
||||||
|
alert('Config file is not set');
|
||||||
|
} else if (r.status === 404) {
|
||||||
|
editor.setValue(''); // config file not exist
|
||||||
|
} else if (r.ok) {
|
||||||
|
dump = await r.text();
|
||||||
|
editor.setValue(dump);
|
||||||
|
} else {
|
||||||
|
alert(`Unknown error: ${r.statusText} (${r.status})`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+477
@@ -0,0 +1,477 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "go2rtc",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"definitions": {
|
||||||
|
"listen": {
|
||||||
|
"type": "string",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": ":[0-9]{1,5}$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"const": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"log_level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"trace",
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"warn",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"api": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"default": ":1984",
|
||||||
|
"examples": [
|
||||||
|
"127.0.0.1:8080"
|
||||||
|
],
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"local_auth": {
|
||||||
|
"description": "Will use Auth header from local network",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"base_path": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"static_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "*"
|
||||||
|
},
|
||||||
|
"tls_listen": {
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
},
|
||||||
|
"tls_cert": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"/etc/ssl/certs/my_certificate.pem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tls_key": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"/etc/ssl/private/my_certificate_key.pem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unix_listen": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"/tmp/go2rtc.sock"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bin": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "ffmpeg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"description": "FFmpeg template",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hass": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"/config/go2rtc.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"default": ":8124",
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"homekit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pin": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "123-45-678"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "go2rtc"
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"device_private": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"category_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 2
|
||||||
|
},
|
||||||
|
"pairings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "color",
|
||||||
|
"enum": [
|
||||||
|
"color",
|
||||||
|
"json",
|
||||||
|
"text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"description": "Defaul log level",
|
||||||
|
"default": "info",
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "stdout",
|
||||||
|
"enum": [
|
||||||
|
"",
|
||||||
|
"stdout",
|
||||||
|
"stderr"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "UNIXMS",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"",
|
||||||
|
"UNIXMS",
|
||||||
|
"UNIXMICRO",
|
||||||
|
"UNIXNANO",
|
||||||
|
"2006-01-02T15:04:05Z07:00",
|
||||||
|
"2006-01-02T15:04:05.999999999Z07:00"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"echo": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"exec": {
|
||||||
|
"description": "Value `exec: debug` will print stderr",
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"expr": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"description": "Will only be displayed with `exec: debug` setting",
|
||||||
|
"default": "error",
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"hass": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"hls": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"homekit": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"mp4": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"ngrok": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"onvif": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"rtmp": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"rtsp": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"streams": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"webrtc": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
},
|
||||||
|
"webtorrent": {
|
||||||
|
"$ref": "#/definitions/log_level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ngrok": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"command": {
|
||||||
|
"description": "ngrok start --all --config ngrok.yaml",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx",
|
||||||
|
"rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rtmp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"examples": [
|
||||||
|
":1935"
|
||||||
|
],
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rtsp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"default": ":8554",
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default_query": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"pkt_size": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"srtp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"default": ":8443",
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"streams": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"title": "Stream",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"description": "Source",
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
|
||||||
|
"rtsp://username:password@192.168.1.123/stream1",
|
||||||
|
"rtsp://username:password@192.168.1.123/h264Preview_01_main",
|
||||||
|
"rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password",
|
||||||
|
"http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password",
|
||||||
|
"http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1",
|
||||||
|
"ffmpeg:media.mp4#video=h264#hardware#width=1920#height=1080#rotate=180#audio=copy",
|
||||||
|
"ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M",
|
||||||
|
"bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0",
|
||||||
|
"dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0",
|
||||||
|
"exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}",
|
||||||
|
"isapi://username:password@192.168.1.123:80/",
|
||||||
|
"kasa://username:password@192.168.1.123:19443/https/stream/mixed",
|
||||||
|
"onvif://username:password@192.168.1.123:80?subtype=0",
|
||||||
|
"tapo://password@192.168.1.123:8800?channel=0&subtype=0",
|
||||||
|
"webtorrent:?share=xxx&pwd=xxx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"description": "Source",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webrtc": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"listen": {
|
||||||
|
"default": ":8555",
|
||||||
|
"$ref": "#/definitions/listen"
|
||||||
|
},
|
||||||
|
"candidates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"192.168.1.123",
|
||||||
|
"stun:stun.l.google.com:19302"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ice_servers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"urls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"examples": [
|
||||||
|
"stun:stun.l.google.com:19302",
|
||||||
|
"turn:123.123.123.123:3478"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"credential": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"candidates": {
|
||||||
|
"description": "Keep only these candidates",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interfaces": {
|
||||||
|
"description": "Keep only these interfaces",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ips": {
|
||||||
|
"description": "Keep only these IP-addresses",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"description": "Use only these network types",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"enum": [
|
||||||
|
"tcp4",
|
||||||
|
"tcp6",
|
||||||
|
"udp4",
|
||||||
|
"udp6"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"udp_ports": {
|
||||||
|
"description": "Use only these UDP ports range [min, max]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"maxItems": 2,
|
||||||
|
"minItems": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webtorrent": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"trackers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shares": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pwd": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"src": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ import "embed"
|
|||||||
|
|
||||||
//go:embed *.html
|
//go:embed *.html
|
||||||
//go:embed *.js
|
//go:embed *.js
|
||||||
|
//go:embed *.json
|
||||||
var Static embed.FS
|
var Static embed.FS
|
||||||
|
|||||||
Reference in New Issue
Block a user