diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 90586bb..fc9c046 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,16 +10,19 @@ jobs: golangci-lint: runs-on: ubuntu-22.04 + env: + CGO_ENABLED: '0' + steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v3 with: - go-version: "1.22" + go-version: "1.23" - uses: golangci/golangci-lint-action@v3 with: - version: v1.59.1 + version: v1.61.0 go-mod-tidy: runs-on: ubuntu-22.04 @@ -29,7 +32,7 @@ jobs: - uses: actions/setup-go@v3 with: - go-version: "1.22" + go-version: "1.23" - run: | go mod tidy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0507767..6e153c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - go: ["1.20", "1.21", "1.22"] + go: ["1.21", "1.22", "1.23"] steps: - uses: actions/checkout@v4 @@ -24,7 +24,7 @@ jobs: - run: make test-nodocker - - if: matrix.go == '1.22' + - if: matrix.go == '1.23' uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Makefile b/Makefile index 8a04aea..7d1bd1d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -BASE_IMAGE = golang:1.22-alpine3.18 -LINT_IMAGE = golangci/golangci-lint:v1.59.1 +BASE_IMAGE = golang:1.23-alpine3.20 +LINT_IMAGE = golangci/golangci-lint:v1.61.0 .PHONY: $(shell ls) diff --git a/README.md b/README.md index 947d95e..e8d7934 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ HLS client and muxer library for the Go programming language, written for [MediaMTX](https://github.com/bluenviron/mediamtx). -Go ≥ 1.20 is required. +Go ≥ 1.21 is required. Features: diff --git a/examples/client-codec-h264-convert-to-jpeg/main.go b/examples/client-codec-h264-convert-to-jpeg/main.go index 745075f..4f3a8a1 100644 --- a/examples/client-codec-h264-convert-to-jpeg/main.go +++ b/examples/client-codec-h264-convert-to-jpeg/main.go @@ -1,3 +1,6 @@ +//go:build cgo +// +build cgo + package main import ( diff --git a/go.mod b/go.mod index 6ad7157..665c705 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bluenviron/gohlslib/v2 -go 1.20 +go 1.21 require ( github.com/asticode/go-astits v1.13.0 diff --git a/muxer.go b/muxer.go index 319820c..a09384a 100644 --- a/muxer.go +++ b/muxer.go @@ -204,7 +204,7 @@ func (m *Muxer) Start() error { } hasVideo = true } else { - hasAudio = true + hasAudio = true //nolint:ineffassign,wastedassign } } } @@ -245,13 +245,9 @@ func (m *Muxer) Start() error { m.mtracksByTrack[track] = mtrack } - if m.Variant == MuxerVariantMPEGTS { - // nothing - } else { - // add initial gaps, required by iOS LL-HLS - if m.Variant == MuxerVariantLowLatency { - m.nextSegmentID = 7 - } + // add initial gaps, required by iOS LL-HLS + if m.Variant == MuxerVariantLowLatency { + m.nextSegmentID = 7 } switch { diff --git a/muxer_segmenter.go b/muxer_segmenter.go index 1fba6d5..fc54aaf 100644 --- a/muxer_segmenter.go +++ b/muxer_segmenter.go @@ -351,23 +351,21 @@ func (s *muxerSegmenter) writeH264( if s.muxer.Variant == MuxerVariantMPEGTS { if track.stream.nextSegment == nil { - err := s.muxer.createFirstSegment(timestampToDuration(dts, track.ClockRate), ntp) + err = s.muxer.createFirstSegment(timestampToDuration(dts, track.ClockRate), ntp) if err != nil { return err } - } else { - // switch segment - if randomAccess && - ((timestampToDuration(dts, track.ClockRate)-track.stream.nextSegment.(*muxerSegmentMPEGTS).startDTS) >= s.muxer.SegmentMinDuration || - paramsChanged) { - err := s.muxer.rotateSegments(timestampToDuration(dts, track.ClockRate), ntp, false) - if err != nil { - return err - } + } else if randomAccess && // switch segment + ((timestampToDuration(dts, track.ClockRate)- + track.stream.nextSegment.(*muxerSegmentMPEGTS).startDTS) >= s.muxer.SegmentMinDuration || + paramsChanged) { + err = s.muxer.rotateSegments(timestampToDuration(dts, track.ClockRate), ntp, false) + if err != nil { + return err } } - err := track.stream.nextSegment.(*muxerSegmentMPEGTS).writeH264( + err = track.stream.nextSegment.(*muxerSegmentMPEGTS).writeH264( track, pts, dts, @@ -379,25 +377,25 @@ func (s *muxerSegmenter) writeH264( } return nil - } else { - ps, err := fmp4.NewPartSampleH26x( - int32(pts-dts), - randomAccess, - au) - if err != nil { - return err - } + } - return s.fmp4WriteSample( - track, - randomAccess, - paramsChanged, - &fmp4AugmentedSample{ - PartSample: *ps, - dts: dts, - ntp: ntp, - }) + ps, err := fmp4.NewPartSampleH26x( + int32(pts-dts), + randomAccess, + au) + if err != nil { + return err } + + return s.fmp4WriteSample( + track, + randomAccess, + paramsChanged, + &fmp4AugmentedSample{ + PartSample: *ps, + dts: dts, + ntp: ntp, + }) } func (s *muxerSegmenter) writeOpus( @@ -445,7 +443,8 @@ func (s *muxerSegmenter) writeMPEG4Audio( return err } } else if track.stream.nextSegment.(*muxerSegmentMPEGTS).audioAUCount >= mpegtsSegmentMinAUCount && // switch segment - (timestampToDuration(pts, track.ClockRate)-track.stream.nextSegment.(*muxerSegmentMPEGTS).startDTS) >= s.muxer.SegmentMinDuration { + (timestampToDuration(pts, track.ClockRate)- + track.stream.nextSegment.(*muxerSegmentMPEGTS).startDTS) >= s.muxer.SegmentMinDuration { err := s.muxer.rotateSegments(timestampToDuration(pts, track.ClockRate), ntp, false) if err != nil { return err @@ -464,34 +463,34 @@ func (s *muxerSegmenter) writeMPEG4Audio( } return nil - } else { - sampleRate := track.Codec.(*codecs.MPEG4Audio).Config.SampleRate - - for i, au := range aus { - auNTP := ntp.Add(time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * - time.Second / time.Duration(sampleRate)) - auPTS := pts + int64(i)*mpeg4audio.SamplesPerAccessUnit* - int64(track.ClockRate)/int64(sampleRate) - - err := s.fmp4WriteSample( - track, - true, - false, - &fmp4AugmentedSample{ - PartSample: fmp4.PartSample{ - Payload: au, - }, - dts: auPTS, - ntp: auNTP, + } + + sampleRate := track.Codec.(*codecs.MPEG4Audio).Config.SampleRate + + for i, au := range aus { + auNTP := ntp.Add(time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * + time.Second / time.Duration(sampleRate)) + auPTS := pts + int64(i)*mpeg4audio.SamplesPerAccessUnit* + int64(track.ClockRate)/int64(sampleRate) + + err := s.fmp4WriteSample( + track, + true, + false, + &fmp4AugmentedSample{ + PartSample: fmp4.PartSample{ + Payload: au, }, - ) - if err != nil { - return err - } + dts: auPTS, + ntp: auNTP, + }, + ) + if err != nil { + return err } - - return nil } + + return nil } // iPhone iOS fails if part durations are less than 85% of maximum part duration. @@ -567,8 +566,10 @@ func (s *muxerSegmenter) fmp4WriteSample( if track.isLeading { // switch segment if randomAccess && (paramsChanged || - (timestampToDuration(track.fmp4NextSample.dts, track.ClockRate)-track.stream.nextSegment.(*muxerSegmentFMP4).startDTS) >= s.muxer.SegmentMinDuration) { - err = s.muxer.rotateSegments(timestampToDuration(track.fmp4NextSample.dts, track.ClockRate), track.fmp4NextSample.ntp, paramsChanged) + (timestampToDuration(track.fmp4NextSample.dts, track.ClockRate)- + track.stream.nextSegment.(*muxerSegmentFMP4).startDTS) >= s.muxer.SegmentMinDuration) { + err = s.muxer.rotateSegments(timestampToDuration(track.fmp4NextSample.dts, track.ClockRate), + track.fmp4NextSample.ntp, paramsChanged) if err != nil { return err } @@ -583,7 +584,8 @@ func (s *muxerSegmenter) fmp4WriteSample( // switch part } else if (s.muxer.Variant == MuxerVariantLowLatency) && - (timestampToDuration(track.fmp4NextSample.dts, track.ClockRate)-track.stream.nextPart.startDTS) >= s.fmp4AdjustedPartDuration { + (timestampToDuration(track.fmp4NextSample.dts, track.ClockRate)- + track.stream.nextPart.startDTS) >= s.fmp4AdjustedPartDuration { err := s.muxer.rotateParts(timestampToDuration(track.fmp4NextSample.dts, track.ClockRate)) if err != nil { return err diff --git a/muxer_stream.go b/muxer_stream.go index a300bb1..69948a4 100644 --- a/muxer_stream.go +++ b/muxer_stream.go @@ -382,7 +382,7 @@ func (s *muxerStream) handleMediaPlaylist(w http.ResponseWriter, r *http.Request } func (s *muxerStream) generateMediaPlaylistMPEGTS( - isDeltaUpdate bool, + _ bool, rawQuery string, ) ([]byte, error) { pl := &playlist.Media{ @@ -728,8 +728,8 @@ func (s *muxerStream) rotateSegments( s.segments = append(s.segments, segment) s.muxer.server.pathHandlers[segment.getPath()] = func(w http.ResponseWriter, _ *http.Request) { - r, err := segment.reader() - if err != nil { + r, err2 := segment.reader() + if err2 != nil { w.WriteHeader(http.StatusInternalServerError) return } @@ -771,7 +771,7 @@ func (s *muxerStream) rotateSegments( // regenerate init files only if missing or codec parameters have changed if s.muxer.Variant != MuxerVariantMPEGTS && (!s.initFilePresent || segment.isFromForcedRotation()) { - err := s.generateAndCacheInitFile() + err = s.generateAndCacheInitFile() if err != nil { return err } @@ -782,7 +782,8 @@ func (s *muxerStream) rotateSegments( if s.muxer.targetDuration == 0 { s.muxer.targetDuration = targetDuration } else if targetDuration > s.muxer.targetDuration { - s.muxer.OnEncodeError(fmt.Errorf("segment duration changed from %ds to %ds - this will cause an error in iOS clients", + s.muxer.OnEncodeError(fmt.Errorf( + "segment duration changed from %ds to %ds - this will cause an error in iOS clients", s.muxer.targetDuration, targetDuration)) s.muxer.targetDuration = targetDuration } diff --git a/muxer_test.go b/muxer_test.go index ceb2506..60a10f3 100644 --- a/muxer_test.go +++ b/muxer_test.go @@ -393,7 +393,8 @@ func TestMuxer(t *testing.T) { "#EXT-X-VERSION:9\n"+ "#EXT-X-INDEPENDENT-SEGMENTS\n"+ "\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\","+ + "NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ "\n"+ "#EXT-X-STREAM-INF:BANDWIDTH=872,AVERAGE-BANDWIDTH=436,CODECS=\"avc1.42c028,mp4a.40.2\","+ "RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO=\"audio\"\n"+ @@ -404,7 +405,8 @@ func TestMuxer(t *testing.T) { "#EXT-X-VERSION:9\n"+ "#EXT-X-INDEPENDENT-SEGMENTS\n"+ "\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\","+ + "NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ "\n"+ "#EXT-X-STREAM-INF:BANDWIDTH=872,AVERAGE-BANDWIDTH=584,CODECS=\"avc1.42c028,mp4a.40.2\","+ "RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO=\"audio\"\n"+ @@ -466,10 +468,13 @@ func TestMuxer(t *testing.T) { "#EXT-X-VERSION:9\n"+ "#EXT-X-INDEPENDENT-SEGMENTS\n"+ "\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio3\",AUTOSELECT=YES,URI=\"audio3_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\","+ + "NAME=\"audio2\",AUTOSELECT=YES,DEFAULT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\","+ + "NAME=\"audio3\",AUTOSELECT=YES,URI=\"audio3_stream.m3u8?key=value\"\n"+ "\n"+ - "#EXT-X-STREAM-INF:BANDWIDTH=872,AVERAGE-BANDWIDTH=403,CODECS=\"avc1.42c028,mp4a.40.2\",RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO=\"audio\"\n"+ + "#EXT-X-STREAM-INF:BANDWIDTH=872,AVERAGE-BANDWIDTH=403,"+ + "CODECS=\"avc1.42c028,mp4a.40.2\",RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO=\"audio\"\n"+ "video1_stream.m3u8?key=value\n", string(byts)) case stream == "multiaudio" && variant == "fmp4": @@ -478,7 +483,8 @@ func TestMuxer(t *testing.T) { "#EXT-X-INDEPENDENT-SEGMENTS\n"+ "\n"+ "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio1\",AUTOSELECT=YES,DEFAULT=YES\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\",AUTOSELECT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\","+ + "AUTOSELECT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ "\n"+ "#EXT-X-STREAM-INF:BANDWIDTH=5184,AVERAGE-BANDWIDTH=3744,CODECS=\"mp4a.40.2\",AUDIO=\"audio\"\n"+ "audio1_stream.m3u8?key=value\n", string(byts)) @@ -489,7 +495,8 @@ func TestMuxer(t *testing.T) { "#EXT-X-INDEPENDENT-SEGMENTS\n"+ "\n"+ "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio1\",AUTOSELECT=YES,DEFAULT=YES\n"+ - "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\",AUTOSELECT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ + "#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"audio\",NAME=\"audio2\","+ + "AUTOSELECT=YES,URI=\"audio2_stream.m3u8?key=value\"\n"+ "\n"+ "#EXT-X-STREAM-INF:BANDWIDTH=5568,AVERAGE-BANDWIDTH=4000,CODECS=\"mp4a.40.2\",AUDIO=\"audio\"\n"+ "audio1_stream.m3u8?key=value\n", string(byts)) @@ -802,10 +809,14 @@ func TestMuxerMaxSegmentSize(t *testing.T) { require.NoError(t, err) defer m.Close() - err = m.WriteH264(testVideoTrack, testTime, int64(2*time.Second)*int64(testVideoTrack.ClockRate)/int64(time.Second), [][]byte{ - testSPS, - {5}, // IDR - }) + err = m.WriteH264( + testVideoTrack, + testTime, + int64(2*time.Second)*int64(testVideoTrack.ClockRate)/int64(time.Second), + [][]byte{ + testSPS, + {5}, // IDR + }) require.EqualError(t, err, "reached maximum segment size") } @@ -828,10 +839,14 @@ func TestMuxerDoubleRead(t *testing.T) { }) require.NoError(t, err) - err = m.WriteH264(testVideoTrack, testTime, int64(2*time.Second)*int64(testVideoTrack.ClockRate)/int64(time.Second), [][]byte{ - {5}, // IDR - {2}, - }) + err = m.WriteH264( + testVideoTrack, + testTime, + int64(2*time.Second)*int64(testVideoTrack.ClockRate)/int64(time.Second), + [][]byte{ + {5}, // IDR + {2}, + }) require.NoError(t, err) byts, _, err := doRequest(m, "main_stream.m3u8") @@ -1208,7 +1223,8 @@ func TestMuxerFMP4NegativeTimestamp(t *testing.T) { "\n"+ `#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="audio",NAME="audio2",AUTOSELECT=YES,DEFAULT=YES,URI="audio2_stream.m3u8"`+"\n"+ "\n"+ - `#EXT-X-STREAM-INF:BANDWIDTH=644,AVERAGE-BANDWIDTH=550,CODECS="avc1.42c028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO="audio"`+"\n"+ + `#EXT-X-STREAM-INF:BANDWIDTH=644,AVERAGE-BANDWIDTH=550,`+ + `CODECS="avc1.42c028,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=30.000,AUDIO="audio"`+"\n"+ "video1_stream.m3u8\n", string(bu)) byts, _, err := doRequest(m, "video1_stream.m3u8") @@ -1459,7 +1475,7 @@ func TestMuxerExpiredSegment(t *testing.T) { defer m.Close() for i := 0; i < 2; i++ { - err := m.WriteH264(testVideoTrack, testTime, + err = m.WriteH264(testVideoTrack, testTime, int64(i)*90000, [][]byte{ testSPS, // SPS @@ -1511,14 +1527,14 @@ func TestMuxerPreloadHint(t *testing.T) { defer m.Close() for i := 0; i < 2; i++ { - err := m.WriteH264(testVideoTrack, testTime, + err2 := m.WriteH264(testVideoTrack, testTime, int64(i)*90000, [][]byte{ testSPS, // SPS {8}, // PPS {5}, // IDR }) - require.NoError(t, err) + require.NoError(t, err2) } byts, _, err := doRequest(m, "index.m3u8") @@ -1570,9 +1586,9 @@ func TestMuxerPreloadHint(t *testing.T) { preloadDone := make(chan []byte) go func() { - byts, _, err := doRequest(m, ma[1]) - require.NoError(t, err) - preloadDone <- byts + byts2, _, err2 := doRequest(m, ma[1]) + require.NoError(t, err2) + preloadDone <- byts2 }() select { diff --git a/pkg/storage/disk_offset_writer.go b/pkg/storage/disk_offset_writer.go deleted file mode 100644 index 4eede74..0000000 --- a/pkg/storage/disk_offset_writer.go +++ /dev/null @@ -1,52 +0,0 @@ -package storage - -import ( - "errors" - "io" -) - -// copy of https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/io/io.go;l=558 -// will be removed when go 1.20 is the minimum supported version. -type offsetWriter struct { - w io.WriterAt - base int64 // the original offset - off int64 // the current offset -} - -// NewoffsetWriter returns an offsetWriter that writes to w -// starting at offset off. -func newOffsetWriter(w io.WriterAt, off int64) *offsetWriter { - return &offsetWriter{w, off, off} -} - -func (o *offsetWriter) Write(p []byte) (n int, err error) { - n, err = o.w.WriteAt(p, o.off) - o.off += int64(n) - return -} - -func (o *offsetWriter) WriteAt(p []byte, off int64) (n int, err error) { - off += o.base - return o.w.WriteAt(p, off) -} - -var ( - errWhence = errors.New("Seek: invalid whence") - errOffset = errors.New("Seek: invalid offset") -) - -func (o *offsetWriter) Seek(offset int64, whence int) (int64, error) { - switch whence { - default: - return 0, errWhence - case io.SeekStart: - offset += o.base - case io.SeekCurrent: - offset += o.off - } - if offset < o.base { - return 0, errOffset - } - o.off = offset - return offset - o.base, nil -} diff --git a/pkg/storage/part_disk.go b/pkg/storage/part_disk.go index fb44432..2191207 100644 --- a/pkg/storage/part_disk.go +++ b/pkg/storage/part_disk.go @@ -26,7 +26,7 @@ func newPartDisk(s *fileDisk, offset uint64) *partDisk { func (p *partDisk) Writer() io.WriteSeeker { // write on both disk and RAM return &doubleWriter{ - w1: newOffsetWriter(p.s.f, int64(p.offset)), + w1: io.NewOffsetWriter(p.s.f, int64(p.offset)), w2: p.buffer, } } diff --git a/scripts/lint.mk b/scripts/lint.mk index 9c73673..a15abc4 100644 --- a/scripts/lint.mk +++ b/scripts/lint.mk @@ -1,4 +1,5 @@ lint: docker run --rm -v $(PWD):/app -w /app \ + -e CGO_ENABLED=0 \ $(LINT_IMAGE) \ golangci-lint run -v