Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1b29275d7 | |||
| 7560bcbc83 | |||
| 090c360747 | |||
| a81bf0daa8 | |||
| c7128897b8 | |||
| 07def5ba04 | |||
| b7f4c63517 | |||
| 92c67df7b4 | |||
| 64c0f287ed | |||
| d96af31f86 | |||
| cc55281f12 | |||
| c10d619df8 | |||
| 65f451e0c5 | |||
| ecd46700db | |||
| 2f588c77c4 |
@@ -0,0 +1,59 @@
|
||||
# https://github.com/home-assistant/builder
|
||||
name: 'Builder'
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ 'v*' ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
hassio:
|
||||
name: Hassio Addon
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Branch name
|
||||
run: |
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
|
||||
echo "TAG=${VERSION}" >> $GITHUB_ENV
|
||||
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build amd64
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build i386
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build aarch64
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
|
||||
|
||||
- name: Build armv7
|
||||
uses: home-assistant/builder@master
|
||||
with:
|
||||
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
|
||||
|
||||
- name: Docker manifest
|
||||
run: |
|
||||
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
|
||||
docker manifest create "${IMAGE}" \
|
||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
||||
docker manifest push "${IMAGE}"
|
||||
|
||||
docker manifest create "${REPO}:latest" \
|
||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
||||
docker manifest push "${REPO}:latest"
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
|
||||
RUN apk add --no-cache git go ffmpeg
|
||||
|
||||
ARG BUILD_ARCH
|
||||
|
||||
WORKDIR app
|
||||
|
||||
RUN git clone https://github.com/AlexxIT/go2rtc .
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
||||
&& unzip ngrok
|
||||
|
||||
COPY run.sh /
|
||||
RUN chmod a+x /run.sh
|
||||
|
||||
CMD [ "/run.sh" ]
|
||||
@@ -0,0 +1,6 @@
|
||||
# https://github.com/home-assistant/builder/blob/master/builder.sh
|
||||
name: go2rtc
|
||||
description: Ultimate camera streaming application
|
||||
url: https://github.com/AlexxIT/go2rtc
|
||||
image: alexxit/go2rtc
|
||||
arch: [ amd64, aarch64, i386, armv7 ]
|
||||
@@ -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"
|
||||
"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 {
|
||||
|
||||
+10
-7
@@ -8,19 +8,22 @@ import (
|
||||
)
|
||||
|
||||
var stackSkip = [][]byte{
|
||||
// debug.go
|
||||
[]byte("github.com/AlexxIT/go2rtc/cmd/debug.handler"),
|
||||
|
||||
// cmd.go
|
||||
[]byte("github.com/AlexxIT/go2rtc/cmd.Run"),
|
||||
// main.go
|
||||
[]byte("main.main()"),
|
||||
[]byte("created by os/signal.Notify"),
|
||||
|
||||
// api.go
|
||||
// api/stack.go
|
||||
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
|
||||
|
||||
// api/api.go
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
|
||||
[]byte("created by net/http.(*connReader).startBackgroundRead"),
|
||||
[]byte("created by net/http.(*Server).Serve"),
|
||||
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
||||
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||
}
|
||||
|
||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io"
|
||||
@@ -9,7 +10,15 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
data, _ = os.ReadFile("go2rtc.yaml")
|
||||
config := flag.String(
|
||||
"config",
|
||||
"go2rtc.yaml",
|
||||
"Path to go2rtc configuration file",
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
data, _ = os.ReadFile(*config)
|
||||
|
||||
var cfg struct {
|
||||
Mod map[string]string `yaml:"log"`
|
||||
|
||||
+1
-1
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -80,6 +82,15 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: fixme
|
||||
if strings.HasPrefix(url, "rtsp://") {
|
||||
port := ":" + rtsp.Port + "/"
|
||||
i := strings.Index(url, port)
|
||||
if i > 0 {
|
||||
url = url[i+len(port):]
|
||||
}
|
||||
}
|
||||
|
||||
stream := streams.Get(url)
|
||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
||||
if err != nil {
|
||||
|
||||
+10
-5
@@ -34,14 +34,19 @@ func Get(name string) *Stream {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Delete(name string) {
|
||||
delete(streams, name)
|
||||
}
|
||||
|
||||
func All() map[string]interface{} {
|
||||
active := map[string]interface{}{}
|
||||
all := map[string]interface{}{}
|
||||
for name, stream := range streams {
|
||||
if stream.Active() {
|
||||
active[name] = stream
|
||||
}
|
||||
all[name] = stream
|
||||
//if stream.Active() {
|
||||
// all[name] = stream
|
||||
//}
|
||||
}
|
||||
return active
|
||||
return all
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
"github.com/pion/sdp/v3"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var candidates []string
|
||||
|
||||
func AddCandidate(address string) {
|
||||
candidates = append(candidates, address)
|
||||
}
|
||||
|
||||
func addCanditates(answer string) (string, error) {
|
||||
if len(candidates) == 0 {
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
sd := &sdp.SessionDescription{}
|
||||
if err := sd.Unmarshal([]byte(answer)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
md := sd.MediaDescriptions[0]
|
||||
|
||||
_, end := md.Attribute("end-of-candidates")
|
||||
if end {
|
||||
md.Attributes = md.Attributes[:len(md.Attributes)-1]
|
||||
}
|
||||
|
||||
for _, address := range candidates {
|
||||
if strings.HasPrefix(address, "stun:") {
|
||||
ip, err := webrtc.GetPublicIP()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] public IP")
|
||||
continue
|
||||
}
|
||||
address = ip.String() + address[4:]
|
||||
|
||||
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
|
||||
}
|
||||
|
||||
cand, err := webrtc.NewCandidate(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||
continue
|
||||
}
|
||||
|
||||
md.WithPropertyAttribute(cand)
|
||||
}
|
||||
|
||||
if end {
|
||||
md.WithPropertyAttribute("end-of-candidates")
|
||||
}
|
||||
|
||||
data, err := sd.Marshal()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
if ctx.Consumer == nil {
|
||||
return
|
||||
}
|
||||
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
|
||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
|
||||
conn.Push(msg)
|
||||
}
|
||||
}
|
||||
+8
-134
@@ -8,10 +8,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
||||
pion "github.com/pion/webrtc/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
@@ -56,112 +53,15 @@ func Init() {
|
||||
|
||||
candidates = cfg.Mod.Candidates
|
||||
|
||||
api.HandleFunc("/api/webrtc", apiHandler)
|
||||
api.HandleFunc("/api/webrtc/camera", cameraHandler)
|
||||
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
|
||||
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
||||
}
|
||||
|
||||
func AddCandidate(address string) {
|
||||
candidates = append(candidates, address)
|
||||
}
|
||||
|
||||
var Port string
|
||||
var log zerolog.Logger
|
||||
var candidates []string
|
||||
|
||||
var NewPConn func() (*pion.PeerConnection, error)
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
stream := streams.Get(url)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get offer
|
||||
offer, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] read offer")
|
||||
return
|
||||
}
|
||||
|
||||
// create new webrtc instance
|
||||
cons := new(webrtc.Conn)
|
||||
cons.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
||||
return
|
||||
}
|
||||
|
||||
cons.UserAgent = r.UserAgent()
|
||||
cons.Listen(func(msg interface{}) {
|
||||
if msg == streamer.StateNull {
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
})
|
||||
|
||||
if err = stream.AddConsumer(cons); err != nil {
|
||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
||||
return
|
||||
}
|
||||
|
||||
cons.Init()
|
||||
|
||||
// exchange sdp with waiting all candidates
|
||||
answer, err := cons.ExchangeSDP(string(offer), true)
|
||||
|
||||
// send SDP to client
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Error().Err(err).Msg("[api.webrtc] send answer")
|
||||
}
|
||||
}
|
||||
|
||||
func cameraHandler(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
stream := streams.Get(url)
|
||||
if stream == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get offer
|
||||
offer, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] read offer")
|
||||
return
|
||||
}
|
||||
|
||||
// create new webrtc instance
|
||||
conn := new(webrtc.Conn)
|
||||
conn.Conn, err = NewPConn()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
||||
return
|
||||
}
|
||||
|
||||
conn.UserAgent = r.UserAgent()
|
||||
conn.Listen(func(msg interface{}) {
|
||||
switch msg.(type) {
|
||||
case pion.PeerConnectionState:
|
||||
if msg == pion.PeerConnectionStateDisconnected {
|
||||
stream.RemoveConsumer(conn)
|
||||
}
|
||||
case streamer.Track:
|
||||
//stream.AddProducer(conn)
|
||||
}
|
||||
})
|
||||
|
||||
conn.Init()
|
||||
|
||||
// exchange sdp with waiting all candidates
|
||||
answer, err := conn.ExchangeSDP(string(offer), true)
|
||||
|
||||
// send SDP to client
|
||||
if _, err = w.Write([]byte(answer)); err != nil {
|
||||
log.Error().Err(err).Msg("[api.webrtc] send answer")
|
||||
}
|
||||
}
|
||||
|
||||
func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
name := ctx.Request.URL.Query().Get("url")
|
||||
stream := streams.Get(name)
|
||||
@@ -216,7 +116,11 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
|
||||
// exchange sdp without waiting all candidates
|
||||
//answer, err := conn.ExchangeSDP(offer, false)
|
||||
answer, err := conn.GetAnswer()
|
||||
//answer, err := conn.GetAnswer()
|
||||
answer, err := conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = addCanditates(answer)
|
||||
}
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
@@ -229,29 +133,6 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
Type: webrtc.MsgTypeAnswer, Value: answer,
|
||||
})
|
||||
|
||||
for _, address := range candidates {
|
||||
if strings.HasPrefix(address, "stun:") {
|
||||
ip, err := webrtc.GetPublicIP()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] public IP")
|
||||
continue
|
||||
}
|
||||
address = ip.String() + address[4:]
|
||||
|
||||
log.Debug().Str("addr", address).Msg("[webrtc] stun public address")
|
||||
}
|
||||
|
||||
cand, err := webrtc.NewCandidate(address)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
||||
continue
|
||||
}
|
||||
|
||||
conn.Fire(&streamer.Message{
|
||||
Type: webrtc.MsgTypeCandidate, Value: cand,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Consumer = conn
|
||||
}
|
||||
|
||||
@@ -295,6 +176,9 @@ func ExchangeSDP(
|
||||
// exchange sdp without waiting all candidates
|
||||
//answer, err := conn.ExchangeSDP(offer, false)
|
||||
answer, err = conn.GetCompleteAnswer()
|
||||
if err == nil {
|
||||
answer, err = addCanditates(answer)
|
||||
}
|
||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
||||
|
||||
if err != nil {
|
||||
@@ -303,13 +187,3 @@ func ExchangeSDP(
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
if ctx.Consumer == nil {
|
||||
return
|
||||
}
|
||||
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
|
||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
|
||||
conn.Push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-71
@@ -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) {
|
||||
@@ -75,76 +77,6 @@ func (c *Conn) Init() {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
}
|
||||
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//for _, tr := range c.Conn.GetTransceivers() {
|
||||
// switch tr.Direction() {
|
||||
// case webrtc.RTPTransceiverDirectionSendonly:
|
||||
// // disable transceivers if we don't have track
|
||||
// // make direction=inactive
|
||||
// // don't really necessary, but anyway
|
||||
// if tr.Sender() == nil {
|
||||
// if err = tr.Stop(); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// case webrtc.RTPTransceiverDirectionRecvonly:
|
||||
// // TODO: change codecs list
|
||||
// caps := webrtc.RTPCodecCapability{
|
||||
// MimeType: webrtc.MimeTypePCMU,
|
||||
// ClockRate: 8000,
|
||||
// }
|
||||
// codecs := []webrtc.RTPCodecParameters{
|
||||
// {RTPCodecCapability: caps},
|
||||
// }
|
||||
// if err = tr.SetCodecPreferences(codecs); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
var sdAnswer webrtc.SessionDescription
|
||||
sdAnswer, err = c.Conn.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//var sd *sdp.SessionDescription
|
||||
//sd, err = sdAnswer.Unmarshal()
|
||||
//for _, media := range sd.MediaDescriptions {
|
||||
// if media.MediaName.Media != "audio" {
|
||||
// continue
|
||||
// }
|
||||
// for i, attr := range media.Attributes {
|
||||
// if attr.Key == "sendonly" {
|
||||
// attr.Key = "inactive"
|
||||
// media.Attributes[i] = attr
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//var b []byte
|
||||
//b, err = sd.Marshal()
|
||||
//sdAnswer.SDP = string(b)
|
||||
|
||||
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if complete {
|
||||
<-webrtc.GatheringCompletePromise(c.Conn)
|
||||
return c.Conn.LocalDescription().SDP, nil
|
||||
}
|
||||
|
||||
return sdAnswer.SDP, nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetOffer(offer string) (err error) {
|
||||
sdOffer := webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer, SDP: offer,
|
||||
|
||||
@@ -47,6 +47,9 @@ func GetPublicIP() (net.IP, error) {
|
||||
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = c.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
return nil, res.Error
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://divtable.com/table-styler/
|
||||
+87
-25
@@ -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>
|
||||
Reference in New Issue
Block a user