mirror of
https://github.com/zhoukk/krtmpd.git
synced 2025-10-05 08:36:50 +08:00
feat: rtmpd
This commit is contained in:
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/zhoukk/krtmpd
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
8
go.sum
Normal file
8
go.sum
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
||||||
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||||
|
github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
369
krtmpd.go
Normal file
369
krtmpd.go
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
package krtmpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/nareix/joy5/av"
|
||||||
|
"github.com/nareix/joy5/format/rtmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gopCacheSnapshot struct {
|
||||||
|
pkts []av.Packet
|
||||||
|
idx int
|
||||||
|
}
|
||||||
|
|
||||||
|
type gopCache struct {
|
||||||
|
pkts []av.Packet
|
||||||
|
idx int
|
||||||
|
curst unsafe.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *gopCache) put(pkt av.Packet) {
|
||||||
|
if pkt.IsKeyFrame {
|
||||||
|
gc.pkts = []av.Packet{}
|
||||||
|
}
|
||||||
|
gc.pkts = append(gc.pkts, pkt)
|
||||||
|
gc.idx++
|
||||||
|
st := &gopCacheSnapshot{
|
||||||
|
pkts: gc.pkts,
|
||||||
|
idx: gc.idx,
|
||||||
|
}
|
||||||
|
atomic.StorePointer(&gc.curst, unsafe.Pointer(st))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gc *gopCache) curSnapshot() *gopCacheSnapshot {
|
||||||
|
return (*gopCacheSnapshot)(atomic.LoadPointer(&gc.curst))
|
||||||
|
}
|
||||||
|
|
||||||
|
type gopCacheReadCursor struct {
|
||||||
|
lastidx int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *gopCacheReadCursor) advance(cur *gopCacheSnapshot) []av.Packet {
|
||||||
|
lastidx := rc.lastidx
|
||||||
|
rc.lastidx = cur.idx
|
||||||
|
if diff := cur.idx - lastidx; diff <= len(cur.pkts) {
|
||||||
|
return cur.pkts[len(cur.pkts)-diff:]
|
||||||
|
} else {
|
||||||
|
return cur.pkts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mergeSeqhdr struct {
|
||||||
|
cb func(av.Packet)
|
||||||
|
hdrpkt av.Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mergeSeqhdr) do(pkt av.Packet) {
|
||||||
|
switch pkt.Type {
|
||||||
|
case av.H264DecoderConfig:
|
||||||
|
m.hdrpkt.VSeqHdr = append([]byte(nil), pkt.Data...)
|
||||||
|
case av.H264:
|
||||||
|
pkt.Metadata = m.hdrpkt.Metadata
|
||||||
|
if pkt.IsKeyFrame {
|
||||||
|
pkt.VSeqHdr = m.hdrpkt.VSeqHdr
|
||||||
|
}
|
||||||
|
m.cb(pkt)
|
||||||
|
case av.AACDecoderConfig:
|
||||||
|
m.hdrpkt.ASeqHdr = append([]byte(nil), pkt.Data...)
|
||||||
|
case av.AAC:
|
||||||
|
pkt.Metadata = m.hdrpkt.Metadata
|
||||||
|
pkt.ASeqHdr = m.hdrpkt.ASeqHdr
|
||||||
|
m.cb(pkt)
|
||||||
|
case av.Metadata:
|
||||||
|
m.hdrpkt.Metadata = pkt.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type splitSeqhdr struct {
|
||||||
|
cb func(av.Packet) error
|
||||||
|
hdrpkt av.Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *splitSeqhdr) sendmeta(pkt av.Packet) error {
|
||||||
|
if bytes.Compare(s.hdrpkt.Metadata, pkt.Metadata) != 0 {
|
||||||
|
if err := s.cb(av.Packet{
|
||||||
|
Type: av.Metadata,
|
||||||
|
Data: pkt.Metadata,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.hdrpkt.Metadata = pkt.Metadata
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *splitSeqhdr) do(pkt av.Packet) error {
|
||||||
|
switch pkt.Type {
|
||||||
|
case av.H264:
|
||||||
|
if err := s.sendmeta(pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pkt.IsKeyFrame {
|
||||||
|
if bytes.Compare(s.hdrpkt.VSeqHdr, pkt.VSeqHdr) != 0 {
|
||||||
|
if err := s.cb(av.Packet{
|
||||||
|
Type: av.H264DecoderConfig,
|
||||||
|
Data: pkt.VSeqHdr,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.hdrpkt.VSeqHdr = pkt.VSeqHdr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.cb(pkt)
|
||||||
|
case av.AAC:
|
||||||
|
if err := s.sendmeta(pkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Compare(s.hdrpkt.ASeqHdr, pkt.ASeqHdr) != 0 {
|
||||||
|
if err := s.cb(av.Packet{
|
||||||
|
Type: av.AACDecoderConfig,
|
||||||
|
Data: pkt.ASeqHdr,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.hdrpkt.ASeqHdr = pkt.ASeqHdr
|
||||||
|
}
|
||||||
|
return s.cb(pkt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamSub struct {
|
||||||
|
notify chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamPub struct {
|
||||||
|
cancel func()
|
||||||
|
gc *gopCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type stream struct {
|
||||||
|
n int64
|
||||||
|
sub sync.Map
|
||||||
|
pub unsafe.Pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) curGopCacheSnapshot() *gopCacheSnapshot {
|
||||||
|
sp := (*streamPub)(atomic.LoadPointer(&s.pub))
|
||||||
|
if sp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sp.gc.curSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) addSub(close <-chan bool, w av.PacketWriter) {
|
||||||
|
ss := &streamSub{
|
||||||
|
notify: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.sub.Store(ss, nil)
|
||||||
|
defer s.sub.Delete(ss)
|
||||||
|
|
||||||
|
var cursor *gopCacheReadCursor
|
||||||
|
var lastsp *streamPub
|
||||||
|
|
||||||
|
seqsplit := splitSeqhdr{
|
||||||
|
cb: func(pkt av.Packet) error {
|
||||||
|
return w.WritePacket(pkt)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var pkts []av.Packet
|
||||||
|
|
||||||
|
sp := (*streamPub)(atomic.LoadPointer(&s.pub))
|
||||||
|
if sp != lastsp {
|
||||||
|
cursor = &gopCacheReadCursor{}
|
||||||
|
lastsp = sp
|
||||||
|
}
|
||||||
|
if sp != nil {
|
||||||
|
cur := sp.gc.curSnapshot()
|
||||||
|
if cur != nil {
|
||||||
|
pkts = cursor.advance(cur)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pkts) == 0 {
|
||||||
|
select {
|
||||||
|
case <-close:
|
||||||
|
return
|
||||||
|
case <-ss.notify:
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, pkt := range pkts {
|
||||||
|
if err := seqsplit.do(pkt); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) notifySub() {
|
||||||
|
s.sub.Range(func(key, value interface{}) bool {
|
||||||
|
ss := key.(*streamSub)
|
||||||
|
select {
|
||||||
|
case ss.notify <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stream) setPub(r av.PacketReader) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sp := &streamPub{
|
||||||
|
cancel: cancel,
|
||||||
|
gc: &gopCache{},
|
||||||
|
}
|
||||||
|
|
||||||
|
oldsp := (*streamPub)(atomic.SwapPointer(&s.pub, unsafe.Pointer(sp)))
|
||||||
|
if oldsp != nil {
|
||||||
|
oldsp.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
seqmerge := mergeSeqhdr{
|
||||||
|
cb: func(pkt av.Packet) {
|
||||||
|
sp.gc.put(pkt)
|
||||||
|
s.notifySub()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt, err := r.ReadPacket()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seqmerge.do(pkt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type streams struct {
|
||||||
|
l sync.RWMutex
|
||||||
|
m map[string]*stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStreams() *streams {
|
||||||
|
return &streams{
|
||||||
|
m: map[string]*stream{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *streams) add(k string) (*stream, func()) {
|
||||||
|
ss.l.Lock()
|
||||||
|
defer ss.l.Unlock()
|
||||||
|
|
||||||
|
s, ok := ss.m[k]
|
||||||
|
if !ok {
|
||||||
|
s = &stream{}
|
||||||
|
ss.m[k] = s
|
||||||
|
}
|
||||||
|
s.n++
|
||||||
|
|
||||||
|
return s, func() {
|
||||||
|
ss.l.Lock()
|
||||||
|
defer ss.l.Unlock()
|
||||||
|
|
||||||
|
s.n--
|
||||||
|
if s.n == 0 {
|
||||||
|
delete(ss.m, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RtmpdHook struct {
|
||||||
|
PushStart func(string, *url.URL) bool
|
||||||
|
PushEnd func(string, *url.URL)
|
||||||
|
|
||||||
|
PullStart func(string, *url.URL) bool
|
||||||
|
PullEnd func(string, *url.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RtmpServer struct {
|
||||||
|
port string
|
||||||
|
rtmpdHook RtmpdHook
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRtmpServer(port string) *RtmpServer {
|
||||||
|
rs := new(RtmpServer)
|
||||||
|
rs.port = port
|
||||||
|
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RtmpServer) SetHook(hook RtmpdHook) {
|
||||||
|
rs.rtmpdHook = hook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RtmpServer) Start() {
|
||||||
|
s := rtmp.NewServer()
|
||||||
|
streams := newStreams()
|
||||||
|
|
||||||
|
s.HandleConn = func(c *rtmp.Conn, nc net.Conn) {
|
||||||
|
stream, remove := streams.add(c.URL.Path)
|
||||||
|
defer func() {
|
||||||
|
remove()
|
||||||
|
|
||||||
|
if c.Publishing {
|
||||||
|
if rs.rtmpdHook.PushEnd != nil {
|
||||||
|
rs.rtmpdHook.PushEnd(nc.RemoteAddr().String(), c.URL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if rs.rtmpdHook.PullEnd != nil {
|
||||||
|
rs.rtmpdHook.PullEnd(nc.RemoteAddr().String(), c.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if c.Publishing {
|
||||||
|
if rs.rtmpdHook.PushStart != nil {
|
||||||
|
if !rs.rtmpdHook.PushStart(nc.RemoteAddr().String(), c.URL) {
|
||||||
|
log.Println("push start hook failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.setPub(c)
|
||||||
|
} else {
|
||||||
|
if rs.rtmpdHook.PullStart != nil {
|
||||||
|
if !rs.rtmpdHook.PullStart(nc.RemoteAddr().String(), c.URL) {
|
||||||
|
log.Println("pull start hook failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.addSub(c.CloseNotify(), c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", rs.port)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go s.HandleNetConn(c)
|
||||||
|
}
|
||||||
|
}
|
29
main/main.go
Normal file
29
main/main.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/zhoukk/krtmpd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rs := krtmpd.NewRtmpServer(":1935")
|
||||||
|
rs.SetHook(krtmpd.RtmpdHook{
|
||||||
|
PushStart: func(s string, u *url.URL) bool {
|
||||||
|
log.Printf("%s push %s start\n", s, u.String())
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
PushEnd: func(s string, u *url.URL) {
|
||||||
|
log.Printf("%s push %s end\n", s, u.String())
|
||||||
|
},
|
||||||
|
PullStart: func(s string, u *url.URL) bool {
|
||||||
|
log.Printf("%s pull %s start\n", s, u.String())
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
PullEnd: func(s string, u *url.URL) {
|
||||||
|
log.Printf("%s pull %s end\n", s, u.String())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
rs.Start()
|
||||||
|
}
|
Reference in New Issue
Block a user