diff --git a/README.md b/README.md index 8221467..e49c989 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/rtmp" ) diff --git a/README_CN.md b/README_CN.md index a563209..d9ab9f5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -9,7 +9,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/rtmp" ) diff --git a/example/default/config.yaml b/example/default/config.yaml index 33c780c..9256b7b 100644 --- a/example/default/config.yaml +++ b/example/default/config.yaml @@ -23,7 +23,15 @@ rtmp: pull: pullonsub: live/pull: rtmp://localhost/live/test -# hdl: +# flv: # pull: # pullonstart: -# live/test: /Users/dexter/Movies/jb-demo.flv \ No newline at end of file +# live/test: /Users/dexter/Movies/jb-demo.flv +gb28181: + sip: + listenaddr: + - udp::5060 + pull: + enableregexp: true + pullonsub: + .* : $0 \ No newline at end of file diff --git a/example/default/main.go b/example/default/main.go index 4b51e6b..0deea38 100644 --- a/example/default/main.go +++ b/example/default/main.go @@ -7,8 +7,8 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/console" _ "m7s.live/m7s/v5/plugin/debug" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/gb28181" - _ "m7s.live/m7s/v5/plugin/hdl" _ "m7s.live/m7s/v5/plugin/logrotate" _ "m7s.live/m7s/v5/plugin/mp4" _ "m7s.live/m7s/v5/plugin/preview" diff --git a/example/default/readflv.yaml b/example/default/readflv.yaml index e7726b9..7ca57ae 100644 --- a/example/default/readflv.yaml +++ b/example/default/readflv.yaml @@ -1,6 +1,6 @@ global: loglevel: trace -hdl: +flv: pull: pullonstart: live/test: /Users/dexter/Movies/jb-demo.flv \ No newline at end of file diff --git a/example/default/stress.yaml b/example/default/stress.yaml index 2bac991..b1bf210 100644 --- a/example/default/stress.yaml +++ b/example/default/stress.yaml @@ -17,7 +17,7 @@ rtsp: rtmp: tcp: autolisten: false -hdl: +flv: pull: pullonstart: live/test: /Users/dexter/Movies/002.flv \ No newline at end of file diff --git a/example/multiple/config2.yaml b/example/multiple/config2.yaml index ac37bb1..dd18ae9 100644 --- a/example/multiple/config2.yaml +++ b/example/multiple/config2.yaml @@ -10,7 +10,7 @@ rtsp: enable: false webrtc: enable: false -hdl: +flv: pull: pullonstart: live/test: /Users/dexter/Movies/jb-demo.flv \ No newline at end of file diff --git a/example/multiple/main.go b/example/multiple/main.go index 9eea995..035f93a 100644 --- a/example/multiple/main.go +++ b/example/multiple/main.go @@ -5,7 +5,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/console" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/logrotate" _ "m7s.live/m7s/v5/plugin/rtmp" _ "m7s.live/m7s/v5/plugin/rtsp" diff --git a/example/rtmp-push/config2.yaml b/example/rtmp-push/config2.yaml index 61384c0..704ce9d 100644 --- a/example/rtmp-push/config2.yaml +++ b/example/rtmp-push/config2.yaml @@ -12,7 +12,7 @@ rtmp: push: pushlist: live/test: rtmp://localhost/live/test -hdl: +flv: pull: pullonstart: live/test: /Users/dexter/Movies/jb-demo.flv \ No newline at end of file diff --git a/example/rtmp-push/main.go b/example/rtmp-push/main.go index 773c446..0706b67 100644 --- a/example/rtmp-push/main.go +++ b/example/rtmp-push/main.go @@ -7,7 +7,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/rtmp" ) diff --git a/example/rtsp-pull/config1.yaml b/example/rtsp-pull/config1.yaml index a6f3ce0..9742515 100644 --- a/example/rtsp-pull/config1.yaml +++ b/example/rtsp-pull/config1.yaml @@ -2,7 +2,7 @@ global: loglevel: info tcp: listenaddr: :50050 -hdl: +flv: publish: pubaudio: false pull: diff --git a/example/rtsp-pull/main.go b/example/rtsp-pull/main.go index 5f00dfe..300d2de 100644 --- a/example/rtsp-pull/main.go +++ b/example/rtsp-pull/main.go @@ -7,7 +7,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/rtsp" ) diff --git a/example/rtsp-push/config2.yaml b/example/rtsp-push/config2.yaml index 02e489b..7e12420 100644 --- a/example/rtsp-push/config2.yaml +++ b/example/rtsp-push/config2.yaml @@ -11,7 +11,7 @@ rtsp: push: pushlist: live/test: rtsp://localhost/live/test -hdl: +flv: publish: pubaudio: false pull: diff --git a/example/rtsp-push/main.go b/example/rtsp-push/main.go index 5f00dfe..300d2de 100644 --- a/example/rtsp-push/main.go +++ b/example/rtsp-push/main.go @@ -7,7 +7,7 @@ import ( "m7s.live/m7s/v5" _ "m7s.live/m7s/v5/plugin/debug" - _ "m7s.live/m7s/v5/plugin/hdl" + _ "m7s.live/m7s/v5/plugin/flv" _ "m7s.live/m7s/v5/plugin/rtsp" ) diff --git a/go.mod b/go.mod index 9f2c2b0..60a676d 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/google/gopacket v1.1.19 github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 + github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 github.com/icholy/digest v0.1.22 github.com/pion/interceptor v0.1.29 github.com/pion/rtcp v1.2.14 diff --git a/go.sum b/go.sum index b8b01f3..9e892f4 100644 --- a/go.sum +++ b/go.sum @@ -125,6 +125,8 @@ github.com/hamba/avro/v2 v2.20.1/go.mod h1:xHiKXbISpb3Ovc809XdzWow+XGTn+Oyf/F9aZ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo= +github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM= github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 257a62b..3efcd7c 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -3,10 +3,11 @@ package config import ( "testing" ) + // TestModify 测试动态修改配置文件,比较值是否修改,修改后是否有Modify属性 func TestModify(t *testing.T) { t.Run(t.Name(), func(t *testing.T) { - var defaultValue struct{ + var defaultValue struct { Subscribe } defaultValue.SubAudio = false @@ -34,8 +35,8 @@ func TestModify(t *testing.T) { // TestGlobal 测试全局配置 func TestGlobal(t *testing.T) { t.Run(t.Name(), func(t *testing.T) { - var defaultValue struct{ - Publish + var defaultValue struct { + Publish } var globalValue struct { Publish diff --git a/pkg/config/udp.go b/pkg/config/udp.go index 6794d4f..6354c9e 100644 --- a/pkg/config/udp.go +++ b/pkg/config/udp.go @@ -1,17 +1,55 @@ package config import ( - "context" + "crypto/tls" "net" + "time" ) -type UDPConfig interface { - ListenUDP(context.Context, func(conn *net.UDPConn)) error -} - type UDP struct { ListenAddr string `desc:"监听地址,格式为ip:port,ip 可省略默认监听所有网卡"` CertFile string `desc:"证书文件"` KeyFile string `desc:"私钥文件"` AutoListen bool `default:"true" desc:"是否自动监听"` + listener net.Listener +} + +func (udp *UDP) Listen(handler func(*net.UDPConn)) (err error) { + udp.listener, err = net.Listen("udp", udp.ListenAddr) + if err == nil { + go udp.listen(udp.listener, handler) + } + return +} + +func (udp *UDP) listen(l net.Listener, handler func(conn *net.UDPConn)) { + var tempDelay time.Duration + for { + conn, err := l.Accept() + if err != nil { + if ne, ok := err.(net.Error); ok && !ne.Timeout() { + if tempDelay == 0 { + tempDelay = 5 * time.Millisecond + } else { + tempDelay *= 2 + } + if max := 1 * time.Second; tempDelay > max { + tempDelay = max + } + // slog.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay) + time.Sleep(tempDelay) + continue + } + return + } + var udpConn *net.UDPConn + switch v := conn.(type) { + case *net.UDPConn: + udpConn = v + case *tls.Conn: + udpConn = v.NetConn().(*net.UDPConn) + } + tempDelay = 0 + go handler(udpConn) + } } diff --git a/pkg/event.go b/pkg/event.go index 0555936..1ca8609 100644 --- a/pkg/event.go +++ b/pkg/event.go @@ -4,4 +4,3 @@ type Event[T any] struct { Type string Data T } - diff --git a/pkg/util/index.go b/pkg/util/index.go index dc10b92..923de3c 100644 --- a/pkg/util/index.go +++ b/pkg/util/index.go @@ -32,6 +32,15 @@ func RandomString(length int) string { return string(b) } +func RandomNumString(length int) string { + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + for i := range b { + b[i] = "0123456789"[seededRand.Intn(10)] + } + return string(b) +} + func initFatalLog() *os.File { fatal_log_dir := "./fatal" if _fatal_log := os.Getenv("M7S_FATAL_LOG"); _fatal_log != "" { diff --git a/pkg/util/ring.go b/pkg/util/ring.go index 23acf83..f979cc6 100644 --- a/pkg/util/ring.go +++ b/pkg/util/ring.go @@ -5,7 +5,6 @@ package util // serves as reference to the entire ring. Empty rings are represented // as nil Ring pointers. The zero value for a Ring is a one-element // ring with a nil Value. -// type Ring[T any] struct { next, prev *Ring[T] Value T // for use by client; untouched by this library @@ -35,7 +34,6 @@ func (r *Ring[T]) Prev() *Ring[T] { // Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0) // in the ring and returns that ring element. r must not be empty. -// func (r *Ring[T]) Move(n int) *Ring[T] { if r.next == nil { return r.init() @@ -84,7 +82,6 @@ func NewRing[T any](n int) *Ring[T] { // them creates a single ring with the elements of s inserted // after r. The result points to the element following the // last element of s after insertion. -// func (r *Ring[T]) Link(s *Ring[T]) *Ring[T] { n := r.Next() if s != nil { @@ -102,7 +99,6 @@ func (r *Ring[T]) Link(s *Ring[T]) *Ring[T] { // Unlink removes n % r.Len() elements from the ring r, starting // at r.Next(). If n % r.Len() == 0, r remains unchanged. // The result is the removed subring. r must not be empty. -// func (r *Ring[T]) Unlink(n int) *Ring[T] { if n <= 0 { return nil @@ -112,7 +108,6 @@ func (r *Ring[T]) Unlink(n int) *Ring[T] { // Len computes the number of elements in ring r. // It executes in time proportional to the number of elements. -// func (r *Ring[T]) Len() int { n := 0 if r != nil { @@ -134,4 +129,3 @@ func (r *Ring[T]) Do(f func(*T)) { } } } - diff --git a/pkg/util/xdp_linux.go b/pkg/util/xdp_linux.go index dbc95ed..cf3daa0 100644 --- a/pkg/util/xdp_linux.go +++ b/pkg/util/xdp_linux.go @@ -1,3 +1,5 @@ +//go:build enable_xdp + package util import ( diff --git a/plugin.go b/plugin.go index 4d44646..ba3d5ae 100644 --- a/plugin.go +++ b/plugin.go @@ -252,6 +252,7 @@ func (p *Plugin) Start() { }() } tcpConf := &p.config.TCP + tcphandler, ok := p.handler.(ITCPPlugin) if !ok { tcphandler = p @@ -277,6 +278,24 @@ func (p *Plugin) Start() { } }() } + udpConf := &p.config.UDP + + udpHandler, ok := p.handler.(IUDPPlugin) + if !ok { + udpHandler = p + } + + if udpConf.ListenAddr != "" && udpConf.AutoListen { + p.Info("listen udp", "addr", udpConf.ListenAddr) + go func() { + err := udpConf.Listen(udpHandler.OnUDPConnect) + if err != nil { + p.Error("listen udp", "addr", udpConf.ListenAddr, "error", err) + p.Stop(err) + } + }() + + } } func (p *Plugin) OnInit() error { @@ -305,6 +324,10 @@ func (p *Plugin) OnTCPConnect(conn *net.TCPConn) { p.handler.OnEvent(conn) } +func (p *Plugin) OnUDPConnect(conn *net.UDPConn) { + p.handler.OnEvent(conn) +} + func (p *Plugin) Publish(streamPath string, options ...any) (publisher *Publisher, err error) { publisher = &Publisher{Publish: p.config.Publish} if p.config.EnableAuth { diff --git a/plugin/hdl/READEME.md b/plugin/flv/READEME.md similarity index 88% rename from plugin/hdl/READEME.md rename to plugin/flv/READEME.md index e6821f3..e0ad1c8 100644 --- a/plugin/hdl/READEME.md +++ b/plugin/flv/READEME.md @@ -1,6 +1,6 @@ -# HDL Plugin +# FLV Plugin -The main function of the HDL plugin is to provide access to the HTTP-FLV protocol. +The main function of the FLV plugin is to provide access to the HTTP-FLV protocol. HTTP-FLV protocol (HDL: Http Dynamic Live) is a dynamic streaming media live broadcast protocol, which implements the function of live broadcast of FLV format video on the ordinary HTTP protocol. The meaning of its name can be mainly divided into three parts: @@ -17,14 +17,14 @@ https://github.com/Monibuca/plugin-hdl ## Plugin Introduction ```go import ( - _ "m7s.live/plugin/hdl/v4" + _ "m7s.live/plugin/flv/v4" ) ``` ## Default Plugin Configuration ```yaml -hdl: +flv: pull: # Format: https://m7s.live/guide/config.html#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE ``` @@ -35,12 +35,12 @@ hdl: If the live/test stream already exists in M7S, then HTTP-FLV protocol can be used for playback. If the listening port is not configured, then the global HTTP port is used (default 8080). ```bash -ffplay http://localhost:8080/hdl/live/test.flv +ffplay http://localhost:8080/flv/live/test.flv ``` ### M7S Pull HTTP-FLV Streams from Remote The available API is: -`/hdl/api/pull?target=[HTTP-FLV address]&streamPath=[stream identifier]&save=[0|1|2]` +`/flv/api/pull?target=[HTTP-FLV address]&streamPath=[stream identifier]&save=[0|1|2]` - save meaning: 0 - do not save 1 - save to pullonstart 2 - save to pullonsub - HTTP-FLV address needs to be urlencoded to prevent special characters from affecting parsing diff --git a/plugin/hdl/index.go b/plugin/flv/index.go similarity index 91% rename from plugin/hdl/index.go rename to plugin/flv/index.go index 1236e8f..aaef1f1 100644 --- a/plugin/hdl/index.go +++ b/plugin/flv/index.go @@ -1,4 +1,4 @@ -package plugin_hdl +package plugin_flv import ( "encoding/binary" @@ -9,27 +9,27 @@ import ( "m7s.live/m7s/v5" . "m7s.live/m7s/v5/pkg" - . "m7s.live/m7s/v5/plugin/hdl/pkg" + . "m7s.live/m7s/v5/plugin/flv/pkg" rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg" ) -type HDLPlugin struct { +type FLVPlugin struct { m7s.Plugin } const defaultConfig m7s.DefaultYaml = `publish: speed: 1` -func (p *HDLPlugin) OnInit() error { +func (p *FLVPlugin) OnInit() error { for streamPath, url := range p.GetCommonConf().PullOnStart { go p.Pull(streamPath, url) } return nil } -var _ = m7s.InstallPlugin[HDLPlugin](defaultConfig, NewPullHandler) +var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, NewPullHandler) -func (p *HDLPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) { +func (p *FLVPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) { at, vt := &sub.Publisher.AudioTrack, &sub.Publisher.VideoTrack hasAudio, hasVideo := at.AVTrack != nil && sub.SubAudio, vt.AVTrack != nil && sub.SubVideo var amf rtmp.AMF @@ -70,7 +70,7 @@ func (p *HDLPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) { return } -func (p *HDLPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (p *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".flv") if r.URL.RawQuery != "" { streamPath += "?" + r.URL.RawQuery diff --git a/plugin/hdl/pkg/flv.go b/plugin/flv/pkg/flv.go similarity index 96% rename from plugin/hdl/pkg/flv.go rename to plugin/flv/pkg/flv.go index ae46b24..e47a09c 100644 --- a/plugin/hdl/pkg/flv.go +++ b/plugin/flv/pkg/flv.go @@ -1,4 +1,4 @@ -package hdl +package flv const ( // FLV Tag Type diff --git a/plugin/hdl/pkg/pull.go b/plugin/flv/pkg/pull.go similarity index 92% rename from plugin/hdl/pkg/pull.go rename to plugin/flv/pkg/pull.go index bd8ae7d..0524bae 100644 --- a/plugin/hdl/pkg/pull.go +++ b/plugin/flv/pkg/pull.go @@ -1,4 +1,4 @@ -package hdl +package flv import ( "errors" @@ -13,7 +13,7 @@ import ( rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg" ) -type HDLPuller struct { +type FLVPuller struct { *util.BufReader *util.ScalableMemoryAllocator hasAudio bool @@ -21,17 +21,17 @@ type HDLPuller struct { absTS uint32 //绝对时间戳 } -func NewHDLPuller() *HDLPuller { - return &HDLPuller{ +func NewFLVPuller() *FLVPuller { + return &FLVPuller{ ScalableMemoryAllocator: util.NewScalableMemoryAllocator(1 << 10), } } func NewPullHandler() m7s.PullHandler { - return NewHDLPuller() + return NewFLVPuller() } -func (puller *HDLPuller) Connect(p *m7s.Client) (err error) { +func (puller *FLVPuller) Connect(p *m7s.Client) (err error) { if strings.HasPrefix(p.RemoteURL, "http") { var res *http.Response client := http.DefaultClient @@ -76,7 +76,7 @@ func (puller *HDLPuller) Connect(p *m7s.Client) (err error) { return } -func (puller *HDLPuller) Pull(p *m7s.Puller) (err error) { +func (puller *FLVPuller) Pull(p *m7s.Puller) (err error) { var startTs uint32 pubConf := p.GetPublishConfig() if !puller.hasAudio { diff --git a/plugin/gb28181/api.go b/plugin/gb28181/api.go index 227ffec..2d558d2 100644 --- a/plugin/gb28181/api.go +++ b/plugin/gb28181/api.go @@ -1,8 +1,12 @@ package plugin_gb28181 import ( + "context" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" "m7s.live/m7s/v5" "m7s.live/m7s/v5/pkg/util" + "m7s.live/m7s/v5/plugin/gb28181/pb" gb28181 "m7s.live/m7s/v5/plugin/gb28181/pkg" "net/http" "os" @@ -10,6 +14,49 @@ import ( "time" ) +func (gb *GB28181Plugin) List(context.Context, *emptypb.Empty) (ret *pb.ResponseList, err error) { + ret = &pb.ResponseList{} + for d := range gb.devices.Range { + var channels []*pb.Channel + for c := range d.channels.Range { + channels = append(channels, &pb.Channel{ + DeviceID: c.DeviceID, + ParentID: c.ParentID, + Name: c.Name, + Manufacturer: c.Manufacturer, + Model: c.Model, + Owner: c.Owner, + CivilCode: c.CivilCode, + Address: c.Address, + Port: int32(c.Port), + Parental: int32(c.Parental), + SafetyWay: int32(c.SafetyWay), + RegisterWay: int32(c.RegisterWay), + Secrecy: int32(c.Secrecy), + Status: string(c.Status), + Longitude: c.Longitude, + Latitude: c.Latitude, + GpsTime: timestamppb.New(c.GpsTime), + }) + } + ret.Data = append(ret.Data, &pb.Device{ + Id: d.ID, + Name: d.Name, + Manufacturer: d.Manufacturer, + Model: d.Model, + Owner: d.Owner, + Status: string(d.Status), + Longitude: d.Longitude, + Latitude: d.Latitude, + GpsTime: timestamppb.New(d.GpsTime), + RegisterTime: timestamppb.New(d.RegisterTime), + UpdateTime: timestamppb.New(d.UpdateTime), + Channels: channels, + }) + } + return +} + func (gb *GB28181Plugin) replayPS(pub *m7s.Publisher, f *os.File) { defer f.Close() var t uint16 @@ -28,11 +75,7 @@ func (gb *GB28181Plugin) replayPS(pub *m7s.Publisher, f *os.File) { if err != nil { return } - err = receiver.Unmarshal(payload) - if err != nil { - return - } - receiver.FeedChan <- receiver.Payload + err = receiver.ReadRTP(payload) } } diff --git a/plugin/gb28181/channel.go b/plugin/gb28181/channel.go new file mode 100644 index 0000000..ec80701 --- /dev/null +++ b/plugin/gb28181/channel.go @@ -0,0 +1,32 @@ +package plugin_gb28181 + +import ( + "log/slog" + "m7s.live/m7s/v5/pkg/util" + gb28181 "m7s.live/m7s/v5/plugin/gb28181/pkg" + "sync/atomic" + "time" +) + +type RecordRequest struct { + SN, SumNum int + *util.Promise[[]gb28181.Record] +} + +func (r *RecordRequest) GetKey() int { + return r.SN +} + +type Channel struct { + Device *Device // 所属设备 + State atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放/对讲 + GpsTime time.Time // gps时间 + Longitude, Latitude string // 经度 + RecordReqs util.Collection[int, *RecordRequest] + *slog.Logger + gb28181.ChannelInfo +} + +func (c *Channel) GetKey() string { + return c.DeviceID +} diff --git a/plugin/gb28181/device.go b/plugin/gb28181/device.go new file mode 100644 index 0000000..c32a8a6 --- /dev/null +++ b/plugin/gb28181/device.go @@ -0,0 +1,212 @@ +package plugin_gb28181 + +import ( + "github.com/emiago/sipgo" + "github.com/emiago/sipgo/sip" + "log/slog" + "m7s.live/m7s/v5/pkg/util" + gb28181 "m7s.live/m7s/v5/plugin/gb28181/pkg" + "net/http" + "strings" + "time" +) + +type DeviceStatus string + +const ( + DeviceRegisterStatus DeviceStatus = "REGISTER" + DeviceRecoverStatus DeviceStatus = "RECOVER" + DeviceOnlineStatus DeviceStatus = "ONLINE" + DeviceOfflineStatus DeviceStatus = "OFFLINE" + DeviceAlarmedStatus DeviceStatus = "ALARMED" +) + +type Device struct { + ID string + Name string + Manufacturer string + Model string + Owner string + RegisterTime, UpdateTime time.Time + LastKeepaliveAt time.Time + Status DeviceStatus + SN int + Recipient sip.Uri + Transport string + channels util.Collection[string, *Channel] + mediaIp string + GpsTime time.Time //gps时间 + Longitude, Latitude string //经度,纬度 + *slog.Logger + eventChan chan any + dialogClient *sipgo.DialogClient +} + +func (d *Device) GetKey() string { + return d.ID +} + +func (d *Device) onMessage(req *sip.Request, tx sip.ServerTransaction, msg *gb28181.Message) (err error) { + var body []byte + if d.Status == DeviceRecoverStatus { + d.Status = DeviceOnlineStatus + } + switch msg.CmdType { + case "Keepalive": + d.LastKeepaliveAt = time.Now() + case "Catalog": + d.eventChan <- msg.DeviceList + case "RecordInfo": + if channel, ok := d.channels.Get(msg.DeviceID); ok { + if req, ok := channel.RecordReqs.Get(msg.SN); ok { + req.Resolve(msg.RecordList) + } + } + case "DeviceInfo": + // 主设备信息 + d.Name = msg.DeviceName + d.Manufacturer = msg.Manufacturer + d.Model = msg.Model + case "Alarm": + d.Status = DeviceAlarmedStatus + body = []byte(gb28181.BuildAlarmResponseXML(d.ID)) + case "Broadcast": + d.Info("broadcast message", "body", req.Body()) + default: + d.Warn("Not supported CmdType", "CmdType", msg.CmdType, "body", req.Body()) + err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusBadRequest, "", nil)) + return + } + err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", body)) + return +} + +func (d *Device) eventLoop(gb *GB28181Plugin) { + send := func(req *sip.Request) (*sip.Response, error) { + return gb.client.Do(gb, req) + } + defer func() { + d.Status = DeviceOfflineStatus + if gb.devices.RemoveByKey(d.ID) { + d.Info("Unregister") + } + }() + response, err := d.catalog(send) + if err != nil { + d.Error("catalog", "err", err) + } else { + d.Debug("catalog", "response", response) + } + response, err = d.queryDeviceInfo(send) + if err != nil { + d.Error("deviceInfo", "err", err) + } else { + d.Debug("deviceInfo", "response", response) + } + subTick := time.NewTicker(time.Second * 3600) + defer subTick.Stop() + catalogTick := time.NewTicker(time.Second * 60) + defer catalogTick.Stop() + for { + select { + case <-subTick.C: + response, err = d.subscribeCatalog(send) + if err != nil { + d.Error("subCatalog", "err", err) + } else { + d.Debug("subCatalog", "response", response) + } + response, err = d.subscribePosition(int(gb.Position.Interval/time.Second), send) + if err != nil { + d.Error("subPosition", "err", err) + } else { + d.Debug("subPosition", "response", response) + } + case <-catalogTick.C: + if time.Since(d.LastKeepaliveAt) > time.Second*3600 { + d.Error("keepalive timeout") + return + } + response, err = d.catalog(send) + if err != nil { + d.Error("catalog", "err", err) + } else { + d.Debug("catalog", "response", response) + } + case event, ok := <-d.eventChan: + if !ok { + return + } + switch v := event.(type) { + case []gb28181.ChannelInfo: + for _, c := range v { + //当父设备非空且存在时、父设备节点增加通道 + if c.ParentID != "" { + path := strings.Split(c.ParentID, "/") + parentId := path[len(path)-1] + //如果父ID并非本身所属设备,一般情况下这是因为下级设备上传了目录信息,该信息通常不需要处理。 + // 暂时不考虑级联目录的实现 + if d.ID != parentId { + if parent, ok := gb.devices.Get(parentId); ok { + parent.addOrUpdateChannel(c) + continue + } else { + c.Model = "Directory " + c.Model + c.Status = "NoParent" + } + } + } + d.addOrUpdateChannel(c) + } + } + } + } +} + +func (d *Device) createRequest(Method sip.RequestMethod) (req *sip.Request) { + req = sip.NewRequest(Method, d.Recipient) + contentType := sip.ContentTypeHeader("Application/MANSCDP+xml") + req.AppendHeader(&contentType) + return +} + +func (d *Device) catalog(send func(*sip.Request) (*sip.Response, error)) (*sip.Response, error) { + request := d.createRequest(sip.MESSAGE) + //d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires)) + request.AppendHeader(sip.NewHeader("Expires", "3600")) + request.SetBody([]byte(gb28181.BuildCatalogXML(d.SN, d.ID))) + return send(request) +} + +func (d *Device) subscribeCatalog(send func(*sip.Request) (*sip.Response, error)) (*sip.Response, error) { + request := d.createRequest(sip.SUBSCRIBE) + request.AppendHeader(sip.NewHeader("Expires", "3600")) + request.SetBody([]byte(gb28181.BuildCatalogXML(d.SN, d.ID))) + return send(request) +} + +func (d *Device) queryDeviceInfo(send func(*sip.Request) (*sip.Response, error)) (*sip.Response, error) { + request := d.createRequest(sip.MESSAGE) + request.SetBody([]byte(gb28181.BuildDeviceInfoXML(d.SN, d.ID))) + return send(request) +} + +func (d *Device) subscribePosition(interval int, send func(*sip.Request) (*sip.Response, error)) (*sip.Response, error) { + request := d.createRequest(sip.SUBSCRIBE) + request.AppendHeader(sip.NewHeader("Expires", "3600")) + request.SetBody([]byte(gb28181.BuildDevicePositionXML(d.SN, d.ID, interval))) + return send(request) +} + +func (d *Device) addOrUpdateChannel(c gb28181.ChannelInfo) { + if channel, ok := d.channels.Get(c.DeviceID); ok { + channel.ChannelInfo = c + } else { + channel = &Channel{ + Device: d, + Logger: d.Logger.With("channel", c.DeviceID), + ChannelInfo: c, + } + d.channels.Add(channel) + } +} diff --git a/plugin/gb28181/dialog.go b/plugin/gb28181/dialog.go new file mode 100644 index 0000000..abf9731 --- /dev/null +++ b/plugin/gb28181/dialog.go @@ -0,0 +1,114 @@ +package plugin_gb28181 + +import ( + "fmt" + "github.com/emiago/sipgo" + "github.com/emiago/sipgo/sip" + "m7s.live/m7s/v5" + "m7s.live/m7s/v5/pkg/util" + gb28181 "m7s.live/m7s/v5/plugin/gb28181/pkg" + "strconv" + "strings" +) + +type Dialog struct { + *Channel + *gb28181.Receiver + gb28181.InviteOptions + gb *GB28181Plugin + session *sipgo.DialogClientSession +} + +func (d *Dialog) Connect(p *m7s.Client) (err error) { + sss := strings.Split(p.RemoteURL, "/") + deviceId, channelId := sss[0], sss[1] + if len(sss) == 2 { + if device, ok := d.gb.devices.Get(deviceId); ok { + if channel, ok := device.channels.Get(channelId); ok { + d.Channel = channel + } else { + return fmt.Errorf("channel %s not found", channelId) + } + } else { + return fmt.Errorf("device %s not found", deviceId) + } + } else if len(sss) == 3 { + var recordRange util.Range[int] + err = recordRange.Resolve(sss[2]) + } + return +} + +func (d *Dialog) Pull(p *m7s.Puller) (err error) { + d.Receiver = gb28181.NewReceiver(&p.Publisher) + ssrc := d.CreateSSRC(d.gb.Serial) + d.gb.dialogs.Set(d) + defer d.gb.dialogs.Remove(d) + if d.gb.MediaPort.Valid() { + select { + case d.MediaPort = <-d.gb.tcpPorts: + defer func() { + d.gb.tcpPorts <- d.MediaPort + }() + default: + return fmt.Errorf("no available tcp port") + } + } else { + d.MediaPort = d.gb.MediaPort[0] + } + sdpInfo := []string{ + "v=0", + fmt.Sprintf("o=%s 0 0 IN IP4 %s", d.DeviceID, d.Device.mediaIp), + "s=" + util.Conditoinal(d.IsLive(), "Play", "Playback"), + "u=" + d.DeviceID + ":0", + "c=IN IP4 " + d.Device.mediaIp, + d.String(), + fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort), + "a=recvonly", + "a=rtpmap:96 PS/90000", + "a=setup:passive", + "a=connection:new", + "y=" + ssrc, + } + contentTypeHeader := sip.ContentTypeHeader("application/sdp") + subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", d.DeviceID, ssrc, d.gb.Serial)) + d.session, err = d.Channel.Device.dialogClient.Invite(d.gb, d.Channel.Device.Recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &contentTypeHeader, subjectHeader) + if err != nil { + return + } + err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{}) + if err != nil { + return + } + inviteResponseBody := d.session.InviteResponse.Body() + d.gb.Info("InviteResponse: %s", inviteResponseBody) + ds := strings.Split(string(inviteResponseBody), "\r\n") + for _, l := range ds { + if ls := strings.Split(l, "="); len(ls) > 1 { + if ls[0] == "y" && len(ls[1]) > 0 { + if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil { + d.SSRC = uint32(_ssrc) + } else { + d.gb.Error("read invite response y ", "err", err) + } + // break + } + if ls[0] == "m" && len(ls[1]) > 0 { + netinfo := strings.Split(ls[1], " ") + if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" { + d.gb.Debug("device support tcp") + } else { + return fmt.Errorf("device not support tcp") + } + } + } + } + err = d.session.Ack(d.gb) + go d.Receiver.ListenTCP(d.MediaPort) + d.Receiver.Demux() + return +} + +func (d *Dialog) GetKey() uint32 { + return d.SSRC +} diff --git a/plugin/gb28181/index.go b/plugin/gb28181/index.go index d2c7d67..f6d3e4b 100644 --- a/plugin/gb28181/index.go +++ b/plugin/gb28181/index.go @@ -1,16 +1,26 @@ package plugin_gb28181 import ( + "crypto/tls" "fmt" "github.com/emiago/sipgo" "github.com/emiago/sipgo/sip" + myip "github.com/husanpao/ip" "github.com/icholy/digest" + "github.com/pion/rtp" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "m7s.live/m7s/v5" + "m7s.live/m7s/v5/pkg/config" "m7s.live/m7s/v5/pkg/util" + "m7s.live/m7s/v5/plugin/gb28181/pb" gb28181 "m7s.live/m7s/v5/plugin/gb28181/pkg" + rtp2 "m7s.live/m7s/v5/plugin/rtp/pkg" + "net" "net/http" + "os" "strconv" + "strings" "sync" "time" ) @@ -18,27 +28,73 @@ import ( type SipConfig struct { ListenAddr []string ListenTLSAddr []string + CertFile string `desc:"证书文件"` + KeyFile string `desc:"私钥文件"` +} + +type PositionConfig struct { + Expires time.Duration `default:"3600s" desc:"订阅周期"` //订阅周期 + Interval time.Duration `default:"6s" desc:"订阅间隔"` //订阅间隔 } type GB28181Plugin struct { + pb.UnimplementedApiServer m7s.Plugin - Username string - Password string - Sip SipConfig - ua *sipgo.UserAgent - server *sipgo.Server - devices util.Collection[string, *gb28181.Device] + Serial string `default:"34020000002000000001" desc:"sip 服务 id"` //sip 服务器 id, 默认 34020000002000000001 + Realm string `default:"3402000000" desc:"sip 服务域"` //sip 服务器域,默认 3402000000 + Username string + Password string + Sip SipConfig + MediaPort util.Range[uint16] `default:"10000-20000" desc:"媒体端口范围"` //媒体端口范围 + Position PositionConfig + ua *sipgo.UserAgent + server *sipgo.Server + client *sipgo.Client + devices util.Collection[string, *Device] + dialogs util.Collection[uint32, *Dialog] + tcpPorts chan uint16 } -var _ = m7s.InstallPlugin[GB28181Plugin]() +var _ = m7s.InstallPlugin[GB28181Plugin](pb.RegisterApiHandler, pb.Api_ServiceDesc) +func init() { + sip.SIPDebug = true +} func (gb *GB28181Plugin) OnInit() (err error) { + logger := zerolog.New(os.Stdout) gb.ua, err = sipgo.NewUA(sipgo.WithUserAgent("monibuca" + m7s.Version)) // Build user agent - gb.server, err = sipgo.NewServer(gb.ua) // Creating server handle for ua + gb.client, _ = sipgo.NewClient(gb.ua, sipgo.WithClientLogger(logger)) // Creating client handle for ua + gb.server, _ = sipgo.NewServer(gb.ua, sipgo.WithServerLogger(logger)) // Creating server handle for ua gb.server.OnRegister(gb.OnRegister) gb.server.OnMessage(gb.OnMessage) gb.devices.L = new(sync.RWMutex) - go gb.server.ListenAndServe(gb, "tcp", "") + + if gb.MediaPort.Valid() { + gb.tcpPorts = make(chan uint16, gb.MediaPort.Size()) + for i := range gb.MediaPort.Size() { + gb.tcpPorts <- gb.MediaPort[0] + i + } + } else { + tcpConfig := &gb.GetCommonConf().TCP + tcpConfig.ListenAddr = fmt.Sprintf(":%d", gb.MediaPort[0]) + } + for _, addr := range gb.Sip.ListenAddr { + netWork, addr, _ := strings.Cut(addr, ":") + go gb.server.ListenAndServe(gb, netWork, addr) + } + keyPair, _ := tls.X509KeyPair(config.LocalCert, config.LocalKey) + if gb.Sip.CertFile != "" || gb.Sip.KeyFile != "" { + keyPair, err = tls.LoadX509KeyPair(gb.Sip.CertFile, gb.Sip.KeyFile) + } + if err == nil { + tslConfig := &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + } + for _, addr := range gb.Sip.ListenTLSAddr { + netWork, addr, _ := strings.Cut(addr, ":") + go gb.server.ListenAndServeTLS(gb, netWork, addr, tslConfig) + } + } return } @@ -50,7 +106,7 @@ func (gb *GB28181Plugin) RegisterHandler() map[string]http.HandlerFunc { func (gb *GB28181Plugin) OnRegister(req *sip.Request, tx sip.ServerTransaction) { from := req.From() - if from.Address.User == "" { + if from == nil || from.Address.User == "" { gb.Error("OnRegister", "error", "no user") return } @@ -77,13 +133,13 @@ func (gb *GB28181Plugin) OnRegister(req *sip.Request, tx sip.ServerTransaction) var digCred *digest.Credentials if h == nil { chal = digest.Challenge{ - Realm: "monibuca-server", + Realm: gb.Realm, Nonce: fmt.Sprintf("%d", time.Now().UnixMicro()), Opaque: "monibuca", Algorithm: "MD5", } - res := sip.NewResponseFromRequest(req, http.StatusUnauthorized, "Unathorized", nil) + res := sip.NewResponseFromRequest(req, sip.StatusUnauthorized, "Unathorized", nil) res.AppendHeader(sip.NewHeader("WWW-Authenticate", chal.String())) err = tx.Respond(res) @@ -93,13 +149,13 @@ func (gb *GB28181Plugin) OnRegister(req *sip.Request, tx sip.ServerTransaction) cred, err = digest.ParseCredentials(h.Value()) if err != nil { log.Error().Err(err).Msg("parsing creds failed") - err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusUnauthorized, "Bad credentials", nil)) + err = tx.Respond(sip.NewResponseFromRequest(req, sip.StatusUnauthorized, "Bad credentials", nil)) return } // Check registry if cred.Username != gb.Username { - err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusNotFound, "Bad authorization header", nil)) + err = tx.Respond(sip.NewResponseFromRequest(req, sip.StatusNotFound, "Bad authorization header", nil)) return } @@ -113,39 +169,143 @@ func (gb *GB28181Plugin) OnRegister(req *sip.Request, tx sip.ServerTransaction) if err != nil { gb.Error("Calc digest failed") - err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusUnauthorized, "Bad credentials", nil)) + err = tx.Respond(sip.NewResponseFromRequest(req, sip.StatusUnauthorized, "Bad credentials", nil)) return } if cred.Response != digCred.Response { - err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusUnauthorized, "Unathorized", nil)) + err = tx.Respond(sip.NewResponseFromRequest(req, sip.StatusUnauthorized, "Unathorized", nil)) return } - err = tx.Respond(sip.NewResponseFromRequest(req, http.StatusOK, "OK", nil)) + err = tx.Respond(sip.NewResponseFromRequest(req, sip.StatusOK, "OK", nil)) } - var d *gb28181.Device if isUnregister { - if gb.devices.RemoveByKey(id) { - gb.Info("Unregister Device", "id", id) - } else { - return + if d, ok := gb.devices.Get(id); ok { + close(d.eventChan) } } else { - var ok bool - if d, ok = gb.devices.Get(id); ok { + if d, ok := gb.devices.Get(id); ok { gb.RecoverDevice(d, req) } else { d = gb.StoreDevice(id, req) } } - DeviceNonce.Delete(id) - DeviceRegisterCount.Delete(id) - if !isUnregister { - //订阅设备更新 - go d.syncChannels() - } } func (gb *GB28181Plugin) OnMessage(req *sip.Request, tx sip.ServerTransaction) { - + from := req.From() + if from == nil || from.Address.User == "" { + gb.Error("OnMessage", "error", "no user") + return + } + id := from.Address.User + if d, ok := gb.devices.Get(id); ok { + d.UpdateTime = time.Now() + temp := &gb28181.Message{} + err := gb28181.DecodeGB2312(temp, req.Body()) + if err != nil { + err = gb28181.DecodeGbk(temp, req.Body()) + if err != nil { + gb.Error("OnMessage", "error", err.Error()) + return + } + } + err = d.onMessage(req, tx, temp) + } +} + +func (gb *GB28181Plugin) RecoverDevice(d *Device, req *sip.Request) { + from := req.From() + source := req.Source() + hostname := strings.Split(source, ":") + port, _ := strconv.Atoi(hostname[1]) + d.Recipient = sip.Uri{ + Host: hostname[0], + Port: port, + User: from.Address.User, + } + d.RegisterTime = time.Now() + d.Status = DeviceRecoverStatus + d.UpdateTime = time.Now() +} + +func (gb *GB28181Plugin) StoreDevice(id string, req *sip.Request) (d *Device) { + from := req.From() + source := req.Source() + servIp := req.Recipient.Host + mediaIP := myip.InternalIPv4() + var ok bool + //如果用户配置过则使用配置的 + if publicIP := gb.GetCommonConf().PublicIP; publicIP != "" { + mediaIP = publicIP + } else if publicIP, ok = m7s.Routes[servIp]; ok { //根据网卡ip获取对应的公网ip + mediaIP = publicIP + } else if publicIP, ok = m7s.Routes[mediaIP]; ok { + mediaIP = publicIP + } + //如果相等,则服务器是内网通道.海康摄像头不支持...自动获取 + //if strings.LastIndex(deviceIp, ".") != -1 && strings.LastIndex(servIp, ".") != -1 { + // if servIp[0:strings.LastIndex(servIp, ".")] == deviceIp[0:strings.LastIndex(deviceIp, ".")] || mediaIP == "" { + // mediaIP = servIp + // } + //} + hostname := strings.Split(source, ":") + port, _ := strconv.Atoi(hostname[1]) + d = &Device{ + ID: id, + RegisterTime: time.Now(), + UpdateTime: time.Now(), + Status: DeviceRegisterStatus, + Recipient: sip.Uri{ + Host: hostname[0], + Port: port, + User: from.Address.User, + }, + Transport: req.Transport(), + Logger: gb.Logger.With("id", id), + mediaIp: mediaIP, + eventChan: make(chan any, 10), + } + contactHDR := sip.ContactHeader{ + Address: req.Recipient, + } + d.dialogClient = sipgo.NewDialogClient(gb.client, contactHDR) + d.channels.L = new(sync.RWMutex) + d.Info("StoreDevice", "source", source, "servIp", servIp, "mediaIP", mediaIP, "recipient", req.Recipient) + gb.devices.Add(d) + if gb.DB != nil { + //TODO + } + go d.eventLoop(gb) + return +} + +func (gb *GB28181Plugin) NewPullHandler() m7s.PullHandler { + return &Dialog{ + gb: gb, + } +} + +func (gb *GB28181Plugin) OnTCPConnect(conn *net.TCPConn) { + var reader = (*rtp2.TCP)(conn) + var theDialog *Dialog + _ = reader.Read(func(data util.Buffer) (err error) { + if theDialog != nil { + return theDialog.ReadRTP(data) + } + var rtpPacket rtp.Packet + if err = rtpPacket.Unmarshal(data); err != nil { + gb.Error("decode rtp", "err", err) + } + ssrc := rtpPacket.SSRC + if dialog, ok := gb.dialogs.Get(ssrc); ok { + theDialog = dialog + return dialog.ReadRTP(data) + } + gb.Warn("dialog not found", "ssrc", ssrc) + return + }) + if theDialog != nil { + close(theDialog.FeedChan) + } } diff --git a/plugin/gb28181/pb/gb28181.pb.go b/plugin/gb28181/pb/gb28181.pb.go new file mode 100644 index 0000000..3844fcb --- /dev/null +++ b/plugin/gb28181/pb/gb28181.pb.go @@ -0,0 +1,583 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.19.1 +// source: gb28181.proto + +//import "global.proto"; + +package pb + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Channel struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DeviceID string `protobuf:"bytes,1,opt,name=DeviceID,proto3" json:"DeviceID,omitempty"` + ParentID string `protobuf:"bytes,2,opt,name=ParentID,proto3" json:"ParentID,omitempty"` + Name string `protobuf:"bytes,3,opt,name=Name,proto3" json:"Name,omitempty"` + Manufacturer string `protobuf:"bytes,4,opt,name=Manufacturer,proto3" json:"Manufacturer,omitempty"` + Model string `protobuf:"bytes,5,opt,name=Model,proto3" json:"Model,omitempty"` + Owner string `protobuf:"bytes,6,opt,name=Owner,proto3" json:"Owner,omitempty"` + CivilCode string `protobuf:"bytes,7,opt,name=CivilCode,proto3" json:"CivilCode,omitempty"` + Address string `protobuf:"bytes,8,opt,name=Address,proto3" json:"Address,omitempty"` + Port int32 `protobuf:"varint,9,opt,name=Port,proto3" json:"Port,omitempty"` + Parental int32 `protobuf:"varint,10,opt,name=Parental,proto3" json:"Parental,omitempty"` + SafetyWay int32 `protobuf:"varint,11,opt,name=SafetyWay,proto3" json:"SafetyWay,omitempty"` + RegisterWay int32 `protobuf:"varint,12,opt,name=RegisterWay,proto3" json:"RegisterWay,omitempty"` + Secrecy int32 `protobuf:"varint,13,opt,name=Secrecy,proto3" json:"Secrecy,omitempty"` + Status string `protobuf:"bytes,14,opt,name=Status,proto3" json:"Status,omitempty"` + GpsTime *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=gpsTime,proto3" json:"gpsTime,omitempty"` + Longitude string `protobuf:"bytes,16,opt,name=Longitude,proto3" json:"Longitude,omitempty"` + Latitude string `protobuf:"bytes,17,opt,name=Latitude,proto3" json:"Latitude,omitempty"` +} + +func (x *Channel) Reset() { + *x = Channel{} + if protoimpl.UnsafeEnabled { + mi := &file_gb28181_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Channel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Channel) ProtoMessage() {} + +func (x *Channel) ProtoReflect() protoreflect.Message { + mi := &file_gb28181_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Channel.ProtoReflect.Descriptor instead. +func (*Channel) Descriptor() ([]byte, []int) { + return file_gb28181_proto_rawDescGZIP(), []int{0} +} + +func (x *Channel) GetDeviceID() string { + if x != nil { + return x.DeviceID + } + return "" +} + +func (x *Channel) GetParentID() string { + if x != nil { + return x.ParentID + } + return "" +} + +func (x *Channel) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Channel) GetManufacturer() string { + if x != nil { + return x.Manufacturer + } + return "" +} + +func (x *Channel) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *Channel) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Channel) GetCivilCode() string { + if x != nil { + return x.CivilCode + } + return "" +} + +func (x *Channel) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Channel) GetPort() int32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *Channel) GetParental() int32 { + if x != nil { + return x.Parental + } + return 0 +} + +func (x *Channel) GetSafetyWay() int32 { + if x != nil { + return x.SafetyWay + } + return 0 +} + +func (x *Channel) GetRegisterWay() int32 { + if x != nil { + return x.RegisterWay + } + return 0 +} + +func (x *Channel) GetSecrecy() int32 { + if x != nil { + return x.Secrecy + } + return 0 +} + +func (x *Channel) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *Channel) GetGpsTime() *timestamppb.Timestamp { + if x != nil { + return x.GpsTime + } + return nil +} + +func (x *Channel) GetLongitude() string { + if x != nil { + return x.Longitude + } + return "" +} + +func (x *Channel) GetLatitude() string { + if x != nil { + return x.Latitude + } + return "" +} + +type Device struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Manufacturer string `protobuf:"bytes,3,opt,name=manufacturer,proto3" json:"manufacturer,omitempty"` + Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"` + Owner string `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` + GpsTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=gpsTime,proto3" json:"gpsTime,omitempty"` + Longitude string `protobuf:"bytes,7,opt,name=longitude,proto3" json:"longitude,omitempty"` + Latitude string `protobuf:"bytes,8,opt,name=latitude,proto3" json:"latitude,omitempty"` + Status string `protobuf:"bytes,9,opt,name=status,proto3" json:"status,omitempty"` + RegisterTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=registerTime,proto3" json:"registerTime,omitempty"` + UpdateTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Channels []*Channel `protobuf:"bytes,12,rep,name=channels,proto3" json:"channels,omitempty"` +} + +func (x *Device) Reset() { + *x = Device{} + if protoimpl.UnsafeEnabled { + mi := &file_gb28181_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Device) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Device) ProtoMessage() {} + +func (x *Device) ProtoReflect() protoreflect.Message { + mi := &file_gb28181_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Device.ProtoReflect.Descriptor instead. +func (*Device) Descriptor() ([]byte, []int) { + return file_gb28181_proto_rawDescGZIP(), []int{1} +} + +func (x *Device) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Device) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Device) GetManufacturer() string { + if x != nil { + return x.Manufacturer + } + return "" +} + +func (x *Device) GetModel() string { + if x != nil { + return x.Model + } + return "" +} + +func (x *Device) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *Device) GetGpsTime() *timestamppb.Timestamp { + if x != nil { + return x.GpsTime + } + return nil +} + +func (x *Device) GetLongitude() string { + if x != nil { + return x.Longitude + } + return "" +} + +func (x *Device) GetLatitude() string { + if x != nil { + return x.Latitude + } + return "" +} + +func (x *Device) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *Device) GetRegisterTime() *timestamppb.Timestamp { + if x != nil { + return x.RegisterTime + } + return nil +} + +func (x *Device) GetUpdateTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Device) GetChannels() []*Channel { + if x != nil { + return x.Channels + } + return nil +} + +type ResponseList struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Data []*Device `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty"` +} + +func (x *ResponseList) Reset() { + *x = ResponseList{} + if protoimpl.UnsafeEnabled { + mi := &file_gb28181_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ResponseList) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResponseList) ProtoMessage() {} + +func (x *ResponseList) ProtoReflect() protoreflect.Message { + mi := &file_gb28181_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResponseList.ProtoReflect.Descriptor instead. +func (*ResponseList) Descriptor() ([]byte, []int) { + return file_gb28181_proto_rawDescGZIP(), []int{2} +} + +func (x *ResponseList) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *ResponseList) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ResponseList) GetData() []*Device { + if x != nil { + return x.Data + } + return nil +} + +var File_gb28181_proto protoreflect.FileDescriptor + +var file_gb28181_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x67, 0x62, 0x32, 0x38, 0x31, 0x38, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x67, 0x62, 0x32, 0x38, 0x31, 0x38, 0x31, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xef, 0x03, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x12, 0x1a, 0x0a, 0x08, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, + 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x4d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, + 0x12, 0x14, 0x0a, 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, + 0x43, 0x69, 0x76, 0x69, 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x43, 0x69, 0x76, 0x69, 0x6c, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x61, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x50, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x61, 0x66, 0x65, 0x74, 0x79, 0x57, 0x61, + 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x53, 0x61, 0x66, 0x65, 0x74, 0x79, 0x57, + 0x61, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x57, 0x61, + 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x57, 0x61, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x65, 0x63, 0x72, 0x65, 0x63, 0x79, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x53, 0x65, 0x63, 0x72, 0x65, 0x63, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x67, 0x70, 0x73, 0x54, 0x69, 0x6d, + 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x07, 0x67, 0x70, 0x73, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, + 0x4c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x4c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x4c, 0x61, + 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x4c, 0x61, + 0x74, 0x69, 0x74, 0x75, 0x64, 0x65, 0x22, 0xae, 0x03, 0x0a, 0x06, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, + 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, + 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, + 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, + 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x07, 0x67, 0x70, 0x73, 0x54, 0x69, 0x6d, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x07, 0x67, 0x70, 0x73, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6c, + 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x6c, 0x6f, 0x6e, 0x67, 0x69, 0x74, 0x75, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x74, + 0x69, 0x74, 0x75, 0x64, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x74, + 0x69, 0x74, 0x75, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3e, 0x0a, + 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3a, 0x0a, + 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x67, 0x62, + 0x32, 0x38, 0x31, 0x38, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x08, 0x63, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0x61, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x67, 0x62, 0x32, 0x38, 0x31, 0x38, 0x31, 0x2e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x57, 0x0a, 0x03, 0x61, 0x70, + 0x69, 0x12, 0x50, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x15, 0x2e, 0x67, 0x62, 0x32, 0x38, 0x31, 0x38, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, + 0x12, 0x11, 0x2f, 0x67, 0x62, 0x32, 0x38, 0x31, 0x38, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6c, + 0x69, 0x73, 0x74, 0x42, 0x23, 0x5a, 0x21, 0x6d, 0x37, 0x73, 0x2e, 0x6c, 0x69, 0x76, 0x65, 0x2f, + 0x6d, 0x37, 0x73, 0x2f, 0x76, 0x35, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x62, + 0x32, 0x38, 0x31, 0x38, 0x31, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_gb28181_proto_rawDescOnce sync.Once + file_gb28181_proto_rawDescData = file_gb28181_proto_rawDesc +) + +func file_gb28181_proto_rawDescGZIP() []byte { + file_gb28181_proto_rawDescOnce.Do(func() { + file_gb28181_proto_rawDescData = protoimpl.X.CompressGZIP(file_gb28181_proto_rawDescData) + }) + return file_gb28181_proto_rawDescData +} + +var file_gb28181_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_gb28181_proto_goTypes = []interface{}{ + (*Channel)(nil), // 0: gb28181.Channel + (*Device)(nil), // 1: gb28181.Device + (*ResponseList)(nil), // 2: gb28181.ResponseList + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 4: google.protobuf.Empty +} +var file_gb28181_proto_depIdxs = []int32{ + 3, // 0: gb28181.Channel.gpsTime:type_name -> google.protobuf.Timestamp + 3, // 1: gb28181.Device.gpsTime:type_name -> google.protobuf.Timestamp + 3, // 2: gb28181.Device.registerTime:type_name -> google.protobuf.Timestamp + 3, // 3: gb28181.Device.updateTime:type_name -> google.protobuf.Timestamp + 0, // 4: gb28181.Device.channels:type_name -> gb28181.Channel + 1, // 5: gb28181.ResponseList.data:type_name -> gb28181.Device + 4, // 6: gb28181.api.List:input_type -> google.protobuf.Empty + 2, // 7: gb28181.api.List:output_type -> gb28181.ResponseList + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_gb28181_proto_init() } +func file_gb28181_proto_init() { + if File_gb28181_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_gb28181_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Channel); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gb28181_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Device); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gb28181_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ResponseList); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_gb28181_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gb28181_proto_goTypes, + DependencyIndexes: file_gb28181_proto_depIdxs, + MessageInfos: file_gb28181_proto_msgTypes, + }.Build() + File_gb28181_proto = out.File + file_gb28181_proto_rawDesc = nil + file_gb28181_proto_goTypes = nil + file_gb28181_proto_depIdxs = nil +} diff --git a/plugin/gb28181/pb/gb28181.pb.gw.go b/plugin/gb28181/pb/gb28181.pb.gw.go new file mode 100644 index 0000000..e57bb54 --- /dev/null +++ b/plugin/gb28181/pb/gb28181.pb.gw.go @@ -0,0 +1,156 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: gb28181.proto + +/* +Package pb is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package pb + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/emptypb" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, client ApiClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := client.List(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Api_List_0(ctx context.Context, marshaler runtime.Marshaler, server ApiServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq emptypb.Empty + var metadata runtime.ServerMetadata + + msg, err := server.List(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterApiHandlerServer registers the http handlers for service Api to "mux". +// UnaryRPC :call ApiServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterApiHandlerFromEndpoint instead. +func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ApiServer) error { + + mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/gb28181.Api/List", runtime.WithHTTPPathPattern("/gb28181/api/list")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Api_List_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Api_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterApiHandlerFromEndpoint is same as RegisterApiHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterApiHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.DialContext(ctx, endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterApiHandler(ctx, mux, conn) +} + +// RegisterApiHandler registers the http handlers for service Api to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterApiHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterApiHandlerClient(ctx, mux, NewApiClient(conn)) +} + +// RegisterApiHandlerClient registers the http handlers for service Api +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ApiClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ApiClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ApiClient" to call the correct interceptors. +func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ApiClient) error { + + mux.Handle("GET", pattern_Api_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/gb28181.Api/List", runtime.WithHTTPPathPattern("/gb28181/api/list")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Api_List_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Api_List_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_Api_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"gb28181", "api", "list"}, "")) +) + +var ( + forward_Api_List_0 = runtime.ForwardResponseMessage +) diff --git a/plugin/gb28181/pb/gb28181.proto b/plugin/gb28181/pb/gb28181.proto new file mode 100644 index 0000000..7884b15 --- /dev/null +++ b/plugin/gb28181/pb/gb28181.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; +import "google/api/annotations.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +//import "global.proto"; +package gb28181; +option go_package="m7s.live/m7s/v5/plugin/gb28181/pb"; + +service api { + rpc List (google.protobuf.Empty) returns (ResponseList) { + option (google.api.http) = { + get: "/gb28181/api/list" + }; + } +} + +message Channel { + string DeviceID =1; + string ParentID =2; + string Name =3; + string Manufacturer =4; + string Model =5; + string Owner =6; + string CivilCode = 7; + string Address = 8; + int32 Port = 9; + int32 Parental = 10; + int32 SafetyWay = 11; + int32 RegisterWay = 12; + int32 Secrecy =13; + string Status =14; + google.protobuf.Timestamp gpsTime=15; + string Longitude = 16; + string Latitude = 17; +} + +message Device { + string id = 1; + string name = 2; + string manufacturer = 3; + string model = 4; + string owner = 5; + google.protobuf.Timestamp gpsTime=6; + string longitude = 7; + string latitude = 8; + string status = 9; + google.protobuf.Timestamp registerTime=10; + google.protobuf.Timestamp updateTime=11; + repeated Channel channels = 12; +} + +message ResponseList { + int32 code = 1; + string message = 2; + repeated Device data = 3; +} \ No newline at end of file diff --git a/plugin/gb28181/pb/gb28181_grpc.pb.go b/plugin/gb28181/pb/gb28181_grpc.pb.go new file mode 100644 index 0000000..a6f34d2 --- /dev/null +++ b/plugin/gb28181/pb/gb28181_grpc.pb.go @@ -0,0 +1,106 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.19.1 +// source: gb28181.proto + +package pb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// ApiClient is the client API for Api service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ApiClient interface { + List(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ResponseList, error) +} + +type apiClient struct { + cc grpc.ClientConnInterface +} + +func NewApiClient(cc grpc.ClientConnInterface) ApiClient { + return &apiClient{cc} +} + +func (c *apiClient) List(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ResponseList, error) { + out := new(ResponseList) + err := c.cc.Invoke(ctx, "/gb28181.api/List", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ApiServer is the server API for Api service. +// All implementations must embed UnimplementedApiServer +// for forward compatibility +type ApiServer interface { + List(context.Context, *emptypb.Empty) (*ResponseList, error) + mustEmbedUnimplementedApiServer() +} + +// UnimplementedApiServer must be embedded to have forward compatible implementations. +type UnimplementedApiServer struct { +} + +func (UnimplementedApiServer) List(context.Context, *emptypb.Empty) (*ResponseList, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedApiServer) mustEmbedUnimplementedApiServer() {} + +// UnsafeApiServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ApiServer will +// result in compilation errors. +type UnsafeApiServer interface { + mustEmbedUnimplementedApiServer() +} + +func RegisterApiServer(s grpc.ServiceRegistrar, srv ApiServer) { + s.RegisterService(&Api_ServiceDesc, srv) +} + +func _Api_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ApiServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gb28181.api/List", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ApiServer).List(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// Api_ServiceDesc is the grpc.ServiceDesc for Api service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Api_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gb28181.api", + HandlerType: (*ApiServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _Api_List_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gb28181.proto", +} diff --git a/plugin/gb28181/pkg/channel.go b/plugin/gb28181/pkg/channel.go deleted file mode 100644 index 6c1ab82..0000000 --- a/plugin/gb28181/pkg/channel.go +++ /dev/null @@ -1,46 +0,0 @@ -package gb28181 - -import ( - "log/slog" - "sync/atomic" - "time" -) - -type ChannelStatus string - -const ( - ChannelOnStatus ChannelStatus = "ON" - ChannelOffStatus ChannelStatus = "OFF" -) - -type Channel struct { - Device *Device // 所属设备 - State atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放/对讲 - LiveSubSP string // 实时子码流,通过rtsp - GpsTime time.Time // gps时间 - Longitude string // 经度 - Latitude string // 纬度 - *slog.Logger - ChannelInfo -} - -func (c *Channel) GetKey() string { - return c.DeviceID -} - -type ChannelInfo struct { - DeviceID string // 通道ID - ParentID string - Name string - Manufacturer string - Model string - Owner string - CivilCode string - Address string - Port int - Parental int - SafetyWay int - RegisterWay int - Secrecy int - Status ChannelStatus -} diff --git a/plugin/gb28181/pkg/device.go b/plugin/gb28181/pkg/device.go deleted file mode 100644 index 2f58396..0000000 --- a/plugin/gb28181/pkg/device.go +++ /dev/null @@ -1,49 +0,0 @@ -package gb28181 - -import ( - "github.com/emiago/sipgo/sip" - "log/slog" - "m7s.live/m7s/v5/pkg/util" - "time" -) - -type DeviceStatus string - -const ( - DeviceRegisterStatus DeviceStatus = "REGISTER" - DeviceRecoverStatus DeviceStatus = "RECOVER" - DeviceOnlineStatus DeviceStatus = "ONLINE" - DeviceOfflineStatus DeviceStatus = "OFFLINE" - DeviceAlarmedStatus DeviceStatus = "ALARMED" -) - -type Device struct { - ID string - Name string - Manufacturer string - Model string - Owner string - RegisterTime time.Time - UpdateTime time.Time - LastKeepaliveAt time.Time - Status DeviceStatus - SN int - Addr sip.Addr - SipIP string //设备对应网卡的服务器ip - MediaIP string //设备对应网卡的服务器ip - NetAddr string - channels util.Collection[string, *Channel] - subscriber struct { - CallID string - Timeout time.Time - } - lastSyncTime time.Time - GpsTime time.Time //gps时间 - Longitude string //经度 - Latitude string //纬度 - *slog.Logger -} - -func (d *Device) GetKey() string { - return d.ID -} diff --git a/plugin/gb28181/pkg/dialog.go b/plugin/gb28181/pkg/dialog.go new file mode 100644 index 0000000..41dc48c --- /dev/null +++ b/plugin/gb28181/pkg/dialog.go @@ -0,0 +1 @@ +package gb28181 diff --git a/plugin/gb28181/pkg/invite-option.go b/plugin/gb28181/pkg/invite-option.go new file mode 100644 index 0000000..2fefece --- /dev/null +++ b/plugin/gb28181/pkg/invite-option.go @@ -0,0 +1,68 @@ +package gb28181 + +import ( + "errors" + "fmt" + "math/rand" + "strconv" +) + +type InviteOptions struct { + Start int + End int + dump string + ssrc string + SSRC uint32 + MediaPort uint16 + StreamPath string + recyclePort func(p uint16) (err error) +} + +func (o InviteOptions) IsLive() bool { + return o.Start == 0 || o.End == 0 +} + +func (o InviteOptions) Record() bool { + return !o.IsLive() +} + +func (o *InviteOptions) Validate(start, end string) error { + if start != "" { + sint, err1 := strconv.ParseInt(start, 10, 0) + if err1 != nil { + return err1 + } + o.Start = int(sint) + } + if end != "" { + eint, err2 := strconv.ParseInt(end, 10, 0) + if err2 != nil { + return err2 + } + o.End = int(eint) + } + if o.Start >= o.End { + return errors.New("start < end") + } + return nil +} + +func (o InviteOptions) String() string { + return fmt.Sprintf("t=%d %d", o.Start, o.End) +} + +func (o *InviteOptions) CreateSSRC(serial string) string { + ssrc := make([]byte, 10) + if o.IsLive() { + ssrc[0] = '0' + } else { + ssrc[0] = '1' + } + copy(ssrc[1:6], serial[3:8]) + randNum := 1000 + rand.Intn(8999) + copy(ssrc[6:], strconv.Itoa(randNum)) + o.ssrc = string(ssrc) + _ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0) + o.SSRC = uint32(_ssrc) + return o.ssrc +} diff --git a/plugin/gb28181/pkg/message.go b/plugin/gb28181/pkg/message.go new file mode 100644 index 0000000..cce8f9e --- /dev/null +++ b/plugin/gb28181/pkg/message.go @@ -0,0 +1,148 @@ +package gb28181 + +import ( + "bytes" + "encoding/xml" + "fmt" + "golang.org/x/net/html/charset" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" + "strconv" + "time" +) + +const ( + // CatalogXML 获取设备列表xml样式 + CatalogXML = ` +Catalog +%d +%s + +` + // RecordInfoXML 获取录像文件列表xml样式 + RecordInfoXML = ` + +RecordInfo +%d +%s +%s +%s +0 +all + +` + // DeviceInfoXML 查询设备详情xml样式 + DeviceInfoXML = ` + +DeviceInfo +%d +%s + +` + // DevicePositionXML 订阅设备位置 + DevicePositionXML = ` + +MobilePosition +%d +%s +%d +` + AlarmResponseXML = ` + +Alarm +17430 +%s +` + ChannelOnStatus ChannelStatus = "ON" + ChannelOffStatus ChannelStatus = "OFF" +) + +func intTotime(t int64) time.Time { + tstr := strconv.FormatInt(t, 10) + if len(tstr) == 10 { + return time.Unix(t, 0) + } + if len(tstr) == 13 { + return time.UnixMilli(t) + } + return time.Now() +} + +// BuildDeviceInfoXML 获取设备详情指令 +func BuildDeviceInfoXML(sn int, id string) string { + return fmt.Sprintf(DeviceInfoXML, sn, id) +} + +// BuildCatalogXML 获取NVR下设备列表指令 +func BuildCatalogXML(sn int, id string) string { + return fmt.Sprintf(CatalogXML, sn, id) +} + +// BuildRecordInfoXML 获取录像文件列表指令 +func BuildRecordInfoXML(sn int, id string, start, end int64) string { + return fmt.Sprintf(RecordInfoXML, sn, id, intTotime(start).Format("2006-01-02T15:04:05"), intTotime(end).Format("2006-01-02T15:04:05")) +} + +// BuildDevicePositionXML 订阅设备位置 +func BuildDevicePositionXML(sn int, id string, interval int) string { + return fmt.Sprintf(DevicePositionXML, sn, id, interval) +} + +func BuildAlarmResponseXML(id string) string { + return fmt.Sprintf(AlarmResponseXML, id) +} + +type ( + ChannelStatus string + Record struct { + DeviceID string + Name string + FilePath string + Address string + StartTime string + EndTime string + Secrecy int + Type string + } + ChannelInfo struct { + DeviceID string // 通道ID + ParentID string + Name string + Manufacturer string + Model string + Owner string + CivilCode string + Address string + Port int + Parental int + SafetyWay int + RegisterWay int + Secrecy int + Status ChannelStatus + } + Message struct { + XMLName xml.Name + CmdType string + SN int // 请求序列号,一般用于对应 request 和 response + DeviceID string + DeviceName string + Manufacturer string + Model string + Channel string + DeviceList []ChannelInfo `xml:"DeviceList>Item"` + RecordList []Record `xml:"RecordList>Item"` + SumNum int // 录像结果的总数 SumNum,录像结果会按照多条消息返回,可用于判断是否全部返回 + } +) + +func DecodeGB2312(v any, body []byte) error { + decoder := xml.NewDecoder(bytes.NewReader(body)) + decoder.CharsetReader = charset.NewReaderLabel + return decoder.Decode(v) +} + +func DecodeGbk(v any, body []byte) error { + decoder := xml.NewDecoder(transform.NewReader(bytes.NewReader(body), simplifiedchinese.GBK.NewDecoder())) + decoder.CharsetReader = charset.NewReaderLabel + return decoder.Decode(v) +} diff --git a/plugin/gb28181/pkg/port-manager.go b/plugin/gb28181/pkg/port-manager.go new file mode 100644 index 0000000..361e7c7 --- /dev/null +++ b/plugin/gb28181/pkg/port-manager.go @@ -0,0 +1,51 @@ +package gb28181 + +import ( + "errors" +) + +var ErrNoAvailablePorts = errors.New("no available ports") + +type PortManager struct { + recycle chan uint16 + max uint16 + pos uint16 + Valid bool +} + +func (pm *PortManager) Init(start, end uint16) { + pm.pos = start - 1 + pm.max = end + if pm.pos > 0 && pm.max > pm.pos { + pm.Valid = true + pm.recycle = make(chan uint16, pm.Range()) + } +} + +func (pm *PortManager) Range() uint16 { + return pm.max - pm.pos +} + +func (pm *PortManager) Recycle(p uint16) (err error) { + select { + case pm.recycle <- p: + return nil + default: + return ErrNoAvailablePorts + } +} + +func (pm *PortManager) GetPort() (p uint16, err error) { + select { + case p = <-pm.recycle: + return + default: + if pm.Range() > 0 { + pm.pos++ + p = pm.pos + return + } else { + return 0, ErrNoAvailablePorts + } + } +} diff --git a/plugin/gb28181/pkg/transceiver.go b/plugin/gb28181/pkg/transceiver.go index 1305c4a..521148e 100644 --- a/plugin/gb28181/pkg/transceiver.go +++ b/plugin/gb28181/pkg/transceiver.go @@ -1,10 +1,14 @@ package gb28181 import ( + "fmt" "github.com/pion/rtp" "m7s.live/m7s/v5" "m7s.live/m7s/v5/pkg" + "m7s.live/m7s/v5/pkg/config" "m7s.live/m7s/v5/pkg/util" + rtp2 "m7s.live/m7s/v5/plugin/rtp/pkg" + "net" "os" ) @@ -120,3 +124,24 @@ func (dec *Receiver) decProgramStreamMap() (err error) { } return nil } + +func (p *Receiver) ListenTCP(port uint16) (err error) { + var tcpConf config.TCP + tcpConf.ListenAddr = fmt.Sprintf(":%d", port) + tcpConf.ListenNum = 1 + return tcpConf.Listen(p.OnTCPConnect) +} + +func (p *Receiver) ReadRTP(rtp util.Buffer) (err error) { + if err = p.Unmarshal(rtp); err != nil { + return + } + p.FeedChan <- p.Packet.Payload + return +} + +func (p *Receiver) OnTCPConnect(conn *net.TCPConn) { + var reader = (*rtp2.TCP)(conn) + reader.Read(p.ReadRTP) + close(p.FeedChan) +} diff --git a/plugin/hls/pkg/ts/mpegts.go b/plugin/hls/pkg/ts/mpegts.go new file mode 100644 index 0000000..2ce36f5 --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts.go @@ -0,0 +1,577 @@ +package mpegts + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "m7s.live/m7s/v5/pkg/util" + //"sync" +) + +// NALU AUD 00 00 00 01 09 F0 + +const ( + TS_PACKET_SIZE = 188 + TS_DVHS_PACKET_SIZE = 192 + TS_FEC_PACKET_SIZE = 204 + + TS_MAX_PACKET_SIZE = 204 + + PID_PAT = 0x0000 + PID_CAT = 0x0001 + PID_TSDT = 0x0002 + PID_RESERVED1 = 0x0003 + PID_RESERVED2 = 0x000F + PID_NIT_ST = 0x0010 + PID_SDT_BAT_ST = 0x0011 + PID_EIT_ST = 0x0012 + PID_RST_ST = 0x0013 + PID_TDT_TOT_ST = 0x0014 + PID_NET_SYNC = 0x0015 + PID_RESERVED3 = 0x0016 + PID_RESERVED4 = 0x001B + PID_SIGNALLING = 0x001C + PID_MEASURE = 0x001D + PID_DIT = 0x001E + PID_SIT = 0x001F + PID_PMT = 0x0100 + PID_VIDEO = 0x0101 + PID_AUDIO = 0x0102 + // 0x0003 - 0x000F Reserved + // 0x0010 - 0x1FFE May be assigned as network_PID, Program_map_PID, elementary_PID, or for other purposes + // 0x1FFF Null Packet + + // program_association_section + // conditional_access_section + // TS_program_map_section + // TS_description_section + // ISO_IEC_14496_scene_description_section + // ISO_IEC_14496_object_descriptor_section + // Metadata_section + // IPMP_Control_Information_section (defined in ISO/IEC 13818-11) + TABLE_PAS = 0x00 + TABLE_CAS = 0x01 + TABLE_TSPMS = 0x02 + TABLE_TSDS = 0x03 + TABLE_ISO_IEC_14496_SDC = 0x04 + TABLE_ISO_IEC_14496_ODC = 0x05 + TABLE_MS = 0x06 + TABLE_IPMP_CIS = 0x07 + // 0x06 - 0x37 ITU-T Rec. H.222.0 | ISO/IEC 13818-1 reserved + // 0x38 - 0x3F Defined in ISO/IEC 13818-6 + // 0x40 - 0xFE User private + // 0xFF Forbidden + STREAM_TYPE_VIDEO_MPEG1 = 0x01 + STREAM_TYPE_VIDEO_MPEG2 = 0x02 + STREAM_TYPE_AUDIO_MPEG1 = 0x03 + STREAM_TYPE_AUDIO_MPEG2 = 0x04 + STREAM_TYPE_PRIVATE_SECTIONS = 0x05 + STREAM_TYPE_PRIVATE_DATA = 0x06 + STREAM_TYPE_MHEG = 0x07 + + STREAM_TYPE_H264 = 0x1B + STREAM_TYPE_H265 = 0x24 + STREAM_TYPE_AAC = 0x0F + STREAM_TYPE_G711A = 0x90 + STREAM_TYPE_G711U = 0x91 + STREAM_TYPE_G722_1 = 0x92 + STREAM_TYPE_G723_1 = 0x93 + STREAM_TYPE_G726 = 0x94 + STREAM_TYPE_G729 = 0x99 + + STREAM_TYPE_ADPCM = 0x11 + STREAM_TYPE_PCM = 0x0A + STREAM_TYPE_AC3 = 0x81 + STREAM_TYPE_DTS = 0x8A + STREAM_TYPE_LPCM = 0x8B + // 1110 xxxx + // 110x xxxx + STREAM_ID_VIDEO = 0xE0 // ITU-T Rec. H.262 | ISO/IEC 13818-2 or ISO/IEC 11172-2 or ISO/IEC14496-2 video stream number xxxx + STREAM_ID_AUDIO = 0xC0 // ISO/IEC 13818-3 or ISO/IEC 11172-3 or ISO/IEC 13818-7 or ISO/IEC14496-3 audio stream number x xxxx + + PAT_PKT_TYPE = 0 + PMT_PKT_TYPE = 1 + PES_PKT_TYPE = 2 +) + +// +// MPEGTS -> PAT + PMT + PES +// ES -> PES -> TS +// + +type MpegTsStream struct { + PAT MpegTsPAT // PAT表信息 + PMT MpegTsPMT // PMT表信息 + PESBuffer map[uint16]*MpegTsPESPacket + PESChan chan *MpegTsPESPacket +} + +// ios13818-1-CN.pdf 33/165 +// +// TS +// + +// Packet == Header + Payload == 188 bytes +type MpegTsPacket struct { + Header MpegTsHeader + Payload []byte +} + +// 前面32bit的数据即TS分组首部,它指出了这个分组的属性 +type MpegTsHeader struct { + SyncByte byte // 8 bits 同步字节,固定为0x47,表示后面是一个TS分组 + TransportErrorIndicator byte // 1 bit 传输错误标志位 + PayloadUnitStartIndicator byte // 1 bit 负载单元开始标志(packet不满188字节时需填充).为1时,表示在4个字节后,有一个调整字节 + TransportPriority byte // 1 bit 传输优先级 + Pid uint16 // 13 bits Packet ID号码,唯一的号码对应不同的包.为0表示携带的是PAT表 + TransportScramblingControl byte // 2 bits 加密标志位(00:未加密;其他表示已加密) + AdaptionFieldControl byte // 2 bits 附加区域控制.表示TS分组首部后面是否跟随有调整字段和有效负载.01仅含有效负载(没有adaptation_field),10仅含调整字段(没有Payload),11含有调整字段和有效负载(有adaptation_field,adaptation_field之后是Payload).为00的话解码器不进行处理.空分组没有调整字段 + ContinuityCounter byte // 4 bits 包递增计数器.范围0-15,具有相同的PID的TS分组传输时每次加1,到15后清0.不过,有些情况下是不计数的. + + MpegTsHeaderAdaptationField +} + +// 调整字段,只可能出现在每一帧的开头(当含有pcr的时候),或者结尾(当不满足188个字节的时候) +// adaptionFieldControl 00 -> 高字节代表调整字段, 低字节代表负载字段 0x20 0x10 +// PCR字段编码在MPEG-2 TS包的自适应字段(Adaptation field)的6个Byte中,其中6 bits为预留位,42 bits为有效位() +// MpegTsHeaderAdaptationField + stuffing bytes +type MpegTsHeaderAdaptationField struct { + AdaptationFieldLength byte // 8bits 本区域除了本字节剩下的长度(不包含本字节!!!切记), if adaptationFieldLength > 0, 那么就有下面8个字段. adaptation_field_length 值必须在 0 到 182 的区间内.当 adaptation_field_control 值为'10'时,adaptation_field_length 值必须为 183 + DiscontinuityIndicator byte // 1bit 置于"1"时,指示当前传输流包的不连续性状态为真.当 discontinuity_indicator 设置为"0"或不存在时,不连续性状态为假.不连续性指示符用于指示两种类型的不连续性,系统时间基不连续性和 continuity_counter 不连续性. + RandomAccessIndicator byte // 1bit 指示当前的传输流包以及可能的具有相同 PID 的后续传输流包,在此点包含有助于随机接入的某些信息.特别的,该比特置于"1"时,在具有当前 PID 的传输流包的有效载荷中起始的下一个 PES 包必须包含一个 discontinuity_indicator 字段中规定的基本流接入点.此外,在视频情况中,显示时间标记必须在跟随基本流接入点的第一图像中存在 + ElementaryStreamPriorityIndicator byte // 1bit 在具有相同 PID 的包之间,它指示此传输流包有效载荷内承载的基本流数据的优先级.1->指示该有效载荷具有比其他传输流包有效载荷更高的优先级 + PCRFlag byte // 1bit 1->指示 adaptation_field 包含以两部分编码的 PCR 字段.0->指示自适应字段不包含任何 PCR 字段 + OPCRFlag byte // 1bit 1->指示 adaptation_field 包含以两部分编码的 OPCR字段.0->指示自适应字段不包含任何 OPCR 字段 + SplicingPointFlag byte // 1bit 1->指示 splice_countdown 字段必须在相关自适应字段中存在,指定拼接点的出现.0->指示自适应字段中 splice_countdown 字段不存在 + TrasportPrivateDataFlag byte // 1bit 1->指示自适应字段包含一个或多个 private_data 字节.0->指示自适应字段不包含任何 private_data 字节 + AdaptationFieldExtensionFlag byte // 1bit 1->指示自适应字段扩展的存在.0->指示自适应字段中自适应字段扩展不存在 + + // Optional Fields + ProgramClockReferenceBase uint64 // 33 bits pcr + Reserved1 byte // 6 bits + ProgramClockReferenceExtension uint16 // 9 bits + OriginalProgramClockReferenceBase uint64 // 33 bits opcr + Reserved2 byte // 6 bits + OriginalProgramClockReferenceExtension uint16 // 9 bits + SpliceCountdown byte // 8 bits + TransportPrivateDataLength byte // 8 bits 指定紧随传输private_data_length 字段的 private_data 字节数. private_data 字节数不能使专用数据扩展超出自适应字段的范围 + PrivateDataByte byte // 8 bits 不通过 ITU-T|ISO/IEC 指定 + AdaptationFieldExtensionLength byte // 8 bits 指定紧随此字段的扩展的自适应字段数据的字节数,包括要保留的字节(如果存在) + LtwFlag byte // 1 bit 1->指示 ltw_offset 字段存在 + PiecewiseRateFlag byte // 1 bit 1->指示 piecewise_rate 字段存在 + SeamlessSpliceFlag byte // 1 bit 1->指示 splice_type 以及 DTS_next_AU 字段存在. 0->指示无论是 splice_type 字段还是 DTS_next_AU 字段均不存在 + + // Optional Fields + LtwValidFlag byte // 1 bit 1->指示 ltw_offset 的值必将生效.0->指示 ltw_offset 字段中该值未定义 + LtwOffset uint16 // 15 bits 其值仅当 ltw_valid 标志字段具有'1'值时才定义.定义时,法定时间窗补偿以(300/fs)秒为度量单位,其中 fs 为此 PID 所归属的节目的系统时钟频率 + Reserved3 byte // 2 bits 保留 + PiecewiseRate uint32 // 22 bits 只要当 ltw_flag 和 ltw_valid_flag 均置于‘1’时,此 22 比特字段的含义才确定 + SpliceType byte // 4 bits + DtsNextAU uint64 // 33 bits (解码时间标记下一个存取单元) + + // stuffing bytes + // 此为固定的 8 比特值等于'1111 1111',能够通过编码器插入.它亦能被解码器丢弃 +} + +// ios13818-1-CN.pdf 77 +// +// Descriptor +// + +type MpegTsDescriptor struct { + Tag byte // 8 bits 标识每一个描述符 + Length byte // 8 bits 指定紧随 descriptor_length 字段的描述符的字节数 + Data []byte +} + +func ReadTsPacket(r io.Reader) (packet MpegTsPacket, err error) { + lr := &io.LimitedReader{R: r, N: TS_PACKET_SIZE} + + // header + packet.Header, err = ReadTsHeader(lr) + if err != nil { + return + } + + // payload + packet.Payload = make([]byte, lr.N) + _, err = lr.Read(packet.Payload) + if err != nil { + return + } + + return +} + +func ReadTsHeader(r io.Reader) (header MpegTsHeader, err error) { + var h uint32 + + // MPEGTS Header 4个字节 + h, err = util.ReadByteToUint32(r, true) + if err != nil { + return + } + + // payloadUnitStartIndicator + // 为1时,表示在4个字节后,有一个调整字节.包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1") + // header.payloadUnitStartIndicator = uint8(h & 0x400000) + + // | 1111 1111 | 0000 0000 | 0000 0000 | 0000 0000 | + + // | 1111 1111 | 0000 0000 | 0000 0000 | 0000 0000 | + header.SyncByte = byte((h & 0xff000000) >> 24) + + if header.SyncByte != 0x47 { + err = errors.New("mpegts header sync error!") + return + } + + // | 0000 0000 | 1000 0000 | 0000 0000 | 0000 0000 | + header.TransportErrorIndicator = byte((h & 0x800000) >> 23) + + // | 0000 0000 | 0100 0000 | 0000 0000 | 0000 0000 | + header.PayloadUnitStartIndicator = byte((h & 0x400000) >> 22) + + // | 0000 0000 | 0010 0000 | 0000 0000 | 0000 0000 | + header.TransportPriority = byte((h & 0x200000) >> 21) + + // | 0000 0000 | 0001 1111 | 1111 1111 | 0000 0000 | + header.Pid = uint16((h & 0x1fff00) >> 8) + + // | 0000 0000 | 0000 0000 | 0000 0000 | 1100 0000 | + header.TransportScramblingControl = byte((h & 0xc0) >> 6) + + // | 0000 0000 | 0000 0000 | 0000 0000 | 0011 0000 | + // 0x30 , 0x20 -> adaptation_field, 0x10 -> Payload + header.AdaptionFieldControl = byte((h & 0x30) >> 4) + + // | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1111 | + header.ContinuityCounter = byte(h & 0xf) + + // | 0010 0000 | + // adaptionFieldControl + // 表示TS分组首部后面是否跟随有调整字段和有效负载. + // 01仅含有效负载(没有adaptation_field) + // 10仅含调整字段(没有Payload) + // 11含有调整字段和有效负载(有adaptation_field,adaptation_field之后是Payload). + // 为00的话解码器不进行处理.空分组没有调整字段 + // 当值为'11时,adaptation_field_length 值必须在0 到182 的区间内. + // 当值为'10'时,adaptation_field_length 值必须为183. + // 对于承载PES 包的传输流包,只要存在欠充足的PES 包数据就需要通过填充来完全填满传输流包的有效载荷字节. + // 填充通过规定自适应字段长度比自适应字段中数据元的长度总和还要长来实现,以致于自适应字段在完全容纳有效的PES 包数据后,有效载荷字节仍有剩余.自适应字段中额外空间采用填充字节填满. + if header.AdaptionFieldControl >= 2 { + // adaptationFieldLength + header.AdaptationFieldLength, err = util.ReadByteToUint8(r) + if err != nil { + return + } + + if header.AdaptationFieldLength > 0 { + lr := &io.LimitedReader{R: r, N: int64(header.AdaptationFieldLength)} + + // discontinuityIndicator(1) + // randomAccessIndicator(1) + // elementaryStreamPriorityIndicator + // PCRFlag + // OPCRFlag + // splicingPointFlag + // trasportPrivateDataFlag + // adaptationFieldExtensionFlag + var flags uint8 + flags, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + + header.DiscontinuityIndicator = flags & 0x80 + header.RandomAccessIndicator = flags & 0x40 + header.ElementaryStreamPriorityIndicator = flags & 0x20 + header.PCRFlag = flags & 0x10 + header.OPCRFlag = flags & 0x08 + header.SplicingPointFlag = flags & 0x04 + header.TrasportPrivateDataFlag = flags & 0x02 + header.AdaptationFieldExtensionFlag = flags & 0x01 + + // randomAccessIndicator + // 在此点包含有助于随机接入的某些信息. + // 特别的,该比特置于"1"时,在具有当前 PID 的传输流包的有效载荷中起始的下一个 PES 包必须包含一个 discontinuity_indicator 字段中规定的基本流接入点. + // 此外,在视频情况中,显示时间标记必须在跟随基本流接入点的第一图像中存在 + if header.RandomAccessIndicator != 0 { + } + + // PCRFlag + // 1->指示 adaptation_field 包含以两部分编码的 PCR 字段. + // 0->指示自适应字段不包含任何 PCR 字段 + if header.PCRFlag != 0 { + var pcr uint64 + pcr, err = util.ReadByteToUint48(lr, true) + if err != nil { + return + } + + // PCR(i) = PCR_base(i)*300 + PCR_ext(i) + // afd.programClockReferenceBase * 300 + afd.programClockReferenceExtension + header.ProgramClockReferenceBase = pcr >> 15 // 9 bits + 6 bits + header.ProgramClockReferenceExtension = uint16(pcr & 0x1ff) // 9 bits -> | 0000 0001 | 1111 1111 | + } + + // OPCRFlag + if header.OPCRFlag != 0 { + var opcr uint64 + opcr, err = util.ReadByteToUint48(lr, true) + if err != nil { + return + } + + // OPCR(i) = OPCR_base(i)*300 + OPCR_ext(i) + // afd.originalProgramClockReferenceBase * 300 + afd.originalProgramClockReferenceExtension + header.OriginalProgramClockReferenceBase = opcr >> 15 // 9 bits + 6 bits + header.OriginalProgramClockReferenceExtension = uint16(opcr & 0x1ff) // 9 bits -> | 0000 0001 | 1111 1111 | + } + + // splicingPointFlag + // 1->指示 splice_countdown 字段必须在相关自适应字段中存在,指定拼接点的出现. + // 0->指示自适应字段中 splice_countdown 字段不存在 + if header.SplicingPointFlag != 0 { + header.SpliceCountdown, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + } + + // trasportPrivateDataFlag + // 1->指示自适应字段包含一个或多个 private_data 字节. + // 0->指示自适应字段不包含任何 private_data 字节 + if header.TrasportPrivateDataFlag != 0 { + header.TransportPrivateDataLength, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + + // privateDataByte + b := make([]byte, header.TransportPrivateDataLength) + if _, err = lr.Read(b); err != nil { + return + } + } + + // adaptationFieldExtensionFlag + if header.AdaptationFieldExtensionFlag != 0 { + } + + // 消耗掉剩下的数据,我们不关心 + if lr.N > 0 { + // Discard 是一个 io.Writer,对它进行的任何 Write 调用都将无条件成功 + // 但是ioutil.Discard不记录copy得到的数值 + // 用于发送需要读取但不想存储的数据,目的是耗尽读取端的数据 + if _, err = io.CopyN(ioutil.Discard, lr, int64(lr.N)); err != nil { + return + } + } + + } + } + + return +} + +func WriteTsHeader(w io.Writer, header MpegTsHeader) (written int, err error) { + if header.SyncByte != 0x47 { + err = errors.New("mpegts header sync error!") + return + } + + h := uint32(header.SyncByte)<<24 + uint32(header.TransportErrorIndicator)<<23 + uint32(header.PayloadUnitStartIndicator)<<22 + uint32(header.TransportPriority)<<21 + uint32(header.Pid)<<8 + uint32(header.TransportScramblingControl)<<6 + uint32(header.AdaptionFieldControl)<<4 + uint32(header.ContinuityCounter) + if err = util.WriteUint32ToByte(w, h, true); err != nil { + return + } + + written += 4 + + if header.AdaptionFieldControl >= 2 { + // adaptationFieldLength(8) + if err = util.WriteUint8ToByte(w, header.AdaptationFieldLength); err != nil { + return + } + + written += 1 + + if header.AdaptationFieldLength > 0 { + + // discontinuityIndicator(1) + // randomAccessIndicator(1) + // elementaryStreamPriorityIndicator(1) + // PCRFlag(1) + // OPCRFlag(1) + // splicingPointFlag(1) + // trasportPrivateDataFlag(1) + // adaptationFieldExtensionFlag(1) + threeIndicatorFiveFlags := uint8(header.DiscontinuityIndicator<<7) + uint8(header.RandomAccessIndicator<<6) + uint8(header.ElementaryStreamPriorityIndicator<<5) + uint8(header.PCRFlag<<4) + uint8(header.OPCRFlag<<3) + uint8(header.SplicingPointFlag<<2) + uint8(header.TrasportPrivateDataFlag<<1) + uint8(header.AdaptationFieldExtensionFlag) + if err = util.WriteUint8ToByte(w, threeIndicatorFiveFlags); err != nil { + return + } + + written += 1 + + // PCR(i) = PCR_base(i)*300 + PCR_ext(i) + if header.PCRFlag != 0 { + pcr := header.ProgramClockReferenceBase<<15 | 0x3f<<9 | uint64(header.ProgramClockReferenceExtension) + if err = util.WriteUint48ToByte(w, pcr, true); err != nil { + return + } + + written += 6 + } + + // OPCRFlag + if header.OPCRFlag != 0 { + opcr := header.OriginalProgramClockReferenceBase<<15 | 0x3f<<9 | uint64(header.OriginalProgramClockReferenceExtension) + if err = util.WriteUint48ToByte(w, opcr, true); err != nil { + return + } + + written += 6 + } + } + + } + + return +} + +// +//func (s *MpegTsStream) TestWrite(fileName string) error { +// +// if fileName != "" { +// file, err := os.Create(fileName) +// if err != nil { +// panic(err) +// } +// defer file.Close() +// +// patTsHeader := []byte{0x47, 0x40, 0x00, 0x10} +// +// if err := WritePATPacket(file, patTsHeader, *s.pat); err != nil { +// panic(err) +// } +// +// // TODO:这里的pid应该是由PAT给的 +// pmtTsHeader := []byte{0x47, 0x41, 0x00, 0x10} +// +// if err := WritePMTPacket(file, pmtTsHeader, *s.pmt); err != nil { +// panic(err) +// } +// } +// +// var videoFrame int +// var audioFrame int +// for { +// tsPesPkt, ok := <-s.TsPesPktChan +// if !ok { +// fmt.Println("frame index, video , audio :", videoFrame, audioFrame) +// break +// } +// +// if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_AUDIO { +// audioFrame++ +// } +// +// if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_VIDEO { +// println(tsPesPkt.PesPkt.Header.Pts) +// videoFrame++ +// } +// +// fmt.Sprintf("%s", tsPesPkt) +// +// // if err := WritePESPacket(file, tsPesPkt.TsPkt.Header, tsPesPkt.PesPkt); err != nil { +// // return err +// // } +// +// } +// +// return nil +//} + +func (s *MpegTsStream) ReadPAT(packet *MpegTsPacket, pr io.Reader) (err error) { + // 首先找到PID==0x00的TS包(PAT) + if PID_PAT == packet.Header.Pid { + if len(packet.Payload) == 188 { + pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff} + } + // Header + PSI + Paylod + s.PAT, err = ReadPAT(pr) + } + return +} +func (s *MpegTsStream) ReadPMT(packet *MpegTsPacket, pr io.Reader) (err error) { + // 在读取PAT中已经将所有频道节目信息(PMT_PID)保存了起来 + // 接着读取所有TS包里面的PID,找出PID==PMT_PID的TS包,就是PMT表 + for _, v := range s.PAT.Program { + if v.ProgramMapPID == packet.Header.Pid { + if len(packet.Payload) == 188 { + pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff} + } + // Header + PSI + Paylod + s.PMT, err = ReadPMT(pr) + } + } + return +} +func (s *MpegTsStream) Feed(ts io.Reader) (err error) { + var reader bytes.Reader + var lr io.LimitedReader + lr.R = &reader + var tsHeader MpegTsHeader + tsData := make([]byte, TS_PACKET_SIZE) + for { + _, err = io.ReadFull(ts, tsData) + if err == io.EOF { + // 文件结尾 把最后面的数据发出去 + for _, pesPkt := range s.PESBuffer { + if pesPkt != nil { + s.PESChan <- pesPkt + } + } + return nil + } else if err != nil { + return + } + reader.Reset(tsData) + lr.N = TS_PACKET_SIZE + if tsHeader, err = ReadTsHeader(&lr); err != nil { + return + } + if tsHeader.Pid == PID_PAT { + if s.PAT, err = ReadPAT(&lr); err != nil { + return + } + continue + } + if len(s.PMT.Stream) == 0 { + for _, v := range s.PAT.Program { + if v.ProgramMapPID == tsHeader.Pid { + if s.PMT, err = ReadPMT(&lr); err != nil { + return + } + for _, v := range s.PMT.Stream { + s.PESBuffer[v.ElementaryPID] = nil + } + } + continue + } + } else if pesPkt, ok := s.PESBuffer[tsHeader.Pid]; ok { + if tsHeader.PayloadUnitStartIndicator == 1 { + if pesPkt != nil { + s.PESChan <- pesPkt + } + pesPkt = &MpegTsPESPacket{} + s.PESBuffer[tsHeader.Pid] = pesPkt + if pesPkt.Header, err = ReadPESHeader(&lr); err != nil { + return + } + } + io.Copy(&pesPkt.Payload, &lr) + } + } +} diff --git a/plugin/hls/pkg/ts/mpegts.md b/plugin/hls/pkg/ts/mpegts.md new file mode 100644 index 0000000..6497206 --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts.md @@ -0,0 +1,520 @@ +#MPEGTS + +---------- + +Name:苏荣 +Data:2016/5/27 09:03:30 + + +---------- + +## PSI(Program Specific Information) 节目特定信息 +PSI 可以认为属于 6 个表: +1) 节目相关表(PAT) +2) TS 节目映射表(PMT) +3) 网络信息表(NIT) +4) 有条件访问表(CAT) +5) 传输流描述表 +6) IPMP 控制信息表 + +##ES流(Elementary Stream):基本码流,不分段的音频、视频或其他信息的连续码流. + +##PES流:把基本流ES分割成段,并加上相应头文件打包成形的打包基本码流 + +##PS流(Program Stream):节目流,将具有共同时间基准的一个或多个PES组合(复合)而成的单一数据流(用于播放或编辑系统,如m2p). + +##TS流(Transport Stream):传输流,将具有共同时间基准或独立时间基准的一个或多个PES组合(复合)而成的单一数据流(用于数据传输). + +##PES ES TS +视频压缩成H264码流,可以称之为ES流,将其每帧打包为PES流,然后分拆为多个188字节,称为TS流. + +H264(ES) = PES1(一帧ES打包) + PES2(一帧ES打包) + PES3(一帧ES打包) + ... + +PES1 = PES1 Header + PES1 Payload = PES1 Packet Start Code Prefix + Stream ID + PES1 Packet Length + Send PES1 Header(不确定大小) + PES1 Payload + +PES1 Payload = TS1 Payload + TS2 Payload + TS3 Payload + ... + +PES1 = TS1 + TS2 + TS3 + .... + +PES1 = TS1(TS1 Header + PES1 Header + TS1 Payload) + TS2(有三种可能) + TS3(有三种可能) + ...... + +TS1(TS流第一个包) = TS1 Header + PES1 Header + TS1 Payload + +TS2(TS流第二个包,第一种情况) = TS2 Header + 自适应字段 + TS2 Payload (出现概率 1%) + +TS2(TS流第二个包,第二种情况) = TS2 Header + 自适应字段 (出现概率 0.1%) + +TS2(TS流第二个包,第三种情况) = TS2 Header + TS2 Payload (出现概率 98.9%) + +一段ES流 = N个PES(N帧) + +同一个PES的TS的PID是相同的 + +##寻找第一个TS包 +Header PID = 0x000 说明数据包是PAT表信息 +第一个TS包 一般叫做 PAT (Program Association Table,节目相关表) + +TS流 : PID=005 + PID=002 + PID=000 + +一般来说第一个TS包一般在第一个位置,本例举出一个特殊情况(第一个TS包在第三) + +在寻找第一个TS包时,不断读取TS包,直到找到pid=000的位置,并将读取过的TS包置入缓冲区 + +##寻找下一个TS包 +第二个TS包 一般叫做PMT(Program Map Table,节目映射表) + +##解析TS包 +payload_unit_start_indicator : 该字段用来表示TS包的有效净荷有PES包或者PSI数据的情况. + +当TS包带有PES包数据时(出现概率99.9%).不带PES包(出现概率0.1%). + +1. 当TS包带有PES包数据时,payload_unit_start_indicator具有以下的特点: +a. 置为1,标识TS包的有效净荷以PES包的第一个字节开始. +b. 置为0,表示TS包的开始不是PES包. + +2. 当TS包带有PSI数据时,payload_unit_start_indicator具有以下特点: +a. 置为1,表示TS包带有PSI部分的第一个字节,即第一个字节带有指针pointer_field. +b. 置为0,表示TS包不带有一个PSI部分的第一个字节,即在有效净荷中没有指针point_field. +c. 对于空包的包,payload_unit_start_indicator应该置为0 + +adaptionFieldControl: +01 -> 仅含有效负载(TS包第三种情况) +10 -> 仅含调整字段(TS包第二种情况) +11 -> 含有调整字段和有效负载(TS包第一种情况) + +TS流,通过一个个的TS包来传送. TS包可以是传送PSI SI等各表的数据包,也可以是传送节目音视频数据(携带的PES包:音视频基本流包)的包;TS携带 PSI SI等表的数据时,各个表以各表对应的Section语法格式做为传输单元存放到TS包中 以便传输; +TS包,有一个TS包的PID,系统就是根据这个PID来找对应的TS包;对于包含音视频数据(PES包)的TS包,系统通过TS的PID找到对应TS数据包,提取其中的数据组合成节目的音视频;对于携带PSI SI等数据的TS包,系统通过TS的PID找到对应TS数据包,提取各个PSI SI数据表格,用来指导系统;因此其中部分PID用来固定传输某些数据内容. + +有了TS的PID后, 如果TS包携带的是PSI SI等表格的Section数据时,有时还不能确定该PID的TS包中携带的数据是什么,SDT BAT ST 等表传送时,都用的是PID为0X0011的TS数据包,对于这种携带PSI SI Section单元的TS包,对应的数据(表的Section语法中)还有一个 TABLE_ID字段,用来可以确定是具体的什么表 + +因此PID+TableID就可以确定负载带了什么,是PES还是PSI. + + +---------- + + +1. 第一个包: + +包头 : 47 60 00 10 +0x47 : syncByte +0x6 : 0110(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1. +0x000 : 0 0000 0000 0000, pid = 0,说明是第一个TS包(PAT表) +0x10 : 0001 0000, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况) + +负载 : 00 00 B0 0D 00 00 C1 00 00 00 01 E0 +81 0C 8C BE 32 FF FF......FF + +指针 : 00 +table id : 00 +固定值 : B (1011) +section_length : 0 0D(值:13) +transport_stream_id : 00 00 +version number & current_next_indicator : C1 +section_number : 00 +last_section_number : 00 +program_number : 00 01 +program_map_PID : E0 81(因为program_number > 0) +CRC_32 : 0C 8C BE 32 + + if (program_number == 0) + { + network_PID + }else + { + program_map_PID + } + +E0 81 = reserved3 + program_map_PID = | 1110 0000 | 1000 0001 | +program_map_PID = 0x81(说明PMT的pid为081) + + +---------- + + +2. 第二个包 + +包头 : 47 60 81 10 +0x47 : syncByte +0x6 : 0110(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1. +0x081 : 0 0000 1000 0001, pid = 0x081(说明是PMT表,因为前面的PAT表给出了) +0x10 : 0001 0000, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况) + +负载 : 00 02 B0 17 00 01 C1 00 00 E8 10 F0 00 1B E8 10 +F0 00 03 E8 14 F0 00 66 74 A4 2D FF FF FF FF FF......FF + +指针 : 00 +table id : 02 +固定值 : B +section_length : 0 17(值:23,表示到后面FF FF FF FF FF FF之前总共有23个字节) +program_number : 00 01 +reserved2 & version_number & current_next_indicator : C1 +section_number : 00 +last_section_number : 00 +PCR_PID : E8 10 +program_info_length : F0 00 前4位为保留位 后12位为描述信息长度 此处为0 + +第一流分析 : 1B E8 10 F0 00 +stream_type : 1B 视频流(H264)(ITU-T H.264建议书| SO/IEC 14496-10 视频中定义的 AVC 视频流) +elementary_PID : E8 10 前3位为保留位取后13位 则PID=810 表示此PID的都是视频流 +ES_info_length : F0 00 前4位为保留位 后12位为描述信息长度 此处为0 + +第二流分析 : 03 E8 14 F0 00 +stream_type : 03 音频流(MP3) +elementary_PID : E8 14 前3位为保留位取后13位 则PID=814 表示此PID的都是音频流 +ES_info_length : F0 00 前4位为保留位 后12位为描述信息长度 此处为0 + + + +CRC : 66 74 A4 2D + + +reserved4 + program_info_length = | 1111 0000 | 0000 0000 | +program_info_length = 0 + +stream_type : 03 表示流是音频流 MP3 格式 814 表示 pid=814 的TS包存储的是MP3格式的音频流. +stream_type : 01 表示流是视频流 h264格式 810 表示 pid=810 的TS包存储的是h264格式的视频流 + + +---------- + + +3. 第三个包 +包头 : 47 48 14 10 +0x47 : syncByte +0x4 : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1. +0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3) +0x10 : 0001 0000, adaptionFieldControl = 01 + +这里: +payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头 +adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况) + +负载 : 00 00 01 C0 01 88 80 80 05 21 00 01 96 07 FF FD 85 00 33 22 22 11 22 11 11 11 11 11 11 24 82 41 00 90 40 00 00 00 00 00 40 00 ....... 70 34 5B CE 64 B7 D2 F5 4E 07 50 8E 11 1E 60 61 21 32 11 59 + +packetStartCodePrefix : 00 00 01 +streamID : C0 +pes_PacketLength : 01 88(值为392,占用392个字节,一帧数据长度,也可以置为0) +Sned PES HEADER : 占用不确定位 本例为:80 80 05 21 00 01 96 07 + + +Sned PES HEADER 包括以下几个字段: 80 80 05 21 00 01 96 07(解析为二进制显示) +| 8 0 | 8 0 | 0 5 | 2 1 | 0 0 | 0 1 | 9 6 | 0 7 | +| 1000 0000| 1000 0000 | 0000 0101 | 0010 0001 | 0000 0000 | 0000 0001 | 1001 0110 | 0000 1110 | + +(注意,下面的数值是用二进制表示,不特别声明,都是用16进制表示) +(0x80) +constTen : 10 固定 +PES_scrambling_control : 00 PES加扰控制 +PES_priority : 0 PES 包中该有效载荷的优先级 +data_alignment_indicator : 0 数据定位指示符 +copyright : 0 PES 包有效载荷的素材依靠版权所保护 +original_or_copy : 0 PES 包有效载荷的内容是原始的 + +(0x80) +PTS_DTS_flags : 10 PES 包头中 PTS 字段存在 +ESCR_flag : 0 +ES_rate_flag : 0 +DSM_trick_mode_flag : 0 +additional_copy_info_flag : 0 +PES_CRC_flag : 0 +PES_extension_flag : 0 + +(0x05) +PES_header_data_length : 0000 0101(值为5)PES头数据长度,表示后面还有5个字节,之后就是一帧的数据 + +(0x4200032C)(十进制:1107297068) +PTS(presentation time stamp): 0010 0001 0000 0000 0000 0001 1001 0110 0 + +下面字段在本例中都没有: +ESCR(42) = ESCR_base(33) + ESCR_extension(9) +ES_rate(22) +DSM特技方式(8) +additional_copy_info(7) +previous_PES_packet_CRC(16) +PES_Extension(不确定) + + +因为 PTS_DTS_flags == 10,所以本例中只有PTS没有DTS. + + +注意 : 本TS包 包含PES头信息 说明开始下一帧 + +---------- + + +4. 第四个包 +包头 : 47 08 14 11 +0x47 : syncByte +0x0 : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0. +0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3) +0x11 : 0001 0001, adaptionFieldControl = 01 + +这里: +payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头 +adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况) + +---------- + + +5. 第五个包 +包头 : 47 08 14 32 +0x47 : syncByte +0x0 : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0. +0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3) +0x32 : 0011 0010, adaptionFieldControl = 11 + +这里: +payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头 +adaptionFieldControl = 11, 说明先有自适应字段,再有有效载荷(TS包第一种情况) + +负载 : 99 00 FF FF FF ... FF 52 DE E6 B5 D0 76 CD CB B2 24 B3 92 AD 4E CD 19 D2 CC 82 D4 78 10 80 6C 0E 99 49 A4 59 C0 + +adaptation_field_length : 99(值为153,表示占用153个字节) + +discontinuity_indicator & random_access_indicator & +elementary_stream_priority_indicator & PCR_flag & +OPCR_flag & splicing_point_flag & +transport_private_data_flag & adaptation_field_extension_flag : 00 剩下的所有字段都为0 + +(00 FF FF FF ... FF)这里都是调整字段,从52 DE E6 B5 D0(从00(FF之前,99之后) 开始算是第1个字节,跳到第153个字节)开始,就是真正的帧数据了 + + +---------- + + +6. 第六个包 +包头 : 47 48 14 13 +0x47 : syncByte +0x4 : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1. +0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3) +0x13 : 0001 0011, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况) + +这里: +payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头 +adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况) + +负载 : 00 00 01 C0 01 88 80 80 05 21 00 01 A6 E7 FF FD + +packetStartCodePrefix : 00 00 01 +streamID : C0 +pes_PacketLength : 01 88(值为392,占用392个字节) +Sned PES HEADER : 占用不确定位 + +所以本包数据流ID 和 第二个包的流ID是一样的 + +注意 : 本TS包 又包含PES头信息 说明开始下一帧 + + +---------- + +7. 第七个包 +包头 : 47 48 10 30 +0x47 : syncByte +0x4 : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1. +0x810 : 0 1000 0001 0000, pid = 0x810(视频H264) +0x30 : 0011 0000, adaptionFieldControl = 11,说明含有调整字段和有效负载(TS包第一种情况) + +这里: +payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头 +adaptionFieldControl = 11, 说明含有调整字段和有效负载(TS包第一种情况) + +负载 : 07 10 00 00 01 0F 7E 88 00 00 01 E0 00 00 80 C0 0A 31 00 01 96 07 11 00 01 7E 91 00 00 00 01 67 4D 40 1E 96 ...... D2 99 71 F3 + +adaptation_field_length : 07(值为7,表示占用153个字节) + +discontinuity_indicator & random_access_indicator & +elementary_stream_priority_indicator & PCR_flag & +OPCR_flag & splicing_point_flag & +transport_private_data_flag & adaptation_field_extension_flag : 10 + +(10 00 00 01 0F 7E 88)调整字段 + +packetStartCodePrefix : 00 00 01 +streamID : EO +pes_PacketLength : 00 00(值为0,占用0个字节,一帧数据长度,也可以置为0,此时需要自己去计算) +Sned PES HEADER : 占用不确定位 + + +---------- + + +8. 第八个包 +包头 : 47 08 10 11 +0x47 : syncByte +0x0 : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0. +0x810 : 0 1000 0001 0000, pid = 0x810(视频H264) +0x11 : 0001 0001, adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况) + +这里: +payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头 +adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况) + + +---------- + +总结这个八个包: + +第一个TS包(PID:0X00) : 包含了PAT. +第二个TS包(PID:0X81) : 包含了PMT. +第三个TS包(PID:0x814) : 音频PES包头所有的TS包. +第四个TS包(PID:0x814) : 音频TS包. +第五个TS包(PID:0x814) : 音频TS包. +第六个TS包(PID:0x814) : 音频PES包头所有的TS包. +第七个TS包(PID:0x810) : 视频PES包头所有的TS包. +第八个TS包(PID:0x810) : 视频TS包. + + +---------- + + +// Packet Header: +// PID是TS流中唯一识别标志,Packet Data是什么内容就是由PID决定的.如果一个TS流中的一个Packet的Packet Header中的PID是0x0000, +// 那么这个Packet的Packet Data就是DVB的PAT表而非其他类型数据(如Video,Audio或其他业务信息). + +// 分析一个Header: +// 二进制: 0100 0111 0000 0111 1110 0101 0001 0010 +// 十六进制: 4 7 0 7 e 5 1 2 + +// syncByte = 0x47 就是0x47,这是DVB TS规定的同步字节,固定是0x47 +// transportErrorIndicator = 0 表示当前包没有发生传输错误 +// payloadUnitStartIndicator = 0 具体含义参考ISO13818-1标准文档 +// transportPriority = 0 表示当前包是低优先级 +// pid = 0x07e5(0 0111 1110 0101) Video PID +// transportScramblingControl = 00 表示节目没有加密 +// adaptionFieldControl = 01 具体含义参考ISO13818-1标准文档 +// continuityCounter = 0010 表示当前传送的相同类型的包是第3个 + + +---------- + + +// 分析一段TS流:(PAT) +// Packet Header : 0x47 0x40 0x00 0x10 +// Packet Data : 00 00 b0 11 00 01 c1 00 00 00 00 e0 1f 00 01 e1 00 24 ac48 84 ff ff ... ff ff + +// Header PID = 0x0000 说明数据包是PAT表信息,包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1") +// 所以,Packet Data就应该是 : 00 b0 11 00 01 c1 00 00 00 00 e0 1f 00 01 e1 00 24 ac48 84 ff ff ... ff ff + +// +// 00 | b0 11 | 00 01 | c1 | 00 | 00 | 00 00 | e0 1f | 00 01 e1 00 | +// + +// table_id = 0000 0000 + +// section_syntax_indicator = 1 +// zero = 0 +// reserved1 = 11 +// sectionLength = 0000 0001 0001 + +// transportStreamID = 0000 0000 0000 0001 + +// reserved2 = 11 +// versionNumber = 0000 0 +// currentNextIndicator 1 + +// sectionNumber = 0000 0000 + +// lastSectionNumber = 0000 0000 + +// programNumber = 0000 0000 0000 0000 + +// reserved3 = 111 +// networkPID = 0 0000 0001 1111 + +// crc32 + + +---------- + + +// 分析一段TS流:(PMT) +// Packet Header : 0x47 0x43 0xe8 0x12 +// Packet Data : 00 02 b0 12 00 01 c1 00 00 e3 e9 f0 00 1b e3 e9 f0 00 f0 af b4 4f ff ff ... ff ff + +// Header PID = 0x03e8 说明数据包是PMT表信息,包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1") +// 所以,Packet Data就应该是 : 02 b0 12 00 01 c1 00 00 e3 e9 f0 00 1b e3 e9 f0 00 f0 af b4 4f ff ff ... ff ff + +// 1 2 3 4 5 6 7 8 9 10 11 12 +// 02 | b0 12 | 00 01 | c1 | 00 | 00 | e3 e9 | f0 00 | 1b | e3 e9 | f0 00 | f0 af b4 4f | +// + +// 1: +// table_id = 0000 0010 + +// 2: +// section_syntax_indicator = 1 +// zero = 0 +// reserved1 = 11 +// section_length = 0000 0001 0010 + +// 3: +// program_number = 0000 0000 0000 0001 + +// 4: +// reserved2 = 11 +// version_number = 00 000 +// current_next_indicator = 1 + +// 5: +// section_number = 0000 0000 + +// 6: +// last_section_number = 0000 0000 + +// 7: +// reserved3 = 111 +// PCR_PID = 0 0011 1110 1001 + +// 8: +// reserved4 = 1111 +// program_info_length = 0000 0000 0000 + +// 9: +// stream_type = 0001 1011 + +// 10: +// reserved5 = 111 +// elementary_PID = 0 0011 1110 1001 + +// 11: +// reserved6 = 1111 +// ES_info_length = 0000 0000 0000 + +// 12: +// crc + + +---------- + + +##TS流解码过程 +1. 获取TS中的PAT +2. 获取TS中的PMT +3. 根据PMT可以知道当前网络中传输的视频(音频)类型(H264),相应的PID,PCR的PID等信息. +4. 设置demux 模块的视频Filter 为相应视频的PID和stream type等. +5. 从视频Demux Filter 后得到的TS数据包中的payload 数据就是 one piece of PES,在TS header中有一些关于此 payload属于哪个 PES的 第多少个数据包. 因此软件中应该将此payload中的数据copy到PES的buffer中,用于拼接一个PES包. +6. 拼接好的PES包的包头会有 PTS,DTS信息,去掉PES的header就是 ES. +7. 直接将 被被拔掉 PES包头的ES包送给decoder就可以进行解码.解码出来的数据就是一帧一帧的视频数据,这些数据至少应当与PES中的PTS关联一下,以便进行视音频同步. +8. I,B,B,P 信息是在ES中的. + + +---------- + + +1. 首先找到PID为0x00的TS包,找到里面的节目映射表(PMT)PID,因为可能有几个节目信息.所以可能有几个PMT_PID,以一个为例 +2. 接着查找该PMT_PID的TS包,通常就紧接着.在该PMT包中找音频和视频的PID.以视频为例. +3. 开始提取一帧ES数据 + 3.1 查找视频PID的TS包 + 3.2 找PES包头,方法:TS包头第2个字节的高6位(有效载荷单元起始指示符)为1的TS包,跳过自适应字段,找到PES包头,提取时间戳,再跳至ES数据,这就是一帧ES数据的开始部分. + 3.3 查找有效载荷单元起始指示符为0的TS包.跳过TS包头,跳过自适应字段,提取后面的ES数据 + 3.4 同3.3接着查找 + 3.5 当碰到有效载荷单元起始指示符又变为1的视频TS包,就知道这是下一帧的开始了,将前面的所有ES数据组合成一帧数据.开始下一轮组帧. + + +---------- + + +##参考文档: + +1. [TS流](http://blog.csdn.net/cabbage2008/article/category/5885203) +1. [TS各个表 与 SECTION 的解析 CAS原理 ](http://blog.sina.com.cn/s/blog_6b94d5680101r5l6.html) \ No newline at end of file diff --git a/plugin/hls/pkg/ts/mpegts_crc32.go b/plugin/hls/pkg/ts/mpegts_crc32.go new file mode 100644 index 0000000..b32d747 --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts_crc32.go @@ -0,0 +1,72 @@ +package mpegts + +import "net" + +// http://www.stmc.edu.hk/~vincent/ffmpeg_0.4.9-pre1/libavformat/mpegtsenc.c + +var Crc32_Table = []uint32{ + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, + 0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7, + 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, + 0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef, + 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, + 0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0, + 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, + 0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08, + 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, + 0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050, + 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, + 0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1, + 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, + 0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9, + 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, + 0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71, + 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, + 0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e, + 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, + 0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676, + 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, + 0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4, +} + +func GetCRC32(data []byte) (crc uint32) { + crc = 0xffffffff + + for _, v := range data { + crc = (crc << 8) ^ Crc32_Table[((crc>>24)^uint32(v))&0xff] + + } + + return +} + +func GetCRC32_2(data net.Buffers) (crc uint32) { + crc = 0xffffffff + for _, v := range data { + for _, v2 := range v { + crc = (crc << 8) ^ Crc32_Table[((crc>>24)^uint32(v2))&0xff] + } + } + return +} diff --git a/plugin/hls/pkg/ts/mpegts_pat.go b/plugin/hls/pkg/ts/mpegts_pat.go new file mode 100644 index 0000000..860f4ee --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts_pat.go @@ -0,0 +1,229 @@ +package mpegts + +import ( + "bytes" + "errors" + "fmt" + "io" + "m7s.live/m7s/v5/pkg/util" +) + +// ios13818-1-CN.pdf 43(57)/166 +// +// PAT +// + +var DefaultPATPacket = []byte{ + // TS Header + 0x47, 0x40, 0x00, 0x10, + + // Pointer Field + 0x00, + + // PSI + 0x00, 0xb0, 0x0d, 0x00, 0x01, 0xc1, 0x00, 0x00, + + // PAT + 0x00, 0x01, 0xe1, 0x00, + + // CRC + 0xe8, 0xf9, 0x5e, 0x7d, + + // Stuffing 167 bytes + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +} + +// TS Header : +// SyncByte = 0x47 +// TransportErrorIndicator = 0(B:0), PayloadUnitStartIndicator = 1(B:0), TransportPriority = 0(B:0), +// Pid = 0, +// TransportScramblingControl = 0(B:00), AdaptionFieldControl = 1(B:01), ContinuityCounter = 0(B:0000), + +// PSI : +// TableID = 0x00, +// SectionSyntaxIndicator = 1(B:1), Zero = 0(B:0), Reserved1 = 3(B:11), +// SectionLength = 13(0x00d) +// TransportStreamID = 0x0001 +// Reserved2 = 3(B:11), VersionNumber = (B:00000), CurrentNextIndicator = 1(B:0), +// SectionNumber = 0x00 +// LastSectionNumber = 0x00 + +// PAT : +// ProgramNumber = 0x0001 +// Reserved3 = 15(B:1110), ProgramMapPID = 4097(0x1001) + +// PAT表主要包含频道号码和每一个频道对应的PMT的PID号码,这些信息我们在处理PAT表格的时候会保存起来,以后会使用到这些数据 +type MpegTsPATProgram struct { + ProgramNumber uint16 // 16 bit 节目号 + Reserved3 byte // 3 bits 保留位 + NetworkPID uint16 // 13 bits 网络信息表(NIT)的PID,节目号为0时对应的PID为network_PID + ProgramMapPID uint16 // 13 bit 节目映射表的PID,节目号大于0时对应的PID.每个节目对应一个 +} + +// Program Association Table (节目关联表) +// 节目号为0x0000时,表示这是NIT,PID=0x001f,即3. +// 节目号为0x0001时,表示这是PMT,PID=0x100,即256 +type MpegTsPAT struct { + // PSI + TableID byte // 8 bits 0x00->PAT,0x02->PMT + SectionSyntaxIndicator byte // 1 bit 段语法标志位,固定为1 + Zero byte // 1 bit 0 + Reserved1 byte // 2 bits 保留位 + SectionLength uint16 // 12 bits 该字段的头两比特必为'00',剩余 10 比特指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC.此字段中的值应不超过 1021(0x3FD) + TransportStreamID uint16 // 16 bits 该字段充当标签,标识网络内此传输流有别于任何其他多路复用流.其值由用户规定 + Reserved2 byte // 2 bits 保留位 + VersionNumber byte // 5 bits 范围0-31,表示PAT的版本号 + CurrentNextIndicator byte // 1 bit 发送的PAT是当前有效还是下一个PAT有效,0则要等待下一个表 + SectionNumber byte // 8 bits 分段的号码.PAT可能分为多段传输.第一段为00,以后每个分段加1,最多可能有256个分段 + LastSectionNumber byte // 8 bits 最后一个分段的号码 + + // N Loop + Program []MpegTsPATProgram // PAT表里面的所有频道索引信息 + + Crc32 uint32 // 32 bits 包含处理全部传输流节目映射分段之后,在附件 B 规定的解码器中给出寄存器零输出的 CRC 值 +} + +func ReadPAT(r io.Reader) (pat MpegTsPAT, err error) { + lr, psi, err := ReadPSI(r, PSI_TYPE_PAT) + if err != nil { + return + } + + pat = psi.Pat + + // N Loop + // 一直循环去读4个字节,用lr的原因是确保不会读过头了. + for lr.N > 0 { + + // 获取每一个频道的节目信息,保存起来 + programs := MpegTsPATProgram{} + + programs.ProgramNumber, err = util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + // 如果programNumber为0,则是NetworkPID,否则是ProgramMapPID(13) + if programs.ProgramNumber == 0 { + programs.NetworkPID, err = util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + programs.NetworkPID = programs.NetworkPID & 0x1fff + } else { + programs.ProgramMapPID, err = util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + programs.ProgramMapPID = programs.ProgramMapPID & 0x1fff + } + + pat.Program = append(pat.Program, programs) + } + if cr, ok := r.(*util.Crc32Reader); ok { + err = cr.ReadCrc32UIntAndCheck() + if err != nil { + return + } + } + + return +} + +func WritePAT(w io.Writer, pat MpegTsPAT) (err error) { + bw := &bytes.Buffer{} + + // 将pat(所有的节目索引信息)写入到缓冲区中 + for _, pats := range pat.Program { + if err = util.WriteUint16ToByte(bw, pats.ProgramNumber, true); err != nil { + return + } + + if pats.ProgramNumber == 0 { + if err = util.WriteUint16ToByte(bw, pats.NetworkPID&0x1fff|7<<13, true); err != nil { + return + } + } else { + // | 0001 1111 | 1111 1111 | + // 7 << 13 -> 1110 0000 0000 0000 + if err = util.WriteUint16ToByte(bw, pats.ProgramMapPID&0x1fff|7<<13, true); err != nil { + return + } + } + } + + if pat.SectionLength == 0 { + pat.SectionLength = 2 + 3 + 4 + uint16(len(bw.Bytes())) + } + + psi := MpegTsPSI{} + + psi.Pat = pat + + if err = WritePSI(w, PSI_TYPE_PAT, psi, bw.Bytes()); err != nil { + return + } + + return +} + +func WritePATPacket(w io.Writer, tsHeader []byte, pat MpegTsPAT) (err error) { + if pat.TableID != TABLE_PAS { + err = errors.New("PAT table ID error") + return + } + + // 将所有要写的数据(PAT),全部放入到buffer中去. + // buffer 里面已经写好了整个pat表(PointerField+PSI+PAT+CRC) + bw := &bytes.Buffer{} + if err = WritePAT(bw, pat); err != nil { + return + } + + // TODO:如果Pat.Program里面包含的信息很大,大于188? + stuffingBytes := util.GetFillBytes(0xff, TS_PACKET_SIZE-4-bw.Len()) + + // PATPacket = TsHeader + PAT + Stuffing Bytes + var PATPacket []byte + PATPacket = append(PATPacket, tsHeader...) + PATPacket = append(PATPacket, bw.Bytes()...) + PATPacket = append(PATPacket, stuffingBytes...) + + fmt.Println("-------------------------") + fmt.Println("Write PAT :", PATPacket) + fmt.Println("-------------------------") + + // 写PAT负载 + if _, err = w.Write(PATPacket); err != nil { + return + } + + return +} + +func WriteDefaultPATPacket(w io.Writer) (err error) { + _, err = w.Write(DefaultPATPacket) + if err != nil { + return + } + + return +} diff --git a/plugin/hls/pkg/ts/mpegts_pes.go b/plugin/hls/pkg/ts/mpegts_pes.go new file mode 100644 index 0000000..78a94cb --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts_pes.go @@ -0,0 +1,454 @@ +package mpegts + +import ( + "errors" + "io" + "m7s.live/m7s/v5/pkg/util" + "net" +) + +// ios13818-1-CN.pdf 45/166 +// +// PES +// + +// 每个传输流和节目流在逻辑上都是由 PES 包构造的 +type MpegTsPesStream struct { + TsPkt MpegTsPacket + PesPkt MpegTsPESPacket +} + +// PES--Packetized Elementary Streams (分组的ES),ES形成的分组称为PES分组,是用来传递ES的一种数据结构 +// 1110 xxxx 为视频流(0xE0) +// 110x xxxx 为音频流(0xC0) +type MpegTsPESPacket struct { + Header MpegTsPESHeader + Payload util.Buffer //从TS包中读取的数据 + Buffers net.Buffers //用于写TS包 +} + +type MpegTsPESHeader struct { + PacketStartCodePrefix uint32 // 24 bits 同跟随它的 stream_id 一起组成标识包起始端的包起始码.packet_start_code_prefix 为比特串"0000 0000 0000 0000 0000 0001"(0x000001) + StreamID byte // 8 bits stream_id 指示基本流的类型和编号,如 stream_id 表 2-22 所定义的.传输流中,stream_id 可以设置为准确描述基本流类型的任何有效值,如表 2-22 所规定的.传输流中,基本流类型在 2.4.4 中所指示的节目特定信息中指定 + PesPacketLength uint16 // 16 bits 指示 PES 包中跟随该字段最后字节的字节数.0->指示 PES 包长度既未指示也未限定并且仅在这样的 PES 包中才被允许,该 PES 包的有效载荷由来自传输流包中所包含的视频基本流的字节组成 + + MpegTsOptionalPESHeader + + PayloadLength uint64 // 这个不是标准文档里面的字段,是自己添加的,方便计算 +} + +// 可选的PES Header = MpegTsOptionalPESHeader + stuffing bytes(0xFF) m * 8 +type MpegTsOptionalPESHeader struct { + ConstTen byte // 2 bits 常量10 + PesScramblingControl byte // 2 bit 指示 PES 包有效载荷的加扰方式.当加扰在 PES 等级上实施时, PES 包头,其中包括任选字段只要存在,应不加扰(见表 2-23) + PesPriority byte // 1 bit 指示在此 PES 包中该有效载荷的优先级.1->指示该 PES 包有效载荷比具有此字段置于"0"的其他 PES 包有效载荷有更高的有效载荷优先级.多路复用器能够使用该PES_priority 比特最佳化基本流内的数据 + DataAlignmentIndicator byte // 1 bit 1->指示 PES 包头之后紧随 2.6.10 中data_stream_alignment_descriptor 字段中指示的视频句法单元或音频同步字,只要该描述符字段存在.若置于值"1"并且该描述符不存在,则要求表 2-53,表 2-54 或表 2-55 的 alignment_type"01"中所指示的那种校准.0->不能确定任何此类校准是否发生 + Copyright byte // 1 bit 1->指示相关 PES 包有效载荷的素材依靠版权所保护.0->不能确定该素材是否依靠版权所保护 + OriginalOrCopy byte // 1 bit 1->指示相关 PES 包有效载荷的内容是原始的.0->指示相关 PES 包有效载荷的内容是复制的 + PtsDtsFlags byte // 2 bits 10->PES 包头中 PTS 字段存在. 11->PES 包头中 PTS 字段和 DTS 字段均存在. 00->PES 包头中既无任何 PTS 字段也无任何 DTS 字段存在. 01->禁用 + EscrFlag byte // 1 bit 1->指示 PES 包头中 ESCR 基准字段和 ESCR 扩展字段均存在.0->指示无任何 ESCR 字段存在 + EsRateFlag byte // 1 bit 1->指示 PES 包头中 ES_rate 字段存在.0->指示无任何 ES_rate 字段存在 + DsmTrickModeFlag byte // 1 bit 1->指示 8 比特特技方式字段存在.0->指示此字段不存在 + AdditionalCopyInfoFlag byte // 1 bit 1->指示 additional_copy_info 存在.0->时指示此字段不存在 + PesCRCFlag byte // 1 bit 1->指示 PES 包中 CRC 字段存在.0->指示此字段不存在 + PesExtensionFlag byte // 1 bit 1->时指示 PES 包头中扩展字段存在.0->指示此字段不存在 + PesHeaderDataLength byte // 8 bits 指示在此 PES包头中包含的由任选字段和任意填充字节所占据的字节总数.任选字段的存在由前导 PES_header_data_length 字段的字节来指定 + + // Optional Field + Pts uint64 // 33 bits 指示时间与解码时间的关系如下: PTS 为三个独立字段编码的 33 比特数.它指示基本流 n 的显示单元 k 在系统目标解码器中的显示时间 tpn(k).PTS 值以系统时钟频率除以 300(产生 90 kHz)的周期为单位指定.显示时间依照以下公式 2-11 从 PTS 中推出.有关编码显示时间标记频率上的限制参阅 2.7.4 + Dts uint64 // 33 bits 指示基本流 n 的存取单元 j 在系统目标解码器中的解码时间 tdn(j). DTS 的值以系统时钟频率除以 300(生成90 kHz)的周期为单位指定.依照以下公式 2-12 从 DTS 中推出解码时间 + EscrBase uint64 // 33 bits 其值由 ESCR_base(i) 给出,如公式 2-14 中给出的 + EscrExtension uint16 // 9 bits 其值由 ESCR_ext(i) 给出,如公式 2-15 中给出的. ESCR 字段指示包含 ESCR_base 最后比特的字节到达 PES流的 PES-STD 输入端的预期时间(参阅 2.5.2.4) + EsRate uint32 // 22 bits 在PES 流情况中,指定系统目标解码器接收 PES 包字节的速率.ES_rate 在包括它的 PES 包以及相同 PES 流的后续 PES 包中持续有效直至遇到新的 ES_rate 字段时为止.ES 速率值以 50 字节/秒为度量单位.0 值禁用 + TrickModeControl byte // 3 bits 指示适用于相关视频流的特技方式.在其他类型基本流的情况中,此字段以及后随 5 比特所规定的那些含义未确定.对于 trick_mode 状态的定义,参阅 2.4.2.3 的特技方式段落 + TrickModeValue byte // 5 bits + AdditionalCopyInfo byte // 7 bits 包含与版权信息有关的专用数据 + PreviousPESPacketCRC uint16 // 16 bits 包含产生解码器中 16 寄存器零输出的 CRC 值, 类似于附件 A 中定义的解码器. 但在处理先前的 PES 包数据字节之后, PES 包头除外,采用多项式 + + // PES Extension + PesPrivateDataFlag byte // 1 bit 1->指示该 PES 包头包含专用数据. 0->指示 PES 包头中不存在专用数据 + PackHeaderFieldFlag byte // 1 bit 1->指示 ISO/IEC 11172-1 包头或节目流包头在此 PES包头中存储.若此字段处于节目流中包含的 PES 包中,则此字段应设置为"0.传输流中, 0->指示该 PES 头中无任何包头存在 + ProgramPacketSequenceCounterFlag byte // 1 bit 1->指示 program_packet_sequence_counter, MPEG1_MPEG2_identifier 以及 original_stuff_length 字段在 PES 包中存在.0->它指示这些字段在 PES 头中不存在 + PSTDBufferFlag byte // 1 bit 1->指示 P-STD_buffer_scale 和 P-STD_buffer_size 在 PES包头中存在.0->指示这些字段在 PES 头中不存在 + Reserved byte // 3 bits + PesExtensionFlag2 byte // 1 bits 1->指示 PES_extension_field_length 字段及相关的字段存在.0->指示 PES_extension_field_length 字段以及任何相关的字段均不存在. + + // Optional Field + PesPrivateData [16]byte // 128 bits 此数据,同前后字段数据结合,应不能仿真packet_start_code_prefix (0x000001) + PackHeaderField byte // 8 bits 指示 pack_header_field() 的长度,以字节为单位 + ProgramPacketSequenceCounter byte // 7 bits + Mpeg1Mpeg2Identifier byte // 1 bit 1->指示此 PES 包承载来自 ISO/IEC 11172-1 流的信息.0->指示此 PES 包承载来自节目流的信息 + OriginalStuffLength byte // 6 bits 在原始 ITU-T H.222.0 建议书| ISO/IEC 13818-1 PES 包头或在原始 ISO/IEC 11172-1 包头中所使用的填充字节数 + PSTDBufferScale byte // 1bit 它的含义仅当节目流中包含此 PES 包时才规定.它指示所使用的标度因子用于解释后续的 P-STD_buffer_size 字段.若前导 stream_id 指示音频流,则P-STD 缓冲器标度字段必为"0"值.若前导 stream_id 指示视频流,则 P-STD_buffer_scale 字段必为"1"值.对于所有其他流类型,该值可为"1"或为"0" + PSTDBufferSize uint16 // 13 bits 其含义仅当节目流中包含此 PES包时才规定.它规定在 P-STD 中,输入缓冲器 BSn 的尺寸.若 STD_buffer_scale 为 "0"值,则 P-STD_buffer_size以 128 字节为单位度量该缓冲器尺寸.若 P-STD_buffer_scale 为"1",则 P-STD_buffer_size 以 1024 字节为单位度量该缓冲器尺寸 + PesExtensionFieldLength byte // 7 bits 指示 PES 扩展字段中跟随此长度字段的直至并包括任何保留字节为止的数据长度,以字节为度量单位 + StreamIDExtensionFlag byte // 1 bits + //pesExtensionField []byte // PES_extension_field_length bits + //packField []byte // pack_field_length bits +} + +// pts_dts_Flags == "10" -> PTS +// 0010 4 +// PTS[32...30] 3 +// marker_bit 1 +// PTS[29...15] 15 +// marker_bit 1 +// PTS[14...0] 15 +// marker_bit 1 + +// pts_dts_Flags == "11" -> PTS + DTS + +type MpegtsPESFrame struct { + Pid uint16 + IsKeyFrame bool + ContinuityCounter byte + ProgramClockReferenceBase uint64 +} + +func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) { + var flags uint8 + var length uint + + // packetStartCodePrefix(24) (0x000001) + header.PacketStartCodePrefix, err = util.ReadByteToUint24(r, true) + if err != nil { + return + } + + if header.PacketStartCodePrefix != 0x0000001 { + err = errors.New("read PacketStartCodePrefix is not 0x0000001") + return + } + + // streamID(8) + header.StreamID, err = util.ReadByteToUint8(r) + if err != nil { + return + } + + // pes_PacketLength(16) + header.PesPacketLength, err = util.ReadByteToUint16(r, true) + if err != nil { + return + } + + length = uint(header.PesPacketLength) + + // PES包长度可能为0,这个时候,需要自己去算 + // 0 <= len <= 65535 + // 如果当length为0,那么先设置为最大值,然后用LimitedReade去读,如果读到最后面剩下的字节数小于65536,才是正确的包大小. + // 一个包一般情况下不可能会读1<<31个字节. + if length == 0 { + length = 1 << 31 + } + + // lrPacket 和 lrHeader 位置指针是在同一位置的 + lrPacket := &io.LimitedReader{R: r, N: int64(length)} + lrHeader := lrPacket + + // constTen(2) + // pes_ScramblingControl(2) + // pes_Priority(1) + // dataAlignmentIndicator(1) + // copyright(1) + // originalOrCopy(1) + flags, err = util.ReadByteToUint8(lrHeader) + if err != nil { + return + } + + header.ConstTen = flags & 0xc0 + header.PesScramblingControl = flags & 0x30 + header.PesPriority = flags & 0x08 + header.DataAlignmentIndicator = flags & 0x04 + header.Copyright = flags & 0x02 + header.OriginalOrCopy = flags & 0x01 + + // pts_dts_Flags(2) + // escr_Flag(1) + // es_RateFlag(1) + // dsm_TrickModeFlag(1) + // additionalCopyInfoFlag(1) + // pes_CRCFlag(1) + // pes_ExtensionFlag(1) + flags, err = util.ReadByteToUint8(lrHeader) + if err != nil { + return + } + + header.PtsDtsFlags = flags & 0xc0 + header.EscrFlag = flags & 0x20 + header.EsRateFlag = flags & 0x10 + header.DsmTrickModeFlag = flags & 0x08 + header.AdditionalCopyInfoFlag = flags & 0x04 + header.PesCRCFlag = flags & 0x02 + header.PesExtensionFlag = flags & 0x01 + + // pes_HeaderDataLength(8) + header.PesHeaderDataLength, err = util.ReadByteToUint8(lrHeader) + if err != nil { + return + } + + length = uint(header.PesHeaderDataLength) + + lrHeader = &io.LimitedReader{R: lrHeader, N: int64(length)} + + // 00 -> PES 包头中既无任何PTS 字段也无任何DTS 字段存在 + // 10 -> PES 包头中PTS 字段存在 + // 11 -> PES 包头中PTS 字段和DTS 字段均存在 + // 01 -> 禁用 + + // PTS(33) + if flags&0x80 != 0 { + var pts uint64 + pts, err = util.ReadByteToUint40(lrHeader, true) + if err != nil { + return + } + + header.Pts = util.GetPtsDts(pts) + } + + // DTS(33) + if flags&0x80 != 0 && flags&0x40 != 0 { + var dts uint64 + dts, err = util.ReadByteToUint40(lrHeader, true) + if err != nil { + return + } + + header.Dts = util.GetPtsDts(dts) + } + + // reserved(2) + escr_Base1(3) + marker_bit(1) + + // escr_Base2(15) + marker_bit(1) + escr_Base23(15) + + // marker_bit(1) + escr_Extension(9) + marker_bit(1) + if header.EscrFlag != 0 { + _, err = util.ReadByteToUint48(lrHeader, true) + if err != nil { + return + } + + //s.pes.escr_Base = escrBaseEx & 0x3fffffffe00 + //s.pes.escr_Extension = uint16(escrBaseEx & 0x1ff) + } + + // es_Rate(22) + if header.EsRateFlag != 0 { + header.EsRate, err = util.ReadByteToUint24(lrHeader, true) + if err != nil { + return + } + } + + // 不知道为什么这里不用 + /* + // trickModeControl(3) + trickModeValue(5) + if s.pes.dsm_TrickModeFlag != 0 { + trickMcMv, err := util.ReadByteToUint8(lrHeader) + if err != nil { + return err + } + + s.pes.trickModeControl = trickMcMv & 0xe0 + s.pes.trickModeValue = trickMcMv & 0x1f + } + */ + + // marker_bit(1) + additionalCopyInfo(7) + if header.AdditionalCopyInfoFlag != 0 { + header.AdditionalCopyInfo, err = util.ReadByteToUint8(lrHeader) + if err != nil { + return + } + + header.AdditionalCopyInfo = header.AdditionalCopyInfo & 0x7f + } + + // previous_PES_Packet_CRC(16) + if header.PesCRCFlag != 0 { + header.PreviousPESPacketCRC, err = util.ReadByteToUint16(lrHeader, true) + if err != nil { + return + } + } + + // pes_PrivateDataFlag(1) + packHeaderFieldFlag(1) + programPacketSequenceCounterFlag(1) + + // p_STD_BufferFlag(1) + reserved(3) + pes_ExtensionFlag2(1) + if header.PesExtensionFlag != 0 { + var flags uint8 + flags, err = util.ReadByteToUint8(lrHeader) + if err != nil { + return + } + + header.PesPrivateDataFlag = flags & 0x80 + header.PackHeaderFieldFlag = flags & 0x40 + header.ProgramPacketSequenceCounterFlag = flags & 0x20 + header.PSTDBufferFlag = flags & 0x10 + header.PesExtensionFlag2 = flags & 0x01 + + // TODO:下面所有的标志位,可能获取到的数据,都简单的读取后,丢弃,如果日后需要,在这里处理 + + // pes_PrivateData(128) + if header.PesPrivateDataFlag != 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(16)); err != nil { + return + } + } + + // packFieldLength(8) + if header.PackHeaderFieldFlag != 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(1)); err != nil { + return + } + } + + // marker_bit(1) + programPacketSequenceCounter(7) + marker_bit(1) + + // mpeg1_mpeg2_Identifier(1) + originalStuffLength(6) + if header.ProgramPacketSequenceCounterFlag != 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil { + return + } + } + + // 01 + p_STD_bufferScale(1) + p_STD_bufferSize(13) + if header.PSTDBufferFlag != 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil { + return + } + } + + // marker_bit(1) + pes_Extension_Field_Length(7) + + // streamIDExtensionFlag(1) + if header.PesExtensionFlag != 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil { + return + } + } + } + + // 把剩下的头的数据消耗掉 + if lrHeader.N > 0 { + if _, err = io.CopyN(io.Discard, lrHeader, int64(lrHeader.N)); err != nil { + return + } + } + + // 2的16次方,16个字节 + if lrPacket.N < 65536 { + // 这里得到的其实是负载长度,因为已经偏移过了Header部分. + //header.pes_PacketLength = uint16(lrPacket.N) + header.PayloadLength = uint64(lrPacket.N) + } + + return +} + +func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error) { + if header.PacketStartCodePrefix != 0x0000001 { + err = errors.New("write PacketStartCodePrefix is not 0x0000001") + return + } + + // packetStartCodePrefix(24) (0x000001) + if err = util.WriteUint24ToByte(w, header.PacketStartCodePrefix, true); err != nil { + return + } + + written += 3 + + // streamID(8) + if err = util.WriteUint8ToByte(w, header.StreamID); err != nil { + return + } + + written += 1 + + // pes_PacketLength(16) + // PES包长度可能为0,这个时候,需要自己去算 + // 0 <= len <= 65535 + if err = util.WriteUint16ToByte(w, header.PesPacketLength, true); err != nil { + return + } + + //fmt.Println("Length :", payloadLength) + //fmt.Println("PES Packet Length :", header.pes_PacketLength) + + written += 2 + + // constTen(2) + // pes_ScramblingControl(2) + // pes_Priority(1) + // dataAlignmentIndicator(1) + // copyright(1) + // originalOrCopy(1) + // 1000 0001 + if header.ConstTen != 0x80 { + err = errors.New("pes header ConstTen != 0x80") + return + } + + flags := header.ConstTen | header.PesScramblingControl | header.PesPriority | header.DataAlignmentIndicator | header.Copyright | header.OriginalOrCopy + if err = util.WriteUint8ToByte(w, flags); err != nil { + return + } + + written += 1 + + // pts_dts_Flags(2) + // escr_Flag(1) + // es_RateFlag(1) + // dsm_TrickModeFlag(1) + // additionalCopyInfoFlag(1) + // pes_CRCFlag(1) + // pes_ExtensionFlag(1) + sevenFlags := header.PtsDtsFlags | header.EscrFlag | header.EsRateFlag | header.DsmTrickModeFlag | header.AdditionalCopyInfoFlag | header.PesCRCFlag | header.PesExtensionFlag + if err = util.WriteUint8ToByte(w, sevenFlags); err != nil { + return + } + + written += 1 + + // pes_HeaderDataLength(8) + if err = util.WriteUint8ToByte(w, header.PesHeaderDataLength); err != nil { + return + } + + written += 1 + + // PtsDtsFlags == 192(11), 128(10), 64(01)禁用, 0(00) + if header.PtsDtsFlags&0x80 != 0 { + // PTS和DTS都存在(11),否则只有PTS(10) + if header.PtsDtsFlags&0x80 != 0 && header.PtsDtsFlags&0x40 != 0 { + // 11:PTS和DTS + // PTS(33) + 4 + 3 + pts := util.PutPtsDts(header.Pts) | 3<<36 + if err = util.WriteUint40ToByte(w, pts, true); err != nil { + return + } + + written += 5 + + // DTS(33) + 4 + 3 + dts := util.PutPtsDts(header.Dts) | 1<<36 + if err = util.WriteUint40ToByte(w, dts, true); err != nil { + return + } + + written += 5 + } else { + // 10:只有PTS + // PTS(33) + 4 + 3 + pts := util.PutPtsDts(header.Pts) | 2<<36 + if err = util.WriteUint40ToByte(w, pts, true); err != nil { + return + } + + written += 5 + } + } + + return +} diff --git a/plugin/hls/pkg/ts/mpegts_pmt.go b/plugin/hls/pkg/ts/mpegts_pmt.go new file mode 100644 index 0000000..cf76b7e --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts_pmt.go @@ -0,0 +1,367 @@ +package mpegts + +import ( + "bytes" + "io" + "m7s.live/m7s/v5/pkg/codec" + "m7s.live/m7s/v5/pkg/util" + "net" +) + +// ios13818-1-CN.pdf 46(60)-153(167)/page +// +// PMT + +var ( + TSHeader = []byte{0x47, 0x40 | (PID_PMT >> 8), PID_PMT & 0xff, 0x10, 0x00} //PID:0x100 + PSI = []byte{0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, 0x00} + PMT = []byte{0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00} //PcrPID:0x101 + h264 = []byte{STREAM_TYPE_H264, 0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00} + h265 = []byte{STREAM_TYPE_H265, 0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00} + aac = []byte{STREAM_TYPE_AAC, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00} + pcma = []byte{STREAM_TYPE_G711A, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00} + pcmu = []byte{STREAM_TYPE_G711U, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00} + Stuffing []byte +) + +func init() { + Stuffing = util.GetFillBytes(0xff, TS_PACKET_SIZE) +} + +// TS Header : +// SyncByte = 0x47 +// TransportErrorIndicator = 0(B:0), PayloadUnitStartIndicator = 1(B:0), TransportPriority = 0(B:0), +// Pid = 4097(0x1001), +// TransportScramblingControl = 0(B:00), AdaptionFieldControl = 1(B:01), ContinuityCounter = 0(B:0000), + +// PSI : +// TableID = 0x02, +// SectionSyntaxIndicator = 1(B:1), Zero = 0(B:0), Reserved1 = 3(B:11), +// SectionLength = 23(0x17) +// ProgramNumber = 0x0001 +// Reserved2 = 3(B:11), VersionNumber = (B:00000), CurrentNextIndicator = 1(B:0), +// SectionNumber = 0x00 +// LastSectionNumber = 0x00 + +// PMT: +// Reserved3 = 15(B:1110), PcrPID = 256(0x100) +// Reserved4 = 16(B:1111), ProgramInfoLength = 0(0x000) +// H264: +// StreamType = 0x1b, +// Reserved5 = 15(B:1110), ElementaryPID = 256(0x100) +// Reserved6 = 16(B:1111), EsInfoLength = 0(0x000) +// AAC: +// StreamType = 0x0f, +// Reserved5 = 15(B:1110), ElementaryPID = 257(0x101) +// Reserved6 = 16(B:1111), EsInfoLength = 0(0x000) + +type MpegTsPmtStream struct { + StreamType byte // 8 bits 指示具有 PID值的包内承载的节目元类型,其 PID值由 elementary_PID所指定 + Reserved5 byte // 3 bits 保留位 + ElementaryPID uint16 // 13 bits 指定承载相关节目元的传输流包的 PID + Reserved6 byte // 4 bits 保留位 + EsInfoLength uint16 // 12 bits 该字段的头两比特必为'00',剩余 10比特指示紧随 ES_info_length字段的相关节目元描述符的字节数 + + // N Loop Descriptors + Descriptor []MpegTsDescriptor // 不确定字节数,可变 +} + +// Program Map Table (节目映射表) +type MpegTsPMT struct { + // PSI + TableID byte // 8 bits 0x00->PAT,0x02->PMT + SectionSyntaxIndicator byte // 1 bit 段语法标志位,固定为1 + Zero byte // 1 bit 0 + Reserved1 byte // 2 bits 保留位 + SectionLength uint16 // 12 bits 该字段的头两比特必为'00',剩余 10 比特指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC.此字段中的值应不超过 1021(0x3FD) + ProgramNumber uint16 // 16 bits 指定 program_map_PID 所适用的节目 + Reserved2 byte // 2 bits 保留位 + VersionNumber byte // 5 bits 范围0-31,表示PAT的版本号 + CurrentNextIndicator byte // 1 bit 发送的PAT是当前有效还是下一个PAT有效 + SectionNumber byte // 8 bits 分段的号码.PAT可能分为多段传输.第一段为00,以后每个分段加1,最多可能有256个分段 + LastSectionNumber byte // 8 bits 最后一个分段的号码 + + Reserved3 byte // 3 bits 保留位 0x07 + PcrPID uint16 // 13 bits 指明TS包的PID值.该TS包含有PCR域,该PCR值对应于由节目号指定的对应节目.如果对于私有数据流的节目定义与PCR无关.这个域的值将为0x1FFF + Reserved4 byte // 4 bits 预留位 0x0F + ProgramInfoLength uint16 // 12 bits 前两位bit为00.该域指出跟随其后对节目信息的描述的byte数 + ProgramInfoDescriptor []MpegTsDescriptor // N Loop Descriptors 可变 节目信息描述 + + // N Loop + Stream []MpegTsPmtStream // PMT表里面的所有音视频索引信息 + + Crc32 uint32 // 32 bits 包含处理全部传输流节目映射分段之后,在附件 B 规定的解码器中给出寄存器零输出的 CRC 值 +} + +func ReadPMT(r io.Reader) (pmt MpegTsPMT, err error) { + lr, psi, err := ReadPSI(r, PSI_TYPE_PMT) + if err != nil { + return + } + + pmt = psi.Pmt + + // reserved3(3) + pcrPID(13) + pcrPID, err := util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + pmt.PcrPID = pcrPID & 0x1fff + + // reserved4(4) + programInfoLength(12) + // programInfoLength(12) == 0x00(固定为0) + programInfoLength(10) + programInfoLength, err := util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + pmt.ProgramInfoLength = programInfoLength & 0x3ff + + // 如果length>0那么,紧跟programInfoLength后面就有length个字节 + if pmt.ProgramInfoLength > 0 { + lr := &io.LimitedReader{R: lr, N: int64(pmt.ProgramInfoLength)} + pmt.ProgramInfoDescriptor, err = ReadPMTDescriptor(lr) + if err != nil { + return + } + } + + // N Loop + // 开始N循环,读取所有的流的信息 + for lr.N > 0 { + var streams MpegTsPmtStream + // streamType(8) + streams.StreamType, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + + // reserved5(3) + elementaryPID(13) + streams.ElementaryPID, err = util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + streams.ElementaryPID = streams.ElementaryPID & 0x1fff + + // reserved6(4) + esInfoLength(12) + // esInfoLength(12) == 0x00(固定为0) + esInfoLength(10) + streams.EsInfoLength, err = util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + streams.EsInfoLength = streams.EsInfoLength & 0x3ff + + // 如果length>0那么,紧跟esInfoLength后面就有length个字节 + if streams.EsInfoLength > 0 { + lr := &io.LimitedReader{R: lr, N: int64(streams.EsInfoLength)} + streams.Descriptor, err = ReadPMTDescriptor(lr) + if err != nil { + return + } + } + + // 每读取一个流的信息(音频流或者视频流或者其他),都保存起来 + pmt.Stream = append(pmt.Stream, streams) + } + if cr, ok := r.(*util.Crc32Reader); ok { + err = cr.ReadCrc32UIntAndCheck() + if err != nil { + return + } + } + return +} + +func ReadPMTDescriptor(lr *io.LimitedReader) (Desc []MpegTsDescriptor, err error) { + var desc MpegTsDescriptor + for lr.N > 0 { + // tag (8) + desc.Tag, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + + // length (8) + desc.Length, err = util.ReadByteToUint8(lr) + if err != nil { + return + } + + desc.Data = make([]byte, desc.Length) + _, err = lr.Read(desc.Data) + if err != nil { + return + } + + Desc = append(Desc, desc) + } + + return +} + +func WritePMTDescriptor(w io.Writer, descs []MpegTsDescriptor) (err error) { + for _, desc := range descs { + // tag(8) + if err = util.WriteUint8ToByte(w, desc.Tag); err != nil { + return + } + + // length (8) + if err = util.WriteUint8ToByte(w, uint8(len(desc.Data))); err != nil { + return + } + + // data + if _, err = w.Write(desc.Data); err != nil { + return + } + } + + return +} + +func WritePMTBody(w io.Writer, pmt MpegTsPMT) (err error) { + // reserved3(3) + pcrPID(13) + if err = util.WriteUint16ToByte(w, pmt.PcrPID|7<<13, true); err != nil { + return + } + + // programInfoDescriptor 节目信息描述,字节数不能确定 + bw := &bytes.Buffer{} + if err = WritePMTDescriptor(bw, pmt.ProgramInfoDescriptor); err != nil { + return + } + + pmt.ProgramInfoLength = uint16(bw.Len()) + + // reserved4(4) + programInfoLength(12) + // programInfoLength(12) == 0x00(固定为0) + programInfoLength(10) + if err = util.WriteUint16ToByte(w, pmt.ProgramInfoLength|0xf000, true); err != nil { + return + } + + // programInfoDescriptor + if _, err = w.Write(bw.Bytes()); err != nil { + return + } + + // 循环读取所有的流的信息(音频或者视频) + for _, esinfo := range pmt.Stream { + // streamType(8) + if err = util.WriteUint8ToByte(w, esinfo.StreamType); err != nil { + return + } + + // reserved5(3) + elementaryPID(13) + if err = util.WriteUint16ToByte(w, esinfo.ElementaryPID|7<<13, true); err != nil { + return + } + + // descriptor ES流信息描述,字节数不能确定 + bw := &bytes.Buffer{} + if err = WritePMTDescriptor(bw, esinfo.Descriptor); err != nil { + return + } + + esinfo.EsInfoLength = uint16(bw.Len()) + + // reserved6(4) + esInfoLength(12) + // esInfoLength(12) == 0x00(固定为0) + esInfoLength(10) + if err = util.WriteUint16ToByte(w, esinfo.EsInfoLength|0xf000, true); err != nil { + return + } + + // descriptor + if _, err = w.Write(bw.Bytes()); err != nil { + return + } + } + + return +} + +func WritePMT(w io.Writer, pmt MpegTsPMT) (err error) { + bw := &bytes.Buffer{} + + if err = WritePMTBody(bw, pmt); err != nil { + return + } + + if pmt.SectionLength == 0 { + pmt.SectionLength = 2 + 3 + 4 + uint16(len(bw.Bytes())) + } + + psi := MpegTsPSI{} + + psi.Pmt = pmt + + if err = WritePSI(w, PSI_TYPE_PMT, psi, bw.Bytes()); err != nil { + return + } + + return +} + +// func WritePMTPacket(w io.Writer, tsHeader []byte, pmt MpegTsPMT) (err error) { +// if pmt.TableID != TABLE_TSPMS { +// err = errors.New("PMT table ID error") +// return +// } + +// // 将所有要写的数据(PMT),全部放入到buffer中去. +// // buffer 里面已经写好了整个PMT表(PointerField+PSI+PMT+CRC) +// bw := &bytes.Buffer{} +// if err = WritePMT(bw, pmt); err != nil { +// return +// } + +// // TODO:如果Pmt.Stream里面包含的信息很大,大于188? +// stuffingBytes := util.GetFillBytes(0xff, TS_PACKET_SIZE-4-bw.Len()) + +// var PMTPacket []byte +// PMTPacket = append(PMTPacket, tsHeader...) +// PMTPacket = append(PMTPacket, bw.Bytes()...) +// PMTPacket = append(PMTPacket, stuffingBytes...) + +// fmt.Println("-------------------------") +// fmt.Println("Write PMT :", PMTPacket) +// fmt.Println("-------------------------") + +// // 写PMT负载 +// if _, err = w.Write(PMTPacket); err != nil { +// return +// } + +// return +// } + +func WritePMTPacket(w io.Writer, videoCodec codec.FourCC, audioCodec codec.FourCC) { + w.Write(TSHeader) + crc := make([]byte, 4) + paddingSize := TS_PACKET_SIZE - len(crc) - len(PSI) - len(PMT) - len(TSHeader) - 10 + pmt := net.Buffers{PSI, PMT} + switch videoCodec { + case codec.FourCC_H264: + pmt = append(pmt, h264) + case codec.FourCC_H265: + pmt = append(pmt, h265) + default: + paddingSize += 5 + } + switch audioCodec { + case codec.FourCC_MP4A: + pmt = append(pmt, aac) + case codec.FourCC_ALAW: + pmt = append(pmt, pcma) + case codec.FourCC_ULAW: + pmt = append(pmt, pcmu) + default: + paddingSize += 5 + } + util.PutBE(crc, GetCRC32_2(pmt)) + pmt = append(pmt, crc, Stuffing[:paddingSize]) + pmt.WriteTo(w) +} diff --git a/plugin/hls/pkg/ts/mpegts_psi.go b/plugin/hls/pkg/ts/mpegts_psi.go new file mode 100644 index 0000000..440f13d --- /dev/null +++ b/plugin/hls/pkg/ts/mpegts_psi.go @@ -0,0 +1,230 @@ +package mpegts + +import ( + "errors" + "fmt" + "io" + "m7s.live/m7s/v5/pkg/util" +) + +// +// PSI +// + +const ( + PSI_TYPE_PAT = 1 + PSI_TYPE_PMT = 2 + PSI_TYPE_NIT = 3 + PSI_TYPE_CAT = 4 + PSI_TYPE_TST = 5 + PSI_TYPE_IPMP_CIT = 6 +) + +type MpegTsPSI struct { + // PAT + // PMT + // CAT + // NIT + Pat MpegTsPAT + Pmt MpegTsPMT +} + +// 当传输流包有效载荷包含 PSI 数据时,payload_unit_start_indicator 具有以下意义: +// 若传输流包承载 PSI分段的首字节,则 payload_unit_start_indicator 值必为 1,指示此传输流包的有效载荷的首字节承载pointer_field. +// 若传输流包不承载 PSI 分段的首字节,则 payload_unit_start_indicator 值必为 0,指示在此有效载荷中不存在 pointer_field +// 只要是PSI就一定会有pointer_field +func ReadPSI(r io.Reader, pt uint32) (lr *io.LimitedReader, psi MpegTsPSI, err error) { + // pointer field(8) + cr, ok := r.(*util.Crc32Reader) + if ok { + r = cr.R + } + pointer_field, err := util.ReadByteToUint8(r) + if err != nil { + return + } + + if pointer_field != 0 { + // 无论如何都应该确保能将pointer_field读取到,并且io.Reader指针向下移动 + // ioutil.Discard常用在,http中,如果Get请求,获取到了很大的Body,要丢弃Body,就用这个方法. + // 因为http默认重链接的时候,必须等body读取完成. + // 用于发送需要读取但不想存储的数据,目的是耗尽读取端的数据 + if _, err = io.CopyN(io.Discard, r, int64(pointer_field)); err != nil { + return + } + } + if ok { + r = cr + } + + // table id(8) + tableId, err := util.ReadByteToUint8(r) + if err != nil { + return + } + + // sectionSyntaxIndicator(1) + zero(1) + reserved1(2) + sectionLength(12) + // sectionLength 前两个字节固定为00 + sectionSyntaxIndicatorAndSectionLength, err := util.ReadByteToUint16(r, true) + if err != nil { + return + } + + // 指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC + // 因此剩下最多只能在读sectionLength长度的字节 + lr = &io.LimitedReader{R: r, N: int64(sectionSyntaxIndicatorAndSectionLength & 0x3FF)} + + // PAT TransportStreamID(16) or PMT ProgramNumber(16) + transportStreamIdOrProgramNumber, err := util.ReadByteToUint16(lr, true) + if err != nil { + return + } + + // reserved2(2) + versionNumber(5) + currentNextIndicator(1) + versionNumberAndCurrentNextIndicator, err := util.ReadByteToUint8(lr) + if err != nil { + return + } + + // sectionNumber(8) + sectionNumber, err := util.ReadByteToUint8(lr) + if err != nil { + return + } + + // lastSectionNumber(8) + lastSectionNumber, err := util.ReadByteToUint8(lr) + if err != nil { + return + } + + // 因为lr.N是从sectionLength开始计算,所以要减去 pointer_field(8) + table id(8) + sectionSyntaxIndicator(1) + zero(1) + reserved1(2) + sectionLength(12) + lr.N -= 4 + + switch pt { + case PSI_TYPE_PAT: + { + if tableId != TABLE_PAS { + err = errors.New(fmt.Sprintf("%s, id=%d", "read pmt table id != 2", tableId)) + return + } + + psi.Pat.TableID = tableId + psi.Pat.SectionSyntaxIndicator = uint8((sectionSyntaxIndicatorAndSectionLength & 0x8000) >> 15) + psi.Pat.SectionLength = sectionSyntaxIndicatorAndSectionLength & 0x3FF + psi.Pat.TransportStreamID = transportStreamIdOrProgramNumber + psi.Pat.VersionNumber = versionNumberAndCurrentNextIndicator & 0x3e + psi.Pat.CurrentNextIndicator = versionNumberAndCurrentNextIndicator & 0x01 + psi.Pat.SectionNumber = sectionNumber + psi.Pat.LastSectionNumber = lastSectionNumber + } + case PSI_TYPE_PMT: + { + if tableId != TABLE_TSPMS { + err = errors.New(fmt.Sprintf("%s, id=%d", "read pmt table id != 2", tableId)) + return + } + + psi.Pmt.TableID = tableId + psi.Pmt.SectionSyntaxIndicator = uint8((sectionSyntaxIndicatorAndSectionLength & 0x8000) >> 15) + psi.Pmt.SectionLength = sectionSyntaxIndicatorAndSectionLength & 0x3FF + psi.Pmt.ProgramNumber = transportStreamIdOrProgramNumber + psi.Pmt.VersionNumber = versionNumberAndCurrentNextIndicator & 0x3e + psi.Pmt.CurrentNextIndicator = versionNumberAndCurrentNextIndicator & 0x01 + psi.Pmt.SectionNumber = sectionNumber + psi.Pmt.LastSectionNumber = lastSectionNumber + } + } + + return +} + +func WritePSI(w io.Writer, pt uint32, psi MpegTsPSI, data []byte) (err error) { + var tableId, versionNumberAndCurrentNextIndicator, sectionNumber, lastSectionNumber uint8 + var sectionSyntaxIndicatorAndSectionLength, transportStreamIdOrProgramNumber uint16 + + switch pt { + case PSI_TYPE_PAT: + { + if psi.Pat.TableID != TABLE_PAS { + err = errors.New(fmt.Sprintf("%s, id=%d", "write pmt table id != 0", tableId)) + return + } + + tableId = psi.Pat.TableID + sectionSyntaxIndicatorAndSectionLength = uint16(psi.Pat.SectionSyntaxIndicator)<<15 | 3<<12 | psi.Pat.SectionLength + transportStreamIdOrProgramNumber = psi.Pat.TransportStreamID + versionNumberAndCurrentNextIndicator = psi.Pat.VersionNumber<<1 | psi.Pat.CurrentNextIndicator + sectionNumber = psi.Pat.SectionNumber + lastSectionNumber = psi.Pat.LastSectionNumber + } + case PSI_TYPE_PMT: + { + if psi.Pmt.TableID != TABLE_TSPMS { + err = errors.New(fmt.Sprintf("%s, id=%d", "write pmt table id != 2", tableId)) + return + } + + tableId = psi.Pmt.TableID + sectionSyntaxIndicatorAndSectionLength = uint16(psi.Pmt.SectionSyntaxIndicator)<<15 | 3<<12 | psi.Pmt.SectionLength + transportStreamIdOrProgramNumber = psi.Pmt.ProgramNumber + versionNumberAndCurrentNextIndicator = psi.Pmt.VersionNumber<<1 | psi.Pmt.CurrentNextIndicator + sectionNumber = psi.Pmt.SectionNumber + lastSectionNumber = psi.Pmt.LastSectionNumber + } + } + + // pointer field(8) + if err = util.WriteUint8ToByte(w, 0); err != nil { + return + } + + cw := &util.Crc32Writer{W: w, Crc32: 0xffffffff} + + // table id(8) + if err = util.WriteUint8ToByte(cw, tableId); err != nil { + return + } + + // sectionSyntaxIndicator(1) + zero(1) + reserved1(2) + sectionLength(12) + // sectionLength 前两个字节固定为00 + // 1 0 11 sectionLength + if err = util.WriteUint16ToByte(cw, sectionSyntaxIndicatorAndSectionLength, true); err != nil { + return + } + + // PAT TransportStreamID(16) or PMT ProgramNumber(16) + if err = util.WriteUint16ToByte(cw, transportStreamIdOrProgramNumber, true); err != nil { + return + } + + // reserved2(2) + versionNumber(5) + currentNextIndicator(1) + // 0x3 << 6 -> 1100 0000 + // 0x3 << 6 | 1 -> 1100 0001 + if err = util.WriteUint8ToByte(cw, versionNumberAndCurrentNextIndicator); err != nil { + return + } + + // sectionNumber(8) + if err = util.WriteUint8ToByte(cw, sectionNumber); err != nil { + return + } + + // lastSectionNumber(8) + if err = util.WriteUint8ToByte(cw, lastSectionNumber); err != nil { + return + } + + // data + if _, err = cw.Write(data); err != nil { + return + } + + // crc32 + crc32 := util.BigLittleSwap(uint(cw.Crc32)) + if err = util.WriteUint32ToByte(cw, uint32(crc32), true); err != nil { + return + } + + return +} diff --git a/plugin/rtp/index.go b/plugin/rtp/index.go index b9608e0..110f4a8 100644 --- a/plugin/rtp/index.go +++ b/plugin/rtp/index.go @@ -1,2 +1 @@ package plugin_rtp - diff --git a/plugin/rtp/pkg/tcp.go b/plugin/rtp/pkg/tcp.go new file mode 100644 index 0000000..3c900fd --- /dev/null +++ b/plugin/rtp/pkg/tcp.go @@ -0,0 +1,64 @@ +package rtp + +import ( + "bufio" + "encoding/binary" + "io" + "m7s.live/m7s/v5/pkg/util" + "net" +) + +type TCP net.TCPConn + +func (t *TCP) Read(onRTP func(util.Buffer) error) (err error) { + reader := bufio.NewReader((*net.TCPConn)(t)) + rtpLenBuf := make([]byte, 4) + buffer := make(util.Buffer, 1024) + for err == nil { + if _, err = io.ReadFull(reader, rtpLenBuf); err != nil { + return + } + rtpLen := int(binary.BigEndian.Uint16(rtpLenBuf[:2])) + if rtpLenBuf[2]>>6 != 2 || rtpLenBuf[2]&0x0f > 15 || rtpLenBuf[3]&0x7f > 127 { //长度后面正常紧跟 rtp 头,如果不是,说明长度不对,此处并非长度,而是可能之前的 rtp 包不完整导致的,需要往前查找 + buffer.Write(rtpLenBuf) //已读的数据先写入缓存 + for i := 12; i < buffer.Len()-2; i++ { // 缓存中 rtp 头就不用判断了,跳过 12 字节往后寻找 + if buffer[i]>>6 != 2 || buffer[i]&0x0f > 15 || buffer[i+1]&0x7f > 127 { // 一直找到 rtp 头为止 + continue + } + rtpLen = int(binary.BigEndian.Uint16(buffer[i-2 : i])) // rtp 头前面两个字节是长度 + if remain := buffer.Len() - i; remain < rtpLen { // 缓存中的数据不够一个 rtp 包,继续读取 + copy(buffer, buffer[i:]) + buffer.Relloc(rtpLen) + if _, err = io.ReadFull(reader, buffer[remain:]); err != nil { + return + } + err = onRTP(buffer) + break + } else { + err = onRTP(buffer.SubBuf(i, rtpLen)) + if err != nil { + return + } + i += rtpLen + if buffer.Len() > i+1 { + i += 2 + } else if buffer.Len() > i { + reader.UnreadByte() + break + } else { + break + } + i-- + } + } + } else { + buffer.Relloc(rtpLen) + copy(buffer, rtpLenBuf[2:]) + if _, err = io.ReadFull(reader, buffer[2:]); err != nil { + return + } + err = onRTP(buffer) + } + } + return +} diff --git a/plugin/stress/api.go b/plugin/stress/api.go index ee22e5e..5e202b5 100644 --- a/plugin/stress/api.go +++ b/plugin/stress/api.go @@ -7,7 +7,7 @@ import ( "m7s.live/m7s/v5" gpb "m7s.live/m7s/v5/pb" "m7s.live/m7s/v5/pkg" - hdl "m7s.live/m7s/v5/plugin/hdl/pkg" + hdl "m7s.live/m7s/v5/plugin/flv/pkg" rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg" rtsp "m7s.live/m7s/v5/plugin/rtsp/pkg" "m7s.live/m7s/v5/plugin/stress/pb" diff --git a/plugin/stress/pb/stress.pb.gw.go b/plugin/stress/pb/stress.pb.gw.go index 4ca8fb1..5c6aba1 100644 --- a/plugin/stress/pb/stress.pb.gw.go +++ b/plugin/stress/pb/stress.pb.gw.go @@ -500,7 +500,7 @@ func RegisterApiHandlerServer(ctx context.Context, mux *runtime.ServeMux, server inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/stress.Api/PullHDL", runtime.WithHTTPPathPattern("/stress/api/pull/hdl/{pullCount}")) + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/stress.Api/PullHDL", runtime.WithHTTPPathPattern("/stress/api/pull/flv/{pullCount}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -727,7 +727,7 @@ func RegisterApiHandlerClient(ctx context.Context, mux *runtime.ServeMux, client inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) var err error var annotatedContext context.Context - annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/stress.Api/PullHDL", runtime.WithHTTPPathPattern("/stress/api/pull/hdl/{pullCount}")) + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/stress.Api/PullHDL", runtime.WithHTTPPathPattern("/stress/api/pull/flv/{pullCount}")) if err != nil { runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) return @@ -821,7 +821,7 @@ var ( pattern_Api_PullRTSP_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"stress", "api", "pull", "rtsp", "pullCount"}, "")) - pattern_Api_PullHDL_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"stress", "api", "pull", "hdl", "pullCount"}, "")) + pattern_Api_PullHDL_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"stress", "api", "pull", "flv", "pullCount"}, "")) pattern_Api_GetCount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"stress", "api", "count"}, "")) diff --git a/plugin/stress/pb/stress.proto b/plugin/stress/pb/stress.proto index 37d885f..dc10eba 100644 --- a/plugin/stress/pb/stress.proto +++ b/plugin/stress/pb/stress.proto @@ -32,7 +32,7 @@ service api { } rpc PullHDL (PullRequest) returns (m7s.SuccessResponse) { option (google.api.http) = { - post: "/stress/api/pull/hdl/{pullCount}" + post: "/stress/api/pull/flv/{pullCount}" body: "*" }; } diff --git a/server.go b/server.go index aad303a..c8e0a83 100644 --- a/server.go +++ b/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + myip "github.com/husanpao/ip" "github.com/phsym/console-slog" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -37,6 +38,7 @@ var ( Version: Version, } Servers = make([]*Server, 10) + Routes = map[string]string{} defaultLogHandler = console.NewHandler(os.Stdout, &console.HandlerOptions{TimeFormat: "15:04:05.000000"}) ) @@ -86,6 +88,16 @@ func Run(ctx context.Context, conf any) error { type rawconfig = map[string]map[string]any +func init() { + for k, v := range myip.LocalAndInternalIPs() { + Routes[k] = v + if lastdot := strings.LastIndex(k, "."); lastdot >= 0 { + Routes[k[0:lastdot]] = k + } + } + slog.Info("init", "routes", Routes) +} + func (s *Server) Run(ctx context.Context, conf any) (err error) { s.StartTime = time.Now() for err = s.run(ctx, conf); err == ErrRestart; err = s.run(ctx, conf) {