mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-10-22 07:09:34 +08:00
support reading H265 tracks with HLS (#1342)
* support reading H265 tracks with HLS * update README
This commit is contained in:
11
README.md
11
README.md
@@ -22,7 +22,7 @@ And can be read from the server with:
|
|||||||
|--------|--------|------|
|
|--------|--------|------|
|
||||||
|RTSP|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG2, M-JPEG, MP3, MPEG4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec|
|
|RTSP|UDP, UDP-Multicast, TCP, RTSPS|H264, H265, VP8, VP9, AV1, MPEG2, M-JPEG, MP3, MPEG4 Audio (AAC), Opus, G711, G722, LPCM and any RTP-compatible codec|
|
||||||
|RTMP|RTMP, RTMPS|H264, MPEG4 Audio (AAC)|
|
|RTMP|RTMP, RTMPS|H264, MPEG4 Audio (AAC)|
|
||||||
|HLS|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, MPEG4 Audio (AAC)|
|
|HLS|Low-Latency HLS, MP4-based HLS, legacy HLS|H264, H265, MPEG4 Audio (AAC)|
|
||||||
|WebRTC||H264, VP8, VP9, Opus, G711, G722|
|
|WebRTC||H264, VP8, VP9, Opus, G711, G722|
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
@@ -89,6 +89,7 @@ Features:
|
|||||||
* [Encryption](#encryption-1)
|
* [Encryption](#encryption-1)
|
||||||
* [HLS protocol](#hls-protocol)
|
* [HLS protocol](#hls-protocol)
|
||||||
* [General usage](#general-usage-2)
|
* [General usage](#general-usage-2)
|
||||||
|
* [Browser support](#browser-support)
|
||||||
* [Embedding](#embedding)
|
* [Embedding](#embedding)
|
||||||
* [Low-Latency variant](#low-latency-variant)
|
* [Low-Latency variant](#low-latency-variant)
|
||||||
* [Decreasing latency](#decreasing-latency)
|
* [Decreasing latency](#decreasing-latency)
|
||||||
@@ -903,7 +904,13 @@ http://localhost:8888/mystream
|
|||||||
|
|
||||||
where `mystream` is the name of a stream that is being published.
|
where `mystream` is the name of a stream that is being published.
|
||||||
|
|
||||||
Please be aware that HLS only supports a single H264 video track and a single AAC audio track due to limitations of most browsers. If you want to use HLS with streams that use other codecs, you have to re-encode them, for instance by using _FFmpeg_:
|
### Browser support
|
||||||
|
|
||||||
|
Although the server can produce HLS with a variety of video and audio codecs (that are listed at the beginningo of the README), not all browsers can read all codecs. You can check what codecs your browser can read by visiting this page:
|
||||||
|
|
||||||
|
https://jsfiddle.net/7nwxmLto
|
||||||
|
|
||||||
|
If you want to increase the compatibility of the stream in order to support most browsers, you have to re-encode it by using the H264 and AAC codecs, for instance by using _FFmpeg_:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -i rtsp://original-source -pix_fmt yuv420p -c:v libx264 -preset ultrafast -b:v 600k -c:a aac -b:a 160k -f rtsp rtsp://localhost:8554/mystream
|
ffmpeg -i rtsp://original-source -pix_fmt yuv420p -c:v libx264 -preset ultrafast -b:v 600k -c:a aac -b:a 160k -f rtsp rtsp://localhost:8554/mystream
|
||||||
|
8
go.mod
8
go.mod
@@ -3,9 +3,9 @@ module github.com/aler9/rtsp-simple-server
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
code.cloudfoundry.org/bytefmt v0.0.0
|
||||||
github.com/abema/go-mp4 v0.8.0
|
github.com/abema/go-mp4 v0.0.0
|
||||||
github.com/aler9/gortsplib/v2 v2.0.0-20221228192116-da21f946e562
|
github.com/aler9/gortsplib/v2 v2.0.0-20221229123705-ce25207cb823
|
||||||
github.com/asticode/go-astits v1.10.1-0.20220319093903-4abe66a9b757
|
github.com/asticode/go-astits v1.10.1-0.20220319093903-4abe66a9b757
|
||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
github.com/gin-gonic/gin v1.8.1
|
github.com/gin-gonic/gin v1.8.1
|
||||||
@@ -68,3 +68,5 @@ require (
|
|||||||
replace github.com/orcaman/writerseeker => github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82
|
replace github.com/orcaman/writerseeker => github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82
|
||||||
|
|
||||||
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5
|
||||||
|
|
||||||
|
replace github.com/abema/go-mp4 => github.com/aler9/go-mp4 v0.0.0-20221229152535-34c82c552218
|
||||||
|
8
go.sum
8
go.sum
@@ -1,11 +1,11 @@
|
|||||||
github.com/abema/go-mp4 v0.8.0 h1:JHYkOvTfBpTnqJHiFFOXe8d6wiFy5MtDnA10fgccNqY=
|
|
||||||
github.com/abema/go-mp4 v0.8.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/aler9/gortsplib/v2 v2.0.0-20221228192116-da21f946e562 h1://BJIsHw2vYKdPL6sKbxZEnlGPpj2PTznNzRpou87ds=
|
github.com/aler9/go-mp4 v0.0.0-20221229152535-34c82c552218 h1:Zak89uY+y0q/gL7jaKbl2XeyMOLT/5qVuW6TIJphEJY=
|
||||||
github.com/aler9/gortsplib/v2 v2.0.0-20221228192116-da21f946e562/go.mod h1:lMdAxc6daduSzVwh75yQkvH9UHCYHpng/vJ8uXKFzdA=
|
github.com/aler9/go-mp4 v0.0.0-20221229152535-34c82c552218/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||||
|
github.com/aler9/gortsplib/v2 v2.0.0-20221229123705-ce25207cb823 h1:EFq9LqgA15drNgXj3hNlmAouxjMYb9jyyBq6hmjDO8U=
|
||||||
|
github.com/aler9/gortsplib/v2 v2.0.0-20221229123705-ce25207cb823/go.mod h1:lMdAxc6daduSzVwh75yQkvH9UHCYHpng/vJ8uXKFzdA=
|
||||||
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4=
|
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82 h1:9WgSzBLo3a9ToSVV7sRTBYZ1GGOZUpq4+5H3SN0UZq4=
|
||||||
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U=
|
github.com/aler9/writerseeker v0.0.0-20220601075008-6f0e685b9c82/go.mod h1:qsMrZCbeBf/mCLOeF16KDkPu4gktn/pOWyaq1aYQE7U=
|
||||||
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
|
github.com/asticode/go-astikit v0.20.0 h1:+7N+J4E4lWx2QOkRdOf6DafWJMv6O4RRfgClwQokrH8=
|
||||||
|
@@ -71,7 +71,7 @@ type dataH264 struct {
|
|||||||
rtpPackets []*rtp.Packet
|
rtpPackets []*rtp.Packet
|
||||||
ntp time.Time
|
ntp time.Time
|
||||||
pts time.Duration
|
pts time.Duration
|
||||||
nalus [][]byte
|
au [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dataH264) getRTPPackets() []*rtp.Packet {
|
func (d *dataH264) getRTPPackets() []*rtp.Packet {
|
||||||
@@ -134,22 +134,23 @@ func (t *formatProcessorH264) updateTrackParametersFromNALUs(nalus [][]byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remux is needed to fix corrupted streams and make streams
|
func (t *formatProcessorH264) remuxAccessUnit(nalus [][]byte) [][]byte {
|
||||||
// compatible with all protocols.
|
addParameters := false
|
||||||
func (t *formatProcessorH264) remuxNALUs(nalus [][]byte) [][]byte {
|
|
||||||
addSPSPPS := false
|
|
||||||
n := 0
|
n := 0
|
||||||
|
|
||||||
for _, nalu := range nalus {
|
for _, nalu := range nalus {
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||||
|
|
||||||
switch typ {
|
switch typ {
|
||||||
case h264.NALUTypeSPS, h264.NALUTypePPS:
|
case h264.NALUTypeSPS, h264.NALUTypePPS: // remove parameters
|
||||||
continue
|
continue
|
||||||
case h264.NALUTypeAccessUnitDelimiter:
|
|
||||||
|
case h264.NALUTypeAccessUnitDelimiter: // remove AUDs
|
||||||
continue
|
continue
|
||||||
case h264.NALUTypeIDR:
|
|
||||||
// prepend SPS and PPS to the group if there's at least an IDR
|
case h264.NALUTypeIDR: // prepend parameters if there's at least an IDR
|
||||||
if !addSPSPPS {
|
if !addParameters {
|
||||||
addSPSPPS = true
|
addParameters = true
|
||||||
n += 2
|
n += 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,7 @@ func (t *formatProcessorH264) remuxNALUs(nalus [][]byte) [][]byte {
|
|||||||
filteredNALUs := make([][]byte, n)
|
filteredNALUs := make([][]byte, n)
|
||||||
i := 0
|
i := 0
|
||||||
|
|
||||||
if addSPSPPS {
|
if addParameters {
|
||||||
filteredNALUs[0] = t.format.SafeSPS()
|
filteredNALUs[0] = t.format.SafeSPS()
|
||||||
filteredNALUs[1] = t.format.SafePPS()
|
filteredNALUs[1] = t.format.SafePPS()
|
||||||
i = 2
|
i = 2
|
||||||
@@ -171,13 +172,12 @@ func (t *formatProcessorH264) remuxNALUs(nalus [][]byte) [][]byte {
|
|||||||
|
|
||||||
for _, nalu := range nalus {
|
for _, nalu := range nalus {
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||||
|
|
||||||
switch typ {
|
switch typ {
|
||||||
case h264.NALUTypeSPS, h264.NALUTypePPS:
|
case h264.NALUTypeSPS, h264.NALUTypePPS:
|
||||||
// remove since they're automatically added
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case h264.NALUTypeAccessUnitDelimiter:
|
case h264.NALUTypeAccessUnitDelimiter:
|
||||||
// remove since it is not needed
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ func (t *formatProcessorH264) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DecodeUntilMarker() is necessary, otherwise Encode() generates partial groups
|
// DecodeUntilMarker() is necessary, otherwise Encode() generates partial groups
|
||||||
nalus, pts, err := t.decoder.DecodeUntilMarker(pkt)
|
au, pts, err := t.decoder.DecodeUntilMarker(pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == rtph264.ErrNonStartingPacketAndNoPrevious || err == rtph264.ErrMorePacketsNeeded {
|
if err == rtph264.ErrNonStartingPacketAndNoPrevious || err == rtph264.ErrMorePacketsNeeded {
|
||||||
return nil
|
return nil
|
||||||
@@ -235,10 +235,9 @@ func (t *formatProcessorH264) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tdata.nalus = nalus
|
tdata.au = au
|
||||||
tdata.pts = pts
|
tdata.pts = pts
|
||||||
|
tdata.au = t.remuxAccessUnit(tdata.au)
|
||||||
tdata.nalus = t.remuxNALUs(tdata.nalus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// route packet as is
|
// route packet as is
|
||||||
@@ -246,11 +245,11 @@ func (t *formatProcessorH264) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.updateTrackParametersFromNALUs(tdata.nalus)
|
t.updateTrackParametersFromNALUs(tdata.au)
|
||||||
tdata.nalus = t.remuxNALUs(tdata.nalus)
|
tdata.au = t.remuxAccessUnit(tdata.au)
|
||||||
}
|
}
|
||||||
|
|
||||||
pkts, err := t.encoder.Encode(tdata.nalus, tdata.pts)
|
pkts, err := t.encoder.Encode(tdata.au, tdata.pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -78,7 +78,7 @@ type dataH265 struct {
|
|||||||
rtpPackets []*rtp.Packet
|
rtpPackets []*rtp.Packet
|
||||||
ntp time.Time
|
ntp time.Time
|
||||||
pts time.Duration
|
pts time.Duration
|
||||||
nalus [][]byte
|
au [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dataH265) getRTPPackets() []*rtp.Packet {
|
func (d *dataH265) getRTPPackets() []*rtp.Packet {
|
||||||
@@ -128,12 +128,82 @@ func (t *formatProcessorH265) updateTrackParametersFromRTPPacket(pkt *rtp.Packet
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *formatProcessorH265) updateTrackParametersFromNALUs(nalus [][]byte) {
|
func (t *formatProcessorH265) updateTrackParametersFromNALUs(nalus [][]byte) {
|
||||||
// TODO: extract VPS, SPS, PPS and set them into the track
|
for _, nalu := range nalus {
|
||||||
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case h265.NALUType_VPS_NUT:
|
||||||
|
if !bytes.Equal(nalu, t.format.SafeVPS()) {
|
||||||
|
t.format.SafeSetVPS(nalu)
|
||||||
|
}
|
||||||
|
|
||||||
|
case h265.NALUType_SPS_NUT:
|
||||||
|
if !bytes.Equal(nalu, t.format.SafePPS()) {
|
||||||
|
t.format.SafeSetSPS(nalu)
|
||||||
|
}
|
||||||
|
|
||||||
|
case h265.NALUType_PPS_NUT:
|
||||||
|
if !bytes.Equal(nalu, t.format.SafePPS()) {
|
||||||
|
t.format.SafeSetPPS(nalu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *formatProcessorH265) remuxNALUs(nalus [][]byte) [][]byte {
|
func (t *formatProcessorH265) remuxAccessUnit(nalus [][]byte) [][]byte {
|
||||||
// TODO: add VPS, SPS, PPS before IDRs
|
addParameters := false
|
||||||
return nalus
|
n := 0
|
||||||
|
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT: // remove parameters
|
||||||
|
continue
|
||||||
|
|
||||||
|
case h265.NALUType_AUD_NUT: // remove AUDs
|
||||||
|
continue
|
||||||
|
|
||||||
|
// prepend parameters if there's at least a random access unit
|
||||||
|
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
|
||||||
|
if !addParameters {
|
||||||
|
addParameters = true
|
||||||
|
n += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredNALUs := make([][]byte, n)
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
if addParameters {
|
||||||
|
filteredNALUs[0] = t.format.SafeVPS()
|
||||||
|
filteredNALUs[1] = t.format.SafeSPS()
|
||||||
|
filteredNALUs[2] = t.format.SafePPS()
|
||||||
|
i = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT:
|
||||||
|
continue
|
||||||
|
|
||||||
|
case h265.NALUType_AUD_NUT:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredNALUs[i] = nalu
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredNALUs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *formatProcessorH265) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
|
func (t *formatProcessorH265) process(dat data, hasNonRTSPReaders bool) error { //nolint:dupl
|
||||||
@@ -175,7 +245,7 @@ func (t *formatProcessorH265) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DecodeUntilMarker() is necessary, otherwise Encode() generates partial groups
|
// DecodeUntilMarker() is necessary, otherwise Encode() generates partial groups
|
||||||
nalus, pts, err := t.decoder.DecodeUntilMarker(pkt)
|
au, pts, err := t.decoder.DecodeUntilMarker(pkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == rtph265.ErrNonStartingPacketAndNoPrevious || err == rtph265.ErrMorePacketsNeeded {
|
if err == rtph265.ErrNonStartingPacketAndNoPrevious || err == rtph265.ErrMorePacketsNeeded {
|
||||||
return nil
|
return nil
|
||||||
@@ -183,10 +253,9 @@ func (t *formatProcessorH265) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tdata.nalus = nalus
|
tdata.au = au
|
||||||
tdata.pts = pts
|
tdata.pts = pts
|
||||||
|
tdata.au = t.remuxAccessUnit(tdata.au)
|
||||||
tdata.nalus = t.remuxNALUs(tdata.nalus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// route packet as is
|
// route packet as is
|
||||||
@@ -194,11 +263,11 @@ func (t *formatProcessorH265) process(dat data, hasNonRTSPReaders bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
t.updateTrackParametersFromNALUs(tdata.nalus)
|
t.updateTrackParametersFromNALUs(tdata.au)
|
||||||
tdata.nalus = t.remuxNALUs(tdata.nalus)
|
tdata.au = t.remuxAccessUnit(tdata.au)
|
||||||
}
|
}
|
||||||
|
|
||||||
pkts, err := t.encoder.Encode(tdata.nalus, tdata.pts)
|
pkts, err := t.encoder.Encode(tdata.au, tdata.pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -243,38 +243,18 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
|||||||
m.path.readerRemove(pathReaderRemoveReq{author: m})
|
m.path.readerRemove(pathReaderRemoveReq{author: m})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var videoFormat *format.H264
|
|
||||||
videoMedia := res.stream.medias().FindFormat(&videoFormat)
|
|
||||||
|
|
||||||
var audioFormat *format.MPEG4Audio
|
|
||||||
audioMedia := res.stream.medias().FindFormat(&audioFormat)
|
|
||||||
|
|
||||||
if videoFormat == nil && audioFormat == nil {
|
|
||||||
return fmt.Errorf("the stream doesn't contain an H264 track or an AAC track")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
m.muxer, err = hls.NewMuxer(
|
|
||||||
hls.MuxerVariant(m.hlsVariant),
|
|
||||||
m.hlsSegmentCount,
|
|
||||||
time.Duration(m.hlsSegmentDuration),
|
|
||||||
time.Duration(m.hlsPartDuration),
|
|
||||||
uint64(m.hlsSegmentMaxSize),
|
|
||||||
videoFormat,
|
|
||||||
audioFormat,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("muxer error: %v", err)
|
|
||||||
}
|
|
||||||
defer m.muxer.Close()
|
|
||||||
|
|
||||||
innerReady <- struct{}{}
|
|
||||||
|
|
||||||
m.ringBuffer, _ = ringbuffer.New(uint64(m.readBufferCount))
|
m.ringBuffer, _ = ringbuffer.New(uint64(m.readBufferCount))
|
||||||
|
|
||||||
var medias media.Medias
|
var medias media.Medias
|
||||||
|
|
||||||
if videoMedia != nil {
|
var videoFormat format.Format
|
||||||
|
var videoMedia *media.Media
|
||||||
|
|
||||||
|
var videoFormatH265 *format.H265
|
||||||
|
videoMedia = res.stream.medias().FindFormat(&videoFormatH265)
|
||||||
|
|
||||||
|
if videoFormatH265 != nil {
|
||||||
|
videoFormat = videoFormatH265
|
||||||
medias = append(medias, videoMedia)
|
medias = append(medias, videoMedia)
|
||||||
|
|
||||||
videoStartPTSFilled := false
|
videoStartPTSFilled := false
|
||||||
@@ -282,9 +262,9 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
|||||||
|
|
||||||
res.stream.readerAdd(m, videoMedia, videoFormat, func(dat data) {
|
res.stream.readerAdd(m, videoMedia, videoFormat, func(dat data) {
|
||||||
m.ringBuffer.Push(func() error {
|
m.ringBuffer.Push(func() error {
|
||||||
tdata := dat.(*dataH264)
|
tdata := dat.(*dataH265)
|
||||||
|
|
||||||
if tdata.nalus == nil {
|
if tdata.au == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +274,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
|||||||
}
|
}
|
||||||
pts := tdata.pts - videoStartPTS
|
pts := tdata.pts - videoStartPTS
|
||||||
|
|
||||||
err := m.muxer.WriteH264(tdata.ntp, pts, tdata.nalus)
|
err := m.muxer.WriteH26x(tdata.ntp, pts, tdata.au)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("muxer error: %v", err)
|
return fmt.Errorf("muxer error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -302,9 +282,46 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
var videoFormatH264 *format.H264
|
||||||
|
videoMedia = res.stream.medias().FindFormat(&videoFormatH264)
|
||||||
|
|
||||||
|
if videoFormatH264 != nil {
|
||||||
|
videoFormat = videoFormatH264
|
||||||
|
medias = append(medias, videoMedia)
|
||||||
|
|
||||||
|
videoStartPTSFilled := false
|
||||||
|
var videoStartPTS time.Duration
|
||||||
|
|
||||||
|
res.stream.readerAdd(m, videoMedia, videoFormat, func(dat data) {
|
||||||
|
m.ringBuffer.Push(func() error {
|
||||||
|
tdata := dat.(*dataH264)
|
||||||
|
|
||||||
|
if tdata.au == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !videoStartPTSFilled {
|
||||||
|
videoStartPTSFilled = true
|
||||||
|
videoStartPTS = tdata.pts
|
||||||
|
}
|
||||||
|
pts := tdata.pts - videoStartPTS
|
||||||
|
|
||||||
|
err := m.muxer.WriteH26x(tdata.ntp, pts, tdata.au)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("muxer error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if audioMedia != nil {
|
var audioFormat *format.MPEG4Audio
|
||||||
|
audioMedia := res.stream.medias().FindFormat(&audioFormat)
|
||||||
|
|
||||||
|
if audioFormat != nil {
|
||||||
medias = append(medias, audioMedia)
|
medias = append(medias, audioMedia)
|
||||||
|
|
||||||
audioStartPTSFilled := false
|
audioStartPTSFilled := false
|
||||||
@@ -342,6 +359,27 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{})
|
|||||||
|
|
||||||
defer res.stream.readerRemove(m)
|
defer res.stream.readerRemove(m)
|
||||||
|
|
||||||
|
if medias == nil {
|
||||||
|
return fmt.Errorf("the stream doesn't contain a supported video or audio track")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
m.muxer, err = hls.NewMuxer(
|
||||||
|
hls.MuxerVariant(m.hlsVariant),
|
||||||
|
m.hlsSegmentCount,
|
||||||
|
time.Duration(m.hlsSegmentDuration),
|
||||||
|
time.Duration(m.hlsPartDuration),
|
||||||
|
uint64(m.hlsSegmentMaxSize),
|
||||||
|
videoFormat,
|
||||||
|
audioFormat,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("muxer error: %v", err)
|
||||||
|
}
|
||||||
|
defer m.muxer.Close()
|
||||||
|
|
||||||
|
innerReady <- struct{}{}
|
||||||
|
|
||||||
m.log(logger.Info, "is converting into HLS, %s",
|
m.log(logger.Info, "is converting into HLS, %s",
|
||||||
sourceMediaInfo(medias))
|
sourceMediaInfo(medias))
|
||||||
|
|
||||||
|
@@ -84,11 +84,11 @@ func (s *hlsSource) run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
onVideoData := func(pts time.Duration, nalus [][]byte) {
|
onVideoData := func(pts time.Duration, au [][]byte) {
|
||||||
err := stream.writeData(videoMedia, videoMedia.Formats[0], &dataH264{
|
err := stream.writeData(videoMedia, videoMedia.Formats[0], &dataH264{
|
||||||
pts: pts,
|
pts: pts,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(logger.Warn, "%v", err)
|
s.Log(logger.Warn, "%v", err)
|
||||||
|
@@ -48,7 +48,7 @@ func (s *rpiCameraSource) run(ctx context.Context) error {
|
|||||||
medias := media.Medias{medi}
|
medias := media.Medias{medi}
|
||||||
var stream *stream
|
var stream *stream
|
||||||
|
|
||||||
onData := func(dts time.Duration, nalus [][]byte) {
|
onData := func(dts time.Duration, au [][]byte) {
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{
|
res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{
|
||||||
medias: medias,
|
medias: medias,
|
||||||
@@ -63,9 +63,9 @@ func (s *rpiCameraSource) run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := stream.writeData(medi, medi.Formats[0], &dataH264{
|
err := stream.writeData(medi, medi.Formats[0], &dataH264{
|
||||||
pts: dts,
|
pts: dts,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(logger.Warn, "%v", err)
|
s.Log(logger.Warn, "%v", err)
|
||||||
|
@@ -281,7 +281,7 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
|||||||
ringBuffer.Push(func() error {
|
ringBuffer.Push(func() error {
|
||||||
tdata := dat.(*dataH264)
|
tdata := dat.(*dataH264)
|
||||||
|
|
||||||
if tdata.nalus == nil {
|
if tdata.au == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
|||||||
idrPresent := false
|
idrPresent := false
|
||||||
nonIDRPresent := false
|
nonIDRPresent := false
|
||||||
|
|
||||||
for _, nalu := range tdata.nalus {
|
for _, nalu := range tdata.au {
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||||
switch typ {
|
switch typ {
|
||||||
case h264.NALUTypeIDR:
|
case h264.NALUTypeIDR:
|
||||||
@@ -317,7 +317,7 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
|||||||
videoDTSExtractor = h264.NewDTSExtractor()
|
videoDTSExtractor = h264.NewDTSExtractor()
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
dts, err = videoDTSExtractor.Extract(tdata.nalus, pts)
|
dts, err = videoDTSExtractor.Extract(tdata.au, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -331,7 +331,7 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
dts, err = videoDTSExtractor.Extract(tdata.nalus, pts)
|
dts, err = videoDTSExtractor.Extract(tdata.au, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +340,7 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
|
|||||||
pts -= videoStartDTS
|
pts -= videoStartDTS
|
||||||
}
|
}
|
||||||
|
|
||||||
avcc, err := h264.AVCCMarshal(tdata.nalus)
|
avcc, err := h264.AVCCMarshal(tdata.au)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -538,22 +538,22 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
|
|||||||
var onVideoData func(time.Duration, [][]byte)
|
var onVideoData func(time.Duration, [][]byte)
|
||||||
|
|
||||||
if _, ok := videoFormat.(*format.H264); ok {
|
if _, ok := videoFormat.(*format.H264); ok {
|
||||||
onVideoData = func(pts time.Duration, nalus [][]byte) {
|
onVideoData = func(pts time.Duration, au [][]byte) {
|
||||||
err = rres.stream.writeData(videoMedia, videoFormat, &dataH264{
|
err = rres.stream.writeData(videoMedia, videoFormat, &dataH264{
|
||||||
pts: pts,
|
pts: pts,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log(logger.Warn, "%v", err)
|
c.log(logger.Warn, "%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onVideoData = func(pts time.Duration, nalus [][]byte) {
|
onVideoData = func(pts time.Duration, au [][]byte) {
|
||||||
err = rres.stream.writeData(videoMedia, videoFormat, &dataH265{
|
err = rres.stream.writeData(videoMedia, videoFormat, &dataH265{
|
||||||
pts: pts,
|
pts: pts,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log(logger.Warn, "%v", err)
|
c.log(logger.Warn, "%v", err)
|
||||||
@@ -577,15 +577,15 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
|
|||||||
return fmt.Errorf("unable to parse H264 config: %v", err)
|
return fmt.Errorf("unable to parse H264 config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nalus := [][]byte{
|
au := [][]byte{
|
||||||
conf.SPS,
|
conf.SPS,
|
||||||
conf.PPS,
|
conf.PPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := rres.stream.writeData(videoMedia, videoFormat, &dataH264{
|
err := rres.stream.writeData(videoMedia, videoFormat, &dataH264{
|
||||||
pts: tmsg.DTS + tmsg.PTSDelta,
|
pts: tmsg.DTS + tmsg.PTSDelta,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.log(logger.Warn, "%v", err)
|
c.log(logger.Warn, "%v", err)
|
||||||
|
@@ -176,15 +176,15 @@ func (s *rtmpSource) run(ctx context.Context) error {
|
|||||||
return fmt.Errorf("received an H264 packet, but track is not set up")
|
return fmt.Errorf("received an H264 packet, but track is not set up")
|
||||||
}
|
}
|
||||||
|
|
||||||
nalus, err := h264.AVCCUnmarshal(tmsg.Payload)
|
au, err := h264.AVCCUnmarshal(tmsg.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to decode AVCC: %v", err)
|
return fmt.Errorf("unable to decode AVCC: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = res.stream.writeData(videoMedia, videoFormat, &dataH264{
|
err = res.stream.writeData(videoMedia, videoFormat, &dataH264{
|
||||||
pts: tmsg.DTS + tmsg.PTSDelta,
|
pts: tmsg.DTS + tmsg.PTSDelta,
|
||||||
nalus: nalus,
|
au: au,
|
||||||
ntp: time.Now(),
|
ntp: time.Now(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log(logger.Warn, "%v", err)
|
s.Log(logger.Warn, "%v", err)
|
||||||
|
@@ -519,12 +519,12 @@ func (c *webRTCConn) allocateTracks(medias media.Medias) ([]*webRTCTrack, error)
|
|||||||
cb: func(dat data, ctx context.Context, writeError chan error) {
|
cb: func(dat data, ctx context.Context, writeError chan error) {
|
||||||
tdata := dat.(*dataH264)
|
tdata := dat.(*dataH264)
|
||||||
|
|
||||||
if tdata.nalus == nil {
|
if tdata.au == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !firstNALUReceived {
|
if !firstNALUReceived {
|
||||||
if !h264.IDRPresent(tdata.nalus) {
|
if !h264.IDRPresent(tdata.au) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +541,7 @@ func (c *webRTCConn) allocateTracks(medias media.Medias) ([]*webRTCTrack, error)
|
|||||||
lastPTS = tdata.pts
|
lastPTS = tdata.pts
|
||||||
}
|
}
|
||||||
|
|
||||||
packets, err := encoder.Encode(tdata.nalus, tdata.pts)
|
packets, err := encoder.Encode(tdata.au, tdata.pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package fmp4
|
|||||||
import (
|
import (
|
||||||
gomp4 "github.com/abema/go-mp4"
|
gomp4 "github.com/abema/go-mp4"
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,24 +47,30 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var sps []byte
|
var h264SPS []byte
|
||||||
var pps []byte
|
var h264PPS []byte
|
||||||
var spsp h264.SPS
|
var h264SPSP h264.SPS
|
||||||
|
|
||||||
|
var h265VPS []byte
|
||||||
|
var h265SPS []byte
|
||||||
|
var h265PPS []byte
|
||||||
|
var h265SPSP h265.SPS
|
||||||
|
|
||||||
var width int
|
var width int
|
||||||
var height int
|
var height int
|
||||||
|
|
||||||
switch ttrack := track.Format.(type) {
|
switch ttrack := track.Format.(type) {
|
||||||
case *format.H264:
|
case *format.H264:
|
||||||
sps = ttrack.SafeSPS()
|
h264SPS = ttrack.SafeSPS()
|
||||||
pps = ttrack.SafePPS()
|
h264PPS = ttrack.SafePPS()
|
||||||
|
|
||||||
err = spsp.Unmarshal(sps)
|
err = h264SPSP.Unmarshal(h264SPS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
width = spsp.Width()
|
width = h264SPSP.Width()
|
||||||
height = spsp.Height()
|
height = h264SPSP.Height()
|
||||||
|
|
||||||
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||||
FullBox: gomp4.FullBox{
|
FullBox: gomp4.FullBox{
|
||||||
@@ -72,7 +79,33 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
TrackID: uint32(track.ID),
|
TrackID: uint32(track.ID),
|
||||||
Width: uint32(width * 65536),
|
Width: uint32(width * 65536),
|
||||||
Height: uint32(height * 65536),
|
Height: uint32(height * 65536),
|
||||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000},
|
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case *format.H265:
|
||||||
|
h265VPS = ttrack.SafeVPS()
|
||||||
|
h265SPS = ttrack.SafeSPS()
|
||||||
|
h265PPS = ttrack.SafePPS()
|
||||||
|
|
||||||
|
err = h265SPSP.Unmarshal(h265SPS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
width = h265SPSP.Width()
|
||||||
|
height = h265SPSP.Height()
|
||||||
|
|
||||||
|
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
|
||||||
|
FullBox: gomp4.FullBox{
|
||||||
|
Flags: [3]byte{0, 0, 3},
|
||||||
|
},
|
||||||
|
TrackID: uint32(track.ID),
|
||||||
|
Width: uint32(width * 65536),
|
||||||
|
Height: uint32(height * 65536),
|
||||||
|
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -86,7 +119,7 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
TrackID: uint32(track.ID),
|
TrackID: uint32(track.ID),
|
||||||
AlternateGroup: 1,
|
AlternateGroup: 1,
|
||||||
Volume: 256,
|
Volume: 256,
|
||||||
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000},
|
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -107,7 +140,7 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch track.Format.(type) {
|
switch track.Format.(type) {
|
||||||
case *format.H264:
|
case *format.H264, *format.H265:
|
||||||
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
|
||||||
HandlerType: [4]byte{'v', 'i', 'd', 'e'},
|
HandlerType: [4]byte{'v', 'i', 'd', 'e'},
|
||||||
Name: "VideoHandler",
|
Name: "VideoHandler",
|
||||||
@@ -132,7 +165,7 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch track.Format.(type) {
|
switch track.Format.(type) {
|
||||||
case *format.H264:
|
case *format.H264, *format.H265:
|
||||||
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
|
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
|
||||||
FullBox: gomp4.FullBox{
|
FullBox: gomp4.FullBox{
|
||||||
Flags: [3]byte{0, 0, 1},
|
Flags: [3]byte{0, 0, 1},
|
||||||
@@ -219,22 +252,22 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
Type: gomp4.BoxTypeAvcC(),
|
Type: gomp4.BoxTypeAvcC(),
|
||||||
},
|
},
|
||||||
ConfigurationVersion: 1,
|
ConfigurationVersion: 1,
|
||||||
Profile: spsp.ProfileIdc,
|
Profile: h264SPSP.ProfileIdc,
|
||||||
ProfileCompatibility: sps[2],
|
ProfileCompatibility: h264SPS[2],
|
||||||
Level: spsp.LevelIdc,
|
Level: h264SPSP.LevelIdc,
|
||||||
LengthSizeMinusOne: 3,
|
LengthSizeMinusOne: 3,
|
||||||
NumOfSequenceParameterSets: 1,
|
NumOfSequenceParameterSets: 1,
|
||||||
SequenceParameterSets: []gomp4.AVCParameterSet{
|
SequenceParameterSets: []gomp4.AVCParameterSet{
|
||||||
{
|
{
|
||||||
Length: uint16(len(sps)),
|
Length: uint16(len(h264SPS)),
|
||||||
NALUnit: sps,
|
NALUnit: h264SPS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NumOfPictureParameterSets: 1,
|
NumOfPictureParameterSets: 1,
|
||||||
PictureParameterSets: []gomp4.AVCParameterSet{
|
PictureParameterSets: []gomp4.AVCParameterSet{
|
||||||
{
|
{
|
||||||
Length: uint16(len(pps)),
|
Length: uint16(len(h264PPS)),
|
||||||
NALUnit: pps,
|
NALUnit: h264PPS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -255,6 +288,90 @@ func (track *InitTrack) marshal(w *mp4Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case *format.H265:
|
||||||
|
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <hev1>
|
||||||
|
SampleEntry: gomp4.SampleEntry{
|
||||||
|
AnyTypeBox: gomp4.AnyTypeBox{
|
||||||
|
Type: gomp4.BoxTypeHev1(),
|
||||||
|
},
|
||||||
|
DataReferenceIndex: 1,
|
||||||
|
},
|
||||||
|
Width: uint16(width),
|
||||||
|
Height: uint16(height),
|
||||||
|
Horizresolution: 4718592,
|
||||||
|
Vertresolution: 4718592,
|
||||||
|
FrameCount: 1,
|
||||||
|
Depth: 24,
|
||||||
|
PreDefined3: -1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.WriteBox(&gomp4.HvcC{ // <hvcC/>
|
||||||
|
ConfigurationVersion: 1,
|
||||||
|
GeneralProfileIdc: h265SPSP.ProfileTierLevel.GeneralProfileIdc,
|
||||||
|
GeneralProfileCompatibility: h265SPSP.ProfileTierLevel.GeneralProfileCompatibilityFlag,
|
||||||
|
GeneralConstraintIndicator: [6]uint8{
|
||||||
|
h265SPS[7], h265SPS[8], h265SPS[9],
|
||||||
|
h265SPS[10], h265SPS[11], h265SPS[12],
|
||||||
|
},
|
||||||
|
GeneralLevelIdc: h265SPSP.ProfileTierLevel.GeneralLevelIdc,
|
||||||
|
// MinSpatialSegmentationIdc
|
||||||
|
// ParallelismType
|
||||||
|
ChromaFormatIdc: uint8(h265SPSP.ChromaFormatIdc),
|
||||||
|
BitDepthLumaMinus8: uint8(h265SPSP.BitDepthLumaMinus8),
|
||||||
|
BitDepthChromaMinus8: uint8(h265SPSP.BitDepthChromaMinus8),
|
||||||
|
// AvgFrameRate
|
||||||
|
// ConstantFrameRate
|
||||||
|
NumTemporalLayers: 1,
|
||||||
|
// TemporalIdNested
|
||||||
|
LengthSizeMinusOne: 3,
|
||||||
|
NumOfNaluArrays: 3,
|
||||||
|
NaluArrays: []gomp4.HEVCNaluArray{
|
||||||
|
{
|
||||||
|
NaluType: byte(h265.NALUType_VPS_NUT),
|
||||||
|
NumNalus: 1,
|
||||||
|
Nalus: []gomp4.HEVCNalu{{
|
||||||
|
Length: uint16(len(h265VPS)),
|
||||||
|
NALUnit: h265VPS,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NaluType: byte(h265.NALUType_SPS_NUT),
|
||||||
|
NumNalus: 1,
|
||||||
|
Nalus: []gomp4.HEVCNalu{{
|
||||||
|
Length: uint16(len(h265SPS)),
|
||||||
|
NALUnit: h265SPS,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NaluType: byte(h265.NALUType_PPS_NUT),
|
||||||
|
NumNalus: 1,
|
||||||
|
Nalus: []gomp4.HEVCNalu{{
|
||||||
|
Length: uint16(len(h265PPS)),
|
||||||
|
NALUnit: h265PPS,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
|
||||||
|
MaxBitrate: 1000000,
|
||||||
|
AvgBitrate: 1000000,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.writeBoxEnd() // </hev1>
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
case *format.MPEG4Audio:
|
case *format.MPEG4Audio:
|
||||||
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
|
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
|
||||||
SampleEntry: gomp4.SampleEntry{
|
SampleEntry: gomp4.SampleEntry{
|
||||||
|
@@ -86,7 +86,7 @@ func (w *Writer) GenerateSegment() []byte {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteH264 writes a group of H264 NALUs.
|
// WriteH264 writes a H264 access unit.
|
||||||
func (w *Writer) WriteH264(
|
func (w *Writer) WriteH264(
|
||||||
pcr time.Duration,
|
pcr time.Duration,
|
||||||
dts time.Duration,
|
dts time.Duration,
|
||||||
|
@@ -28,20 +28,24 @@ func NewMuxer(
|
|||||||
segmentDuration time.Duration,
|
segmentDuration time.Duration,
|
||||||
partDuration time.Duration,
|
partDuration time.Duration,
|
||||||
segmentMaxSize uint64,
|
segmentMaxSize uint64,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
) (*Muxer, error) {
|
) (*Muxer, error) {
|
||||||
m := &Muxer{}
|
m := &Muxer{}
|
||||||
|
|
||||||
switch variant {
|
switch variant {
|
||||||
case MuxerVariantMPEGTS:
|
case MuxerVariantMPEGTS:
|
||||||
m.variant = newMuxerVariantMPEGTS(
|
var err error
|
||||||
|
m.variant, err = newMuxerVariantMPEGTS(
|
||||||
segmentCount,
|
segmentCount,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
segmentMaxSize,
|
segmentMaxSize,
|
||||||
videoTrack,
|
videoTrack,
|
||||||
audioTrack,
|
audioTrack,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
case MuxerVariantFMP4:
|
case MuxerVariantFMP4:
|
||||||
m.variant = newMuxerVariantFMP4(
|
m.variant = newMuxerVariantFMP4(
|
||||||
@@ -76,12 +80,12 @@ func (m *Muxer) Close() {
|
|||||||
m.variant.close()
|
m.variant.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteH264 writes H264 NALUs, grouped by timestamp.
|
// WriteH26x writes an H264 or an H265 access unit.
|
||||||
func (m *Muxer) WriteH264(ntp time.Time, pts time.Duration, nalus [][]byte) error {
|
func (m *Muxer) WriteH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
|
||||||
return m.variant.writeH264(ntp, pts, nalus)
|
return m.variant.writeH26x(ntp, pts, au)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteAAC writes AAC AUs, grouped by timestamp.
|
// WriteAAC writes an AAC access unit.
|
||||||
func (m *Muxer) WriteAAC(ntp time.Time, pts time.Duration, au []byte) error {
|
func (m *Muxer) WriteAAC(ntp time.Time, pts time.Duration, au []byte) error {
|
||||||
return m.variant.writeAAC(ntp, pts, au)
|
return m.variant.writeAAC(ntp, pts, au)
|
||||||
}
|
}
|
||||||
|
@@ -8,18 +8,43 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func codecParameters(track format.Format) string {
|
||||||
|
switch ttrack := track.(type) {
|
||||||
|
case *format.H264:
|
||||||
|
sps := ttrack.SafeSPS()
|
||||||
|
if len(sps) >= 4 {
|
||||||
|
return "avc1." + hex.EncodeToString(sps[1:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
case *format.H265:
|
||||||
|
var sps h265.SPS
|
||||||
|
err := sps.Unmarshal(ttrack.SafeSPS())
|
||||||
|
if err == nil {
|
||||||
|
return "hvc1." + strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralProfileIdc), 10) +
|
||||||
|
".4.L" + strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralLevelIdc), 10) + ".B0"
|
||||||
|
}
|
||||||
|
|
||||||
|
case *format.MPEG4Audio:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
|
||||||
|
return "mp4a.40." + strconv.FormatInt(int64(ttrack.Config.Type), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type muxerPrimaryPlaylist struct {
|
type muxerPrimaryPlaylist struct {
|
||||||
fmp4 bool
|
fmp4 bool
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMuxerPrimaryPlaylist(
|
func newMuxerPrimaryPlaylist(
|
||||||
fmp4 bool,
|
fmp4 bool,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
) *muxerPrimaryPlaylist {
|
) *muxerPrimaryPlaylist {
|
||||||
return &muxerPrimaryPlaylist{
|
return &muxerPrimaryPlaylist{
|
||||||
@@ -39,15 +64,10 @@ func (p *muxerPrimaryPlaylist) file() *MuxerFileResponse {
|
|||||||
var codecs []string
|
var codecs []string
|
||||||
|
|
||||||
if p.videoTrack != nil {
|
if p.videoTrack != nil {
|
||||||
sps := p.videoTrack.SafeSPS()
|
codecs = append(codecs, codecParameters(p.videoTrack))
|
||||||
if len(sps) >= 4 {
|
|
||||||
codecs = append(codecs, "avc1."+hex.EncodeToString(sps[1:4]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter
|
|
||||||
if p.audioTrack != nil {
|
if p.audioTrack != nil {
|
||||||
codecs = append(codecs, "mp4a.40."+strconv.FormatInt(int64(p.audioTrack.Config.Type), 10))
|
codecs = append(codecs, codecParameters(p.audioTrack))
|
||||||
}
|
}
|
||||||
|
|
||||||
var version int
|
var version int
|
||||||
|
@@ -57,17 +57,17 @@ func TestMuxerVideoAudio(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
// group without IDR
|
// access unit without IDR
|
||||||
d := 1 * time.Second
|
d := 1 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-1*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
|
||||||
{0x06},
|
{0x06},
|
||||||
{0x07},
|
{0x07},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d = 2 * time.Second
|
d = 2 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-1*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
|
||||||
testSPS, // SPS
|
testSPS, // SPS
|
||||||
{8}, // PPS
|
{8}, // PPS
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
@@ -86,9 +86,9 @@ func TestMuxerVideoAudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group without IDR
|
// access unit without IDR
|
||||||
d = 4 * time.Second
|
d = 4 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-1*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
|
||||||
{1}, // non-IDR
|
{1}, // non-IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -99,16 +99,16 @@ func TestMuxerVideoAudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d = 6 * time.Second
|
d = 6 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-1*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d = 7 * time.Second
|
d = 7 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-1*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -203,25 +203,25 @@ func TestMuxerVideoOnly(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d := 2 * time.Second
|
d := 2 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-2*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
|
||||||
testSPS, // SPS
|
testSPS, // SPS
|
||||||
{8}, // PPS
|
{8}, // PPS
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d = 6 * time.Second
|
d = 6 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-2*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
d = 7 * time.Second
|
d = 7 * time.Second
|
||||||
err = m.WriteH264(testTime.Add(d-2*time.Second), d, [][]byte{
|
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -415,8 +415,8 @@ func TestMuxerCloseBeforeFirstSegmentReader(t *testing.T) {
|
|||||||
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
|
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// group with IDR
|
// access unit with IDR
|
||||||
err = m.WriteH264(testTime, 2*time.Second, [][]byte{
|
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
|
||||||
testSPS, // SPS
|
testSPS, // SPS
|
||||||
{8}, // PPS
|
{8}, // PPS
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
@@ -441,7 +441,7 @@ func TestMuxerMaxSegmentSize(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
err = m.WriteH264(testTime, 2*time.Second, [][]byte{
|
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
|
||||||
testSPS,
|
testSPS,
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
})
|
})
|
||||||
@@ -460,14 +460,14 @@ func TestMuxerDoubleRead(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
err = m.WriteH264(testTime, 0, [][]byte{
|
err = m.WriteH26x(testTime, 0, [][]byte{
|
||||||
testSPS,
|
testSPS,
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
{1},
|
{1},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = m.WriteH264(testTime, 2*time.Second, [][]byte{
|
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
|
||||||
{5}, // IDR
|
{5}, // IDR
|
||||||
{2},
|
{2},
|
||||||
})
|
})
|
||||||
|
@@ -16,7 +16,7 @@ const (
|
|||||||
|
|
||||||
type muxerVariant interface {
|
type muxerVariant interface {
|
||||||
close()
|
close()
|
||||||
writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error
|
writeH26x(time.Time, time.Duration, [][]byte) error
|
||||||
writeAAC(ntp time.Time, pts time.Duration, au []byte) error
|
writeAAC(time.Time, time.Duration, []byte) error
|
||||||
file(name string, msn string, part string, skip string) *MuxerFileResponse
|
file(name string, msn string, part string, skip string) *MuxerFileResponse
|
||||||
}
|
}
|
||||||
|
@@ -11,16 +11,48 @@ import (
|
|||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func extractVideoParams(track format.Format) [][]byte {
|
||||||
|
switch ttrack := track.(type) {
|
||||||
|
case *format.H264:
|
||||||
|
params := make([][]byte, 2)
|
||||||
|
params[0] = ttrack.SafeSPS()
|
||||||
|
params[1] = ttrack.SafePPS()
|
||||||
|
return params
|
||||||
|
|
||||||
|
case *format.H265:
|
||||||
|
params := make([][]byte, 3)
|
||||||
|
params[0] = ttrack.SafeVPS()
|
||||||
|
params[1] = ttrack.SafeSPS()
|
||||||
|
params[2] = ttrack.SafePPS()
|
||||||
|
return params
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoParamsEqual(p1 [][]byte, p2 [][]byte) bool {
|
||||||
|
if len(p1) != len(p2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range p1 {
|
||||||
|
if !bytes.Equal(p2[i], p) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type muxerVariantFMP4 struct {
|
type muxerVariantFMP4 struct {
|
||||||
playlist *muxerVariantFMP4Playlist
|
playlist *muxerVariantFMP4Playlist
|
||||||
segmenter *muxerVariantFMP4Segmenter
|
segmenter *muxerVariantFMP4Segmenter
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
videoLastSPS []byte
|
lastVideoParams [][]byte
|
||||||
videoLastPPS []byte
|
initContent []byte
|
||||||
initContent []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMuxerVariantFMP4(
|
func newMuxerVariantFMP4(
|
||||||
@@ -29,7 +61,7 @@ func newMuxerVariantFMP4(
|
|||||||
segmentDuration time.Duration,
|
segmentDuration time.Duration,
|
||||||
partDuration time.Duration,
|
partDuration time.Duration,
|
||||||
segmentMaxSize uint64,
|
segmentMaxSize uint64,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
) *muxerVariantFMP4 {
|
) *muxerVariantFMP4 {
|
||||||
v := &muxerVariantFMP4{
|
v := &muxerVariantFMP4{
|
||||||
@@ -63,28 +95,34 @@ func (v *muxerVariantFMP4) close() {
|
|||||||
v.playlist.close()
|
v.playlist.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error {
|
func (v *muxerVariantFMP4) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
|
||||||
return v.segmenter.writeH264(ntp, pts, nalus)
|
return v.segmenter.writeH26x(ntp, pts, au)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) writeAAC(ntp time.Time, pts time.Duration, au []byte) error {
|
func (v *muxerVariantFMP4) writeAAC(ntp time.Time, pts time.Duration, au []byte) error {
|
||||||
return v.segmenter.writeAAC(ntp, pts, au)
|
return v.segmenter.writeAAC(ntp, pts, au)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *muxerVariantFMP4) mustRegenerateInit() bool {
|
||||||
|
if v.videoTrack == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
videoParams := extractVideoParams(v.videoTrack)
|
||||||
|
if !videoParamsEqual(videoParams, v.lastVideoParams) {
|
||||||
|
v.lastVideoParams = videoParams
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (v *muxerVariantFMP4) file(name string, msn string, part string, skip string) *MuxerFileResponse {
|
func (v *muxerVariantFMP4) file(name string, msn string, part string, skip string) *MuxerFileResponse {
|
||||||
if name == "init.mp4" {
|
if name == "init.mp4" {
|
||||||
v.mutex.Lock()
|
v.mutex.Lock()
|
||||||
defer v.mutex.Unlock()
|
defer v.mutex.Unlock()
|
||||||
|
|
||||||
var sps []byte
|
if v.initContent == nil || v.mustRegenerateInit() {
|
||||||
var pps []byte
|
|
||||||
if v.videoTrack != nil {
|
|
||||||
sps = v.videoTrack.SafeSPS()
|
|
||||||
pps = v.videoTrack.SafePPS()
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.initContent == nil ||
|
|
||||||
(v.videoTrack != nil && (!bytes.Equal(v.videoLastSPS, sps) || !bytes.Equal(v.videoLastPPS, pps))) {
|
|
||||||
init := fmp4.Init{}
|
init := fmp4.Init{}
|
||||||
trackID := 1
|
trackID := 1
|
||||||
|
|
||||||
@@ -105,14 +143,11 @@ func (v *muxerVariantFMP4) file(name string, msn string, part string, skip strin
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
initContent, err := init.Marshal()
|
var err error
|
||||||
|
v.initContent, err = init.Marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &MuxerFileResponse{Status: http.StatusInternalServerError}
|
return &MuxerFileResponse{Status: http.StatusInternalServerError}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.videoLastSPS = sps
|
|
||||||
v.videoLastPPS = pps
|
|
||||||
v.initContent = initContent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &MuxerFileResponse{
|
return &MuxerFileResponse{
|
||||||
|
@@ -17,7 +17,7 @@ func fmp4PartName(id uint64) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type muxerVariantFMP4Part struct {
|
type muxerVariantFMP4Part struct {
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
id uint64
|
id uint64
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ type muxerVariantFMP4Part struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newMuxerVariantFMP4Part(
|
func newMuxerVariantFMP4Part(
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
id uint64,
|
id uint64,
|
||||||
) *muxerVariantFMP4Part {
|
) *muxerVariantFMP4Part {
|
||||||
|
@@ -70,7 +70,7 @@ func partTargetDuration(
|
|||||||
type muxerVariantFMP4Playlist struct {
|
type muxerVariantFMP4Playlist struct {
|
||||||
lowLatency bool
|
lowLatency bool
|
||||||
segmentCount int
|
segmentCount int
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
@@ -89,7 +89,7 @@ type muxerVariantFMP4Playlist struct {
|
|||||||
func newMuxerVariantFMP4Playlist(
|
func newMuxerVariantFMP4Playlist(
|
||||||
lowLatency bool,
|
lowLatency bool,
|
||||||
segmentCount int,
|
segmentCount int,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
) *muxerVariantFMP4Playlist {
|
) *muxerVariantFMP4Playlist {
|
||||||
p := &muxerVariantFMP4Playlist{
|
p := &muxerVariantFMP4Playlist{
|
||||||
|
@@ -45,7 +45,7 @@ type muxerVariantFMP4Segment struct {
|
|||||||
startTime time.Time
|
startTime time.Time
|
||||||
startDTS time.Duration
|
startDTS time.Duration
|
||||||
segmentMaxSize uint64
|
segmentMaxSize uint64
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
genPartID func() uint64
|
genPartID func() uint64
|
||||||
onPartFinalized func(*muxerVariantFMP4Part)
|
onPartFinalized func(*muxerVariantFMP4Part)
|
||||||
@@ -63,7 +63,7 @@ func newMuxerVariantFMP4Segment(
|
|||||||
startTime time.Time,
|
startTime time.Time,
|
||||||
startDTS time.Duration,
|
startDTS time.Duration,
|
||||||
segmentMaxSize uint64,
|
segmentMaxSize uint64,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
genPartID func() uint64,
|
genPartID func() uint64,
|
||||||
onPartFinalized func(*muxerVariantFMP4Part),
|
onPartFinalized func(*muxerVariantFMP4Part),
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
package hls
|
package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
||||||
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
|
|
||||||
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
|
||||||
@@ -45,6 +46,21 @@ func findCompatiblePartDuration(
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dtsExtractor interface {
|
||||||
|
Extract([][]byte, time.Duration) (time.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func allocateDTSExtractor(track format.Format) dtsExtractor {
|
||||||
|
switch track.(type) {
|
||||||
|
case *format.H264:
|
||||||
|
return h264.NewDTSExtractor()
|
||||||
|
|
||||||
|
case *format.H265:
|
||||||
|
return h265.NewDTSExtractor()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type augmentedVideoSample struct {
|
type augmentedVideoSample struct {
|
||||||
fmp4.PartSample
|
fmp4.PartSample
|
||||||
dts time.Duration
|
dts time.Duration
|
||||||
@@ -62,23 +78,23 @@ type muxerVariantFMP4Segmenter struct {
|
|||||||
segmentDuration time.Duration
|
segmentDuration time.Duration
|
||||||
partDuration time.Duration
|
partDuration time.Duration
|
||||||
segmentMaxSize uint64
|
segmentMaxSize uint64
|
||||||
videoTrack *format.H264
|
videoTrack format.Format
|
||||||
audioTrack *format.MPEG4Audio
|
audioTrack *format.MPEG4Audio
|
||||||
onSegmentFinalized func(*muxerVariantFMP4Segment)
|
onSegmentFinalized func(*muxerVariantFMP4Segment)
|
||||||
onPartFinalized func(*muxerVariantFMP4Part)
|
onPartFinalized func(*muxerVariantFMP4Part)
|
||||||
|
|
||||||
startDTS time.Duration
|
startDTS time.Duration
|
||||||
videoFirstIDRReceived bool
|
videoFirstRandomAccessReceived bool
|
||||||
videoDTSExtractor *h264.DTSExtractor
|
videoDTSExtractor dtsExtractor
|
||||||
videoSPS []byte
|
lastVideoParams [][]byte
|
||||||
currentSegment *muxerVariantFMP4Segment
|
currentSegment *muxerVariantFMP4Segment
|
||||||
nextSegmentID uint64
|
nextSegmentID uint64
|
||||||
nextPartID uint64
|
nextPartID uint64
|
||||||
nextVideoSample *augmentedVideoSample
|
nextVideoSample *augmentedVideoSample
|
||||||
nextAudioSample *augmentedAudioSample
|
nextAudioSample *augmentedAudioSample
|
||||||
firstSegmentFinalized bool
|
firstSegmentFinalized bool
|
||||||
sampleDurations map[time.Duration]struct{}
|
sampleDurations map[time.Duration]struct{}
|
||||||
adjustedPartDuration time.Duration
|
adjustedPartDuration time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMuxerVariantFMP4Segmenter(
|
func newMuxerVariantFMP4Segmenter(
|
||||||
@@ -87,7 +103,7 @@ func newMuxerVariantFMP4Segmenter(
|
|||||||
segmentDuration time.Duration,
|
segmentDuration time.Duration,
|
||||||
partDuration time.Duration,
|
partDuration time.Duration,
|
||||||
segmentMaxSize uint64,
|
segmentMaxSize uint64,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
onSegmentFinalized func(*muxerVariantFMP4Segment),
|
onSegmentFinalized func(*muxerVariantFMP4Segment),
|
||||||
onPartFinalized func(*muxerVariantFMP4Part),
|
onPartFinalized func(*muxerVariantFMP4Part),
|
||||||
@@ -140,50 +156,65 @@ func (m *muxerVariantFMP4Segmenter) adjustPartDuration(du time.Duration) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error {
|
func (m *muxerVariantFMP4Segmenter) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
|
||||||
idrPresent := false
|
randomAccessPresent := false
|
||||||
nonIDRPresent := false
|
|
||||||
|
|
||||||
for _, nalu := range nalus {
|
switch m.videoTrack.(type) {
|
||||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
case *format.H264:
|
||||||
switch typ {
|
nonIDRPresent := false
|
||||||
case h264.NALUTypeIDR:
|
|
||||||
idrPresent = true
|
|
||||||
|
|
||||||
case h264.NALUTypeNonIDR:
|
for _, nalu := range au {
|
||||||
nonIDRPresent = true
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case h264.NALUTypeIDR:
|
||||||
|
randomAccessPresent = true
|
||||||
|
|
||||||
|
case h264.NALUTypeNonIDR:
|
||||||
|
nonIDRPresent = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !idrPresent && !nonIDRPresent {
|
if !randomAccessPresent && !nonIDRPresent {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.writeH264Entry(ntp, pts, nalus, idrPresent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|
||||||
ntp time.Time,
|
|
||||||
pts time.Duration,
|
|
||||||
nalus [][]byte,
|
|
||||||
idrPresent bool,
|
|
||||||
) error {
|
|
||||||
var dts time.Duration
|
|
||||||
|
|
||||||
if !m.videoFirstIDRReceived {
|
|
||||||
// skip sample silently until we find one with an IDR
|
|
||||||
if !idrPresent {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.videoFirstIDRReceived = true
|
case *format.H265:
|
||||||
m.videoDTSExtractor = h264.NewDTSExtractor()
|
for _, nalu := range au {
|
||||||
m.videoSPS = m.videoTrack.SafeSPS()
|
typ := h265.NALUType((nalu[0] >> 1) & 0b111111)
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:
|
||||||
|
randomAccessPresent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.writeH26xEntry(ntp, pts, au, randomAccessPresent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *muxerVariantFMP4Segmenter) writeH26xEntry(
|
||||||
|
ntp time.Time,
|
||||||
|
pts time.Duration,
|
||||||
|
au [][]byte,
|
||||||
|
randomAccessPresent bool,
|
||||||
|
) error {
|
||||||
|
var dts time.Duration
|
||||||
|
|
||||||
|
if !m.videoFirstRandomAccessReceived {
|
||||||
|
// skip sample silently until we find one with an IDR
|
||||||
|
if !randomAccessPresent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.videoFirstRandomAccessReceived = true
|
||||||
|
m.videoDTSExtractor = allocateDTSExtractor(m.videoTrack)
|
||||||
|
m.lastVideoParams = extractVideoParams(m.videoTrack)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
dts, err = m.videoDTSExtractor.Extract(au, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to extract DTS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.startDTS = dts
|
m.startDTS = dts
|
||||||
@@ -191,16 +222,16 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|||||||
pts -= m.startDTS
|
pts -= m.startDTS
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
dts, err = m.videoDTSExtractor.Extract(au, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to extract DTS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dts -= m.startDTS
|
dts -= m.startDTS
|
||||||
pts -= m.startDTS
|
pts -= m.startDTS
|
||||||
}
|
}
|
||||||
|
|
||||||
avcc, err := h264.AVCCMarshal(nalus)
|
avcc, err := h264.AVCCMarshal(au)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -208,7 +239,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|||||||
sample := &augmentedVideoSample{
|
sample := &augmentedVideoSample{
|
||||||
PartSample: fmp4.PartSample{
|
PartSample: fmp4.PartSample{
|
||||||
PTSOffset: int32(durationGoToMp4(pts-dts, 90000)),
|
PTSOffset: int32(durationGoToMp4(pts-dts, 90000)),
|
||||||
IsNonSyncSample: !idrPresent,
|
IsNonSyncSample: !randomAccessPresent,
|
||||||
Payload: avcc,
|
Payload: avcc,
|
||||||
},
|
},
|
||||||
dts: dts,
|
dts: dts,
|
||||||
@@ -247,12 +278,12 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// switch segment
|
// switch segment
|
||||||
if idrPresent {
|
if randomAccessPresent {
|
||||||
sps := m.videoTrack.SafeSPS()
|
videoParams := extractVideoParams(m.videoTrack)
|
||||||
spsChanged := !bytes.Equal(m.videoSPS, sps)
|
paramsChanged := !videoParamsEqual(m.lastVideoParams, videoParams)
|
||||||
|
|
||||||
if (m.nextVideoSample.dts-m.currentSegment.startDTS) >= m.segmentDuration ||
|
if (m.nextVideoSample.dts-m.currentSegment.startDTS) >= m.segmentDuration ||
|
||||||
spsChanged {
|
paramsChanged {
|
||||||
err := m.currentSegment.finalize(m.nextVideoSample.dts)
|
err := m.currentSegment.finalize(m.nextVideoSample.dts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -273,10 +304,11 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|||||||
m.onPartFinalized,
|
m.onPartFinalized,
|
||||||
)
|
)
|
||||||
|
|
||||||
// if SPS changed, reset adjusted part duration
|
if paramsChanged {
|
||||||
if spsChanged {
|
m.lastVideoParams = videoParams
|
||||||
m.videoSPS = sps
|
|
||||||
m.firstSegmentFinalized = false
|
m.firstSegmentFinalized = false
|
||||||
|
|
||||||
|
// reset adjusted part duration
|
||||||
m.sampleDurations = make(map[time.Duration]struct{})
|
m.sampleDurations = make(map[time.Duration]struct{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +320,7 @@ func (m *muxerVariantFMP4Segmenter) writeH264Entry(
|
|||||||
func (m *muxerVariantFMP4Segmenter) writeAAC(ntp time.Time, dts time.Duration, au []byte) error {
|
func (m *muxerVariantFMP4Segmenter) writeAAC(ntp time.Time, dts time.Duration, au []byte) error {
|
||||||
if m.videoTrack != nil {
|
if m.videoTrack != nil {
|
||||||
// wait for the video track
|
// wait for the video track
|
||||||
if !m.videoFirstIDRReceived {
|
if !m.videoFirstRandomAccessReceived {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package hls
|
package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/format"
|
"github.com/aler9/gortsplib/v2/pkg/format"
|
||||||
@@ -15,9 +16,19 @@ func newMuxerVariantMPEGTS(
|
|||||||
segmentCount int,
|
segmentCount int,
|
||||||
segmentDuration time.Duration,
|
segmentDuration time.Duration,
|
||||||
segmentMaxSize uint64,
|
segmentMaxSize uint64,
|
||||||
videoTrack *format.H264,
|
videoTrack format.Format,
|
||||||
audioTrack *format.MPEG4Audio,
|
audioTrack *format.MPEG4Audio,
|
||||||
) *muxerVariantMPEGTS {
|
) (*muxerVariantMPEGTS, error) {
|
||||||
|
var videoTrackH264 *format.H264
|
||||||
|
if videoTrack != nil {
|
||||||
|
var ok bool
|
||||||
|
videoTrackH264, ok = videoTrack.(*format.H264)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"the MPEG-TS variant of HLS doesn't support H265. Use the fMP4 or Low-Latency variants instead")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
v := &muxerVariantMPEGTS{}
|
v := &muxerVariantMPEGTS{}
|
||||||
|
|
||||||
v.playlist = newMuxerVariantMPEGTSPlaylist(segmentCount)
|
v.playlist = newMuxerVariantMPEGTSPlaylist(segmentCount)
|
||||||
@@ -25,21 +36,21 @@ func newMuxerVariantMPEGTS(
|
|||||||
v.segmenter = newMuxerVariantMPEGTSSegmenter(
|
v.segmenter = newMuxerVariantMPEGTSSegmenter(
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
segmentMaxSize,
|
segmentMaxSize,
|
||||||
videoTrack,
|
videoTrackH264,
|
||||||
audioTrack,
|
audioTrack,
|
||||||
func(seg *muxerVariantMPEGTSSegment) {
|
func(seg *muxerVariantMPEGTSSegment) {
|
||||||
v.playlist.pushSegment(seg)
|
v.playlist.pushSegment(seg)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return v
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) close() {
|
func (v *muxerVariantMPEGTS) close() {
|
||||||
v.playlist.close()
|
v.playlist.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *muxerVariantMPEGTS) writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error {
|
func (v *muxerVariantMPEGTS) writeH26x(ntp time.Time, pts time.Duration, nalus [][]byte) error {
|
||||||
return v.segmenter.writeH264(ntp, pts, nalus)
|
return v.segmenter.writeH264(ntp, pts, nalus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package hls
|
package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
|
||||||
@@ -84,7 +85,7 @@ func (m *muxerVariantMPEGTSSegmenter) writeH264(ntp time.Time, pts time.Duration
|
|||||||
var err error
|
var err error
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to extract DTS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.startPCR = ntp
|
m.startPCR = ntp
|
||||||
@@ -108,7 +109,7 @@ func (m *muxerVariantMPEGTSSegmenter) writeH264(ntp time.Time, pts time.Duration
|
|||||||
var err error
|
var err error
|
||||||
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unable to extract DTS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dts -= m.startDTS
|
dts -= m.startDTS
|
||||||
|
Reference in New Issue
Block a user