Browse Source

move HLS implementation into gohlslib (#1557)

pull/1560/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
8ad376de53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      README.md
  2. 32
      go.mod
  3. 76
      go.sum
  4. 2
      internal/conf/hlsvariant.go
  5. 2
      internal/core/hls_muxer.go
  6. 11
      internal/core/hls_source.go
  7. 127
      internal/hls/client.go
  8. 297
      internal/hls/client_downloader_primary.go
  9. 260
      internal/hls/client_downloader_stream.go
  10. 221
      internal/hls/client_processor_fmp4.go
  11. 72
      internal/hls/client_processor_fmp4_track.go
  12. 221
      internal/hls/client_processor_mpegts.go
  13. 63
      internal/hls/client_processor_mpegts_track.go
  14. 52
      internal/hls/client_routine_pool.go
  15. 79
      internal/hls/client_segment_queue.go
  16. 445
      internal/hls/client_test.go
  17. 59
      internal/hls/client_timesync_fmp4.go
  18. 46
      internal/hls/client_timesync_mpegts.go
  19. 143
      internal/hls/codecparameters.go
  20. 53
      internal/hls/fmp4/boxes_opus.go
  21. 2
      internal/hls/fmp4/fmp4.go
  22. 363
      internal/hls/fmp4/init.go
  23. 1453
      internal/hls/fmp4/init_test.go
  24. 526
      internal/hls/fmp4/init_track.go
  25. 93
      internal/hls/fmp4/mp4_writer.go
  26. 263
      internal/hls/fmp4/part.go
  27. 249
      internal/hls/fmp4/part_test.go
  28. 111
      internal/hls/fmp4/part_track.go
  29. 106
      internal/hls/m3u8/m3u8.go
  30. 106
      internal/hls/mpegts/tracks.go
  31. 201
      internal/hls/mpegts/writer.go
  32. 371
      internal/hls/mpegts/writer_test.go
  33. 46
      internal/hls/mpegtstimedec/decoder.go
  34. 72
      internal/hls/mpegtstimedec/decoder_test.go
  35. 100
      internal/hls/muxer.go
  36. 62
      internal/hls/muxer_primary_playlist.go
  37. 595
      internal/hls/muxer_test.go
  38. 22
      internal/hls/muxer_variant.go
  39. 163
      internal/hls/muxer_variant_fmp4.go
  40. 140
      internal/hls/muxer_variant_fmp4_part.go
  41. 489
      internal/hls/muxer_variant_fmp4_playlist.go
  42. 186
      internal/hls/muxer_variant_fmp4_segment.go
  43. 406
      internal/hls/muxer_variant_fmp4_segmenter.go
  44. 73
      internal/hls/muxer_variant_mpegts.go
  45. 145
      internal/hls/muxer_variant_mpegts_playlist.go
  46. 118
      internal/hls/muxer_variant_mpegts_segment.go
  47. 193
      internal/hls/muxer_variant_mpegts_segmenter.go

1
README.md

@ -1162,6 +1162,7 @@ For more advanced options, you can create and serve a custom web page by startin @@ -1162,6 +1162,7 @@ For more advanced options, you can create and serve a custom web page by startin
Related projects
* gortsplib (RTSP library used internally) https://github.com/aler9/gortsplib
* gohlslib (HLS library used internally) https://github.com/bluenviron/gohlslib
* pion/sdp (SDP library used internally) https://github.com/pion/sdp
* pion/rtp (RTP library used internally) https://github.com/pion/rtp
* pion/rtcp (RTCP library used internally) https://github.com/pion/rtcp

32
go.mod

@ -4,42 +4,46 @@ go 1.18 @@ -4,42 +4,46 @@ go 1.18
require (
code.cloudfoundry.org/bytefmt v0.0.0
github.com/abema/go-mp4 v0.10.0
github.com/alecthomas/kong v0.7.1
github.com/aler9/gortsplib/v2 v2.1.3
github.com/asticode/go-astits v1.11.0
github.com/bluenviron/gohlslib v0.0.0-20230310115623-ec8d496cca25
github.com/fsnotify/fsnotify v1.4.9
github.com/gin-gonic/gin v1.8.1
github.com/gin-gonic/gin v1.9.0
github.com/google/uuid v1.3.0
github.com/gookit/color v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/grafov/m3u8 v0.11.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/notedit/rtmp v0.0.2
github.com/orcaman/writerseeker v0.0.0
github.com/pion/ice/v2 v2.2.11
github.com/pion/interceptor v0.1.11
github.com/pion/rtp v1.7.13
github.com/pion/webrtc/v3 v3.1.47
github.com/stretchr/testify v1.8.1
github.com/stretchr/testify v1.8.2
golang.org/x/crypto v0.5.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/abema/go-mp4 v0.10.1 // indirect
github.com/asticode/go-astikit v0.30.0 // indirect
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect
github.com/grafov/m3u8 v0.11.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/orcaman/writerseeker v0.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.2.4 // indirect
github.com/pion/logging v0.2.2 // indirect
@ -55,12 +59,14 @@ require ( @@ -55,12 +59,14 @@ require (
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

76
go.sum

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
github.com/abema/go-mp4 v0.10.0 h1:76eRo2PlJQUHAKKsQJnROsdIHdt+aPXhJkmPFMjj3+4=
github.com/abema/go-mp4 v0.10.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/abema/go-mp4 v0.10.1 h1:wOhZgNxjduc8r4FJdwPa5x/gdBSSX+8MTnfNj/xkJaE=
github.com/abema/go-mp4 v0.10.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
@ -12,10 +12,17 @@ github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflx @@ -12,10 +12,17 @@ github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflx
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
github.com/bluenviron/gohlslib v0.0.0-20230310115623-ec8d496cca25 h1:F5rtyEy7a8yILctfOrWjuCAFQhL9MG3E+rWrG8gKZV8=
github.com/bluenviron/gohlslib v0.0.0-20230310115623-ec8d496cca25/go.mod h1:lIJHdX5oH3VLUfH3cjA+w3HGIswtgiriB9koNv6ZKCc=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5 h1:xB7KkA98BcUdzVcwyZxb5R0FGIHxNPHgZOzkjPEY5gM=
github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5/go.mod h1:v4VVB6oBMz/c9fRY6vZrwr5xKRWOH5NPDjQZlPk0Gbs=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -24,19 +31,18 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo @@ -24,19 +31,18 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@ -67,19 +73,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr @@ -67,19 +73,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@ -99,8 +104,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y @@ -99,8 +104,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
@ -144,13 +149,10 @@ github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= @@ -144,13 +149,10 @@ github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pion/webrtc/v3 v3.1.47 h1:2dFEKRI1rzFvehXDq43hK9OGGyTGJSusUi3j6QKHC5s=
github.com/pion/webrtc/v3 v3.1.47/go.mod h1:8U39MYZCLVV4sIBn01htASVNkWQN2zDa/rx5xisEXWs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@ -162,20 +164,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ @@ -162,20 +164,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@ -221,14 +226,13 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w @@ -221,14 +226,13 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -264,14 +268,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi @@ -264,14 +268,12 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
@ -284,6 +286,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -284,6 +286,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

2
internal/conf/hlsvariant.go

@ -4,7 +4,7 @@ import ( @@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/aler9/rtsp-simple-server/internal/hls"
"github.com/bluenviron/gohlslib"
)
// HLSVariant is the hlsVariant parameter.

2
internal/core/hls_muxer.go

@ -20,8 +20,8 @@ import ( @@ -20,8 +20,8 @@ import (
"github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/formatprocessor"
"github.com/aler9/rtsp-simple-server/internal/hls"
"github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/bluenviron/gohlslib"
)
const (

11
internal/core/hls_source.go

@ -9,8 +9,9 @@ import ( @@ -9,8 +9,9 @@ import (
"github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/formatprocessor"
"github.com/aler9/rtsp-simple-server/internal/hls"
"github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/bluenviron/gohlslib"
hlslogger "github.com/bluenviron/gohlslib/pkg/logger"
)
type hlsSourceParent interface {
@ -23,6 +24,12 @@ type hlsSource struct { @@ -23,6 +24,12 @@ type hlsSource struct {
parent hlsSourceParent
}
type hlsLoggerWrapper func(level logger.Level, format string, args ...interface{})
func (w hlsLoggerWrapper) Log(level hlslogger.Level, format string, args ...interface{}) {
w(logger.Level(level), format, args)
}
func newHLSSource(
parent hlsSourceParent,
) *hlsSource {
@ -48,7 +55,7 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan @@ -48,7 +55,7 @@ func (s *hlsSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan
c, err := hls.NewClient(
cnf.Source,
cnf.SourceFingerprint,
s,
hlsLoggerWrapper(s.Log),
)
if err != nil {
return err

127
internal/hls/client.go

@ -1,127 +0,0 @@ @@ -1,127 +0,0 @@
package hls
import (
"context"
"fmt"
"net/url"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
const (
clientMPEGTSEntryQueueSize = 100
clientFMP4MaxPartTracksPerSegment = 200
clientLiveStartingInvPosition = 3
clientLiveMaxInvPosition = 5
clientMaxDTSRTCDiff = 10 * time.Second
)
func clientAbsoluteURL(base *url.URL, relative string) (*url.URL, error) {
u, err := url.Parse(relative)
if err != nil {
return nil, err
}
return base.ResolveReference(u), nil
}
// ClientLogger allows to receive log lines.
type ClientLogger interface {
Log(level logger.Level, format string, args ...interface{})
}
// Client is a HLS client.
type Client struct {
fingerprint string
logger ClientLogger
ctx context.Context
ctxCancel func()
onTracks func([]format.Format) error
onData map[format.Format]func(time.Duration, interface{})
playlistURL *url.URL
// out
outErr chan error
}
// NewClient allocates a Client.
func NewClient(
playlistURLStr string,
fingerprint string,
logger ClientLogger,
) (*Client, error) {
playlistURL, err := url.Parse(playlistURLStr)
if err != nil {
return nil, err
}
ctx, ctxCancel := context.WithCancel(context.Background())
c := &Client{
fingerprint: fingerprint,
logger: logger,
ctx: ctx,
ctxCancel: ctxCancel,
playlistURL: playlistURL,
onData: make(map[format.Format]func(time.Duration, interface{})),
outErr: make(chan error, 1),
}
return c, nil
}
// Start starts the client.
func (c *Client) Start() {
go c.run()
}
// Close closes all the Client resources.
func (c *Client) Close() {
c.ctxCancel()
}
// Wait waits for any error of the Client.
func (c *Client) Wait() chan error {
return c.outErr
}
// OnTracks sets a callback that is called when tracks are read.
func (c *Client) OnTracks(cb func([]format.Format) error) {
c.onTracks = cb
}
// OnData sets a callback that is called when data arrives.
func (c *Client) OnData(forma format.Format, cb func(time.Duration, interface{})) {
c.onData[forma] = cb
}
func (c *Client) run() {
c.outErr <- c.runInner()
}
func (c *Client) runInner() error {
rp := newClientRoutinePool()
dl := newClientDownloaderPrimary(
c.playlistURL,
c.fingerprint,
c.logger,
rp,
c.onTracks,
c.onData,
)
rp.add(dl)
select {
case err := <-rp.errorChan():
rp.close()
return err
case <-c.ctx.Done():
rp.close()
return fmt.Errorf("terminated")
}
}

297
internal/hls/client_downloader_primary.go

@ -1,297 +0,0 @@ @@ -1,297 +0,0 @@
package hls
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
gm3u8 "github.com/grafov/m3u8"
"github.com/aler9/rtsp-simple-server/internal/hls/m3u8"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
func clientDownloadPlaylist(ctx context.Context, httpClient *http.Client, ur *url.URL) (m3u8.Playlist, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ur.String(), nil)
if err != nil {
return nil, err
}
res, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
byts, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return m3u8.Unmarshal(byts)
}
func pickLeadingPlaylist(variants []*gm3u8.Variant) *gm3u8.Variant {
var candidates []*gm3u8.Variant //nolint:prealloc
for _, v := range variants {
if v.Codecs != "" && !codecParametersAreSupported(v.Codecs) {
continue
}
candidates = append(candidates, v)
}
if candidates == nil {
return nil
}
// pick the variant with the greatest bandwidth
var leadingPlaylist *gm3u8.Variant
for _, v := range candidates {
if leadingPlaylist == nil ||
v.VariantParams.Bandwidth > leadingPlaylist.VariantParams.Bandwidth {
leadingPlaylist = v
}
}
return leadingPlaylist
}
func pickAudioPlaylist(alternatives []*gm3u8.Alternative, groupID string) *gm3u8.Alternative {
candidates := func() []*gm3u8.Alternative {
var ret []*gm3u8.Alternative
for _, alt := range alternatives {
if alt.GroupId == groupID {
ret = append(ret, alt)
}
}
return ret
}()
if candidates == nil {
return nil
}
// pick the default audio playlist
for _, alt := range candidates {
if alt.Default {
return alt
}
}
// alternatively, pick the first one
return candidates[0]
}
type clientTimeSync interface{}
type clientDownloaderPrimary struct {
primaryPlaylistURL *url.URL
logger ClientLogger
onTracks func([]format.Format) error
onData map[format.Format]func(time.Duration, interface{})
rp *clientRoutinePool
httpClient *http.Client
leadingTimeSync clientTimeSync
// in
streamTracks chan []format.Format
// out
startStreaming chan struct{}
leadingTimeSyncReady chan struct{}
}
func newClientDownloaderPrimary(
primaryPlaylistURL *url.URL,
fingerprint string,
logger ClientLogger,
rp *clientRoutinePool,
onTracks func([]format.Format) error,
onData map[format.Format]func(time.Duration, interface{}),
) *clientDownloaderPrimary {
var tlsConfig *tls.Config
if fingerprint != "" {
tlsConfig = &tls.Config{
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil))
fingerprintLower := strings.ToLower(fingerprint)
if hstr != fingerprintLower {
return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
fingerprintLower, hstr)
}
return nil
},
}
}
return &clientDownloaderPrimary{
primaryPlaylistURL: primaryPlaylistURL,
logger: logger,
onTracks: onTracks,
onData: onData,
rp: rp,
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
},
streamTracks: make(chan []format.Format),
startStreaming: make(chan struct{}),
leadingTimeSyncReady: make(chan struct{}),
}
}
func (d *clientDownloaderPrimary) run(ctx context.Context) error {
d.logger.Log(logger.Debug, "downloading primary playlist %s", d.primaryPlaylistURL)
pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.primaryPlaylistURL)
if err != nil {
return err
}
streamCount := 0
switch plt := pl.(type) {
case *m3u8.MediaPlaylist:
d.logger.Log(logger.Debug, "primary playlist is a stream playlist")
ds := newClientDownloaderStream(
true,
d.httpClient,
d.primaryPlaylistURL,
plt,
d.logger,
d.rp,
d.onStreamTracks,
d.onSetLeadingTimeSync,
d.onGetLeadingTimeSync,
d.onData,
)
d.rp.add(ds)
streamCount++
case *m3u8.MasterPlaylist:
leadingPlaylist := pickLeadingPlaylist(plt.Variants)
if leadingPlaylist == nil {
return fmt.Errorf("no variants with supported codecs found")
}
u, err := clientAbsoluteURL(d.primaryPlaylistURL, leadingPlaylist.URI)
if err != nil {
return err
}
ds := newClientDownloaderStream(
true,
d.httpClient,
u,
nil,
d.logger,
d.rp,
d.onStreamTracks,
d.onSetLeadingTimeSync,
d.onGetLeadingTimeSync,
d.onData,
)
d.rp.add(ds)
streamCount++
if leadingPlaylist.Audio != "" {
audioPlaylist := pickAudioPlaylist(plt.Alternatives, leadingPlaylist.Audio)
if audioPlaylist == nil {
return fmt.Errorf("audio playlist with id \"%s\" not found", leadingPlaylist.Audio)
}
u, err := clientAbsoluteURL(d.primaryPlaylistURL, audioPlaylist.URI)
if err != nil {
return err
}
ds := newClientDownloaderStream(
false,
d.httpClient,
u,
nil,
d.logger,
d.rp,
d.onStreamTracks,
d.onSetLeadingTimeSync,
d.onGetLeadingTimeSync,
d.onData,
)
d.rp.add(ds)
streamCount++
}
default:
return fmt.Errorf("invalid playlist")
}
var tracks []format.Format
for i := 0; i < streamCount; i++ {
select {
case streamTracks := <-d.streamTracks:
tracks = append(tracks, streamTracks...)
case <-ctx.Done():
return fmt.Errorf("terminated")
}
}
if len(tracks) == 0 {
return fmt.Errorf("no supported tracks found")
}
err = d.onTracks(tracks)
if err != nil {
return err
}
close(d.startStreaming)
return nil
}
func (d *clientDownloaderPrimary) onStreamTracks(ctx context.Context, tracks []format.Format) bool {
select {
case d.streamTracks <- tracks:
case <-ctx.Done():
return false
}
select {
case <-d.startStreaming:
case <-ctx.Done():
return false
}
return true
}
func (d *clientDownloaderPrimary) onSetLeadingTimeSync(ts clientTimeSync) {
d.leadingTimeSync = ts
close(d.leadingTimeSyncReady)
}
func (d *clientDownloaderPrimary) onGetLeadingTimeSync(ctx context.Context) (clientTimeSync, bool) {
select {
case <-d.leadingTimeSyncReady:
case <-ctx.Done():
return nil, false
}
return d.leadingTimeSync, true
}

260
internal/hls/client_downloader_stream.go

@ -1,260 +0,0 @@ @@ -1,260 +0,0 @@
package hls
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
gm3u8 "github.com/grafov/m3u8"
"github.com/aler9/rtsp-simple-server/internal/hls/m3u8"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
func segmentsLen(segments []*gm3u8.MediaSegment) int {
for i, seg := range segments {
if seg == nil {
return i
}
}
return 0
}
func findSegmentWithInvPosition(segments []*gm3u8.MediaSegment, pos int) *gm3u8.MediaSegment {
index := len(segments) - pos
if index < 0 {
return nil
}
return segments[index]
}
func findSegmentWithID(seqNo uint64, segments []*gm3u8.MediaSegment, id uint64) (*gm3u8.MediaSegment, int) {
index := int(int64(id) - int64(seqNo))
if index < 0 || index >= len(segments) {
return nil, 0
}
return segments[index], len(segments) - index
}
type clientDownloaderStream struct {
isLeading bool
httpClient *http.Client
playlistURL *url.URL
initialPlaylist *m3u8.MediaPlaylist
logger ClientLogger
rp *clientRoutinePool
onStreamTracks func(context.Context, []format.Format) bool
onSetLeadingTimeSync func(clientTimeSync)
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool)
onData map[format.Format]func(time.Duration, interface{})
curSegmentID *uint64
}
func newClientDownloaderStream(
isLeading bool,
httpClient *http.Client,
playlistURL *url.URL,
initialPlaylist *m3u8.MediaPlaylist,
logger ClientLogger,
rp *clientRoutinePool,
onStreamTracks func(context.Context, []format.Format) bool,
onSetLeadingTimeSync func(clientTimeSync),
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool),
onData map[format.Format]func(time.Duration, interface{}),
) *clientDownloaderStream {
return &clientDownloaderStream{
isLeading: isLeading,
httpClient: httpClient,
playlistURL: playlistURL,
initialPlaylist: initialPlaylist,
logger: logger,
rp: rp,
onStreamTracks: onStreamTracks,
onSetLeadingTimeSync: onSetLeadingTimeSync,
onGetLeadingTimeSync: onGetLeadingTimeSync,
onData: onData,
}
}
func (d *clientDownloaderStream) run(ctx context.Context) error {
initialPlaylist := d.initialPlaylist
d.initialPlaylist = nil
if initialPlaylist == nil {
var err error
initialPlaylist, err = d.downloadPlaylist(ctx)
if err != nil {
return err
}
}
segmentQueue := newClientSegmentQueue()
if initialPlaylist.Map != nil && initialPlaylist.Map.URI != "" {
byts, err := d.downloadSegment(ctx, initialPlaylist.Map.URI, initialPlaylist.Map.Offset, initialPlaylist.Map.Limit)
if err != nil {
return err
}
proc, err := newClientProcessorFMP4(
ctx,
d.isLeading,
byts,
segmentQueue,
d.logger,
d.rp,
d.onStreamTracks,
d.onSetLeadingTimeSync,
d.onGetLeadingTimeSync,
d.onData,
)
if err != nil {
return err
}
d.rp.add(proc)
} else {
proc := newClientProcessorMPEGTS(
d.isLeading,
segmentQueue,
d.logger,
d.rp,
d.onStreamTracks,
d.onSetLeadingTimeSync,
d.onGetLeadingTimeSync,
d.onData,
)
d.rp.add(proc)
}
err := d.fillSegmentQueue(ctx, initialPlaylist, segmentQueue)
if err != nil {
return err
}
for {
ok := segmentQueue.waitUntilSizeIsBelow(ctx, 1)
if !ok {
return fmt.Errorf("terminated")
}
pl, err := d.downloadPlaylist(ctx)
if err != nil {
return err
}
err = d.fillSegmentQueue(ctx, pl, segmentQueue)
if err != nil {
return err
}
}
}
func (d *clientDownloaderStream) downloadPlaylist(ctx context.Context) (*m3u8.MediaPlaylist, error) {
d.logger.Log(logger.Debug, "downloading stream playlist %s", d.playlistURL.String())
pl, err := clientDownloadPlaylist(ctx, d.httpClient, d.playlistURL)
if err != nil {
return nil, err
}
plt, ok := pl.(*m3u8.MediaPlaylist)
if !ok {
return nil, fmt.Errorf("invalid playlist")
}
return plt, nil
}
func (d *clientDownloaderStream) downloadSegment(ctx context.Context,
uri string, offset int64, limit int64,
) ([]byte, error) {
u, err := clientAbsoluteURL(d.playlistURL, uri)
if err != nil {
return nil, err
}
d.logger.Log(logger.Debug, "downloading segment %s", u)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
if limit != 0 {
req.Header.Add("Range", "bytes="+strconv.FormatInt(offset, 10)+"-"+strconv.FormatInt(offset+limit-1, 10))
}
res, err := d.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusPartialContent {
return nil, fmt.Errorf("bad status code: %d", res.StatusCode)
}
byts, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return byts, nil
}
func (d *clientDownloaderStream) fillSegmentQueue(ctx context.Context,
pl *m3u8.MediaPlaylist, segmentQueue *clientSegmentQueue,
) error {
pl.Segments = pl.Segments[:segmentsLen(pl.Segments)]
var seg *gm3u8.MediaSegment
if d.curSegmentID == nil {
if !pl.Closed { // live stream: start from clientLiveStartingInvPosition
seg = findSegmentWithInvPosition(pl.Segments, clientLiveStartingInvPosition)
if seg == nil {
return fmt.Errorf("there aren't enough segments to fill the buffer")
}
} else { // VOD stream: start from beginning
if len(pl.Segments) == 0 {
return fmt.Errorf("no segments found")
}
seg = pl.Segments[0]
}
} else {
var invPos int
seg, invPos = findSegmentWithID(pl.SeqNo, pl.Segments, *d.curSegmentID+1)
if seg == nil {
return fmt.Errorf("following segment not found or not ready yet")
}
d.logger.Log(logger.Debug, "segment inverse position: %d", invPos)
if !pl.Closed && invPos > clientLiveMaxInvPosition {
return fmt.Errorf("playback is too late")
}
}
v := seg.SeqId
d.curSegmentID = &v
byts, err := d.downloadSegment(ctx, seg.URI, seg.Offset, seg.Limit)
if err != nil {
return err
}
segmentQueue.push(byts)
if pl.Closed && pl.Segments[len(pl.Segments)-1] == seg {
<-ctx.Done()
return fmt.Errorf("stream has ended")
}
return nil
}

221
internal/hls/client_processor_fmp4.go

@ -1,221 +0,0 @@ @@ -1,221 +0,0 @@
package hls
import (
"context"
"fmt"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
)
func fmp4PickLeadingTrack(init *fmp4.Init) int {
// pick first video track
for _, track := range init.Tracks {
switch track.Format.(type) {
case *format.H264, *format.H265:
return track.ID
}
}
// otherwise, pick first track
return init.Tracks[0].ID
}
type clientProcessorFMP4 struct {
isLeading bool
segmentQueue *clientSegmentQueue
logger ClientLogger
rp *clientRoutinePool
onSetLeadingTimeSync func(clientTimeSync)
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool)
onData map[format.Format]func(time.Duration, interface{})
init fmp4.Init
leadingTrackID int
trackProcs map[int]*clientProcessorFMP4Track
// in
subpartProcessed chan struct{}
}
func newClientProcessorFMP4(
ctx context.Context,
isLeading bool,
initFile []byte,
segmentQueue *clientSegmentQueue,
logger ClientLogger,
rp *clientRoutinePool,
onStreamFormats func(context.Context, []format.Format) bool,
onSetLeadingTimeSync func(clientTimeSync),
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool),
onData map[format.Format]func(time.Duration, interface{}),
) (*clientProcessorFMP4, error) {
p := &clientProcessorFMP4{
isLeading: isLeading,
segmentQueue: segmentQueue,
logger: logger,
rp: rp,
onSetLeadingTimeSync: onSetLeadingTimeSync,
onGetLeadingTimeSync: onGetLeadingTimeSync,
onData: onData,
subpartProcessed: make(chan struct{}, clientFMP4MaxPartTracksPerSegment),
}
err := p.init.Unmarshal(initFile)
if err != nil {
return nil, err
}
p.leadingTrackID = fmp4PickLeadingTrack(&p.init)
tracks := make([]format.Format, len(p.init.Tracks))
for i, track := range p.init.Tracks {
tracks[i] = track.Format
}
ok := onStreamFormats(ctx, tracks)
if !ok {
return nil, fmt.Errorf("terminated")
}
return p, nil
}
func (p *clientProcessorFMP4) run(ctx context.Context) error {
for {
seg, ok := p.segmentQueue.pull(ctx)
if !ok {
return fmt.Errorf("terminated")
}
err := p.processSegment(ctx, seg)
if err != nil {
return err
}
}
}
func (p *clientProcessorFMP4) processSegment(ctx context.Context, byts []byte) error {
var parts fmp4.Parts
err := parts.Unmarshal(byts)
if err != nil {
return err
}
processingCount := 0
for _, part := range parts {
for _, track := range part.Tracks {
if p.trackProcs == nil {
var ts *clientTimeSyncFMP4
if p.isLeading {
if track.ID != p.leadingTrackID {
continue
}
timeScale := func() uint32 {
for _, track := range p.init.Tracks {
if track.ID == p.leadingTrackID {
return track.TimeScale
}
}
return 0
}()
ts = newClientTimeSyncFMP4(timeScale, track.BaseTime)
p.onSetLeadingTimeSync(ts)
} else {
rawTS, ok := p.onGetLeadingTimeSync(ctx)
if !ok {
return fmt.Errorf("terminated")
}
ts, ok = rawTS.(*clientTimeSyncFMP4)
if !ok {
return fmt.Errorf("stream playlists are mixed MPEGTS/FMP4")
}
}
p.initializeTrackProcs(ts)
}
proc, ok := p.trackProcs[track.ID]
if !ok {
continue
}
if processingCount >= (clientFMP4MaxPartTracksPerSegment - 1) {
return fmt.Errorf("too many part tracks at once")
}
select {
case proc.queue <- track:
case <-ctx.Done():
return fmt.Errorf("terminated")
}
processingCount++
}
}
for i := 0; i < processingCount; i++ {
select {
case <-p.subpartProcessed:
case <-ctx.Done():
return fmt.Errorf("terminated")
}
}
return nil
}
func (p *clientProcessorFMP4) onPartTrackProcessed(ctx context.Context) {
select {
case p.subpartProcessed <- struct{}{}:
case <-ctx.Done():
}
}
func (p *clientProcessorFMP4) initializeTrackProcs(ts *clientTimeSyncFMP4) {
p.trackProcs = make(map[int]*clientProcessorFMP4Track)
for _, track := range p.init.Tracks {
var cb func(time.Duration, []byte) error
cb2, ok := p.onData[track.Format]
if !ok {
cb2 = func(time.Duration, interface{}) {
}
}
switch track.Format.(type) {
case *format.H264, *format.H265:
cb = func(pts time.Duration, payload []byte) error {
nalus, err := h264.AVCCUnmarshal(payload)
if err != nil {
return err
}
cb2(pts, nalus)
return nil
}
case *format.MPEG4Audio, *format.Opus:
cb = func(pts time.Duration, payload []byte) error {
cb2(pts, payload)
return nil
}
}
proc := newClientProcessorFMP4Track(
track.TimeScale,
ts,
p.onPartTrackProcessed,
cb,
)
p.rp.add(proc)
p.trackProcs[track.ID] = proc
}
}

72
internal/hls/client_processor_fmp4_track.go

@ -1,72 +0,0 @@ @@ -1,72 +0,0 @@
package hls
import (
"context"
"time"
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
)
type clientProcessorFMP4Track struct {
timeScale uint32
ts *clientTimeSyncFMP4
onPartTrackProcessed func(context.Context)
onEntry func(time.Duration, []byte) error
// in
queue chan *fmp4.PartTrack
}
func newClientProcessorFMP4Track(
timeScale uint32,
ts *clientTimeSyncFMP4,
onPartTrackProcessed func(context.Context),
onEntry func(time.Duration, []byte) error,
) *clientProcessorFMP4Track {
return &clientProcessorFMP4Track{
timeScale: timeScale,
ts: ts,
onPartTrackProcessed: onPartTrackProcessed,
onEntry: onEntry,
queue: make(chan *fmp4.PartTrack, clientFMP4MaxPartTracksPerSegment),
}
}
func (t *clientProcessorFMP4Track) run(ctx context.Context) error {
for {
select {
case entry := <-t.queue:
err := t.processPartTrack(ctx, entry)
if err != nil {
return err
}
t.onPartTrackProcessed(ctx)
case <-ctx.Done():
return nil
}
}
}
func (t *clientProcessorFMP4Track) processPartTrack(ctx context.Context, pt *fmp4.PartTrack) error {
rawDTS := pt.BaseTime
for _, sample := range pt.Samples {
pts, err := t.ts.convertAndSync(ctx, t.timeScale, rawDTS, sample.PTSOffset)
if err != nil {
return err
}
if pts >= 0 { // silently discard packets prior to the first packet of the leading track
err = t.onEntry(pts, sample.Payload)
if err != nil {
return err
}
}
rawDTS += uint64(sample.Duration)
}
return nil
}

221
internal/hls/client_processor_mpegts.go

@ -1,221 +0,0 @@ @@ -1,221 +0,0 @@
package hls
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/asticode/go-astits"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
func mpegtsPickLeadingTrack(mpegtsTracks []*mpegts.Track) uint16 {
// pick first video track
for _, mt := range mpegtsTracks {
if _, ok := mt.Format.(*format.H264); ok {
return mt.ES.ElementaryPID
}
}
// otherwise, pick first track
return mpegtsTracks[0].ES.ElementaryPID
}
type clientProcessorMPEGTS struct {
isLeading bool
segmentQueue *clientSegmentQueue
logger ClientLogger
rp *clientRoutinePool
onStreamFormats func(context.Context, []format.Format) bool
onSetLeadingTimeSync func(clientTimeSync)
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool)
onData map[format.Format]func(time.Duration, interface{})
mpegtsTracks []*mpegts.Track
leadingTrackPID uint16
trackProcs map[uint16]*clientProcessorMPEGTSTrack
}
func newClientProcessorMPEGTS(
isLeading bool,
segmentQueue *clientSegmentQueue,
logger ClientLogger,
rp *clientRoutinePool,
onStreamFormats func(context.Context, []format.Format) bool,
onSetLeadingTimeSync func(clientTimeSync),
onGetLeadingTimeSync func(context.Context) (clientTimeSync, bool),
onData map[format.Format]func(time.Duration, interface{}),
) *clientProcessorMPEGTS {
return &clientProcessorMPEGTS{
isLeading: isLeading,
segmentQueue: segmentQueue,
logger: logger,
rp: rp,
onStreamFormats: onStreamFormats,
onSetLeadingTimeSync: onSetLeadingTimeSync,
onGetLeadingTimeSync: onGetLeadingTimeSync,
onData: onData,
}
}
func (p *clientProcessorMPEGTS) run(ctx context.Context) error {
for {
seg, ok := p.segmentQueue.pull(ctx)
if !ok {
return fmt.Errorf("terminated")
}
err := p.processSegment(ctx, seg)
if err != nil {
return err
}
}
}
func (p *clientProcessorMPEGTS) processSegment(ctx context.Context, byts []byte) error {
if p.mpegtsTracks == nil {
var err error
p.mpegtsTracks, err = mpegts.FindTracks(byts)
if err != nil {
return err
}
p.leadingTrackPID = mpegtsPickLeadingTrack(p.mpegtsTracks)
tracks := make([]format.Format, len(p.mpegtsTracks))
for i, mt := range p.mpegtsTracks {
tracks[i] = mt.Format
}
ok := p.onStreamFormats(ctx, tracks)
if !ok {
return fmt.Errorf("terminated")
}
}
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts))
for {
data, err := dem.NextData()
if err != nil {
if err == astits.ErrNoMorePackets {
return nil
}
if strings.HasPrefix(err.Error(), "astits: parsing PES data failed") {
continue
}
return err
}
if data.PES == nil {
continue
}
if data.PES.Header.OptionalHeader == nil ||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorNoPTSOrDTS ||
data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorIsForbidden {
return fmt.Errorf("PTS is missing")
}
if p.trackProcs == nil {
var ts *clientTimeSyncMPEGTS
if p.isLeading {
if data.PID != p.leadingTrackPID {
continue
}
var dts int64
if data.PES.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent {
dts = data.PES.Header.OptionalHeader.DTS.Base
} else {
dts = data.PES.Header.OptionalHeader.PTS.Base
}
ts = newClientTimeSyncMPEGTS(dts)
p.onSetLeadingTimeSync(ts)
} else {
rawTS, ok := p.onGetLeadingTimeSync(ctx)
if !ok {
return fmt.Errorf("terminated")
}
ts, ok = rawTS.(*clientTimeSyncMPEGTS)
if !ok {
return fmt.Errorf("stream playlists are mixed MPEGTS/FMP4")
}
}
p.initializeTrackProcs(ts)
}
proc, ok := p.trackProcs[data.PID]
if !ok {
continue
}
select {
case proc.queue <- data.PES:
case <-ctx.Done():
}
}
}
func (p *clientProcessorMPEGTS) initializeTrackProcs(ts *clientTimeSyncMPEGTS) {
p.trackProcs = make(map[uint16]*clientProcessorMPEGTSTrack)
for _, track := range p.mpegtsTracks {
var cb func(time.Duration, []byte) error
cb2, ok := p.onData[track.Format]
if !ok {
cb2 = func(time.Duration, interface{}) {
}
}
switch track.Format.(type) {
case *format.H264:
cb = func(pts time.Duration, payload []byte) error {
nalus, err := h264.AnnexBUnmarshal(payload)
if err != nil {
p.logger.Log(logger.Warn, "unable to decode Annex-B: %s", err)
return nil
}
cb2(pts, nalus)
return nil
}
case *format.MPEG4Audio:
cb = func(pts time.Duration, payload []byte) error {
var adtsPkts mpeg4audio.ADTSPackets
err := adtsPkts.Unmarshal(payload)
if err != nil {
return fmt.Errorf("unable to decode ADTS: %s", err)
}
for i, pkt := range adtsPkts {
cb2(
pts+time.Duration(i)*mpeg4audio.SamplesPerAccessUnit*time.Second/time.Duration(pkt.SampleRate),
pkt.AU)
}
return nil
}
}
proc := newClientProcessorMPEGTSTrack(
ts,
cb,
)
p.rp.add(proc)
p.trackProcs[track.ES.ElementaryPID] = proc
}
}

63
internal/hls/client_processor_mpegts_track.go

@ -1,63 +0,0 @@ @@ -1,63 +0,0 @@
package hls
import (
"context"
"time"
"github.com/asticode/go-astits"
)
type clientProcessorMPEGTSTrack struct {
ts *clientTimeSyncMPEGTS
onEntry func(time.Duration, []byte) error
queue chan *astits.PESData
}
func newClientProcessorMPEGTSTrack(
ts *clientTimeSyncMPEGTS,
onEntry func(time.Duration, []byte) error,
) *clientProcessorMPEGTSTrack {
return &clientProcessorMPEGTSTrack{
ts: ts,
onEntry: onEntry,
queue: make(chan *astits.PESData, clientMPEGTSEntryQueueSize),
}
}
func (t *clientProcessorMPEGTSTrack) run(ctx context.Context) error {
for {
select {
case pes := <-t.queue:
err := t.processEntry(ctx, pes)
if err != nil {
return err
}
case <-ctx.Done():
return nil
}
}
}
func (t *clientProcessorMPEGTSTrack) processEntry(ctx context.Context, pes *astits.PESData) error {
rawPTS := pes.Header.OptionalHeader.PTS.Base
var rawDTS int64
if pes.Header.OptionalHeader.PTSDTSIndicator == astits.PTSDTSIndicatorBothPresent {
rawDTS = pes.Header.OptionalHeader.DTS.Base
} else {
rawDTS = rawPTS
}
pts, err := t.ts.convertAndSync(ctx, rawDTS, rawPTS)
if err != nil {
return err
}
// silently discard packets prior to the first packet of the leading track
if pts < 0 {
return nil
}
return t.onEntry(pts, pes.Data)
}

52
internal/hls/client_routine_pool.go

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
package hls
import (
"context"
"sync"
)
type clientRoutinePoolRunnable interface {
run(context.Context) error
}
type clientRoutinePool struct {
ctx context.Context
ctxCancel func()
wg sync.WaitGroup
err chan error
}
func newClientRoutinePool() *clientRoutinePool {
ctx, ctxCancel := context.WithCancel(context.Background())
return &clientRoutinePool{
ctx: ctx,
ctxCancel: ctxCancel,
err: make(chan error),
}
}
func (rp *clientRoutinePool) close() {
rp.ctxCancel()
rp.wg.Wait()
}
func (rp *clientRoutinePool) errorChan() chan error {
return rp.err
}
func (rp *clientRoutinePool) add(r clientRoutinePoolRunnable) {
rp.wg.Add(1)
go func() {
defer rp.wg.Done()
err := r.run(rp.ctx)
if err != nil {
select {
case rp.err <- err:
case <-rp.ctx.Done():
}
}
}()
}

79
internal/hls/client_segment_queue.go

@ -1,79 +0,0 @@ @@ -1,79 +0,0 @@
package hls
import (
"context"
"sync"
)
type clientSegmentQueue struct {
mutex sync.Mutex
queue [][]byte
didPush chan struct{}
didPull chan struct{}
}
func newClientSegmentQueue() *clientSegmentQueue {
return &clientSegmentQueue{
didPush: make(chan struct{}),
didPull: make(chan struct{}),
}
}
func (q *clientSegmentQueue) push(seg []byte) {
q.mutex.Lock()
queueWasEmpty := (len(q.queue) == 0)
q.queue = append(q.queue, seg)
if queueWasEmpty {
close(q.didPush)
q.didPush = make(chan struct{})
}
q.mutex.Unlock()
}
func (q *clientSegmentQueue) waitUntilSizeIsBelow(ctx context.Context, n int) bool {
q.mutex.Lock()
for len(q.queue) > n {
q.mutex.Unlock()
select {
case <-q.didPull:
case <-ctx.Done():
return false
}
q.mutex.Lock()
}
q.mutex.Unlock()
return true
}
func (q *clientSegmentQueue) pull(ctx context.Context) ([]byte, bool) {
q.mutex.Lock()
for len(q.queue) == 0 {
didPush := q.didPush
q.mutex.Unlock()
select {
case <-didPush:
case <-ctx.Done():
return nil, false
}
q.mutex.Lock()
}
var seg []byte
seg, q.queue = q.queue[0], q.queue[1:]
close(q.didPull)
q.didPull = make(chan struct{})
q.mutex.Unlock()
return seg, true
}

445
internal/hls/client_test.go

@ -1,445 +0,0 @@ @@ -1,445 +0,0 @@
package hls
import (
"bytes"
"context"
"io"
"log"
"net"
"net/http"
"os"
"testing"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/asticode/go-astits"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
"github.com/aler9/rtsp-simple-server/internal/logger"
)
type testLogger struct{}
func (testLogger) Log(level logger.Level, format string, args ...interface{}) {
log.Printf(format, args...)
}
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy
MTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj
zOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv
NJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp
OzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I
qkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e
nI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud
DgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a
u9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj
3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO
xfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu
tEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI
XpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7
7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd
XQxaORfgM//NzX9LhUPk
-----END CERTIFICATE-----
`)
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/
KwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y
1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY
cI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3
6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE
CxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC
kaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT
kYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP
bB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S
Wm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj
5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb
agQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ
M9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3
ygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz
ulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl
+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX
4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp
xF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj
7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf
3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a
r5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO
y++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD
94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK
6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1
+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=
-----END RSA PRIVATE KEY-----
`)
func writeTempFile(byts []byte) (string, error) {
tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-")
if err != nil {
return "", err
}
defer tmpf.Close()
_, err = tmpf.Write(byts)
if err != nil {
return "", err
}
return tmpf.Name(), nil
}
func mpegtsSegment(w io.Writer) {
mux := astits.NewMuxer(context.Background(), w)
mux.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
mux.SetPCRPID(256)
mux.WriteTables()
enc, _ := h264.AnnexBMarshal([][]byte{
{7, 1, 2, 3}, // SPS
{8}, // PPS
{5}, // IDR
})
mux.WriteData(&astits.MuxerData{
PID: 256,
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: &astits.PESOptionalHeader{
MarkerBits: 2,
PTSDTSIndicator: astits.PTSDTSIndicatorBothPresent,
PTS: &astits.ClockReference{Base: 90000}, // +1 sec
DTS: &astits.ClockReference{Base: 0x1FFFFFFFF - 90000 + 1}, // -1 sec
},
StreamID: 224, // = video
},
Data: enc,
},
})
}
func mp4Init(t *testing.T, w io.Writer) {
i := &fmp4.Init{
Tracks: []*fmp4.InitTrack{
{
ID: 1,
TimeScale: 90000,
Format: &format.H264{
SPS: []byte{
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20,
},
PPS: []byte{0x01, 0x02, 0x03, 0x04},
},
},
},
}
byts, err := i.Marshal()
require.NoError(t, err)
_, err = w.Write(byts)
require.NoError(t, err)
}
func mp4Segment(t *testing.T, w io.Writer) {
payload, _ := h264.AVCCMarshal([][]byte{
{7, 1, 2, 3}, // SPS
{8}, // PPS
{5}, // IDR
})
p := &fmp4.Part{
Tracks: []*fmp4.PartTrack{
{
ID: 1,
IsVideo: true,
Samples: []*fmp4.PartSample{{
Duration: 90000 / 30,
PTSOffset: 90000 * 2,
Payload: payload,
}},
},
},
}
byts, err := p.Marshal()
require.NoError(t, err)
_, err = w.Write(byts)
require.NoError(t, err)
}
type testHLSServer struct {
s *http.Server
}
func newTestHLSServer(router http.Handler, isTLS bool) (*testHLSServer, error) {
ln, err := net.Listen("tcp", "localhost:5780")
if err != nil {
return nil, err
}
s := &testHLSServer{
s: &http.Server{Handler: router},
}
if isTLS {
go func() {
serverCertFpath, err := writeTempFile(serverCert)
if err != nil {
panic(err)
}
defer os.Remove(serverCertFpath)
serverKeyFpath, err := writeTempFile(serverKey)
if err != nil {
panic(err)
}
defer os.Remove(serverKeyFpath)
s.s.ServeTLS(ln, serverCertFpath, serverKeyFpath)
}()
} else {
go s.s.Serve(ln)
}
return s, nil
}
func (s *testHLSServer) close() {
s.s.Shutdown(context.Background())
}
func TestClientMPEGTS(t *testing.T) {
for _, ca := range []string{
"plain",
"tls",
"segment with query",
} {
t.Run(ca, func(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
segment := "segment.ts"
if ca == "segment with query" {
segment = "segment.ts?key=val"
}
sent := false
router.GET("/stream.m3u8", func(ctx *gin.Context) {
if sent {
return
}
sent = true
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`)
io.Copy(ctx.Writer, bytes.NewReader([]byte(`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:2,
`+segment+`
#EXT-X-ENDLIST
`)))
})
router.GET("/segment.ts", func(ctx *gin.Context) {
if ca == "segment with query" {
require.Equal(t, "val", ctx.Query("key"))
}
ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
mpegtsSegment(ctx.Writer)
})
s, err := newTestHLSServer(router, ca == "tls")
require.NoError(t, err)
defer s.close()
packetRecv := make(chan struct{})
prefix := "http"
if ca == "tls" {
prefix = "https"
}
c, err := NewClient(
prefix+"://localhost:5780/stream.m3u8",
"33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739",
testLogger{},
)
require.NoError(t, err)
onH264 := func(pts time.Duration, unit interface{}) {
require.Equal(t, 2*time.Second, pts)
require.Equal(t, [][]byte{
{7, 1, 2, 3},
{8},
{5},
}, unit)
close(packetRecv)
}
c.OnTracks(func(tracks []format.Format) error {
require.Equal(t, 1, len(tracks))
require.Equal(t, &format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}, tracks[0])
c.OnData(tracks[0], onH264)
return nil
})
c.Start()
<-packetRecv
c.Close()
<-c.Wait()
})
}
}
func TestClientFMP4(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.GET("/stream.m3u8", func(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`)
io.Copy(ctx.Writer, bytes.NewReader([]byte(`#EXTM3U
#EXT-X-VERSION:7
#EXT-X-MEDIA-SEQUENCE:20
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="init.mp4"
#EXTINF:2,
segment.mp4
#EXT-X-ENDLIST
`)))
})
router.GET("/init.mp4", func(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `video/mp4`)
mp4Init(t, ctx.Writer)
})
router.GET("/segment.mp4", func(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `video/mp4`)
mp4Segment(t, ctx.Writer)
})
s, err := newTestHLSServer(router, false)
require.NoError(t, err)
defer s.close()
packetRecv := make(chan struct{})
onH264 := func(pts time.Duration, unit interface{}) {
require.Equal(t, 2*time.Second, pts)
require.Equal(t, [][]byte{
{7, 1, 2, 3},
{8},
{5},
}, unit)
close(packetRecv)
}
c, err := NewClient(
"http://localhost:5780/stream.m3u8",
"",
testLogger{},
)
require.NoError(t, err)
c.OnTracks(func(tracks []format.Format) error {
require.Equal(t, 1, len(tracks))
_, ok := tracks[0].(*format.H264)
require.Equal(t, true, ok)
c.OnData(tracks[0], onH264)
return nil
})
c.Start()
<-packetRecv
c.Close()
<-c.Wait()
}
func TestClientInvalidSequenceID(t *testing.T) {
router := gin.New()
firstPlaylist := true
router.GET("/stream.m3u8", func(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `application/x-mpegURL`)
if firstPlaylist {
firstPlaylist = false
io.Copy(ctx.Writer, bytes.NewReader([]byte(
`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:2
#EXTINF:2,
segment1.ts
#EXTINF:2,
segment1.ts
#EXTINF:2,
segment1.ts
`)))
} else {
io.Copy(ctx.Writer, bytes.NewReader([]byte(
`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:2
#EXT-X-MEDIA-SEQUENCE:4
#EXTINF:2,
segment1.ts
#EXTINF:2,
segment1.ts
#EXTINF:2,
segment1.ts
`)))
}
})
router.GET("/segment1.ts", func(ctx *gin.Context) {
ctx.Writer.Header().Set("Content-Type", `video/MP2T`)
mpegtsSegment(ctx.Writer)
})
s, err := newTestHLSServer(router, false)
require.NoError(t, err)
defer s.close()
c, err := NewClient(
"http://localhost:5780/stream.m3u8",
"",
testLogger{},
)
require.NoError(t, err)
c.OnTracks(func(tracks []format.Format) error {
return nil
})
c.Start()
err = <-c.Wait()
require.EqualError(t, err, "following segment not found or not ready yet")
c.Close()
}

59
internal/hls/client_timesync_fmp4.go

@ -1,59 +0,0 @@ @@ -1,59 +0,0 @@
package hls
import (
"context"
"fmt"
"time"
)
func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
timeScale64 := uint64(timeScale)
secs := v / time.Second
dec := v % time.Second
return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
}
func durationMp4ToGo(v uint64, timeScale uint32) time.Duration {
timeScale64 := uint64(timeScale)
secs := v / timeScale64
dec := v % timeScale64
return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)
}
type clientTimeSyncFMP4 struct {
startRTC time.Time
startDTS time.Duration
}
func newClientTimeSyncFMP4(timeScale uint32, baseTime uint64) *clientTimeSyncFMP4 {
return &clientTimeSyncFMP4{
startRTC: time.Now(),
startDTS: durationMp4ToGo(baseTime, timeScale),
}
}
func (ts *clientTimeSyncFMP4) convertAndSync(ctx context.Context, timeScale uint32,
rawDTS uint64, ptsOffset int32,
) (time.Duration, error) {
pts := durationMp4ToGo(rawDTS+uint64(ptsOffset), timeScale)
dts := durationMp4ToGo(rawDTS, timeScale)
pts -= ts.startDTS
dts -= ts.startDTS
elapsed := time.Since(ts.startRTC)
if dts > elapsed {
diff := dts - elapsed
if diff > clientMaxDTSRTCDiff {
return 0, fmt.Errorf("difference between DTS and RTC is too big")
}
select {
case <-time.After(diff):
case <-ctx.Done():
return 0, fmt.Errorf("terminated")
}
}
return pts, nil
}

46
internal/hls/client_timesync_mpegts.go

@ -1,46 +0,0 @@ @@ -1,46 +0,0 @@
package hls
import (
"context"
"fmt"
"sync"
"time"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegtstimedec"
)
type clientTimeSyncMPEGTS struct {
startRTC time.Time
td *mpegtstimedec.Decoder
mutex sync.Mutex
}
func newClientTimeSyncMPEGTS(startDTS int64) *clientTimeSyncMPEGTS {
return &clientTimeSyncMPEGTS{
startRTC: time.Now(),
td: mpegtstimedec.New(startDTS),
}
}
func (ts *clientTimeSyncMPEGTS) convertAndSync(ctx context.Context, rawDTS int64, rawPTS int64) (time.Duration, error) {
ts.mutex.Lock()
dts := ts.td.Decode(rawDTS)
pts := ts.td.Decode(rawPTS)
ts.mutex.Unlock()
elapsed := time.Since(ts.startRTC)
if dts > elapsed {
diff := dts - elapsed
if diff > clientMaxDTSRTCDiff {
return 0, fmt.Errorf("difference between DTS and RTC is too big")
}
select {
case <-time.After(diff):
case <-ctx.Done():
return 0, fmt.Errorf("terminated")
}
}
return pts, nil
}

143
internal/hls/codecparameters.go

@ -1,143 +0,0 @@ @@ -1,143 +0,0 @@
package hls
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
"github.com/aler9/gortsplib/v2/pkg/format"
)
func encodeProfileSpace(v uint8) string {
switch v {
case 1:
return "A"
case 2:
return "B"
case 3:
return "C"
}
return ""
}
func encodeCompatibilityFlag(v [32]bool) string {
var o uint32
for i, b := range v {
if b {
o |= 1 << i
}
}
return fmt.Sprintf("%x", o)
}
func encodeGeneralTierFlag(v uint8) string {
if v > 0 {
return "H"
}
return "L"
}
func encodeGeneralConstraintIndicatorFlags(v *h265.SPS_ProfileTierLevel) string {
var ret []string
var o1 uint8
if v.GeneralProgressiveSourceFlag {
o1 |= 1 << 7
}
if v.GeneralInterlacedSourceFlag {
o1 |= 1 << 6
}
if v.GeneralNonPackedConstraintFlag {
o1 |= 1 << 5
}
if v.GeneralFrameOnlyConstraintFlag {
o1 |= 1 << 4
}
if v.GeneralMax12bitConstraintFlag {
o1 |= 1 << 3
}
if v.GeneralMax10bitConstraintFlag {
o1 |= 1 << 2
}
if v.GeneralMax8bitConstraintFlag {
o1 |= 1 << 1
}
if v.GeneralMax422ChromeConstraintFlag {
o1 |= 1 << 0
}
ret = append(ret, fmt.Sprintf("%x", o1))
var o2 uint8
if v.GeneralMax420ChromaConstraintFlag {
o2 |= 1 << 7
}
if v.GeneralMaxMonochromeConstraintFlag {
o2 |= 1 << 6
}
if v.GeneralIntraConstraintFlag {
o2 |= 1 << 5
}
if v.GeneralOnePictureOnlyConstraintFlag {
o2 |= 1 << 4
}
if v.GeneralLowerBitRateConstraintFlag {
o2 |= 1 << 3
}
if v.GeneralMax14BitConstraintFlag {
o2 |= 1 << 2
}
if o2 != 0 {
ret = append(ret, fmt.Sprintf("%x", o2))
}
return strings.Join(ret, ".")
}
func codecParametersGenerate(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." +
encodeProfileSpace(sps.ProfileTierLevel.GeneralProfileSpace) +
strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralProfileIdc), 10) + "." +
encodeCompatibilityFlag(sps.ProfileTierLevel.GeneralProfileCompatibilityFlag) + "." +
encodeGeneralTierFlag(sps.ProfileTierLevel.GeneralTierFlag) +
strconv.FormatInt(int64(sps.ProfileTierLevel.GeneralLevelIdc), 10) + "." +
encodeGeneralConstraintIndicatorFlags(&sps.ProfileTierLevel)
}
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)
case *format.Opus:
return "opus"
}
return ""
}
func codecParametersAreSupported(codecs string) bool {
for _, codec := range strings.Split(codecs, ",") {
if !strings.HasPrefix(codec, "avc1.") &&
!strings.HasPrefix(codec, "hvc1.") &&
!strings.HasPrefix(codec, "hev1.") &&
!strings.HasPrefix(codec, "mp4a.") &&
codec != "opus" {
return false
}
}
return true
}

53
internal/hls/fmp4/boxes_opus.go

@ -1,53 +0,0 @@ @@ -1,53 +0,0 @@
//nolint:gochecknoinits,revive,gocritic
package fmp4
import (
gomp4 "github.com/abema/go-mp4"
)
func BoxTypeOpus() gomp4.BoxType { return gomp4.StrToBoxType("Opus") }
func init() {
gomp4.AddAnyTypeBoxDef(&gomp4.AudioSampleEntry{}, BoxTypeOpus())
}
func BoxTypeDOps() gomp4.BoxType { return gomp4.StrToBoxType("dOps") }
func init() {
gomp4.AddBoxDef(&DOps{})
}
type DOpsChannelMappingTable struct{}
type DOps struct {
gomp4.Box
Version uint8 `mp4:"0,size=8"`
OutputChannelCount uint8 `mp4:"1,size=8"`
PreSkip uint16 `mp4:"2,size=16"`
InputSampleRate uint32 `mp4:"3,size=32"`
OutputGain int16 `mp4:"4,size=16"`
ChannelMappingFamily uint8 `mp4:"5,size=8"`
StreamCount uint8 `mp4:"6,opt=dynamic,size=8"`
CoupledCount uint8 `mp4:"7,opt=dynamic,size=8"`
ChannelMapping []uint8 `mp4:"8,opt=dynamic,size=8,len=dynamic"`
}
func (DOps) GetType() gomp4.BoxType {
return BoxTypeDOps()
}
func (dops DOps) IsOptFieldEnabled(name string, ctx gomp4.Context) bool {
switch name {
case "StreamCount", "CoupledCount", "ChannelMapping":
return dops.ChannelMappingFamily != 0
}
return false
}
func (ops DOps) GetFieldLength(name string, ctx gomp4.Context) uint {
switch name {
case "ChannelMapping":
return uint(ops.OutputChannelCount)
}
return 0
}

2
internal/hls/fmp4/fmp4.go

@ -1,2 +0,0 @@ @@ -1,2 +0,0 @@
// Package fmp4 contains a fMP4 reader and writer.
package fmp4

363
internal/hls/fmp4/init.go

@ -1,363 +0,0 @@ @@ -1,363 +0,0 @@
package fmp4
import (
"bytes"
"fmt"
gomp4 "github.com/abema/go-mp4"
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
)
// Init is a FMP4 initialization file.
type Init struct {
Tracks []*InitTrack
}
// Unmarshal decodes a FMP4 initialization file.
func (i *Init) Unmarshal(byts []byte) error {
type readState int
const (
waitingTrak readState = iota
waitingTkhd
waitingMdhd
waitingCodec
waitingAvcC
waitingHvcC
waitingEsds
waitingDOps
)
state := waitingTrak
var curTrack *InitTrack
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "trak":
if state != waitingTrak {
return nil, fmt.Errorf("unexpected box 'trak'")
}
curTrack = &InitTrack{}
i.Tracks = append(i.Tracks, curTrack)
state = waitingTkhd
case "tkhd":
if state != waitingTkhd {
return nil, fmt.Errorf("unexpected box 'tkhd'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tkhd := box.(*gomp4.Tkhd)
curTrack.ID = int(tkhd.TrackID)
state = waitingMdhd
case "mdhd":
if state != waitingMdhd {
return nil, fmt.Errorf("unexpected box 'mdhd'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
mdhd := box.(*gomp4.Mdhd)
curTrack.TimeScale = mdhd.Timescale
state = waitingCodec
case "avc1":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box 'avc1'")
}
state = waitingAvcC
case "avcC":
if state != waitingAvcC {
return nil, fmt.Errorf("unexpected box 'avcC'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
avcc := box.(*gomp4.AVCDecoderConfiguration)
if len(avcc.SequenceParameterSets) > 1 {
return nil, fmt.Errorf("multiple SPS are not supported")
}
var sps []byte
if len(avcc.SequenceParameterSets) == 1 {
sps = avcc.SequenceParameterSets[0].NALUnit
}
if len(avcc.PictureParameterSets) > 1 {
return nil, fmt.Errorf("multiple PPS are not supported")
}
var pps []byte
if len(avcc.PictureParameterSets) == 1 {
pps = avcc.PictureParameterSets[0].NALUnit
}
curTrack.Format = &format.H264{
PayloadTyp: 96,
SPS: sps,
PPS: pps,
PacketizationMode: 1,
}
state = waitingTrak
case "hev1", "hvc1":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box 'hev1'")
}
state = waitingHvcC
case "hvcC":
if state != waitingHvcC {
return nil, fmt.Errorf("unexpected box 'hvcC'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
hvcc := box.(*gomp4.HvcC)
var vps []byte
var sps []byte
var pps []byte
for _, arr := range hvcc.NaluArrays {
switch h265.NALUType(arr.NaluType) {
case h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT:
if arr.NumNalus != 1 {
return nil, fmt.Errorf("multiple VPS/SPS/PPS are not supported")
}
}
switch h265.NALUType(arr.NaluType) {
case h265.NALUType_VPS_NUT:
vps = arr.Nalus[0].NALUnit
case h265.NALUType_SPS_NUT:
sps = arr.Nalus[0].NALUnit
case h265.NALUType_PPS_NUT:
pps = arr.Nalus[0].NALUnit
}
}
if vps == nil {
return nil, fmt.Errorf("VPS not provided")
}
if sps == nil {
return nil, fmt.Errorf("SPS not provided")
}
if pps == nil {
return nil, fmt.Errorf("PPS not provided")
}
curTrack.Format = &format.H265{
PayloadTyp: 96,
VPS: vps,
SPS: sps,
PPS: pps,
}
state = waitingTrak
case "mp4a":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box 'mp4a'")
}
state = waitingEsds
case "esds":
if state != waitingEsds {
return nil, fmt.Errorf("unexpected box 'esds'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
esds := box.(*gomp4.Esds)
encodedConf := func() []byte {
for _, desc := range esds.Descriptors {
if desc.Tag == gomp4.DecSpecificInfoTag {
return desc.Data
}
}
return nil
}()
if encodedConf == nil {
return nil, fmt.Errorf("unable to find MPEG4-audio configuration")
}
var c mpeg4audio.Config
err = c.Unmarshal(encodedConf)
if err != nil {
return nil, fmt.Errorf("invalid MPEG4-audio configuration: %s", err)
}
curTrack.Format = &format.MPEG4Audio{
PayloadTyp: 96,
Config: &c,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
state = waitingTrak
case "Opus":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box 'Opus'")
}
state = waitingDOps
case "dOps":
if state != waitingDOps {
return nil, fmt.Errorf("unexpected box 'dOps'")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
dops := box.(*DOps)
curTrack.Format = &format.Opus{
PayloadTyp: 96,
SampleRate: int(dops.InputSampleRate),
ChannelCount: int(dops.OutputChannelCount),
}
state = waitingTrak
case "ac-3": // ac-3, not supported yet
i.Tracks = i.Tracks[:len(i.Tracks)-1]
state = waitingTrak
return nil, nil
case "ec-3": // ec-3, not supported yet
i.Tracks = i.Tracks[:len(i.Tracks)-1]
state = waitingTrak
return nil, nil
case "c608", "c708": // closed captions, not supported yet
i.Tracks = i.Tracks[:len(i.Tracks)-1]
state = waitingTrak
return nil, nil
case "chrm", "nmhd":
return nil, nil
}
return h.Expand()
})
if err != nil {
return err
}
if state != waitingTrak {
return fmt.Errorf("parse error")
}
if len(i.Tracks) == 0 {
return fmt.Errorf("no tracks found")
}
return nil
}
// Marshal encodes a FMP4 initialization file.
func (i *Init) Marshal() ([]byte, error) {
/*
- ftyp
- moov
- mvhd
- trak
- trak
- ...
- mvex
- trex
- trex
- ...
*/
w := newMP4Writer()
_, err := w.WriteBox(&gomp4.Ftyp{ // <ftyp/>
MajorBrand: [4]byte{'m', 'p', '4', '2'},
MinorVersion: 1,
CompatibleBrands: []gomp4.CompatibleBrandElem{
{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}},
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}},
{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}},
{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}},
},
})
if err != nil {
return nil, err
}
_, err = w.writeBoxStart(&gomp4.Moov{}) // <moov>
if err != nil {
return nil, err
}
_, err = w.WriteBox(&gomp4.Mvhd{ // <mvhd/>
Timescale: 1000,
Rate: 65536,
Volume: 256,
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000},
NextTrackID: 4294967295,
})
if err != nil {
return nil, err
}
for _, track := range i.Tracks {
err := track.marshal(w)
if err != nil {
return nil, err
}
}
_, err = w.writeBoxStart(&gomp4.Mvex{}) // <mvex>
if err != nil {
return nil, err
}
for _, track := range i.Tracks {
_, err = w.WriteBox(&gomp4.Trex{ // <trex/>
TrackID: uint32(track.ID),
DefaultSampleDescriptionIndex: 1,
})
if err != nil {
return nil, err
}
}
err = w.writeBoxEnd() // </mvex>
if err != nil {
return nil, err
}
err = w.writeBoxEnd() // </moov>
if err != nil {
return nil, err
}
return w.bytes(), nil
}

1453
internal/hls/fmp4/init_test.go

File diff suppressed because it is too large Load Diff

526
internal/hls/fmp4/init_track.go

@ -1,526 +0,0 @@ @@ -1,526 +0,0 @@
package fmp4
import (
gomp4 "github.com/abema/go-mp4"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/codecs/h265"
"github.com/aler9/gortsplib/v2/pkg/format"
)
// InitTrack is a track of Init.
type InitTrack struct {
ID int
TimeScale uint32
Format format.Format
}
func (track *InitTrack) marshal(w *mp4Writer) error {
/*
trak
- tkhd
- mdia
- mdhd
- hdlr
- minf
- vmhd (video)
- smhd (audio)
- dinf
- dref
- url
- stbl
- stsd
- avc1 (h264)
- avcC
- btrt
- hev1 (h265)
- hvcC
- mp4a (mpeg4audio)
- esds
- btrt
- Opus (opus)
- dOps
- btrt
- stts
- stsc
- stsz
- stco
*/
_, err := w.writeBoxStart(&gomp4.Trak{}) // <trak>
if err != nil {
return err
}
var h264SPS []byte
var h264PPS []byte
var h264SPSP h264.SPS
var h265VPS []byte
var h265SPS []byte
var h265PPS []byte
var h265SPSP h265.SPS
var width int
var height int
switch ttrack := track.Format.(type) {
case *format.H264:
h264SPS = ttrack.SafeSPS()
h264PPS = ttrack.SafePPS()
err = h264SPSP.Unmarshal(h264SPS)
if err != nil {
return err
}
width = h264SPSP.Width()
height = h264SPSP.Height()
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()
}
switch track.Format.(type) {
case *format.H264, *format.H265:
_, 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 {
return err
}
case *format.MPEG4Audio, *format.Opus:
_, err = w.WriteBox(&gomp4.Tkhd{ // <tkhd/>
FullBox: gomp4.FullBox{
Flags: [3]byte{0, 0, 3},
},
TrackID: uint32(track.ID),
AlternateGroup: 1,
Volume: 256,
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
})
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&gomp4.Mdia{}) // <mdia>
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Mdhd{ // <mdhd/>
Timescale: track.TimeScale,
Language: [3]byte{'u', 'n', 'd'},
})
if err != nil {
return err
}
switch track.Format.(type) {
case *format.H264, *format.H265:
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
HandlerType: [4]byte{'v', 'i', 'd', 'e'},
Name: "VideoHandler",
})
if err != nil {
return err
}
case *format.MPEG4Audio, *format.Opus:
_, err = w.WriteBox(&gomp4.Hdlr{ // <hdlr/>
HandlerType: [4]byte{'s', 'o', 'u', 'n'},
Name: "SoundHandler",
})
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&gomp4.Minf{}) // <minf>
if err != nil {
return err
}
switch track.Format.(type) {
case *format.H264, *format.H265:
_, err = w.WriteBox(&gomp4.Vmhd{ // <vmhd/>
FullBox: gomp4.FullBox{
Flags: [3]byte{0, 0, 1},
},
})
if err != nil {
return err
}
case *format.MPEG4Audio, *format.Opus:
_, err = w.WriteBox(&gomp4.Smhd{ // <smhd/>
})
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&gomp4.Dinf{}) // <dinf>
if err != nil {
return err
}
_, err = w.writeBoxStart(&gomp4.Dref{ // <dref>
EntryCount: 1,
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Url{ // <url/>
FullBox: gomp4.FullBox{
Flags: [3]byte{0, 0, 1},
},
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </dref>
if err != nil {
return err
}
err = w.writeBoxEnd() // </dinf>
if err != nil {
return err
}
_, err = w.writeBoxStart(&gomp4.Stbl{}) // <stbl>
if err != nil {
return err
}
_, err = w.writeBoxStart(&gomp4.Stsd{ // <stsd>
EntryCount: 1,
})
if err != nil {
return err
}
switch ttrack := track.Format.(type) {
case *format.H264:
_, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // <avc1>
SampleEntry: gomp4.SampleEntry{
AnyTypeBox: gomp4.AnyTypeBox{
Type: gomp4.BoxTypeAvc1(),
},
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.AVCDecoderConfiguration{ // <avcc/>
AnyTypeBox: gomp4.AnyTypeBox{
Type: gomp4.BoxTypeAvcC(),
},
ConfigurationVersion: 1,
Profile: h264SPSP.ProfileIdc,
ProfileCompatibility: h264SPS[2],
Level: h264SPSP.LevelIdc,
LengthSizeMinusOne: 3,
NumOfSequenceParameterSets: 1,
SequenceParameterSets: []gomp4.AVCParameterSet{
{
Length: uint16(len(h264SPS)),
NALUnit: h264SPS,
},
},
NumOfPictureParameterSets: 1,
PictureParameterSets: []gomp4.AVCParameterSet{
{
Length: uint16(len(h264PPS)),
NALUnit: h264PPS,
},
},
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
MaxBitrate: 1000000,
AvgBitrate: 1000000,
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </avc1>
if err != nil {
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:
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <mp4a>
SampleEntry: gomp4.SampleEntry{
AnyTypeBox: gomp4.AnyTypeBox{
Type: gomp4.BoxTypeMp4a(),
},
DataReferenceIndex: 1,
},
ChannelCount: uint16(ttrack.Config.ChannelCount),
SampleSize: 16,
SampleRate: uint32(ttrack.ClockRate() * 65536),
})
if err != nil {
return err
}
enc, _ := ttrack.Config.Marshal()
_, err = w.WriteBox(&gomp4.Esds{ // <esds/>
Descriptors: []gomp4.Descriptor{
{
Tag: gomp4.ESDescrTag,
Size: 32 + uint32(len(enc)),
ESDescriptor: &gomp4.ESDescriptor{
ESID: uint16(track.ID),
},
},
{
Tag: gomp4.DecoderConfigDescrTag,
Size: 18 + uint32(len(enc)),
DecoderConfigDescriptor: &gomp4.DecoderConfigDescriptor{
ObjectTypeIndication: 0x40,
StreamType: 0x05,
UpStream: false,
Reserved: true,
MaxBitrate: 128825,
AvgBitrate: 128825,
},
},
{
Tag: gomp4.DecSpecificInfoTag,
Size: uint32(len(enc)),
Data: enc,
},
{
Tag: gomp4.SLConfigDescrTag,
Size: 1,
Data: []byte{0x02},
},
},
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
MaxBitrate: 128825,
AvgBitrate: 128825,
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </mp4a>
if err != nil {
return err
}
case *format.Opus:
_, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // <Opus>
SampleEntry: gomp4.SampleEntry{
AnyTypeBox: gomp4.AnyTypeBox{
Type: BoxTypeOpus(),
},
DataReferenceIndex: 1,
},
ChannelCount: uint16(ttrack.ChannelCount),
SampleSize: 16,
SampleRate: uint32(ttrack.ClockRate() * 65536),
})
if err != nil {
return err
}
_, err = w.WriteBox(&DOps{ // <dOps/>
OutputChannelCount: uint8(ttrack.ChannelCount),
PreSkip: 312,
InputSampleRate: uint32(ttrack.ClockRate()),
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Btrt{ // <btrt/>
MaxBitrate: 128825,
AvgBitrate: 128825,
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </Opus>
if err != nil {
return err
}
}
err = w.writeBoxEnd() // </stsd>
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Stts{ // <stts>
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Stsc{ // <stsc>
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Stsz{ // <stsz>
})
if err != nil {
return err
}
_, err = w.WriteBox(&gomp4.Stco{ // <stco>
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </stbl>
if err != nil {
return err
}
err = w.writeBoxEnd() // </minf>
if err != nil {
return err
}
err = w.writeBoxEnd() // </mdia>
if err != nil {
return err
}
err = w.writeBoxEnd() // </trak>
if err != nil {
return err
}
return nil
}

93
internal/hls/fmp4/mp4_writer.go

@ -1,93 +0,0 @@ @@ -1,93 +0,0 @@
package fmp4
import (
"io"
gomp4 "github.com/abema/go-mp4"
"github.com/orcaman/writerseeker"
)
type mp4Writer struct {
buf *writerseeker.WriterSeeker
w *gomp4.Writer
}
func newMP4Writer() *mp4Writer {
w := &mp4Writer{
buf: &writerseeker.WriterSeeker{},
}
w.w = gomp4.NewWriter(w.buf)
return w
}
func (w *mp4Writer) writeBoxStart(box gomp4.IImmutableBox) (int, error) {
bi := &gomp4.BoxInfo{
Type: box.GetType(),
}
var err error
bi, err = w.w.StartBox(bi)
if err != nil {
return 0, err
}
_, err = gomp4.Marshal(w.w, box, gomp4.Context{})
if err != nil {
return 0, err
}
return int(bi.Offset), nil
}
func (w *mp4Writer) writeBoxEnd() error {
_, err := w.w.EndBox()
return err
}
func (w *mp4Writer) WriteBox(box gomp4.IImmutableBox) (int, error) {
off, err := w.writeBoxStart(box)
if err != nil {
return 0, err
}
err = w.writeBoxEnd()
if err != nil {
return 0, err
}
return off, nil
}
func (w *mp4Writer) rewriteBox(off int, box gomp4.IImmutableBox) error {
prevOff, err := w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
_, err = w.w.Seek(int64(off), io.SeekStart)
if err != nil {
return err
}
_, err = w.writeBoxStart(box)
if err != nil {
return err
}
err = w.writeBoxEnd()
if err != nil {
return err
}
_, err = w.w.Seek(prevOff, io.SeekStart)
if err != nil {
return err
}
return nil
}
func (w *mp4Writer) bytes() []byte {
return w.buf.Bytes()
}

263
internal/hls/fmp4/part.go

@ -1,263 +0,0 @@ @@ -1,263 +0,0 @@
package fmp4
import (
"bytes"
"fmt"
gomp4 "github.com/abema/go-mp4"
)
const (
trunFlagDataOffsetPreset = 0x01
trunFlagSampleDurationPresent = 0x100
trunFlagSampleSizePresent = 0x200
trunFlagSampleFlagsPresent = 0x400
trunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800
sampleFlagIsNonSyncSample = 1 << 16
)
// Part is a FMP4 part file.
type Part struct {
Tracks []*PartTrack
}
// Parts is a sequence of FMP4 parts.
type Parts []*Part
// Unmarshal decodes one or more FMP4 parts.
func (ps *Parts) Unmarshal(byts []byte) error {
type readState int
const (
waitingMoof readState = iota
waitingTraf
waitingTfdtTfhdTrun
)
state := waitingMoof
var curPart *Part
var moofOffset uint64
var curTrack *PartTrack
var tfdt *gomp4.Tfdt
var tfhd *gomp4.Tfhd
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) {
switch h.BoxInfo.Type.String() {
case "moof":
if state != waitingMoof {
return nil, fmt.Errorf("unexpected moof")
}
curPart = &Part{}
*ps = append(*ps, curPart)
moofOffset = h.BoxInfo.Offset
state = waitingTraf
case "traf":
if state != waitingTraf && state != waitingTfdtTfhdTrun {
return nil, fmt.Errorf("unexpected traf")
}
if curTrack != nil {
if tfdt == nil || tfhd == nil || curTrack.Samples == nil {
return nil, fmt.Errorf("parse error")
}
}
curTrack = &PartTrack{}
curPart.Tracks = append(curPart.Tracks, curTrack)
tfdt = nil
tfhd = nil
state = waitingTfdtTfhdTrun
case "tfhd":
if state != waitingTfdtTfhdTrun || tfhd != nil {
return nil, fmt.Errorf("unexpected tfhd")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfhd = box.(*gomp4.Tfhd)
curTrack.ID = int(tfhd.TrackID)
case "tfdt":
if state != waitingTfdtTfhdTrun || tfdt != nil {
return nil, fmt.Errorf("unexpected tfdt")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tfdt = box.(*gomp4.Tfdt)
if tfdt.FullBox.Version != 1 {
return nil, fmt.Errorf("unsupported tfdt version")
}
curTrack.BaseTime = tfdt.BaseMediaDecodeTimeV1
case "trun":
if state != waitingTfdtTfhdTrun || tfhd == nil {
return nil, fmt.Errorf("unexpected trun")
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
trun := box.(*gomp4.Trun)
trunFlags := uint16(trun.Flags[1])<<8 | uint16(trun.Flags[2])
if (trunFlags & trunFlagDataOffsetPreset) == 0 {
return nil, fmt.Errorf("unsupported flags")
}
existing := len(curTrack.Samples)
tmp := make([]*PartSample, existing+len(trun.Entries))
copy(tmp, curTrack.Samples)
curTrack.Samples = tmp
ptr := byts[uint64(trun.DataOffset)+moofOffset:]
for i, e := range trun.Entries {
s := &PartSample{}
if (trunFlags & trunFlagSampleDurationPresent) != 0 {
s.Duration = e.SampleDuration
} else {
s.Duration = tfhd.DefaultSampleDuration
}
s.PTSOffset = e.SampleCompositionTimeOffsetV1
var sampleFlags uint32
if (trunFlags & trunFlagSampleFlagsPresent) != 0 {
sampleFlags = e.SampleFlags
} else {
sampleFlags = tfhd.DefaultSampleFlags
}
s.IsNonSyncSample = ((sampleFlags & sampleFlagIsNonSyncSample) != 0)
var size uint32
if (trunFlags & trunFlagSampleSizePresent) != 0 {
size = e.SampleSize
} else {
size = tfhd.DefaultSampleSize
}
s.Payload = ptr[:size]
ptr = ptr[size:]
curTrack.Samples[existing+i] = s
}
case "mdat":
if state != waitingTraf && state != waitingTfdtTfhdTrun {
return nil, fmt.Errorf("unexpected mdat")
}
if curTrack != nil {
if tfdt == nil || tfhd == nil || curTrack.Samples == nil {
return nil, fmt.Errorf("parse error")
}
}
state = waitingMoof
return nil, nil
}
return h.Expand()
})
if err != nil {
return err
}
if state != waitingMoof {
return fmt.Errorf("decode error")
}
return nil
}
// Marshal encodes a FMP4 part file.
func (p *Part) Marshal() ([]byte, error) {
/*
moof
- mfhd
- traf (video)
- traf (audio)
mdat
*/
w := newMP4Writer()
moofOffset, err := w.writeBoxStart(&gomp4.Moof{}) // <moof>
if err != nil {
return nil, err
}
_, err = w.WriteBox(&gomp4.Mfhd{ // <mfhd/>
SequenceNumber: 0,
})
if err != nil {
return nil, err
}
trackLen := len(p.Tracks)
truns := make([]*gomp4.Trun, trackLen)
trunOffsets := make([]int, trackLen)
dataOffsets := make([]int, trackLen)
dataSize := 0
for i, track := range p.Tracks {
trun, trunOffset, err := track.marshal(w)
if err != nil {
return nil, err
}
dataOffsets[i] = dataSize
for _, sample := range track.Samples {
dataSize += len(sample.Payload)
}
truns[i] = trun
trunOffsets[i] = trunOffset
}
err = w.writeBoxEnd() // </moof>
if err != nil {
return nil, err
}
mdat := &gomp4.Mdat{} // <mdat/>
mdat.Data = make([]byte, dataSize)
pos := 0
for _, track := range p.Tracks {
for _, sample := range track.Samples {
pos += copy(mdat.Data[pos:], sample.Payload)
}
}
mdatOffset, err := w.WriteBox(mdat)
if err != nil {
return nil, err
}
for i := range p.Tracks {
truns[i].DataOffset = int32(dataOffsets[i] + mdatOffset - moofOffset + 8)
err = w.rewriteBox(trunOffsets[i], truns[i])
if err != nil {
return nil, err
}
}
return w.bytes(), nil
}

249
internal/hls/fmp4/part_test.go

@ -1,249 +0,0 @@ @@ -1,249 +0,0 @@
package fmp4
import (
"bytes"
"testing"
gomp4 "github.com/abema/go-mp4"
"github.com/stretchr/testify/require"
)
func testMP4(t *testing.T, byts []byte, boxes []gomp4.BoxPath) {
i := 0
_, err := gomp4.ReadBoxStructure(bytes.NewReader(byts), func(h *gomp4.ReadHandle) (interface{}, error) {
require.Equal(t, boxes[i], h.Path)
i++
return h.Expand()
})
require.NoError(t, err)
}
func TestPartMarshal(t *testing.T) {
testVideoSamples := []*PartSample{
{
Duration: 2 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x04,
0x01, 0x02, 0x03, 0x04, // SPS
0x00, 0x00, 0x00, 0x01,
0x08, // PPS
0x00, 0x00, 0x00, 0x01,
0x05, // IDR
},
},
{
Duration: 2 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x01,
0x01, // non-IDR
},
IsNonSyncSample: true,
},
{
Duration: 1 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x01,
0x01, // non-IDR
},
IsNonSyncSample: true,
},
}
testAudioSamples := []*PartSample{
{
Duration: 500 * 48000 / 1000,
Payload: []byte{
0x01, 0x02, 0x03, 0x04,
},
},
{
Duration: 1 * 48000,
Payload: []byte{
0x01, 0x02, 0x03, 0x04,
},
},
}
t.Run("video + audio", func(t *testing.T) {
part := Part{
Tracks: []*PartTrack{
{
ID: 1,
Samples: testVideoSamples,
IsVideo: true,
},
{
ID: 2,
BaseTime: 3 * 48000,
Samples: testAudioSamples,
},
},
}
byts, err := part.Marshal()
require.NoError(t, err)
boxes := []gomp4.BoxPath{
{gomp4.BoxTypeMoof()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()},
{gomp4.BoxTypeMdat()},
}
testMP4(t, byts, boxes)
})
t.Run("video only", func(t *testing.T) {
part := Part{
Tracks: []*PartTrack{
{
ID: 1,
Samples: testVideoSamples,
IsVideo: true,
},
},
}
byts, err := part.Marshal()
require.NoError(t, err)
boxes := []gomp4.BoxPath{
{gomp4.BoxTypeMoof()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()},
{gomp4.BoxTypeMdat()},
}
testMP4(t, byts, boxes)
})
t.Run("audio only", func(t *testing.T) {
part := Part{
Tracks: []*PartTrack{
{
ID: 1,
Samples: testAudioSamples,
},
},
}
byts, err := part.Marshal()
require.NoError(t, err)
boxes := []gomp4.BoxPath{
{gomp4.BoxTypeMoof()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeMfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfhd()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTfdt()},
{gomp4.BoxTypeMoof(), gomp4.BoxTypeTraf(), gomp4.BoxTypeTrun()},
{gomp4.BoxTypeMdat()},
}
testMP4(t, byts, boxes)
})
}
func TestPartUnmarshal(t *testing.T) {
byts := []byte{
0x00, 0x00, 0x00, 0xd8, 0x6d, 0x6f, 0x6f, 0x66,
0x00, 0x00, 0x00, 0x10, 0x6d, 0x66, 0x68, 0x64,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x70, 0x74, 0x72, 0x61, 0x66,
0x00, 0x00, 0x00, 0x10, 0x74, 0x66, 0x68, 0x64,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x14, 0x74, 0x66, 0x64, 0x74,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44,
0x74, 0x72, 0x75, 0x6e, 0x01, 0x00, 0x0f, 0x01,
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xe0,
0x00, 0x02, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0xbf, 0x20, 0x00, 0x00, 0x00, 0x05,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x5f, 0x90, 0x00, 0x00, 0x00, 0x05,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x50, 0x74, 0x72, 0x61, 0x66,
0x00, 0x00, 0x00, 0x10, 0x74, 0x66, 0x68, 0x64,
0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x14, 0x74, 0x66, 0x64, 0x74,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x32, 0x80, 0x00, 0x00, 0x00, 0x24,
0x74, 0x72, 0x75, 0x6e, 0x01, 0x00, 0x03, 0x01,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0xfc,
0x00, 0x00, 0x5d, 0xc0, 0x00, 0x00, 0x00, 0x04,
0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, 0x00, 0x04,
0x00, 0x00, 0x00, 0x2c, 0x6d, 0x64, 0x61, 0x74,
0x00, 0x00, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04,
0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00,
0x01, 0x05, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00,
0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x03, 0x04,
0x01, 0x02, 0x03, 0x04,
}
var parts Parts
err := parts.Unmarshal(byts)
require.NoError(t, err)
require.Equal(t, Parts{{
Tracks: []*PartTrack{
{
ID: 1,
Samples: []*PartSample{
{
Duration: 2 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x04,
0x01, 0x02, 0x03, 0x04, // SPS
0x00, 0x00, 0x00, 0x01,
0x08, // PPS
0x00, 0x00, 0x00, 0x01,
0x05, // IDR
},
},
{
Duration: 2 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x01,
0x01, // non-IDR
},
IsNonSyncSample: true,
},
{
Duration: 1 * 90000,
Payload: []byte{
0x00, 0x00, 0x00, 0x01,
0x01, // non-IDR
},
IsNonSyncSample: true,
},
},
},
{
ID: 2,
BaseTime: 3 * 48000,
Samples: []*PartSample{
{
Duration: 500 * 48000 / 1000,
Payload: []byte{
0x01, 0x02, 0x03, 0x04,
},
},
{
Duration: 1 * 48000,
Payload: []byte{
0x01, 0x02, 0x03, 0x04,
},
},
},
},
},
}}, parts)
}

111
internal/hls/fmp4/part_track.go

@ -1,111 +0,0 @@ @@ -1,111 +0,0 @@
package fmp4
import (
gomp4 "github.com/abema/go-mp4"
)
// PartSample is a sample of a PartTrack.
type PartSample struct {
Duration uint32
PTSOffset int32
IsNonSyncSample bool
Payload []byte
}
// PartTrack is a track of Part.
type PartTrack struct {
ID int
BaseTime uint64
Samples []*PartSample
IsVideo bool // marshal only
}
func (pt *PartTrack) marshal(w *mp4Writer) (*gomp4.Trun, int, error) {
/*
traf
- tfhd
- tfdt
- trun
*/
_, err := w.writeBoxStart(&gomp4.Traf{}) // <traf>
if err != nil {
return nil, 0, err
}
flags := 0
_, err = w.WriteBox(&gomp4.Tfhd{ // <tfhd/>
FullBox: gomp4.FullBox{
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
},
TrackID: uint32(pt.ID),
})
if err != nil {
return nil, 0, err
}
_, err = w.WriteBox(&gomp4.Tfdt{ // <tfdt/>
FullBox: gomp4.FullBox{
Version: 1,
},
// sum of decode durations of all earlier samples
BaseMediaDecodeTimeV1: pt.BaseTime,
})
if err != nil {
return nil, 0, err
}
if pt.IsVideo {
flags = trunFlagDataOffsetPreset |
trunFlagSampleDurationPresent |
trunFlagSampleSizePresent |
trunFlagSampleFlagsPresent |
trunFlagSampleCompositionTimeOffsetPresentOrV1
} else {
flags = trunFlagDataOffsetPreset |
trunFlagSampleDurationPresent |
trunFlagSampleSizePresent
}
trun := &gomp4.Trun{ // <trun/>
FullBox: gomp4.FullBox{
Version: 1,
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
},
SampleCount: uint32(len(pt.Samples)),
}
for _, sample := range pt.Samples {
if pt.IsVideo {
var flags uint32
if sample.IsNonSyncSample {
flags |= sampleFlagIsNonSyncSample
}
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
SampleDuration: sample.Duration,
SampleSize: uint32(len(sample.Payload)),
SampleFlags: flags,
SampleCompositionTimeOffsetV1: sample.PTSOffset,
})
} else {
trun.Entries = append(trun.Entries, gomp4.TrunEntry{
SampleDuration: sample.Duration,
SampleSize: uint32(len(sample.Payload)),
})
}
}
trunOffset, err := w.WriteBox(trun)
if err != nil {
return nil, 0, err
}
err = w.writeBoxEnd() // </traf>
if err != nil {
return nil, 0, err
}
return trun, trunOffset, nil
}

106
internal/hls/m3u8/m3u8.go

@ -1,106 +0,0 @@ @@ -1,106 +0,0 @@
// Package m3u8 contains a M3U8 parser.
package m3u8
import (
"bytes"
"errors"
"regexp"
"strings"
gm3u8 "github.com/grafov/m3u8"
)
var reKeyValue = regexp.MustCompile(`([a-zA-Z0-9_-]+)=("[^"]+"|[^",]+)`)
func decodeParamsLine(line string) map[string]string {
out := make(map[string]string)
for _, kv := range reKeyValue.FindAllStringSubmatch(line, -1) {
k, v := kv[1], kv[2]
out[k] = strings.Trim(v, ` "`)
}
return out
}
// MasterPlaylist is a master playlist.
type MasterPlaylist struct {
gm3u8.MasterPlaylist
Alternatives []*gm3u8.Alternative
}
func (MasterPlaylist) isPlaylist() {}
func newMasterPlaylist(byts []byte, mpl *gm3u8.MasterPlaylist) (*MasterPlaylist, error) {
var alternatives []*gm3u8.Alternative
// https://github.com/grafov/m3u8/blob/036100c52a87e26c62be56df85450e9c703201a6/reader.go#L301
for _, line := range strings.Split(string(byts), "\n") {
if strings.HasPrefix(line, "#EXT-X-MEDIA:") {
var alt gm3u8.Alternative
for k, v := range decodeParamsLine(line[13:]) {
switch k {
case "TYPE":
alt.Type = v
case "GROUP-ID":
alt.GroupId = v
case "LANGUAGE":
alt.Language = v
case "NAME":
alt.Name = v
case "DEFAULT":
switch {
case strings.ToUpper(v) == "YES":
alt.Default = true
case strings.ToUpper(v) == "NO":
alt.Default = false
default:
return nil, errors.New("value must be YES or NO")
}
case "AUTOSELECT":
alt.Autoselect = v
case "FORCED":
alt.Forced = v
case "CHARACTERISTICS":
alt.Characteristics = v
case "SUBTITLES":
alt.Subtitles = v
case "URI":
alt.URI = v
}
}
alternatives = append(alternatives, &alt)
}
}
return &MasterPlaylist{
MasterPlaylist: *mpl,
Alternatives: alternatives,
}, nil
}
// MediaPlaylist is a media playlist.
type MediaPlaylist gm3u8.MediaPlaylist
func (MediaPlaylist) isPlaylist() {}
// Playlist is a M3U8 playlist.
type Playlist interface {
isPlaylist()
}
// Unmarshal decodes a M3U8 Playlist.
func Unmarshal(byts []byte) (Playlist, error) {
pl, _, err := gm3u8.Decode(*(bytes.NewBuffer(byts)), true)
if err != nil {
return nil, err
}
switch tpl := pl.(type) {
case *gm3u8.MasterPlaylist:
return newMasterPlaylist(byts, tpl)
case *gm3u8.MediaPlaylist:
return (*MediaPlaylist)(tpl), nil
}
panic("unexpected playlist type")
}

106
internal/hls/mpegts/tracks.go

@ -1,106 +0,0 @@ @@ -1,106 +0,0 @@
package mpegts
import (
"bytes"
"context"
"fmt"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/asticode/go-astits"
)
func findMPEG4AudioConfig(dem *astits.Demuxer, pid uint16) (*mpeg4audio.Config, error) {
for {
data, err := dem.NextData()
if err != nil {
return nil, err
}
if data.PES == nil || data.PID != pid {
continue
}
var adtsPkts mpeg4audio.ADTSPackets
err = adtsPkts.Unmarshal(data.PES.Data)
if err != nil {
return nil, fmt.Errorf("unable to decode ADTS: %s", err)
}
pkt := adtsPkts[0]
return &mpeg4audio.Config{
Type: pkt.Type,
SampleRate: pkt.SampleRate,
ChannelCount: pkt.ChannelCount,
}, nil
}
}
// Track is a MPEG-TS track.
type Track struct {
ES *astits.PMTElementaryStream
Format format.Format
}
// FindTracks finds the tracks in a MPEG-TS stream.
func FindTracks(byts []byte) ([]*Track, error) {
var tracks []*Track
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts))
for {
data, err := dem.NextData()
if err != nil {
return nil, err
}
if data.PMT != nil {
for _, es := range data.PMT.ElementaryStreams {
switch es.StreamType {
case astits.StreamTypeH264Video,
astits.StreamTypeAACAudio:
case astits.StreamTypeMetadata:
continue
default:
return nil, fmt.Errorf("track type %d not supported (yet)", es.StreamType)
}
tracks = append(tracks, &Track{
ES: es,
})
}
break
}
}
if tracks == nil {
return nil, fmt.Errorf("no tracks found")
}
for _, t := range tracks {
switch t.ES.StreamType {
case astits.StreamTypeH264Video:
t.Format = &format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}
case astits.StreamTypeAACAudio:
conf, err := findMPEG4AudioConfig(dem, t.ES.ElementaryPID)
if err != nil {
return nil, err
}
t.Format = &format.MPEG4Audio{
PayloadTyp: 96,
Config: conf,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
}
}
return tracks, nil
}

201
internal/hls/mpegts/writer.go

@ -1,201 +0,0 @@ @@ -1,201 +0,0 @@
// Package mpegts contains a MPEG-TS reader and writer.
package mpegts
import (
"bytes"
"context"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/asticode/go-astits"
)
const (
pcrOffset = 400 * time.Millisecond // 2 samples @ 5fps
)
type writerFunc func(p []byte) (int, error)
func (f writerFunc) Write(p []byte) (int, error) {
return f(p)
}
// Writer is a MPEG-TS writer.
type Writer struct {
videoFormat *format.H264
audioFormat *format.MPEG4Audio
buf *bytes.Buffer
inner *astits.Muxer
pcrCounter int
}
// NewWriter allocates a Writer.
func NewWriter(
videoFormat *format.H264,
audioFormat *format.MPEG4Audio,
) *Writer {
w := &Writer{
videoFormat: videoFormat,
audioFormat: audioFormat,
buf: bytes.NewBuffer(nil),
}
w.inner = astits.NewMuxer(
context.Background(),
writerFunc(func(p []byte) (int, error) {
return w.buf.Write(p)
}))
if videoFormat != nil {
w.inner.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 256,
StreamType: astits.StreamTypeH264Video,
})
}
if audioFormat != nil {
w.inner.AddElementaryStream(astits.PMTElementaryStream{
ElementaryPID: 257,
StreamType: astits.StreamTypeAACAudio,
})
}
if videoFormat != nil {
w.inner.SetPCRPID(256)
} else {
w.inner.SetPCRPID(257)
}
// WriteTable() is not necessary
// since it's called automatically when WriteData() is called with
// * PID == PCRPID
// * AdaptationField != nil
// * RandomAccessIndicator = true
return w
}
// GenerateSegment generates a MPEG-TS segment.
func (w *Writer) GenerateSegment() []byte {
w.pcrCounter = 0
ret := w.buf.Bytes()
w.buf = bytes.NewBuffer(nil)
return ret
}
// WriteH264 writes a H264 access unit.
func (w *Writer) WriteH264(
pcr time.Duration,
dts time.Duration,
pts time.Duration,
idrPresent bool,
nalus [][]byte,
) error {
// prepend an AUD. This is required by video.js and iOS
nalus = append([][]byte{{byte(h264.NALUTypeAccessUnitDelimiter), 240}}, nalus...)
enc, err := h264.AnnexBMarshal(nalus)
if err != nil {
return err
}
var af *astits.PacketAdaptationField
if idrPresent {
af = &astits.PacketAdaptationField{}
af.RandomAccessIndicator = true
}
// send PCR once in a while
if w.pcrCounter == 0 {
if af == nil {
af = &astits.PacketAdaptationField{}
}
af.HasPCR = true
af.PCR = &astits.ClockReference{Base: int64(pcr.Seconds() * 90000)}
w.pcrCounter = 3
}
w.pcrCounter--
oh := &astits.PESOptionalHeader{
MarkerBits: 2,
}
if dts == pts {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS
oh.PTS = &astits.ClockReference{Base: int64((pts + pcrOffset).Seconds() * 90000)}
} else {
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent
oh.DTS = &astits.ClockReference{Base: int64((dts + pcrOffset).Seconds() * 90000)}
oh.PTS = &astits.ClockReference{Base: int64((pts + pcrOffset).Seconds() * 90000)}
}
_, err = w.inner.WriteData(&astits.MuxerData{
PID: 256,
AdaptationField: af,
PES: &astits.PESData{
Header: &astits.PESHeader{
OptionalHeader: oh,
StreamID: 224, // video
},
Data: enc,
},
})
return err
}
// WriteAAC writes an AAC AU.
func (w *Writer) WriteAAC(
pcr time.Duration,
pts time.Duration,
au []byte,
) error {
pkts := mpeg4audio.ADTSPackets{
{
Type: w.audioFormat.Config.Type,
SampleRate: w.audioFormat.Config.SampleRate,
ChannelCount: w.audioFormat.Config.ChannelCount,
AU: au,
},
}
enc, err := pkts.Marshal()
if err != nil {
return err
}
af := &astits.PacketAdaptationField{
RandomAccessIndicator: true,
}
if w.videoFormat == nil {
// send PCR once in a while
if w.pcrCounter == 0 {
af.HasPCR = true
af.PCR = &astits.ClockReference{Base: int64(pcr.Seconds() * 90000)}
w.pcrCounter = 3
}
w.pcrCounter--
}
_, err = w.inner.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 + pcrOffset).Seconds() * 90000)},
},
PacketLength: uint16(len(enc) + 8),
StreamID: 192, // audio
},
Data: enc,
},
})
return err
}

371
internal/hls/mpegts/writer_test.go

@ -1,371 +0,0 @@ @@ -1,371 +0,0 @@
package mpegts
import (
"bytes"
"context"
"testing"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/asticode/go-astits"
"github.com/stretchr/testify/require"
)
func TestWriter(t *testing.T) {
testSPS := []byte{
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20,
}
testVideoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
testAudioTrack := &format.MPEG4Audio{
PayloadTyp: 97,
Config: &mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
type videoSample struct {
NALUs [][]byte
PTS time.Duration
DTS time.Duration
}
type audioSample struct {
AU []byte
PTS time.Duration
}
type sample interface{}
testSamples := []sample{
videoSample{
NALUs: [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
},
PTS: 2 * time.Second,
DTS: 2 * time.Second,
},
audioSample{
AU: []byte{
0x01, 0x02, 0x03, 0x04,
},
PTS: 3 * time.Second,
},
audioSample{
AU: []byte{
0x01, 0x02, 0x03, 0x04,
},
PTS: 3500 * time.Millisecond,
},
videoSample{
NALUs: [][]byte{
{1}, // non-IDR
},
PTS: 4 * time.Second,
DTS: 4 * time.Second,
},
audioSample{
AU: []byte{
0x01, 0x02, 0x03, 0x04,
},
PTS: 4500 * time.Millisecond,
},
videoSample{
NALUs: [][]byte{
{1}, // non-IDR
},
PTS: 6 * time.Second,
DTS: 6 * time.Second,
},
}
t.Run("video + audio", func(t *testing.T) {
w := NewWriter(testVideoTrack, testAudioTrack)
for _, sample := range testSamples {
switch tsample := sample.(type) {
case videoSample:
err := w.WriteH264(
tsample.DTS-2*time.Second,
tsample.DTS,
tsample.PTS,
h264.IDRPresent(tsample.NALUs),
tsample.NALUs)
require.NoError(t, err)
case audioSample:
err := w.WriteAAC(
tsample.PTS-2*time.Second,
tsample.PTS,
tsample.AU)
require.NoError(t, err)
}
}
byts := w.GenerateSegment()
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts),
astits.DemuxerOptPacketSize(188))
// PMT
pkt, err := dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 0,
},
Payload: append([]byte{
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00,
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8,
0x78,
}, bytes.Repeat([]byte{0xff}, 167)...),
}, pkt)
// PAT
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 4096,
},
Payload: append([]byte{
0x00, 0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00,
0x00, 0xe1, 0x00, 0xf0, 0x00, 0x1b, 0xe1, 0x00,
0xf0, 0x00, 0x0f, 0xe1, 0x01, 0xf0, 0x00, 0x2f,
0x44, 0xb9, 0x9b,
}, bytes.Repeat([]byte{0xff}, 157)...),
}, pkt)
// PES (H264)
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
AdaptationField: &astits.PacketAdaptationField{
Length: 124,
StuffingLength: 117,
HasPCR: true,
PCR: &astits.ClockReference{},
RandomAccessIndicator: true,
},
Header: &astits.PacketHeader{
HasAdaptationField: true,
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 256,
},
Payload: []byte{
0x00, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x80, 0x80,
0x05, 0x21, 0x00, 0x0d, 0x97, 0x81, 0x00, 0x00,
0x00, 0x01, 0x09, 0xf0, 0x00, 0x00, 0x00, 0x01,
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00,
0x00, 0x01, 0x05,
},
}, pkt)
// PES (AAC)
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
AdaptationField: &astits.PacketAdaptationField{
Length: 158,
StuffingLength: 157,
RandomAccessIndicator: true,
},
Header: &astits.PacketHeader{
HasAdaptationField: true,
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 257,
},
Payload: []byte{
0x00, 0x00, 0x01, 0xc0, 0x00, 0x13, 0x80, 0x80,
0x05, 0x21, 0x00, 0x13, 0x56, 0xa1, 0xff, 0xf1,
0x50, 0x80, 0x01, 0x7f, 0xfc, 0x01, 0x02, 0x03,
0x04,
},
}, pkt)
})
t.Run("video only", func(t *testing.T) {
w := NewWriter(testVideoTrack, nil)
for _, sample := range testSamples {
if tsample, ok := sample.(videoSample); ok {
err := w.WriteH264(
tsample.DTS-2*time.Second,
tsample.DTS,
tsample.PTS,
h264.IDRPresent(tsample.NALUs),
tsample.NALUs)
require.NoError(t, err)
}
}
byts := w.GenerateSegment()
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts),
astits.DemuxerOptPacketSize(188))
// PMT
pkt, err := dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 0,
},
Payload: append([]byte{
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00,
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8,
0x78,
}, bytes.Repeat([]byte{0xff}, 167)...),
}, pkt)
// PAT
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 4096,
},
Payload: append([]byte{
0x00, 0x02, 0xb0, 0x12, 0x00, 0x01, 0xc1, 0x00,
0x00, 0xe1, 0x00, 0xf0, 0x00, 0x1b, 0xe1, 0x00,
0xf0, 0x00, 0x15, 0xbd, 0x4d, 0x56,
}, bytes.Repeat([]byte{0xff}, 162)...),
}, pkt)
// PES (H264)
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
AdaptationField: &astits.PacketAdaptationField{
Length: 124,
StuffingLength: 117,
HasPCR: true,
PCR: &astits.ClockReference{},
RandomAccessIndicator: true,
},
Header: &astits.PacketHeader{
HasAdaptationField: true,
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 256,
},
Payload: []byte{
0x00, 0x00, 0x01, 0xe0, 0x00, 0x00, 0x80, 0x80,
0x05, 0x21, 0x00, 0x0d, 0x97, 0x81, 0x00, 0x00,
0x00, 0x01, 0x09, 0xf0, 0x00, 0x00, 0x00, 0x01,
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00,
0x00, 0x01, 0x05,
},
}, pkt)
})
t.Run("audio only", func(t *testing.T) {
w := NewWriter(nil, testAudioTrack)
for _, sample := range testSamples {
if tsample, ok := sample.(audioSample); ok {
err := w.WriteAAC(
tsample.PTS-2*time.Second,
tsample.PTS,
tsample.AU)
require.NoError(t, err)
}
}
byts := w.GenerateSegment()
dem := astits.NewDemuxer(context.Background(), bytes.NewReader(byts),
astits.DemuxerOptPacketSize(188))
// PMT
pkt, err := dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 0,
},
Payload: append([]byte{
0x00, 0x00, 0xb0, 0x0d, 0x00, 0x00, 0xc1, 0x00,
0x00, 0x00, 0x01, 0xf0, 0x00, 0x71, 0x10, 0xd8,
0x78,
}, bytes.Repeat([]byte{0xff}, 167)...),
}, pkt)
// PAT
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
Header: &astits.PacketHeader{
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 4096,
},
Payload: append([]byte{
0x00, 0x02, 0xb0, 0x12, 0x00, 0x01, 0xc1, 0x00,
0x00, 0xe1, 0x01, 0xf0, 0x00, 0x0f, 0xe1, 0x01,
0xf0, 0x00, 0xec, 0xe2, 0xb0, 0x94,
}, bytes.Repeat([]byte{0xff}, 162)...),
}, pkt)
// PES (AAC)
pkt, err = dem.NextPacket()
require.NoError(t, err)
require.Equal(t, &astits.Packet{
AdaptationField: &astits.PacketAdaptationField{
Length: 158,
StuffingLength: 151,
RandomAccessIndicator: true,
HasPCR: true,
PCR: &astits.ClockReference{Base: 90000},
},
Header: &astits.PacketHeader{
HasAdaptationField: true,
HasPayload: true,
PayloadUnitStartIndicator: true,
PID: 257,
},
Payload: []byte{
0x00, 0x00, 0x01, 0xc0, 0x00, 0x13, 0x80, 0x80,
0x05, 0x21, 0x00, 0x13, 0x56, 0xa1, 0xff, 0xf1,
0x50, 0x80, 0x01, 0x7f, 0xfc, 0x01, 0x02, 0x03,
0x04,
},
}, pkt)
})
}

46
internal/hls/mpegtstimedec/decoder.go

@ -1,46 +0,0 @@ @@ -1,46 +0,0 @@
// Package mpegtstimedec contains a MPEG-TS timestamp decoder.
package mpegtstimedec
import (
"time"
)
const (
maximum = 0x1FFFFFFFF // 33 bits
negativeThreshold = 0x1FFFFFFFF / 2
clockRate = 90000
)
// Decoder is a MPEG-TS timestamp decoder.
type Decoder struct {
overall time.Duration
prev int64
}
// New allocates a Decoder.
func New(start int64) *Decoder {
return &Decoder{
prev: start,
}
}
// Decode decodes a MPEG-TS timestamp.
func (d *Decoder) Decode(ts int64) time.Duration {
diff := (ts - d.prev) & maximum
// negative difference
if diff > negativeThreshold {
diff = (d.prev - ts) & maximum
d.prev = ts
d.overall -= time.Duration(diff)
} else {
d.prev = ts
d.overall += time.Duration(diff)
}
// avoid an int64 overflow and preserve resolution by splitting division into two parts:
// first add the integer part, then the decimal part.
secs := d.overall / clockRate
dec := d.overall % clockRate
return secs*time.Second + dec*time.Second/clockRate
}

72
internal/hls/mpegtstimedec/decoder_test.go

@ -1,72 +0,0 @@ @@ -1,72 +0,0 @@
package mpegtstimedec
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestNegativeDiff(t *testing.T) {
d := New(64523434)
ts := d.Decode(64523434 - 90000)
require.Equal(t, -1*time.Second, ts)
ts = d.Decode(64523434)
require.Equal(t, time.Duration(0), ts)
ts = d.Decode(64523434 + 90000*2)
require.Equal(t, 2*time.Second, ts)
ts = d.Decode(64523434 + 90000)
require.Equal(t, 1*time.Second, ts)
}
func TestOverflow(t *testing.T) {
d := New(0x1FFFFFFFF - 20)
i := int64(0x1FFFFFFFF - 20)
secs := time.Duration(0)
const stride = 150
lim := int64(uint64(0x1FFFFFFFF - (stride * 90000)))
for n := 0; n < 100; n++ {
// overflow
i += 90000 * stride
secs += stride
ts := d.Decode(i)
require.Equal(t, secs*time.Second, ts)
// reach 2^32 slowly
secs += stride
i += 90000 * stride
for ; i < lim; i += 90000 * stride {
ts = d.Decode(i)
require.Equal(t, secs*time.Second, ts)
secs += stride
}
}
}
func TestOverflowAndBack(t *testing.T) {
d := New(0x1FFFFFFFF - 90000 + 1)
ts := d.Decode(0x1FFFFFFFF - 90000 + 1)
require.Equal(t, time.Duration(0), ts)
ts = d.Decode(90000)
require.Equal(t, 2*time.Second, ts)
ts = d.Decode(0x1FFFFFFFF - 90000 + 1)
require.Equal(t, time.Duration(0), ts)
ts = d.Decode(0x1FFFFFFFF - 90000*2 + 1)
require.Equal(t, -1*time.Second, ts)
ts = d.Decode(0x1FFFFFFFF - 90000 + 1)
require.Equal(t, time.Duration(0), ts)
ts = d.Decode(90000)
require.Equal(t, 2*time.Second, ts)
}

100
internal/hls/muxer.go

@ -1,100 +0,0 @@ @@ -1,100 +0,0 @@
// Package hls contains a HLS muxer and client.
package hls
import (
"io"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
)
// MuxerFileResponse is a response of the Muxer's File() func.
type MuxerFileResponse struct {
Status int
Header map[string]string
Body io.Reader
}
// Muxer is a HLS muxer.
type Muxer struct {
primaryPlaylist *muxerPrimaryPlaylist
variant muxerVariant
}
// NewMuxer allocates a Muxer.
func NewMuxer(
variant MuxerVariant,
segmentCount int,
segmentDuration time.Duration,
partDuration time.Duration,
segmentMaxSize uint64,
videoTrack format.Format,
audioTrack format.Format,
) (*Muxer, error) {
m := &Muxer{}
switch variant {
case MuxerVariantMPEGTS:
var err error
m.variant, err = newMuxerVariantMPEGTS(
segmentCount,
segmentDuration,
segmentMaxSize,
videoTrack,
audioTrack,
)
if err != nil {
return nil, err
}
case MuxerVariantFMP4:
m.variant = newMuxerVariantFMP4(
false,
segmentCount,
segmentDuration,
partDuration,
segmentMaxSize,
videoTrack,
audioTrack,
)
default: // MuxerVariantLowLatency
m.variant = newMuxerVariantFMP4(
true,
segmentCount,
segmentDuration,
partDuration,
segmentMaxSize,
videoTrack,
audioTrack,
)
}
m.primaryPlaylist = newMuxerPrimaryPlaylist(variant != MuxerVariantMPEGTS, videoTrack, audioTrack)
return m, nil
}
// Close closes a Muxer.
func (m *Muxer) Close() {
m.variant.close()
}
// WriteH26x writes an H264 or an H265 access unit.
func (m *Muxer) WriteH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
return m.variant.writeH26x(ntp, pts, au)
}
// WriteAudio writes an audio access unit.
func (m *Muxer) WriteAudio(ntp time.Time, pts time.Duration, au []byte) error {
return m.variant.writeAudio(ntp, pts, au)
}
// File returns a file reader.
func (m *Muxer) File(name string, msn string, part string, skip string) *MuxerFileResponse {
if name == "index.m3u8" {
return m.primaryPlaylist.file()
}
return m.variant.file(name, msn, part, skip)
}

62
internal/hls/muxer_primary_playlist.go

@ -1,62 +0,0 @@ @@ -1,62 +0,0 @@
package hls
import (
"bytes"
"io"
"net/http"
"strconv"
"strings"
"github.com/aler9/gortsplib/v2/pkg/format"
)
type muxerPrimaryPlaylist struct {
fmp4 bool
videoTrack format.Format
audioTrack format.Format
}
func newMuxerPrimaryPlaylist(
fmp4 bool,
videoTrack format.Format,
audioTrack format.Format,
) *muxerPrimaryPlaylist {
return &muxerPrimaryPlaylist{
fmp4: fmp4,
videoTrack: videoTrack,
audioTrack: audioTrack,
}
}
func (p *muxerPrimaryPlaylist) file() *MuxerFileResponse {
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": `application/x-mpegURL`,
},
Body: func() io.Reader {
var codecs []string
if p.videoTrack != nil {
codecs = append(codecs, codecParametersGenerate(p.videoTrack))
}
if p.audioTrack != nil {
codecs = append(codecs, codecParametersGenerate(p.audioTrack))
}
var version int
if !p.fmp4 {
version = 3
} else {
version = 9
}
return bytes.NewReader([]byte("#EXTM3U\n" +
"#EXT-X-VERSION:" + strconv.FormatInt(int64(version), 10) + "\n" +
"#EXT-X-INDEPENDENT-SEGMENTS\n" +
"\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"" + strings.Join(codecs, ",") + "\"\n" +
"stream.m3u8\n"))
}(),
}
}

595
internal/hls/muxer_test.go

@ -1,595 +0,0 @@ @@ -1,595 +0,0 @@
package hls
import (
"io"
"net/http"
"regexp"
"testing"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/stretchr/testify/require"
)
var testTime = time.Date(2010, 0o1, 0o1, 0o1, 0o1, 0o1, 0, time.UTC)
// baseline profile without POC
var testSPS = []byte{
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9,
0x20,
}
func TestMuxerVideoAudio(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
audioTrack := &format.MPEG4Audio{
PayloadTyp: 97,
Config: &mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
for _, ca := range []string{
"mpegts",
"fmp4",
"lowLatency",
} {
t.Run(ca, func(t *testing.T) {
var v MuxerVariant
switch ca {
case "mpegts":
v = MuxerVariantMPEGTS
case "fmp4":
v = MuxerVariantFMP4
case "lowLatency":
v = MuxerVariantLowLatency
}
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, audioTrack)
require.NoError(t, err)
defer m.Close()
// access unit without IDR
d := 1 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
{0x06},
{0x07},
})
require.NoError(t, err)
// access unit with IDR
d = 2 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
})
require.NoError(t, err)
d = 3 * time.Second
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
d = 3500 * time.Millisecond
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
// access unit without IDR
d = 4 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
{1}, // non-IDR
})
require.NoError(t, err)
d = 4500 * time.Millisecond
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
// access unit with IDR
d = 6 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
{5}, // IDR
})
require.NoError(t, err)
// access unit with IDR
d = 7 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
{5}, // IDR
})
require.NoError(t, err)
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body)
require.NoError(t, err)
switch ca {
case "mpegts":
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:3\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028,mp4a.40.2\"\n"+
"stream.m3u8\n", string(byts))
case "fmp4", "lowLatency":
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:9\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028,mp4a.40.2\"\n"+
"stream.m3u8\n", string(byts))
}
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body)
require.NoError(t, err)
switch ca {
case "mpegts":
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:3\n` +
`#EXT-X-ALLOW-CACHE:NO\n` +
`#EXT-X-TARGETDURATION:4\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:4,\n` +
`(seg0\.ts)\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:1,\n` +
`(seg1\.ts)\n$`)
ma := re.FindStringSubmatch(string(byts))
require.NotEqual(t, 0, len(ma))
seg := m.File(ma[2], "", "", "")
require.Equal(t, http.StatusOK, seg.Status)
_, err := io.ReadAll(seg.Body)
require.NoError(t, err)
case "fmp4":
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:9\n` +
`#EXT-X-TARGETDURATION:4\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-MAP:URI="init.mp4"\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:4.00000,\n` +
`(seg0\.mp4)\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:1.00000,\n` +
`(seg1\.mp4)\n$`)
ma := re.FindStringSubmatch(string(byts))
require.NotEqual(t, 0, len(ma))
init := m.File("init.mp4", "", "", "")
require.Equal(t, http.StatusOK, init.Status)
_, err := io.ReadAll(init.Body)
require.NoError(t, err)
seg := m.File(ma[2], "", "", "")
require.Equal(t, http.StatusOK, seg.Status)
_, err = io.ReadAll(seg.Body)
require.NoError(t, err)
case "lowLatency":
require.Equal(t,
"#EXTM3U\n"+
"#EXT-X-VERSION:9\n"+
"#EXT-X-TARGETDURATION:4\n"+
"#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=5.00000,CAN-SKIP-UNTIL=24\n"+
"#EXT-X-PART-INF:PART-TARGET=2\n"+
"#EXT-X-MEDIA-SEQUENCE:2\n"+
"#EXT-X-MAP:URI=\"init.mp4\"\n"+
"#EXT-X-GAP\n"+
"#EXTINF:4.00000,\n"+
"gap.mp4\n"+
"#EXT-X-GAP\n"+
"#EXTINF:4.00000,\n"+
"gap.mp4\n"+
"#EXT-X-GAP\n"+
"#EXTINF:4.00000,\n"+
"gap.mp4\n"+
"#EXT-X-GAP\n"+
"#EXTINF:4.00000,\n"+
"gap.mp4\n"+
"#EXT-X-GAP\n"+
"#EXTINF:4.00000,\n"+
"gap.mp4\n"+
"#EXT-X-PROGRAM-DATE-TIME:2010-01-01T01:01:02Z\n"+
"#EXT-X-PART:DURATION=2.00000,URI=\"part0.mp4\",INDEPENDENT=YES\n"+
"#EXT-X-PART:DURATION=2.00000,URI=\"part1.mp4\"\n"+
"#EXTINF:4.00000,\n"+
"seg7.mp4\n"+
"#EXT-X-PROGRAM-DATE-TIME:2010-01-01T01:01:06Z\n"+
"#EXT-X-PART:DURATION=1.00000,URI=\"part3.mp4\",INDEPENDENT=YES\n"+
"#EXTINF:1.00000,\n"+
"seg8.mp4\n"+
"#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"part4.mp4\"\n", string(byts))
part := m.File("part3.mp4", "", "", "")
require.Equal(t, http.StatusOK, part.Status)
_, err = io.ReadAll(part.Body)
require.NoError(t, err)
recv := make(chan struct{})
go func() {
part = m.File("part4.mp4", "", "", "")
_, err := io.ReadAll(part.Body)
require.NoError(t, err)
close(recv)
}()
d = 9 * time.Second
err = m.WriteH26x(testTime.Add(d-1*time.Second), d, [][]byte{
{1}, // non-IDR
})
require.NoError(t, err)
<-recv
}
})
}
}
func TestMuxerVideoOnly(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
for _, ca := range []string{
"mpegts",
"fmp4",
} {
t.Run(ca, func(t *testing.T) {
var v MuxerVariant
if ca == "mpegts" {
v = MuxerVariantMPEGTS
} else {
v = MuxerVariantFMP4
}
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
require.NoError(t, err)
defer m.Close()
// access unit with IDR
d := 2 * time.Second
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
})
require.NoError(t, err)
// access unit with IDR
d = 6 * time.Second
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
{5}, // IDR
})
require.NoError(t, err)
// access unit with IDR
d = 7 * time.Second
err = m.WriteH26x(testTime.Add(d-2*time.Second), d, [][]byte{
{5}, // IDR
})
require.NoError(t, err)
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body)
require.NoError(t, err)
if ca == "mpegts" {
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:3\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028\"\n"+
"stream.m3u8\n", string(byts))
} else {
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:9\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"avc1.42c028\"\n"+
"stream.m3u8\n", string(byts))
}
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body)
require.NoError(t, err)
var ma []string
if ca == "mpegts" {
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:3\n` +
`#EXT-X-ALLOW-CACHE:NO\n` +
`#EXT-X-TARGETDURATION:4\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:4,\n` +
`(seg0\.ts)\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:1,\n` +
`(seg1\.ts)\n$`)
ma = re.FindStringSubmatch(string(byts))
} else {
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:9\n` +
`#EXT-X-TARGETDURATION:4\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-MAP:URI="init.mp4"\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:4.00000,\n` +
`(seg0\.mp4)\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:1.00000,\n` +
`(seg1\.mp4)\n$`)
ma = re.FindStringSubmatch(string(byts))
}
require.NotEqual(t, 0, len(ma))
if ca == "mpegts" {
_, err := io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
} else {
_, err := io.ReadAll(m.File("init.mp4", "", "", "").Body)
require.NoError(t, err)
_, err = io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
}
})
}
}
func TestMuxerAudioOnly(t *testing.T) {
audioTrack := &format.MPEG4Audio{
PayloadTyp: 97,
Config: &mpeg4audio.Config{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
},
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
}
for _, ca := range []string{
"mpegts",
"fmp4",
} {
t.Run(ca, func(t *testing.T) {
var v MuxerVariant
if ca == "mpegts" {
v = MuxerVariantMPEGTS
} else {
v = MuxerVariantFMP4
}
m, err := NewMuxer(v, 3, 1*time.Second, 0, 50*1024*1024, nil, audioTrack)
require.NoError(t, err)
defer m.Close()
for i := 0; i < 100; i++ {
d := 1 * time.Second
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
}
d := 2 * time.Second
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
d = 3 * time.Second
err = m.WriteAudio(testTime.Add(d-1*time.Second), d, []byte{
0x01, 0x02, 0x03, 0x04,
})
require.NoError(t, err)
byts, err := io.ReadAll(m.File("index.m3u8", "", "", "").Body)
require.NoError(t, err)
if ca == "mpegts" {
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:3\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"mp4a.40.2\"\n"+
"stream.m3u8\n", string(byts))
} else {
require.Equal(t, "#EXTM3U\n"+
"#EXT-X-VERSION:9\n"+
"#EXT-X-INDEPENDENT-SEGMENTS\n"+
"\n"+
"#EXT-X-STREAM-INF:BANDWIDTH=200000,CODECS=\"mp4a.40.2\"\n"+
"stream.m3u8\n", string(byts))
}
byts, err = io.ReadAll(m.File("stream.m3u8", "", "", "").Body)
require.NoError(t, err)
var ma []string
if ca == "mpegts" {
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:3\n` +
`#EXT-X-ALLOW-CACHE:NO\n` +
`#EXT-X-TARGETDURATION:1\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:1,\n` +
`(seg0\.ts)\n$`)
ma = re.FindStringSubmatch(string(byts))
} else {
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:9\n` +
`#EXT-X-TARGETDURATION:2\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-MAP:URI="init.mp4"\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:2.32200,\n` +
`(seg0\.mp4)\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:0.02322,\n` +
`(seg1\.mp4)\n$`)
ma = re.FindStringSubmatch(string(byts))
}
require.NotEqual(t, 0, len(ma))
if ca == "mpegts" {
_, err := io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
} else {
_, err := io.ReadAll(m.File("init.mp4", "", "", "").Body)
require.NoError(t, err)
_, err = io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
}
})
}
}
func TestMuxerCloseBeforeFirstSegmentReader(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
require.NoError(t, err)
// access unit with IDR
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
})
require.NoError(t, err)
m.Close()
b := m.File("stream.m3u8", "", "", "").Body
require.Equal(t, nil, b)
}
func TestMuxerMaxSegmentSize(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 0, videoTrack, nil)
require.NoError(t, err)
defer m.Close()
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
testSPS,
{5}, // IDR
})
require.EqualError(t, err, "reached maximum segment size")
}
func TestMuxerDoubleRead(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
m, err := NewMuxer(MuxerVariantMPEGTS, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
require.NoError(t, err)
defer m.Close()
err = m.WriteH26x(testTime, 0, [][]byte{
testSPS,
{5}, // IDR
{1},
})
require.NoError(t, err)
err = m.WriteH26x(testTime, 2*time.Second, [][]byte{
{5}, // IDR
{2},
})
require.NoError(t, err)
byts, err := io.ReadAll(m.File("stream.m3u8", "", "", "").Body)
require.NoError(t, err)
re := regexp.MustCompile(`^#EXTM3U\n` +
`#EXT-X-VERSION:3\n` +
`#EXT-X-ALLOW-CACHE:NO\n` +
`#EXT-X-TARGETDURATION:2\n` +
`#EXT-X-MEDIA-SEQUENCE:0\n` +
`#EXT-X-PROGRAM-DATE-TIME:(.*?)\n` +
`#EXTINF:2,\n` +
`(seg0\.ts)\n$`)
ma := re.FindStringSubmatch(string(byts))
require.NotEqual(t, 0, len(ma))
byts1, err := io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
byts2, err := io.ReadAll(m.File(ma[2], "", "", "").Body)
require.NoError(t, err)
require.Equal(t, byts1, byts2)
}
func TestMuxerFMP4ZeroDuration(t *testing.T) {
videoTrack := &format.H264{
PayloadTyp: 96,
SPS: testSPS,
PPS: []byte{0x08},
PacketizationMode: 1,
}
m, err := NewMuxer(MuxerVariantLowLatency, 3, 1*time.Second, 0, 50*1024*1024, videoTrack, nil)
require.NoError(t, err)
defer m.Close()
err = m.WriteH26x(time.Now(), 0, [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
})
require.NoError(t, err)
err = m.WriteH26x(time.Now(), 1*time.Nanosecond, [][]byte{
testSPS, // SPS
{8}, // PPS
{5}, // IDR
})
require.NoError(t, err)
}

22
internal/hls/muxer_variant.go

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
package hls
import (
"time"
)
// MuxerVariant is a muxer variant.
type MuxerVariant int
// supported variants.
const (
MuxerVariantMPEGTS MuxerVariant = iota
MuxerVariantFMP4
MuxerVariantLowLatency
)
type muxerVariant interface {
close()
writeH26x(time.Time, time.Duration, [][]byte) error
writeAudio(time.Time, time.Duration, []byte) error
file(name string, msn string, part string, skip string) *MuxerFileResponse
}

163
internal/hls/muxer_variant_fmp4.go

@ -1,163 +0,0 @@ @@ -1,163 +0,0 @@
package hls
import (
"bytes"
"net/http"
"sync"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
"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 {
playlist *muxerVariantFMP4Playlist
segmenter *muxerVariantFMP4Segmenter
videoTrack format.Format
audioTrack format.Format
mutex sync.Mutex
lastVideoParams [][]byte
initContent []byte
}
func newMuxerVariantFMP4(
lowLatency bool,
segmentCount int,
segmentDuration time.Duration,
partDuration time.Duration,
segmentMaxSize uint64,
videoTrack format.Format,
audioTrack format.Format,
) *muxerVariantFMP4 {
v := &muxerVariantFMP4{
videoTrack: videoTrack,
audioTrack: audioTrack,
}
v.playlist = newMuxerVariantFMP4Playlist(
lowLatency,
segmentCount,
videoTrack,
audioTrack,
)
v.segmenter = newMuxerVariantFMP4Segmenter(
lowLatency,
segmentCount,
segmentDuration,
partDuration,
segmentMaxSize,
videoTrack,
audioTrack,
v.playlist.onSegmentFinalized,
v.playlist.onPartFinalized,
)
return v
}
func (v *muxerVariantFMP4) close() {
v.playlist.close()
}
func (v *muxerVariantFMP4) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
return v.segmenter.writeH26x(ntp, pts, au)
}
func (v *muxerVariantFMP4) writeAudio(ntp time.Time, pts time.Duration, au []byte) error {
return v.segmenter.writeAudio(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 {
if name == "init.mp4" {
v.mutex.Lock()
defer v.mutex.Unlock()
if v.initContent == nil || v.mustRegenerateInit() {
init := fmp4.Init{}
trackID := 1
if v.videoTrack != nil {
init.Tracks = append(init.Tracks, &fmp4.InitTrack{
ID: trackID,
TimeScale: 90000,
Format: v.videoTrack,
})
trackID++
}
if v.audioTrack != nil {
init.Tracks = append(init.Tracks, &fmp4.InitTrack{
ID: trackID,
TimeScale: uint32(v.audioTrack.ClockRate()),
Format: v.audioTrack,
})
}
var err error
v.initContent, err = init.Marshal()
if err != nil {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": "video/mp4",
},
Body: bytes.NewReader(v.initContent),
}
}
return v.playlist.file(name, msn, part, skip)
}

140
internal/hls/muxer_variant_fmp4_part.go

@ -1,140 +0,0 @@ @@ -1,140 +0,0 @@
package hls
import (
"bytes"
"io"
"strconv"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/rtsp-simple-server/internal/hls/fmp4"
)
func fmp4PartName(id uint64) string {
return "part" + strconv.FormatUint(id, 10)
}
type muxerVariantFMP4Part struct {
videoTrack format.Format
audioTrack format.Format
id uint64
isIndependent bool
videoSamples []*fmp4.PartSample
audioSamples []*fmp4.PartSample
content []byte
renderedDuration time.Duration
videoStartDTSFilled bool
videoStartDTS time.Duration
audioStartDTSFilled bool
audioStartDTS time.Duration
}
func newMuxerVariantFMP4Part(
videoTrack format.Format,
audioTrack format.Format,
id uint64,
) *muxerVariantFMP4Part {
p := &muxerVariantFMP4Part{
videoTrack: videoTrack,
audioTrack: audioTrack,
id: id,
}
if videoTrack == nil {
p.isIndependent = true
}
return p
}
func (p *muxerVariantFMP4Part) name() string {
return fmp4PartName(p.id)
}
func (p *muxerVariantFMP4Part) reader() io.Reader {
return bytes.NewReader(p.content)
}
func (p *muxerVariantFMP4Part) duration() time.Duration {
if p.videoTrack != nil {
ret := uint64(0)
for _, e := range p.videoSamples {
ret += uint64(e.Duration)
}
return durationMp4ToGo(ret, 90000)
}
// use the sum of the default duration of all samples,
// not the real duration,
// otherwise on iPhone iOS the stream freezes.
return time.Duration(len(p.audioSamples)) * time.Second *
time.Duration(mpeg4audio.SamplesPerAccessUnit) / time.Duration(p.audioTrack.ClockRate())
}
func (p *muxerVariantFMP4Part) finalize() error {
if p.videoSamples != nil || p.audioSamples != nil {
part := fmp4.Part{}
if p.videoSamples != nil {
part.Tracks = append(part.Tracks, &fmp4.PartTrack{
ID: 1,
BaseTime: durationGoToMp4(p.videoStartDTS, 90000),
Samples: p.videoSamples,
IsVideo: true,
})
}
if p.audioSamples != nil {
var id int
if p.videoTrack != nil {
id = 2
} else {
id = 1
}
part.Tracks = append(part.Tracks, &fmp4.PartTrack{
ID: id,
BaseTime: durationGoToMp4(p.audioStartDTS, uint32(p.audioTrack.ClockRate())),
Samples: p.audioSamples,
})
}
var err error
p.content, err = part.Marshal()
if err != nil {
return err
}
p.renderedDuration = p.duration()
}
p.videoSamples = nil
p.audioSamples = nil
return nil
}
func (p *muxerVariantFMP4Part) writeH264(sample *augmentedVideoSample) {
if !p.videoStartDTSFilled {
p.videoStartDTSFilled = true
p.videoStartDTS = sample.dts
}
if !sample.IsNonSyncSample {
p.isIndependent = true
}
p.videoSamples = append(p.videoSamples, &sample.PartSample)
}
func (p *muxerVariantFMP4Part) writeAudio(sample *augmentedAudioSample) {
if !p.audioStartDTSFilled {
p.audioStartDTSFilled = true
p.audioStartDTS = sample.dts
}
p.audioSamples = append(p.audioSamples, &sample.PartSample)
}

489
internal/hls/muxer_variant_fmp4_playlist.go

@ -1,489 +0,0 @@ @@ -1,489 +0,0 @@
package hls
import (
"bytes"
"io"
"math"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
)
type muxerVariantFMP4SegmentOrGap interface {
getRenderedDuration() time.Duration
}
type muxerVariantFMP4Gap struct {
renderedDuration time.Duration
}
func (g muxerVariantFMP4Gap) getRenderedDuration() time.Duration {
return g.renderedDuration
}
func targetDuration(segments []muxerVariantFMP4SegmentOrGap) uint {
ret := uint(0)
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
for _, sog := range segments {
v := uint(math.Round(sog.getRenderedDuration().Seconds()))
if v > ret {
ret = v
}
}
return ret
}
func partTargetDuration(
segments []muxerVariantFMP4SegmentOrGap,
nextSegmentParts []*muxerVariantFMP4Part,
) time.Duration {
var ret time.Duration
for _, sog := range segments {
seg, ok := sog.(*muxerVariantFMP4Segment)
if !ok {
continue
}
for _, part := range seg.parts {
if part.renderedDuration > ret {
ret = part.renderedDuration
}
}
}
for _, part := range nextSegmentParts {
if part.renderedDuration > ret {
ret = part.renderedDuration
}
}
return ret
}
type muxerVariantFMP4Playlist struct {
lowLatency bool
segmentCount int
videoTrack format.Format
audioTrack format.Format
mutex sync.Mutex
cond *sync.Cond
closed bool
segments []muxerVariantFMP4SegmentOrGap
segmentsByName map[string]*muxerVariantFMP4Segment
segmentDeleteCount int
parts []*muxerVariantFMP4Part
partsByName map[string]*muxerVariantFMP4Part
nextSegmentID uint64
nextSegmentParts []*muxerVariantFMP4Part
nextPartID uint64
}
func newMuxerVariantFMP4Playlist(
lowLatency bool,
segmentCount int,
videoTrack format.Format,
audioTrack format.Format,
) *muxerVariantFMP4Playlist {
p := &muxerVariantFMP4Playlist{
lowLatency: lowLatency,
segmentCount: segmentCount,
videoTrack: videoTrack,
audioTrack: audioTrack,
segmentsByName: make(map[string]*muxerVariantFMP4Segment),
partsByName: make(map[string]*muxerVariantFMP4Part),
}
p.cond = sync.NewCond(&p.mutex)
return p
}
func (p *muxerVariantFMP4Playlist) close() {
func() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.closed = true
}()
p.cond.Broadcast()
}
func (p *muxerVariantFMP4Playlist) hasContent() bool {
if p.lowLatency {
return len(p.segments) >= 1
}
return len(p.segments) >= 2
}
func (p *muxerVariantFMP4Playlist) hasPart(segmentID uint64, partID uint64) bool {
if !p.hasContent() {
return false
}
for _, sop := range p.segments {
seg, ok := sop.(*muxerVariantFMP4Segment)
if !ok {
continue
}
if segmentID != seg.id {
continue
}
// If the Client requests a Part Index greater than that of the final
// Partial Segment of the Parent Segment, the Server MUST treat the
// request as one for Part Index 0 of the following Parent Segment.
if partID >= uint64(len(seg.parts)) {
segmentID++
partID = 0
continue
}
return true
}
if segmentID != p.nextSegmentID {
return false
}
if partID >= uint64(len(p.nextSegmentParts)) {
return false
}
return true
}
func (p *muxerVariantFMP4Playlist) file(name string, msn string, part string, skip string) *MuxerFileResponse {
switch {
case name == "stream.m3u8":
return p.playlistReader(msn, part, skip)
case strings.HasSuffix(name, ".mp4"):
return p.segmentReader(name)
default:
return &MuxerFileResponse{Status: http.StatusNotFound}
}
}
func (p *muxerVariantFMP4Playlist) playlistReader(msn string, part string, skip string) *MuxerFileResponse {
isDeltaUpdate := false
if p.lowLatency {
isDeltaUpdate = skip == "YES" || skip == "v2"
var msnint uint64
if msn != "" {
var err error
msnint, err = strconv.ParseUint(msn, 10, 64)
if err != nil {
return &MuxerFileResponse{Status: http.StatusBadRequest}
}
}
var partint uint64
if part != "" {
var err error
partint, err = strconv.ParseUint(part, 10, 64)
if err != nil {
return &MuxerFileResponse{Status: http.StatusBadRequest}
}
}
if msn != "" {
p.mutex.Lock()
defer p.mutex.Unlock()
// If the _HLS_msn is greater than the Media Sequence Number of the last
// Media Segment in the current Playlist plus two, or if the _HLS_part
// exceeds the last Partial Segment in the current Playlist by the
// Advance Part Limit, then the server SHOULD immediately return Bad
// Request, such as HTTP 400.
if msnint > (p.nextSegmentID + 1) {
return &MuxerFileResponse{Status: http.StatusBadRequest}
}
for !p.closed && !p.hasPart(msnint, partint) {
p.cond.Wait()
}
if p.closed {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": `application/x-mpegURL`,
},
Body: p.fullPlaylist(isDeltaUpdate),
}
}
// part without msn is not supported.
if part != "" {
return &MuxerFileResponse{Status: http.StatusBadRequest}
}
}
p.mutex.Lock()
defer p.mutex.Unlock()
for !p.closed && !p.hasContent() {
p.cond.Wait()
}
if p.closed {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": `application/x-mpegURL`,
},
Body: p.fullPlaylist(isDeltaUpdate),
}
}
func (p *muxerVariantFMP4Playlist) fullPlaylist(isDeltaUpdate bool) io.Reader {
cnt := "#EXTM3U\n"
cnt += "#EXT-X-VERSION:9\n"
targetDuration := targetDuration(p.segments)
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n"
skipBoundary := float64(targetDuration * 6)
if p.lowLatency {
partTargetDuration := partTargetDuration(p.segments, p.nextSegmentParts)
// The value is an enumerated-string whose value is YES if the server
// supports Blocking Playlist Reload
cnt += "#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES"
// The value is a decimal-floating-point number of seconds that
// indicates the server-recommended minimum distance from the end of
// the Playlist at which clients should begin to play or to which
// they should seek when playing in Low-Latency Mode. Its value MUST
// be at least twice the Part Target Duration. Its value SHOULD be
// at least three times the Part Target Duration.
cnt += ",PART-HOLD-BACK=" + strconv.FormatFloat((partTargetDuration).Seconds()*2.5, 'f', 5, 64)
// Indicates that the Server can produce Playlist Delta Updates in
// response to the _HLS_skip Delivery Directive. Its value is the
// Skip Boundary, a decimal-floating-point number of seconds. The
// Skip Boundary MUST be at least six times the Target Duration.
cnt += ",CAN-SKIP-UNTIL=" + strconv.FormatFloat(skipBoundary, 'f', -1, 64)
cnt += "\n"
cnt += "#EXT-X-PART-INF:PART-TARGET=" + strconv.FormatFloat(partTargetDuration.Seconds(), 'f', -1, 64) + "\n"
}
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n"
skipped := 0
if !isDeltaUpdate {
cnt += "#EXT-X-MAP:URI=\"init.mp4\"\n"
} else {
var curDuration time.Duration
shown := 0
for _, segment := range p.segments {
curDuration += segment.getRenderedDuration()
if curDuration.Seconds() >= skipBoundary {
break
}
shown++
}
skipped = len(p.segments) - shown
cnt += "#EXT-X-SKIP:SKIPPED-SEGMENTS=" + strconv.FormatInt(int64(skipped), 10) + "\n"
}
for i, sog := range p.segments {
if i < skipped {
continue
}
switch seg := sog.(type) {
case *muxerVariantFMP4Segment:
if (len(p.segments) - i) <= 2 {
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + seg.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n"
}
if p.lowLatency && (len(p.segments)-i) <= 2 {
for _, part := range seg.parts {
cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) +
",URI=\"" + part.name() + ".mp4\""
if part.isIndependent {
cnt += ",INDEPENDENT=YES"
}
cnt += "\n"
}
}
cnt += "#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" +
seg.name + ".mp4\n"
case *muxerVariantFMP4Gap:
cnt += "#EXT-X-GAP\n" +
"#EXTINF:" + strconv.FormatFloat(seg.renderedDuration.Seconds(), 'f', 5, 64) + ",\n" +
"gap.mp4\n"
}
}
if p.lowLatency {
for _, part := range p.nextSegmentParts {
cnt += "#EXT-X-PART:DURATION=" + strconv.FormatFloat(part.renderedDuration.Seconds(), 'f', 5, 64) +
",URI=\"" + part.name() + ".mp4\""
if part.isIndependent {
cnt += ",INDEPENDENT=YES"
}
cnt += "\n"
}
// preload hint must always be present
// otherwise hls.js goes into a loop
cnt += "#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"" + fmp4PartName(p.nextPartID) + ".mp4\"\n"
}
return bytes.NewReader([]byte(cnt))
}
func (p *muxerVariantFMP4Playlist) segmentReader(fname string) *MuxerFileResponse {
switch {
case strings.HasPrefix(fname, "seg"):
base := strings.TrimSuffix(fname, ".mp4")
p.mutex.Lock()
segment, ok := p.segmentsByName[base]
p.mutex.Unlock()
if !ok {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": "video/mp4",
},
Body: segment.reader(),
}
case strings.HasPrefix(fname, "part"):
base := strings.TrimSuffix(fname, ".mp4")
p.mutex.Lock()
part, ok := p.partsByName[base]
nextPartID := p.nextPartID
p.mutex.Unlock()
if ok {
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": "video/mp4",
},
Body: part.reader(),
}
}
// EXT-X-PRELOAD-HINT support
nextPartName := fmp4PartName(p.nextPartID)
if base == nextPartName {
p.mutex.Lock()
defer p.mutex.Unlock()
for {
if p.closed {
break
}
if p.nextPartID > nextPartID {
break
}
p.cond.Wait()
}
if p.closed {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": "video/mp4",
},
Body: p.partsByName[nextPartName].reader(),
}
}
return &MuxerFileResponse{Status: http.StatusNotFound}
default:
return &MuxerFileResponse{Status: http.StatusNotFound}
}
}
func (p *muxerVariantFMP4Playlist) onSegmentFinalized(segment *muxerVariantFMP4Segment) {
func() {
p.mutex.Lock()
defer p.mutex.Unlock()
// add initial gaps, required by iOS LL-HLS
if p.lowLatency && len(p.segments) == 0 {
for i := 0; i < 7; i++ {
p.segments = append(p.segments, &muxerVariantFMP4Gap{
renderedDuration: segment.renderedDuration,
})
}
}
p.segmentsByName[segment.name] = segment
p.segments = append(p.segments, segment)
p.nextSegmentID = segment.id + 1
p.nextSegmentParts = p.nextSegmentParts[:0]
if len(p.segments) > p.segmentCount {
toDelete := p.segments[0]
if toDeleteSeg, ok := toDelete.(*muxerVariantFMP4Segment); ok {
for _, part := range toDeleteSeg.parts {
delete(p.partsByName, part.name())
}
p.parts = p.parts[len(toDeleteSeg.parts):]
delete(p.segmentsByName, toDeleteSeg.name)
}
p.segments = p.segments[1:]
p.segmentDeleteCount++
}
}()
p.cond.Broadcast()
}
func (p *muxerVariantFMP4Playlist) onPartFinalized(part *muxerVariantFMP4Part) {
func() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.partsByName[part.name()] = part
p.parts = append(p.parts, part)
p.nextSegmentParts = append(p.nextSegmentParts, part)
p.nextPartID = part.id + 1
}()
p.cond.Broadcast()
}

186
internal/hls/muxer_variant_fmp4_segment.go

@ -1,186 +0,0 @@ @@ -1,186 +0,0 @@
package hls
import (
"fmt"
"io"
"strconv"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
)
type partsReader struct {
parts []*muxerVariantFMP4Part
curPart int
curPos int
}
func (mbr *partsReader) Read(p []byte) (int, error) {
n := 0
lenp := len(p)
for {
if mbr.curPart >= len(mbr.parts) {
return n, io.EOF
}
copied := copy(p[n:], mbr.parts[mbr.curPart].content[mbr.curPos:])
mbr.curPos += copied
n += copied
if mbr.curPos == len(mbr.parts[mbr.curPart].content) {
mbr.curPart++
mbr.curPos = 0
}
if n == lenp {
return n, nil
}
}
}
type muxerVariantFMP4Segment struct {
lowLatency bool
id uint64
startTime time.Time
startDTS time.Duration
segmentMaxSize uint64
videoTrack format.Format
audioTrack format.Format
genPartID func() uint64
onPartFinalized func(*muxerVariantFMP4Part)
name string
size uint64
parts []*muxerVariantFMP4Part
currentPart *muxerVariantFMP4Part
renderedDuration time.Duration
}
func newMuxerVariantFMP4Segment(
lowLatency bool,
id uint64,
startTime time.Time,
startDTS time.Duration,
segmentMaxSize uint64,
videoTrack format.Format,
audioTrack format.Format,
genPartID func() uint64,
onPartFinalized func(*muxerVariantFMP4Part),
) *muxerVariantFMP4Segment {
s := &muxerVariantFMP4Segment{
lowLatency: lowLatency,
id: id,
startTime: startTime,
startDTS: startDTS,
segmentMaxSize: segmentMaxSize,
videoTrack: videoTrack,
audioTrack: audioTrack,
genPartID: genPartID,
onPartFinalized: onPartFinalized,
name: "seg" + strconv.FormatUint(id, 10),
}
s.currentPart = newMuxerVariantFMP4Part(
s.videoTrack,
s.audioTrack,
s.genPartID(),
)
return s
}
func (s *muxerVariantFMP4Segment) reader() io.Reader {
return &partsReader{parts: s.parts}
}
func (s *muxerVariantFMP4Segment) getRenderedDuration() time.Duration {
return s.renderedDuration
}
func (s *muxerVariantFMP4Segment) finalize(
nextVideoSampleDTS time.Duration,
) error {
err := s.currentPart.finalize()
if err != nil {
return err
}
if s.currentPart.content != nil {
s.onPartFinalized(s.currentPart)
s.parts = append(s.parts, s.currentPart)
}
s.currentPart = nil
if s.videoTrack != nil {
s.renderedDuration = nextVideoSampleDTS - s.startDTS
} else {
s.renderedDuration = 0
for _, pa := range s.parts {
s.renderedDuration += pa.renderedDuration
}
}
return nil
}
func (s *muxerVariantFMP4Segment) writeH264(sample *augmentedVideoSample, adjustedPartDuration time.Duration) error {
size := uint64(len(sample.Payload))
if (s.size + size) > s.segmentMaxSize {
return fmt.Errorf("reached maximum segment size")
}
s.size += size
s.currentPart.writeH264(sample)
// switch part
if s.lowLatency &&
s.currentPart.duration() >= adjustedPartDuration {
err := s.currentPart.finalize()
if err != nil {
return err
}
s.parts = append(s.parts, s.currentPart)
s.onPartFinalized(s.currentPart)
s.currentPart = newMuxerVariantFMP4Part(
s.videoTrack,
s.audioTrack,
s.genPartID(),
)
}
return nil
}
func (s *muxerVariantFMP4Segment) writeAudio(sample *augmentedAudioSample, adjustedPartDuration time.Duration) error {
size := uint64(len(sample.Payload))
if (s.size + size) > s.segmentMaxSize {
return fmt.Errorf("reached maximum segment size")
}
s.size += size
s.currentPart.writeAudio(sample)
// switch part
if s.lowLatency && s.videoTrack == nil &&
s.currentPart.duration() >= adjustedPartDuration {
err := s.currentPart.finalize()
if err != nil {
return err
}
s.parts = append(s.parts, s.currentPart)
s.onPartFinalized(s.currentPart)
s.currentPart = newMuxerVariantFMP4Part(
s.videoTrack,
s.audioTrack,
s.genPartID(),
)
}
return nil
}

406
internal/hls/muxer_variant_fmp4_segmenter.go

@ -1,406 +0,0 @@ @@ -1,406 +0,0 @@
package hls
import (
"fmt"
"time"
"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/rtsp-simple-server/internal/hls/fmp4"
)
func partDurationIsCompatible(partDuration time.Duration, sampleDuration time.Duration) bool {
if sampleDuration > partDuration {
return false
}
f := (partDuration / sampleDuration)
if (partDuration % sampleDuration) != 0 {
f++
}
f *= sampleDuration
return partDuration > ((f * 85) / 100)
}
func partDurationIsCompatibleWithAll(partDuration time.Duration, sampleDurations map[time.Duration]struct{}) bool {
for sd := range sampleDurations {
if !partDurationIsCompatible(partDuration, sd) {
return false
}
}
return true
}
func findCompatiblePartDuration(
minPartDuration time.Duration,
sampleDurations map[time.Duration]struct{},
) time.Duration {
i := minPartDuration
for ; i < 5*time.Second; i += 5 * time.Millisecond {
if partDurationIsCompatibleWithAll(i, sampleDurations) {
break
}
}
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 {
fmp4.PartSample
dts time.Duration
ntp time.Time
}
type augmentedAudioSample struct {
fmp4.PartSample
dts time.Duration
ntp time.Time
}
type muxerVariantFMP4Segmenter struct {
lowLatency bool
segmentDuration time.Duration
partDuration time.Duration
segmentMaxSize uint64
videoTrack format.Format
audioTrack format.Format
onSegmentFinalized func(*muxerVariantFMP4Segment)
onPartFinalized func(*muxerVariantFMP4Part)
startDTS time.Duration
videoFirstRandomAccessReceived bool
videoDTSExtractor dtsExtractor
lastVideoParams [][]byte
currentSegment *muxerVariantFMP4Segment
nextSegmentID uint64
nextPartID uint64
nextVideoSample *augmentedVideoSample
nextAudioSample *augmentedAudioSample
firstSegmentFinalized bool
sampleDurations map[time.Duration]struct{}
adjustedPartDuration time.Duration
}
func newMuxerVariantFMP4Segmenter(
lowLatency bool,
segmentCount int,
segmentDuration time.Duration,
partDuration time.Duration,
segmentMaxSize uint64,
videoTrack format.Format,
audioTrack format.Format,
onSegmentFinalized func(*muxerVariantFMP4Segment),
onPartFinalized func(*muxerVariantFMP4Part),
) *muxerVariantFMP4Segmenter {
m := &muxerVariantFMP4Segmenter{
lowLatency: lowLatency,
segmentDuration: segmentDuration,
partDuration: partDuration,
segmentMaxSize: segmentMaxSize,
videoTrack: videoTrack,
audioTrack: audioTrack,
onSegmentFinalized: onSegmentFinalized,
onPartFinalized: onPartFinalized,
sampleDurations: make(map[time.Duration]struct{}),
}
// add initial gaps, required by iOS LL-HLS
if m.lowLatency {
m.nextSegmentID = 7
}
return m
}
func (m *muxerVariantFMP4Segmenter) genSegmentID() uint64 {
id := m.nextSegmentID
m.nextSegmentID++
return id
}
func (m *muxerVariantFMP4Segmenter) genPartID() uint64 {
id := m.nextPartID
m.nextPartID++
return id
}
// iPhone iOS fails if part durations are less than 85% of maximum part duration.
// find a part duration that is compatible with all received sample durations
func (m *muxerVariantFMP4Segmenter) adjustPartDuration(du time.Duration) {
if !m.lowLatency || m.firstSegmentFinalized {
return
}
// avoid a crash by skipping invalid durations
if du == 0 {
return
}
if _, ok := m.sampleDurations[du]; !ok {
m.sampleDurations[du] = struct{}{}
m.adjustedPartDuration = findCompatiblePartDuration(
m.partDuration,
m.sampleDurations,
)
}
}
func (m *muxerVariantFMP4Segmenter) writeH26x(ntp time.Time, pts time.Duration, au [][]byte) error {
randomAccessPresent := false
switch m.videoTrack.(type) {
case *format.H264:
nonIDRPresent := false
for _, nalu := range au {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR:
randomAccessPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
}
if !randomAccessPresent && !nonIDRPresent {
return nil
}
case *format.H265:
for _, nalu := range au {
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
dts, err = m.videoDTSExtractor.Extract(au, pts)
if err != nil {
return fmt.Errorf("unable to extract DTS: %v", err)
}
m.startDTS = dts
dts = 0
pts -= m.startDTS
} else {
var err error
dts, err = m.videoDTSExtractor.Extract(au, pts)
if err != nil {
return fmt.Errorf("unable to extract DTS: %v", err)
}
dts -= m.startDTS
pts -= m.startDTS
}
avcc, err := h264.AVCCMarshal(au)
if err != nil {
return err
}
sample := &augmentedVideoSample{
PartSample: fmp4.PartSample{
PTSOffset: int32(durationGoToMp4(pts-dts, 90000)),
IsNonSyncSample: !randomAccessPresent,
Payload: avcc,
},
dts: dts,
ntp: ntp,
}
// put samples into a queue in order to
// - compute sample duration
// - check if next sample is IDR
sample, m.nextVideoSample = m.nextVideoSample, sample
if sample == nil {
return nil
}
sample.Duration = uint32(durationGoToMp4(m.nextVideoSample.dts-sample.dts, 90000))
if m.currentSegment == nil {
// create first segment
m.currentSegment = newMuxerVariantFMP4Segment(
m.lowLatency,
m.genSegmentID(),
sample.ntp,
sample.dts,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.genPartID,
m.onPartFinalized,
)
}
m.adjustPartDuration(durationMp4ToGo(uint64(sample.Duration), 90000))
err = m.currentSegment.writeH264(sample, m.adjustedPartDuration)
if err != nil {
return err
}
// switch segment
if randomAccessPresent {
videoParams := extractVideoParams(m.videoTrack)
paramsChanged := !videoParamsEqual(m.lastVideoParams, videoParams)
if (m.nextVideoSample.dts-m.currentSegment.startDTS) >= m.segmentDuration ||
paramsChanged {
err := m.currentSegment.finalize(m.nextVideoSample.dts)
if err != nil {
return err
}
m.onSegmentFinalized(m.currentSegment)
m.firstSegmentFinalized = true
m.currentSegment = newMuxerVariantFMP4Segment(
m.lowLatency,
m.genSegmentID(),
m.nextVideoSample.ntp,
m.nextVideoSample.dts,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.genPartID,
m.onPartFinalized,
)
if paramsChanged {
m.lastVideoParams = videoParams
m.firstSegmentFinalized = false
// reset adjusted part duration
m.sampleDurations = make(map[time.Duration]struct{})
}
}
}
return nil
}
func (m *muxerVariantFMP4Segmenter) writeAudio(ntp time.Time, dts time.Duration, au []byte) error {
if m.videoTrack != nil {
// wait for the video track
if !m.videoFirstRandomAccessReceived {
return nil
}
dts -= m.startDTS
if dts < 0 {
return nil
}
}
sample := &augmentedAudioSample{
PartSample: fmp4.PartSample{
Payload: au,
},
dts: dts,
ntp: ntp,
}
// put samples into a queue in order to compute the sample duration
sample, m.nextAudioSample = m.nextAudioSample, sample
if sample == nil {
return nil
}
sample.Duration = uint32(durationGoToMp4(m.nextAudioSample.dts-sample.dts, uint32(m.audioTrack.ClockRate())))
if m.videoTrack == nil {
if m.currentSegment == nil {
// create first segment
m.currentSegment = newMuxerVariantFMP4Segment(
m.lowLatency,
m.genSegmentID(),
sample.ntp,
sample.dts,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.genPartID,
m.onPartFinalized,
)
}
} else {
// wait for the video track
if m.currentSegment == nil {
return nil
}
}
err := m.currentSegment.writeAudio(sample, m.partDuration)
if err != nil {
return err
}
// switch segment
if m.videoTrack == nil &&
(m.nextAudioSample.dts-m.currentSegment.startDTS) >= m.segmentDuration {
err := m.currentSegment.finalize(0)
if err != nil {
return err
}
m.onSegmentFinalized(m.currentSegment)
m.firstSegmentFinalized = true
m.currentSegment = newMuxerVariantFMP4Segment(
m.lowLatency,
m.genSegmentID(),
m.nextAudioSample.ntp,
m.nextAudioSample.dts,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.genPartID,
m.onPartFinalized,
)
}
return nil
}

73
internal/hls/muxer_variant_mpegts.go

@ -1,73 +0,0 @@ @@ -1,73 +0,0 @@
package hls
import (
"fmt"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
)
type muxerVariantMPEGTS struct {
playlist *muxerVariantMPEGTSPlaylist
segmenter *muxerVariantMPEGTSSegmenter
}
func newMuxerVariantMPEGTS(
segmentCount int,
segmentDuration time.Duration,
segmentMaxSize uint64,
videoTrack format.Format,
audioTrack format.Format,
) (*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 only supports H264 video. Use the fMP4 or Low-Latency variants instead")
}
}
var audioTrackMPEG4Audio *format.MPEG4Audio
if audioTrack != nil {
var ok bool
audioTrackMPEG4Audio, ok = audioTrack.(*format.MPEG4Audio)
if !ok {
return nil, fmt.Errorf(
"the MPEG-TS variant of HLS only supports MPEG4-audio. Use the fMP4 or Low-Latency variants instead")
}
}
v := &muxerVariantMPEGTS{}
v.playlist = newMuxerVariantMPEGTSPlaylist(segmentCount)
v.segmenter = newMuxerVariantMPEGTSSegmenter(
segmentDuration,
segmentMaxSize,
videoTrackH264,
audioTrackMPEG4Audio,
func(seg *muxerVariantMPEGTSSegment) {
v.playlist.pushSegment(seg)
},
)
return v, nil
}
func (v *muxerVariantMPEGTS) close() {
v.playlist.close()
}
func (v *muxerVariantMPEGTS) writeH26x(ntp time.Time, pts time.Duration, nalus [][]byte) error {
return v.segmenter.writeH264(ntp, pts, nalus)
}
func (v *muxerVariantMPEGTS) writeAudio(ntp time.Time, pts time.Duration, au []byte) error {
return v.segmenter.writeAAC(ntp, pts, au)
}
func (v *muxerVariantMPEGTS) file(name string, msn string, part string, skip string) *MuxerFileResponse {
return v.playlist.file(name)
}

145
internal/hls/muxer_variant_mpegts_playlist.go

@ -1,145 +0,0 @@ @@ -1,145 +0,0 @@
package hls
import (
"bytes"
"io"
"math"
"net/http"
"strconv"
"strings"
"sync"
)
type muxerVariantMPEGTSPlaylist struct {
segmentCount int
mutex sync.Mutex
cond *sync.Cond
closed bool
segments []*muxerVariantMPEGTSSegment
segmentByName map[string]*muxerVariantMPEGTSSegment
segmentDeleteCount int
}
func newMuxerVariantMPEGTSPlaylist(segmentCount int) *muxerVariantMPEGTSPlaylist {
p := &muxerVariantMPEGTSPlaylist{
segmentCount: segmentCount,
segmentByName: make(map[string]*muxerVariantMPEGTSSegment),
}
p.cond = sync.NewCond(&p.mutex)
return p
}
func (p *muxerVariantMPEGTSPlaylist) close() {
func() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.closed = true
}()
p.cond.Broadcast()
}
func (p *muxerVariantMPEGTSPlaylist) file(name string) *MuxerFileResponse {
switch {
case name == "stream.m3u8":
return p.playlistReader()
case strings.HasSuffix(name, ".ts"):
return p.segmentReader(name)
default:
return &MuxerFileResponse{Status: http.StatusNotFound}
}
}
func (p *muxerVariantMPEGTSPlaylist) playlist() io.Reader {
cnt := "#EXTM3U\n"
cnt += "#EXT-X-VERSION:3\n"
cnt += "#EXT-X-ALLOW-CACHE:NO\n"
targetDuration := func() uint {
ret := uint(0)
// EXTINF, when rounded to the nearest integer, must be <= EXT-X-TARGETDURATION
for _, s := range p.segments {
v2 := uint(math.Round(s.duration().Seconds()))
if v2 > ret {
ret = v2
}
}
return ret
}()
cnt += "#EXT-X-TARGETDURATION:" + strconv.FormatUint(uint64(targetDuration), 10) + "\n"
cnt += "#EXT-X-MEDIA-SEQUENCE:" + strconv.FormatInt(int64(p.segmentDeleteCount), 10) + "\n"
for _, s := range p.segments {
cnt += "#EXT-X-PROGRAM-DATE-TIME:" + s.startTime.Format("2006-01-02T15:04:05.999Z07:00") + "\n" +
"#EXTINF:" + strconv.FormatFloat(s.duration().Seconds(), 'f', -1, 64) + ",\n" +
s.name + ".ts\n"
}
return bytes.NewReader([]byte(cnt))
}
func (p *muxerVariantMPEGTSPlaylist) playlistReader() *MuxerFileResponse {
p.mutex.Lock()
defer p.mutex.Unlock()
if !p.closed && len(p.segments) == 0 {
p.cond.Wait()
}
if p.closed {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": `application/x-mpegURL`,
},
Body: p.playlist(),
}
}
func (p *muxerVariantMPEGTSPlaylist) segmentReader(fname string) *MuxerFileResponse {
base := strings.TrimSuffix(fname, ".ts")
p.mutex.Lock()
f, ok := p.segmentByName[base]
p.mutex.Unlock()
if !ok {
return &MuxerFileResponse{Status: http.StatusNotFound}
}
return &MuxerFileResponse{
Status: http.StatusOK,
Header: map[string]string{
"Content-Type": "video/MP2T",
},
Body: f.reader(),
}
}
func (p *muxerVariantMPEGTSPlaylist) pushSegment(t *muxerVariantMPEGTSSegment) {
func() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.segmentByName[t.name] = t
p.segments = append(p.segments, t)
if len(p.segments) > p.segmentCount {
delete(p.segmentByName, p.segments[0].name)
p.segments = p.segments[1:]
p.segmentDeleteCount++
}
}()
p.cond.Broadcast()
}

118
internal/hls/muxer_variant_mpegts_segment.go

@ -1,118 +0,0 @@ @@ -1,118 +0,0 @@
package hls
import (
"bytes"
"fmt"
"io"
"strconv"
"time"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts"
)
type muxerVariantMPEGTSSegment struct {
segmentMaxSize uint64
videoTrack *format.H264
audioTrack *format.MPEG4Audio
writer *mpegts.Writer
size uint64
startTime time.Time
name string
startDTS *time.Duration
endDTS time.Duration
audioAUCount int
content []byte
}
func newMuxerVariantMPEGTSSegment(
id uint64,
startTime time.Time,
segmentMaxSize uint64,
videoTrack *format.H264,
audioTrack *format.MPEG4Audio,
writer *mpegts.Writer,
) *muxerVariantMPEGTSSegment {
t := &muxerVariantMPEGTSSegment{
segmentMaxSize: segmentMaxSize,
videoTrack: videoTrack,
audioTrack: audioTrack,
writer: writer,
startTime: startTime,
name: "seg" + strconv.FormatUint(id, 10),
}
return t
}
func (t *muxerVariantMPEGTSSegment) duration() time.Duration {
return t.endDTS - *t.startDTS
}
func (t *muxerVariantMPEGTSSegment) reader() io.Reader {
return bytes.NewReader(t.content)
}
func (t *muxerVariantMPEGTSSegment) finalize(endDTS time.Duration) {
t.endDTS = endDTS
t.content = t.writer.GenerateSegment()
}
func (t *muxerVariantMPEGTSSegment) writeH264(
pcr time.Duration,
dts time.Duration,
pts time.Duration,
idrPresent bool,
nalus [][]byte,
) error {
size := uint64(0)
for _, nalu := range nalus {
size += uint64(len(nalu))
}
if (t.size + size) > t.segmentMaxSize {
return fmt.Errorf("reached maximum segment size")
}
t.size += size
err := t.writer.WriteH264(pcr, dts, pts, idrPresent, nalus)
if err != nil {
return err
}
if t.startDTS == nil {
t.startDTS = &dts
}
t.endDTS = dts
return nil
}
func (t *muxerVariantMPEGTSSegment) writeAAC(
pcr time.Duration,
pts time.Duration,
au []byte,
) error {
size := uint64(len(au))
if (t.size + size) > t.segmentMaxSize {
return fmt.Errorf("reached maximum segment size")
}
t.size += size
err := t.writer.WriteAAC(pcr, pts, au)
if err != nil {
return err
}
if t.videoTrack == nil {
t.audioAUCount++
if t.startDTS == nil {
t.startDTS = &pts
}
t.endDTS = pts
}
return nil
}

193
internal/hls/muxer_variant_mpegts_segmenter.go

@ -1,193 +0,0 @@ @@ -1,193 +0,0 @@
package hls
import (
"fmt"
"time"
"github.com/aler9/gortsplib/v2/pkg/codecs/h264"
"github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/rtsp-simple-server/internal/hls/mpegts"
)
const (
mpegtsSegmentMinAUCount = 100
)
type muxerVariantMPEGTSSegmenter struct {
segmentDuration time.Duration
segmentMaxSize uint64
videoTrack *format.H264
audioTrack *format.MPEG4Audio
onSegmentReady func(*muxerVariantMPEGTSSegment)
writer *mpegts.Writer
nextSegmentID uint64
currentSegment *muxerVariantMPEGTSSegment
videoDTSExtractor *h264.DTSExtractor
startPCR time.Time
startDTS time.Duration
}
func newMuxerVariantMPEGTSSegmenter(
segmentDuration time.Duration,
segmentMaxSize uint64,
videoTrack *format.H264,
audioTrack *format.MPEG4Audio,
onSegmentReady func(*muxerVariantMPEGTSSegment),
) *muxerVariantMPEGTSSegmenter {
m := &muxerVariantMPEGTSSegmenter{
segmentDuration: segmentDuration,
segmentMaxSize: segmentMaxSize,
videoTrack: videoTrack,
audioTrack: audioTrack,
onSegmentReady: onSegmentReady,
}
m.writer = mpegts.NewWriter(
videoTrack,
audioTrack)
return m
}
func (m *muxerVariantMPEGTSSegmenter) genSegmentID() uint64 {
id := m.nextSegmentID
m.nextSegmentID++
return id
}
func (m *muxerVariantMPEGTSSegmenter) writeH264(ntp time.Time, pts time.Duration, nalus [][]byte) error {
idrPresent := false
nonIDRPresent := false
for _, nalu := range nalus {
typ := h264.NALUType(nalu[0] & 0x1F)
switch typ {
case h264.NALUTypeIDR:
idrPresent = true
case h264.NALUTypeNonIDR:
nonIDRPresent = true
}
}
var dts time.Duration
if m.currentSegment == nil {
// skip groups silently until we find one with a IDR
if !idrPresent {
return nil
}
m.videoDTSExtractor = h264.NewDTSExtractor()
var err error
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
if err != nil {
return fmt.Errorf("unable to extract DTS: %v", err)
}
m.startPCR = ntp
m.startDTS = dts
dts = 0
pts -= m.startDTS
// create first segment
m.currentSegment = newMuxerVariantMPEGTSSegment(
m.genSegmentID(),
ntp,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.writer)
} else {
if !idrPresent && !nonIDRPresent {
return nil
}
var err error
dts, err = m.videoDTSExtractor.Extract(nalus, pts)
if err != nil {
return fmt.Errorf("unable to extract DTS: %v", err)
}
dts -= m.startDTS
pts -= m.startDTS
// switch segment
if idrPresent &&
(dts-*m.currentSegment.startDTS) >= m.segmentDuration {
m.currentSegment.finalize(dts)
m.onSegmentReady(m.currentSegment)
m.currentSegment = newMuxerVariantMPEGTSSegment(
m.genSegmentID(),
ntp,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.writer)
}
}
err := m.currentSegment.writeH264(
ntp.Sub(m.startPCR),
dts,
pts,
idrPresent,
nalus)
if err != nil {
return err
}
return nil
}
func (m *muxerVariantMPEGTSSegmenter) writeAAC(ntp time.Time, pts time.Duration, au []byte) error {
if m.videoTrack == nil {
if m.currentSegment == nil {
m.startPCR = ntp
m.startDTS = pts
pts = 0
// create first segment
m.currentSegment = newMuxerVariantMPEGTSSegment(
m.genSegmentID(),
ntp,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.writer)
} else {
pts -= m.startDTS
// switch segment
if m.currentSegment.audioAUCount >= mpegtsSegmentMinAUCount &&
(pts-*m.currentSegment.startDTS) >= m.segmentDuration {
m.currentSegment.finalize(pts)
m.onSegmentReady(m.currentSegment)
m.currentSegment = newMuxerVariantMPEGTSSegment(
m.genSegmentID(),
ntp,
m.segmentMaxSize,
m.videoTrack,
m.audioTrack,
m.writer)
}
}
} else {
// wait for the video track
if m.currentSegment == nil {
return nil
}
pts -= m.startDTS
}
err := m.currentSegment.writeAAC(ntp.Sub(m.startPCR), pts, au)
if err != nil {
return err
}
return nil
}
Loading…
Cancel
Save