[feature] Enable basic video support (mp4 only) (#1274)
* [feature] basic video support * fix missing semicolon * replace text shadow with stacked icons Co-authored-by: f0x <f0x@cthu.lu>
This commit is contained in:
parent
0f38e7c9b0
commit
2bbc64be43
|
@ -42,7 +42,7 @@ Here's a screenshot of the instance landing page!
|
|||
- [Credits](#credits)
|
||||
- [Libraries](#libraries)
|
||||
- [Image Attribution](#image-attribution)
|
||||
- [Developers](#developers)
|
||||
- [Team](#team)
|
||||
- [Special Thanks](#special-thanks)
|
||||
- [Sponsorship + Funding](#sponsorship--funding)
|
||||
- [OpenCollective](#opencollective)
|
||||
|
@ -210,6 +210,7 @@ For bugs and feature requests, please check to see if there's [already an issue]
|
|||
|
||||
The following libraries and frameworks are used by GoToSocial, with gratitude 💕
|
||||
|
||||
- [abema/go-mp4](https://github.com/abema/go-mp4); mp4 parsing. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
- [buckket/go-blurhash](https://github.com/buckket/go-blurhash); used for generating image blurhashes. [GPL-3.0 License](https://spdx.org/licenses/GPL-3.0-only.html).
|
||||
- [coreos/go-oidc](https://github.com/coreos/go-oidc); OIDC client library. [Apache-2.0 License](https://spdx.org/licenses/Apache-2.0.html).
|
||||
- [disintegration/imaging](https://github.com/disintegration/imaging); image resizing. [MIT License](https://spdx.org/licenses/MIT.html).
|
||||
|
|
1
go.mod
1
go.mod
|
@ -13,6 +13,7 @@ require (
|
|||
codeberg.org/gruf/go-mutexes v1.1.4
|
||||
codeberg.org/gruf/go-runners v1.3.1
|
||||
codeberg.org/gruf/go-store/v2 v2.0.10
|
||||
github.com/abema/go-mp4 v0.8.0
|
||||
github.com/buckket/go-blurhash v1.1.0
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/cornelk/hashmap v1.0.8
|
||||
|
|
8
go.sum
8
go.sum
|
@ -110,6 +110,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
|
|||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/abema/go-mp4 v0.8.0 h1:JHYkOvTfBpTnqJHiFFOXe8d6wiFy5MtDnA10fgccNqY=
|
||||
github.com/abema/go-mp4 v0.8.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
|
@ -491,6 +493,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
|
|||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
|
@ -568,6 +572,7 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
|||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/superseriousbusiness/activity v1.2.1-gts h1:wh7v0zYa1mJmqB35PSfvgl4cs51Dh5PyfKvcZLSxMQU=
|
||||
github.com/superseriousbusiness/activity v1.2.1-gts/go.mod h1:AZw0Xb4Oju8rmaJCZ21gc5CPg47MmNgyac+Hx5jo8VM=
|
||||
github.com/superseriousbusiness/exif-terminator v0.5.0 h1:57SO/geyaOl2v/lJSQLVcQbdghpyFuK8ZTtaHL81fUQ=
|
||||
|
@ -1177,11 +1182,14 @@ gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD
|
|||
gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
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=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
|
|
|
@ -65,7 +65,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Example Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"someone@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||
|
@ -95,7 +95,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"Geoff's Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
||||
|
@ -125,7 +125,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch4() {
|
||||
|
@ -216,7 +216,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
b, err := io.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
suite.Equal(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/assets/logo.png","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, string(b))
|
||||
}
|
||||
|
||||
func (suite *InstancePatchTestSuite) TestInstancePatch7() {
|
||||
|
@ -279,7 +279,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
}
|
||||
suite.NotEmpty(instanceAccount.AvatarMediaAttachmentID)
|
||||
|
||||
expectedInstanceResponse := fmt.Sprintf(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/fileserver/%s/attachment/original/%s.gif","thumbnail_type":"image/gif","thumbnail_description":"A bouncing little green peglin.","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, instanceAccount.ID, instanceAccount.AvatarMediaAttachmentID)
|
||||
expectedInstanceResponse := fmt.Sprintf(`{"uri":"http://localhost:8080","account_domain":"localhost:8080","title":"GoToSocial Testrig Instance","description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","short_description":"\u003cp\u003eThis is the GoToSocial testrig. It doesn't federate or anything.\u003c/p\u003e\u003cp\u003eWhen the testrig is shut down, all data on it will be deleted.\u003c/p\u003e\u003cp\u003eDon't use this in production!\u003c/p\u003e","email":"admin@example.org","version":"0.0.0-testrig","registrations":true,"approval_required":true,"invites_enabled":false,"configuration":{"statuses":{"max_characters":5000,"max_media_attachments":6,"characters_reserved_per_url":25},"media_attachments":{"supported_mime_types":["image/jpeg","image/gif","image/png","image/webp","video/mp4"],"image_size_limit":10485760,"image_matrix_limit":16777216,"video_size_limit":41943040,"video_frame_rate_limit":60,"video_matrix_limit":16777216},"polls":{"max_options":6,"max_characters_per_option":50,"min_expiration":300,"max_expiration":2629746},"accounts":{"allow_custom_css":true},"emojis":{"emoji_size_limit":51200}},"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":2,"status_count":16,"user_count":4},"thumbnail":"http://localhost:8080/fileserver/%s/attachment/original/%s.gif","thumbnail_type":"image/gif","thumbnail_description":"A bouncing little green peglin.","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[],"enable_rss":true,"role":"admin"},"max_toot_chars":5000}`, instanceAccount.ID, instanceAccount.AvatarMediaAttachmentID)
|
||||
suite.Equal(expectedInstanceResponse, string(b))
|
||||
}
|
||||
|
||||
|
|
|
@ -38,16 +38,7 @@ const (
|
|||
thumbnailMaxHeight = 512
|
||||
)
|
||||
|
||||
type imageMeta struct {
|
||||
width int
|
||||
height int
|
||||
size int
|
||||
aspect float64
|
||||
blurhash string // defined only for calls to deriveThumbnail if createBlurhash is true
|
||||
small []byte // defined only for calls to deriveStaticEmoji or deriveThumbnail
|
||||
}
|
||||
|
||||
func decodeGif(r io.Reader) (*imageMeta, error) {
|
||||
func decodeGif(r io.Reader) (*mediaMeta, error) {
|
||||
gif, err := gif.DecodeAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -59,7 +50,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) {
|
|||
size := width * height
|
||||
aspect := float64(width) / float64(height)
|
||||
|
||||
return &imageMeta{
|
||||
return &mediaMeta{
|
||||
width: width,
|
||||
height: height,
|
||||
size: size,
|
||||
|
@ -67,7 +58,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func decodeImage(r io.Reader, contentType string) (*imageMeta, error) {
|
||||
func decodeImage(r io.Reader, contentType string) (*mediaMeta, error) {
|
||||
var i image.Image
|
||||
var err error
|
||||
|
||||
|
@ -96,7 +87,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) {
|
|||
size := width * height
|
||||
aspect := float64(width) / float64(height)
|
||||
|
||||
return &imageMeta{
|
||||
return &mediaMeta{
|
||||
width: width,
|
||||
height: height,
|
||||
size: size,
|
||||
|
@ -104,8 +95,37 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// deriveThumbnail returns a byte slice and metadata for a thumbnail
|
||||
// of a given jpeg, png, gif or webp, or an error if something goes wrong.
|
||||
// deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png.
|
||||
func deriveStaticEmoji(r io.Reader, contentType string) (*mediaMeta, error) {
|
||||
var i image.Image
|
||||
var err error
|
||||
|
||||
switch contentType {
|
||||
case mimeImagePng:
|
||||
i, err = StrippedPngDecode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case mimeImageGif:
|
||||
i, err = gif.Decode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("content type %s not allowed for emoji", contentType)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
if err := png.Encode(out, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mediaMeta{
|
||||
small: out.Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// deriveThumbnailFromImage returns a byte slice and metadata for a thumbnail
|
||||
// of a given piece of media, or an error if something goes wrong.
|
||||
//
|
||||
// If createBlurhash is true, then a blurhash will also be generated from a tiny
|
||||
// version of the image. This costs precious CPU cycles, so only use it if you
|
||||
|
@ -113,7 +133,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) {
|
|||
//
|
||||
// If createBlurhash is false, then the blurhash field on the returned ImageAndMeta
|
||||
// will be an empty string.
|
||||
func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*imageMeta, error) {
|
||||
func deriveThumbnailFromImage(r io.Reader, contentType string, createBlurhash bool) (*mediaMeta, error) {
|
||||
var i image.Image
|
||||
var err error
|
||||
|
||||
|
@ -126,7 +146,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima
|
|||
})
|
||||
i, err = imaging.Decode(strippedPngReader, imaging.AutoOrientation(true))
|
||||
default:
|
||||
err = fmt.Errorf("content type %s can't be thumbnailed", contentType)
|
||||
err = fmt.Errorf("content type %s can't be thumbnailed as an image", contentType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -149,7 +169,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima
|
|||
size := thumbX * thumbY
|
||||
aspect := float64(thumbX) / float64(thumbY)
|
||||
|
||||
im := &imageMeta{
|
||||
im := &mediaMeta{
|
||||
width: thumbX,
|
||||
height: thumbY,
|
||||
size: size,
|
||||
|
@ -178,32 +198,3 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima
|
|||
|
||||
return im, nil
|
||||
}
|
||||
|
||||
// deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png.
|
||||
func deriveStaticEmoji(r io.Reader, contentType string) (*imageMeta, error) {
|
||||
var i image.Image
|
||||
var err error
|
||||
|
||||
switch contentType {
|
||||
case mimeImagePng:
|
||||
i, err = StrippedPngDecode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case mimeImageGif:
|
||||
i, err = gif.Decode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("content type %s not allowed for emoji", contentType)
|
||||
}
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
if err := png.Encode(out, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &imageMeta{
|
||||
small: out.Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -376,6 +376,78 @@ func (suite *ManagerTestSuite) TestSimpleJpegProcessBlocking() {
|
|||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestSlothVineProcessBlocking() {
|
||||
ctx := context.Background()
|
||||
|
||||
data := func(_ context.Context) (io.ReadCloser, int64, error) {
|
||||
// load bytes from a test video
|
||||
b, err := os.ReadFile("./test/test-mp4-original.mp4")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return io.NopCloser(bytes.NewBuffer(b)), int64(len(b)), nil
|
||||
}
|
||||
|
||||
accountID := "01FS1X72SK9ZPW0J1QQ68BD264"
|
||||
|
||||
// process the media with no additional info provided
|
||||
processingMedia, err := suite.manager.ProcessMedia(ctx, data, nil, accountID, nil)
|
||||
suite.NoError(err)
|
||||
// fetch the attachment id from the processing media
|
||||
attachmentID := processingMedia.AttachmentID()
|
||||
|
||||
// do a blocking call to fetch the attachment
|
||||
attachment, err := processingMedia.LoadAttachment(ctx)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(attachment)
|
||||
|
||||
// make sure it's got the stuff set on it that we expect
|
||||
// the attachment ID and accountID we expect
|
||||
suite.Equal(attachmentID, attachment.ID)
|
||||
suite.Equal(accountID, attachment.AccountID)
|
||||
|
||||
// file meta should be correctly derived from the video
|
||||
suite.EqualValues(gtsmodel.Original{
|
||||
Width: 338, Height: 240, Size: 81120, Aspect: 1.4083333333333334,
|
||||
}, attachment.FileMeta.Original)
|
||||
suite.EqualValues(gtsmodel.Small{
|
||||
Width: 338, Height: 240, Size: 81120, Aspect: 1.4083333333333334,
|
||||
}, attachment.FileMeta.Small)
|
||||
suite.Equal("video/mp4", attachment.File.ContentType)
|
||||
suite.Equal("image/jpeg", attachment.Thumbnail.ContentType)
|
||||
suite.Equal(312413, attachment.File.FileSize)
|
||||
suite.Equal("", attachment.Blurhash)
|
||||
|
||||
// now make sure the attachment is in the database
|
||||
dbAttachment, err := suite.db.GetAttachmentByID(ctx, attachmentID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(dbAttachment)
|
||||
|
||||
// make sure the processed file is in storage
|
||||
processedFullBytes, err := suite.storage.Get(ctx, attachment.File.Path)
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(processedFullBytes)
|
||||
|
||||
// load the processed bytes from our test folder, to compare
|
||||
processedFullBytesExpected, err := os.ReadFile("./test/test-mp4-processed.mp4")
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(processedFullBytesExpected)
|
||||
|
||||
// the bytes in storage should be what we expected
|
||||
suite.Equal(processedFullBytesExpected, processedFullBytes)
|
||||
|
||||
// now do the same for the thumbnail and make sure it's what we expected
|
||||
processedThumbnailBytes, err := suite.storage.Get(ctx, attachment.Thumbnail.Path)
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(processedThumbnailBytes)
|
||||
|
||||
processedThumbnailBytesExpected, err := os.ReadFile("./test/test-mp4-thumbnail.jpg")
|
||||
suite.NoError(err)
|
||||
suite.NotEmpty(processedThumbnailBytesExpected)
|
||||
|
||||
suite.Equal(processedThumbnailBytesExpected, processedThumbnailBytes)
|
||||
}
|
||||
|
||||
func (suite *ManagerTestSuite) TestSimpleJpegProcessBlockingNoContentLengthGiven() {
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
|
@ -88,11 +88,11 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.loadThumb(ctx); err != nil {
|
||||
if err := p.loadFullSize(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.loadFullSize(ctx); err != nil {
|
||||
if err := p.loadThumb(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -128,7 +128,6 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error {
|
|||
switch processState(thumbState) {
|
||||
case received:
|
||||
// we haven't processed a thumbnail for this media yet so do it now
|
||||
|
||||
// check if we need to create a blurhash or if there's already one set
|
||||
var createBlurhash bool
|
||||
if p.attachment.Blurhash == "" {
|
||||
|
@ -136,27 +135,46 @@ func (p *ProcessingMedia) loadThumb(ctx context.Context) error {
|
|||
createBlurhash = true
|
||||
}
|
||||
|
||||
// stream the original file out of storage
|
||||
stored, err := p.storage.GetStream(ctx, p.attachment.File.Path)
|
||||
if err != nil {
|
||||
p.err = fmt.Errorf("loadThumb: error fetching file from storage: %s", err)
|
||||
var (
|
||||
thumb *mediaMeta
|
||||
err error
|
||||
)
|
||||
switch ct := p.attachment.File.ContentType; ct {
|
||||
case mimeImageJpeg, mimeImagePng, mimeImageWebp, mimeImageGif:
|
||||
// thumbnail the image from the original stored full size version
|
||||
stored, err := p.storage.GetStream(ctx, p.attachment.File.Path)
|
||||
if err != nil {
|
||||
p.err = fmt.Errorf("loadThumb: error fetching file from storage: %s", err)
|
||||
atomic.StoreInt32(&p.thumbState, int32(errored))
|
||||
return p.err
|
||||
}
|
||||
|
||||
thumb, err = deriveThumbnailFromImage(stored, ct, createBlurhash)
|
||||
|
||||
// try to close the stored stream we had open, no matter what
|
||||
if closeErr := stored.Close(); closeErr != nil {
|
||||
log.Errorf("error closing stream: %s", closeErr)
|
||||
}
|
||||
|
||||
// now check if we managed to get a thumbnail
|
||||
if err != nil {
|
||||
p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err)
|
||||
atomic.StoreInt32(&p.thumbState, int32(errored))
|
||||
return p.err
|
||||
}
|
||||
case mimeVideoMp4:
|
||||
// create a generic thumbnail based on video height + width
|
||||
thumb, err = deriveThumbnailFromVideo(p.attachment.FileMeta.Original.Height, p.attachment.FileMeta.Original.Width)
|
||||
if err != nil {
|
||||
p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err)
|
||||
atomic.StoreInt32(&p.thumbState, int32(errored))
|
||||
return p.err
|
||||
}
|
||||
default:
|
||||
p.err = fmt.Errorf("loadThumb: content type %s not a processible image type", ct)
|
||||
atomic.StoreInt32(&p.thumbState, int32(errored))
|
||||
return p.err
|
||||
}
|
||||
defer stored.Close()
|
||||
|
||||
// stream the file from storage straight into the derive thumbnail function
|
||||
thumb, err := deriveThumbnail(stored, p.attachment.File.ContentType, createBlurhash)
|
||||
if err != nil {
|
||||
p.err = fmt.Errorf("loadThumb: error deriving thumbnail: %s", err)
|
||||
atomic.StoreInt32(&p.thumbState, int32(errored))
|
||||
return p.err
|
||||
}
|
||||
|
||||
// Close stored media now we're done
|
||||
if err := stored.Close(); err != nil {
|
||||
log.Errorf("loadThumb: error closing stored full size: %s", err)
|
||||
}
|
||||
|
||||
// put the thumbnail in storage
|
||||
if err := p.storage.Put(ctx, p.attachment.Thumbnail.Path, thumb.small); err != nil && err != storage.ErrAlreadyExists {
|
||||
|
@ -195,7 +213,7 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) error {
|
|||
switch processState(fullSizeState) {
|
||||
case received:
|
||||
var err error
|
||||
var decoded *imageMeta
|
||||
var decoded *mediaMeta
|
||||
|
||||
// stream the original file out of storage...
|
||||
stored, err := p.storage.GetStream(ctx, p.attachment.File.Path)
|
||||
|
@ -218,6 +236,8 @@ func (p *ProcessingMedia) loadFullSize(ctx context.Context) error {
|
|||
decoded, err = decodeImage(stored, ct)
|
||||
case mimeImageGif:
|
||||
decoded, err = decodeGif(stored)
|
||||
case mimeVideoMp4:
|
||||
decoded, err = decodeVideo(stored, ct)
|
||||
default:
|
||||
err = fmt.Errorf("loadFullSize: content type %s not a processible image type", ct)
|
||||
}
|
||||
|
@ -295,7 +315,7 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// bail if this is a type we can't process
|
||||
if !supportedImage(contentType) {
|
||||
if !supportedAttachment(contentType) {
|
||||
return fmt.Errorf("store: media type %s not (yet) supported", contentType)
|
||||
}
|
||||
|
||||
|
@ -338,6 +358,10 @@ func (p *ProcessingMedia) store(ctx context.Context) error {
|
|||
// can't terminate if we don't know the file size, so just store the multiReader
|
||||
readerToStore = multiReader
|
||||
}
|
||||
case mimeMp4:
|
||||
p.attachment.Type = gtsmodel.FileTypeVideo
|
||||
// nothing to terminate, we can just store the multireader
|
||||
readerToStore = multiReader
|
||||
default:
|
||||
return fmt.Errorf("store: couldn't process %s", extension)
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -34,6 +34,7 @@ const maxFileHeaderBytes = 261
|
|||
// mime consts
|
||||
const (
|
||||
mimeImage = "image"
|
||||
mimeVideo = "video"
|
||||
|
||||
mimeJpeg = "jpeg"
|
||||
mimeImageJpeg = mimeImage + "/" + mimeJpeg
|
||||
|
@ -46,6 +47,9 @@ const (
|
|||
|
||||
mimeWebp = "webp"
|
||||
mimeImageWebp = mimeImage + "/" + mimeWebp
|
||||
|
||||
mimeMp4 = "mp4"
|
||||
mimeVideoMp4 = mimeVideo + "/" + mimeMp4
|
||||
)
|
||||
|
||||
type processState int32
|
||||
|
@ -128,3 +132,12 @@ type DataFunc func(ctx context.Context) (reader io.ReadCloser, fileSize int64, e
|
|||
//
|
||||
// This can be set to nil, and will then not be executed.
|
||||
type PostDataCallbackFunc func(ctx context.Context) error
|
||||
|
||||
type mediaMeta struct {
|
||||
width int
|
||||
height int
|
||||
size int
|
||||
aspect float64
|
||||
blurhash string
|
||||
small []byte
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ func AllSupportedMIMETypes() []string {
|
|||
mimeImageGif,
|
||||
mimeImagePng,
|
||||
mimeImageWebp,
|
||||
mimeVideoMp4,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,16 +62,10 @@ func parseContentType(fileHeader []byte) (string, error) {
|
|||
return kind.MIME.Value, nil
|
||||
}
|
||||
|
||||
// supportedImage checks mime type of an image against a slice of accepted types,
|
||||
// and returns True if the mime type is accepted.
|
||||
func supportedImage(mimeType string) bool {
|
||||
acceptedImageTypes := []string{
|
||||
mimeImageJpeg,
|
||||
mimeImageGif,
|
||||
mimeImagePng,
|
||||
mimeImageWebp,
|
||||
}
|
||||
for _, accepted := range acceptedImageTypes {
|
||||
// supportedAttachment checks mime type of an attachment against a
|
||||
// slice of accepted types, and returns True if the mime type is accepted.
|
||||
func supportedAttachment(mimeType string) bool {
|
||||
for _, accepted := range AllSupportedMIMETypes() {
|
||||
if mimeType == accepted {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package media
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
var thumbFill = color.RGBA{42, 43, 47, 0} // the color to fill video thumbnails with
|
||||
|
||||
func decodeVideo(r io.Reader, contentType string) (*mediaMeta, error) {
|
||||
// We'll need a readseeker to decode the video. We can get a readseeker
|
||||
// without burning too much mem by first copying the reader into a temp file.
|
||||
// First create the file in the temporary directory...
|
||||
tempFile, err := os.CreateTemp(os.TempDir(), "gotosocial-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create temporary file while decoding video: %w", err)
|
||||
}
|
||||
tempFileName := tempFile.Name()
|
||||
|
||||
// Make sure to clean up the temporary file when we're done with it
|
||||
defer func() {
|
||||
if err := tempFile.Close(); err != nil {
|
||||
log.Errorf("could not close file %s: %s", tempFileName, err)
|
||||
}
|
||||
if err := os.Remove(tempFileName); err != nil {
|
||||
log.Errorf("could not remove file %s: %s", tempFileName, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Now copy the entire reader we've been provided into the
|
||||
// temporary file; we won't use the reader again after this.
|
||||
if _, err := io.Copy(tempFile, r); err != nil {
|
||||
return nil, fmt.Errorf("could not copy video reader into temporary file %s: %w", tempFileName, err)
|
||||
}
|
||||
|
||||
// define some vars we need to pull the width/height out of the video
|
||||
var (
|
||||
height int
|
||||
width int
|
||||
readHandler = getReadHandler(&height, &width)
|
||||
)
|
||||
|
||||
// do the actual decoding here, providing the temporary file we created as readseeker
|
||||
if _, err := mp4.ReadBoxStructure(tempFile, readHandler); err != nil {
|
||||
return nil, fmt.Errorf("parsing video data: %w", err)
|
||||
}
|
||||
|
||||
// width + height should now be updated by the readHandler
|
||||
return &mediaMeta{
|
||||
width: width,
|
||||
height: height,
|
||||
size: height * width,
|
||||
aspect: float64(width) / float64(height),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getReadHandler returns a handler function that updates the underling
|
||||
// values of the given height and width int pointers to the hightest and
|
||||
// widest points of the video.
|
||||
func getReadHandler(height *int, width *int) func(h *mp4.ReadHandle) (interface{}, error) {
|
||||
return func(rh *mp4.ReadHandle) (interface{}, error) {
|
||||
if rh.BoxInfo.Type == mp4.BoxTypeTkhd() {
|
||||
box, _, err := rh.ReadPayload()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read mp4 payload: %w", err)
|
||||
}
|
||||
|
||||
tkhd, ok := box.(*mp4.Tkhd)
|
||||
if !ok {
|
||||
return nil, errors.New("box was not of type *mp4.Tkhd")
|
||||
}
|
||||
|
||||
// if height + width of this box are greater than what
|
||||
// we have stored, then update our stored values
|
||||
if h := int(tkhd.GetHeight()); h > *height {
|
||||
*height = h
|
||||
}
|
||||
|
||||
if w := int(tkhd.GetWidth()); w > *width {
|
||||
*width = w
|
||||
}
|
||||
}
|
||||
|
||||
if rh.BoxInfo.IsSupportedType() {
|
||||
return rh.Expand()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deriveThumbnailFromVideo(height int, width int) (*mediaMeta, error) {
|
||||
// create a rectangle with the same dimensions as the video
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// fill the rectangle with our desired fill color
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{thumbFill}, image.Point{}, draw.Src)
|
||||
|
||||
// we can get away with using extremely poor quality for this monocolor thumbnail
|
||||
out := &bytes.Buffer{}
|
||||
if err := jpeg.Encode(out, img, &jpeg.Options{Quality: 1}); err != nil {
|
||||
return nil, fmt.Errorf("error encoding video thumbnail: %w", err)
|
||||
}
|
||||
|
||||
return &mediaMeta{
|
||||
width: width,
|
||||
height: height,
|
||||
size: width * height,
|
||||
aspect: float64(width) / float64(height),
|
||||
small: out.Bytes(),
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
vendor
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 AbemaTV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,153 @@
|
|||
go-mp4
|
||||
------
|
||||
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/abema/go-mp4.svg)](https://pkg.go.dev/github.com/abema/go-mp4)
|
||||
![Test](https://github.com/abema/go-mp4/actions/workflows/test.yml/badge.svg)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/abema/go-mp4/badge.svg)](https://coveralls.io/github/abema/go-mp4)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/abema/go-mp4)](https://goreportcard.com/report/github.com/abema/go-mp4)
|
||||
|
||||
go-mp4 is Go library for reading and writing MP4.
|
||||
|
||||
## Integration with your Go application
|
||||
|
||||
### Reading
|
||||
|
||||
You can parse MP4 file as follows:
|
||||
|
||||
```go
|
||||
// expand all boxes
|
||||
_, err := mp4.ReadBoxStructure(file, func(h *mp4.ReadHandle) (interface{}, error) {
|
||||
fmt.Println("depth", len(h.Path))
|
||||
|
||||
// Box Type (e.g. "mdhd", "tfdt", "mdat")
|
||||
fmt.Println("type", h.BoxInfo.Type.String())
|
||||
|
||||
// Box Size
|
||||
fmt.Println("size", h.BoxInfo.Size)
|
||||
|
||||
if h.BoxInfo.IsSupportedType() {
|
||||
// Payload
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
str, err := mp4.Stringify(box, h.BoxInfo.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println("payload", str)
|
||||
|
||||
// Expands children
|
||||
return h.Expand()
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
```
|
||||
|
||||
```go
|
||||
// extract specific boxes
|
||||
boxes, err := mp4.ExtractBoxWithPayload(file, nil, mp4.BoxPath{mp4.BoxTypeMoov(), mp4.BoxTypeTrak(), mp4.BoxTypeTkhd()})
|
||||
if err != nil {
|
||||
:
|
||||
}
|
||||
for _, box := range boxes {
|
||||
tkhd := box.Payload.(*mp4.Tkhd)
|
||||
fmt.Println("track ID:", tkhd.TrackID)
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// get basic informations
|
||||
info, err := mp4.Probe(bufseekio.NewReadSeeker(file, 1024, 4))
|
||||
if err != nil {
|
||||
:
|
||||
}
|
||||
fmt.Println("track num:", len(info.Tracks))
|
||||
```
|
||||
|
||||
### Writing
|
||||
|
||||
Writer helps you to write box tree.
|
||||
The following sample code edits emsg box and writes to another file.
|
||||
|
||||
```go
|
||||
r := bufseekio.NewReadSeeker(inputFile, 128*1024, 4)
|
||||
w := mp4.NewWriter(outputFile)
|
||||
_, err = mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
|
||||
switch h.BoxInfo.Type {
|
||||
case mp4.BoxTypeEmsg():
|
||||
// write box size and box type
|
||||
_, err := w.StartBox(&h.BoxInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// read payload
|
||||
box, _, err := h.ReadPayload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// update MessageData
|
||||
emsg := box.(*mp4.Emsg)
|
||||
emsg.MessageData = []byte("hello world")
|
||||
// write box playload
|
||||
if _, err := mp4.Marshal(w, emsg, h.BoxInfo.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// rewrite box size
|
||||
_, err = w.EndBox()
|
||||
return nil, err
|
||||
default:
|
||||
// copy all
|
||||
return nil, w.CopyBox(r, &h.BoxInfo)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### User-defined Boxes
|
||||
|
||||
You can create additional box definition as follows:
|
||||
|
||||
```go
|
||||
func BoxTypeXxxx() BoxType { return mp4.StrToBoxType("xxxx") }
|
||||
|
||||
func init() {
|
||||
mp4.AddBoxDef(&Xxxx{}, 0)
|
||||
}
|
||||
|
||||
type Xxxx struct {
|
||||
FullBox `mp4:"0,extend"`
|
||||
UI32 uint32 `mp4:"1,size=32"`
|
||||
ByteArray []byte `mp4:"2,size=8,len=dynamic"`
|
||||
}
|
||||
|
||||
func (*Xxxx) GetType() BoxType {
|
||||
return BoxTypeXxxx()
|
||||
}
|
||||
```
|
||||
|
||||
### Buffering
|
||||
|
||||
go-mp4 has no buffering feature for I/O.
|
||||
If you should reduce Read function calls, you can wrap the io.ReadSeeker by [bufseekio](https://github.com/sunfish-shogi/bufseekio).
|
||||
|
||||
## Command Line Tool
|
||||
|
||||
Install mp4tool as follows:
|
||||
|
||||
```sh
|
||||
go install github.com/abema/go-mp4/mp4tool@latest
|
||||
|
||||
mp4tool -help
|
||||
```
|
||||
|
||||
For example, `mp4tool dump MP4_FILE_NAME` command prints MP4 box tree as follows:
|
||||
|
||||
```
|
||||
[moof] Size=504
|
||||
[mfhd] Size=16 Version=0 Flags=0x000000 SequenceNumber=1
|
||||
[traf] Size=480
|
||||
[tfhd] Size=28 Version=0 Flags=0x020038 TrackID=1 DefaultSampleDuration=9000 DefaultSampleSize=33550 DefaultSampleFlags=0x1010000
|
||||
[tfdt] Size=20 Version=1 Flags=0x000000 BaseMediaDecodeTimeV1=0
|
||||
[trun] Size=424 ... (use -a option to show all)
|
||||
[mdat] Size=44569 Data=[...] (use -mdat option to expand)
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
package mp4
|
||||
|
||||
type IAnyType interface {
|
||||
IBox
|
||||
SetType(BoxType)
|
||||
}
|
||||
|
||||
type AnyTypeBox struct {
|
||||
Box
|
||||
Type BoxType
|
||||
}
|
||||
|
||||
func (e *AnyTypeBox) GetType() BoxType {
|
||||
return e.Type
|
||||
}
|
||||
|
||||
func (e *AnyTypeBox) SetType(boxType BoxType) {
|
||||
e.Type = boxType
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package bitio
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidAlignment = errors.New("invalid alignment")
|
||||
ErrDiscouragedReader = errors.New("discouraged reader implementation")
|
||||
)
|
|
@ -0,0 +1,97 @@
|
|||
package bitio
|
||||
|
||||
import "io"
|
||||
|
||||
type Reader interface {
|
||||
io.Reader
|
||||
|
||||
// alignment:
|
||||
// |-1-byte-block-|--------------|--------------|--------------|
|
||||
// |<-offset->|<-------------------width---------------------->|
|
||||
ReadBits(width uint) (data []byte, err error)
|
||||
|
||||
ReadBit() (bit bool, err error)
|
||||
}
|
||||
|
||||
type ReadSeeker interface {
|
||||
Reader
|
||||
io.Seeker
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
reader io.Reader
|
||||
octet byte
|
||||
width uint
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader) Reader {
|
||||
return &reader{reader: r}
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (n int, err error) {
|
||||
if r.width != 0 {
|
||||
return 0, ErrInvalidAlignment
|
||||
}
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
func (r *reader) ReadBits(size uint) ([]byte, error) {
|
||||
bytes := (size + 7) / 8
|
||||
data := make([]byte, bytes)
|
||||
offset := (bytes * 8) - (size)
|
||||
|
||||
for i := uint(0); i < size; i++ {
|
||||
bit, err := r.ReadBit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
byteIdx := (offset + i) / 8
|
||||
bitIdx := 7 - (offset+i)%8
|
||||
if bit {
|
||||
data[byteIdx] |= 0x1 << bitIdx
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *reader) ReadBit() (bool, error) {
|
||||
if r.width == 0 {
|
||||
buf := make([]byte, 1)
|
||||
if n, err := r.reader.Read(buf); err != nil {
|
||||
return false, err
|
||||
} else if n != 1 {
|
||||
return false, ErrDiscouragedReader
|
||||
}
|
||||
r.octet = buf[0]
|
||||
r.width = 8
|
||||
}
|
||||
|
||||
r.width--
|
||||
return (r.octet>>r.width)&0x01 != 0, nil
|
||||
}
|
||||
|
||||
type readSeeker struct {
|
||||
reader
|
||||
seeker io.Seeker
|
||||
}
|
||||
|
||||
func NewReadSeeker(r io.ReadSeeker) ReadSeeker {
|
||||
return &readSeeker{
|
||||
reader: reader{reader: r},
|
||||
seeker: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *readSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
if whence == io.SeekCurrent && r.reader.width != 0 {
|
||||
return 0, ErrInvalidAlignment
|
||||
}
|
||||
n, err := r.seeker.Seek(offset, whence)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
r.reader.width = 0
|
||||
return n, nil
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package bitio
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Writer interface {
|
||||
io.Writer
|
||||
|
||||
// alignment:
|
||||
// |-1-byte-block-|--------------|--------------|--------------|
|
||||
// |<-offset->|<-------------------width---------------------->|
|
||||
WriteBits(data []byte, width uint) error
|
||||
|
||||
WriteBit(bit bool) error
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
writer io.Writer
|
||||
octet byte
|
||||
width uint
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer) Writer {
|
||||
return &writer{writer: w}
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
if w.width != 0 {
|
||||
return 0, ErrInvalidAlignment
|
||||
}
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
|
||||
func (w *writer) WriteBits(data []byte, width uint) error {
|
||||
length := uint(len(data)) * 8
|
||||
offset := length - width
|
||||
for i := offset; i < length; i++ {
|
||||
oi := i / 8
|
||||
if err := w.WriteBit((data[oi]>>(7-i%8))&0x01 != 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *writer) WriteBit(bit bool) error {
|
||||
if bit {
|
||||
w.octet |= 0x1 << (7 - w.width)
|
||||
}
|
||||
w.width++
|
||||
|
||||
if w.width == 8 {
|
||||
if _, err := w.writer.Write([]byte{w.octet}); err != nil {
|
||||
return err
|
||||
}
|
||||
w.octet = 0x00
|
||||
w.width = 0
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/abema/go-mp4/bitio"
|
||||
)
|
||||
|
||||
const LengthUnlimited = math.MaxUint32
|
||||
|
||||
type ICustomFieldObject interface {
|
||||
// GetFieldSize returns size of dynamic field
|
||||
GetFieldSize(name string, ctx Context) uint
|
||||
|
||||
// GetFieldLength returns length of dynamic field
|
||||
GetFieldLength(name string, ctx Context) uint
|
||||
|
||||
// IsOptFieldEnabled check whether if the optional field is enabled
|
||||
IsOptFieldEnabled(name string, ctx Context) bool
|
||||
|
||||
// StringifyField returns field value as string
|
||||
StringifyField(name string, indent string, depth int, ctx Context) (string, bool)
|
||||
|
||||
IsPString(name string, bytes []byte, remainingSize uint64, ctx Context) bool
|
||||
|
||||
BeforeUnmarshal(r io.ReadSeeker, size uint64, ctx Context) (n uint64, override bool, err error)
|
||||
|
||||
OnReadField(name string, r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error)
|
||||
|
||||
OnWriteField(name string, w bitio.Writer, ctx Context) (wbits uint64, override bool, err error)
|
||||
}
|
||||
|
||||
type BaseCustomFieldObject struct {
|
||||
}
|
||||
|
||||
// GetFieldSize returns size of dynamic field
|
||||
func (box *BaseCustomFieldObject) GetFieldSize(string, Context) uint {
|
||||
panic(errors.New("GetFieldSize not implemented"))
|
||||
}
|
||||
|
||||
// GetFieldLength returns length of dynamic field
|
||||
func (box *BaseCustomFieldObject) GetFieldLength(string, Context) uint {
|
||||
panic(errors.New("GetFieldLength not implemented"))
|
||||
}
|
||||
|
||||
// IsOptFieldEnabled check whether if the optional field is enabled
|
||||
func (box *BaseCustomFieldObject) IsOptFieldEnabled(string, Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// StringifyField returns field value as string
|
||||
func (box *BaseCustomFieldObject) StringifyField(string, string, int, Context) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (*BaseCustomFieldObject) IsPString(name string, bytes []byte, remainingSize uint64, ctx Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (*BaseCustomFieldObject) BeforeUnmarshal(io.ReadSeeker, uint64, Context) (uint64, bool, error) {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
func (*BaseCustomFieldObject) OnReadField(string, bitio.ReadSeeker, uint64, Context) (uint64, bool, error) {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
func (*BaseCustomFieldObject) OnWriteField(string, bitio.Writer, Context) (uint64, bool, error) {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// IImmutableBox is common interface of box
|
||||
type IImmutableBox interface {
|
||||
ICustomFieldObject
|
||||
|
||||
// GetVersion returns the box version
|
||||
GetVersion() uint8
|
||||
|
||||
// GetFlags returns the flags
|
||||
GetFlags() uint32
|
||||
|
||||
// CheckFlag checks the flag status
|
||||
CheckFlag(uint32) bool
|
||||
|
||||
// GetType returns the BoxType
|
||||
GetType() BoxType
|
||||
}
|
||||
|
||||
// IBox is common interface of box
|
||||
type IBox interface {
|
||||
IImmutableBox
|
||||
|
||||
// SetVersion sets the box version
|
||||
SetVersion(uint8)
|
||||
|
||||
// SetFlags sets the flags
|
||||
SetFlags(uint32)
|
||||
|
||||
// AddFlag adds the flag
|
||||
AddFlag(uint32)
|
||||
|
||||
// RemoveFlag removes the flag
|
||||
RemoveFlag(uint32)
|
||||
}
|
||||
|
||||
type Box struct {
|
||||
BaseCustomFieldObject
|
||||
}
|
||||
|
||||
// GetVersion returns the box version
|
||||
func (box *Box) GetVersion() uint8 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetVersion sets the box version
|
||||
func (box *Box) SetVersion(uint8) {
|
||||
}
|
||||
|
||||
// GetFlags returns the flags
|
||||
func (box *Box) GetFlags() uint32 {
|
||||
return 0x000000
|
||||
}
|
||||
|
||||
// CheckFlag checks the flag status
|
||||
func (box *Box) CheckFlag(flag uint32) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetFlags sets the flags
|
||||
func (box *Box) SetFlags(uint32) {
|
||||
}
|
||||
|
||||
// AddFlag adds the flag
|
||||
func (box *Box) AddFlag(flag uint32) {
|
||||
}
|
||||
|
||||
// RemoveFlag removes the flag
|
||||
func (box *Box) RemoveFlag(flag uint32) {
|
||||
}
|
||||
|
||||
// FullBox is ISOBMFF FullBox
|
||||
type FullBox struct {
|
||||
BaseCustomFieldObject
|
||||
Version uint8 `mp4:"0,size=8"`
|
||||
Flags [3]byte `mp4:"1,size=8"`
|
||||
}
|
||||
|
||||
// GetVersion returns the box version
|
||||
func (box *FullBox) GetVersion() uint8 {
|
||||
return box.Version
|
||||
}
|
||||
|
||||
// SetVersion sets the box version
|
||||
func (box *FullBox) SetVersion(version uint8) {
|
||||
box.Version = version
|
||||
}
|
||||
|
||||
// GetFlags returns the flags
|
||||
func (box *FullBox) GetFlags() uint32 {
|
||||
flag := uint32(box.Flags[0]) << 16
|
||||
flag ^= uint32(box.Flags[1]) << 8
|
||||
flag ^= uint32(box.Flags[2])
|
||||
return flag
|
||||
}
|
||||
|
||||
// CheckFlag checks the flag status
|
||||
func (box *FullBox) CheckFlag(flag uint32) bool {
|
||||
return box.GetFlags()&flag != 0
|
||||
}
|
||||
|
||||
// SetFlags sets the flags
|
||||
func (box *FullBox) SetFlags(flags uint32) {
|
||||
box.Flags[0] = byte(flags >> 16)
|
||||
box.Flags[1] = byte(flags >> 8)
|
||||
box.Flags[2] = byte(flags)
|
||||
}
|
||||
|
||||
// AddFlag adds the flag
|
||||
func (box *FullBox) AddFlag(flag uint32) {
|
||||
box.SetFlags(box.GetFlags() | flag)
|
||||
}
|
||||
|
||||
// RemoveFlag removes the flag
|
||||
func (box *FullBox) RemoveFlag(flag uint32) {
|
||||
box.SetFlags(box.GetFlags() & (^flag))
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
// IsQuickTimeCompatible represents whether ftyp.compatible_brands contains "qt ".
|
||||
IsQuickTimeCompatible bool
|
||||
|
||||
// UnderWave represents whether current box is under the wave box.
|
||||
UnderWave bool
|
||||
|
||||
// UnderIlst represents whether current box is under the ilst box.
|
||||
UnderIlst bool
|
||||
|
||||
// UnderIlstMeta represents whether current box is under the metadata box under the ilst box.
|
||||
UnderIlstMeta bool
|
||||
|
||||
// UnderIlstFreeMeta represents whether current box is under "----" box.
|
||||
UnderIlstFreeMeta bool
|
||||
|
||||
// UnderUdta represents whether current box is under the udta box.
|
||||
UnderUdta bool
|
||||
}
|
||||
|
||||
// BoxInfo has common infomations of box
|
||||
type BoxInfo struct {
|
||||
// Offset specifies an offset of the box in a file.
|
||||
Offset uint64
|
||||
|
||||
// Size specifies size(bytes) of box.
|
||||
Size uint64
|
||||
|
||||
// HeaderSize specifies size(bytes) of common fields which are defined as "Box" class member at ISO/IEC 14496-12.
|
||||
HeaderSize uint64
|
||||
|
||||
// Type specifies box type which is represented by 4 characters.
|
||||
Type BoxType
|
||||
|
||||
// ExtendToEOF is set true when Box.size is zero. It means that end of box equals to end of file.
|
||||
ExtendToEOF bool
|
||||
|
||||
// Context would be set by ReadBoxStructure, not ReadBoxInfo.
|
||||
Context
|
||||
}
|
||||
|
||||
func (bi *BoxInfo) IsSupportedType() bool {
|
||||
return bi.Type.IsSupported(bi.Context)
|
||||
}
|
||||
|
||||
const (
|
||||
SmallHeaderSize = 8
|
||||
LargeHeaderSize = 16
|
||||
)
|
||||
|
||||
// WriteBoxInfo writes common fields which are defined as "Box" class member at ISO/IEC 14496-12.
|
||||
// This function ignores bi.Offset and returns BoxInfo which contains real Offset and recalculated Size/HeaderSize.
|
||||
func WriteBoxInfo(w io.WriteSeeker, bi *BoxInfo) (*BoxInfo, error) {
|
||||
offset, err := w.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if bi.ExtendToEOF {
|
||||
data = make([]byte, SmallHeaderSize)
|
||||
} else if bi.Size <= math.MaxUint32 && bi.HeaderSize != LargeHeaderSize {
|
||||
data = make([]byte, SmallHeaderSize)
|
||||
binary.BigEndian.PutUint32(data, uint32(bi.Size))
|
||||
} else {
|
||||
data = make([]byte, LargeHeaderSize)
|
||||
binary.BigEndian.PutUint32(data, 1)
|
||||
binary.BigEndian.PutUint64(data[SmallHeaderSize:], bi.Size)
|
||||
}
|
||||
data[4] = bi.Type[0]
|
||||
data[5] = bi.Type[1]
|
||||
data[6] = bi.Type[2]
|
||||
data[7] = bi.Type[3]
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BoxInfo{
|
||||
Offset: uint64(offset),
|
||||
Size: bi.Size - bi.HeaderSize + uint64(len(data)),
|
||||
HeaderSize: uint64(len(data)),
|
||||
Type: bi.Type,
|
||||
ExtendToEOF: bi.ExtendToEOF,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadBoxInfo reads common fields which are defined as "Box" class member at ISO/IEC 14496-12.
|
||||
func ReadBoxInfo(r io.ReadSeeker) (*BoxInfo, error) {
|
||||
offset, err := r.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bi := &BoxInfo{
|
||||
Offset: uint64(offset),
|
||||
}
|
||||
|
||||
// read 8 bytes
|
||||
buf := bytes.NewBuffer(make([]byte, 0, SmallHeaderSize))
|
||||
if _, err := io.CopyN(buf, r, SmallHeaderSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi.HeaderSize += SmallHeaderSize
|
||||
|
||||
// pick size and type
|
||||
data := buf.Bytes()
|
||||
bi.Size = uint64(binary.BigEndian.Uint32(data))
|
||||
bi.Type = BoxType{data[4], data[5], data[6], data[7]}
|
||||
|
||||
if bi.Size == 0 {
|
||||
// box extends to end of file
|
||||
offsetEOF, err := r.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi.Size = uint64(offsetEOF) - bi.Offset
|
||||
bi.ExtendToEOF = true
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if bi.Size == 1 {
|
||||
// read more 8 bytes
|
||||
buf.Reset()
|
||||
if _, err := io.CopyN(buf, r, LargeHeaderSize-SmallHeaderSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi.HeaderSize += LargeHeaderSize - SmallHeaderSize
|
||||
bi.Size = binary.BigEndian.Uint64(buf.Bytes())
|
||||
}
|
||||
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (bi *BoxInfo) SeekToStart(s io.Seeker) (int64, error) {
|
||||
return s.Seek(int64(bi.Offset), io.SeekStart)
|
||||
}
|
||||
|
||||
func (bi *BoxInfo) SeekToPayload(s io.Seeker) (int64, error) {
|
||||
return s.Seek(int64(bi.Offset+bi.HeaderSize), io.SeekStart)
|
||||
}
|
||||
|
||||
func (bi *BoxInfo) SeekToEnd(s io.Seeker) (int64, error) {
|
||||
return s.Seek(int64(bi.Offset+bi.Size), io.SeekStart)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,98 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type BoxInfoWithPayload struct {
|
||||
Info BoxInfo
|
||||
Payload IBox
|
||||
}
|
||||
|
||||
func ExtractBoxWithPayload(r io.ReadSeeker, parent *BoxInfo, path BoxPath) ([]*BoxInfoWithPayload, error) {
|
||||
return ExtractBoxesWithPayload(r, parent, []BoxPath{path})
|
||||
}
|
||||
|
||||
func ExtractBoxesWithPayload(r io.ReadSeeker, parent *BoxInfo, paths []BoxPath) ([]*BoxInfoWithPayload, error) {
|
||||
bis, err := ExtractBoxes(r, parent, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs := make([]*BoxInfoWithPayload, 0, len(bis))
|
||||
for _, bi := range bis {
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ctx Context
|
||||
if parent != nil {
|
||||
ctx = parent.Context
|
||||
}
|
||||
box, _, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bs = append(bs, &BoxInfoWithPayload{
|
||||
Info: *bi,
|
||||
Payload: box,
|
||||
})
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func ExtractBox(r io.ReadSeeker, parent *BoxInfo, path BoxPath) ([]*BoxInfo, error) {
|
||||
return ExtractBoxes(r, parent, []BoxPath{path})
|
||||
}
|
||||
|
||||
func ExtractBoxes(r io.ReadSeeker, parent *BoxInfo, paths []BoxPath) ([]*BoxInfo, error) {
|
||||
if len(paths) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i := range paths {
|
||||
if len(paths[i]) == 0 {
|
||||
return nil, errors.New("box path must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
boxes := make([]*BoxInfo, 0, 8)
|
||||
|
||||
handler := func(handle *ReadHandle) (interface{}, error) {
|
||||
path := handle.Path
|
||||
if parent != nil {
|
||||
path = path[1:]
|
||||
}
|
||||
if handle.BoxInfo.Type == BoxTypeAny() {
|
||||
return nil, nil
|
||||
}
|
||||
fm, m := matchPath(paths, path)
|
||||
if m {
|
||||
boxes = append(boxes, &handle.BoxInfo)
|
||||
}
|
||||
|
||||
if fm {
|
||||
if _, err := handle.Expand(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if parent != nil {
|
||||
_, err := ReadBoxStructureFromInternal(r, parent, handler)
|
||||
return boxes, err
|
||||
}
|
||||
_, err := ReadBoxStructure(r, handler)
|
||||
return boxes, err
|
||||
}
|
||||
|
||||
func matchPath(paths []BoxPath, path BoxPath) (forwardMatch bool, match bool) {
|
||||
for i := range paths {
|
||||
fm, m := path.compareWith(paths[i])
|
||||
forwardMatch = forwardMatch || fm
|
||||
match = match || m
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
stringType uint8
|
||||
fieldFlag uint16
|
||||
)
|
||||
|
||||
const (
|
||||
stringType_C stringType = iota
|
||||
stringType_C_P
|
||||
|
||||
fieldString fieldFlag = 1 << iota // 0
|
||||
fieldExtend // 1
|
||||
fieldDec // 2
|
||||
fieldHex // 3
|
||||
fieldISO639_2 // 4
|
||||
fieldUUID // 5
|
||||
fieldHidden // 6
|
||||
fieldOptDynamic // 7
|
||||
fieldVarint // 8
|
||||
fieldSizeDynamic // 9
|
||||
fieldLengthDynamic // 10
|
||||
)
|
||||
|
||||
type field struct {
|
||||
children []*field
|
||||
name string
|
||||
cnst string
|
||||
order int
|
||||
optFlag uint32
|
||||
nOptFlag uint32
|
||||
size uint
|
||||
length uint
|
||||
flags fieldFlag
|
||||
strType stringType
|
||||
version uint8
|
||||
nVersion uint8
|
||||
}
|
||||
|
||||
func (f *field) set(flag fieldFlag) {
|
||||
f.flags |= flag
|
||||
}
|
||||
|
||||
func (f *field) is(flag fieldFlag) bool {
|
||||
return f.flags&flag != 0
|
||||
}
|
||||
|
||||
func buildFields(box IImmutableBox) []*field {
|
||||
t := reflect.TypeOf(box).Elem()
|
||||
return buildFieldsStruct(t)
|
||||
}
|
||||
|
||||
func buildFieldsStruct(t reflect.Type) []*field {
|
||||
fs := make([]*field, 0, 8)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
ft := t.Field(i).Type
|
||||
tag, ok := t.Field(i).Tag.Lookup("mp4")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
f := buildField(t.Field(i).Name, tag)
|
||||
f.children = buildFieldsAny(ft)
|
||||
fs = append(fs, f)
|
||||
}
|
||||
sort.SliceStable(fs, func(i, j int) bool {
|
||||
return fs[i].order < fs[j].order
|
||||
})
|
||||
return fs
|
||||
}
|
||||
|
||||
func buildFieldsAny(t reflect.Type) []*field {
|
||||
switch t.Kind() {
|
||||
case reflect.Struct:
|
||||
return buildFieldsStruct(t)
|
||||
case reflect.Ptr, reflect.Array, reflect.Slice:
|
||||
return buildFieldsAny(t.Elem())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildField(fieldName string, tag string) *field {
|
||||
f := &field{
|
||||
name: fieldName,
|
||||
}
|
||||
tagMap := parseFieldTag(tag)
|
||||
for key, val := range tagMap {
|
||||
if val != "" {
|
||||
continue
|
||||
}
|
||||
if order, err := strconv.Atoi(key); err == nil {
|
||||
f.order = order
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if val, contained := tagMap["string"]; contained {
|
||||
f.set(fieldString)
|
||||
if val == "c_p" {
|
||||
f.strType = stringType_C_P
|
||||
fmt.Fprint(os.Stderr, "go-mp4: string=c_p tag is deprecated!! See https://github.com/abema/go-mp4/issues/76\n")
|
||||
}
|
||||
}
|
||||
|
||||
if _, contained := tagMap["varint"]; contained {
|
||||
f.set(fieldVarint)
|
||||
}
|
||||
|
||||
if val, contained := tagMap["opt"]; contained {
|
||||
if val == "dynamic" {
|
||||
f.set(fieldOptDynamic)
|
||||
} else {
|
||||
base := 10
|
||||
if strings.HasPrefix(val, "0x") {
|
||||
val = val[2:]
|
||||
base = 16
|
||||
}
|
||||
opt, err := strconv.ParseUint(val, base, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.optFlag = uint32(opt)
|
||||
}
|
||||
}
|
||||
|
||||
if val, contained := tagMap["nopt"]; contained {
|
||||
base := 10
|
||||
if strings.HasPrefix(val, "0x") {
|
||||
val = val[2:]
|
||||
base = 16
|
||||
}
|
||||
nopt, err := strconv.ParseUint(val, base, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.nOptFlag = uint32(nopt)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["extend"]; contained {
|
||||
f.set(fieldExtend)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["dec"]; contained {
|
||||
f.set(fieldDec)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["hex"]; contained {
|
||||
f.set(fieldHex)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["iso639-2"]; contained {
|
||||
f.set(fieldISO639_2)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["uuid"]; contained {
|
||||
f.set(fieldUUID)
|
||||
}
|
||||
|
||||
if _, contained := tagMap["hidden"]; contained {
|
||||
f.set(fieldHidden)
|
||||
}
|
||||
|
||||
if val, contained := tagMap["const"]; contained {
|
||||
f.cnst = val
|
||||
}
|
||||
|
||||
f.version = anyVersion
|
||||
if val, contained := tagMap["ver"]; contained {
|
||||
ver, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.version = uint8(ver)
|
||||
}
|
||||
|
||||
f.nVersion = anyVersion
|
||||
if val, contained := tagMap["nver"]; contained {
|
||||
ver, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.nVersion = uint8(ver)
|
||||
}
|
||||
|
||||
if val, contained := tagMap["size"]; contained {
|
||||
if val == "dynamic" {
|
||||
f.set(fieldSizeDynamic)
|
||||
} else {
|
||||
size, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.size = uint(size)
|
||||
}
|
||||
}
|
||||
|
||||
f.length = LengthUnlimited
|
||||
if val, contained := tagMap["len"]; contained {
|
||||
if val == "dynamic" {
|
||||
f.set(fieldLengthDynamic)
|
||||
} else {
|
||||
l, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f.length = uint(l)
|
||||
}
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func parseFieldTag(str string) map[string]string {
|
||||
tag := make(map[string]string, 8)
|
||||
|
||||
list := strings.Split(str, ",")
|
||||
for _, e := range list {
|
||||
kv := strings.SplitN(e, "=", 2)
|
||||
if len(kv) == 2 {
|
||||
tag[strings.Trim(kv[0], " ")] = strings.Trim(kv[1], " ")
|
||||
} else {
|
||||
tag[strings.Trim(kv[0], " ")] = ""
|
||||
}
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
type fieldInstance struct {
|
||||
field
|
||||
cfo ICustomFieldObject
|
||||
}
|
||||
|
||||
func resolveFieldInstance(f *field, box IImmutableBox, parent reflect.Value, ctx Context) *fieldInstance {
|
||||
fi := fieldInstance{
|
||||
field: *f,
|
||||
}
|
||||
|
||||
cfo, ok := parent.Addr().Interface().(ICustomFieldObject)
|
||||
if ok {
|
||||
fi.cfo = cfo
|
||||
} else {
|
||||
fi.cfo = box
|
||||
}
|
||||
|
||||
if fi.is(fieldSizeDynamic) {
|
||||
fi.size = fi.cfo.GetFieldSize(f.name, ctx)
|
||||
}
|
||||
|
||||
if fi.is(fieldLengthDynamic) {
|
||||
fi.length = fi.cfo.GetFieldLength(f.name, ctx)
|
||||
}
|
||||
|
||||
return &fi
|
||||
}
|
||||
|
||||
func isTargetField(box IImmutableBox, fi *fieldInstance, ctx Context) bool {
|
||||
if box.GetVersion() != anyVersion {
|
||||
if fi.version != anyVersion && box.GetVersion() != fi.version {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi.nVersion != anyVersion && box.GetVersion() == fi.nVersion {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if fi.optFlag != 0 && box.GetFlags()&fi.optFlag == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi.nOptFlag != 0 && box.GetFlags()&fi.nOptFlag != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if fi.is(fieldOptDynamic) && !fi.cfo.IsOptFieldEnabled(fi.name, ctx) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,639 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/abema/go-mp4/bitio"
|
||||
)
|
||||
|
||||
const (
|
||||
anyVersion = math.MaxUint8
|
||||
)
|
||||
|
||||
var ErrUnsupportedBoxVersion = errors.New("unsupported box version")
|
||||
|
||||
type marshaller struct {
|
||||
writer bitio.Writer
|
||||
wbits uint64
|
||||
src IImmutableBox
|
||||
ctx Context
|
||||
}
|
||||
|
||||
func Marshal(w io.Writer, src IImmutableBox, ctx Context) (n uint64, err error) {
|
||||
boxDef := src.GetType().getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return 0, ErrBoxInfoNotFound
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(src).Elem()
|
||||
|
||||
m := &marshaller{
|
||||
writer: bitio.NewWriter(w),
|
||||
src: src,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := m.marshalStruct(v, boxDef.fields); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if m.wbits%8 != 0 {
|
||||
return 0, fmt.Errorf("box size is not multiple of 8 bits: type=%s, bits=%d", src.GetType().String(), m.wbits)
|
||||
}
|
||||
|
||||
return m.wbits / 8, nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshal(v reflect.Value, fi *fieldInstance) error {
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Ptr:
|
||||
return m.marshalPtr(v, fi)
|
||||
case reflect.Struct:
|
||||
return m.marshalStruct(v, fi.children)
|
||||
case reflect.Array:
|
||||
return m.marshalArray(v, fi)
|
||||
case reflect.Slice:
|
||||
return m.marshalSlice(v, fi)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return m.marshalInt(v, fi)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return m.marshalUint(v, fi)
|
||||
case reflect.Bool:
|
||||
return m.marshalBool(v, fi)
|
||||
case reflect.String:
|
||||
return m.marshalString(v)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", v.Type().Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalPtr(v reflect.Value, fi *fieldInstance) error {
|
||||
return m.marshal(v.Elem(), fi)
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalStruct(v reflect.Value, fs []*field) error {
|
||||
for _, f := range fs {
|
||||
fi := resolveFieldInstance(f, m.src, v, m.ctx)
|
||||
|
||||
if !isTargetField(m.src, fi, m.ctx) {
|
||||
continue
|
||||
}
|
||||
|
||||
wbits, override, err := fi.cfo.OnWriteField(f.name, m.writer, m.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += wbits
|
||||
if override {
|
||||
continue
|
||||
}
|
||||
|
||||
err = m.marshal(v.FieldByName(f.name), fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalArray(v reflect.Value, fi *fieldInstance) error {
|
||||
size := v.Type().Size()
|
||||
for i := 0; i < int(size)/int(v.Type().Elem().Size()); i++ {
|
||||
var err error
|
||||
err = m.marshal(v.Index(i), fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalSlice(v reflect.Value, fi *fieldInstance) error {
|
||||
length := uint64(v.Len())
|
||||
if fi.length != LengthUnlimited {
|
||||
if length < uint64(fi.length) {
|
||||
return fmt.Errorf("the slice has too few elements: required=%d actual=%d", fi.length, length)
|
||||
}
|
||||
length = uint64(fi.length)
|
||||
}
|
||||
|
||||
elemType := v.Type().Elem()
|
||||
if elemType.Kind() == reflect.Uint8 && fi.size == 8 && m.wbits%8 == 0 {
|
||||
if _, err := io.CopyN(m.writer, bytes.NewBuffer(v.Bytes()), int64(length)); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += length * 8
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < int(length); i++ {
|
||||
m.marshal(v.Index(i), fi)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalInt(v reflect.Value, fi *fieldInstance) error {
|
||||
signed := v.Int()
|
||||
|
||||
if fi.is(fieldVarint) {
|
||||
return errors.New("signed varint is unsupported")
|
||||
}
|
||||
|
||||
signBit := signed < 0
|
||||
val := uint64(signed)
|
||||
for i := uint(0); i < fi.size; i += 8 {
|
||||
v := val
|
||||
size := uint(8)
|
||||
if fi.size > i+8 {
|
||||
v = v >> (fi.size - (i + 8))
|
||||
} else if fi.size < i+8 {
|
||||
size = fi.size - i
|
||||
}
|
||||
|
||||
// set sign bit
|
||||
if i == 0 {
|
||||
if signBit {
|
||||
v |= 0x1 << (size - 1)
|
||||
} else {
|
||||
v &= 0x1<<(size-1) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.writer.WriteBits([]byte{byte(v)}, size); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += uint64(size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalUint(v reflect.Value, fi *fieldInstance) error {
|
||||
val := v.Uint()
|
||||
|
||||
if fi.is(fieldVarint) {
|
||||
m.writeUvarint(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := uint(0); i < fi.size; i += 8 {
|
||||
v := val
|
||||
size := uint(8)
|
||||
if fi.size > i+8 {
|
||||
v = v >> (fi.size - (i + 8))
|
||||
} else if fi.size < i+8 {
|
||||
size = fi.size - i
|
||||
}
|
||||
if err := m.writer.WriteBits([]byte{byte(v)}, size); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += uint64(size)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalBool(v reflect.Value, fi *fieldInstance) error {
|
||||
var val byte
|
||||
if v.Bool() {
|
||||
val = 0xff
|
||||
} else {
|
||||
val = 0x00
|
||||
}
|
||||
if err := m.writer.WriteBits([]byte{val}, fi.size); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += uint64(fi.size)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) marshalString(v reflect.Value) error {
|
||||
data := []byte(v.String())
|
||||
for _, b := range data {
|
||||
if err := m.writer.WriteBits([]byte{b}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += 8
|
||||
}
|
||||
// null character
|
||||
if err := m.writer.WriteBits([]byte{0x00}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += 8
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *marshaller) writeUvarint(u uint64) error {
|
||||
for i := 21; i > 0; i -= 7 {
|
||||
if err := m.writer.WriteBits([]byte{(byte(u >> uint(i))) | 0x80}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += 8
|
||||
}
|
||||
|
||||
if err := m.writer.WriteBits([]byte{byte(u) & 0x7f}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
m.wbits += 8
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type unmarshaller struct {
|
||||
reader bitio.ReadSeeker
|
||||
dst IBox
|
||||
size uint64
|
||||
rbits uint64
|
||||
ctx Context
|
||||
}
|
||||
|
||||
func UnmarshalAny(r io.ReadSeeker, boxType BoxType, payloadSize uint64, ctx Context) (box IBox, n uint64, err error) {
|
||||
dst, err := boxType.New(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n, err = Unmarshal(r, payloadSize, dst, ctx)
|
||||
return dst, n, err
|
||||
}
|
||||
|
||||
func Unmarshal(r io.ReadSeeker, payloadSize uint64, dst IBox, ctx Context) (n uint64, err error) {
|
||||
boxDef := dst.GetType().getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return 0, ErrBoxInfoNotFound
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(dst).Elem()
|
||||
|
||||
dst.SetVersion(anyVersion)
|
||||
|
||||
u := &unmarshaller{
|
||||
reader: bitio.NewReadSeeker(r),
|
||||
dst: dst,
|
||||
size: payloadSize,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if n, override, err := dst.BeforeUnmarshal(r, payloadSize, u.ctx); err != nil {
|
||||
return 0, err
|
||||
} else if override {
|
||||
return n, nil
|
||||
} else {
|
||||
u.rbits = n * 8
|
||||
}
|
||||
|
||||
sn, err := r.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := u.unmarshalStruct(v, boxDef.fields); err != nil {
|
||||
if err == ErrUnsupportedBoxVersion {
|
||||
r.Seek(sn, io.SeekStart)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if u.rbits%8 != 0 {
|
||||
return 0, fmt.Errorf("box size is not multiple of 8 bits: type=%s, size=%d, bits=%d", dst.GetType().String(), u.size, u.rbits)
|
||||
}
|
||||
|
||||
if u.rbits > u.size*8 {
|
||||
return 0, fmt.Errorf("overrun error: type=%s, size=%d, bits=%d", dst.GetType().String(), u.size, u.rbits)
|
||||
}
|
||||
|
||||
return u.rbits / 8, nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshal(v reflect.Value, fi *fieldInstance) error {
|
||||
var err error
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Ptr:
|
||||
err = u.unmarshalPtr(v, fi)
|
||||
case reflect.Struct:
|
||||
err = u.unmarshalStructInternal(v, fi)
|
||||
case reflect.Array:
|
||||
err = u.unmarshalArray(v, fi)
|
||||
case reflect.Slice:
|
||||
err = u.unmarshalSlice(v, fi)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
err = u.unmarshalInt(v, fi)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
err = u.unmarshalUint(v, fi)
|
||||
case reflect.Bool:
|
||||
err = u.unmarshalBool(v, fi)
|
||||
case reflect.String:
|
||||
err = u.unmarshalString(v, fi)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", v.Type().Kind())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalPtr(v reflect.Value, fi *fieldInstance) error {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
return u.unmarshal(v.Elem(), fi)
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalStructInternal(v reflect.Value, fi *fieldInstance) error {
|
||||
if fi.size != 0 && fi.size%8 == 0 {
|
||||
u2 := *u
|
||||
u2.size = uint64(fi.size / 8)
|
||||
u2.rbits = 0
|
||||
if err := u2.unmarshalStruct(v, fi.children); err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += u2.rbits
|
||||
if u2.rbits != uint64(fi.size) {
|
||||
return errors.New("invalid alignment")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return u.unmarshalStruct(v, fi.children)
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalStruct(v reflect.Value, fs []*field) error {
|
||||
for _, f := range fs {
|
||||
fi := resolveFieldInstance(f, u.dst, v, u.ctx)
|
||||
|
||||
if !isTargetField(u.dst, fi, u.ctx) {
|
||||
continue
|
||||
}
|
||||
|
||||
rbits, override, err := fi.cfo.OnReadField(f.name, u.reader, u.size*8-u.rbits, u.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += rbits
|
||||
if override {
|
||||
continue
|
||||
}
|
||||
|
||||
err = u.unmarshal(v.FieldByName(f.name), fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v.FieldByName(f.name).Type() == reflect.TypeOf(FullBox{}) && !u.dst.GetType().IsSupportedVersion(u.dst.GetVersion(), u.ctx) {
|
||||
return ErrUnsupportedBoxVersion
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalArray(v reflect.Value, fi *fieldInstance) error {
|
||||
size := v.Type().Size()
|
||||
for i := 0; i < int(size)/int(v.Type().Elem().Size()); i++ {
|
||||
var err error
|
||||
err = u.unmarshal(v.Index(i), fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalSlice(v reflect.Value, fi *fieldInstance) error {
|
||||
var slice reflect.Value
|
||||
elemType := v.Type().Elem()
|
||||
|
||||
length := uint64(fi.length)
|
||||
if fi.length == LengthUnlimited {
|
||||
if fi.size != 0 {
|
||||
left := (u.size)*8 - u.rbits
|
||||
if left%uint64(fi.size) != 0 {
|
||||
return errors.New("invalid alignment")
|
||||
}
|
||||
length = left / uint64(fi.size)
|
||||
} else {
|
||||
length = 0
|
||||
}
|
||||
}
|
||||
|
||||
if length > math.MaxInt32 {
|
||||
return fmt.Errorf("out of memory: requestedSize=%d", length)
|
||||
}
|
||||
|
||||
if fi.size != 0 && fi.size%8 == 0 && u.rbits%8 == 0 && elemType.Kind() == reflect.Uint8 && fi.size == 8 {
|
||||
totalSize := length * uint64(fi.size) / 8
|
||||
buf := bytes.NewBuffer(make([]byte, 0, totalSize))
|
||||
if _, err := io.CopyN(buf, u.reader, int64(totalSize)); err != nil {
|
||||
return err
|
||||
}
|
||||
slice = reflect.ValueOf(buf.Bytes())
|
||||
u.rbits += uint64(totalSize) * 8
|
||||
|
||||
} else {
|
||||
slice = reflect.MakeSlice(v.Type(), 0, int(length))
|
||||
for i := 0; ; i++ {
|
||||
if fi.length != LengthUnlimited && uint(i) >= fi.length {
|
||||
break
|
||||
}
|
||||
if fi.length == LengthUnlimited && u.rbits >= u.size*8 {
|
||||
break
|
||||
}
|
||||
slice = reflect.Append(slice, reflect.Zero(elemType))
|
||||
if err := u.unmarshal(slice.Index(i), fi); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.rbits > u.size*8 {
|
||||
return fmt.Errorf("failed to read array completely: fieldName=\"%s\"", fi.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v.Set(slice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalInt(v reflect.Value, fi *fieldInstance) error {
|
||||
if fi.is(fieldVarint) {
|
||||
return errors.New("signed varint is unsupported")
|
||||
}
|
||||
|
||||
if fi.size == 0 {
|
||||
return fmt.Errorf("size must not be zero: %s", fi.name)
|
||||
}
|
||||
|
||||
data, err := u.reader.ReadBits(fi.size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += uint64(fi.size)
|
||||
|
||||
signBit := false
|
||||
if len(data) > 0 {
|
||||
signMask := byte(0x01) << ((fi.size - 1) % 8)
|
||||
signBit = data[0]&signMask != 0
|
||||
if signBit {
|
||||
data[0] |= ^(signMask - 1)
|
||||
}
|
||||
}
|
||||
|
||||
var val uint64
|
||||
if signBit {
|
||||
val = ^uint64(0)
|
||||
}
|
||||
for i := range data {
|
||||
val <<= 8
|
||||
val |= uint64(data[i])
|
||||
}
|
||||
v.SetInt(int64(val))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalUint(v reflect.Value, fi *fieldInstance) error {
|
||||
if fi.is(fieldVarint) {
|
||||
val, err := u.readUvarint()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.SetUint(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.size == 0 {
|
||||
return fmt.Errorf("size must not be zero: %s", fi.name)
|
||||
}
|
||||
|
||||
data, err := u.reader.ReadBits(fi.size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += uint64(fi.size)
|
||||
|
||||
val := uint64(0)
|
||||
for i := range data {
|
||||
val <<= 8
|
||||
val |= uint64(data[i])
|
||||
}
|
||||
v.SetUint(val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalBool(v reflect.Value, fi *fieldInstance) error {
|
||||
if fi.size == 0 {
|
||||
return fmt.Errorf("size must not be zero: %s", fi.name)
|
||||
}
|
||||
|
||||
data, err := u.reader.ReadBits(fi.size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += uint64(fi.size)
|
||||
|
||||
val := false
|
||||
for _, b := range data {
|
||||
val = val || (b != byte(0))
|
||||
}
|
||||
v.SetBool(val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalString(v reflect.Value, fi *fieldInstance) error {
|
||||
switch fi.strType {
|
||||
case stringType_C:
|
||||
return u.unmarshalStringC(v)
|
||||
case stringType_C_P:
|
||||
return u.unmarshalStringCP(v, fi)
|
||||
default:
|
||||
return fmt.Errorf("unknown string type: %d", fi.strType)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalStringC(v reflect.Value) error {
|
||||
data := make([]byte, 0, 16)
|
||||
for {
|
||||
if u.rbits >= u.size*8 {
|
||||
break
|
||||
}
|
||||
|
||||
c, err := u.reader.ReadBits(8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.rbits += 8
|
||||
|
||||
if c[0] == 0 {
|
||||
break // null character
|
||||
}
|
||||
|
||||
data = append(data, c[0])
|
||||
}
|
||||
v.SetString(string(data))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) unmarshalStringCP(v reflect.Value, fi *fieldInstance) error {
|
||||
if ok, err := u.tryReadPString(v, fi); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
return nil
|
||||
}
|
||||
return u.unmarshalStringC(v)
|
||||
}
|
||||
|
||||
func (u *unmarshaller) tryReadPString(v reflect.Value, fi *fieldInstance) (ok bool, err error) {
|
||||
remainingSize := (u.size*8 - u.rbits) / 8
|
||||
if remainingSize < 2 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
offset, err := u.reader.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
if err == nil && !ok {
|
||||
_, err = u.reader.Seek(offset, io.SeekStart)
|
||||
}
|
||||
}()
|
||||
|
||||
buf0 := make([]byte, 1)
|
||||
if _, err := io.ReadFull(u.reader, buf0); err != nil {
|
||||
return false, err
|
||||
}
|
||||
remainingSize--
|
||||
plen := buf0[0]
|
||||
if uint64(plen) > remainingSize {
|
||||
return false, nil
|
||||
}
|
||||
buf := make([]byte, int(plen))
|
||||
if _, err := io.ReadFull(u.reader, buf); err != nil {
|
||||
return false, err
|
||||
}
|
||||
remainingSize -= uint64(plen)
|
||||
if fi.cfo.IsPString(fi.name, buf, remainingSize, u.ctx) {
|
||||
u.rbits += uint64(len(buf)+1) * 8
|
||||
v.SetString(string(buf))
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (u *unmarshaller) readUvarint() (uint64, error) {
|
||||
var val uint64
|
||||
for {
|
||||
octet, err := u.reader.ReadBits(8)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
u.rbits += 8
|
||||
|
||||
val = (val << 7) + uint64(octet[0]&0x7f)
|
||||
|
||||
if octet[0]&0x80 == 0 {
|
||||
return val, nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrBoxInfoNotFound = errors.New("box info not found")
|
||||
|
||||
// BoxType is mpeg box type
|
||||
type BoxType [4]byte
|
||||
|
||||
func StrToBoxType(code string) BoxType {
|
||||
if len(code) != 4 {
|
||||
panic(fmt.Errorf("invalid box type id length: [%s]", code))
|
||||
}
|
||||
return BoxType{code[0], code[1], code[2], code[3]}
|
||||
}
|
||||
|
||||
func (boxType BoxType) String() string {
|
||||
if isPrintable(boxType[0]) && isPrintable(boxType[1]) && isPrintable(boxType[2]) && isPrintable(boxType[3]) {
|
||||
s := string([]byte{boxType[0], boxType[1], boxType[2], boxType[3]})
|
||||
s = strings.ReplaceAll(s, string([]byte{0xa9}), "(c)")
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("0x%02x%02x%02x%02x", boxType[0], boxType[1], boxType[2], boxType[3])
|
||||
}
|
||||
|
||||
func isASCII(c byte) bool {
|
||||
return c >= 0x20 && c <= 0x7e
|
||||
}
|
||||
|
||||
func isPrintable(c byte) bool {
|
||||
return isASCII(c) || c == 0xa9
|
||||
}
|
||||
|
||||
func (lhs BoxType) MatchWith(rhs BoxType) bool {
|
||||
if lhs == boxTypeAny || rhs == boxTypeAny {
|
||||
return true
|
||||
}
|
||||
return lhs == rhs
|
||||
}
|
||||
|
||||
var boxTypeAny = BoxType{0x00, 0x00, 0x00, 0x00}
|
||||
|
||||
func BoxTypeAny() BoxType {
|
||||
return boxTypeAny
|
||||
}
|
||||
|
||||
type boxDef struct {
|
||||
dataType reflect.Type
|
||||
versions []uint8
|
||||
isTarget func(Context) bool
|
||||
fields []*field
|
||||
}
|
||||
|
||||
var boxMap = make(map[BoxType][]boxDef, 64)
|
||||
|
||||
func AddBoxDef(payload IBox, versions ...uint8) {
|
||||
boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{
|
||||
dataType: reflect.TypeOf(payload).Elem(),
|
||||
versions: versions,
|
||||
fields: buildFields(payload),
|
||||
})
|
||||
}
|
||||
|
||||
func AddBoxDefEx(payload IBox, isTarget func(Context) bool, versions ...uint8) {
|
||||
boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{
|
||||
dataType: reflect.TypeOf(payload).Elem(),
|
||||
versions: versions,
|
||||
isTarget: isTarget,
|
||||
fields: buildFields(payload),
|
||||
})
|
||||
}
|
||||
|
||||
func AddAnyTypeBoxDef(payload IAnyType, boxType BoxType, versions ...uint8) {
|
||||
boxMap[boxType] = append(boxMap[boxType], boxDef{
|
||||
dataType: reflect.TypeOf(payload).Elem(),
|
||||
versions: versions,
|
||||
fields: buildFields(payload),
|
||||
})
|
||||
}
|
||||
|
||||
func AddAnyTypeBoxDefEx(payload IAnyType, boxType BoxType, isTarget func(Context) bool, versions ...uint8) {
|
||||
boxMap[boxType] = append(boxMap[boxType], boxDef{
|
||||
dataType: reflect.TypeOf(payload).Elem(),
|
||||
versions: versions,
|
||||
isTarget: isTarget,
|
||||
fields: buildFields(payload),
|
||||
})
|
||||
}
|
||||
|
||||
func (boxType BoxType) getBoxDef(ctx Context) *boxDef {
|
||||
boxDefs := boxMap[boxType]
|
||||
for i := len(boxDefs) - 1; i >= 0; i-- {
|
||||
boxDef := &boxDefs[i]
|
||||
if boxDef.isTarget == nil || boxDef.isTarget(ctx) {
|
||||
return boxDef
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (boxType BoxType) IsSupported(ctx Context) bool {
|
||||
return boxType.getBoxDef(ctx) != nil
|
||||
}
|
||||
|
||||
func (boxType BoxType) New(ctx Context) (IBox, error) {
|
||||
boxDef := boxType.getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return nil, ErrBoxInfoNotFound
|
||||
}
|
||||
|
||||
box, ok := reflect.New(boxDef.dataType).Interface().(IBox)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("box type not implements IBox interface: %s", boxType.String())
|
||||
}
|
||||
|
||||
anyTypeBox, ok := box.(IAnyType)
|
||||
if ok {
|
||||
anyTypeBox.SetType(boxType)
|
||||
}
|
||||
|
||||
return box, nil
|
||||
}
|
||||
|
||||
func (boxType BoxType) GetSupportedVersions(ctx Context) ([]uint8, error) {
|
||||
boxDef := boxType.getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return nil, ErrBoxInfoNotFound
|
||||
}
|
||||
return boxDef.versions, nil
|
||||
}
|
||||
|
||||
func (boxType BoxType) IsSupportedVersion(ver uint8, ctx Context) bool {
|
||||
boxDef := boxType.getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return false
|
||||
}
|
||||
if len(boxDef.versions) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, sver := range boxDef.versions {
|
||||
if ver == sver {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,673 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/abema/go-mp4/bitio"
|
||||
)
|
||||
|
||||
type ProbeInfo struct {
|
||||
MajorBrand [4]byte
|
||||
MinorVersion uint32
|
||||
CompatibleBrands [][4]byte
|
||||
FastStart bool
|
||||
Timescale uint32
|
||||
Duration uint64
|
||||
Tracks Tracks
|
||||
Segments Segments
|
||||
}
|
||||
|
||||
// Deprecated: replace with ProbeInfo
|
||||
type FraProbeInfo = ProbeInfo
|
||||
|
||||
type Tracks []*Track
|
||||
|
||||
// Deprecated: replace with Track
|
||||
type TrackInfo = Track
|
||||
|
||||
type Track struct {
|
||||
TrackID uint32
|
||||
Timescale uint32
|
||||
Duration uint64
|
||||
Codec Codec
|
||||
Encrypted bool
|
||||
EditList EditList
|
||||
Samples Samples
|
||||
Chunks Chunks
|
||||
AVC *AVCDecConfigInfo
|
||||
MP4A *MP4AInfo
|
||||
}
|
||||
|
||||
type Codec int
|
||||
|
||||
const (
|
||||
CodecUnknown Codec = iota
|
||||
CodecAVC1
|
||||
CodecMP4A
|
||||
)
|
||||
|
||||
type EditList []*EditListEntry
|
||||
|
||||
type EditListEntry struct {
|
||||
MediaTime int64
|
||||
SegmentDuration uint64
|
||||
}
|
||||
|
||||
type Samples []*Sample
|
||||
|
||||
type Sample struct {
|
||||
Size uint32
|
||||
TimeDelta uint32
|
||||
CompositionTimeOffset int64
|
||||
}
|
||||
|
||||
type Chunks []*Chunk
|
||||
|
||||
type Chunk struct {
|
||||
DataOffset uint32
|
||||
SamplesPerChunk uint32
|
||||
}
|
||||
|
||||
type AVCDecConfigInfo struct {
|
||||
ConfigurationVersion uint8
|
||||
Profile uint8
|
||||
ProfileCompatibility uint8
|
||||
Level uint8
|
||||
LengthSize uint16
|
||||
Width uint16
|
||||
Height uint16
|
||||
}
|
||||
|
||||
type MP4AInfo struct {
|
||||
OTI uint8
|
||||
AudOTI uint8
|
||||
ChannelCount uint16
|
||||
}
|
||||
|
||||
type Segments []*Segment
|
||||
|
||||
// Deprecated: replace with Segment
|
||||
type SegmentInfo = Segment
|
||||
|
||||
type Segment struct {
|
||||
TrackID uint32
|
||||
MoofOffset uint64
|
||||
BaseMediaDecodeTime uint64
|
||||
DefaultSampleDuration uint32
|
||||
SampleCount uint32
|
||||
Duration uint32
|
||||
CompositionTimeOffset int32
|
||||
Size uint32
|
||||
}
|
||||
|
||||
// Probe probes MP4 file
|
||||
func Probe(r io.ReadSeeker) (*ProbeInfo, error) {
|
||||
probeInfo := &ProbeInfo{
|
||||
Tracks: make([]*Track, 0, 8),
|
||||
Segments: make([]*Segment, 0, 8),
|
||||
}
|
||||
bis, err := ExtractBoxes(r, nil, []BoxPath{
|
||||
{BoxTypeFtyp()},
|
||||
{BoxTypeMoov()},
|
||||
{BoxTypeMoov(), BoxTypeMvhd()},
|
||||
{BoxTypeMoov(), BoxTypeTrak()},
|
||||
{BoxTypeMoof()},
|
||||
{BoxTypeMdat()},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var mdatAppeared bool
|
||||
for _, bi := range bis {
|
||||
switch bi.Type {
|
||||
case BoxTypeFtyp():
|
||||
var ftyp Ftyp
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &ftyp, bi.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
probeInfo.MajorBrand = ftyp.MajorBrand
|
||||
probeInfo.MinorVersion = ftyp.MinorVersion
|
||||
probeInfo.CompatibleBrands = make([][4]byte, 0, len(ftyp.CompatibleBrands))
|
||||
for _, entry := range ftyp.CompatibleBrands {
|
||||
probeInfo.CompatibleBrands = append(probeInfo.CompatibleBrands, entry.CompatibleBrand)
|
||||
}
|
||||
case BoxTypeMoov():
|
||||
probeInfo.FastStart = !mdatAppeared
|
||||
case BoxTypeMvhd():
|
||||
var mvhd Mvhd
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &mvhd, bi.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
probeInfo.Timescale = mvhd.Timescale
|
||||
if mvhd.GetVersion() == 0 {
|
||||
probeInfo.Duration = uint64(mvhd.DurationV0)
|
||||
} else {
|
||||
probeInfo.Duration = mvhd.DurationV1
|
||||
}
|
||||
case BoxTypeTrak():
|
||||
track, err := probeTrak(r, bi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
probeInfo.Tracks = append(probeInfo.Tracks, track)
|
||||
case BoxTypeMoof():
|
||||
segment, err := probeMoof(r, bi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
probeInfo.Segments = append(probeInfo.Segments, segment)
|
||||
case BoxTypeMdat():
|
||||
mdatAppeared = true
|
||||
}
|
||||
}
|
||||
return probeInfo, nil
|
||||
}
|
||||
|
||||
// ProbeFra probes fragmented MP4 file
|
||||
// Deprecated: replace with Probe
|
||||
func ProbeFra(r io.ReadSeeker) (*FraProbeInfo, error) {
|
||||
probeInfo, err := Probe(r)
|
||||
return (*FraProbeInfo)(probeInfo), err
|
||||
}
|
||||
|
||||
func probeTrak(r io.ReadSeeker, bi *BoxInfo) (*Track, error) {
|
||||
track := new(Track)
|
||||
|
||||
bips, err := ExtractBoxesWithPayload(r, bi, []BoxPath{
|
||||
{BoxTypeTkhd()},
|
||||
{BoxTypeEdts(), BoxTypeElst()},
|
||||
{BoxTypeMdia(), BoxTypeMdhd()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeAvc1()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeAvc1(), BoxTypeAvcC()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeEncv()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeEncv(), BoxTypeAvcC()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeMp4a()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeMp4a(), BoxTypeEsds()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeMp4a(), BoxTypeWave(), BoxTypeEsds()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeEnca()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsd(), BoxTypeEnca(), BoxTypeEsds()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStco()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStts()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeCtts()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsc()},
|
||||
{BoxTypeMdia(), BoxTypeMinf(), BoxTypeStbl(), BoxTypeStsz()},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tkhd *Tkhd
|
||||
var elst *Elst
|
||||
var mdhd *Mdhd
|
||||
var avc1 *VisualSampleEntry
|
||||
var avcC *AVCDecoderConfiguration
|
||||
var audioSampleEntry *AudioSampleEntry
|
||||
var esds *Esds
|
||||
var stco *Stco
|
||||
var stts *Stts
|
||||
var stsc *Stsc
|
||||
var ctts *Ctts
|
||||
var stsz *Stsz
|
||||
for _, bip := range bips {
|
||||
switch bip.Info.Type {
|
||||
case BoxTypeTkhd():
|
||||
tkhd = bip.Payload.(*Tkhd)
|
||||
case BoxTypeElst():
|
||||
elst = bip.Payload.(*Elst)
|
||||
case BoxTypeMdhd():
|
||||
mdhd = bip.Payload.(*Mdhd)
|
||||
case BoxTypeAvc1():
|
||||
track.Codec = CodecAVC1
|
||||
avc1 = bip.Payload.(*VisualSampleEntry)
|
||||
case BoxTypeAvcC():
|
||||
avcC = bip.Payload.(*AVCDecoderConfiguration)
|
||||
case BoxTypeEncv():
|
||||
track.Codec = CodecAVC1
|
||||
track.Encrypted = true
|
||||
case BoxTypeMp4a():
|
||||
track.Codec = CodecMP4A
|
||||
audioSampleEntry = bip.Payload.(*AudioSampleEntry)
|
||||
case BoxTypeEnca():
|
||||
track.Codec = CodecMP4A
|
||||
track.Encrypted = true
|
||||
audioSampleEntry = bip.Payload.(*AudioSampleEntry)
|
||||
case BoxTypeEsds():
|
||||
esds = bip.Payload.(*Esds)
|
||||
case BoxTypeStco():
|
||||
stco = bip.Payload.(*Stco)
|
||||
case BoxTypeStts():
|
||||
stts = bip.Payload.(*Stts)
|
||||
case BoxTypeStsc():
|
||||
stsc = bip.Payload.(*Stsc)
|
||||
case BoxTypeCtts():
|
||||
ctts = bip.Payload.(*Ctts)
|
||||
case BoxTypeStsz():
|
||||
stsz = bip.Payload.(*Stsz)
|
||||
}
|
||||
}
|
||||
|
||||
if tkhd == nil {
|
||||
return nil, errors.New("tkhd box not found")
|
||||
}
|
||||
track.TrackID = tkhd.TrackID
|
||||
|
||||
if elst != nil {
|
||||
editList := make([]*EditListEntry, 0, len(elst.Entries))
|
||||
for i := range elst.Entries {
|
||||
editList = append(editList, &EditListEntry{
|
||||
MediaTime: elst.GetMediaTime(i),
|
||||
SegmentDuration: elst.GetSegmentDuration(i),
|
||||
})
|
||||
}
|
||||
track.EditList = editList
|
||||
}
|
||||
|
||||
if mdhd == nil {
|
||||
return nil, errors.New("mdhd box not found")
|
||||
}
|
||||
track.Timescale = mdhd.Timescale
|
||||
track.Duration = mdhd.GetDuration()
|
||||
|
||||
if avc1 != nil && avcC != nil {
|
||||
track.AVC = &AVCDecConfigInfo{
|
||||
ConfigurationVersion: avcC.ConfigurationVersion,
|
||||
Profile: avcC.Profile,
|
||||
ProfileCompatibility: avcC.ProfileCompatibility,
|
||||
Level: avcC.Level,
|
||||
LengthSize: uint16(avcC.LengthSizeMinusOne) + 1,
|
||||
Width: avc1.Width,
|
||||
Height: avc1.Height,
|
||||
}
|
||||
}
|
||||
|
||||
if audioSampleEntry != nil && esds != nil {
|
||||
oti, audOTI, err := detectAACProfile(esds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
track.MP4A = &MP4AInfo{
|
||||
OTI: oti,
|
||||
AudOTI: audOTI,
|
||||
ChannelCount: audioSampleEntry.ChannelCount,
|
||||
}
|
||||
}
|
||||
|
||||
if stco == nil {
|
||||
return nil, errors.New("stco box not found")
|
||||
}
|
||||
track.Chunks = make([]*Chunk, 0)
|
||||
for _, offset := range stco.ChunkOffset {
|
||||
track.Chunks = append(track.Chunks, &Chunk{
|
||||
DataOffset: offset,
|
||||
})
|
||||
}
|
||||
|
||||
if stts == nil {
|
||||
return nil, errors.New("stts box not found")
|
||||
}
|
||||
track.Samples = make([]*Sample, 0)
|
||||
for _, entry := range stts.Entries {
|
||||
for i := uint32(0); i < entry.SampleCount; i++ {
|
||||
track.Samples = append(track.Samples, &Sample{
|
||||
TimeDelta: entry.SampleDelta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if stsc == nil {
|
||||
return nil, errors.New("stsc box not found")
|
||||
}
|
||||
for si, entry := range stsc.Entries {
|
||||
end := uint32(len(track.Chunks))
|
||||
if si != len(stsc.Entries)-1 && stsc.Entries[si+1].FirstChunk-1 < end {
|
||||
end = stsc.Entries[si+1].FirstChunk - 1
|
||||
}
|
||||
for ci := entry.FirstChunk - 1; ci < end; ci++ {
|
||||
track.Chunks[ci].SamplesPerChunk = entry.SamplesPerChunk
|
||||
}
|
||||
}
|
||||
|
||||
if ctts != nil {
|
||||
var si uint32
|
||||
for ci, entry := range ctts.Entries {
|
||||
for i := uint32(0); i < entry.SampleCount; i++ {
|
||||
if si >= uint32(len(track.Samples)) {
|
||||
break
|
||||
}
|
||||
track.Samples[si].CompositionTimeOffset = ctts.GetSampleOffset(ci)
|
||||
si++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stsz != nil {
|
||||
for i := 0; i < len(stsz.EntrySize) && i < len(track.Samples); i++ {
|
||||
track.Samples[i].Size = stsz.EntrySize[i]
|
||||
}
|
||||
}
|
||||
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func detectAACProfile(esds *Esds) (oti, audOTI uint8, err error) {
|
||||
configDscr := findDescriptorByTag(esds.Descriptors, DecoderConfigDescrTag)
|
||||
if configDscr == nil || configDscr.DecoderConfigDescriptor == nil {
|
||||
return 0, 0, nil
|
||||
}
|
||||
if configDscr.DecoderConfigDescriptor.ObjectTypeIndication != 0x40 {
|
||||
return configDscr.DecoderConfigDescriptor.ObjectTypeIndication, 0, nil
|
||||
}
|
||||
|
||||
specificDscr := findDescriptorByTag(esds.Descriptors, DecSpecificInfoTag)
|
||||
if specificDscr == nil {
|
||||
return 0, 0, errors.New("DecoderSpecificationInfoDescriptor not found")
|
||||
}
|
||||
|
||||
r := bitio.NewReader(bytes.NewReader(specificDscr.Data))
|
||||
remaining := len(specificDscr.Data) * 8
|
||||
|
||||
// audio object type
|
||||
audioObjectType, read, err := getAudioObjectType(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= read
|
||||
|
||||
// sampling frequency index
|
||||
samplingFrequencyIndex, err := r.ReadBits(4)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 4
|
||||
if samplingFrequencyIndex[0] == 0x0f {
|
||||
if _, err = r.ReadBits(24); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 24
|
||||
}
|
||||
|
||||
if audioObjectType == 2 && remaining >= 20 {
|
||||
if _, err = r.ReadBits(4); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 4
|
||||
syncExtensionType, err := r.ReadBits(11)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 11
|
||||
if syncExtensionType[0] == 0x2 && syncExtensionType[1] == 0xb7 {
|
||||
extAudioObjectType, _, err := getAudioObjectType(r)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if extAudioObjectType == 5 || extAudioObjectType == 22 {
|
||||
sbr, err := r.ReadBits(1)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining--
|
||||
if sbr[0] != 0 {
|
||||
if extAudioObjectType == 5 {
|
||||
sfi, err := r.ReadBits(4)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 4
|
||||
if sfi[0] == 0xf {
|
||||
if _, err := r.ReadBits(24); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
remaining -= 24
|
||||
}
|
||||
if remaining >= 12 {
|
||||
syncExtensionType, err := r.ReadBits(11)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if syncExtensionType[0] == 0x5 && syncExtensionType[1] == 0x48 {
|
||||
ps, err := r.ReadBits(1)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if ps[0] != 0 {
|
||||
return 0x40, 29, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0x40, 5, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0x40, audioObjectType, nil
|
||||
}
|
||||
|
||||
func findDescriptorByTag(dscrs []Descriptor, tag int8) *Descriptor {
|
||||
for _, dscr := range dscrs {
|
||||
if dscr.Tag == tag {
|
||||
return &dscr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAudioObjectType(r bitio.Reader) (byte, int, error) {
|
||||
audioObjectType, err := r.ReadBits(5)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if audioObjectType[0] != 0x1f {
|
||||
return audioObjectType[0], 5, nil
|
||||
}
|
||||
audioObjectType, err = r.ReadBits(6)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return audioObjectType[0] + 32, 11, nil
|
||||
}
|
||||
|
||||
func probeMoof(r io.ReadSeeker, bi *BoxInfo) (*Segment, error) {
|
||||
bips, err := ExtractBoxesWithPayload(r, bi, []BoxPath{
|
||||
{BoxTypeTraf(), BoxTypeTfhd()},
|
||||
{BoxTypeTraf(), BoxTypeTfdt()},
|
||||
{BoxTypeTraf(), BoxTypeTrun()},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tfhd *Tfhd
|
||||
var tfdt *Tfdt
|
||||
var trun *Trun
|
||||
|
||||
segment := &Segment{
|
||||
MoofOffset: bi.Offset,
|
||||
}
|
||||
for _, bip := range bips {
|
||||
switch bip.Info.Type {
|
||||
case BoxTypeTfhd():
|
||||
tfhd = bip.Payload.(*Tfhd)
|
||||
case BoxTypeTfdt():
|
||||
tfdt = bip.Payload.(*Tfdt)
|
||||
case BoxTypeTrun():
|
||||
trun = bip.Payload.(*Trun)
|
||||
}
|
||||
}
|
||||
|
||||
if tfhd == nil {
|
||||
return nil, errors.New("tfhd not found")
|
||||
}
|
||||
segment.TrackID = tfhd.TrackID
|
||||
segment.DefaultSampleDuration = tfhd.DefaultSampleDuration
|
||||
|
||||
if tfdt != nil {
|
||||
if tfdt.Version == 0 {
|
||||
segment.BaseMediaDecodeTime = uint64(tfdt.BaseMediaDecodeTimeV0)
|
||||
} else {
|
||||
segment.BaseMediaDecodeTime = tfdt.BaseMediaDecodeTimeV1
|
||||
}
|
||||
}
|
||||
|
||||
if trun != nil {
|
||||
segment.SampleCount = trun.SampleCount
|
||||
|
||||
if trun.CheckFlag(0x000100) {
|
||||
segment.Duration = 0
|
||||
for ei := range trun.Entries {
|
||||
segment.Duration += trun.Entries[ei].SampleDuration
|
||||
}
|
||||
} else {
|
||||
segment.Duration = tfhd.DefaultSampleDuration * segment.SampleCount
|
||||
}
|
||||
|
||||
if trun.CheckFlag(0x000200) {
|
||||
segment.Size = 0
|
||||
for ei := range trun.Entries {
|
||||
segment.Size += trun.Entries[ei].SampleSize
|
||||
}
|
||||
} else {
|
||||
segment.Size = tfhd.DefaultSampleSize * segment.SampleCount
|
||||
}
|
||||
|
||||
var duration uint32
|
||||
for ei := range trun.Entries {
|
||||
offset := int32(duration) + int32(trun.GetSampleCompositionTimeOffset(ei))
|
||||
if ei == 0 || offset < segment.CompositionTimeOffset {
|
||||
segment.CompositionTimeOffset = offset
|
||||
}
|
||||
if trun.CheckFlag(0x000100) {
|
||||
duration += trun.Entries[ei].SampleDuration
|
||||
} else {
|
||||
duration += tfhd.DefaultSampleDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segment, nil
|
||||
}
|
||||
|
||||
func FindIDRFrames(r io.ReadSeeker, trackInfo *TrackInfo) ([]int, error) {
|
||||
if trackInfo.AVC == nil {
|
||||
return nil, nil
|
||||
}
|
||||
lengthSize := uint32(trackInfo.AVC.LengthSize)
|
||||
|
||||
var si int
|
||||
idxs := make([]int, 0, 8)
|
||||
for _, chunk := range trackInfo.Chunks {
|
||||
end := si + int(chunk.SamplesPerChunk)
|
||||
dataOffset := chunk.DataOffset
|
||||
for ; si < end && si < len(trackInfo.Samples); si++ {
|
||||
sample := trackInfo.Samples[si]
|
||||
if sample.Size == 0 {
|
||||
continue
|
||||
}
|
||||
for nalOffset := uint32(0); nalOffset+lengthSize+1 <= sample.Size; {
|
||||
if _, err := r.Seek(int64(dataOffset+nalOffset), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make([]byte, lengthSize+1)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var length uint32
|
||||
for i := 0; i < int(lengthSize); i++ {
|
||||
length = (length << 8) + uint32(data[i])
|
||||
}
|
||||
nalHeader := data[lengthSize]
|
||||
nalType := nalHeader & 0x1f
|
||||
if nalType == 5 {
|
||||
idxs = append(idxs, si)
|
||||
break
|
||||
}
|
||||
nalOffset += lengthSize + length
|
||||
}
|
||||
dataOffset += sample.Size
|
||||
}
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
||||
func (samples Samples) GetBitrate(timescale uint32) uint64 {
|
||||
var totalSize uint64
|
||||
var totalDuration uint64
|
||||
for _, sample := range samples {
|
||||
totalSize += uint64(sample.Size)
|
||||
totalDuration += uint64(sample.TimeDelta)
|
||||
}
|
||||
if totalDuration == 0 {
|
||||
return 0
|
||||
}
|
||||
return 8 * totalSize * uint64(timescale) / totalDuration
|
||||
}
|
||||
|
||||
func (samples Samples) GetMaxBitrate(timescale uint32, timeDelta uint64) uint64 {
|
||||
if timeDelta == 0 {
|
||||
return 0
|
||||
}
|
||||
var maxBitrate uint64
|
||||
var size uint64
|
||||
var duration uint64
|
||||
var begin int
|
||||
var end int
|
||||
for end < len(samples) {
|
||||
for {
|
||||
size += uint64(samples[end].Size)
|
||||
duration += uint64(samples[end].TimeDelta)
|
||||
end++
|
||||
if duration >= timeDelta || end == len(samples) {
|
||||
break
|
||||
}
|
||||
}
|
||||
bitrate := 8 * size * uint64(timescale) / duration
|
||||
if bitrate > maxBitrate {
|
||||
maxBitrate = bitrate
|
||||
}
|
||||
for {
|
||||
size -= uint64(samples[begin].Size)
|
||||
duration -= uint64(samples[begin].TimeDelta)
|
||||
begin++
|
||||
if duration < timeDelta {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxBitrate
|
||||
}
|
||||
|
||||
func (segments Segments) GetBitrate(trackID uint32, timescale uint32) uint64 {
|
||||
var totalSize uint64
|
||||
var totalDuration uint64
|
||||
for _, segment := range segments {
|
||||
if segment.TrackID == trackID {
|
||||
totalSize += uint64(segment.Size)
|
||||
totalDuration += uint64(segment.Duration)
|
||||
}
|
||||
}
|
||||
if totalDuration == 0 {
|
||||
return 0
|
||||
}
|
||||
return 8 * totalSize * uint64(timescale) / totalDuration
|
||||
}
|
||||
|
||||
func (segments Segments) GetMaxBitrate(trackID uint32, timescale uint32) uint64 {
|
||||
var maxBitrate uint64
|
||||
for _, segment := range segments {
|
||||
if segment.TrackID == trackID && segment.Duration != 0 {
|
||||
bitrate := 8 * uint64(segment.Size) * uint64(timescale) / uint64(segment.Duration)
|
||||
if bitrate > maxBitrate {
|
||||
maxBitrate = bitrate
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxBitrate
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type BoxPath []BoxType
|
||||
|
||||
func (lhs BoxPath) compareWith(rhs BoxPath) (forwardMatch bool, match bool) {
|
||||
if len(lhs) > len(rhs) {
|
||||
return false, false
|
||||
}
|
||||
for i := 0; i < len(lhs); i++ {
|
||||
if !lhs[i].MatchWith(rhs[i]) {
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
if len(lhs) < len(rhs) {
|
||||
return true, false
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
|
||||
type ReadHandle struct {
|
||||
Params []interface{}
|
||||
BoxInfo BoxInfo
|
||||
Path BoxPath
|
||||
ReadPayload func() (box IBox, n uint64, err error)
|
||||
ReadData func(io.Writer) (n uint64, err error)
|
||||
Expand func(params ...interface{}) (vals []interface{}, err error)
|
||||
}
|
||||
|
||||
type ReadHandler func(handle *ReadHandle) (val interface{}, err error)
|
||||
|
||||
func ReadBoxStructure(r io.ReadSeeker, handler ReadHandler, params ...interface{}) ([]interface{}, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readBoxStructure(r, 0, true, nil, Context{}, handler, params)
|
||||
}
|
||||
|
||||
func ReadBoxStructureFromInternal(r io.ReadSeeker, bi *BoxInfo, handler ReadHandler, params ...interface{}) (interface{}, error) {
|
||||
return readBoxStructureFromInternal(r, bi, nil, handler, params)
|
||||
}
|
||||
|
||||
func readBoxStructureFromInternal(r io.ReadSeeker, bi *BoxInfo, path BoxPath, handler ReadHandler, params []interface{}) (interface{}, error) {
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check comatible-brands
|
||||
if len(path) == 0 && bi.Type == BoxTypeFtyp() {
|
||||
var ftyp Ftyp
|
||||
if _, err := Unmarshal(r, bi.Size-bi.HeaderSize, &ftyp, bi.Context); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ftyp.HasCompatibleBrand(BrandQT()) {
|
||||
bi.IsQuickTimeCompatible = true
|
||||
}
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctx := bi.Context
|
||||
if bi.Type == BoxTypeWave() {
|
||||
ctx.UnderWave = true
|
||||
} else if bi.Type == BoxTypeIlst() {
|
||||
ctx.UnderIlst = true
|
||||
} else if bi.UnderIlst && !bi.UnderIlstMeta && IsIlstMetaBoxType(bi.Type) {
|
||||
ctx.UnderIlstMeta = true
|
||||
if bi.Type == StrToBoxType("----") {
|
||||
ctx.UnderIlstFreeMeta = true
|
||||
}
|
||||
} else if bi.Type == BoxTypeUdta() {
|
||||
ctx.UnderUdta = true
|
||||
}
|
||||
|
||||
newPath := make(BoxPath, len(path)+1)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = bi.Type
|
||||
|
||||
h := &ReadHandle{
|
||||
Params: params,
|
||||
BoxInfo: *bi,
|
||||
Path: newPath,
|
||||
}
|
||||
|
||||
var childrenOffset uint64
|
||||
|
||||
h.ReadPayload = func() (IBox, uint64, error) {
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
box, n, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, bi.Context)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
childrenOffset = bi.Offset + bi.HeaderSize + n
|
||||
return box, n, nil
|
||||
}
|
||||
|
||||
h.ReadData = func(w io.Writer) (uint64, error) {
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
size := bi.Size - bi.HeaderSize
|
||||
if _, err := io.CopyN(w, r, int64(size)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
|
||||
h.Expand = func(params ...interface{}) ([]interface{}, error) {
|
||||
if childrenOffset == 0 {
|
||||
if _, err := bi.SeekToPayload(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, n, err := UnmarshalAny(r, bi.Type, bi.Size-bi.HeaderSize, bi.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
childrenOffset = bi.Offset + bi.HeaderSize + n
|
||||
} else {
|
||||
if _, err := r.Seek(int64(childrenOffset), io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
childrenSize := bi.Offset + bi.Size - childrenOffset
|
||||
return readBoxStructure(r, childrenSize, false, newPath, ctx, handler, params)
|
||||
}
|
||||
|
||||
if val, err := handler(h); err != nil {
|
||||
return nil, err
|
||||
} else if _, err := bi.SeekToEnd(r); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return val, nil
|
||||
}
|
||||
}
|
||||
|
||||
func readBoxStructure(r io.ReadSeeker, totalSize uint64, isRoot bool, path BoxPath, ctx Context, handler ReadHandler, params []interface{}) ([]interface{}, error) {
|
||||
vals := make([]interface{}, 0, 8)
|
||||
|
||||
for isRoot || totalSize != 0 {
|
||||
bi, err := ReadBoxInfo(r)
|
||||
if isRoot && err == io.EOF {
|
||||
return vals, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isRoot && bi.Size > totalSize {
|
||||
return nil, fmt.Errorf("too large box size: type=%s, size=%d, actualBufSize=%d", bi.Type.String(), bi.Size, totalSize)
|
||||
}
|
||||
totalSize -= bi.Size
|
||||
|
||||
bi.Context = ctx
|
||||
|
||||
val, err := readBoxStructureFromInternal(r, bi, path, handler, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vals = append(vals, val)
|
||||
|
||||
if bi.IsQuickTimeCompatible {
|
||||
ctx.IsQuickTimeCompatible = true
|
||||
}
|
||||
}
|
||||
|
||||
if totalSize != 0 {
|
||||
return nil, errors.New("Unexpected EOF")
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/abema/go-mp4/util"
|
||||
)
|
||||
|
||||
type stringifier struct {
|
||||
buf *bytes.Buffer
|
||||
src IImmutableBox
|
||||
indent string
|
||||
ctx Context
|
||||
}
|
||||
|
||||
func Stringify(src IImmutableBox, ctx Context) (string, error) {
|
||||
return StringifyWithIndent(src, "", ctx)
|
||||
}
|
||||
|
||||
func StringifyWithIndent(src IImmutableBox, indent string, ctx Context) (string, error) {
|
||||
boxDef := src.GetType().getBoxDef(ctx)
|
||||
if boxDef == nil {
|
||||
return "", ErrBoxInfoNotFound
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(src).Elem()
|
||||
|
||||
m := &stringifier{
|
||||
buf: bytes.NewBuffer(nil),
|
||||
src: src,
|
||||
indent: indent,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
err := m.stringifyStruct(v, boxDef.fields, 0, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return m.buf.String(), nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringify(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Ptr:
|
||||
return m.stringifyPtr(v, fi, depth)
|
||||
case reflect.Struct:
|
||||
return m.stringifyStruct(v, fi.children, depth, fi.is(fieldExtend))
|
||||
case reflect.Array:
|
||||
return m.stringifyArray(v, fi, depth)
|
||||
case reflect.Slice:
|
||||
return m.stringifySlice(v, fi, depth)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return m.stringifyInt(v, fi, depth)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return m.stringifyUint(v, fi, depth)
|
||||
case reflect.Bool:
|
||||
return m.stringifyBool(v, depth)
|
||||
case reflect.String:
|
||||
return m.stringifyString(v, depth)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type: %s", v.Type().Kind())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyPtr(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
return m.stringify(v.Elem(), fi, depth)
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyStruct(v reflect.Value, fs []*field, depth int, extended bool) error {
|
||||
if !extended {
|
||||
m.buf.WriteString("{")
|
||||
if m.indent != "" {
|
||||
m.buf.WriteString("\n")
|
||||
}
|
||||
depth++
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
fi := resolveFieldInstance(f, m.src, v, m.ctx)
|
||||
|
||||
if !isTargetField(m.src, fi, m.ctx) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.cnst != "" || f.is(fieldHidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !f.is(fieldExtend) {
|
||||
if m.indent != "" {
|
||||
writeIndent(m.buf, m.indent, depth+1)
|
||||
} else if m.buf.Len() != 0 && m.buf.Bytes()[m.buf.Len()-1] != '{' {
|
||||
m.buf.WriteString(" ")
|
||||
}
|
||||
m.buf.WriteString(f.name)
|
||||
m.buf.WriteString("=")
|
||||
}
|
||||
|
||||
str, ok := fi.cfo.StringifyField(f.name, m.indent, depth+1, m.ctx)
|
||||
if ok {
|
||||
m.buf.WriteString(str)
|
||||
if !f.is(fieldExtend) && m.indent != "" {
|
||||
m.buf.WriteString("\n")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if f.name == "Version" {
|
||||
m.buf.WriteString(strconv.Itoa(int(m.src.GetVersion())))
|
||||
} else if f.name == "Flags" {
|
||||
fmt.Fprintf(m.buf, "0x%06x", m.src.GetFlags())
|
||||
} else {
|
||||
err := m.stringify(v.FieldByName(f.name), fi, depth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !f.is(fieldExtend) && m.indent != "" {
|
||||
m.buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
if !extended {
|
||||
if m.indent != "" {
|
||||
writeIndent(m.buf, m.indent, depth)
|
||||
}
|
||||
m.buf.WriteString("}")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyArray(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
begin, sep, end := "[", ", ", "]"
|
||||
if fi.is(fieldString) || fi.is(fieldISO639_2) {
|
||||
begin, sep, end = "\"", "", "\""
|
||||
} else if fi.is(fieldUUID) {
|
||||
begin, sep, end = "", "", ""
|
||||
}
|
||||
|
||||
m.buf.WriteString(begin)
|
||||
|
||||
m2 := *m
|
||||
if fi.is(fieldString) {
|
||||
m2.buf = bytes.NewBuffer(nil)
|
||||
}
|
||||
size := v.Type().Size()
|
||||
for i := 0; i < int(size)/int(v.Type().Elem().Size()); i++ {
|
||||
if i != 0 {
|
||||
m2.buf.WriteString(sep)
|
||||
}
|
||||
|
||||
if err := m2.stringify(v.Index(i), fi, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.is(fieldUUID) && (i == 3 || i == 5 || i == 7 || i == 9) {
|
||||
m.buf.WriteString("-")
|
||||
}
|
||||
}
|
||||
if fi.is(fieldString) {
|
||||
m.buf.WriteString(util.EscapeUnprintables(m2.buf.String()))
|
||||
}
|
||||
|
||||
m.buf.WriteString(end)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifySlice(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
begin, sep, end := "[", ", ", "]"
|
||||
if fi.is(fieldString) || fi.is(fieldISO639_2) {
|
||||
begin, sep, end = "\"", "", "\""
|
||||
}
|
||||
|
||||
m.buf.WriteString(begin)
|
||||
|
||||
m2 := *m
|
||||
if fi.is(fieldString) {
|
||||
m2.buf = bytes.NewBuffer(nil)
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if fi.length != LengthUnlimited && uint(i) >= fi.length {
|
||||
break
|
||||
}
|
||||
|
||||
if i != 0 {
|
||||
m2.buf.WriteString(sep)
|
||||
}
|
||||
|
||||
if err := m2.stringify(v.Index(i), fi, depth+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fi.is(fieldString) {
|
||||
m.buf.WriteString(util.EscapeUnprintables(m2.buf.String()))
|
||||
}
|
||||
|
||||
m.buf.WriteString(end)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyInt(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
if fi.is(fieldHex) {
|
||||
val := v.Int()
|
||||
if val >= 0 {
|
||||
m.buf.WriteString("0x")
|
||||
m.buf.WriteString(strconv.FormatInt(val, 16))
|
||||
} else {
|
||||
m.buf.WriteString("-0x")
|
||||
m.buf.WriteString(strconv.FormatInt(-val, 16))
|
||||
}
|
||||
} else {
|
||||
m.buf.WriteString(strconv.FormatInt(v.Int(), 10))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyUint(v reflect.Value, fi *fieldInstance, depth int) error {
|
||||
if fi.is(fieldISO639_2) {
|
||||
m.buf.WriteString(string([]byte{byte(v.Uint() + 0x60)}))
|
||||
} else if fi.is(fieldUUID) {
|
||||
fmt.Fprintf(m.buf, "%02x", v.Uint())
|
||||
} else if fi.is(fieldString) {
|
||||
m.buf.WriteString(string([]byte{byte(v.Uint())}))
|
||||
} else if fi.is(fieldHex) || (!fi.is(fieldDec) && v.Type().Kind() == reflect.Uint8) || v.Type().Kind() == reflect.Uintptr {
|
||||
m.buf.WriteString("0x")
|
||||
m.buf.WriteString(strconv.FormatUint(v.Uint(), 16))
|
||||
} else {
|
||||
m.buf.WriteString(strconv.FormatUint(v.Uint(), 10))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyBool(v reflect.Value, depth int) error {
|
||||
m.buf.WriteString(strconv.FormatBool(v.Bool()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stringifier) stringifyString(v reflect.Value, depth int) error {
|
||||
m.buf.WriteString("\"")
|
||||
m.buf.WriteString(util.EscapeUnprintables(v.String()))
|
||||
m.buf.WriteString("\"")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIndent(w io.Writer, indent string, depth int) {
|
||||
for i := 0; i < depth; i++ {
|
||||
io.WriteString(w, indent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
func ReadString(r io.Reader) (string, error) {
|
||||
b := make([]byte, 1)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for {
|
||||
if _, err := r.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if b[0] == 0 {
|
||||
return buf.String(), nil
|
||||
}
|
||||
buf.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
func WriteString(w io.Writer, s string) error {
|
||||
if _, err := w.Write([]byte(s)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func FormatSignedFixedFloat1616(val int32) string {
|
||||
if val&0xffff == 0 {
|
||||
return strconv.Itoa(int(val >> 16))
|
||||
} else {
|
||||
return strconv.FormatFloat(float64(val)/(1<<16), 'f', 5, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatUnsignedFixedFloat1616(val uint32) string {
|
||||
if val&0xffff == 0 {
|
||||
return strconv.Itoa(int(val >> 16))
|
||||
} else {
|
||||
return strconv.FormatFloat(float64(val)/(1<<16), 'f', 5, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func FormatSignedFixedFloat88(val int16) string {
|
||||
if val&0xff == 0 {
|
||||
return strconv.Itoa(int(val >> 8))
|
||||
} else {
|
||||
return strconv.FormatFloat(float64(val)/(1<<8), 'f', 3, 32)
|
||||
}
|
||||
}
|
||||
|
||||
func EscapeUnprintable(r rune) rune {
|
||||
if unicode.IsGraphic(r) {
|
||||
return r
|
||||
}
|
||||
return rune('.')
|
||||
}
|
||||
|
||||
func EscapeUnprintables(src string) string {
|
||||
return strings.Map(EscapeUnprintable, src)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package mp4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
writer io.WriteSeeker
|
||||
biStack []*BoxInfo
|
||||
}
|
||||
|
||||
func NewWriter(w io.WriteSeeker) *Writer {
|
||||
return &Writer{
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) Write(p []byte) (int, error) {
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
|
||||
func (w *Writer) Seek(offset int64, whence int) (int64, error) {
|
||||
return w.writer.Seek(offset, whence)
|
||||
}
|
||||
|
||||
func (w *Writer) StartBox(bi *BoxInfo) (*BoxInfo, error) {
|
||||
bi, err := WriteBoxInfo(w.writer, bi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.biStack = append(w.biStack, bi)
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (w *Writer) EndBox() (*BoxInfo, error) {
|
||||
bi := w.biStack[len(w.biStack)-1]
|
||||
w.biStack = w.biStack[:len(w.biStack)-1]
|
||||
end, err := w.writer.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi.Size = uint64(end) - bi.Offset
|
||||
if _, err = bi.SeekToStart(w.writer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bi2, err := WriteBoxInfo(w.writer, bi); err != nil {
|
||||
return nil, err
|
||||
} else if bi.HeaderSize != bi2.HeaderSize {
|
||||
return nil, errors.New("header size changed")
|
||||
}
|
||||
if _, err := w.writer.Seek(end, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
func (w *Writer) CopyBox(r io.ReadSeeker, bi *BoxInfo) error {
|
||||
if _, err := bi.SeekToStart(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if n, err := io.CopyN(w, r, int64(bi.Size)); err != nil {
|
||||
return err
|
||||
} else if n != int64(bi.Size) {
|
||||
return errors.New("failed to copy box")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -66,6 +66,11 @@ codeberg.org/gruf/go-sched
|
|||
codeberg.org/gruf/go-store/v2/kv
|
||||
codeberg.org/gruf/go-store/v2/storage
|
||||
codeberg.org/gruf/go-store/v2/util
|
||||
# github.com/abema/go-mp4 v0.8.0
|
||||
## explicit; go 1.14
|
||||
github.com/abema/go-mp4
|
||||
github.com/abema/go-mp4/bitio
|
||||
github.com/abema/go-mp4/util
|
||||
# github.com/aymerick/douceur v0.2.0
|
||||
## explicit
|
||||
github.com/aymerick/douceur/css
|
||||
|
|
|
@ -232,6 +232,9 @@ main {
|
|||
}
|
||||
|
||||
input.sensitive-checkbox:checked { /* Media is shown */
|
||||
& ~ .video-play {
|
||||
display: flex;
|
||||
}
|
||||
& ~ .sensitive {
|
||||
.closed {
|
||||
transition: 0.8s;
|
||||
|
@ -256,6 +259,32 @@ main {
|
|||
}
|
||||
}
|
||||
|
||||
.video-play {
|
||||
.icon-span {
|
||||
align-self: center;
|
||||
display: initial;
|
||||
z-index: 4;
|
||||
|
||||
.icon {
|
||||
color: $white1;
|
||||
}
|
||||
|
||||
.icon-bg {
|
||||
color: $gray1;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
display: none;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 7em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sensitive {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
@ -412,4 +441,4 @@ footer + div { /* something weird from the devstack.. */
|
|||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js");
|
||||
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
|
||||
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
|
||||
const PhotoswipeVideoPlugin = require("photoswipe-video-plugin").default;
|
||||
|
||||
let [_, _user, type, id] = window.location.pathname.split("/");
|
||||
if (type == "statuses") {
|
||||
|
@ -39,6 +40,7 @@ const lightbox = new PhotoswipeLightbox({
|
|||
new PhotoswipeCaptionPlugin(lightbox, {
|
||||
type: 'auto',
|
||||
});
|
||||
new PhotoswipeVideoPlugin(lightbox, {});
|
||||
|
||||
lightbox.init();
|
||||
|
||||
|
@ -46,14 +48,14 @@ Array.from(document.getElementsByClassName("spoiler-label")).forEach((label) =>
|
|||
let checkbox = document.getElementById(label.htmlFor);
|
||||
if (checkbox != undefined) {
|
||||
function update() {
|
||||
if(checkbox.checked) {
|
||||
if (checkbox.checked) {
|
||||
label.innerHTML = "Show more";
|
||||
} else {
|
||||
label.innerHTML = "Show less";
|
||||
}
|
||||
}
|
||||
update();
|
||||
|
||||
label.addEventListener("click", () => {setTimeout(update, 1);});
|
||||
|
||||
label.addEventListener("click", () => { setTimeout(update, 1); });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"modern-normalize": "^1.1.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"photoswipe-video-plugin": "^1.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
|
|
|
@ -4201,6 +4201,11 @@ photoswipe-dynamic-caption-plugin@^1.2.7:
|
|||
resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2"
|
||||
integrity sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q==
|
||||
|
||||
photoswipe-video-plugin@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/photoswipe-video-plugin/-/photoswipe-video-plugin-1.0.2.tgz#156b6a72ffa86e6c6e2b486e8ec5b48f6696941a"
|
||||
integrity sha512-skNHaalLU7rptZ3zq4XfS5hPqSDD65ctvpf2X8buvC8BpOt6XKSIgRkLzTwgQOUm9yQ8kQ4mMget7CIqGcqtDg==
|
||||
|
||||
photoswipe@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.3.3.tgz#86351a33502a3ab7d1e483127fe596b20054218a"
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
{{range .}}
|
||||
<div class="media-wrapper">
|
||||
{{if not .Description}}
|
||||
<div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing image description</span></div>
|
||||
<div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing media description</span></div>
|
||||
{{end}}
|
||||
<input type="checkbox" id="sensitiveMedia-{{.ID}}" class="sensitive-checkbox hidden" {{if not $.Sensitive}}checked{{end}}/>
|
||||
<div class="sensitive">
|
||||
|
@ -35,7 +35,21 @@
|
|||
<label for="sensitiveMedia-{{.ID}}" class="button" role="button" tabindex="0">Show sensitive media</label>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{.URL}}" target="_blank" {{if .Description}}title="{{.Description}}"{{end}} data-pswp-width="{{.Meta.Original.Width}}px" data-pswp-height="{{.Meta.Original.Height}}px" data-cropped="true">
|
||||
{{ if eq .Type "video" }}
|
||||
<div class="video-play">
|
||||
<span class="icon-span fa-stack" aria-hidden="true">
|
||||
<i class="icon-bg fa fa-fw fa-circle fa-stack-1x"></i>
|
||||
<i class="icon fa fa-fw fa-play-circle fa-stack-1x"></i>
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="{{.URL}}"
|
||||
target="_blank"
|
||||
{{if .Description}}title="{{.Description}}"{{end}}
|
||||
data-pswp-width="{{.Meta.Original.Width}}px"
|
||||
data-pswp-height="{{.Meta.Original.Height}}px"
|
||||
{{if eq .Type "video"}}data-pswp-type="video"{{end}}
|
||||
data-cropped="true">
|
||||
<img src="{{.PreviewURL}}" {{if .Description}}alt="{{.Description}}"{{end}} data-blurhash="{{.Blurhash}}"/>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -51,4 +65,4 @@
|
|||
<div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a data-nosnippet href="{{.URL}}" class="toot-link">View toot</a>
|
||||
<a data-nosnippet href="{{.URL}}" class="toot-link">View toot</a>
|
||||
|
|
Loading…
Reference in New Issue