Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ddbb326b4 | |||
| a2e58d928e | |||
| 3c48fb8bea | |||
| 4b0cbb5a73 | |||
| e28b49ea86 | |||
| 5c17d8fcb6 | |||
| e040fb591f | |||
| 140014f2a6 | |||
| 23f72d111e | |||
| f9d5ab9d0a | |||
| 8628c48db8 | |||
| 6e49d51c33 | |||
| 6a61b5234e | |||
| 7a0091777d | |||
| d23d2a7eff |
@@ -19,6 +19,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
esport CGO_ENABLED=0
|
||||||
|
|
||||||
mkdir artifacts
|
mkdir artifacts
|
||||||
export GOOS=windows
|
export GOOS=windows
|
||||||
export GOARCH=amd64
|
export GOARCH=amd64
|
||||||
@@ -63,12 +65,12 @@ jobs:
|
|||||||
|
|
||||||
export GOOS=darwin
|
export GOOS=darwin
|
||||||
export GOARCH=amd64
|
export GOARCH=amd64
|
||||||
export FILENAME=go2rtc_mac_amd64.zip
|
export FILENAME=artifacts/go2rtc_mac_amd64.zip
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
||||||
|
|
||||||
export GOOS=darwin
|
export GOOS=darwin
|
||||||
export GOARCH=arm64
|
export GOARCH=arm64
|
||||||
export FILENAME=go2rtc_mac_arm64.zip
|
export FILENAME=artifacts/go2rtc_mac_arm64.zip
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
||||||
|
|
||||||
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
||||||
|
|||||||
+4
-5
@@ -33,13 +33,12 @@ FROM scratch AS rootfs
|
|||||||
|
|
||||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||||
COPY ./build/docker/run.sh /
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Final image
|
# 3. Final image
|
||||||
FROM base
|
FROM base
|
||||||
|
|
||||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
# Install ffmpeg, tini (for signal handling),
|
||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
RUN apk add --no-cache tini ffmpeg bash curl jq
|
RUN apk add --no-cache tini ffmpeg bash curl jq
|
||||||
|
|
||||||
@@ -55,8 +54,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
|||||||
|
|
||||||
COPY --from=rootfs / /
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
RUN chmod a+x /run.sh && mkdir -p /config
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
VOLUME /config
|
||||||
|
WORKDIR /config
|
||||||
|
|
||||||
CMD ["/run.sh"]
|
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||||
|
|||||||
+29
-7
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ func Init() {
|
|||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
BasePath string `yaml:"base_path"`
|
BasePath string `yaml:"base_path"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
Origin string `yaml:"origin"`
|
Origin string `yaml:"origin"`
|
||||||
@@ -52,14 +55,18 @@ func Init() {
|
|||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
||||||
|
|
||||||
s := http.Server{}
|
s := http.Server{}
|
||||||
s.Handler = http.DefaultServeMux
|
s.Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
s.Handler = middlewareLog(s.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Mod.Origin == "*" {
|
if cfg.Mod.Origin == "*" {
|
||||||
s.Handler = middlewareCORS(s.Handler)
|
s.Handler = middlewareCORS(s.Handler) // 3rd
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Mod.Username != "" {
|
||||||
|
s.Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, s.Handler) // 2nd
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.Trace().Enabled() {
|
||||||
|
s.Handler = middlewareLog(s.Handler) // 1st
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -87,7 +94,22 @@ var log zerolog.Logger
|
|||||||
|
|
||||||
func middlewareLog(next http.Handler) http.Handler {
|
func middlewareLog(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Msgf("[api] %s %s", r.Method, r.URL)
|
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
if !ok || user != username || pass != password {
|
||||||
|
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -9,11 +9,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func configHandler(w http.ResponseWriter, r *http.Request) {
|
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if app.ConfigPath == "" {
|
||||||
|
http.Error(w, "", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
data, err := os.ReadFile(app.ConfigPath)
|
data, err := os.ReadFile(app.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.Error(w, "", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err = w.Write(data); err != nil {
|
if _, err = w.Write(data); err != nil {
|
||||||
|
|||||||
+8
-5
@@ -8,12 +8,13 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "0.1-rc.9"
|
var Version = "1.0.0"
|
||||||
var UserAgent = "go2rtc/" + Version
|
var UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
var ConfigPath string
|
var ConfigPath string
|
||||||
@@ -52,8 +53,10 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ConfigPath != "" {
|
if ConfigPath != "" {
|
||||||
if cwd, err := os.Getwd(); err == nil {
|
if !filepath.IsAbs(ConfigPath) {
|
||||||
ConfigPath = path.Join(cwd, ConfigPath)
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
ConfigPath = filepath.Join(cwd, ConfigPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Info["config_path"] = ConfigPath
|
Info["config_path"] = ConfigPath
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ func NewLogger(format string, level string) zerolog.Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
lvl, err := zerolog.ParseLevel(level)
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
if err != nil || lvl == zerolog.NoLevel {
|
||||||
|
|||||||
+5
-1
@@ -71,7 +71,11 @@ func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
|||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
cons := &mp4.Segment{
|
||||||
|
RemoteAddr: tr.Request.RemoteAddr,
|
||||||
|
UserAgent: tr.Request.UserAgent(),
|
||||||
|
OnlyKeyframe: true,
|
||||||
|
}
|
||||||
|
|
||||||
if codecs, ok := msg.Value.(string); ok {
|
if codecs, ok := msg.Value.(string); ok {
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ func tcpHandler(conn *rtsp.Conn) {
|
|||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
||||||
|
|
||||||
|
conn.SessionName = app.UserAgent
|
||||||
|
|
||||||
initMedias(conn)
|
initMedias(conn)
|
||||||
|
|
||||||
if err := stream.AddConsumer(conn); err != nil {
|
if err := stream.AddConsumer(conn); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package httpflv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeNumber byte = iota
|
||||||
|
TypeBoolean
|
||||||
|
TypeString
|
||||||
|
TypeObject
|
||||||
|
TypeEcmaArray = 8
|
||||||
|
TypeObjectEnd = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
var Err = errors.New("amf0 read error")
|
||||||
|
|
||||||
|
// AMF0 spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
|
||||||
|
type AMF0 struct {
|
||||||
|
buf []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReader(b []byte) *AMF0 {
|
||||||
|
return &AMF0{buf: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadMetaData() map[string]interface{} {
|
||||||
|
if b, _ := a.ReadByte(); b != TypeString {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s, _ := a.ReadString(); s != "onMetaData" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := a.ReadByte()
|
||||||
|
switch b {
|
||||||
|
case TypeObject:
|
||||||
|
v, _ := a.ReadObject()
|
||||||
|
return v
|
||||||
|
case TypeEcmaArray:
|
||||||
|
v, _ := a.ReadEcmaArray()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadMap() (map[interface{}]interface{}, error) {
|
||||||
|
dict := make(map[interface{}]interface{})
|
||||||
|
|
||||||
|
for a.pos < len(a.buf) {
|
||||||
|
k, err := a.ReadItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, err := a.ReadItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dict[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadItem() (interface{}, error) {
|
||||||
|
dataType, err := a.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dataType {
|
||||||
|
case TypeNumber:
|
||||||
|
return a.ReadNumber()
|
||||||
|
|
||||||
|
case TypeBoolean:
|
||||||
|
v, err := a.ReadByte()
|
||||||
|
return v != 0, err
|
||||||
|
|
||||||
|
case TypeString:
|
||||||
|
return a.ReadString()
|
||||||
|
|
||||||
|
case TypeObject:
|
||||||
|
return a.ReadObject()
|
||||||
|
|
||||||
|
case TypeObjectEnd:
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadByte() (byte, error) {
|
||||||
|
if a.pos >= len(a.buf) {
|
||||||
|
return 0, Err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := a.buf[a.pos]
|
||||||
|
a.pos++
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadNumber() (float64, error) {
|
||||||
|
if a.pos+8 >= len(a.buf) {
|
||||||
|
return 0, Err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])
|
||||||
|
a.pos += 8
|
||||||
|
return math.Float64frombits(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadString() (string, error) {
|
||||||
|
if a.pos+2 >= len(a.buf) {
|
||||||
|
return "", Err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int(binary.BigEndian.Uint16(a.buf[a.pos:]))
|
||||||
|
a.pos += 2
|
||||||
|
|
||||||
|
if a.pos+size >= len(a.buf) {
|
||||||
|
return "", Err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := string(a.buf[a.pos : a.pos+size])
|
||||||
|
a.pos += size
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadObject() (map[string]interface{}, error) {
|
||||||
|
obj := make(map[string]interface{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
k, err := a.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := a.ReadItem()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if k == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
obj[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AMF0) ReadEcmaArray() (map[string]interface{}, error) {
|
||||||
|
if a.pos+4 >= len(a.buf) {
|
||||||
|
return nil, Err
|
||||||
|
}
|
||||||
|
a.pos += 4 // skip size
|
||||||
|
|
||||||
|
return a.ReadObject()
|
||||||
|
}
|
||||||
+127
-21
@@ -2,8 +2,9 @@ package httpflv
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"bytes"
|
||||||
"github.com/deepch/vdk/av"
|
"github.com/deepch/vdk/av"
|
||||||
|
"github.com/deepch/vdk/codec/aacparser"
|
||||||
"github.com/deepch/vdk/codec/h264parser"
|
"github.com/deepch/vdk/codec/h264parser"
|
||||||
"github.com/deepch/vdk/format/flv/flvio"
|
"github.com/deepch/vdk/format/flv/flvio"
|
||||||
"github.com/deepch/vdk/utils/bits/pio"
|
"github.com/deepch/vdk/utils/bits/pio"
|
||||||
@@ -41,8 +42,12 @@ func Accept(res *http.Response) (*Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags&flvio.FILE_HAS_VIDEO == 0 {
|
if flags&flvio.FILE_HAS_VIDEO != 0 {
|
||||||
return nil, errors.New("not supported")
|
c.videoIdx = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags&flvio.FILE_HAS_AUDIO != 0 {
|
||||||
|
c.audioIdx = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = c.reader.Discard(n); err != nil {
|
if _, err = c.reader.Discard(n); err != nil {
|
||||||
@@ -56,26 +61,80 @@ type Conn struct {
|
|||||||
conn io.ReadCloser
|
conn io.ReadCloser
|
||||||
reader *bufio.Reader
|
reader *bufio.Reader
|
||||||
buf []byte
|
buf []byte
|
||||||
|
|
||||||
|
videoIdx int8
|
||||||
|
audioIdx int8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Streams() ([]av.CodecData, error) {
|
func (c *Conn) Streams() ([]av.CodecData, error) {
|
||||||
for {
|
var video, audio av.CodecData
|
||||||
|
|
||||||
|
// Normal software sends:
|
||||||
|
// 1. Video/audio flag in header
|
||||||
|
// 2. MetaData as first tag (with video/audio codec info)
|
||||||
|
// 3. Video/audio headers in 2nd and 3rd tag
|
||||||
|
|
||||||
|
// Reolink camera sends:
|
||||||
|
// 1. Empty video/audio flag
|
||||||
|
// 2. MedaData without stereo key for AAC
|
||||||
|
// 3. Audio header after Video keyframe tag
|
||||||
|
|
||||||
|
waitVideo := c.videoIdx != 0
|
||||||
|
waitAudio := c.audioIdx != 0
|
||||||
|
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
tag, _, err := flvio.ReadTag(c.reader, c.buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AAC_SEQHDR {
|
//log.Printf("[FLV] type=%d avc=%d aac=%d video=%t audio=%t", tag.Type, tag.AVCPacketType, tag.AACPacketType, video != nil, audio != nil)
|
||||||
continue
|
|
||||||
|
switch tag.Type {
|
||||||
|
case flvio.TAG_SCRIPTDATA:
|
||||||
|
if meta := NewReader(tag.Data).ReadMetaData(); meta != nil {
|
||||||
|
waitVideo = meta["videocodecid"] != nil
|
||||||
|
|
||||||
|
// don't wait audio tag because parse all info from MetaData
|
||||||
|
waitAudio = false
|
||||||
|
|
||||||
|
audio = parseAudioConfig(meta)
|
||||||
|
} else {
|
||||||
|
waitVideo = bytes.Contains(tag.Data, []byte("videocodecid"))
|
||||||
|
waitAudio = bytes.Contains(tag.Data, []byte("audiocodecid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
case flvio.TAG_VIDEO:
|
||||||
|
if tag.AVCPacketType == flvio.AVC_SEQHDR {
|
||||||
|
video, _ = h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
||||||
|
}
|
||||||
|
waitVideo = false
|
||||||
|
|
||||||
|
case flvio.TAG_AUDIO:
|
||||||
|
if tag.SoundFormat == flvio.SOUND_AAC && tag.AACPacketType == flvio.AAC_SEQHDR {
|
||||||
|
audio, _ = aacparser.NewCodecDataFromMPEG4AudioConfigBytes(tag.Data)
|
||||||
|
}
|
||||||
|
waitAudio = false
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := h264parser.NewCodecDataFromAVCDecoderConfRecord(tag.Data)
|
if !waitVideo && !waitAudio {
|
||||||
if err != nil {
|
break
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return []av.CodecData{stream}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if video != nil && audio != nil {
|
||||||
|
c.videoIdx = 0
|
||||||
|
c.audioIdx = 1
|
||||||
|
return []av.CodecData{video, audio}, nil
|
||||||
|
} else if video != nil {
|
||||||
|
c.videoIdx = 0
|
||||||
|
return []av.CodecData{video}, nil
|
||||||
|
} else if audio != nil {
|
||||||
|
c.audioIdx = 0
|
||||||
|
return []av.CodecData{audio}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) ReadPacket() (av.Packet, error) {
|
func (c *Conn) ReadPacket() (av.Packet, error) {
|
||||||
@@ -85,20 +144,67 @@ func (c *Conn) ReadPacket() (av.Packet, error) {
|
|||||||
return av.Packet{}, err
|
return av.Packet{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag.Type != flvio.TAG_VIDEO || tag.AVCPacketType != flvio.AVC_NALU {
|
switch tag.Type {
|
||||||
continue
|
case flvio.TAG_VIDEO:
|
||||||
}
|
if tag.AVCPacketType != flvio.AVC_NALU {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
return av.Packet{
|
return av.Packet{
|
||||||
Idx: 0,
|
Idx: c.videoIdx,
|
||||||
Data: tag.Data,
|
Data: tag.Data,
|
||||||
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
CompositionTime: flvio.TsToTime(tag.CompositionTime),
|
||||||
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
IsKeyFrame: tag.FrameType == flvio.FRAME_KEY,
|
||||||
Time: flvio.TsToTime(ts),
|
Time: flvio.TsToTime(ts),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
case flvio.TAG_AUDIO:
|
||||||
|
if tag.SoundFormat != flvio.SOUND_AAC || tag.AACPacketType != flvio.AAC_RAW {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return av.Packet{Idx: c.audioIdx, Data: tag.Data, Time: flvio.TsToTime(ts)}, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Close() (err error) {
|
func (c *Conn) Close() (err error) {
|
||||||
return c.conn.Close()
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAudioConfig(meta map[string]interface{}) av.CodecData {
|
||||||
|
if meta["audiocodecid"] != float64(10) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config := aacparser.MPEG4AudioConfig{
|
||||||
|
ObjectType: aacparser.AOT_AAC_LC,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := meta["audiosamplerate"].(type) {
|
||||||
|
case float64:
|
||||||
|
config.SampleRate = int(v)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch meta["stereo"] {
|
||||||
|
case true:
|
||||||
|
config.ChannelConfig = 2
|
||||||
|
config.ChannelLayout = av.CH_STEREO
|
||||||
|
default:
|
||||||
|
// Reolink doesn't have this setting
|
||||||
|
config.ChannelConfig = 1
|
||||||
|
config.ChannelLayout = av.CH_MONO
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
if err := aacparser.WriteMPEG4AudioConfig(buf, config); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return aacparser.CodecData{
|
||||||
|
Config: config,
|
||||||
|
ConfigBytes: buf.Bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+21
-1
@@ -1,18 +1,25 @@
|
|||||||
package mp4
|
package mp4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h264"
|
"github.com/AlexxIT/go2rtc/pkg/h264"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/h265"
|
"github.com/AlexxIT/go2rtc/pkg/h265"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Segment struct {
|
type Segment struct {
|
||||||
streamer.Element
|
streamer.Element
|
||||||
|
|
||||||
Medias []*streamer.Media
|
Medias []*streamer.Media
|
||||||
|
UserAgent string
|
||||||
|
RemoteAddr string
|
||||||
|
|
||||||
MimeType string
|
MimeType string
|
||||||
OnlyKeyframe bool
|
OnlyKeyframe bool
|
||||||
|
|
||||||
|
send uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Segment) GetMedias() []*streamer.Media {
|
func (c *Segment) GetMedias() []*streamer.Media {
|
||||||
@@ -56,6 +63,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := muxer.Marshal(0, packet)
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(append(init, buf...))
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -73,6 +81,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
buf = append(buf, b...)
|
buf = append(buf, b...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(buf)
|
c.Fire(buf)
|
||||||
|
|
||||||
buf = buf[:0]
|
buf = buf[:0]
|
||||||
@@ -106,6 +115,7 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := muxer.Marshal(0, packet)
|
buf := muxer.Marshal(0, packet)
|
||||||
|
atomic.AddUint32(&c.send, uint32(len(buf)))
|
||||||
c.Fire(append(init, buf...))
|
c.Fire(append(init, buf...))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -121,3 +131,13 @@ func (c *Segment) AddTrack(media *streamer.Media, track *streamer.Track) *stream
|
|||||||
|
|
||||||
panic("unsupported codec")
|
panic("unsupported codec")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Segment) MarshalJSON() ([]byte, error) {
|
||||||
|
info := &streamer.Info{
|
||||||
|
Type: "WS/MP4 client",
|
||||||
|
RemoteAddr: c.RemoteAddr,
|
||||||
|
UserAgent: c.UserAgent,
|
||||||
|
Send: atomic.LoadUint32(&c.send),
|
||||||
|
}
|
||||||
|
return json.Marshal(info)
|
||||||
|
}
|
||||||
|
|||||||
+10
-3
@@ -78,6 +78,7 @@ type Conn struct {
|
|||||||
// public
|
// public
|
||||||
|
|
||||||
Backchannel bool
|
Backchannel bool
|
||||||
|
SessionName string
|
||||||
|
|
||||||
Medias []*streamer.Media
|
Medias []*streamer.Media
|
||||||
Session string
|
Session string
|
||||||
@@ -618,7 +619,7 @@ func (c *Conn) Accept() error {
|
|||||||
medias = append(medias, media)
|
medias = append(medias, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body, err = streamer.MarshalSDP(medias)
|
res.Body, err = streamer.MarshalSDP(c.SessionName, medias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -654,6 +655,12 @@ func (c *Conn) Accept() error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
case MethodTeardown:
|
||||||
|
res := &tcp.Response{Request: req}
|
||||||
|
_ = c.Response(res)
|
||||||
|
c.state = StateNone
|
||||||
|
return c.conn.Close()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported method: %s", req.Method)
|
return fmt.Errorf("unsupported method: %s", req.Method)
|
||||||
}
|
}
|
||||||
@@ -792,12 +799,12 @@ func (c *Conn) Handle() (err error) {
|
|||||||
msg := &RTCP{Channel: channelID}
|
msg := &RTCP{Channel: channelID}
|
||||||
|
|
||||||
if err = msg.Header.Unmarshal(buf); err != nil {
|
if err = msg.Header.Unmarshal(buf); err != nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.Packets, err = rtcp.Unmarshal(buf)
|
msg.Packets, err = rtcp.Unmarshal(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Fire(msg)
|
c.Fire(msg)
|
||||||
|
|||||||
+16
-2
@@ -183,8 +183,22 @@ func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
|
|||||||
return medias, nil
|
return medias, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalSDP(medias []*Media) ([]byte, error) {
|
func MarshalSDP(name string, medias []*Media) ([]byte, error) {
|
||||||
sd := &sdp.SessionDescription{}
|
sd := &sdp.SessionDescription{
|
||||||
|
Origin: sdp.Origin{
|
||||||
|
Username: "-", SessionID: 1, SessionVersion: 1,
|
||||||
|
NetworkType: "IN", AddressType: "IP4", UnicastAddress: "0.0.0.0",
|
||||||
|
},
|
||||||
|
SessionName: sdp.SessionName(name),
|
||||||
|
ConnectionInformation: &sdp.ConnectionInformation{
|
||||||
|
NetworkType: "IN", AddressType: "IP4", Address: &sdp.Address{
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TimeDescriptions: []sdp.TimeDescription{
|
||||||
|
{Timing: sdp.Timing{}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
payloadType := uint8(96)
|
payloadType := uint8(96)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package streamer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pion/sdp/v3"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSDP(t *testing.T) {
|
||||||
|
medias := []*Media{{
|
||||||
|
Kind: KindAudio, Direction: DirectionSendonly,
|
||||||
|
Codecs: []*Codec{
|
||||||
|
{Name: CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
data, err := MarshalSDP("go2rtc/1.0.0", medias)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
|
||||||
|
sd := &sdp.SessionDescription{}
|
||||||
|
err = sd.Unmarshal(data)
|
||||||
|
assert.Empty(t, err)
|
||||||
|
}
|
||||||
+1
-1
@@ -58,7 +58,7 @@
|
|||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
|
|
||||||
fetch(`${baseUrl}/api/devices`)
|
fetch(`${baseUrl}/api/devices`, {cache: 'no-cache'})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.querySelector("body > table > tbody").innerHTML =
|
document.querySelector("body > table > tbody").innerHTML =
|
||||||
|
|||||||
+13
-3
@@ -41,7 +41,7 @@
|
|||||||
method: 'POST', body: editor.getValue()
|
method: 'POST', body: editor.getValue()
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
alert("OK");
|
alert('OK');
|
||||||
fetch('api/exit', {method: 'POST'});
|
fetch('api/exit', {method: 'POST'});
|
||||||
} else {
|
} else {
|
||||||
r.text().then(alert);
|
r.text().then(alert);
|
||||||
@@ -50,8 +50,18 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
fetch('api/config').then(r => r.text()).then(data => {
|
fetch('api/config', {cache: 'no-cache'}).then(r => {
|
||||||
editor.setValue(data);
|
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) {
|
||||||
|
r.text().then(data => {
|
||||||
|
editor.setValue(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(`Unknown error: ${r.statusText} (${r.status})`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
0, location.pathname.lastIndexOf("/")
|
0, location.pathname.lastIndexOf("/")
|
||||||
);
|
);
|
||||||
|
|
||||||
fetch(`${baseUrl}/api/homekit`)
|
fetch(`${baseUrl}/api/homekit`, {cache: 'no-cache'})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.querySelector("body > table > tbody").innerHTML =
|
document.querySelector("body > table > tbody").innerHTML =
|
||||||
|
|||||||
+2
-2
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
const url = new URL("api/streams", location.href);
|
const url = new URL("api/streams", location.href);
|
||||||
fetch(url).then(r => r.json()).then(data => {
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(data)) {
|
for (const [name, value] of Object.entries(data)) {
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL("api", location.href);
|
const url = new URL("api", location.href);
|
||||||
fetch(url).then(r => r.json()).then(data => {
|
fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {
|
||||||
const info = document.querySelector(".info");
|
const info = document.querySelector(".info");
|
||||||
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
|
info.innerText = `Version: ${data.version}, Config: ${data.config_path}`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user