mirror of
https://github.com/bluenviron/mediamtx
synced 2025-02-15 02:47:38 +00:00
240 lines
5.0 KiB
Go
240 lines
5.0 KiB
Go
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
|
|
}
|