[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:
tobi 2022-12-17 05:38:56 +01:00 committed by GitHub
parent 0f38e7c9b0
commit 2bbc64be43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 6276 additions and 93 deletions

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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))
}

View file

@ -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
}

View file

@ -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()

View file

@ -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

View file

@ -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
}

View file

@ -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
}

140
internal/media/video.go Normal file
View file

@ -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
}

1
vendor/github.com/abema/go-mp4/.gitignore generated vendored Normal file
View file

@ -0,0 +1 @@
vendor

21
vendor/github.com/abema/go-mp4/LICENSE generated vendored Normal file
View file

@ -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.

153
vendor/github.com/abema/go-mp4/README.md generated vendored Normal file
View file

@ -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)
```

19
vendor/github.com/abema/go-mp4/anytype.go generated vendored Normal file
View file

@ -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
}

8
vendor/github.com/abema/go-mp4/bitio/bitio.go generated vendored Normal file
View file

@ -0,0 +1,8 @@
package bitio
import "errors"
var (
ErrInvalidAlignment = errors.New("invalid alignment")
ErrDiscouragedReader = errors.New("discouraged reader implementation")
)

97
vendor/github.com/abema/go-mp4/bitio/read.go generated vendored Normal file
View file

@ -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
}

61
vendor/github.com/abema/go-mp4/bitio/write.go generated vendored Normal file
View file

@ -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
}

188
vendor/github.com/abema/go-mp4/box.go generated vendored Normal file
View file

@ -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))
}

155
vendor/github.com/abema/go-mp4/box_info.go generated vendored Normal file
View file

@ -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)
}

2745
vendor/github.com/abema/go-mp4/box_types.go generated vendored Normal file

File diff suppressed because it is too large Load diff

98
vendor/github.com/abema/go-mp4/extract.go generated vendored Normal file
View file

@ -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
}

290
vendor/github.com/abema/go-mp4/field.go generated vendored Normal file
View file

@ -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
}

639
vendor/github.com/abema/go-mp4/marshaller.go generated vendored Normal file
View file

@ -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
}
}
}

151
vendor/github.com/abema/go-mp4/mp4.go generated vendored Normal file
View file

@ -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
}

673
vendor/github.com/abema/go-mp4/probe.go generated vendored Normal file
View file

@ -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
}

182
vendor/github.com/abema/go-mp4/read.go generated vendored Normal file
View file

@ -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
}

261
vendor/github.com/abema/go-mp4/string.go generated vendored Normal file
View file

@ -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)
}
}

30
vendor/github.com/abema/go-mp4/util/io.go generated vendored Normal file
View file

@ -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
}

42
vendor/github.com/abema/go-mp4/util/string.go generated vendored Normal file
View file

@ -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)
}

68
vendor/github.com/abema/go-mp4/write.go generated vendored Normal file
View file

@ -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
}

5
vendor/modules.txt vendored
View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -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); });
}
});

View file

@ -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",

View file

@ -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"

View file

@ -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>