golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
239 lines
5.0 KiB
239 lines
5.0 KiB
package hls |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"io" |
|
"strconv" |
|
"time" |
|
|
|
"github.com/aler9/gortsplib" |
|
"github.com/asticode/go-astits" |
|
|
|
"github.com/aler9/rtsp-simple-server/internal/aac" |
|
"github.com/aler9/rtsp-simple-server/internal/h264" |
|
) |
|
|
|
type segment struct { |
|
videoTrack *gortsplib.Track |
|
h264Conf *gortsplib.TrackConfigH264 |
|
aacConf *gortsplib.TrackConfigAAC |
|
|
|
name string |
|
buf bytes.Buffer |
|
mux *astits.Muxer |
|
firstPacketWritten bool |
|
minPTS time.Duration |
|
maxPTS time.Duration |
|
startPCR time.Time |
|
pcrSendCounter int |
|
} |
|
|
|
func newSegment( |
|
videoTrack *gortsplib.Track, |
|
audioTrack *gortsplib.Track, |
|
h264Conf *gortsplib.TrackConfigH264, |
|
aacConf *gortsplib.TrackConfigAAC, |
|
) *segment { |
|
t := &segment{ |
|
videoTrack: videoTrack, |
|
h264Conf: h264Conf, |
|
aacConf: aacConf, |
|
name: strconv.FormatInt(time.Now().Unix(), 10), |
|
} |
|
|
|
t.mux = astits.NewMuxer(context.Background(), &t.buf) |
|
|
|
if videoTrack != nil { |
|
t.mux.AddElementaryStream(astits.PMTElementaryStream{ |
|
ElementaryPID: 256, |
|
StreamType: astits.StreamTypeH264Video, |
|
}) |
|
} |
|
|
|
if audioTrack != nil { |
|
t.mux.AddElementaryStream(astits.PMTElementaryStream{ |
|
ElementaryPID: 257, |
|
StreamType: astits.StreamTypeAACAudio, |
|
}) |
|
} |
|
|
|
if videoTrack != nil { |
|
t.mux.SetPCRPID(256) |
|
} else { |
|
t.mux.SetPCRPID(257) |
|
} |
|
|
|
// WriteTable() is called automatically when WriteData() is called with |
|
// - PID == PCRPID |
|
// - AdaptationField != nil |
|
// - RandomAccessIndicator = true |
|
|
|
return t |
|
} |
|
|
|
func (t *segment) duration() time.Duration { |
|
return t.maxPTS - t.minPTS |
|
} |
|
|
|
func (t *segment) setStartPCR(startPCR time.Time) { |
|
t.startPCR = startPCR |
|
} |
|
|
|
func (t *segment) reader() io.Reader { |
|
return bytes.NewReader(t.buf.Bytes()) |
|
} |
|
|
|
func (t *segment) writeH264( |
|
dts time.Duration, |
|
pts time.Duration, |
|
isIDR bool, |
|
nalus [][]byte) error { |
|
if !t.firstPacketWritten { |
|
t.firstPacketWritten = true |
|
t.minPTS = pts |
|
t.maxPTS = pts |
|
} else { |
|
if pts < t.minPTS { |
|
t.minPTS = pts |
|
} |
|
if pts > t.maxPTS { |
|
t.maxPTS = pts |
|
} |
|
} |
|
|
|
filteredNALUs := [][]byte{ |
|
// prepend an AUD. This is required by video.js and iOS |
|
{byte(h264.NALUTypeAccessUnitDelimiter), 240}, |
|
} |
|
|
|
for _, nalu := range nalus { |
|
// remove existing SPS, PPS, AUD |
|
typ := h264.NALUType(nalu[0] & 0x1F) |
|
switch typ { |
|
case h264.NALUTypeSPS, h264.NALUTypePPS, h264.NALUTypeAccessUnitDelimiter: |
|
continue |
|
} |
|
|
|
// add SPS and PPS before IDR |
|
if typ == h264.NALUTypeIDR { |
|
filteredNALUs = append(filteredNALUs, t.h264Conf.SPS) |
|
filteredNALUs = append(filteredNALUs, t.h264Conf.PPS) |
|
} |
|
|
|
filteredNALUs = append(filteredNALUs, nalu) |
|
} |
|
|
|
enc, err := h264.EncodeAnnexB(filteredNALUs) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var af *astits.PacketAdaptationField |
|
|
|
if isIDR { |
|
if af == nil { |
|
af = &astits.PacketAdaptationField{} |
|
} |
|
af.RandomAccessIndicator = true |
|
} |
|
|
|
// send PCR once in a while |
|
if t.pcrSendCounter == 0 { |
|
if af == nil { |
|
af = &astits.PacketAdaptationField{} |
|
} |
|
af.HasPCR = true |
|
af.PCR = &astits.ClockReference{Base: int64(time.Since(t.startPCR).Seconds() * 90000)} |
|
t.pcrSendCounter = 3 |
|
} |
|
t.pcrSendCounter-- |
|
|
|
oh := &astits.PESOptionalHeader{ |
|
MarkerBits: 2, |
|
} |
|
|
|
if dts == pts { |
|
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS |
|
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)} |
|
} else { |
|
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent |
|
oh.DTS = &astits.ClockReference{Base: int64(dts.Seconds() * 90000)} |
|
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)} |
|
} |
|
|
|
_, err = t.mux.WriteData(&astits.MuxerData{ |
|
PID: 256, |
|
AdaptationField: af, |
|
PES: &astits.PESData{ |
|
Header: &astits.PESHeader{ |
|
OptionalHeader: oh, |
|
StreamID: 224, // = video |
|
}, |
|
Data: enc, |
|
}, |
|
}) |
|
return err |
|
} |
|
|
|
func (t *segment) writeAAC( |
|
pts time.Duration, |
|
au []byte) error { |
|
if t.videoTrack == nil { |
|
if !t.firstPacketWritten { |
|
t.firstPacketWritten = true |
|
t.minPTS = pts |
|
t.maxPTS = pts |
|
} else { |
|
if pts < t.minPTS { |
|
t.minPTS = pts |
|
} |
|
if pts > t.maxPTS { |
|
t.maxPTS = pts |
|
} |
|
} |
|
} |
|
|
|
adtsPkt, err := aac.EncodeADTS([]*aac.ADTSPacket{ |
|
{ |
|
SampleRate: t.aacConf.SampleRate, |
|
ChannelCount: t.aacConf.ChannelCount, |
|
AU: au, |
|
}, |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
af := &astits.PacketAdaptationField{ |
|
RandomAccessIndicator: true, |
|
} |
|
|
|
// if audio is the only track |
|
if t.videoTrack == nil { |
|
// send PCR once in a while |
|
if t.pcrSendCounter == 0 { |
|
af.HasPCR = true |
|
af.PCR = &astits.ClockReference{Base: int64(time.Since(t.startPCR).Seconds() * 90000)} |
|
t.pcrSendCounter = 3 |
|
} |
|
} |
|
|
|
_, err = t.mux.WriteData(&astits.MuxerData{ |
|
PID: 257, |
|
AdaptationField: af, |
|
PES: &astits.PESData{ |
|
Header: &astits.PESHeader{ |
|
OptionalHeader: &astits.PESOptionalHeader{ |
|
MarkerBits: 2, |
|
PTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS, |
|
PTS: &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}, |
|
}, |
|
PacketLength: uint16(len(adtsPkt) + 8), |
|
StreamID: 192, // = audio |
|
}, |
|
Data: adtsPkt, |
|
}, |
|
}) |
|
return err |
|
}
|
|
|