Compare commits

...

8 Commits

Author SHA1 Message Date
Alexey Khit d1b29275d7 Adds API for create and delete stream 2022-08-21 09:29:44 +03:00
Alexey Khit 7560bcbc83 Adds log info about serve static dir 2022-08-21 09:29:20 +03:00
Alexey Khit 090c360747 Adds fast script for building linux/amd64 2022-08-21 09:28:47 +03:00
Alexey Khit a81bf0daa8 Update web interface 2022-08-21 09:28:26 +03:00
Alexey Khit c7128897b8 Fix webrtc ontrack panic 2022-08-21 09:27:33 +03:00
Alexey Khit 07def5ba04 Adds restarts support to docker container 2022-08-21 09:27:02 +03:00
Alexey Khit b7f4c63517 Update exec timeout to 15 2022-08-21 06:56:43 +03:00
Alexey Khit 92c67df7b4 Rewrite ffmpeg query format 2022-08-21 06:56:24 +03:00
13 changed files with 168 additions and 39 deletions
+3 -3
View File
@@ -130,7 +130,7 @@ streams:
You can get any stream or file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
Format: `ffmpeg:{input}#{params}`. Examples:
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
```yaml
streams:
@@ -141,7 +141,7 @@ streams:
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy&audio=pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy#audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
@@ -150,7 +150,7 @@ streams:
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
# [RTSP] video and audio will be copied
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy&audio=copy
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=copy#audio=copy
```
All trascoding formats has built-in templates. But you can override them via YAML config. You can also add your own formats to config and use them with source params.
+4 -1
View File
@@ -16,4 +16,7 @@ RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
&& unzip ngrok
CMD [ "/app/go2rtc", "-config", "/config/go2rtc.yaml" ]
COPY run.sh /
RUN chmod a+x /run.sh
CMD [ "/run.sh" ]
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/with-contenv bashio
set +e
while true; do
if [ -x /config/go2rtc ]; then
/config/go2rtc -config /config/go2rtc.yaml
else
/app/go2rtc -config /config/go2rtc.yaml
fi
sleep 5
done
+29 -6
View File
@@ -9,6 +9,8 @@ import (
"github.com/rs/zerolog"
"net"
"net/http"
"os"
"strconv"
)
func Init() {
@@ -38,7 +40,8 @@ func Init() {
HandleFunc("/api/frame.mp4", frameHandler)
HandleFunc("/api/frame.raw", frameHandler)
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/stats", statsHandler)
HandleFunc("/api/streams", streamsHandler)
HandleFunc("/api/exit", exitHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors
@@ -69,19 +72,39 @@ var basePath string
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func statsHandler(w http.ResponseWriter, _ *http.Request) {
v := map[string]interface{}{
"streams": streams.All(),
func streamsHandler(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
switch r.Method {
case "PUT":
streams.Get(src)
return
case "DELETE":
streams.Delete(src)
return
}
var v interface{}
if src != "" {
v = streams.Get(src)
} else {
v = streams.All()
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.stats] marshal")
log.Error().Err(err).Msg("[api.streams] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.stats] write")
log.Error().Err(err).Msg("[api.streams] write")
}
}
func exitHandler(w http.ResponseWriter, r *http.Request) {
s := r.URL.Query().Get("code")
code, _ := strconv.Atoi(s)
os.Exit(code)
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {
+1
View File
@@ -8,6 +8,7 @@ import (
func initStatic(staticDir string) {
var root http.FileSystem
if staticDir != "" {
log.Info().Str("dir", staticDir).Msg("[api] serve static")
root = http.Dir(staticDir)
} else {
root = http.FS(www.Static)
+1 -1
View File
@@ -70,7 +70,7 @@ func Handle(url string) (streamer.Producer, error) {
}
select {
case <-time.After(time.Second * 10):
case <-time.After(time.Second * 15):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
+14 -1
View File
@@ -54,7 +54,7 @@ func Init() {
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query, _ = url.ParseQuery(s[i+1:])
query = parseQuery(s[i+1:])
s = s[:i]
}
@@ -110,3 +110,16 @@ func Init() {
return exec.Handle(s)
})
}
func parseQuery(s string) map[string][]string {
query := map[string][]string{}
for _, key := range strings.Split(s, "#") {
var value string
i := strings.IndexByte(key, '=')
if i > 0 {
key, value = key[:i], key[i+1:]
}
query[key] = append(query[key], value)
}
return query
}
+4
View File
@@ -34,6 +34,10 @@ func Get(name string) *Stream {
return nil
}
func Delete(name string) {
delete(streams, name)
}
func All() map[string]interface{} {
all := map[string]interface{}{}
for name, stream := range streams {
+3 -1
View File
@@ -1,6 +1,7 @@
package webrtc
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3"
)
@@ -57,7 +58,8 @@ func (c *Conn) Init() {
}
}
panic("something wrong")
fmt.Printf("TODO: webrtc ontrack %+v\n", remote)
fmt.Printf("TODO: webrtc ontrack %#v\n", remote)
})
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
+4
View File
@@ -0,0 +1,4 @@
@SET GOOS=linux
@SET GOARCH=amd64
cd ..
go build -ldflags "-s -w" -trimpath && upx-3.96 go2rtc
+5 -1
View File
@@ -46,4 +46,8 @@ pc.ontrack = ev => {
video.srcObject = ev.streams[0];
}
```
```
## Useful links
- https://divtable.com/table-styler/
+87 -25
View File
@@ -1,44 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>go2rtc</title>
<style>
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
table tbody td {
font-size: 13px;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
.header {
padding: 5px 5px;
}
</style>
</head>
<body>
<div id="header"></div>
<table id="items"></table>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
</div>
<table id="streams">
<thead>
<tr>
<th>Name</th>
<th>Online</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const header = document.getElementById('header');
header.innerHTML = `<a href="api/stats">stats</a>`;
const links = [
'<a href="webrtc-async.html?url={name}">webrtc-async</a>',
// '<a href="webrtc-sync.html?url={name}">webrtc-sync</a>',
'<a href="api/frame.mp4?url={name}">frame.mp4</a>',
'<a href="api/frame.raw?url={name}">frame.raw</a>',
'<a href="webrtc.html?url={name}">webrtc</a>',
'<a href="mse.html?url={name}">mse</a>',
'<a href="api/frame.mp4?url={name}">frame.mp4</a>',
'<a href="api/streams?src={name}">info</a>',
];
fetch(`${baseUrl}/api/stats`).then(r => {
r.json().then(data => {
const content = document.getElementById('items');
function reload() {
fetch(`${baseUrl}/api/streams`).then(r => {
r.json().then(data => {
let html = '';
for (let name in data.streams) {
let html = `<tr><td>${name || 'default'}</td>`;
links.forEach(link => {
html += `<td>${link.replace('{name}', name)}</td>`
})
html += `</tr>`;
content.innerHTML += html
}
});
})
for (const [name, value] of Object.entries(data)) {
const online = value !== null ? value.length : 0
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`;
links.forEach(link => {
html += link.replace('{name}', encodeURIComponent(name)) + ' ';
})
html += `<a href="#" onclick="deleteStream('${name}')">delete</a>`;
html += `</td></tr>`;
}
let content = document.getElementById('streams').getElementsByTagName('tbody')[0];
content.innerHTML = html
});
})
}
function deleteStream(src) {
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src)}`, {method: 'DELETE'}).then(reload);
}
const addButton = document.querySelector('a#add');
addButton.onclick = () => {
let src = document.querySelector('input#src');
fetch(`${baseUrl}/api/streams?src=${encodeURIComponent(src.value)}`, {method: 'PUT'}).then(reload);
}
reload();
</script>
</body>
</html>