From 8ad376de53dc0c0741c357770ebd3bd0ae7d5c3e Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Fri, 10 Mar 2023 13:26:56 +0100 Subject: [PATCH] move HLS implementation into gohlslib (#1557) --- README.md | 1 + go.mod | 32 +- go.sum | 76 +- internal/conf/hlsvariant.go | 2 +- internal/core/hls_muxer.go | 2 +- internal/core/hls_source.go | 11 +- internal/hls/client.go | 127 -- internal/hls/client_downloader_primary.go | 297 ---- internal/hls/client_downloader_stream.go | 260 --- internal/hls/client_processor_fmp4.go | 221 --- internal/hls/client_processor_fmp4_track.go | 72 - internal/hls/client_processor_mpegts.go | 221 --- internal/hls/client_processor_mpegts_track.go | 63 - internal/hls/client_routine_pool.go | 52 - internal/hls/client_segment_queue.go | 79 - internal/hls/client_test.go | 445 ----- internal/hls/client_timesync_fmp4.go | 59 - internal/hls/client_timesync_mpegts.go | 46 - internal/hls/codecparameters.go | 143 -- internal/hls/fmp4/boxes_opus.go | 53 - internal/hls/fmp4/fmp4.go | 2 - internal/hls/fmp4/init.go | 363 ---- internal/hls/fmp4/init_test.go | 1453 ----------------- internal/hls/fmp4/init_track.go | 526 ------ internal/hls/fmp4/mp4_writer.go | 93 -- internal/hls/fmp4/part.go | 263 --- internal/hls/fmp4/part_test.go | 249 --- internal/hls/fmp4/part_track.go | 111 -- internal/hls/m3u8/m3u8.go | 106 -- internal/hls/mpegts/tracks.go | 106 -- internal/hls/mpegts/writer.go | 201 --- internal/hls/mpegts/writer_test.go | 371 ----- internal/hls/mpegtstimedec/decoder.go | 46 - internal/hls/mpegtstimedec/decoder_test.go | 72 - internal/hls/muxer.go | 100 -- internal/hls/muxer_primary_playlist.go | 62 - internal/hls/muxer_test.go | 595 ------- internal/hls/muxer_variant.go | 22 - internal/hls/muxer_variant_fmp4.go | 163 -- internal/hls/muxer_variant_fmp4_part.go | 140 -- internal/hls/muxer_variant_fmp4_playlist.go | 489 ------ internal/hls/muxer_variant_fmp4_segment.go | 186 --- internal/hls/muxer_variant_fmp4_segmenter.go | 406 ----- internal/hls/muxer_variant_mpegts.go | 73 - internal/hls/muxer_variant_mpegts_playlist.go | 145 -- internal/hls/muxer_variant_mpegts_segment.go | 118 -- .../hls/muxer_variant_mpegts_segmenter.go | 193 --- 47 files changed, 70 insertions(+), 8846 deletions(-) delete mode 100644 internal/hls/client.go delete mode 100644 internal/hls/client_downloader_primary.go delete mode 100644 internal/hls/client_downloader_stream.go delete mode 100644 internal/hls/client_processor_fmp4.go delete mode 100644 internal/hls/client_processor_fmp4_track.go delete mode 100644 internal/hls/client_processor_mpegts.go delete mode 100644 internal/hls/client_processor_mpegts_track.go delete mode 100644 internal/hls/client_routine_pool.go delete mode 100644 internal/hls/client_segment_queue.go delete mode 100644 internal/hls/client_test.go delete mode 100644 internal/hls/client_timesync_fmp4.go delete mode 100644 internal/hls/client_timesync_mpegts.go delete mode 100644 internal/hls/codecparameters.go delete mode 100644 internal/hls/fmp4/boxes_opus.go delete mode 100644 internal/hls/fmp4/fmp4.go delete mode 100644 internal/hls/fmp4/init.go delete mode 100644 internal/hls/fmp4/init_test.go delete mode 100644 internal/hls/fmp4/init_track.go delete mode 100644 internal/hls/fmp4/mp4_writer.go delete mode 100644 internal/hls/fmp4/part.go delete mode 100644 internal/hls/fmp4/part_test.go delete mode 100644 internal/hls/fmp4/part_track.go delete mode 100644 internal/hls/m3u8/m3u8.go delete mode 100644 internal/hls/mpegts/tracks.go delete mode 100644 internal/hls/mpegts/writer.go delete mode 100644 internal/hls/mpegts/writer_test.go delete mode 100644 internal/hls/mpegtstimedec/decoder.go delete mode 100644 internal/hls/mpegtstimedec/decoder_test.go delete mode 100644 internal/hls/muxer.go delete mode 100644 internal/hls/muxer_primary_playlist.go delete mode 100644 internal/hls/muxer_test.go delete mode 100644 internal/hls/muxer_variant.go delete mode 100644 internal/hls/muxer_variant_fmp4.go delete mode 100644 internal/hls/muxer_variant_fmp4_part.go delete mode 100644 internal/hls/muxer_variant_fmp4_playlist.go delete mode 100644 internal/hls/muxer_variant_fmp4_segment.go delete mode 100644 internal/hls/muxer_variant_fmp4_segmenter.go delete mode 100644 internal/hls/muxer_variant_mpegts.go delete mode 100644 internal/hls/muxer_variant_mpegts_playlist.go delete mode 100644 internal/hls/muxer_variant_mpegts_segment.go delete mode 100644 internal/hls/muxer_variant_mpegts_segmenter.go diff --git a/README.md b/README.md index 2e0bd42a..7bc86ea9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/go.mod b/go.mod index d1122f92..66c30096 100644 --- a/go.mod +++ b/go.mod @@ -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 ( 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 ) diff --git a/go.sum b/go.sum index a666c11e..2c924432 100644 --- a/go.sum +++ b/go.sum @@ -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 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 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 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 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= 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/ 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 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 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= 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= diff --git a/internal/conf/hlsvariant.go b/internal/conf/hlsvariant.go index 133e0f63..33629ffe 100644 --- a/internal/conf/hlsvariant.go +++ b/internal/conf/hlsvariant.go @@ -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. diff --git a/internal/core/hls_muxer.go b/internal/core/hls_muxer.go index 94cc1ab2..a5c9a2f6 100644 --- a/internal/core/hls_muxer.go +++ b/internal/core/hls_muxer.go @@ -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 ( diff --git a/internal/core/hls_source.go b/internal/core/hls_source.go index cdfda106..d3e296fb 100644 --- a/internal/core/hls_source.go +++ b/internal/core/hls_source.go @@ -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 { 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 c, err := hls.NewClient( cnf.Source, cnf.SourceFingerprint, - s, + hlsLoggerWrapper(s.Log), ) if err != nil { return err diff --git a/internal/hls/client.go b/internal/hls/client.go deleted file mode 100644 index b30baccd..00000000 --- a/internal/hls/client.go +++ /dev/null @@ -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") - } -} diff --git a/internal/hls/client_downloader_primary.go b/internal/hls/client_downloader_primary.go deleted file mode 100644 index 0851e90e..00000000 --- a/internal/hls/client_downloader_primary.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/client_downloader_stream.go b/internal/hls/client_downloader_stream.go deleted file mode 100644 index 9b91797c..00000000 --- a/internal/hls/client_downloader_stream.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/client_processor_fmp4.go b/internal/hls/client_processor_fmp4.go deleted file mode 100644 index 7060a413..00000000 --- a/internal/hls/client_processor_fmp4.go +++ /dev/null @@ -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 - } -} diff --git a/internal/hls/client_processor_fmp4_track.go b/internal/hls/client_processor_fmp4_track.go deleted file mode 100644 index 58408fc6..00000000 --- a/internal/hls/client_processor_fmp4_track.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/client_processor_mpegts.go b/internal/hls/client_processor_mpegts.go deleted file mode 100644 index 62451221..00000000 --- a/internal/hls/client_processor_mpegts.go +++ /dev/null @@ -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 - } -} diff --git a/internal/hls/client_processor_mpegts_track.go b/internal/hls/client_processor_mpegts_track.go deleted file mode 100644 index 5831ed25..00000000 --- a/internal/hls/client_processor_mpegts_track.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/client_routine_pool.go b/internal/hls/client_routine_pool.go deleted file mode 100644 index ca7f5944..00000000 --- a/internal/hls/client_routine_pool.go +++ /dev/null @@ -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(): - } - } - }() -} diff --git a/internal/hls/client_segment_queue.go b/internal/hls/client_segment_queue.go deleted file mode 100644 index 67a4e43d..00000000 --- a/internal/hls/client_segment_queue.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/client_test.go b/internal/hls/client_test.go deleted file mode 100644 index 1ec10dfd..00000000 --- a/internal/hls/client_test.go +++ /dev/null @@ -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() -} diff --git a/internal/hls/client_timesync_fmp4.go b/internal/hls/client_timesync_fmp4.go deleted file mode 100644 index 5289c689..00000000 --- a/internal/hls/client_timesync_fmp4.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/client_timesync_mpegts.go b/internal/hls/client_timesync_mpegts.go deleted file mode 100644 index 9ffcc0b1..00000000 --- a/internal/hls/client_timesync_mpegts.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/codecparameters.go b/internal/hls/codecparameters.go deleted file mode 100644 index cbc26538..00000000 --- a/internal/hls/codecparameters.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/fmp4/boxes_opus.go b/internal/hls/fmp4/boxes_opus.go deleted file mode 100644 index 131a5a00..00000000 --- a/internal/hls/fmp4/boxes_opus.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/fmp4/fmp4.go b/internal/hls/fmp4/fmp4.go deleted file mode 100644 index 53be064e..00000000 --- a/internal/hls/fmp4/fmp4.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package fmp4 contains a fMP4 reader and writer. -package fmp4 diff --git a/internal/hls/fmp4/init.go b/internal/hls/fmp4/init.go deleted file mode 100644 index c321cabe..00000000 --- a/internal/hls/fmp4/init.go +++ /dev/null @@ -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{ // - 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{}) // - if err != nil { - return nil, err - } - - _, err = w.WriteBox(&gomp4.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{}) // - if err != nil { - return nil, err - } - - for _, track := range i.Tracks { - _, err = w.WriteBox(&gomp4.Trex{ // - TrackID: uint32(track.ID), - DefaultSampleDescriptionIndex: 1, - }) - if err != nil { - return nil, err - } - } - - err = w.writeBoxEnd() // - if err != nil { - return nil, err - } - - err = w.writeBoxEnd() // - if err != nil { - return nil, err - } - - return w.bytes(), nil -} diff --git a/internal/hls/fmp4/init_test.go b/internal/hls/fmp4/init_test.go deleted file mode 100644 index 697d93cf..00000000 --- a/internal/hls/fmp4/init_test.go +++ /dev/null @@ -1,1453 +0,0 @@ -package fmp4 - -import ( - "testing" - - "github.com/aler9/gortsplib/v2/pkg/codecs/mpeg4audio" - "github.com/aler9/gortsplib/v2/pkg/format" - "github.com/stretchr/testify/require" -) - -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, -} - -var testVideoTrack = &format.H264{ - PayloadTyp: 96, - SPS: testSPS, - PPS: []byte{0x08}, - PacketizationMode: 1, -} - -var testAudioTrack = &format.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, -} - -var casesInit = []struct { - name string - enc []byte - dec Init -}{ - { - "h264", - []byte{ - 0x00, 0x00, 0x00, 0x20, - 'f', 't', 'y', 'p', - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0x88, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xec, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x88, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x5f, 0x90, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x2d, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, - 0x33, - 'm', 'i', 'n', 'f', - 0x00, 0x00, 0x00, - 0x14, - 'v', 'm', 'h', 'd', - 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, - 'd', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf3, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0xa7, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x97, 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x04, - 0x38, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2d, 0x61, - 0x76, 0x63, 0x43, 0x01, 0x42, 0xc0, 0x28, 0x03, - 0x01, 0x00, 0x19, 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, 0x01, 0x00, 0x01, 0x08, - 0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, - 0x00, 0x0f, 0x42, 0x40, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 1, - TimeScale: 90000, - Format: testVideoTrack, - }, - }, - }, - }, - { - "h265", - []byte{ - 0x00, 0x00, 0x00, 0x20, - 'f', 't', 'y', 'p', - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0xb8, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x02, 0x1c, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x01, 0xb8, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x5f, 0x90, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x2d, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, - 0x63, 0x6d, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, - 0x14, 0x76, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, - 0x23, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0xd7, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xc7, 0x68, 0x65, 0x76, 0x31, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x04, - 0x38, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x5d, 0x68, - 0x76, 0x63, 0x43, 0x01, 0x01, 0x60, 0x00, 0x00, - 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x78, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x13, 0x03, 0x20, 0x00, 0x01, 0x00, 0x04, 0x01, - 0x02, 0x03, 0x04, 0x21, 0x00, 0x01, 0x00, 0x2a, - 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, - 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, - 0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x10, 0xe5, - 0x96, 0x66, 0x69, 0x24, 0xca, 0xe0, 0x10, 0x00, - 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x01, - 0xe0, 0x80, 0x22, 0x00, 0x01, 0x00, 0x01, 0x08, - 0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, - 0x00, 0x0f, 0x42, 0x40, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 1, - TimeScale: 90000, - Format: &format.H265{ - PayloadTyp: 96, - VPS: []byte{0x01, 0x02, 0x03, 0x04}, - SPS: []byte{ - 0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, - 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, - 0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x10, 0xe5, - 0x96, 0x66, 0x69, 0x24, 0xca, 0xe0, 0x10, 0x00, - 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x01, - 0xe0, 0x80, - }, - PPS: []byte{0x08}, - }, - }, - }, - }, - }, - { - "mpeg4audio", - []byte{ - 0x00, 0x00, 0x00, 0x20, - 'f', 't', 'y', 'p', - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0x58, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xbc, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x58, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xac, 0x44, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x2d, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, - 0x03, - 'm', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x10, - 's', 'm', 'h', 'd', - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x24, - 'd', 'i', 'n', 'f', - 0x00, 0x00, 0x00, - 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xc7, 0x73, 0x74, 0x62, - 0x6c, 0x00, 0x00, 0x00, 0x7b, 0x73, 0x74, 0x73, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x6b, - 'm', 'p', '4', 'a', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, - 'e', 's', 'd', 's', - 0x00, 0x00, 0x00, - 0x00, 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x01, - 0x00, 0x04, 0x80, 0x80, 0x80, 0x14, 0x40, 0x15, - 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7, 0x39, 0x00, - 0x01, 0xf7, 0x39, 0x05, 0x80, 0x80, 0x80, 0x02, - 0x12, 0x10, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, - 0x00, 0x00, 0x00, 0x14, - 'b', 't', 'r', 't', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7, 0x39, - 0x00, 0x01, 0xf7, 0x39, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x28, - 'm', 'v', 'e', 'x', - 0x00, 0x00, 0x00, 0x20, - 't', 'r', 'e', 'x', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 1, - TimeScale: uint32(testAudioTrack.ClockRate()), - Format: testAudioTrack, - }, - }, - }, - }, - { - "opus", - []byte{ - 0x00, 0x00, 0x00, 0x20, - 'f', 't', 'y', 'p', - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0x38, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x9c, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x38, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x2d, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, - 0xe3, 0x6d, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, - 0x10, 0x73, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, - 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xa7, 0x73, 0x74, 0x62, - 0x6c, 0x00, 0x00, 0x00, 0x5b, 0x73, 0x74, 0x73, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x4b, 0x4f, 0x70, 0x75, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x02, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0xbb, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x13, 0x64, 0x4f, 0x70, 0x73, 0x00, 0x02, 0x01, - 0x38, 0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xf7, 0x39, - 0x00, 0x01, 0xf7, 0x39, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 1, - TimeScale: 48000, - Format: &format.Opus{ - PayloadTyp: 96, - SampleRate: 48000, - ChannelCount: 2, - }, - }, - }, - }, - }, - { - "h264 + mpeg4audio", - []byte{ - 0x00, 0x00, 0x00, 0x20, - 'f', 't', 'y', 'p', - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x04, 0x64, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xe8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xec, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x88, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x5f, 0x90, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x2d, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, - 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, - 0x33, - 'm', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x14, - 'v', 'm', 'h', 'd', - 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xf3, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0xa7, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x97, 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x04, - 0x38, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x2d, 0x61, - 0x76, 0x63, 0x43, 0x01, 0x42, 0xc0, 0x28, 0x03, - 0x01, 0x00, 0x19, 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, 0x01, 0x00, 0x01, 0x08, - 0x00, 0x00, 0x00, 0x14, 0x62, 0x74, 0x72, 0x74, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, - 0x00, 0x0f, 0x42, 0x40, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0xbc, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x58, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xac, 0x44, 0x00, 0x00, 0x00, 0x00, - 0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x73, 0x6f, 0x75, 0x6e, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x53, 0x6f, 0x75, 0x6e, - 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, - 0x00, 0x00, 0x00, 0x01, 0x03, 0x6d, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x10, - 's', 'm', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0xc7, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0x7b, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x6b, 0x6d, 0x70, 0x34, 0x61, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x00, 0xac, 0x44, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x33, 0x65, 0x73, 0x64, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x80, - 0x80, 0x22, 0x00, 0x02, 0x00, 0x04, 0x80, 0x80, - 0x80, 0x14, 0x40, 0x15, 0x00, 0x00, 0x00, 0x00, - 0x01, 0xf7, 0x39, 0x00, 0x01, 0xf7, 0x39, 0x05, - 0x80, 0x80, 0x80, 0x02, 0x12, 0x10, 0x06, 0x80, - 0x80, 0x80, 0x01, 0x02, 0x00, 0x00, 0x00, 0x14, - 0x62, 0x74, 0x72, 0x74, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0xf7, 0x39, 0x00, 0x01, 0xf7, 0x39, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, - 0x6d, 0x76, 0x65, 0x78, 0x00, 0x00, 0x00, 0x20, - 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, - 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 1, - TimeScale: 90000, - Format: testVideoTrack, - }, - { - ID: 2, - TimeScale: uint32(testAudioTrack.ClockRate()), - Format: testAudioTrack, - }, - }, - }, - }, -} - -func TestInitMarshal(t *testing.T) { - for _, ca := range casesInit { - t.Run(ca.name, func(t *testing.T) { - byts, err := ca.dec.Marshal() - require.NoError(t, err) - require.Equal(t, ca.enc, byts) - }) - } -} - -func TestInitUnmarshal(t *testing.T) { - for _, ca := range casesInit { - t.Run(ca.name, func(t *testing.T) { - var init Init - err := init.Unmarshal(ca.enc) - require.NoError(t, err) - require.Equal(t, ca.dec, init) - }) - } -} - -func TestInitUnmarshalExternal(t *testing.T) { - for _, ca := range []struct { - name string - byts []byte - init Init - }{ - { - "h264", - []byte{ - 0x00, 0x00, 0x00, 0x1c, - 'f', 't', 'y', 'p', - 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00, 0x01, - 0x69, 0x73, 0x6f, 0x6d, 0x61, 0x76, 0x63, 0x31, - 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x02, 0x92, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x98, 0x96, 0x80, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x01, 0xf6, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x00, 0x00, - 0x02, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x92, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x98, 0x96, 0x80, 0x00, 0x00, 0x00, 0x00, - 0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x65, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x42, 0x72, 0x6f, 0x61, - 0x64, 0x70, 0x65, 0x61, 0x6b, 0x20, 0x56, 0x69, - 0x64, 0x65, 0x6f, 0x20, 0x48, 0x61, 0x6e, 0x64, - 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x01, 0x32, - 'm', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x14, - 'v', 'm', 'h', 'd', - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x24, - 'd', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf2, - 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, 0xa6, - 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x96, - 0x61, 0x76, 0x63, 0x31, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x03, 0xc0, 0x02, 0x1c, - 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x68, - 0x32, 0x36, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, - 0xff, 0xff, 0x00, 0x00, 0x00, 0x30, 0x61, 0x76, - 0x63, 0x43, 0x01, 0x42, 0xc0, 0x1f, 0xff, 0xe1, - 0x00, 0x19, 0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, - 0xf0, 0x11, 0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, - 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, - 0x18, 0x32, 0x48, 0x01, 0x00, 0x04, 0x68, 0xcb, - 0x8c, 0xb2, 0x00, 0x00, 0x00, 0x10, 0x70, 0x61, - 0x73, 0x70, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, - 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, - 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, - 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, 0x00, 0x00, - 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 256, - TimeScale: 10000000, - Format: &format.H264{ - PayloadTyp: 96, - SPS: []byte{ - 0x67, 0x42, 0xc0, 0x1f, 0xd9, 0x00, 0xf0, 0x11, - 0x7e, 0xf0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, - 0x00, 0x00, 0x03, 0x00, 0x30, 0x8f, 0x18, 0x32, - 0x48, - }, - PPS: []byte{ - 0x68, 0xcb, 0x8c, 0xb2, - }, - PacketizationMode: 1, - }, - }, - }, - }, - }, - { - "h265 apple", - []byte{ - 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x04, 0x96, 0x6d, 0x6f, 0x6f, 0x76, - 0x00, 0x00, 0x00, 0x6c, 0x6d, 0x76, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x84, - 0xd5, 0x5b, 0xc6, 0x84, 0x00, 0x00, 0x02, 0x58, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x70, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, - 0xd5, 0x5b, 0xc6, 0x84, 0xd5, 0x5b, 0xc6, 0x84, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x03, 0xc0, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, - 0x00, 0x00, 0x02, 0x0c, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x84, - 0xd5, 0x5b, 0xc6, 0x84, 0x00, 0x00, 0x17, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x15, 0xc7, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x31, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x20, 0x56, 0x69, 0x64, 0x65, 0x6f, - 0x00, 0x00, 0x00, 0x01, 0xb3, 0x6d, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x14, 0x76, 0x6d, 0x68, - 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, - 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x01, 0x73, 0x73, 0x74, 0x62, - 0x6c, 0x00, 0x00, 0x01, 0x27, 0x73, 0x74, 0x73, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x01, 0x17, 0x68, 0x76, 0x63, - 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x03, 0xc0, 0x02, 0x1c, 0x00, 0x48, 0x00, - 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, - 0x00, 0x00, 0x8a, 0x68, 0x76, 0x63, 0x43, 0x01, - 0x02, 0x00, 0x00, 0x00, 0x04, 0xb0, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x7b, 0xf0, 0x00, 0xfc, 0xfd, - 0xfa, 0xfa, 0x00, 0x00, 0x0f, 0x03, 0xa0, 0x00, - 0x01, 0x00, 0x18, 0x40, 0x01, 0x0c, 0x01, 0xff, - 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0xb0, - 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x7b, - 0x18, 0xb0, 0x24, 0xa1, 0x00, 0x01, 0x00, 0x3c, - 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, - 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, - 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, - 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, - 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, - 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, - 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, - 0x02, 0x02, 0x02, 0x01, 0xa2, 0x00, 0x01, 0x00, - 0x08, 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, - 0x40, 0x00, 0x00, 0x00, 0x13, 0x63, 0x6f, 0x6c, - 0x72, 0x6e, 0x63, 0x6c, 0x78, 0x00, 0x01, 0x00, - 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x0a, - 0x66, 0x69, 0x65, 0x6c, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x0a, 0x63, 0x68, 0x72, 0x6d, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x70, 0x61, 0x73, 0x70, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x6a, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, - 0xd5, 0x5b, 0xc6, 0x84, 0xd5, 0x5b, 0xc6, 0x84, - 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x06, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x84, - 0xd5, 0x5b, 0xc6, 0x84, 0x00, 0x00, 0x75, 0x30, - 0x00, 0x00, 0x00, 0x00, 0x15, 0xc7, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x3a, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x63, 0x6c, 0x63, 0x70, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x20, 0x43, 0x6c, 0x6f, 0x73, 0x65, - 0x64, 0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x6d, 0x69, - 0x6e, 0x66, 0x00, 0x00, 0x00, 0x0c, 0x6e, 0x6d, - 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, 0x00, - 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x6c, 0x73, 0x74, - 0x62, 0x6c, 0x00, 0x00, 0x00, 0x20, 0x73, 0x74, - 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x63, 0x36, - 0x30, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, - 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, - 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, - 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x48, 0x6d, 0x76, 0x65, 0x78, 0x00, 0x00, - 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{{ - ID: 1, - TimeScale: 6000, - Format: &format.H265{ - PayloadTyp: 96, - VPS: []byte{ - 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, - 0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, - 0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24, - }, - SPS: []byte{ - 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, - 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, - 0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d, - 0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88, - 0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9, - 0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc, - 0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a, - 0x02, 0x02, 0x02, 0x01, - }, - PPS: []byte{ - 0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40, - }, - }, - }}, - }, - }, - { - "mpeg4audio", - []byte{ - 0x00, 0x00, 0x00, 0x18, - 'f', 't', 'y', 'p', - 0x69, 0x73, 0x6f, 0x35, 0x00, 0x00, 0x00, 0x01, - 0x69, 0x73, 0x6f, 0x35, 0x64, 0x61, 0x73, 0x68, - 0x00, 0x00, 0x02, 0x43, - 'm', 'o', 'o', 'v', - 0x00, 0x00, 0x00, 0x6c, - 'm', 'v', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x96, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0xa7, - 't', 'r', 'a', 'k', - 0x00, 0x00, 0x00, 0x5c, - 't', 'k', 'h', 'd', - 0x00, 0x00, 0x00, 0x07, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x43, - 'm', 'd', 'i', 'a', - 0x00, 0x00, 0x00, 0x20, - 'm', 'd', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x96, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x38, - 'h', 'd', 'l', 'r', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x42, 0x72, 0x6f, 0x61, 0x64, 0x70, 0x65, 0x61, - 0x6b, 0x20, 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x20, - 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00, - 0x00, 0x00, 0x00, 0xe3, - 'm', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x10, - 's', 'm', 'h', 'd', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x24, - 'd', 'i', 'n', 'f', - 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xa7, - 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, 0x5b, - 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x4b, - 0x6d, 0x70, 0x34, 0x61, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x10, - 0x00, 0x00, 0x00, 0x00, 0xbb, 0x80, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x27, 0x65, 0x73, 0x64, 0x73, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x19, 0x00, 0x00, - 0x00, 0x04, 0x11, 0x40, 0x15, 0x00, 0x30, 0x00, - 0x00, 0x11, 0x94, 0x00, 0x00, 0x11, 0x94, 0x00, - 0x05, 0x02, 0x11, 0x90, 0x06, 0x01, 0x02, 0x00, - 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, - 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x6d, - 0x76, 0x65, 0x78, 0x00, 0x00, 0x00, 0x20, 0x74, - 0x72, 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{ - { - ID: 257, - TimeScale: 10000000, - Format: &format.MPEG4Audio{ - PayloadTyp: 96, - Config: &mpeg4audio.Config{ - Type: mpeg4audio.ObjectTypeAACLC, - SampleRate: 48000, - ChannelCount: 2, - }, - SizeLength: 13, - IndexLength: 3, - IndexDeltaLength: 3, - }, - }, - }, - }, - }, - { - "ignored closed captions", - []byte{ - 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x04, 0x3f, 0x6d, 0x6f, 0x6f, 0x76, - 0x00, 0x00, 0x00, 0x6c, 0x6d, 0x76, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x62, - 0xd5, 0x5b, 0xc6, 0x62, 0x00, 0x00, 0x02, 0x58, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x19, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, - 0xd5, 0x5b, 0xc6, 0x62, 0xd5, 0x5b, 0xc6, 0x62, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x07, 0x80, 0x00, 0x00, 0x04, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x01, 0xb5, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x62, - 0xd5, 0x5b, 0xc6, 0x62, 0x00, 0x00, 0x17, 0x70, - 0x00, 0x00, 0x00, 0x00, 0x15, 0xc7, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x31, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x76, 0x69, 0x64, 0x65, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x20, 0x56, 0x69, 0x64, 0x65, 0x6f, - 0x00, 0x00, 0x00, 0x01, 0x5c, 0x6d, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x14, 0x76, 0x6d, 0x68, - 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, - 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x01, 0x1c, 0x73, 0x74, 0x62, - 0x6c, 0x00, 0x00, 0x00, 0xd0, 0x73, 0x74, 0x73, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0xc0, 0x61, 0x76, 0x63, - 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x07, 0x80, 0x04, 0x38, 0x00, 0x48, 0x00, - 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, - 0x00, 0x00, 0x33, 0x61, 0x76, 0x63, 0x43, 0x01, - 0x64, 0x00, 0x2a, 0xff, 0xe1, 0x00, 0x1b, 0x27, - 0x64, 0x00, 0x2a, 0xac, 0x52, 0x14, 0x07, 0x80, - 0x22, 0x7e, 0x5f, 0xfc, 0x00, 0x04, 0x00, 0x05, - 0xa8, 0x08, 0x08, 0x0d, 0xb6, 0x15, 0xaf, 0x7b, - 0xe0, 0x20, 0x01, 0x00, 0x05, 0x28, 0xf9, 0x09, - 0x09, 0xcb, 0x00, 0x00, 0x00, 0x13, 0x63, 0x6f, - 0x6c, 0x72, 0x6e, 0x63, 0x6c, 0x78, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x0a, 0x66, 0x69, 0x65, 0x6c, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x0a, 0x63, 0x68, 0x72, 0x6d, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x10, 0x70, 0x61, 0x73, - 0x70, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, - 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73, - 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x6a, 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, - 0x5c, 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, - 0x01, 0xd5, 0x5b, 0xc6, 0x62, 0xd5, 0x5b, 0xc6, - 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x06, 0x6d, 0x64, 0x69, - 0x61, 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, - 0x64, 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, - 0x62, 0xd5, 0x5b, 0xc6, 0x62, 0x00, 0x00, 0x75, - 0x30, 0x00, 0x00, 0x00, 0x00, 0x15, 0xc7, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x3a, 0x68, 0x64, 0x6c, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x63, 0x6c, 0x63, 0x70, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, - 0x64, 0x69, 0x61, 0x20, 0x43, 0x6c, 0x6f, 0x73, - 0x65, 0x64, 0x20, 0x43, 0x61, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0xa4, 0x6d, - 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, 0x0c, 0x6e, - 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, 0x66, 0x00, - 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x6c, 0x73, - 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, 0x20, 0x73, - 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x63, - 0x36, 0x30, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x73, - 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, - 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, - 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x48, 0x6d, 0x76, 0x65, 0x78, 0x00, - 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - Init{ - Tracks: []*InitTrack{{ - ID: 1, - TimeScale: 6000, - Format: &format.H264{ - PayloadTyp: 96, - PacketizationMode: 1, - SPS: []byte{ - 0x27, 0x64, 0x00, 0x2a, 0xac, 0x52, 0x14, 0x07, - 0x80, 0x22, 0x7e, 0x5f, 0xfc, 0x00, 0x04, 0x00, - 0x05, 0xa8, 0x08, 0x08, 0x0d, 0xb6, 0x15, 0xaf, - 0x7b, 0xe0, 0x20, - }, - PPS: []byte{ - 0x28, 0xf9, 0x09, 0x09, 0xcb, - }, - }, - }}, - }, - }, - } { - t.Run(ca.name, func(t *testing.T) { - var init Init - err := init.Unmarshal(ca.byts) - require.NoError(t, err) - require.Equal(t, ca.init, init) - }) - } -} - -func TestInitUnmarshalErrors(t *testing.T) { - for _, ca := range []struct { - name string - byts []byte - err string - }{ - { - "ac3", - []byte{ - 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0x20, 0x6d, 0x6f, 0x6f, 0x76, - 0x00, 0x00, 0x00, 0x6c, 0x6d, 0x76, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x5d, - 0xd5, 0x5b, 0xc6, 0x5d, 0x00, 0x00, 0x02, 0x58, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x84, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, - 0xd5, 0x5b, 0xc6, 0x5d, 0xd5, 0x5b, 0xc6, 0x5d, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x20, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x5d, - 0xd5, 0x5b, 0xc6, 0x5d, 0x00, 0x00, 0xbb, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x31, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x20, 0x41, 0x75, 0x64, 0x69, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0xc7, 0x6d, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x10, 0x73, 0x6d, 0x68, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x8b, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0x3f, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x2f, 0x61, 0x63, 0x2d, 0x33, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x80, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0b, 0x64, 0x61, 0x63, - 0x33, 0x0c, 0x3d, 0x40, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, - 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, - 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65, 0x78, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }, - "no tracks found", - }, - { - "ac-3", - []byte{ - 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, - 0x6d, 0x70, 0x34, 0x32, 0x00, 0x00, 0x00, 0x01, - 0x6d, 0x70, 0x34, 0x31, 0x6d, 0x70, 0x34, 0x32, - 0x69, 0x73, 0x6f, 0x6d, 0x68, 0x6c, 0x73, 0x66, - 0x00, 0x00, 0x02, 0x22, 0x6d, 0x6f, 0x6f, 0x76, - 0x00, 0x00, 0x00, 0x6c, 0x6d, 0x76, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x5d, - 0xd5, 0x5b, 0xc6, 0x5d, 0x00, 0x00, 0x02, 0x58, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x86, - 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, - 0x74, 0x6b, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, - 0xd5, 0x5b, 0xc6, 0x5d, 0xd5, 0x5b, 0xc6, 0x5d, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x01, 0x22, 0x6d, 0x64, 0x69, 0x61, - 0x00, 0x00, 0x00, 0x20, 0x6d, 0x64, 0x68, 0x64, - 0x00, 0x00, 0x00, 0x00, 0xd5, 0x5b, 0xc6, 0x5d, - 0xd5, 0x5b, 0xc6, 0x5d, 0x00, 0x00, 0xbb, 0x80, - 0x00, 0x00, 0x00, 0x00, 0x55, 0xc4, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x31, 0x68, 0x64, 0x6c, 0x72, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x73, 0x6f, 0x75, 0x6e, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x43, 0x6f, 0x72, 0x65, 0x20, 0x4d, 0x65, 0x64, - 0x69, 0x61, 0x20, 0x41, 0x75, 0x64, 0x69, 0x6f, - 0x00, 0x00, 0x00, 0x00, 0xc9, 0x6d, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x10, 0x73, 0x6d, 0x68, - 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, - 0x66, 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, - 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, - 0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x8d, 0x73, 0x74, 0x62, 0x6c, 0x00, 0x00, 0x00, - 0x41, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x31, 0x65, 0x63, 0x2d, 0x33, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x00, 0xbb, 0x80, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0d, 0x64, 0x65, 0x63, - 0x33, 0x00, 0xc0, 0x20, 0x0f, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x73, 0x74, 0x74, 0x73, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x14, 0x73, 0x74, 0x73, 0x7a, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, - 0x63, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x6d, 0x76, - 0x65, 0x78, 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, - 0x65, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - }, - "no tracks found", - }, - } { - t.Run(ca.name, func(t *testing.T) { - var init Init - err := init.Unmarshal(ca.byts) - require.EqualError(t, err, ca.err) - }) - } -} diff --git a/internal/hls/fmp4/init_track.go b/internal/hls/fmp4/init_track.go deleted file mode 100644 index df7a6e0c..00000000 --- a/internal/hls/fmp4/init_track.go +++ /dev/null @@ -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{}) // - 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{ // - 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{ // - 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{}) // - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.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{ // - HandlerType: [4]byte{'v', 'i', 'd', 'e'}, - Name: "VideoHandler", - }) - if err != nil { - return err - } - - case *format.MPEG4Audio, *format.Opus: - _, err = w.WriteBox(&gomp4.Hdlr{ // - HandlerType: [4]byte{'s', 'o', 'u', 'n'}, - Name: "SoundHandler", - }) - if err != nil { - return err - } - } - - _, err = w.writeBoxStart(&gomp4.Minf{}) // - if err != nil { - return err - } - - switch track.Format.(type) { - case *format.H264, *format.H265: - _, err = w.WriteBox(&gomp4.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{ // - }) - if err != nil { - return err - } - } - - _, err = w.writeBoxStart(&gomp4.Dinf{}) // - if err != nil { - return err - } - - _, err = w.writeBoxStart(&gomp4.Dref{ // - EntryCount: 1, - }) - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Url{ // - FullBox: gomp4.FullBox{ - Flags: [3]byte{0, 0, 1}, - }, - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - _, err = w.writeBoxStart(&gomp4.Stbl{}) // - if err != nil { - return err - } - - _, err = w.writeBoxStart(&gomp4.Stsd{ // - EntryCount: 1, - }) - if err != nil { - return err - } - - switch ttrack := track.Format.(type) { - case *format.H264: - _, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // - 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{ // - 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{ // - MaxBitrate: 1000000, - AvgBitrate: 1000000, - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - case *format.H265: - _, err = w.writeBoxStart(&gomp4.VisualSampleEntry{ // - 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{ // - 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{ // - MaxBitrate: 1000000, - AvgBitrate: 1000000, - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - case *format.MPEG4Audio: - _, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // - 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{ // - 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{ // - MaxBitrate: 128825, - AvgBitrate: 128825, - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - case *format.Opus: - _, err = w.writeBoxStart(&gomp4.AudioSampleEntry{ // - 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{ // - OutputChannelCount: uint8(ttrack.ChannelCount), - PreSkip: 312, - InputSampleRate: uint32(ttrack.ClockRate()), - }) - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Btrt{ // - MaxBitrate: 128825, - AvgBitrate: 128825, - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Stts{ // - }) - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Stsc{ // - }) - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Stsz{ // - }) - if err != nil { - return err - } - - _, err = w.WriteBox(&gomp4.Stco{ // - }) - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - err = w.writeBoxEnd() // - if err != nil { - return err - } - - return nil -} diff --git a/internal/hls/fmp4/mp4_writer.go b/internal/hls/fmp4/mp4_writer.go deleted file mode 100644 index 39312578..00000000 --- a/internal/hls/fmp4/mp4_writer.go +++ /dev/null @@ -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() -} diff --git a/internal/hls/fmp4/part.go b/internal/hls/fmp4/part.go deleted file mode 100644 index 9deaf569..00000000 --- a/internal/hls/fmp4/part.go +++ /dev/null @@ -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{}) // - if err != nil { - return nil, err - } - - _, err = w.WriteBox(&gomp4.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() // - if err != nil { - return nil, err - } - - mdat := &gomp4.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 -} diff --git a/internal/hls/fmp4/part_test.go b/internal/hls/fmp4/part_test.go deleted file mode 100644 index ea7b5c46..00000000 --- a/internal/hls/fmp4/part_test.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/fmp4/part_track.go b/internal/hls/fmp4/part_track.go deleted file mode 100644 index 1c996cb1..00000000 --- a/internal/hls/fmp4/part_track.go +++ /dev/null @@ -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{}) // - if err != nil { - return nil, 0, err - } - - flags := 0 - - _, err = w.WriteBox(&gomp4.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{ // - 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{ // - 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() // - if err != nil { - return nil, 0, err - } - - return trun, trunOffset, nil -} diff --git a/internal/hls/m3u8/m3u8.go b/internal/hls/m3u8/m3u8.go deleted file mode 100644 index 4955a871..00000000 --- a/internal/hls/m3u8/m3u8.go +++ /dev/null @@ -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") -} diff --git a/internal/hls/mpegts/tracks.go b/internal/hls/mpegts/tracks.go deleted file mode 100644 index cb3a0fc5..00000000 --- a/internal/hls/mpegts/tracks.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/mpegts/writer.go b/internal/hls/mpegts/writer.go deleted file mode 100644 index 4f112612..00000000 --- a/internal/hls/mpegts/writer.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/mpegts/writer_test.go b/internal/hls/mpegts/writer_test.go deleted file mode 100644 index 9e750b34..00000000 --- a/internal/hls/mpegts/writer_test.go +++ /dev/null @@ -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) - }) -} diff --git a/internal/hls/mpegtstimedec/decoder.go b/internal/hls/mpegtstimedec/decoder.go deleted file mode 100644 index 6edbbd6d..00000000 --- a/internal/hls/mpegtstimedec/decoder.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/mpegtstimedec/decoder_test.go b/internal/hls/mpegtstimedec/decoder_test.go deleted file mode 100644 index 5e3c4444..00000000 --- a/internal/hls/mpegtstimedec/decoder_test.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer.go b/internal/hls/muxer.go deleted file mode 100644 index 771c01b2..00000000 --- a/internal/hls/muxer.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer_primary_playlist.go b/internal/hls/muxer_primary_playlist.go deleted file mode 100644 index 5724abb1..00000000 --- a/internal/hls/muxer_primary_playlist.go +++ /dev/null @@ -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")) - }(), - } -} diff --git a/internal/hls/muxer_test.go b/internal/hls/muxer_test.go deleted file mode 100644 index 61b36356..00000000 --- a/internal/hls/muxer_test.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer_variant.go b/internal/hls/muxer_variant.go deleted file mode 100644 index a7881f75..00000000 --- a/internal/hls/muxer_variant.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/muxer_variant_fmp4.go b/internal/hls/muxer_variant_fmp4.go deleted file mode 100644 index b0bf17c4..00000000 --- a/internal/hls/muxer_variant_fmp4.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer_variant_fmp4_part.go b/internal/hls/muxer_variant_fmp4_part.go deleted file mode 100644 index 9a25dad3..00000000 --- a/internal/hls/muxer_variant_fmp4_part.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer_variant_fmp4_playlist.go b/internal/hls/muxer_variant_fmp4_playlist.go deleted file mode 100644 index 5d801873..00000000 --- a/internal/hls/muxer_variant_fmp4_playlist.go +++ /dev/null @@ -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() -} diff --git a/internal/hls/muxer_variant_fmp4_segment.go b/internal/hls/muxer_variant_fmp4_segment.go deleted file mode 100644 index 4e226cd2..00000000 --- a/internal/hls/muxer_variant_fmp4_segment.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/muxer_variant_fmp4_segmenter.go b/internal/hls/muxer_variant_fmp4_segmenter.go deleted file mode 100644 index 8ea8496d..00000000 --- a/internal/hls/muxer_variant_fmp4_segmenter.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/muxer_variant_mpegts.go b/internal/hls/muxer_variant_mpegts.go deleted file mode 100644 index b92ebcb1..00000000 --- a/internal/hls/muxer_variant_mpegts.go +++ /dev/null @@ -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) -} diff --git a/internal/hls/muxer_variant_mpegts_playlist.go b/internal/hls/muxer_variant_mpegts_playlist.go deleted file mode 100644 index dbe25f57..00000000 --- a/internal/hls/muxer_variant_mpegts_playlist.go +++ /dev/null @@ -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() -} diff --git a/internal/hls/muxer_variant_mpegts_segment.go b/internal/hls/muxer_variant_mpegts_segment.go deleted file mode 100644 index 4cc822d7..00000000 --- a/internal/hls/muxer_variant_mpegts_segment.go +++ /dev/null @@ -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 -} diff --git a/internal/hls/muxer_variant_mpegts_segmenter.go b/internal/hls/muxer_variant_mpegts_segmenter.go deleted file mode 100644 index a9542499..00000000 --- a/internal/hls/muxer_variant_mpegts_segmenter.go +++ /dev/null @@ -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 -}