mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-10-18 22:24:38 +08:00
Adds support local USB cameras
This commit is contained in:
@@ -49,7 +49,7 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
)
|
||||
|
||||
// remove `exec:`
|
||||
args := strings.Split(url[5:], " ")
|
||||
args := QuoteSplit(url[5:])
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
@@ -83,3 +83,39 @@ func Handle(url string) (streamer.Producer, error) {
|
||||
|
||||
var log zerolog.Logger
|
||||
var waiters map[string]chan streamer.Producer
|
||||
|
||||
func QuoteSplit(s string) []string {
|
||||
var a []string
|
||||
|
||||
for len(s) > 0 {
|
||||
is := strings.IndexByte(s, ' ')
|
||||
if is >= 0 {
|
||||
// skip prefix and double spaces
|
||||
if is == 0 {
|
||||
// goto next symbol
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// check if quote in word
|
||||
if i := strings.IndexByte(s[:is], '"'); i >= 0 {
|
||||
// search quote end
|
||||
if is = strings.Index(s, `" `); is > 0 {
|
||||
is += 1
|
||||
} else {
|
||||
is = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if is >= 0 {
|
||||
a = append(a, strings.ReplaceAll(s[:is], `"`, ""))
|
||||
s = s[is+1:]
|
||||
} else {
|
||||
//add last word
|
||||
a = append(a, s)
|
||||
break
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
@@ -1,6 +1,41 @@
|
||||
## Devices Windows
|
||||
|
||||
```
|
||||
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
|
||||
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
|
||||
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
|
||||
```
|
||||
|
||||
## Devices Mac
|
||||
|
||||
```
|
||||
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
|
||||
```
|
||||
|
||||
## Devices Linux
|
||||
|
||||
```
|
||||
# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0
|
||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
|
||||
- https://codec.fandom.com/ru/wiki/X264_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F
|
||||
- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования
|
||||
- https://html5test.com/
|
||||
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||
|
63
cmd/ffmpeg/device_darwin.go
Normal file
63
cmd/ffmpeg/device_darwin.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f avfoundation"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `"` + video.Title + `:` + audio.Title + `"`
|
||||
case video != nil:
|
||||
return `"` + video.Title + `"`
|
||||
case audio != nil:
|
||||
return `"` + audio.Title + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
tpl["bin"], "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
var kind string
|
||||
|
||||
lines := strings.Split(buf.String(), "\n")
|
||||
process:
|
||||
for _, line := range lines {
|
||||
switch {
|
||||
case strings.HasSuffix(line, "video devices:"):
|
||||
kind = streamer.KindVideo
|
||||
continue
|
||||
case strings.HasSuffix(line, "audio devices:"):
|
||||
kind = streamer.KindAudio
|
||||
continue
|
||||
case strings.HasPrefix(line, "dummy"):
|
||||
break process
|
||||
}
|
||||
|
||||
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
|
||||
name := line[42:]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
return &streamer.Media{
|
||||
Kind: kind, Title: name,
|
||||
}
|
||||
}
|
36
cmd/ffmpeg/device_linux.go
Normal file
36
cmd/ffmpeg/device_linux.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog/log"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
const deviceInputPrefix = "-f v4l2"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
return video.Title
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
files, err := ioutil.ReadDir("/dev")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
log.Trace().Msg("[ffmpeg] " + file.Name())
|
||||
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
|
||||
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
return &streamer.Media{
|
||||
Kind: kind, Title: name,
|
||||
}
|
||||
}
|
59
cmd/ffmpeg/device_windows.go
Normal file
59
cmd/ffmpeg/device_windows.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
||||
const deviceInputPrefix = "-f dshow"
|
||||
|
||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
||||
video := findMedia(streamer.KindVideo, videoIdx)
|
||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
||||
switch {
|
||||
case video != nil && audio != nil:
|
||||
return `video="` + video.Title + `":audio=` + audio.Title + `"`
|
||||
case video != nil:
|
||||
return `video="` + video.Title + `"`
|
||||
case audio != nil:
|
||||
return `audio="` + audio.Title + `"`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func loadMedias() {
|
||||
cmd := exec.Command(
|
||||
tpl["bin"], "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stderr = &buf
|
||||
_ = cmd.Run()
|
||||
|
||||
lines := strings.Split(buf.String(), "\r\n")
|
||||
for _, line := range lines {
|
||||
var kind string
|
||||
if strings.HasSuffix(line, "(video)") {
|
||||
kind = streamer.KindVideo
|
||||
} else if strings.HasSuffix(line, "(audio)") {
|
||||
kind = streamer.KindAudio
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
// hope we have constant prefix and suffix sizes
|
||||
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
|
||||
name := line[28 : len(line)-9]
|
||||
media := loadMedia(kind, name)
|
||||
medias = append(medias, media)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMedia(kind, name string) *streamer.Media {
|
||||
return &streamer.Media{
|
||||
Kind: kind, Title: name,
|
||||
}
|
||||
}
|
73
cmd/ffmpeg/devices.go
Normal file
73
cmd/ffmpeg/devices.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getDevice(src string) (string, error) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
input := deviceInputPrefix
|
||||
|
||||
var videoIdx, audioIdx int
|
||||
if i := strings.IndexByte(src, '?'); i > 0 {
|
||||
query, err := url.ParseQuery(src[i+1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for key, value := range query {
|
||||
switch key {
|
||||
case "video":
|
||||
videoIdx, _ = strconv.Atoi(value[0])
|
||||
case "audio":
|
||||
audioIdx, _ = strconv.Atoi(value[0])
|
||||
case "framerate":
|
||||
input += " -framerate " + value[0]
|
||||
case "resolution":
|
||||
input += " -video_size " + value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
|
||||
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var medias []*streamer.Media
|
||||
|
||||
func findMedia(kind string, index int) *streamer.Media {
|
||||
for _, media := range medias {
|
||||
if media.Kind != kind {
|
||||
continue
|
||||
}
|
||||
if index == 0 {
|
||||
return media
|
||||
}
|
||||
index--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleDevices(w http.ResponseWriter, r *http.Request) {
|
||||
if medias == nil {
|
||||
loadMedias()
|
||||
}
|
||||
|
||||
data, err := json.Marshal(medias)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(data); err != nil {
|
||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
||||
@@ -20,9 +21,9 @@ func Init() {
|
||||
"bin": "ffmpeg",
|
||||
|
||||
// inputs
|
||||
"link": "-hide_banner -i {input}",
|
||||
"rtsp": "-hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
|
||||
"file": "-hide_banner -re -stream_loop -1 -i {input}",
|
||||
"link": "-i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
|
||||
"file": "-re -stream_loop -1 -i {input}",
|
||||
|
||||
// output
|
||||
"out": "-rtsp_transport tcp -f rtsp {output}",
|
||||
@@ -31,7 +32,8 @@ func Init() {
|
||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||
// `-tune zerolatency` - for minimal latency
|
||||
// `-profile main -level 4.1` - most used streaming profile
|
||||
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1",
|
||||
// `-pix_fmt yuv420p` - if input pix format 4:2:2
|
||||
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p",
|
||||
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
|
||||
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
|
||||
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
|
||||
@@ -47,7 +49,7 @@ func Init() {
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
tpl := cfg.Mod
|
||||
tpl = cfg.Mod
|
||||
|
||||
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
|
||||
s = s[7:] // remove `ffmpeg:`
|
||||
@@ -62,13 +64,15 @@ func Init() {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "rtsp"):
|
||||
template = tpl["rtsp"]
|
||||
case strings.HasPrefix(s, "device"):
|
||||
template, _ = getDevice(s)
|
||||
case strings.Contains(s, "://"):
|
||||
template = tpl["link"]
|
||||
default:
|
||||
template = tpl["file"]
|
||||
}
|
||||
|
||||
s = "exec:" + tpl["bin"] + " " +
|
||||
s = "exec:" + tpl["bin"] + " -hide_banner " +
|
||||
strings.Replace(template, "{input}", s, 1)
|
||||
|
||||
if query != nil {
|
||||
@@ -109,8 +113,12 @@ func Init() {
|
||||
|
||||
return exec.Handle(s)
|
||||
})
|
||||
|
||||
api.HandleFunc("/api/devices", handleDevices)
|
||||
}
|
||||
|
||||
var tpl map[string]string
|
||||
|
||||
func parseQuery(s string) map[string][]string {
|
||||
query := map[string][]string{}
|
||||
for _, key := range strings.Split(s, "#") {
|
||||
|
11
go.mod
11
go.mod
@@ -38,4 +38,13 @@ require (
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
||||
)
|
||||
|
||||
replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
|
||||
replace (
|
||||
// windows support: https://github.com/brutella/dnssd/pull/35
|
||||
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
|
||||
// RTP tlv8 fix
|
||||
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657
|
||||
// MSE update
|
||||
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e
|
||||
// AES_256_CM_HMAC_SHA1_80 support
|
||||
github.com/pion/srtp/v2 v2.0.10 => github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg=
|
||||
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
|
||||
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
@@ -90,8 +92,6 @@ github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0
|
||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
||||
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
|
||||
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
||||
|
@@ -46,12 +46,13 @@ func GetKind(name string) string {
|
||||
// - deepch/vdk/format/rtsp/sdp.Media
|
||||
// - pion/sdp.MediaDescription
|
||||
type Media struct {
|
||||
Kind string // video, audio
|
||||
Direction string
|
||||
Codecs []*Codec
|
||||
Kind string `json:"kind,omitempty"` // video or audio
|
||||
Direction string `json:"direction,omitempty"`
|
||||
Codecs []*Codec `json:"codecs,omitempty"`
|
||||
|
||||
MID string // TODO: fixme?
|
||||
Control string // TODO: fixme?
|
||||
MID string `json:"mid,omitempty"` // TODO: fixme?
|
||||
Control string `json:"control,omitempty"` // TODO: fixme?
|
||||
Title string `json:"title,omitempty"` // TODO: fixme?
|
||||
}
|
||||
|
||||
func (m *Media) String() string {
|
||||
|
82
www/devices.html
Normal file
82
www/devices.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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 class="header">
|
||||
<input id="src" type="text" placeholder="url">
|
||||
<a id="add" href="#">add</a>
|
||||
</div>
|
||||
<table id="devices">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kind</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
const baseUrl = location.origin + location.pathname.substr(
|
||||
0, location.pathname.lastIndexOf("/")
|
||||
);
|
||||
|
||||
function reload() {
|
||||
fetch(`${baseUrl}/api/devices`).then(r => {
|
||||
r.json().then(data => {
|
||||
let html = '';
|
||||
|
||||
data.forEach(function (item) {
|
||||
html += `<tr><td>${item.kind}</td><td>${item.title}</td></tr>`;
|
||||
})
|
||||
|
||||
let content = document.getElementById('devices').getElementsByTagName('tbody')[0];
|
||||
content.innerHTML = html
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
reload();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -45,6 +45,7 @@
|
||||
<div class="header">
|
||||
<input id="src" type="text" placeholder="url">
|
||||
<a id="add" href="#">add</a>
|
||||
<a href="devices.html">devices</a>
|
||||
</div>
|
||||
<table id="streams">
|
||||
<thead>
|
||||
|
Reference in New Issue
Block a user