10 changed files with 1027 additions and 113 deletions
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
|
||||
# supported stream protocols (the handshake is always performed with TCP) |
||||
protocols: [udp, tcp] |
||||
# port of the TCP rtsp listener |
||||
rtspPort: 8554 |
||||
# port of the UDP rtp listener |
||||
rtpPort: 8000 |
||||
# port of the UDP rtcp listener |
||||
rtcpPort: 8001 |
||||
# timeout of read operations |
||||
readTimeout: 5s |
||||
# timeout of write operations |
||||
writeTimeout: 5s |
||||
# script to run when a client connects |
||||
preScript: |
||||
# script to run when a client disconnects |
||||
postScript: |
||||
# enable pprof on port 9999 to monitor performance |
||||
pprof: false |
||||
|
||||
# these settings are path-dependent. The settings under the path 'all' are |
||||
# applied to all paths that do not match a specific entry. |
||||
paths: |
||||
all: |
||||
# source of the stream - this can be: |
||||
# * record -> the stream is provided by a client through the RECORD command (like ffmpeg) |
||||
# * rtsp://url -> the stream is pulled from another RTSP server |
||||
source: record |
||||
# if the source is an RTSP url, this is the protocol that will be used to pull the stream |
||||
sourceProtocol: udp |
||||
|
||||
# username required to publish |
||||
publishUser: |
||||
# password required to publish |
||||
publishPass: |
||||
# IPs or networks (x.x.x.x/24) allowed to publish |
||||
publishIps: [] |
||||
|
||||
# username required to read |
||||
readUser: |
||||
# password required to read |
||||
readPass: |
||||
# IPs or networks (x.x.x.x/24) allowed to read |
||||
readIps: [] |
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"net" |
||||
"time" |
||||
) |
||||
|
||||
type streamerUdpListenerState int |
||||
|
||||
const ( |
||||
_UDPL_STATE_STARTING streamerUdpListenerState = iota |
||||
_UDPL_STATE_RUNNING |
||||
) |
||||
|
||||
type streamerUdpListener struct { |
||||
p *program |
||||
streamer *streamer |
||||
trackId int |
||||
trackFlowType trackFlowType |
||||
publisherIp net.IP |
||||
publisherPort int |
||||
nconn *net.UDPConn |
||||
running bool |
||||
readBuf1 []byte |
||||
readBuf2 []byte |
||||
readCurBuf bool |
||||
lastFrameTime time.Time |
||||
|
||||
done chan struct{} |
||||
} |
||||
|
||||
func newStreamerUdpListener(p *program, port int, streamer *streamer, |
||||
trackId int, trackFlowType trackFlowType, publisherIp net.IP) (*streamerUdpListener, error) { |
||||
nconn, err := net.ListenUDP("udp", &net.UDPAddr{ |
||||
Port: port, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
l := &streamerUdpListener{ |
||||
p: p, |
||||
streamer: streamer, |
||||
trackId: trackId, |
||||
trackFlowType: trackFlowType, |
||||
publisherIp: publisherIp, |
||||
nconn: nconn, |
||||
readBuf1: make([]byte, 2048), |
||||
readBuf2: make([]byte, 2048), |
||||
lastFrameTime: time.Now(), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
return l, nil |
||||
} |
||||
|
||||
func (l *streamerUdpListener) close() { |
||||
l.nconn.Close() |
||||
|
||||
if l.running { |
||||
<-l.done |
||||
} |
||||
} |
||||
|
||||
func (l *streamerUdpListener) start() { |
||||
l.running = true |
||||
go l.run() |
||||
} |
||||
|
||||
func (l *streamerUdpListener) run() { |
||||
for { |
||||
var buf []byte |
||||
if !l.readCurBuf { |
||||
buf = l.readBuf1 |
||||
} else { |
||||
buf = l.readBuf2 |
||||
} |
||||
l.readCurBuf = !l.readCurBuf |
||||
|
||||
n, addr, err := l.nconn.ReadFromUDP(buf) |
||||
if err != nil { |
||||
break |
||||
} |
||||
|
||||
if !l.publisherIp.Equal(addr.IP) || addr.Port != l.publisherPort { |
||||
continue |
||||
} |
||||
|
||||
l.lastFrameTime = time.Now() |
||||
|
||||
l.p.events <- programEventStreamerFrame{l.streamer, l.trackId, l.trackFlowType, buf[:n]} |
||||
} |
||||
|
||||
close(l.done) |
||||
} |
@ -0,0 +1,618 @@
@@ -0,0 +1,618 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math/rand" |
||||
"net" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/aler9/gortsplib" |
||||
"gortc.io/sdp" |
||||
) |
||||
|
||||
const ( |
||||
_DIAL_TIMEOUT = 10 * time.Second |
||||
_RETRY_INTERVAL = 5 * time.Second |
||||
_CHECK_STREAM_INTERVAL = 6 * time.Second |
||||
_STREAM_DEAD_AFTER = 5 * time.Second |
||||
_KEEPALIVE_INTERVAL = 60 * time.Second |
||||
) |
||||
|
||||
type streamerUdpListenerPair struct { |
||||
udplRtp *streamerUdpListener |
||||
udplRtcp *streamerUdpListener |
||||
} |
||||
|
||||
type streamer struct { |
||||
p *program |
||||
path string |
||||
ur *url.URL |
||||
proto streamProtocol |
||||
ready bool |
||||
clientSdpParsed *sdp.Message |
||||
serverSdpText []byte |
||||
serverSdpParsed *sdp.Message |
||||
firstTime bool |
||||
readBuf1 []byte |
||||
readBuf2 []byte |
||||
readCurBuf bool |
||||
|
||||
terminate chan struct{} |
||||
done chan struct{} |
||||
} |
||||
|
||||
func newStreamer(p *program, path string, source string, sourceProtocol string) (*streamer, error) { |
||||
ur, err := url.Parse(source) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("'%s' is not a valid source not an RTSP url", source) |
||||
} |
||||
if ur.Scheme != "rtsp" { |
||||
return nil, fmt.Errorf("'%s' is not a valid RTSP url", source) |
||||
} |
||||
|
||||
if ur.User != nil { |
||||
pass, _ := ur.User.Password() |
||||
user := ur.User.Username() |
||||
if user != "" && pass == "" || |
||||
user == "" && pass != "" { |
||||
fmt.Errorf("username and password must be both provided") |
||||
} |
||||
} |
||||
|
||||
proto, err := func() (streamProtocol, error) { |
||||
switch sourceProtocol { |
||||
case "udp": |
||||
return _STREAM_PROTOCOL_UDP, nil |
||||
|
||||
case "tcp": |
||||
return _STREAM_PROTOCOL_TCP, nil |
||||
} |
||||
return streamProtocol(0), fmt.Errorf("unsupported protocol '%s'", sourceProtocol) |
||||
}() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
s := &streamer{ |
||||
p: p, |
||||
path: path, |
||||
ur: ur, |
||||
proto: proto, |
||||
firstTime: true, |
||||
readBuf1: make([]byte, 0, 512*1024), |
||||
readBuf2: make([]byte, 0, 512*1024), |
||||
terminate: make(chan struct{}), |
||||
done: make(chan struct{}), |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
func (s *streamer) log(format string, args ...interface{}) { |
||||
s.p.log("[streamer "+s.path+"] "+format, args...) |
||||
} |
||||
|
||||
func (s *streamer) publisherIsReady() bool { |
||||
return s.ready |
||||
} |
||||
|
||||
func (s *streamer) publisherSdpText() []byte { |
||||
return s.serverSdpText |
||||
} |
||||
|
||||
func (s *streamer) publisherSdpParsed() *sdp.Message { |
||||
return s.serverSdpParsed |
||||
} |
||||
|
||||
func (s *streamer) run() { |
||||
for { |
||||
ok := s.do() |
||||
if !ok { |
||||
break |
||||
} |
||||
} |
||||
|
||||
close(s.done) |
||||
} |
||||
|
||||
func (s *streamer) do() bool { |
||||
if s.firstTime { |
||||
s.firstTime = false |
||||
} else { |
||||
t := time.NewTimer(_RETRY_INTERVAL) |
||||
select { |
||||
case <-s.terminate: |
||||
return false |
||||
case <-t.C: |
||||
} |
||||
} |
||||
|
||||
s.log("initializing with protocol %s", s.proto) |
||||
|
||||
var nconn net.Conn |
||||
var err error |
||||
dialDone := make(chan struct{}) |
||||
go func() { |
||||
nconn, err = net.DialTimeout("tcp", s.ur.Host, _DIAL_TIMEOUT) |
||||
close(dialDone) |
||||
}() |
||||
|
||||
select { |
||||
case <-s.terminate: |
||||
return false |
||||
case <-dialDone: |
||||
} |
||||
|
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
defer nconn.Close() |
||||
|
||||
conn, err := gortsplib.NewConnClient(gortsplib.ConnClientConf{ |
||||
NConn: nconn, |
||||
Username: func() string { |
||||
if s.ur.User != nil { |
||||
return s.ur.User.Username() |
||||
} |
||||
return "" |
||||
}(), |
||||
Password: func() string { |
||||
if s.ur.User != nil { |
||||
pass, _ := s.ur.User.Password() |
||||
return pass |
||||
} |
||||
return "" |
||||
}(), |
||||
ReadTimeout: s.p.conf.ReadTimeout, |
||||
WriteTimeout: s.p.conf.WriteTimeout, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
res, err := conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.OPTIONS, |
||||
Url: &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: "/", |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
// OPTIONS is not available in some cameras
|
||||
if res.StatusCode != gortsplib.StatusOK && res.StatusCode != gortsplib.StatusNotFound { |
||||
s.log("ERR: OPTIONS returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
return true |
||||
} |
||||
|
||||
res, err = conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.DESCRIBE, |
||||
Url: &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: s.ur.Path, |
||||
RawQuery: s.ur.RawQuery, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
if res.StatusCode != gortsplib.StatusOK { |
||||
s.log("ERR: DESCRIBE returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
return true |
||||
} |
||||
|
||||
contentType, ok := res.Header["Content-Type"] |
||||
if !ok || len(contentType) != 1 { |
||||
s.log("ERR: Content-Type not provided") |
||||
return true |
||||
} |
||||
|
||||
if contentType[0] != "application/sdp" { |
||||
s.log("ERR: wrong Content-Type, expected application/sdp") |
||||
return true |
||||
} |
||||
|
||||
clientSdpParsed, err := gortsplib.SDPParse(res.Content) |
||||
if err != nil { |
||||
s.log("ERR: invalid SDP: %s", err) |
||||
return true |
||||
} |
||||
|
||||
// create a filtered SDP that is used by the server (not by the client)
|
||||
serverSdpParsed, serverSdpText := gortsplib.SDPFilter(clientSdpParsed, res.Content) |
||||
|
||||
s.clientSdpParsed = clientSdpParsed |
||||
s.serverSdpText = serverSdpText |
||||
s.serverSdpParsed = serverSdpParsed |
||||
|
||||
if s.proto == _STREAM_PROTOCOL_UDP { |
||||
return s.runUdp(conn) |
||||
} else { |
||||
return s.runTcp(conn) |
||||
} |
||||
} |
||||
|
||||
func (s *streamer) runUdp(conn *gortsplib.ConnClient) bool { |
||||
publisherIp := conn.NetConn().RemoteAddr().(*net.TCPAddr).IP |
||||
|
||||
var streamerUdpListenerPairs []streamerUdpListenerPair |
||||
|
||||
defer func() { |
||||
for _, pair := range streamerUdpListenerPairs { |
||||
pair.udplRtp.close() |
||||
pair.udplRtcp.close() |
||||
} |
||||
}() |
||||
|
||||
for i, media := range s.clientSdpParsed.Medias { |
||||
var rtpPort int |
||||
var rtcpPort int |
||||
var udplRtp *streamerUdpListener |
||||
var udplRtcp *streamerUdpListener |
||||
func() { |
||||
for { |
||||
// choose two consecutive ports in range 65536-10000
|
||||
// rtp must be pair and rtcp odd
|
||||
rtpPort = (rand.Intn((65535-10000)/2) * 2) + 10000 |
||||
rtcpPort = rtpPort + 1 |
||||
|
||||
var err error |
||||
udplRtp, err = newStreamerUdpListener(s.p, rtpPort, s, i, |
||||
_TRACK_FLOW_RTP, publisherIp) |
||||
if err != nil { |
||||
continue |
||||
} |
||||
|
||||
udplRtcp, err = newStreamerUdpListener(s.p, rtcpPort, s, i, |
||||
_TRACK_FLOW_RTCP, publisherIp) |
||||
if err != nil { |
||||
udplRtp.close() |
||||
continue |
||||
} |
||||
|
||||
return |
||||
} |
||||
}() |
||||
|
||||
res, err := conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.SETUP, |
||||
Url: func() *url.URL { |
||||
control := media.Attributes.Value("control") |
||||
|
||||
// no control attribute
|
||||
if control == "" { |
||||
return s.ur |
||||
} |
||||
|
||||
// absolute path
|
||||
if strings.HasPrefix(control, "rtsp://") { |
||||
ur, err := url.Parse(control) |
||||
if err != nil { |
||||
return s.ur |
||||
} |
||||
return ur |
||||
} |
||||
|
||||
// relative path
|
||||
return &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: func() string { |
||||
ret := s.ur.Path |
||||
|
||||
if len(ret) == 0 || ret[len(ret)-1] != '/' { |
||||
ret += "/" |
||||
} |
||||
|
||||
control := media.Attributes.Value("control") |
||||
if control != "" { |
||||
ret += control |
||||
} else { |
||||
ret += "trackID=" + strconv.FormatInt(int64(i+1), 10) |
||||
} |
||||
|
||||
return ret |
||||
}(), |
||||
RawQuery: s.ur.RawQuery, |
||||
} |
||||
}(), |
||||
Header: gortsplib.Header{ |
||||
"Transport": []string{strings.Join([]string{ |
||||
"RTP/AVP/UDP", |
||||
"unicast", |
||||
fmt.Sprintf("client_port=%d-%d", rtpPort, rtcpPort), |
||||
}, ";")}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
udplRtp.close() |
||||
udplRtcp.close() |
||||
return true |
||||
} |
||||
|
||||
if res.StatusCode != gortsplib.StatusOK { |
||||
s.log("ERR: SETUP returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
udplRtp.close() |
||||
udplRtcp.close() |
||||
return true |
||||
} |
||||
|
||||
tsRaw, ok := res.Header["Transport"] |
||||
if !ok || len(tsRaw) != 1 { |
||||
s.log("ERR: transport header not provided") |
||||
udplRtp.close() |
||||
udplRtcp.close() |
||||
return true |
||||
} |
||||
|
||||
th := gortsplib.ReadHeaderTransport(tsRaw[0]) |
||||
rtpServerPort, rtcpServerPort := th.GetPorts("server_port") |
||||
if rtpServerPort == 0 { |
||||
s.log("ERR: server ports not provided") |
||||
udplRtp.close() |
||||
udplRtcp.close() |
||||
return true |
||||
} |
||||
|
||||
udplRtp.publisherPort = rtpServerPort |
||||
udplRtcp.publisherPort = rtcpServerPort |
||||
|
||||
streamerUdpListenerPairs = append(streamerUdpListenerPairs, streamerUdpListenerPair{ |
||||
udplRtp: udplRtp, |
||||
udplRtcp: udplRtcp, |
||||
}) |
||||
} |
||||
|
||||
res, err := conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.PLAY, |
||||
Url: &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: s.ur.Path, |
||||
RawQuery: s.ur.RawQuery, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
if res.StatusCode != gortsplib.StatusOK { |
||||
s.log("ERR: PLAY returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
return true |
||||
} |
||||
|
||||
for _, pair := range streamerUdpListenerPairs { |
||||
pair.udplRtp.start() |
||||
pair.udplRtcp.start() |
||||
} |
||||
|
||||
tickerSendKeepalive := time.NewTicker(_KEEPALIVE_INTERVAL) |
||||
defer tickerSendKeepalive.Stop() |
||||
|
||||
tickerCheckStream := time.NewTicker(_CHECK_STREAM_INTERVAL) |
||||
defer tickerSendKeepalive.Stop() |
||||
|
||||
s.p.events <- programEventStreamerReady{s} |
||||
|
||||
defer func() { |
||||
s.p.events <- programEventStreamerNotReady{s} |
||||
}() |
||||
|
||||
for { |
||||
select { |
||||
case <-s.terminate: |
||||
return false |
||||
|
||||
case <-tickerSendKeepalive.C: |
||||
_, err = conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.OPTIONS, |
||||
Url: &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: "/", |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
case <-tickerCheckStream.C: |
||||
lastFrameTime := time.Time{} |
||||
|
||||
for _, pair := range streamerUdpListenerPairs { |
||||
lft := pair.udplRtp.lastFrameTime |
||||
if lft.After(lastFrameTime) { |
||||
lastFrameTime = lft |
||||
} |
||||
|
||||
lft = pair.udplRtp.lastFrameTime |
||||
if lft.After(lastFrameTime) { |
||||
lastFrameTime = lft |
||||
} |
||||
} |
||||
|
||||
if time.Since(lastFrameTime) >= _STREAM_DEAD_AFTER { |
||||
s.log("ERR: stream is dead") |
||||
return true |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *streamer) runTcp(conn *gortsplib.ConnClient) bool { |
||||
for i, media := range s.clientSdpParsed.Medias { |
||||
interleaved := fmt.Sprintf("interleaved=%d-%d", (i * 2), (i*2)+1) |
||||
|
||||
res, err := conn.WriteRequest(&gortsplib.Request{ |
||||
Method: gortsplib.SETUP, |
||||
Url: func() *url.URL { |
||||
control := media.Attributes.Value("control") |
||||
|
||||
// no control attribute
|
||||
if control == "" { |
||||
return s.ur |
||||
} |
||||
|
||||
// absolute path
|
||||
if strings.HasPrefix(control, "rtsp://") { |
||||
ur, err := url.Parse(control) |
||||
if err != nil { |
||||
return s.ur |
||||
} |
||||
return ur |
||||
} |
||||
|
||||
// relative path
|
||||
return &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: func() string { |
||||
ret := s.ur.Path |
||||
|
||||
if len(ret) == 0 || ret[len(ret)-1] != '/' { |
||||
ret += "/" |
||||
} |
||||
|
||||
control := media.Attributes.Value("control") |
||||
if control != "" { |
||||
ret += control |
||||
} else { |
||||
ret += "trackID=" + strconv.FormatInt(int64(i+1), 10) |
||||
} |
||||
|
||||
return ret |
||||
}(), |
||||
RawQuery: s.ur.RawQuery, |
||||
} |
||||
}(), |
||||
Header: gortsplib.Header{ |
||||
"Transport": []string{strings.Join([]string{ |
||||
"RTP/AVP/TCP", |
||||
"unicast", |
||||
interleaved, |
||||
}, ";")}, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
if res.StatusCode != gortsplib.StatusOK { |
||||
s.log("ERR: SETUP returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
return true |
||||
} |
||||
|
||||
tsRaw, ok := res.Header["Transport"] |
||||
if !ok || len(tsRaw) != 1 { |
||||
s.log("ERR: transport header not provided") |
||||
return true |
||||
} |
||||
|
||||
th := gortsplib.ReadHeaderTransport(tsRaw[0]) |
||||
|
||||
_, ok = th[interleaved] |
||||
if !ok { |
||||
s.log("ERR: transport header does not have %s (%s)", interleaved, tsRaw[0]) |
||||
return true |
||||
} |
||||
} |
||||
|
||||
err := conn.WriteRequestNoResponse(&gortsplib.Request{ |
||||
Method: gortsplib.PLAY, |
||||
Url: &url.URL{ |
||||
Scheme: "rtsp", |
||||
Host: s.ur.Host, |
||||
Path: s.ur.Path, |
||||
RawQuery: s.ur.RawQuery, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
frame := &gortsplib.InterleavedFrame{} |
||||
|
||||
outer: |
||||
for { |
||||
if !s.readCurBuf { |
||||
frame.Content = s.readBuf1 |
||||
} else { |
||||
frame.Content = s.readBuf2 |
||||
} |
||||
|
||||
frame.Content = frame.Content[:cap(frame.Content)] |
||||
s.readCurBuf = !s.readCurBuf |
||||
|
||||
vres, err := conn.ReadInterleavedFrameOrResponse(frame) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
return true |
||||
} |
||||
|
||||
switch res := vres.(type) { |
||||
case *gortsplib.Response: |
||||
if res.StatusCode != gortsplib.StatusOK { |
||||
s.log("ERR: PLAY returned code %d (%s)", res.StatusCode, res.StatusMessage) |
||||
return true |
||||
} |
||||
break outer |
||||
|
||||
case *gortsplib.InterleavedFrame: |
||||
// ignore the frames sent before the response
|
||||
} |
||||
} |
||||
|
||||
s.p.events <- programEventStreamerReady{s} |
||||
|
||||
defer func() { |
||||
s.p.events <- programEventStreamerNotReady{s} |
||||
}() |
||||
|
||||
chanConnError := make(chan struct{}) |
||||
go func() { |
||||
for { |
||||
frame := &gortsplib.InterleavedFrame{ |
||||
Content: make([]byte, 512*1024), |
||||
} |
||||
err := conn.ReadInterleavedFrame(frame) |
||||
if err != nil { |
||||
s.log("ERR: %s", err) |
||||
close(chanConnError) |
||||
break |
||||
} |
||||
|
||||
trackId, trackFlowType := interleavedChannelToTrack(frame.Channel) |
||||
|
||||
s.p.events <- programEventStreamerFrame{s, trackId, trackFlowType, frame.Content} |
||||
} |
||||
}() |
||||
|
||||
select { |
||||
case <-s.terminate: |
||||
return false |
||||
case <-chanConnError: |
||||
return true |
||||
} |
||||
} |
||||
|
||||
func (s *streamer) close() { |
||||
close(s.terminate) |
||||
<-s.done |
||||
} |
Loading…
Reference in new issue