Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1b29275d7 | |||
| 7560bcbc83 | |||
| 090c360747 | |||
| a81bf0daa8 | |||
| c7128897b8 | |||
| 07def5ba04 | |||
| b7f4c63517 | |||
| 92c67df7b4 |
@@ -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.
|
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
|
```yaml
|
||||||
streams:
|
streams:
|
||||||
@@ -141,7 +141,7 @@ streams:
|
|||||||
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
|
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
|
||||||
|
|
||||||
# [FILE] video will be copied, audio will be transcoded to pcmu
|
# [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] 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
|
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
|
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] 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.
|
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.
|
||||||
|
|||||||
@@ -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 \
|
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||||
&& unzip ngrok
|
&& unzip ngrok
|
||||||
|
|
||||||
CMD [ "/app/go2rtc", "-config", "/config/go2rtc.yaml" ]
|
COPY run.sh /
|
||||||
|
RUN chmod a+x /run.sh
|
||||||
|
|
||||||
|
CMD [ "/run.sh" ]
|
||||||
|
|||||||
@@ -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
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -38,7 +40,8 @@ func Init() {
|
|||||||
HandleFunc("/api/frame.mp4", frameHandler)
|
HandleFunc("/api/frame.mp4", frameHandler)
|
||||||
HandleFunc("/api/frame.raw", frameHandler)
|
HandleFunc("/api/frame.raw", frameHandler)
|
||||||
HandleFunc("/api/stack", stackHandler)
|
HandleFunc("/api/stack", stackHandler)
|
||||||
HandleFunc("/api/stats", statsHandler)
|
HandleFunc("/api/streams", streamsHandler)
|
||||||
|
HandleFunc("/api/exit", exitHandler)
|
||||||
HandleFunc("/api/ws", apiWS)
|
HandleFunc("/api/ws", apiWS)
|
||||||
|
|
||||||
// ensure we can listen without errors
|
// ensure we can listen without errors
|
||||||
@@ -69,19 +72,39 @@ var basePath string
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
var wsHandlers = make(map[string]WSHandler)
|
var wsHandlers = make(map[string]WSHandler)
|
||||||
|
|
||||||
func statsHandler(w http.ResponseWriter, _ *http.Request) {
|
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
v := map[string]interface{}{
|
src := r.URL.Query().Get("src")
|
||||||
"streams": streams.All(),
|
|
||||||
|
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)
|
data, err := json.Marshal(v)
|
||||||
if err != nil {
|
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 {
|
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) {
|
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := new(Context)
|
ctx := new(Context)
|
||||||
if err := ctx.Upgrade(w, r); err != nil {
|
if err := ctx.Upgrade(w, r); err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
func initStatic(staticDir string) {
|
func initStatic(staticDir string) {
|
||||||
var root http.FileSystem
|
var root http.FileSystem
|
||||||
if staticDir != "" {
|
if staticDir != "" {
|
||||||
|
log.Info().Str("dir", staticDir).Msg("[api] serve static")
|
||||||
root = http.Dir(staticDir)
|
root = http.Dir(staticDir)
|
||||||
} else {
|
} else {
|
||||||
root = http.FS(www.Static)
|
root = http.FS(www.Static)
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ func Handle(url string) (streamer.Producer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 10):
|
case <-time.After(time.Second * 15):
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
log.Error().Str("url", url).Msg("[exec] timeout")
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("timeout")
|
||||||
|
|||||||
+14
-1
@@ -54,7 +54,7 @@ func Init() {
|
|||||||
|
|
||||||
var query url.Values
|
var query url.Values
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
if i := strings.IndexByte(s, '#'); i > 0 {
|
||||||
query, _ = url.ParseQuery(s[i+1:])
|
query = parseQuery(s[i+1:])
|
||||||
s = s[:i]
|
s = s[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,3 +110,16 @@ func Init() {
|
|||||||
return exec.Handle(s)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ func Get(name string) *Stream {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Delete(name string) {
|
||||||
|
delete(streams, name)
|
||||||
|
}
|
||||||
|
|
||||||
func All() map[string]interface{} {
|
func All() map[string]interface{} {
|
||||||
all := map[string]interface{}{}
|
all := map[string]interface{}{}
|
||||||
for name, stream := range streams {
|
for name, stream := range streams {
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,7 @@
|
|||||||
package webrtc
|
package webrtc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/webrtc/v3"
|
"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) {
|
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@SET GOOS=linux
|
||||||
|
@SET GOARCH=amd64
|
||||||
|
cd ..
|
||||||
|
go build -ldflags "-s -w" -trimpath && upx-3.96 go2rtc
|
||||||
+5
-1
@@ -46,4 +46,8 @@ pc.ontrack = ev => {
|
|||||||
|
|
||||||
video.srcObject = ev.streams[0];
|
video.srcObject = ev.streams[0];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://divtable.com/table-styler/
|
||||||
+87
-25
@@ -1,44 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport"
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
|
||||||
<title>go2rtc</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header"></div>
|
<div class="header">
|
||||||
<table id="items"></table>
|
<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>
|
<script>
|
||||||
const baseUrl = location.origin + location.pathname.substr(
|
const baseUrl = location.origin + location.pathname.substr(
|
||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
|
|
||||||
const header = document.getElementById('header');
|
|
||||||
header.innerHTML = `<a href="api/stats">stats</a>`;
|
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
'<a href="webrtc-async.html?url={name}">webrtc-async</a>',
|
'<a href="webrtc.html?url={name}">webrtc</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="mse.html?url={name}">mse</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 => {
|
function reload() {
|
||||||
r.json().then(data => {
|
fetch(`${baseUrl}/api/streams`).then(r => {
|
||||||
const content = document.getElementById('items');
|
r.json().then(data => {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
for (let name in data.streams) {
|
for (const [name, value] of Object.entries(data)) {
|
||||||
let html = `<tr><td>${name || 'default'}</td>`;
|
const online = value !== null ? value.length : 0
|
||||||
links.forEach(link => {
|
html += `<tr><td>${name || 'default'}</td><td>${online}</td><td>`;
|
||||||
html += `<td>${link.replace('{name}', name)}</td>`
|
links.forEach(link => {
|
||||||
})
|
html += link.replace('{name}', encodeURIComponent(name)) + ' ';
|
||||||
html += `</tr>`;
|
})
|
||||||
content.innerHTML += html
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user