mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-10-05 23:56:54 +08:00
normalize sources
This commit is contained in:
@@ -74,9 +74,20 @@ func (s *hlsSource) run() {
|
|||||||
|
|
||||||
outer:
|
outer:
|
||||||
for {
|
for {
|
||||||
ok := s.runInner()
|
innerCtx, innerCtxCancel := context.WithCancel(context.Background())
|
||||||
if !ok {
|
innerErr := make(chan error)
|
||||||
break outer
|
go func() {
|
||||||
|
innerErr <- s.runInner(innerCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-innerErr:
|
||||||
|
innerCtxCancel()
|
||||||
|
s.Log(logger.Info, "ERR: %v", err)
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
innerCtxCancel()
|
||||||
|
<-innerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -89,7 +100,7 @@ outer:
|
|||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hlsSource) runInner() bool {
|
func (s *hlsSource) runInner(innerCtx context.Context) error {
|
||||||
var stream *stream
|
var stream *stream
|
||||||
var videoTrackID int
|
var videoTrackID int
|
||||||
var audioTrackID int
|
var audioTrackID int
|
||||||
@@ -198,19 +209,17 @@ func (s *hlsSource) runInner() bool {
|
|||||||
s,
|
s,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(logger.Info, "ERR: %v", err)
|
return err
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-c.Wait():
|
case err := <-c.Wait():
|
||||||
s.Log(logger.Info, "ERR: %v", err)
|
return err
|
||||||
return true
|
|
||||||
|
|
||||||
case <-s.ctx.Done():
|
case <-innerCtx.Done():
|
||||||
c.Close()
|
c.Close()
|
||||||
<-c.Wait()
|
<-c.Wait()
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -61,7 +61,7 @@ func newRTMPSource(
|
|||||||
ctxCancel: ctxCancel,
|
ctxCancel: ctxCancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log(logger.Info, "started")
|
s.Log(logger.Info, "started")
|
||||||
|
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go s.run()
|
go s.run()
|
||||||
@@ -71,11 +71,11 @@ func newRTMPSource(
|
|||||||
|
|
||||||
// Close closes a Source.
|
// Close closes a Source.
|
||||||
func (s *rtmpSource) close() {
|
func (s *rtmpSource) close() {
|
||||||
s.log(logger.Info, "stopped")
|
s.Log(logger.Info, "stopped")
|
||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtmpSource) log(level logger.Level, format string, args ...interface{}) {
|
func (s *rtmpSource) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
s.parent.log(level, "[rtmp source] "+format, args...)
|
s.parent.log(level, "[rtmp source] "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +84,20 @@ func (s *rtmpSource) run() {
|
|||||||
|
|
||||||
outer:
|
outer:
|
||||||
for {
|
for {
|
||||||
ok := s.runInner()
|
innerCtx, innerCtxCancel := context.WithCancel(context.Background())
|
||||||
if !ok {
|
innerErr := make(chan error)
|
||||||
break outer
|
go func() {
|
||||||
|
innerErr <- s.runInner(innerCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-innerErr:
|
||||||
|
innerCtxCancel()
|
||||||
|
s.Log(logger.Info, "ERR: %v", err)
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
innerCtxCancel()
|
||||||
|
<-innerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -99,186 +110,167 @@ outer:
|
|||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtmpSource) runInner() bool {
|
func (s *rtmpSource) runInner(innerCtx context.Context) error {
|
||||||
innerCtx, innerCtxCancel := context.WithCancel(s.ctx)
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
runErr := make(chan error)
|
u, err := url.Parse(s.ur)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add default port
|
||||||
|
_, _, err = net.SplitHostPort(u.Host)
|
||||||
|
if err != nil {
|
||||||
|
u.Host = net.JoinHostPort(u.Host, "1935")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2, cancel2 := context.WithTimeout(innerCtx, time.Duration(s.readTimeout))
|
||||||
|
defer cancel2()
|
||||||
|
|
||||||
|
var d net.Dialer
|
||||||
|
nconn, err := d.DialContext(ctx2, "tcp", u.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := rtmp.NewConn(nconn)
|
||||||
|
|
||||||
|
readDone := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
runErr <- func() error {
|
readDone <- func() error {
|
||||||
s.log(logger.Debug, "connecting")
|
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
||||||
|
nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.writeTimeout)))
|
||||||
u, err := url.Parse(s.ur)
|
err = conn.InitializeClient(u, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add default port
|
nconn.SetWriteDeadline(time.Time{})
|
||||||
_, _, err = net.SplitHostPort(u.Host)
|
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
||||||
if err != nil {
|
videoTrack, audioTrack, err := conn.ReadTracks()
|
||||||
u.Host = net.JoinHostPort(u.Host, "1935")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx2, cancel2 := context.WithTimeout(innerCtx, time.Duration(s.readTimeout))
|
|
||||||
defer cancel2()
|
|
||||||
|
|
||||||
var d net.Dialer
|
|
||||||
nconn, err := d.DialContext(ctx2, "tcp", u.Host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn := rtmp.NewConn(nconn)
|
var tracks gortsplib.Tracks
|
||||||
|
videoTrackID := -1
|
||||||
|
audioTrackID := -1
|
||||||
|
|
||||||
readDone := make(chan error)
|
var h264Encoder *rtph264.Encoder
|
||||||
go func() {
|
if videoTrack != nil {
|
||||||
readDone <- func() error {
|
h264Encoder = &rtph264.Encoder{PayloadType: 96}
|
||||||
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
h264Encoder.Init()
|
||||||
nconn.SetWriteDeadline(time.Now().Add(time.Duration(s.writeTimeout)))
|
videoTrackID = len(tracks)
|
||||||
err = conn.InitializeClient(u, true)
|
tracks = append(tracks, videoTrack)
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nconn.SetWriteDeadline(time.Time{})
|
var aacEncoder *rtpaac.Encoder
|
||||||
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
if audioTrack != nil {
|
||||||
videoTrack, audioTrack, err := conn.ReadTracks()
|
aacEncoder = &rtpaac.Encoder{
|
||||||
if err != nil {
|
PayloadType: 96,
|
||||||
return err
|
SampleRate: audioTrack.ClockRate(),
|
||||||
}
|
SizeLength: 13,
|
||||||
|
IndexLength: 3,
|
||||||
|
IndexDeltaLength: 3,
|
||||||
|
}
|
||||||
|
aacEncoder.Init()
|
||||||
|
audioTrackID = len(tracks)
|
||||||
|
tracks = append(tracks, audioTrack)
|
||||||
|
}
|
||||||
|
|
||||||
var tracks gortsplib.Tracks
|
res := s.parent.onSourceStaticSetReady(pathSourceStaticSetReadyReq{
|
||||||
videoTrackID := -1
|
source: s,
|
||||||
audioTrackID := -1
|
tracks: tracks,
|
||||||
|
})
|
||||||
|
if res.err != nil {
|
||||||
|
return res.err
|
||||||
|
}
|
||||||
|
|
||||||
var h264Encoder *rtph264.Encoder
|
s.Log(logger.Info, "ready")
|
||||||
if videoTrack != nil {
|
|
||||||
h264Encoder = &rtph264.Encoder{PayloadType: 96}
|
|
||||||
h264Encoder.Init()
|
|
||||||
videoTrackID = len(tracks)
|
|
||||||
tracks = append(tracks, videoTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
var aacEncoder *rtpaac.Encoder
|
defer func() {
|
||||||
if audioTrack != nil {
|
s.parent.onSourceStaticSetNotReady(pathSourceStaticSetNotReadyReq{source: s})
|
||||||
aacEncoder = &rtpaac.Encoder{
|
|
||||||
PayloadType: 96,
|
|
||||||
SampleRate: audioTrack.ClockRate(),
|
|
||||||
SizeLength: 13,
|
|
||||||
IndexLength: 3,
|
|
||||||
IndexDeltaLength: 3,
|
|
||||||
}
|
|
||||||
aacEncoder.Init()
|
|
||||||
audioTrackID = len(tracks)
|
|
||||||
tracks = append(tracks, audioTrack)
|
|
||||||
}
|
|
||||||
|
|
||||||
res := s.parent.onSourceStaticSetReady(pathSourceStaticSetReadyReq{
|
|
||||||
source: s,
|
|
||||||
tracks: tracks,
|
|
||||||
})
|
|
||||||
if res.err != nil {
|
|
||||||
return res.err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log(logger.Info, "ready")
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
s.parent.onSourceStaticSetNotReady(pathSourceStaticSetNotReadyReq{source: s})
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
|
||||||
msg, err := conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tmsg := msg.(type) {
|
|
||||||
case *message.MsgVideo:
|
|
||||||
if tmsg.H264Type == flvio.AVC_NALU {
|
|
||||||
if videoTrack == nil {
|
|
||||||
return fmt.Errorf("received an H264 packet, but track is not set up")
|
|
||||||
}
|
|
||||||
|
|
||||||
nalus, err := h264.AVCCUnmarshal(tmsg.Payload)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to decode AVCC: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
pts := tmsg.DTS + tmsg.PTSDelta
|
|
||||||
|
|
||||||
pkts, err := h264Encoder.Encode(nalus, pts)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while encoding H264: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastPkt := len(pkts) - 1
|
|
||||||
for i, pkt := range pkts {
|
|
||||||
if i != lastPkt {
|
|
||||||
res.stream.writeData(&data{
|
|
||||||
trackID: videoTrackID,
|
|
||||||
rtp: pkt,
|
|
||||||
ptsEqualsDTS: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.stream.writeData(&data{
|
|
||||||
trackID: videoTrackID,
|
|
||||||
rtp: pkt,
|
|
||||||
ptsEqualsDTS: h264.IDRPresent(nalus),
|
|
||||||
h264NALUs: nalus,
|
|
||||||
h264PTS: pts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case *message.MsgAudio:
|
|
||||||
if tmsg.AACType == flvio.AAC_RAW {
|
|
||||||
if audioTrack == nil {
|
|
||||||
return fmt.Errorf("received an AAC packet, but track is not set up")
|
|
||||||
}
|
|
||||||
|
|
||||||
pkts, err := aacEncoder.Encode([][]byte{tmsg.Payload}, tmsg.DTS)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error while encoding AAC: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkt := range pkts {
|
|
||||||
res.stream.writeData(&data{
|
|
||||||
trackID: audioTrackID,
|
|
||||||
rtp: pkt,
|
|
||||||
ptsEqualsDTS: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
for {
|
||||||
case err := <-readDone:
|
nconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout)))
|
||||||
nconn.Close()
|
msg, err := conn.ReadMessage()
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
case <-innerCtx.Done():
|
switch tmsg := msg.(type) {
|
||||||
nconn.Close()
|
case *message.MsgVideo:
|
||||||
<-readDone
|
if tmsg.H264Type == flvio.AVC_NALU {
|
||||||
return nil
|
if videoTrack == nil {
|
||||||
|
return fmt.Errorf("received an H264 packet, but track is not set up")
|
||||||
|
}
|
||||||
|
|
||||||
|
nalus, err := h264.AVCCUnmarshal(tmsg.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode AVCC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pts := tmsg.DTS + tmsg.PTSDelta
|
||||||
|
|
||||||
|
pkts, err := h264Encoder.Encode(nalus, pts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while encoding H264: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPkt := len(pkts) - 1
|
||||||
|
for i, pkt := range pkts {
|
||||||
|
if i != lastPkt {
|
||||||
|
res.stream.writeData(&data{
|
||||||
|
trackID: videoTrackID,
|
||||||
|
rtp: pkt,
|
||||||
|
ptsEqualsDTS: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.stream.writeData(&data{
|
||||||
|
trackID: videoTrackID,
|
||||||
|
rtp: pkt,
|
||||||
|
ptsEqualsDTS: h264.IDRPresent(nalus),
|
||||||
|
h264NALUs: nalus,
|
||||||
|
h264PTS: pts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *message.MsgAudio:
|
||||||
|
if tmsg.AACType == flvio.AAC_RAW {
|
||||||
|
if audioTrack == nil {
|
||||||
|
return fmt.Errorf("received an AAC packet, but track is not set up")
|
||||||
|
}
|
||||||
|
|
||||||
|
pkts, err := aacEncoder.Encode([][]byte{tmsg.Payload}, tmsg.DTS)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while encoding AAC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range pkts {
|
||||||
|
res.stream.writeData(&data{
|
||||||
|
trackID: audioTrackID,
|
||||||
|
rtp: pkt,
|
||||||
|
ptsEqualsDTS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-runErr:
|
case err := <-readDone:
|
||||||
innerCtxCancel()
|
nconn.Close()
|
||||||
s.log(logger.Info, "ERR: %s", err)
|
return err
|
||||||
return true
|
|
||||||
|
|
||||||
case <-s.ctx.Done():
|
case <-innerCtx.Done():
|
||||||
innerCtxCancel()
|
nconn.Close()
|
||||||
<-runErr
|
<-readDone
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -71,7 +71,7 @@ func newRTSPSource(
|
|||||||
ctxCancel: ctxCancel,
|
ctxCancel: ctxCancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log(logger.Info, "started")
|
s.Log(logger.Info, "started")
|
||||||
|
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go s.run()
|
go s.run()
|
||||||
@@ -80,44 +80,49 @@ func newRTSPSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtspSource) close() {
|
func (s *rtspSource) close() {
|
||||||
s.log(logger.Info, "stopped")
|
s.Log(logger.Info, "stopped")
|
||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtspSource) log(level logger.Level, format string, args ...interface{}) {
|
func (s *rtspSource) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
s.parent.log(level, "[rtsp source] "+format, args...)
|
s.parent.log(level, "[rtsp source] "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtspSource) run() {
|
func (s *rtspSource) run() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
|
|
||||||
|
outer:
|
||||||
for {
|
for {
|
||||||
ok := func() bool {
|
innerCtx, innerCtxCancel := context.WithCancel(context.Background())
|
||||||
ok := s.runInner()
|
innerErr := make(chan error)
|
||||||
if !ok {
|
go func() {
|
||||||
return false
|
innerErr <- s.runInner(innerCtx)
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(rtspSourceRetryPause):
|
|
||||||
return true
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
if !ok {
|
|
||||||
break
|
select {
|
||||||
|
case err := <-innerErr:
|
||||||
|
innerCtxCancel()
|
||||||
|
s.Log(logger.Info, "ERR: %v", err)
|
||||||
|
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
innerCtxCancel()
|
||||||
|
<-innerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(rtspSourceRetryPause):
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
break outer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ctxCancel()
|
s.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *rtspSource) runInner() bool {
|
func (s *rtspSource) runInner(innerCtx context.Context) error {
|
||||||
s.log(logger.Debug, "connecting")
|
s.Log(logger.Debug, "connecting")
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
|
|
||||||
if s.fingerprint != "" {
|
if s.fingerprint != "" {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
@@ -145,23 +150,21 @@ func (s *rtspSource) runInner() bool {
|
|||||||
ReadBufferCount: s.readBufferCount,
|
ReadBufferCount: s.readBufferCount,
|
||||||
AnyPortEnable: s.anyPortEnable,
|
AnyPortEnable: s.anyPortEnable,
|
||||||
OnRequest: func(req *base.Request) {
|
OnRequest: func(req *base.Request) {
|
||||||
s.log(logger.Debug, "c->s %v", req)
|
s.Log(logger.Debug, "c->s %v", req)
|
||||||
},
|
},
|
||||||
OnResponse: func(res *base.Response) {
|
OnResponse: func(res *base.Response) {
|
||||||
s.log(logger.Debug, "s->c %v", res)
|
s.Log(logger.Debug, "s->c %v", res)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(s.ur)
|
u, err := url.Parse(s.ur)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log(logger.Info, "ERR: %s", err)
|
return err
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = c.Start(u.Scheme, u.Host)
|
err = c.Start(u.Scheme, u.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log(logger.Info, "ERR: %s", err)
|
return err
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
@@ -188,7 +191,7 @@ func (s *rtspSource) runInner() bool {
|
|||||||
return res.err
|
return res.err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log(logger.Info, "ready")
|
s.Log(logger.Info, "ready")
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
s.parent.onSourceStaticSetNotReady(pathSourceStaticSetNotReadyReq{source: s})
|
s.parent.onSourceStaticSetNotReady(pathSourceStaticSetNotReadyReq{source: s})
|
||||||
@@ -223,13 +226,12 @@ func (s *rtspSource) runInner() bool {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-readErr:
|
case err := <-readErr:
|
||||||
s.log(logger.Info, "ERR: %s", err)
|
return err
|
||||||
return true
|
|
||||||
|
|
||||||
case <-s.ctx.Done():
|
case <-innerCtx.Done():
|
||||||
c.Close()
|
c.Close()
|
||||||
<-readErr
|
<-readErr
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user