diff --git a/README.md b/README.md
index b21abd1e0..83ce09fa9 100644
--- a/README.md
+++ b/README.md
@@ -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).
diff --git a/go.mod b/go.mod
index 3815daf68..3a3615fa1 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
codeberg.org/gruf/go-mutexes v1.1.4
codeberg.org/gruf/go-runners v1.3.1
codeberg.org/gruf/go-store/v2 v2.0.10
+ github.com/abema/go-mp4 v0.8.0
github.com/buckket/go-blurhash v1.1.0
github.com/coreos/go-oidc/v3 v3.4.0
github.com/cornelk/hashmap v1.0.8
diff --git a/go.sum b/go.sum
index bbe6bbeb9..5a97a1879 100644
--- a/go.sum
+++ b/go.sum
@@ -110,6 +110,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/abema/go-mp4 v0.8.0 h1:JHYkOvTfBpTnqJHiFFOXe8d6wiFy5MtDnA10fgccNqY=
+github.com/abema/go-mp4 v0.8.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
@@ -491,6 +493,8 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
+github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
@@ -568,6 +572,7 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
github.com/superseriousbusiness/activity v1.2.1-gts h1:wh7v0zYa1mJmqB35PSfvgl4cs51Dh5PyfKvcZLSxMQU=
github.com/superseriousbusiness/activity v1.2.1-gts/go.mod h1:AZw0Xb4Oju8rmaJCZ21gc5CPg47MmNgyac+Hx5jo8VM=
github.com/superseriousbusiness/exif-terminator v0.5.0 h1:57SO/geyaOl2v/lJSQLVcQbdghpyFuK8ZTtaHL81fUQ=
@@ -1177,11 +1182,14 @@ gopkg.in/mcuadros/go-syslog.v2 v2.3.0 h1:kcsiS+WsTKyIEPABJBJtoG0KkOS6yzvJ+/eZlhD
gopkg.in/mcuadros/go-syslog.v2 v2.3.0/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go
index edf724d4a..353ae9c16 100644
--- a/internal/api/client/instance/instancepatch_test.go
+++ b/internal/api/client/instance/instancepatch_test.go
@@ -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))
}
diff --git a/internal/media/image.go b/internal/media/image.go
index b095a6c49..aedac5707 100644
--- a/internal/media/image.go
+++ b/internal/media/image.go
@@ -38,16 +38,7 @@
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
-}
diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go
index 659740af6..a8912bde0 100644
--- a/internal/media/manager_test.go
+++ b/internal/media/manager_test.go
@@ -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()
diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go
index 94c8f9a7a..a7ea4dbab 100644
--- a/internal/media/processingmedia.go
+++ b/internal/media/processingmedia.go
@@ -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)
}
diff --git a/internal/media/test/test-mp4-original.mp4 b/internal/media/test/test-mp4-original.mp4
new file mode 100644
index 000000000..f78f51de6
Binary files /dev/null and b/internal/media/test/test-mp4-original.mp4 differ
diff --git a/internal/media/test/test-mp4-processed.mp4 b/internal/media/test/test-mp4-processed.mp4
new file mode 100644
index 000000000..f78f51de6
Binary files /dev/null and b/internal/media/test/test-mp4-processed.mp4 differ
diff --git a/internal/media/test/test-mp4-thumbnail.jpg b/internal/media/test/test-mp4-thumbnail.jpg
new file mode 100644
index 000000000..8bfdf1540
Binary files /dev/null and b/internal/media/test/test-mp4-thumbnail.jpg differ
diff --git a/internal/media/types.go b/internal/media/types.go
index b855d72b5..e7edfe643 100644
--- a/internal/media/types.go
+++ b/internal/media/types.go
@@ -34,6 +34,7 @@
// mime consts
const (
mimeImage = "image"
+ mimeVideo = "video"
mimeJpeg = "jpeg"
mimeImageJpeg = mimeImage + "/" + mimeJpeg
@@ -46,6 +47,9 @@
mimeWebp = "webp"
mimeImageWebp = mimeImage + "/" + mimeWebp
+
+ mimeMp4 = "mp4"
+ mimeVideoMp4 = mimeVideo + "/" + mimeMp4
)
type processState int32
@@ -128,3 +132,12 @@ type AdditionalEmojiInfo struct {
//
// 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
+}
diff --git a/internal/media/util.go b/internal/media/util.go
index 60661cbc0..387f5d65a 100644
--- a/internal/media/util.go
+++ b/internal/media/util.go
@@ -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
}
diff --git a/internal/media/video.go b/internal/media/video.go
new file mode 100644
index 000000000..ef486d63d
--- /dev/null
+++ b/internal/media/video.go
@@ -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 .
+*/
+
+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
+}
diff --git a/vendor/github.com/abema/go-mp4/.gitignore b/vendor/github.com/abema/go-mp4/.gitignore
new file mode 100644
index 000000000..22d0d82f8
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/.gitignore
@@ -0,0 +1 @@
+vendor
diff --git a/vendor/github.com/abema/go-mp4/LICENSE b/vendor/github.com/abema/go-mp4/LICENSE
new file mode 100644
index 000000000..c06ca63d3
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/LICENSE
@@ -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.
diff --git a/vendor/github.com/abema/go-mp4/README.md b/vendor/github.com/abema/go-mp4/README.md
new file mode 100644
index 000000000..2c6acfdd3
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/README.md
@@ -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)
+```
diff --git a/vendor/github.com/abema/go-mp4/anytype.go b/vendor/github.com/abema/go-mp4/anytype.go
new file mode 100644
index 000000000..d995f59b6
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/anytype.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/bitio/bitio.go b/vendor/github.com/abema/go-mp4/bitio/bitio.go
new file mode 100644
index 000000000..404fd1b82
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/bitio/bitio.go
@@ -0,0 +1,8 @@
+package bitio
+
+import "errors"
+
+var (
+ ErrInvalidAlignment = errors.New("invalid alignment")
+ ErrDiscouragedReader = errors.New("discouraged reader implementation")
+)
diff --git a/vendor/github.com/abema/go-mp4/bitio/read.go b/vendor/github.com/abema/go-mp4/bitio/read.go
new file mode 100644
index 000000000..4da76eae6
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/bitio/read.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/bitio/write.go b/vendor/github.com/abema/go-mp4/bitio/write.go
new file mode 100644
index 000000000..5f63dd2d2
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/bitio/write.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/box.go b/vendor/github.com/abema/go-mp4/box.go
new file mode 100644
index 000000000..72a137951
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/box.go
@@ -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))
+}
diff --git a/vendor/github.com/abema/go-mp4/box_info.go b/vendor/github.com/abema/go-mp4/box_info.go
new file mode 100644
index 000000000..b5c587fd9
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/box_info.go
@@ -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)
+}
diff --git a/vendor/github.com/abema/go-mp4/box_types.go b/vendor/github.com/abema/go-mp4/box_types.go
new file mode 100644
index 000000000..d662dddbe
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/box_types.go
@@ -0,0 +1,2745 @@
+package mp4
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/abema/go-mp4/bitio"
+ "github.com/abema/go-mp4/util"
+ "github.com/google/uuid"
+)
+
+/*************************** btrt ****************************/
+
+func BoxTypeBtrt() BoxType { return StrToBoxType("btrt") }
+
+func init() {
+ AddBoxDef(&Btrt{}, 0)
+}
+
+type Btrt struct {
+ Box
+ BufferSizeDB uint32 `mp4:"0,size=32"`
+ MaxBitrate uint32 `mp4:"1,size=32"`
+ AvgBitrate uint32 `mp4:"2,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Btrt) GetType() BoxType {
+ return BoxTypeBtrt()
+}
+
+/*************************** co64 ****************************/
+
+func BoxTypeCo64() BoxType { return StrToBoxType("co64") }
+
+func init() {
+ AddBoxDef(&Co64{}, 0)
+}
+
+type Co64 struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ ChunkOffset []uint64 `mp4:"2,size=64,len=dynamic"`
+}
+
+// GetType returns the BoxType
+func (*Co64) GetType() BoxType {
+ return BoxTypeCo64()
+}
+
+// GetFieldLength returns length of dynamic field
+func (co64 *Co64) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "ChunkOffset":
+ return uint(co64.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=co64 fieldName=%s", name))
+}
+
+/*************************** colr ****************************/
+
+func BoxTypeColr() BoxType { return StrToBoxType("colr") }
+
+func init() {
+ AddBoxDef(&Colr{})
+}
+
+type Colr struct {
+ Box
+ ColourType [4]byte `mp4:"0,size=8,string"`
+ ColourPrimaries uint16 `mp4:"1,size=16,opt=dynamic"`
+ TransferCharacteristics uint16 `mp4:"2,size=16,opt=dynamic"`
+ MatrixCoefficients uint16 `mp4:"3,size=16,opt=dynamic"`
+ FullRangeFlag bool `mp4:"4,size=1,opt=dynamic"`
+ Reserved uint8 `mp4:"5,size=7,opt=dynamic"`
+ Profile []byte `mp4:"6,size=8,opt=dynamic"`
+ Unknown []byte `mp4:"7,size=8,opt=dynamic"`
+}
+
+func (colr *Colr) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch colr.ColourType {
+ case [4]byte{'n', 'c', 'l', 'x'}:
+ switch name {
+ case "ColourType",
+ "ColourPrimaries",
+ "TransferCharacteristics",
+ "MatrixCoefficients",
+ "FullRangeFlag",
+ "Reserved":
+ return true
+ default:
+ return false
+ }
+ case [4]byte{'r', 'I', 'C', 'C'}, [4]byte{'p', 'r', 'o', 'f'}:
+ return name == "Profile"
+ default:
+ return name == "Unknown"
+ }
+}
+
+// GetType returns the BoxType
+func (*Colr) GetType() BoxType {
+ return BoxTypeColr()
+}
+
+/*************************** cslg ****************************/
+
+func BoxTypeCslg() BoxType { return StrToBoxType("cslg") }
+
+func init() {
+ AddBoxDef(&Cslg{}, 0, 1)
+}
+
+type Cslg struct {
+ FullBox `mp4:"0,extend"`
+ CompositionToDTSShiftV0 int32 `mp4:"1,size=32,ver=0"`
+ LeastDecodeToDisplayDeltaV0 int32 `mp4:"2,size=32,ver=0"`
+ GreatestDecodeToDisplayDeltaV0 int32 `mp4:"3,size=32,ver=0"`
+ CompositionStartTimeV0 int32 `mp4:"4,size=32,ver=0"`
+ CompositionEndTimeV0 int32 `mp4:"5,size=32,ver=0"`
+ CompositionToDTSShiftV1 int64 `mp4:"6,size=64,nver=0"`
+ LeastDecodeToDisplayDeltaV1 int64 `mp4:"7,size=64,nver=0"`
+ GreatestDecodeToDisplayDeltaV1 int64 `mp4:"8,size=64,nver=0"`
+ CompositionStartTimeV1 int64 `mp4:"9,size=64,nver=0"`
+ CompositionEndTimeV1 int64 `mp4:"10,size=64,nver=0"`
+}
+
+// GetType returns the BoxType
+func (*Cslg) GetType() BoxType {
+ return BoxTypeCslg()
+}
+
+func (cslg *Cslg) GetCompositionToDTSShift() int64 {
+ switch cslg.GetVersion() {
+ case 0:
+ return int64(cslg.CompositionToDTSShiftV0)
+ case 1:
+ return cslg.CompositionToDTSShiftV1
+ default:
+ return 0
+ }
+}
+
+func (cslg *Cslg) GetLeastDecodeToDisplayDelta() int64 {
+ switch cslg.GetVersion() {
+ case 0:
+ return int64(cslg.LeastDecodeToDisplayDeltaV0)
+ case 1:
+ return cslg.LeastDecodeToDisplayDeltaV1
+ default:
+ return 0
+ }
+}
+
+func (cslg *Cslg) GetGreatestDecodeToDisplayDelta() int64 {
+ switch cslg.GetVersion() {
+ case 0:
+ return int64(cslg.GreatestDecodeToDisplayDeltaV0)
+ case 1:
+ return cslg.GreatestDecodeToDisplayDeltaV1
+ default:
+ return 0
+ }
+}
+
+func (cslg *Cslg) GetCompositionStartTime() int64 {
+ switch cslg.GetVersion() {
+ case 0:
+ return int64(cslg.CompositionStartTimeV0)
+ case 1:
+ return cslg.CompositionStartTimeV1
+ default:
+ return 0
+ }
+}
+
+func (cslg *Cslg) GetCompositionEndTime() int64 {
+ switch cslg.GetVersion() {
+ case 0:
+ return int64(cslg.CompositionEndTimeV0)
+ case 1:
+ return cslg.CompositionEndTimeV1
+ default:
+ return 0
+ }
+}
+
+/*************************** ctts ****************************/
+
+func BoxTypeCtts() BoxType { return StrToBoxType("ctts") }
+
+func init() {
+ AddBoxDef(&Ctts{}, 0, 1)
+}
+
+type Ctts struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ Entries []CttsEntry `mp4:"2,len=dynamic,size=64"`
+}
+
+type CttsEntry struct {
+ SampleCount uint32 `mp4:"0,size=32"`
+ SampleOffsetV0 uint32 `mp4:"1,size=32,ver=0"`
+ SampleOffsetV1 int32 `mp4:"2,size=32,ver=1"`
+}
+
+// GetType returns the BoxType
+func (*Ctts) GetType() BoxType {
+ return BoxTypeCtts()
+}
+
+// GetFieldLength returns length of dynamic field
+func (ctts *Ctts) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(ctts.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=ctts fieldName=%s", name))
+}
+
+func (ctts *Ctts) GetSampleOffset(index int) int64 {
+ switch ctts.GetVersion() {
+ case 0:
+ return int64(ctts.Entries[index].SampleOffsetV0)
+ case 1:
+ return int64(ctts.Entries[index].SampleOffsetV1)
+ default:
+ return 0
+ }
+}
+
+/*************************** dinf ****************************/
+
+func BoxTypeDinf() BoxType { return StrToBoxType("dinf") }
+
+func init() {
+ AddBoxDef(&Dinf{})
+}
+
+// Dinf is ISOBMFF dinf box type
+type Dinf struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Dinf) GetType() BoxType {
+ return BoxTypeDinf()
+}
+
+/*************************** dref ****************************/
+
+func BoxTypeDref() BoxType { return StrToBoxType("dref") }
+func BoxTypeUrl() BoxType { return StrToBoxType("url ") }
+func BoxTypeUrn() BoxType { return StrToBoxType("urn ") }
+
+func init() {
+ AddBoxDef(&Dref{}, 0)
+ AddBoxDef(&Url{}, 0)
+ AddBoxDef(&Urn{}, 0)
+}
+
+// Dref is ISOBMFF dref box type
+type Dref struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Dref) GetType() BoxType {
+ return BoxTypeDref()
+}
+
+type Url struct {
+ FullBox `mp4:"0,extend"`
+ Location string `mp4:"1,string,nopt=0x000001"`
+}
+
+func (*Url) GetType() BoxType {
+ return BoxTypeUrl()
+}
+
+const UrlSelfContained = 0x000001
+
+type Urn struct {
+ FullBox `mp4:"0,extend"`
+ Name string `mp4:"1,string,nopt=0x000001"`
+ Location string `mp4:"2,string,nopt=0x000001"`
+}
+
+func (*Urn) GetType() BoxType {
+ return BoxTypeUrn()
+}
+
+const UrnSelfContained = 0x000001
+
+/*************************** edts ****************************/
+
+func BoxTypeEdts() BoxType { return StrToBoxType("edts") }
+
+func init() {
+ AddBoxDef(&Edts{})
+}
+
+// Edts is ISOBMFF edts box type
+type Edts struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Edts) GetType() BoxType {
+ return BoxTypeEdts()
+}
+
+/*************************** elst ****************************/
+
+func BoxTypeElst() BoxType { return StrToBoxType("elst") }
+
+func init() {
+ AddBoxDef(&Elst{}, 0, 1)
+}
+
+// Elst is ISOBMFF elst box type
+type Elst struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ Entries []ElstEntry `mp4:"2,len=dynamic,size=dynamic"`
+}
+
+type ElstEntry struct {
+ SegmentDurationV0 uint32 `mp4:"0,size=32,ver=0"`
+ MediaTimeV0 int32 `mp4:"1,size=32,ver=0"`
+ SegmentDurationV1 uint64 `mp4:"2,size=64,ver=1"`
+ MediaTimeV1 int64 `mp4:"3,size=64,ver=1"`
+ MediaRateInteger int16 `mp4:"4,size=16"`
+ MediaRateFraction int16 `mp4:"5,size=16,const=0"`
+}
+
+// GetType returns the BoxType
+func (*Elst) GetType() BoxType {
+ return BoxTypeElst()
+}
+
+// GetFieldSize returns size of dynamic field
+func (elst *Elst) GetFieldSize(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ switch elst.GetVersion() {
+ case 0:
+ return 0 +
+ /* segmentDurationV0 */ 32 +
+ /* mediaTimeV0 */ 32 +
+ /* mediaRateInteger */ 16 +
+ /* mediaRateFraction */ 16
+ case 1:
+ return 0 +
+ /* segmentDurationV1 */ 64 +
+ /* mediaTimeV1 */ 64 +
+ /* mediaRateInteger */ 16 +
+ /* mediaRateFraction */ 16
+ }
+ }
+ panic(fmt.Errorf("invalid name of dynamic-size field: boxType=elst fieldName=%s", name))
+}
+
+// GetFieldLength returns length of dynamic field
+func (elst *Elst) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(elst.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=elst fieldName=%s", name))
+}
+
+func (elst *Elst) GetSegmentDuration(index int) uint64 {
+ switch elst.GetVersion() {
+ case 0:
+ return uint64(elst.Entries[index].SegmentDurationV0)
+ case 1:
+ return elst.Entries[index].SegmentDurationV1
+ default:
+ return 0
+ }
+}
+
+func (elst *Elst) GetMediaTime(index int) int64 {
+ switch elst.GetVersion() {
+ case 0:
+ return int64(elst.Entries[index].MediaTimeV0)
+ case 1:
+ return elst.Entries[index].MediaTimeV1
+ default:
+ return 0
+ }
+}
+
+/*************************** emsg ****************************/
+
+func BoxTypeEmsg() BoxType { return StrToBoxType("emsg") }
+
+func init() {
+ AddBoxDef(&Emsg{}, 0, 1)
+}
+
+// Emsg is ISOBMFF emsg box type
+type Emsg struct {
+ FullBox `mp4:"0,extend"`
+ SchemeIdUri string `mp4:"1,string"`
+ Value string `mp4:"2,string"`
+ Timescale uint32 `mp4:"3,size=32"`
+ PresentationTimeDelta uint32 `mp4:"4,size=32,ver=0"`
+ PresentationTime uint64 `mp4:"5,size=64,ver=1"`
+ EventDuration uint32 `mp4:"6,size=32"`
+ Id uint32 `mp4:"7,size=32"`
+ MessageData []byte `mp4:"8,size=8,string"`
+}
+
+func (emsg *Emsg) OnReadField(name string, r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error) {
+ if emsg.GetVersion() == 0 {
+ return
+ }
+ switch name {
+ case "SchemeIdUri", "Value":
+ override = true
+ return
+ case "MessageData":
+ emsg.SchemeIdUri, err = util.ReadString(r)
+ if err != nil {
+ return
+ }
+ emsg.Value, err = util.ReadString(r)
+ if err != nil {
+ return
+ }
+ rbits += uint64(len(emsg.SchemeIdUri)+len(emsg.Value)+2) * 8
+ return
+ default:
+ return
+ }
+}
+
+func (emsg *Emsg) OnWriteField(name string, w bitio.Writer, ctx Context) (wbits uint64, override bool, err error) {
+ if emsg.GetVersion() == 0 {
+ return
+ }
+ switch name {
+ case "SchemeIdUri", "Value":
+ override = true
+ return
+ case "MessageData":
+ if err = util.WriteString(w, emsg.SchemeIdUri); err != nil {
+ return
+ }
+ if err = util.WriteString(w, emsg.Value); err != nil {
+ return
+ }
+ wbits += uint64(len(emsg.SchemeIdUri)+len(emsg.Value)+2) * 8
+ return
+ default:
+ return
+ }
+}
+
+// GetType returns the BoxType
+func (*Emsg) GetType() BoxType {
+ return BoxTypeEmsg()
+}
+
+/*************************** esds ****************************/
+
+// https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
+
+func BoxTypeEsds() BoxType { return StrToBoxType("esds") }
+
+func init() {
+ AddBoxDef(&Esds{}, 0)
+}
+
+const (
+ ESDescrTag = 0x03
+ DecoderConfigDescrTag = 0x04
+ DecSpecificInfoTag = 0x05
+ SLConfigDescrTag = 0x06
+)
+
+// Esds is ES descripter box
+type Esds struct {
+ FullBox `mp4:"0,extend"`
+ Descriptors []Descriptor `mp4:"1,array"`
+}
+
+// GetType returns the BoxType
+func (*Esds) GetType() BoxType {
+ return BoxTypeEsds()
+}
+
+type Descriptor struct {
+ BaseCustomFieldObject
+ Tag int8 `mp4:"0,size=8"` // must be 0x03
+ Size uint32 `mp4:"1,varint"`
+ ESDescriptor *ESDescriptor `mp4:"2,extend,opt=dynamic"`
+ DecoderConfigDescriptor *DecoderConfigDescriptor `mp4:"3,extend,opt=dynamic"`
+ Data []byte `mp4:"4,size=8,opt=dynamic,len=dynamic"`
+}
+
+// GetFieldLength returns length of dynamic field
+func (ds *Descriptor) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Data":
+ return uint(ds.Size)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=esds fieldName=%s", name))
+}
+
+func (ds *Descriptor) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch ds.Tag {
+ case ESDescrTag:
+ return name == "ESDescriptor"
+ case DecoderConfigDescrTag:
+ return name == "DecoderConfigDescriptor"
+ default:
+ return name == "Data"
+ }
+}
+
+// StringifyField returns field value as string
+func (ds *Descriptor) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "Tag":
+ switch ds.Tag {
+ case ESDescrTag:
+ return "ESDescr", true
+ case DecoderConfigDescrTag:
+ return "DecoderConfigDescr", true
+ case DecSpecificInfoTag:
+ return "DecSpecificInfo", true
+ case SLConfigDescrTag:
+ return "SLConfigDescr", true
+ default:
+ return "", false
+ }
+ default:
+ return "", false
+ }
+}
+
+type ESDescriptor struct {
+ BaseCustomFieldObject
+ ESID uint16 `mp4:"0,size=16"`
+ StreamDependenceFlag bool `mp4:"1,size=1"`
+ UrlFlag bool `mp4:"2,size=1"`
+ OcrStreamFlag bool `mp4:"3,size=1"`
+ StreamPriority int8 `mp4:"4,size=5"`
+ DependsOnESID uint16 `mp4:"5,size=16,opt=dynamic"`
+ URLLength uint8 `mp4:"6,size=8,opt=dynamic"`
+ URLString []byte `mp4:"7,size=8,len=dynamic,opt=dynamic,string"`
+ OCRESID uint16 `mp4:"8,size=16,opt=dynamic"`
+}
+
+func (esds *ESDescriptor) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "URLString":
+ return uint(esds.URLLength)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=ESDescriptor fieldName=%s", name))
+}
+
+func (esds *ESDescriptor) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch name {
+ case "DependsOnESID":
+ return esds.StreamDependenceFlag
+ case "URLLength", "URLString":
+ return esds.UrlFlag
+ case "OCRESID":
+ return esds.OcrStreamFlag
+ default:
+ return false
+ }
+}
+
+type DecoderConfigDescriptor struct {
+ BaseCustomFieldObject
+ ObjectTypeIndication byte `mp4:"0,size=8"`
+ StreamType int8 `mp4:"1,size=6"`
+ UpStream bool `mp4:"2,size=1"`
+ Reserved bool `mp4:"3,size=1"`
+ BufferSizeDB uint32 `mp4:"4,size=24"`
+ MaxBitrate uint32 `mp4:"5,size=32"`
+ AvgBitrate uint32 `mp4:"6,size=32"`
+}
+
+/************************ free, skip *************************/
+
+func BoxTypeFree() BoxType { return StrToBoxType("free") }
+func BoxTypeSkip() BoxType { return StrToBoxType("skip") }
+
+func init() {
+ AddBoxDef(&Free{})
+ AddBoxDef(&Skip{})
+}
+
+type FreeSpace struct {
+ Box
+ Data []uint8 `mp4:"0,size=8"`
+}
+
+type Free FreeSpace
+
+func (*Free) GetType() BoxType {
+ return BoxTypeFree()
+}
+
+type Skip FreeSpace
+
+func (*Skip) GetType() BoxType {
+ return BoxTypeSkip()
+}
+
+/*************************** frma ****************************/
+
+func BoxTypeFrma() BoxType { return StrToBoxType("frma") }
+
+func init() {
+ AddBoxDef(&Frma{})
+}
+
+// Frma is ISOBMFF frma box type
+type Frma struct {
+ Box
+ DataFormat [4]byte `mp4:"0,size=8,string"`
+}
+
+// GetType returns the BoxType
+func (*Frma) GetType() BoxType {
+ return BoxTypeFrma()
+}
+
+/*************************** ftyp ****************************/
+
+func BoxTypeFtyp() BoxType { return StrToBoxType("ftyp") }
+
+func init() {
+ AddBoxDef(&Ftyp{})
+}
+
+func BrandQT() [4]byte { return [4]byte{'q', 't', ' ', ' '} }
+func BrandISOM() [4]byte { return [4]byte{'i', 's', 'o', 'm'} }
+func BrandISO2() [4]byte { return [4]byte{'i', 's', 'o', '2'} }
+func BrandISO3() [4]byte { return [4]byte{'i', 's', 'o', '3'} }
+func BrandISO4() [4]byte { return [4]byte{'i', 's', 'o', '4'} }
+func BrandISO5() [4]byte { return [4]byte{'i', 's', 'o', '5'} }
+func BrandISO6() [4]byte { return [4]byte{'i', 's', 'o', '6'} }
+func BrandISO7() [4]byte { return [4]byte{'i', 's', 'o', '7'} }
+func BrandISO8() [4]byte { return [4]byte{'i', 's', 'o', '8'} }
+func BrandISO9() [4]byte { return [4]byte{'i', 's', 'o', '9'} }
+func BrandAVC1() [4]byte { return [4]byte{'a', 'v', 'c', '1'} }
+func BrandMP41() [4]byte { return [4]byte{'m', 'p', '4', '1'} }
+func BrandMP71() [4]byte { return [4]byte{'m', 'p', '7', '1'} }
+
+// Ftyp is ISOBMFF ftyp box type
+type Ftyp struct {
+ Box
+ MajorBrand [4]byte `mp4:"0,size=8,string"`
+ MinorVersion uint32 `mp4:"1,size=32"`
+ CompatibleBrands []CompatibleBrandElem `mp4:"2,size=32"` // reach to end of the box
+}
+
+type CompatibleBrandElem struct {
+ CompatibleBrand [4]byte `mp4:"0,size=8,string"`
+}
+
+func (ftyp *Ftyp) AddCompatibleBrand(cb [4]byte) {
+ if !ftyp.HasCompatibleBrand(cb) {
+ ftyp.CompatibleBrands = append(ftyp.CompatibleBrands, CompatibleBrandElem{
+ CompatibleBrand: cb,
+ })
+ }
+}
+
+func (ftyp *Ftyp) RemoveCompatibleBrand(cb [4]byte) {
+ for i := 0; i < len(ftyp.CompatibleBrands); {
+ if ftyp.CompatibleBrands[i].CompatibleBrand != cb {
+ i++
+ continue
+ }
+ ftyp.CompatibleBrands[i] = ftyp.CompatibleBrands[len(ftyp.CompatibleBrands)-1]
+ ftyp.CompatibleBrands = ftyp.CompatibleBrands[:len(ftyp.CompatibleBrands)-1]
+ }
+}
+
+func (ftyp *Ftyp) HasCompatibleBrand(cb [4]byte) bool {
+ for i := range ftyp.CompatibleBrands {
+ if ftyp.CompatibleBrands[i].CompatibleBrand == cb {
+ return true
+ }
+ }
+ return false
+}
+
+// GetType returns the BoxType
+func (*Ftyp) GetType() BoxType {
+ return BoxTypeFtyp()
+}
+
+/*************************** hdlr ****************************/
+
+func BoxTypeHdlr() BoxType { return StrToBoxType("hdlr") }
+
+func init() {
+ AddBoxDef(&Hdlr{}, 0)
+}
+
+// Hdlr is ISOBMFF hdlr box type
+type Hdlr struct {
+ FullBox `mp4:"0,extend"`
+ // Predefined corresponds to component_type of QuickTime.
+ // pre_defined of ISO-14496 has always zero,
+ // however component_type has "mhlr" or "dhlr".
+ PreDefined uint32 `mp4:"1,size=32"`
+ HandlerType [4]byte `mp4:"2,size=8,string"`
+ Reserved [3]uint32 `mp4:"3,size=32,const=0"`
+ Name string `mp4:"4,string"`
+}
+
+// GetType returns the BoxType
+func (*Hdlr) GetType() BoxType {
+ return BoxTypeHdlr()
+}
+
+func (hdlr *Hdlr) OnReadField(name string, r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error) {
+ switch name {
+ case "Name":
+ return hdlr.OnReadName(r, leftBits, ctx)
+ default:
+ return 0, false, nil
+ }
+}
+
+func (hdlr *Hdlr) OnReadName(r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error) {
+ size := leftBits / 8
+ if size == 0 {
+ hdlr.Name = ""
+ return 0, true, nil
+ }
+
+ buf := make([]byte, size)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return 0, false, err
+ }
+
+ plen := buf[0]
+ if hdlr.PreDefined != 0 && size >= 2 && size == uint64(plen+1) {
+ // Pascal-style String
+ hdlr.Name = string(buf[1 : plen+1])
+ } else {
+ // C-style String
+ clen := 0
+ for _, c := range buf {
+ if c == 0x00 {
+ break
+ }
+ clen++
+ }
+ hdlr.Name = string(buf[:clen])
+ }
+ return leftBits, true, nil
+}
+
+/*************************** ilst ****************************/
+
+func BoxTypeIlst() BoxType { return StrToBoxType("ilst") }
+func BoxTypeData() BoxType { return StrToBoxType("data") }
+
+var ilstMetaBoxTypes = []BoxType{
+ StrToBoxType("----"),
+ StrToBoxType("aART"),
+ StrToBoxType("akID"),
+ StrToBoxType("apID"),
+ StrToBoxType("atID"),
+ StrToBoxType("cmID"),
+ StrToBoxType("cnID"),
+ StrToBoxType("covr"),
+ StrToBoxType("cpil"),
+ StrToBoxType("cprt"),
+ StrToBoxType("desc"),
+ StrToBoxType("disk"),
+ StrToBoxType("egid"),
+ StrToBoxType("geID"),
+ StrToBoxType("gnre"),
+ StrToBoxType("pcst"),
+ StrToBoxType("pgap"),
+ StrToBoxType("plID"),
+ StrToBoxType("purd"),
+ StrToBoxType("purl"),
+ StrToBoxType("rtng"),
+ StrToBoxType("sfID"),
+ StrToBoxType("soaa"),
+ StrToBoxType("soal"),
+ StrToBoxType("soar"),
+ StrToBoxType("soco"),
+ StrToBoxType("sonm"),
+ StrToBoxType("sosn"),
+ StrToBoxType("stik"),
+ StrToBoxType("tmpo"),
+ StrToBoxType("trkn"),
+ StrToBoxType("tven"),
+ StrToBoxType("tves"),
+ StrToBoxType("tvnn"),
+ StrToBoxType("tvsh"),
+ StrToBoxType("tvsn"),
+ {0xA9, 'A', 'R', 'T'},
+ {0xA9, 'a', 'l', 'b'},
+ {0xA9, 'c', 'm', 't'},
+ {0xA9, 'c', 'o', 'm'},
+ {0xA9, 'd', 'a', 'y'},
+ {0xA9, 'g', 'e', 'n'},
+ {0xA9, 'g', 'r', 'p'},
+ {0xA9, 'n', 'a', 'm'},
+ {0xA9, 't', 'o', 'o'},
+ {0xA9, 'w', 'r', 't'},
+}
+
+func IsIlstMetaBoxType(boxType BoxType) bool {
+ for _, bt := range ilstMetaBoxTypes {
+ if boxType == bt {
+ return true
+ }
+ }
+ return false
+}
+
+func init() {
+ AddBoxDef(&Ilst{})
+ AddBoxDefEx(&Data{}, isUnderIlstMeta)
+ for _, bt := range ilstMetaBoxTypes {
+ AddAnyTypeBoxDefEx(&IlstMetaContainer{}, bt, isIlstMetaContainer)
+ }
+ AddAnyTypeBoxDefEx(&StringData{}, StrToBoxType("mean"), isUnderIlstFreeFormat)
+ AddAnyTypeBoxDefEx(&StringData{}, StrToBoxType("name"), isUnderIlstFreeFormat)
+}
+
+type Ilst struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Ilst) GetType() BoxType {
+ return BoxTypeIlst()
+}
+
+type IlstMetaContainer struct {
+ AnyTypeBox
+}
+
+func isIlstMetaContainer(ctx Context) bool {
+ return ctx.UnderIlst && !ctx.UnderIlstMeta
+}
+
+const (
+ DataTypeBinary = 0
+ DataTypeStringUTF8 = 1
+ DataTypeStringUTF16 = 2
+ DataTypeStringMac = 3
+ DataTypeStringJPEG = 14
+ DataTypeSignedIntBigEndian = 21
+ DataTypeFloat32BigEndian = 22
+ DataTypeFloat64BigEndian = 23
+)
+
+type Data struct {
+ Box
+ DataType uint32 `mp4:"0,size=32"`
+ DataLang uint32 `mp4:"1,size=32"`
+ Data []byte `mp4:"2,size=8"`
+}
+
+// GetType returns the BoxType
+func (*Data) GetType() BoxType {
+ return BoxTypeData()
+}
+
+func isUnderIlstMeta(ctx Context) bool {
+ return ctx.UnderIlstMeta
+}
+
+// StringifyField returns field value as string
+func (data *Data) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "DataType":
+ switch data.DataType {
+ case DataTypeBinary:
+ return "BINARY", true
+ case DataTypeStringUTF8:
+ return "UTF8", true
+ case DataTypeStringUTF16:
+ return "UTF16", true
+ case DataTypeStringMac:
+ return "MAC_STR", true
+ case DataTypeStringJPEG:
+ return "JPEG", true
+ case DataTypeSignedIntBigEndian:
+ return "INT", true
+ case DataTypeFloat32BigEndian:
+ return "FLOAT32", true
+ case DataTypeFloat64BigEndian:
+ return "FLOAT64", true
+ }
+ case "Data":
+ switch data.DataType {
+ case DataTypeStringUTF8:
+ return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(data.Data))), true
+ }
+ }
+ return "", false
+}
+
+type StringData struct {
+ AnyTypeBox
+ Data []byte `mp4:"0,size=8"`
+}
+
+// StringifyField returns field value as string
+func (sd *StringData) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ if name == "Data" {
+ return fmt.Sprintf("\"%s\"", util.EscapeUnprintables(string(sd.Data))), true
+ }
+ return "", false
+}
+
+func isUnderIlstFreeFormat(ctx Context) bool {
+ return ctx.UnderIlstFreeMeta
+}
+
+/*************************** mdat ****************************/
+
+func BoxTypeMdat() BoxType { return StrToBoxType("mdat") }
+
+func init() {
+ AddBoxDef(&Mdat{})
+}
+
+// Mdat is ISOBMFF mdat box type
+type Mdat struct {
+ Box
+ Data []byte `mp4:"0,size=8"`
+}
+
+// GetType returns the BoxType
+func (*Mdat) GetType() BoxType {
+ return BoxTypeMdat()
+}
+
+/*************************** mdhd ****************************/
+
+func BoxTypeMdhd() BoxType { return StrToBoxType("mdhd") }
+
+func init() {
+ AddBoxDef(&Mdhd{}, 0, 1)
+}
+
+// Mdhd is ISOBMFF mdhd box type
+type Mdhd struct {
+ FullBox `mp4:"0,extend"`
+ CreationTimeV0 uint32 `mp4:"1,size=32,ver=0"`
+ ModificationTimeV0 uint32 `mp4:"2,size=32,ver=0"`
+ CreationTimeV1 uint64 `mp4:"3,size=64,ver=1"`
+ ModificationTimeV1 uint64 `mp4:"4,size=64,ver=1"`
+ Timescale uint32 `mp4:"5,size=32"`
+ DurationV0 uint32 `mp4:"6,size=32,ver=0"`
+ DurationV1 uint64 `mp4:"7,size=64,ver=1"`
+ //
+ Pad bool `mp4:"8,size=1,hidden"`
+ Language [3]byte `mp4:"9,size=5,iso639-2"` // ISO-639-2/T language code
+ PreDefined uint16 `mp4:"10,size=16"`
+}
+
+// GetType returns the BoxType
+func (*Mdhd) GetType() BoxType {
+ return BoxTypeMdhd()
+}
+
+func (mdhd *Mdhd) GetCreationTime() uint64 {
+ switch mdhd.GetVersion() {
+ case 0:
+ return uint64(mdhd.CreationTimeV0)
+ case 1:
+ return mdhd.CreationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (mdhd *Mdhd) GetModificationTime() uint64 {
+ switch mdhd.GetVersion() {
+ case 0:
+ return uint64(mdhd.ModificationTimeV0)
+ case 1:
+ return mdhd.ModificationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (mdhd *Mdhd) GetDuration() uint64 {
+ switch mdhd.GetVersion() {
+ case 0:
+ return uint64(mdhd.DurationV0)
+ case 1:
+ return mdhd.DurationV1
+ default:
+ return 0
+ }
+}
+
+/*************************** mdia ****************************/
+
+func BoxTypeMdia() BoxType { return StrToBoxType("mdia") }
+
+func init() {
+ AddBoxDef(&Mdia{})
+}
+
+// Mdia is ISOBMFF mdia box type
+type Mdia struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Mdia) GetType() BoxType {
+ return BoxTypeMdia()
+}
+
+/*************************** mehd ****************************/
+
+func BoxTypeMehd() BoxType { return StrToBoxType("mehd") }
+
+func init() {
+ AddBoxDef(&Mehd{}, 0, 1)
+}
+
+// Mehd is ISOBMFF mehd box type
+type Mehd struct {
+ FullBox `mp4:"0,extend"`
+ FragmentDurationV0 uint32 `mp4:"1,size=32,ver=0"`
+ FragmentDurationV1 uint64 `mp4:"2,size=64,ver=1"`
+}
+
+// GetType returns the BoxType
+func (*Mehd) GetType() BoxType {
+ return BoxTypeMehd()
+}
+
+func (mdhd *Mehd) GetFragmentDuration() uint64 {
+ switch mdhd.GetVersion() {
+ case 0:
+ return uint64(mdhd.FragmentDurationV0)
+ case 1:
+ return mdhd.FragmentDurationV1
+ default:
+ return 0
+ }
+}
+
+/*************************** meta ****************************/
+
+func BoxTypeMeta() BoxType { return StrToBoxType("meta") }
+
+func init() {
+ AddBoxDef(&Meta{}, 0)
+}
+
+// Meta is ISOBMFF meta box type
+type Meta struct {
+ FullBox `mp4:"0,extend"`
+}
+
+// GetType returns the BoxType
+func (*Meta) GetType() BoxType {
+ return BoxTypeMeta()
+}
+
+func (meta *Meta) BeforeUnmarshal(r io.ReadSeeker, size uint64, ctx Context) (n uint64, override bool, err error) {
+ // for Apple Quick Time
+ buf := make([]byte, 4)
+ if _, err := io.ReadFull(r, buf); err != nil {
+ return 0, false, err
+ }
+ if _, err := r.Seek(-int64(len(buf)), io.SeekCurrent); err != nil {
+ return 0, false, err
+ }
+ if buf[0]|buf[1]|buf[2]|buf[3] != 0x00 {
+ meta.Version = 0
+ meta.Flags = [3]byte{0, 0, 0}
+ return 0, true, nil
+ }
+ return 0, false, nil
+}
+
+/*************************** mfhd ****************************/
+
+func BoxTypeMfhd() BoxType { return StrToBoxType("mfhd") }
+
+func init() {
+ AddBoxDef(&Mfhd{}, 0)
+}
+
+// Mfhd is ISOBMFF mfhd box type
+type Mfhd struct {
+ FullBox `mp4:"0,extend"`
+ SequenceNumber uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Mfhd) GetType() BoxType {
+ return BoxTypeMfhd()
+}
+
+/*************************** mfra ****************************/
+
+func BoxTypeMfra() BoxType { return StrToBoxType("mfra") }
+
+func init() {
+ AddBoxDef(&Mfra{})
+}
+
+// Mfra is ISOBMFF mfra box type
+type Mfra struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Mfra) GetType() BoxType {
+ return BoxTypeMfra()
+}
+
+/*************************** mfro ****************************/
+
+func BoxTypeMfro() BoxType { return StrToBoxType("mfro") }
+
+func init() {
+ AddBoxDef(&Mfro{}, 0)
+}
+
+// Mfro is ISOBMFF mfro box type
+type Mfro struct {
+ FullBox `mp4:"0,extend"`
+ Size uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Mfro) GetType() BoxType {
+ return BoxTypeMfro()
+}
+
+/*************************** minf ****************************/
+
+func BoxTypeMinf() BoxType { return StrToBoxType("minf") }
+
+func init() {
+ AddBoxDef(&Minf{})
+}
+
+// Minf is ISOBMFF minf box type
+type Minf struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Minf) GetType() BoxType {
+ return BoxTypeMinf()
+}
+
+/*************************** moof ****************************/
+
+func BoxTypeMoof() BoxType { return StrToBoxType("moof") }
+
+func init() {
+ AddBoxDef(&Moof{})
+}
+
+// Moof is ISOBMFF moof box type
+type Moof struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Moof) GetType() BoxType {
+ return BoxTypeMoof()
+}
+
+/*************************** moov ****************************/
+
+func BoxTypeMoov() BoxType { return StrToBoxType("moov") }
+
+func init() {
+ AddBoxDef(&Moov{})
+}
+
+// Moov is ISOBMFF moov box type
+type Moov struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Moov) GetType() BoxType {
+ return BoxTypeMoov()
+}
+
+/*************************** mvex ****************************/
+
+func BoxTypeMvex() BoxType { return StrToBoxType("mvex") }
+
+func init() {
+ AddBoxDef(&Mvex{})
+}
+
+// Mvex is ISOBMFF mvex box type
+type Mvex struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Mvex) GetType() BoxType {
+ return BoxTypeMvex()
+}
+
+/*************************** mvhd ****************************/
+
+func BoxTypeMvhd() BoxType { return StrToBoxType("mvhd") }
+
+func init() {
+ AddBoxDef(&Mvhd{}, 0, 1)
+}
+
+// Mvhd is ISOBMFF mvhd box type
+type Mvhd struct {
+ FullBox `mp4:"0,extend"`
+ CreationTimeV0 uint32 `mp4:"1,size=32,ver=0"`
+ ModificationTimeV0 uint32 `mp4:"2,size=32,ver=0"`
+ CreationTimeV1 uint64 `mp4:"3,size=64,ver=1"`
+ ModificationTimeV1 uint64 `mp4:"4,size=64,ver=1"`
+ Timescale uint32 `mp4:"5,size=32"`
+ DurationV0 uint32 `mp4:"6,size=32,ver=0"`
+ DurationV1 uint64 `mp4:"7,size=64,ver=1"`
+ Rate int32 `mp4:"8,size=32"` // fixed-point 16.16 - template=0x00010000
+ Volume int16 `mp4:"9,size=16"` // template=0x0100
+ Reserved int16 `mp4:"10,size=16,const=0"`
+ Reserved2 [2]uint32 `mp4:"11,size=32,const=0"`
+ Matrix [9]int32 `mp4:"12,size=32,hex"` // template={ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
+ PreDefined [6]int32 `mp4:"13,size=32"`
+ NextTrackID uint32 `mp4:"14,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Mvhd) GetType() BoxType {
+ return BoxTypeMvhd()
+}
+
+// StringifyField returns field value as string
+func (mvhd *Mvhd) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "Rate":
+ return util.FormatSignedFixedFloat1616(mvhd.Rate), true
+ default:
+ return "", false
+ }
+}
+
+func (mvhd *Mvhd) GetCreationTime() uint64 {
+ switch mvhd.GetVersion() {
+ case 0:
+ return uint64(mvhd.CreationTimeV0)
+ case 1:
+ return mvhd.CreationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (mvhd *Mvhd) GetModificationTime() uint64 {
+ switch mvhd.GetVersion() {
+ case 0:
+ return uint64(mvhd.ModificationTimeV0)
+ case 1:
+ return mvhd.ModificationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (mvhd *Mvhd) GetDuration() uint64 {
+ switch mvhd.GetVersion() {
+ case 0:
+ return uint64(mvhd.DurationV0)
+ case 1:
+ return mvhd.DurationV1
+ default:
+ return 0
+ }
+}
+
+// GetRate returns value of rate as float64
+func (mvhd *Mvhd) GetRate() float64 {
+ return float64(mvhd.Rate) / (1 << 16)
+}
+
+// GetRateInt returns value of rate as int16
+func (mvhd *Mvhd) GetRateInt() int16 {
+ return int16(mvhd.Rate >> 16)
+}
+
+/*************************** pssh ****************************/
+
+func BoxTypePssh() BoxType { return StrToBoxType("pssh") }
+
+func init() {
+ AddBoxDef(&Pssh{}, 0, 1)
+}
+
+// Pssh is ISOBMFF pssh box type
+type Pssh struct {
+ FullBox `mp4:"0,extend"`
+ SystemID [16]byte `mp4:"1,size=8,uuid"`
+ KIDCount uint32 `mp4:"2,size=32,nver=0"`
+ KIDs []PsshKID `mp4:"3,nver=0,len=dynamic,size=128"`
+ DataSize int32 `mp4:"4,size=32"`
+ Data []byte `mp4:"5,size=8,len=dynamic"`
+}
+
+type PsshKID struct {
+ KID [16]byte `mp4:"0,size=8,uuid"`
+}
+
+// GetFieldLength returns length of dynamic field
+func (pssh *Pssh) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "KIDs":
+ return uint(pssh.KIDCount)
+ case "Data":
+ return uint(pssh.DataSize)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=pssh fieldName=%s", name))
+}
+
+// StringifyField returns field value as string
+func (pssh *Pssh) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "KIDs":
+ buf := bytes.NewBuffer(nil)
+ buf.WriteString("[")
+ for i, e := range pssh.KIDs {
+ if i != 0 {
+ buf.WriteString(", ")
+ }
+ buf.WriteString(uuid.UUID(e.KID).String())
+ }
+ buf.WriteString("]")
+ return buf.String(), true
+
+ default:
+ return "", false
+ }
+}
+
+// GetType returns the BoxType
+func (*Pssh) GetType() BoxType {
+ return BoxTypePssh()
+}
+
+/*************************** saio ****************************/
+
+func BoxTypeSaio() BoxType { return StrToBoxType("saio") }
+
+func init() {
+ AddBoxDef(&Saio{}, 0, 1)
+}
+
+type Saio struct {
+ FullBox `mp4:"0,extend"`
+ AuxInfoType [4]byte `mp4:"1,size=8,opt=0x000001,string"`
+ AuxInfoTypeParameter uint32 `mp4:"2,size=32,opt=0x000001,hex"`
+ EntryCount uint32 `mp4:"3,size=32"`
+ OffsetV0 []uint32 `mp4:"4,size=32,ver=0,len=dynamic"`
+ OffsetV1 []uint64 `mp4:"5,size=64,nver=0,len=dynamic"`
+}
+
+func (saio *Saio) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "OffsetV0", "OffsetV1":
+ return uint(saio.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=saio fieldName=%s", name))
+}
+
+func (*Saio) GetType() BoxType {
+ return BoxTypeSaio()
+}
+
+func (saio *Saio) GetOffset(index int) uint64 {
+ switch saio.GetVersion() {
+ case 0:
+ return uint64(saio.OffsetV0[index])
+ case 1:
+ return saio.OffsetV1[index]
+ default:
+ return 0
+ }
+}
+
+/*************************** saiz ****************************/
+
+func BoxTypeSaiz() BoxType { return StrToBoxType("saiz") }
+
+func init() {
+ AddBoxDef(&Saiz{}, 0)
+}
+
+type Saiz struct {
+ FullBox `mp4:"0,extend"`
+ AuxInfoType [4]byte `mp4:"1,size=8,opt=0x000001,string"`
+ AuxInfoTypeParameter uint32 `mp4:"2,size=32,opt=0x000001,hex"`
+ DefaultSampleInfoSize uint8 `mp4:"3,size=8,dec"`
+ SampleCount uint32 `mp4:"4,size=32"`
+ SampleInfoSize []uint8 `mp4:"5,size=8,opt=dynamic,len=dynamic,dec"`
+}
+
+func (saiz *Saiz) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch name {
+ case "SampleInfoSize":
+ return saiz.DefaultSampleInfoSize == 0
+ }
+ return false
+}
+
+func (saiz *Saiz) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "SampleInfoSize":
+ return uint(saiz.SampleCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=saiz fieldName=%s", name))
+}
+
+func (*Saiz) GetType() BoxType {
+ return BoxTypeSaiz()
+}
+
+/*********************** SampleEntry *************************/
+
+func BoxTypeAvc1() BoxType { return StrToBoxType("avc1") }
+func BoxTypeEncv() BoxType { return StrToBoxType("encv") }
+func BoxTypeMp4a() BoxType { return StrToBoxType("mp4a") }
+func BoxTypeEnca() BoxType { return StrToBoxType("enca") }
+func BoxTypeAvcC() BoxType { return StrToBoxType("avcC") }
+func BoxTypePasp() BoxType { return StrToBoxType("pasp") }
+
+func init() {
+ AddAnyTypeBoxDef(&VisualSampleEntry{}, BoxTypeAvc1())
+ AddAnyTypeBoxDef(&VisualSampleEntry{}, BoxTypeEncv())
+ AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeMp4a())
+ AddAnyTypeBoxDef(&AudioSampleEntry{}, BoxTypeEnca())
+ AddAnyTypeBoxDef(&AVCDecoderConfiguration{}, BoxTypeAvcC())
+ AddAnyTypeBoxDef(&PixelAspectRatioBox{}, BoxTypePasp())
+}
+
+type SampleEntry struct {
+ AnyTypeBox
+ Reserved [6]uint8 `mp4:"0,size=8,const=0"`
+ DataReferenceIndex uint16 `mp4:"1,size=16"`
+}
+
+type VisualSampleEntry struct {
+ SampleEntry `mp4:"0,extend"`
+ PreDefined uint16 `mp4:"1,size=16"`
+ Reserved uint16 `mp4:"2,size=16,const=0"`
+ PreDefined2 [3]uint32 `mp4:"3,size=32"`
+ Width uint16 `mp4:"4,size=16"`
+ Height uint16 `mp4:"5,size=16"`
+ Horizresolution uint32 `mp4:"6,size=32"`
+ Vertresolution uint32 `mp4:"7,size=32"`
+ Reserved2 uint32 `mp4:"8,size=32,const=0"`
+ FrameCount uint16 `mp4:"9,size=16"`
+ Compressorname [32]byte `mp4:"10,size=8"`
+ Depth uint16 `mp4:"11,size=16"`
+ PreDefined3 int16 `mp4:"12,size=16"`
+}
+
+// StringifyField returns field value as string
+func (vse *VisualSampleEntry) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "Compressorname":
+ if vse.Compressorname[0] <= 31 {
+ return `"` + util.EscapeUnprintables(string(vse.Compressorname[1:vse.Compressorname[0]+1])) + `"`, true
+ }
+ return "", false
+ default:
+ return "", false
+ }
+}
+
+type AudioSampleEntry struct {
+ SampleEntry `mp4:"0,extend,opt=dynamic"`
+ EntryVersion uint16 `mp4:"1,size=16,opt=dynamic"`
+ Reserved [3]uint16 `mp4:"2,size=16,opt=dynamic,const=0"`
+ ChannelCount uint16 `mp4:"3,size=16,opt=dynamic"`
+ SampleSize uint16 `mp4:"4,size=16,opt=dynamic"`
+ PreDefined uint16 `mp4:"5,size=16,opt=dynamic"`
+ Reserved2 uint16 `mp4:"6,size=16,opt=dynamic,const=0"`
+ SampleRate uint32 `mp4:"7,size=32,opt=dynamic"`
+ QuickTimeData []byte `mp4:"8,size=8,opt=dynamic,len=dynamic"`
+}
+
+func (ase *AudioSampleEntry) IsOptFieldEnabled(name string, ctx Context) bool {
+ if name == "QuickTimeData" {
+ return ctx.IsQuickTimeCompatible && (ctx.UnderWave || ase.EntryVersion == 1 || ase.EntryVersion == 2)
+ }
+ if ctx.IsQuickTimeCompatible && ctx.UnderWave {
+ return false
+ }
+ return true
+}
+
+func (ase *AudioSampleEntry) GetFieldLength(name string, ctx Context) uint {
+ if name == "QuickTimeData" && ctx.IsQuickTimeCompatible {
+ if ctx.UnderWave {
+ return LengthUnlimited
+ } else if ase.EntryVersion == 1 {
+ return 16
+ } else if ase.EntryVersion == 2 {
+ return 36
+ }
+ }
+ return 0
+}
+
+const (
+ AVCBaselineProfile uint8 = 66 // 0x42
+ AVCMainProfile uint8 = 77 // 0x4d
+ AVCExtendedProfile uint8 = 88 // 0x58
+ AVCHighProfile uint8 = 100 // 0x64
+ AVCHigh10Profile uint8 = 110 // 0x6e
+ AVCHigh422Profile uint8 = 122 // 0x7a
+)
+
+type AVCDecoderConfiguration struct {
+ AnyTypeBox
+ ConfigurationVersion uint8 `mp4:"0,size=8"`
+ Profile uint8 `mp4:"1,size=8"`
+ ProfileCompatibility uint8 `mp4:"2,size=8"`
+ Level uint8 `mp4:"3,size=8"`
+ Reserved uint8 `mp4:"4,size=6,const=63"`
+ LengthSizeMinusOne uint8 `mp4:"5,size=2"`
+ Reserved2 uint8 `mp4:"6,size=3,const=7"`
+ NumOfSequenceParameterSets uint8 `mp4:"7,size=5"`
+ SequenceParameterSets []AVCParameterSet `mp4:"8,len=dynamic"`
+ NumOfPictureParameterSets uint8 `mp4:"9,size=8"`
+ PictureParameterSets []AVCParameterSet `mp4:"10,len=dynamic"`
+ HighProfileFieldsEnabled bool `mp4:"11,hidden"`
+ Reserved3 uint8 `mp4:"12,size=6,opt=dynamic,const=63"`
+ ChromaFormat uint8 `mp4:"13,size=2,opt=dynamic"`
+ Reserved4 uint8 `mp4:"14,size=5,opt=dynamic,const=31"`
+ BitDepthLumaMinus8 uint8 `mp4:"15,size=3,opt=dynamic"`
+ Reserved5 uint8 `mp4:"16,size=5,opt=dynamic,const=31"`
+ BitDepthChromaMinus8 uint8 `mp4:"17,size=3,opt=dynamic"`
+ NumOfSequenceParameterSetExt uint8 `mp4:"18,size=8,opt=dynamic"`
+ SequenceParameterSetsExt []AVCParameterSet `mp4:"19,len=dynamic,opt=dynamic"`
+}
+
+func (avcc *AVCDecoderConfiguration) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "SequenceParameterSets":
+ return uint(avcc.NumOfSequenceParameterSets)
+ case "PictureParameterSets":
+ return uint(avcc.NumOfPictureParameterSets)
+ case "SequenceParameterSetsExt":
+ return uint(avcc.NumOfSequenceParameterSetExt)
+ }
+ return 0
+}
+
+func (avcc *AVCDecoderConfiguration) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch name {
+ case "Reserved3",
+ "ChromaFormat",
+ "Reserved4",
+ "BitDepthLumaMinus8",
+ "Reserved5",
+ "BitDepthChromaMinus8",
+ "NumOfSequenceParameterSetExt",
+ "SequenceParameterSetsExt":
+ return avcc.HighProfileFieldsEnabled
+ }
+ return false
+}
+
+func (avcc *AVCDecoderConfiguration) OnReadField(name string, r bitio.ReadSeeker, leftBits uint64, ctx Context) (rbits uint64, override bool, err error) {
+ if name == "HighProfileFieldsEnabled" {
+ avcc.HighProfileFieldsEnabled = leftBits >= 32 &&
+ (avcc.Profile == AVCHighProfile ||
+ avcc.Profile == AVCHigh10Profile ||
+ avcc.Profile == AVCHigh422Profile ||
+ avcc.Profile == 144)
+ return 0, true, nil
+ }
+ return 0, false, nil
+}
+
+func (avcc *AVCDecoderConfiguration) OnWriteField(name string, w bitio.Writer, ctx Context) (wbits uint64, override bool, err error) {
+ if name == "HighProfileFieldsEnabled" {
+ if avcc.HighProfileFieldsEnabled &&
+ avcc.Profile != AVCHighProfile &&
+ avcc.Profile != AVCHigh10Profile &&
+ avcc.Profile != AVCHigh422Profile &&
+ avcc.Profile != 144 {
+ return 0, false, errors.New("each values of Profile and HighProfileFieldsEnabled are inconsistent")
+ }
+ return 0, true, nil
+ }
+ return 0, false, nil
+}
+
+type AVCParameterSet struct {
+ BaseCustomFieldObject
+ Length uint16 `mp4:"0,size=16"`
+ NALUnit []byte `mp4:"1,size=8,len=dynamic"`
+}
+
+func (s *AVCParameterSet) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "NALUnit":
+ return uint(s.Length)
+ }
+ return 0
+}
+
+type PixelAspectRatioBox struct {
+ AnyTypeBox
+ HSpacing uint32 `mp4:"0,size=32"`
+ VSpacing uint32 `mp4:"1,size=32"`
+}
+
+/*************************** sbgp ****************************/
+
+func BoxTypeSbgp() BoxType { return StrToBoxType("sbgp") }
+
+func init() {
+ AddBoxDef(&Sbgp{}, 0, 1)
+}
+
+type Sbgp struct {
+ FullBox `mp4:"0,extend"`
+ GroupingType uint32 `mp4:"1,size=32"`
+ GroupingTypeParameter uint32 `mp4:"2,size=32,ver=1"`
+ EntryCount uint32 `mp4:"3,size=32"`
+ Entries []SbgpEntry `mp4:"4,len=dynamic,size=64"`
+}
+
+type SbgpEntry struct {
+ SampleCount uint32 `mp4:"0,size=32"`
+ GroupDescriptionIndex uint32 `mp4:"1,size=32"`
+}
+
+func (sbgp *Sbgp) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(sbgp.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=sbgp fieldName=%s", name))
+}
+
+func (*Sbgp) GetType() BoxType {
+ return BoxTypeSbgp()
+}
+
+/*************************** schi ****************************/
+
+func BoxTypeSchi() BoxType { return StrToBoxType("schi") }
+
+func init() {
+ AddBoxDef(&Schi{})
+}
+
+type Schi struct {
+ Box
+}
+
+func (*Schi) GetType() BoxType {
+ return BoxTypeSchi()
+}
+
+/*************************** schm ****************************/
+
+func BoxTypeSchm() BoxType { return StrToBoxType("schm") }
+
+func init() {
+ AddBoxDef(&Schm{}, 0)
+}
+
+type Schm struct {
+ FullBox `mp4:"0,extend"`
+ SchemeType [4]byte `mp4:"1,size=8,string"`
+ SchemeVersion uint32 `mp4:"2,size=32,hex"`
+ SchemeUri []byte `mp4:"3,size=8,opt=0x000001,string"`
+}
+
+func (*Schm) GetType() BoxType {
+ return BoxTypeSchm()
+}
+
+/*************************** sdtp ****************************/
+
+func BoxTypeSdtp() BoxType { return StrToBoxType("sdtp") }
+
+func init() {
+ AddBoxDef(&Sdtp{}, 0)
+}
+
+type Sdtp struct {
+ FullBox `mp4:"0,extend"`
+ Samples []SdtpSampleElem `mp4:"1,size=8"`
+}
+
+type SdtpSampleElem struct {
+ IsLeading uint8 `mp4:"0,size=2"`
+ SampleDependsOn uint8 `mp4:"1,size=2"`
+ SampleIsDependedOn uint8 `mp4:"2,size=2"`
+ SampleHasRedundancy uint8 `mp4:"3,size=2"`
+}
+
+func (*Sdtp) GetType() BoxType {
+ return BoxTypeSdtp()
+}
+
+/*************************** sgpd ****************************/
+
+func BoxTypeSgpd() BoxType { return StrToBoxType("sgpd") }
+
+func init() {
+ AddBoxDef(&Sgpd{}, 1, 2) // version 0 is deprecated by ISO/IEC 14496-12
+}
+
+type Sgpd struct {
+ FullBox `mp4:"0,extend"`
+ GroupingType [4]byte `mp4:"1,size=8,string"`
+ DefaultLength uint32 `mp4:"2,size=32,ver=1"`
+ DefaultSampleDescriptionIndex uint32 `mp4:"3,size=32,ver=2"`
+ EntryCount uint32 `mp4:"4,size=32"`
+ RollDistances []int16 `mp4:"5,size=16,opt=dynamic"`
+ RollDistancesL []RollDistanceWithLength `mp4:"6,size=16,opt=dynamic"`
+ AlternativeStartupEntries []AlternativeStartupEntry `mp4:"7,size=dynamic,len=dynamic,opt=dynamic"`
+ AlternativeStartupEntriesL []AlternativeStartupEntryL `mp4:"8,len=dynamic,opt=dynamic"`
+ VisualRandomAccessEntries []VisualRandomAccessEntry `mp4:"9,len=dynamic,opt=dynamic"`
+ VisualRandomAccessEntriesL []VisualRandomAccessEntryL `mp4:"10,len=dynamic,opt=dynamic"`
+ TemporalLevelEntries []TemporalLevelEntry `mp4:"11,len=dynamic,opt=dynamic"`
+ TemporalLevelEntriesL []TemporalLevelEntryL `mp4:"12,len=dynamic,opt=dynamic"`
+ Unsupported []byte `mp4:"13,size=8,opt=dynamic"`
+}
+
+type RollDistanceWithLength struct {
+ DescriptionLength uint32 `mp4:"0,size=32"`
+ RollDistance int16 `mp4:"1,size=16"`
+}
+
+type AlternativeStartupEntry struct {
+ BaseCustomFieldObject
+ RollCount uint16 `mp4:"0,size=16"`
+ FirstOutputSample uint16 `mp4:"1,size=16"`
+ SampleOffset []uint32 `mp4:"2,size=32,len=dynamic"`
+ Opts []AlternativeStartupEntryOpt `mp4:"3,size=32"`
+}
+
+type AlternativeStartupEntryL struct {
+ DescriptionLength uint32 `mp4:"0,size=32"`
+ AlternativeStartupEntry `mp4:"1,extend,size=dynamic"`
+}
+
+type AlternativeStartupEntryOpt struct {
+ NumOutputSamples uint16 `mp4:"0,size=16"`
+ NumTotalSamples uint16 `mp4:"1,size=16"`
+}
+
+type VisualRandomAccessEntry struct {
+ NumLeadingSamplesKnown bool `mp4:"0,size=1"`
+ NumLeadingSamples uint8 `mp4:"1,size=7"`
+}
+
+type VisualRandomAccessEntryL struct {
+ DescriptionLength uint32 `mp4:"0,size=32"`
+ VisualRandomAccessEntry `mp4:"1,extend"`
+}
+
+type TemporalLevelEntry struct {
+ LevelIndependentlyDecodable bool `mp4:"0,size=1"`
+ Reserved uint8 `mp4:"1,size=7,const=0"`
+}
+
+type TemporalLevelEntryL struct {
+ DescriptionLength uint32 `mp4:"0,size=32"`
+ TemporalLevelEntry `mp4:"1,extend"`
+}
+
+func (sgpd *Sgpd) GetFieldSize(name string, ctx Context) uint {
+ switch name {
+ case "AlternativeStartupEntries":
+ return uint(sgpd.DefaultLength * 8)
+ }
+ return 0
+}
+
+func (sgpd *Sgpd) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "RollDistances", "RollDistancesL",
+ "AlternativeStartupEntries", "AlternativeStartupEntriesL",
+ "VisualRandomAccessEntries", "VisualRandomAccessEntriesL",
+ "TemporalLevelEntries", "TemporalLevelEntriesL":
+ return uint(sgpd.EntryCount)
+ }
+ return 0
+}
+
+func (sgpd *Sgpd) IsOptFieldEnabled(name string, ctx Context) bool {
+ noDefaultLength := sgpd.Version == 1 && sgpd.DefaultLength == 0
+ rollDistances := sgpd.GroupingType == [4]byte{'r', 'o', 'l', 'l'} ||
+ sgpd.GroupingType == [4]byte{'p', 'r', 'o', 'l'}
+ alternativeStartupEntries := sgpd.GroupingType == [4]byte{'a', 'l', 's', 't'}
+ visualRandomAccessEntries := sgpd.GroupingType == [4]byte{'r', 'a', 'p', ' '}
+ temporalLevelEntries := sgpd.GroupingType == [4]byte{'t', 'e', 'l', 'e'}
+ switch name {
+ case "RollDistances":
+ return rollDistances && !noDefaultLength
+ case "RollDistancesL":
+ return rollDistances && noDefaultLength
+ case "AlternativeStartupEntries":
+ return alternativeStartupEntries && !noDefaultLength
+ case "AlternativeStartupEntriesL":
+ return alternativeStartupEntries && noDefaultLength
+ case "VisualRandomAccessEntries":
+ return visualRandomAccessEntries && !noDefaultLength
+ case "VisualRandomAccessEntriesL":
+ return visualRandomAccessEntries && noDefaultLength
+ case "TemporalLevelEntries":
+ return temporalLevelEntries && !noDefaultLength
+ case "TemporalLevelEntriesL":
+ return temporalLevelEntries && noDefaultLength
+ case "Unsupported":
+ return !rollDistances &&
+ !alternativeStartupEntries &&
+ !visualRandomAccessEntries &&
+ !temporalLevelEntries
+ default:
+ return false
+ }
+}
+
+func (*Sgpd) GetType() BoxType {
+ return BoxTypeSgpd()
+}
+
+func (entry *AlternativeStartupEntry) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "SampleOffset":
+ return uint(entry.RollCount)
+ }
+ return 0
+}
+
+func (entry *AlternativeStartupEntryL) GetFieldSize(name string, ctx Context) uint {
+ switch name {
+ case "AlternativeStartupEntry":
+ return uint(entry.DescriptionLength * 8)
+ }
+ return 0
+}
+
+/*************************** sidx ****************************/
+
+func BoxTypeSidx() BoxType { return StrToBoxType("sidx") }
+
+func init() {
+ AddBoxDef(&Sidx{}, 0, 1)
+}
+
+type Sidx struct {
+ FullBox `mp4:"0,extend"`
+ ReferenceID uint32 `mp4:"1,size=32"`
+ Timescale uint32 `mp4:"2,size=32"`
+ EarliestPresentationTimeV0 uint32 `mp4:"3,size=32,ver=0"`
+ FirstOffsetV0 uint32 `mp4:"4,size=32,ver=0"`
+ EarliestPresentationTimeV1 uint64 `mp4:"5,size=64,nver=0"`
+ FirstOffsetV1 uint64 `mp4:"6,size=64,nver=0"`
+ Reserved uint16 `mp4:"7,size=16,const=0"`
+ ReferenceCount uint16 `mp4:"8,size=16"`
+ References []SidxReference `mp4:"9,size=96,len=dynamic"`
+}
+
+type SidxReference struct {
+ ReferenceType bool `mp4:"0,size=1"`
+ ReferencedSize uint32 `mp4:"1,size=31"`
+ SubsegmentDuration uint32 `mp4:"2,size=32"`
+ StartsWithSAP bool `mp4:"3,size=1"`
+ SAPType uint32 `mp4:"4,size=3"`
+ SAPDeltaTime uint32 `mp4:"5,size=28"`
+}
+
+func (*Sidx) GetType() BoxType {
+ return BoxTypeSidx()
+}
+
+func (sidx *Sidx) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "References":
+ return uint(sidx.ReferenceCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=sidx fieldName=%s", name))
+}
+
+func (sidx *Sidx) GetEarliestPresentationTime() uint64 {
+ switch sidx.GetVersion() {
+ case 0:
+ return uint64(sidx.EarliestPresentationTimeV0)
+ case 1:
+ return sidx.EarliestPresentationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (sidx *Sidx) GetFirstOffset() uint64 {
+ switch sidx.GetVersion() {
+ case 0:
+ return uint64(sidx.FirstOffsetV0)
+ case 1:
+ return sidx.FirstOffsetV1
+ default:
+ return 0
+ }
+}
+
+/*************************** sinf ****************************/
+
+func BoxTypeSinf() BoxType { return StrToBoxType("sinf") }
+
+func init() {
+ AddBoxDef(&Sinf{})
+}
+
+type Sinf struct {
+ Box
+}
+
+func (*Sinf) GetType() BoxType {
+ return BoxTypeSinf()
+}
+
+/*************************** smhd ****************************/
+
+func BoxTypeSmhd() BoxType { return StrToBoxType("smhd") }
+
+func init() {
+ AddBoxDef(&Smhd{}, 0)
+}
+
+type Smhd struct {
+ FullBox `mp4:"0,extend"`
+ Balance int16 `mp4:"1,size=16"` // fixed-point 8.8 template=0
+ Reserved uint16 `mp4:"2,size=16,const=0"`
+}
+
+func (*Smhd) GetType() BoxType {
+ return BoxTypeSmhd()
+}
+
+// StringifyField returns field value as string
+func (smhd *Smhd) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "Balance":
+ return util.FormatSignedFixedFloat88(smhd.Balance), true
+ default:
+ return "", false
+ }
+}
+
+// GetBalance returns value of width as float32
+func (smhd *Smhd) GetBalance() float32 {
+ return float32(smhd.Balance) / (1 << 8)
+}
+
+// GetBalanceInt returns value of width as int8
+func (smhd *Smhd) GetBalanceInt() int8 {
+ return int8(smhd.Balance >> 8)
+}
+
+/*************************** stbl ****************************/
+
+func BoxTypeStbl() BoxType { return StrToBoxType("stbl") }
+
+func init() {
+ AddBoxDef(&Stbl{})
+}
+
+// Stbl is ISOBMFF stbl box type
+type Stbl struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Stbl) GetType() BoxType {
+ return BoxTypeStbl()
+}
+
+/*************************** stco ****************************/
+
+func BoxTypeStco() BoxType { return StrToBoxType("stco") }
+
+func init() {
+ AddBoxDef(&Stco{}, 0)
+}
+
+// Stco is ISOBMFF stco box type
+type Stco struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ ChunkOffset []uint32 `mp4:"2,size=32,len=dynamic"`
+}
+
+// GetType returns the BoxType
+func (*Stco) GetType() BoxType {
+ return BoxTypeStco()
+}
+
+// GetFieldLength returns length of dynamic field
+func (stco *Stco) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "ChunkOffset":
+ return uint(stco.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=stco fieldName=%s", name))
+}
+
+/*************************** stsc ****************************/
+
+func BoxTypeStsc() BoxType { return StrToBoxType("stsc") }
+
+func init() {
+ AddBoxDef(&Stsc{}, 0)
+}
+
+// Stsc is ISOBMFF stsc box type
+type Stsc struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ Entries []StscEntry `mp4:"2,len=dynamic,size=96"`
+}
+
+type StscEntry struct {
+ FirstChunk uint32 `mp4:"0,size=32"`
+ SamplesPerChunk uint32 `mp4:"1,size=32"`
+ SampleDescriptionIndex uint32 `mp4:"2,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Stsc) GetType() BoxType {
+ return BoxTypeStsc()
+}
+
+// GetFieldLength returns length of dynamic field
+func (stsc *Stsc) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(stsc.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=stsc fieldName=%s", name))
+}
+
+/*************************** stsd ****************************/
+
+func BoxTypeStsd() BoxType { return StrToBoxType("stsd") }
+
+func init() {
+ AddBoxDef(&Stsd{}, 0)
+}
+
+// Stsd is ISOBMFF stsd box type
+type Stsd struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Stsd) GetType() BoxType {
+ return BoxTypeStsd()
+}
+
+/*************************** stss ****************************/
+
+func BoxTypeStss() BoxType { return StrToBoxType("stss") }
+
+func init() {
+ AddBoxDef(&Stss{}, 0)
+}
+
+type Stss struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ SampleNumber []uint32 `mp4:"2,len=dynamic,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Stss) GetType() BoxType {
+ return BoxTypeStss()
+}
+
+// GetFieldLength returns length of dynamic field
+func (stss *Stss) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "SampleNumber":
+ return uint(stss.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=stss fieldName=%s", name))
+}
+
+/*************************** stsz ****************************/
+
+func BoxTypeStsz() BoxType { return StrToBoxType("stsz") }
+
+func init() {
+ AddBoxDef(&Stsz{}, 0)
+}
+
+// Stsz is ISOBMFF stsz box type
+type Stsz struct {
+ FullBox `mp4:"0,extend"`
+ SampleSize uint32 `mp4:"1,size=32"`
+ SampleCount uint32 `mp4:"2,size=32"`
+ EntrySize []uint32 `mp4:"3,size=32,len=dynamic"`
+}
+
+// GetType returns the BoxType
+func (*Stsz) GetType() BoxType {
+ return BoxTypeStsz()
+}
+
+// GetFieldLength returns length of dynamic field
+func (stsz *Stsz) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "EntrySize":
+ if stsz.SampleSize == 0 {
+ return uint(stsz.SampleCount)
+ }
+ return 0
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=stsz fieldName=%s", name))
+}
+
+/*************************** stts ****************************/
+
+func BoxTypeStts() BoxType { return StrToBoxType("stts") }
+
+func init() {
+ AddBoxDef(&Stts{}, 0)
+}
+
+// Stts is ISOBMFF stts box type
+type Stts struct {
+ FullBox `mp4:"0,extend"`
+ EntryCount uint32 `mp4:"1,size=32"`
+ Entries []SttsEntry `mp4:"2,len=dynamic,size=64"`
+}
+
+type SttsEntry struct {
+ SampleCount uint32 `mp4:"0,size=32"`
+ SampleDelta uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Stts) GetType() BoxType {
+ return BoxTypeStts()
+}
+
+// GetFieldLength returns length of dynamic field
+func (stts *Stts) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(stts.EntryCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=stts fieldName=%s", name))
+}
+
+/*************************** styp ****************************/
+
+func BoxTypeStyp() BoxType { return StrToBoxType("styp") }
+
+func init() {
+ AddBoxDef(&Styp{})
+}
+
+type Styp struct {
+ Box
+ MajorBrand [4]byte `mp4:"0,size=8,string"`
+ MinorVersion uint32 `mp4:"1,size=32"`
+ CompatibleBrands []CompatibleBrandElem `mp4:"2,size=32"` // reach to end of the box
+}
+
+func (*Styp) GetType() BoxType {
+ return BoxTypeStyp()
+}
+
+/*************************** tenc ****************************/
+
+func BoxTypeTenc() BoxType { return StrToBoxType("tenc") }
+
+func init() {
+ AddBoxDef(&Tenc{}, 0, 1)
+}
+
+// Tenc is ISOBMFF tenc box type
+type Tenc struct {
+ FullBox `mp4:"0,extend"`
+ Reserved uint8 `mp4:"1,size=8,dec"`
+ DefaultCryptByteBlock uint8 `mp4:"2,size=4,dec"` // always 0 on version 0
+ DefaultSkipByteBlock uint8 `mp4:"3,size=4,dec"` // always 0 on version 0
+ DefaultIsProtected uint8 `mp4:"4,size=8,dec"`
+ DefaultPerSampleIVSize uint8 `mp4:"5,size=8,dec"`
+ DefaultKID [16]byte `mp4:"6,size=8,uuid"`
+ DefaultConstantIVSize uint8 `mp4:"7,size=8,opt=dynamic,dec"`
+ DefaultConstantIV []byte `mp4:"8,size=8,opt=dynamic,len=dynamic"`
+}
+
+func (tenc *Tenc) IsOptFieldEnabled(name string, ctx Context) bool {
+ switch name {
+ case "DefaultConstantIVSize", "DefaultConstantIV":
+ return tenc.DefaultIsProtected == 1 && tenc.DefaultPerSampleIVSize == 0
+ }
+ return false
+}
+
+func (tenc *Tenc) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "DefaultConstantIV":
+ return uint(tenc.DefaultConstantIVSize)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=tenc fieldName=%s", name))
+}
+
+// GetType returns the BoxType
+func (*Tenc) GetType() BoxType {
+ return BoxTypeTenc()
+}
+
+/*************************** tfdt ****************************/
+
+func BoxTypeTfdt() BoxType { return StrToBoxType("tfdt") }
+
+func init() {
+ AddBoxDef(&Tfdt{}, 0, 1)
+}
+
+// Tfdt is ISOBMFF tfdt box type
+type Tfdt struct {
+ FullBox `mp4:"0,extend"`
+ BaseMediaDecodeTimeV0 uint32 `mp4:"1,size=32,ver=0"`
+ BaseMediaDecodeTimeV1 uint64 `mp4:"2,size=64,ver=1"`
+}
+
+// GetType returns the BoxType
+func (*Tfdt) GetType() BoxType {
+ return BoxTypeTfdt()
+}
+
+func (tfdt *Tfdt) GetBaseMediaDecodeTime() uint64 {
+ switch tfdt.GetVersion() {
+ case 0:
+ return uint64(tfdt.BaseMediaDecodeTimeV0)
+ case 1:
+ return tfdt.BaseMediaDecodeTimeV1
+ default:
+ return 0
+ }
+}
+
+/*************************** tfhd ****************************/
+
+func BoxTypeTfhd() BoxType { return StrToBoxType("tfhd") }
+
+func init() {
+ AddBoxDef(&Tfhd{}, 0)
+}
+
+// Tfhd is ISOBMFF tfhd box type
+type Tfhd struct {
+ FullBox `mp4:"0,extend"`
+ TrackID uint32 `mp4:"1,size=32"`
+
+ // optional
+ BaseDataOffset uint64 `mp4:"2,size=64,opt=0x000001"`
+ SampleDescriptionIndex uint32 `mp4:"3,size=32,opt=0x000002"`
+ DefaultSampleDuration uint32 `mp4:"4,size=32,opt=0x000008"`
+ DefaultSampleSize uint32 `mp4:"5,size=32,opt=0x000010"`
+ DefaultSampleFlags uint32 `mp4:"6,size=32,opt=0x000020,hex"`
+}
+
+const (
+ TfhdBaseDataOffsetPresent = 0x000001
+ TfhdSampleDescriptionIndexPresent = 0x000002
+ TfhdDefaultSampleDurationPresent = 0x000008
+ TfhdDefaultSampleSizePresent = 0x000010
+ TfhdDefaultSampleFlagsPresent = 0x000020
+ TfhdDurationIsEmpty = 0x010000
+ TfhdDefaultBaseIsMoof = 0x020000
+)
+
+// GetType returns the BoxType
+func (*Tfhd) GetType() BoxType {
+ return BoxTypeTfhd()
+}
+
+/*************************** tfra ****************************/
+
+func BoxTypeTfra() BoxType { return StrToBoxType("tfra") }
+
+func init() {
+ AddBoxDef(&Tfra{}, 0, 1)
+}
+
+// Tfra is ISOBMFF tfra box type
+type Tfra struct {
+ FullBox `mp4:"0,extend"`
+ TrackID uint32 `mp4:"1,size=32"`
+ Reserved uint32 `mp4:"2,size=26,const=0"`
+ LengthSizeOfTrafNum byte `mp4:"3,size=2"`
+ LengthSizeOfTrunNum byte `mp4:"4,size=2"`
+ LengthSizeOfSampleNum byte `mp4:"5,size=2"`
+ NumberOfEntry uint32 `mp4:"6,size=32"`
+ Entries []TfraEntry `mp4:"7,len=dynamic,size=dynamic"`
+}
+
+type TfraEntry struct {
+ TimeV0 uint32 `mp4:"0,size=32,ver=0"`
+ MoofOffsetV0 uint32 `mp4:"1,size=32,ver=0"`
+ TimeV1 uint64 `mp4:"2,size=64,ver=1"`
+ MoofOffsetV1 uint64 `mp4:"3,size=64,ver=1"`
+ TrafNumber uint32 `mp4:"4,size=dynamic"`
+ TrunNumber uint32 `mp4:"5,size=dynamic"`
+ SampleNumber uint32 `mp4:"6,size=dynamic"`
+}
+
+// GetType returns the BoxType
+func (*Tfra) GetType() BoxType {
+ return BoxTypeTfra()
+}
+
+// GetFieldSize returns size of dynamic field
+func (tfra *Tfra) GetFieldSize(name string, ctx Context) uint {
+ switch name {
+ case "TrafNumber":
+ return (uint(tfra.LengthSizeOfTrafNum) + 1) * 8
+ case "TrunNumber":
+ return (uint(tfra.LengthSizeOfTrunNum) + 1) * 8
+ case "SampleNumber":
+ return (uint(tfra.LengthSizeOfSampleNum) + 1) * 8
+ case "Entries":
+ switch tfra.GetVersion() {
+ case 0:
+ return 0 +
+ /* TimeV0 */ 32 +
+ /* MoofOffsetV0 */ 32 +
+ /* TrafNumber */ (uint(tfra.LengthSizeOfTrafNum)+1)*8 +
+ /* TrunNumber */ (uint(tfra.LengthSizeOfTrunNum)+1)*8 +
+ /* SampleNumber */ (uint(tfra.LengthSizeOfSampleNum)+1)*8
+ case 1:
+ return 0 +
+ /* TimeV1 */ 64 +
+ /* MoofOffsetV1 */ 64 +
+ /* TrafNumber */ (uint(tfra.LengthSizeOfTrafNum)+1)*8 +
+ /* TrunNumber */ (uint(tfra.LengthSizeOfTrunNum)+1)*8 +
+ /* SampleNumber */ (uint(tfra.LengthSizeOfSampleNum)+1)*8
+ }
+ }
+ panic(fmt.Errorf("invalid name of dynamic-size field: boxType=tfra fieldName=%s", name))
+}
+
+// GetFieldLength returns length of dynamic field
+func (tfra *Tfra) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(tfra.NumberOfEntry)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=tfra fieldName=%s", name))
+}
+
+func (tfra *Tfra) GetTime(index int) uint64 {
+ switch tfra.GetVersion() {
+ case 0:
+ return uint64(tfra.Entries[index].TimeV0)
+ case 1:
+ return tfra.Entries[index].TimeV1
+ default:
+ return 0
+ }
+}
+
+func (tfra *Tfra) GetMoofOffset(index int) uint64 {
+ switch tfra.GetVersion() {
+ case 0:
+ return uint64(tfra.Entries[index].MoofOffsetV0)
+ case 1:
+ return tfra.Entries[index].MoofOffsetV1
+ default:
+ return 0
+ }
+}
+
+/*************************** tkhd ****************************/
+
+func BoxTypeTkhd() BoxType { return StrToBoxType("tkhd") }
+
+func init() {
+ AddBoxDef(&Tkhd{}, 0, 1)
+}
+
+// Tkhd is ISOBMFF tkhd box type
+type Tkhd struct {
+ FullBox `mp4:"0,extend"`
+ CreationTimeV0 uint32 `mp4:"1,size=32,ver=0"`
+ ModificationTimeV0 uint32 `mp4:"2,size=32,ver=0"`
+ CreationTimeV1 uint64 `mp4:"3,size=64,ver=1"`
+ ModificationTimeV1 uint64 `mp4:"4,size=64,ver=1"`
+ TrackID uint32 `mp4:"5,size=32"`
+ Reserved0 uint32 `mp4:"6,size=32,const=0"`
+ DurationV0 uint32 `mp4:"7,size=32,ver=0"`
+ DurationV1 uint64 `mp4:"8,size=64,ver=1"`
+ //
+ Reserved1 [2]uint32 `mp4:"9,size=32,const=0"`
+ Layer int16 `mp4:"10,size=16"` // template=0
+ AlternateGroup int16 `mp4:"11,size=16"` // template=0
+ Volume int16 `mp4:"12,size=16"` // template={if track_is_audio 0x0100 else 0}
+ Reserved2 uint16 `mp4:"13,size=16,const=0"`
+ Matrix [9]int32 `mp4:"14,size=32,hex"` // template={ 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 };
+ Width uint32 `mp4:"15,size=32"` // fixed-point 16.16
+ Height uint32 `mp4:"16,size=32"` // fixed-point 16.16
+}
+
+// GetType returns the BoxType
+func (*Tkhd) GetType() BoxType {
+ return BoxTypeTkhd()
+}
+
+// StringifyField returns field value as string
+func (tkhd *Tkhd) StringifyField(name string, indent string, depth int, ctx Context) (string, bool) {
+ switch name {
+ case "Width":
+ return util.FormatUnsignedFixedFloat1616(tkhd.Width), true
+ case "Height":
+ return util.FormatUnsignedFixedFloat1616(tkhd.Height), true
+ default:
+ return "", false
+ }
+}
+
+func (tkhd *Tkhd) GetCreationTime() uint64 {
+ switch tkhd.GetVersion() {
+ case 0:
+ return uint64(tkhd.CreationTimeV0)
+ case 1:
+ return tkhd.CreationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (tkhd *Tkhd) GetModificationTime() uint64 {
+ switch tkhd.GetVersion() {
+ case 0:
+ return uint64(tkhd.ModificationTimeV0)
+ case 1:
+ return tkhd.ModificationTimeV1
+ default:
+ return 0
+ }
+}
+
+func (tkhd *Tkhd) GetDuration() uint64 {
+ switch tkhd.GetVersion() {
+ case 0:
+ return uint64(tkhd.DurationV0)
+ case 1:
+ return tkhd.DurationV1
+ default:
+ return 0
+ }
+}
+
+// GetWidth returns value of width as float64
+func (tkhd *Tkhd) GetWidth() float64 {
+ return float64(tkhd.Width) / (1 << 16)
+}
+
+// GetWidthInt returns value of width as uint16
+func (tkhd *Tkhd) GetWidthInt() uint16 {
+ return uint16(tkhd.Width >> 16)
+}
+
+// GetHeight returns value of height as float64
+func (tkhd *Tkhd) GetHeight() float64 {
+ return float64(tkhd.Height) / (1 << 16)
+}
+
+// GetHeightInt returns value of height as uint16
+func (tkhd *Tkhd) GetHeightInt() uint16 {
+ return uint16(tkhd.Height >> 16)
+}
+
+/*************************** traf ****************************/
+
+func BoxTypeTraf() BoxType { return StrToBoxType("traf") }
+
+func init() {
+ AddBoxDef(&Traf{})
+}
+
+// Traf is ISOBMFF traf box type
+type Traf struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Traf) GetType() BoxType {
+ return BoxTypeTraf()
+}
+
+/*************************** trak ****************************/
+
+func BoxTypeTrak() BoxType { return StrToBoxType("trak") }
+
+func init() {
+ AddBoxDef(&Trak{})
+}
+
+// Trak is ISOBMFF trak box type
+type Trak struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Trak) GetType() BoxType {
+ return BoxTypeTrak()
+}
+
+/*************************** trep ****************************/
+
+func BoxTypeTrep() BoxType { return StrToBoxType("trep") }
+
+func init() {
+ AddBoxDef(&Trep{}, 0)
+}
+
+// Trep is ISOBMFF trep box type
+type Trep struct {
+ FullBox `mp4:"0,extend"`
+ TrackID uint32 `mp4:"1,size=32"`
+}
+
+// GetType returns the BoxType
+func (*Trep) GetType() BoxType {
+ return BoxTypeTrep()
+}
+
+/*************************** trex ****************************/
+
+func BoxTypeTrex() BoxType { return StrToBoxType("trex") }
+
+func init() {
+ AddBoxDef(&Trex{}, 0)
+}
+
+// Trex is ISOBMFF trex box type
+type Trex struct {
+ FullBox `mp4:"0,extend"`
+ TrackID uint32 `mp4:"1,size=32"`
+ DefaultSampleDescriptionIndex uint32 `mp4:"2,size=32"`
+ DefaultSampleDuration uint32 `mp4:"3,size=32"`
+ DefaultSampleSize uint32 `mp4:"4,size=32"`
+ DefaultSampleFlags uint32 `mp4:"5,size=32,hex"`
+}
+
+// GetType returns the BoxType
+func (*Trex) GetType() BoxType {
+ return BoxTypeTrex()
+}
+
+/*************************** trun ****************************/
+
+func BoxTypeTrun() BoxType { return StrToBoxType("trun") }
+
+func init() {
+ AddBoxDef(&Trun{}, 0, 1)
+}
+
+// Trun is ISOBMFF trun box type
+type Trun struct {
+ FullBox `mp4:"0,extend"`
+ SampleCount uint32 `mp4:"1,size=32"`
+
+ // optional fields
+ DataOffset int32 `mp4:"2,size=32,opt=0x000001"`
+ FirstSampleFlags uint32 `mp4:"3,size=32,opt=0x000004,hex"`
+ Entries []TrunEntry `mp4:"4,len=dynamic,size=dynamic"`
+}
+
+type TrunEntry struct {
+ SampleDuration uint32 `mp4:"0,size=32,opt=0x000100"`
+ SampleSize uint32 `mp4:"1,size=32,opt=0x000200"`
+ SampleFlags uint32 `mp4:"2,size=32,opt=0x000400,hex"`
+ SampleCompositionTimeOffsetV0 uint32 `mp4:"3,size=32,opt=0x000800,ver=0"`
+ SampleCompositionTimeOffsetV1 int32 `mp4:"4,size=32,opt=0x000800,nver=0"`
+}
+
+// GetType returns the BoxType
+func (*Trun) GetType() BoxType {
+ return BoxTypeTrun()
+}
+
+// GetFieldSize returns size of dynamic field
+func (trun *Trun) GetFieldSize(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ var size uint
+ flags := trun.GetFlags()
+ if flags&0x100 != 0 {
+ size += 32 // SampleDuration
+ }
+ if flags&0x200 != 0 {
+ size += 32 // SampleSize
+ }
+ if flags&0x400 != 0 {
+ size += 32 // SampleFlags
+ }
+ if flags&0x800 != 0 {
+ size += 32 // SampleCompositionTimeOffsetV0 or V1
+ }
+ return size
+ }
+ panic(fmt.Errorf("invalid name of dynamic-size field: boxType=trun fieldName=%s", name))
+}
+
+// GetFieldLength returns length of dynamic field
+func (trun *Trun) GetFieldLength(name string, ctx Context) uint {
+ switch name {
+ case "Entries":
+ return uint(trun.SampleCount)
+ }
+ panic(fmt.Errorf("invalid name of dynamic-length field: boxType=trun fieldName=%s", name))
+}
+
+func (trun *Trun) GetSampleCompositionTimeOffset(index int) int64 {
+ switch trun.GetVersion() {
+ case 0:
+ return int64(trun.Entries[index].SampleCompositionTimeOffsetV0)
+ case 1:
+ return int64(trun.Entries[index].SampleCompositionTimeOffsetV1)
+ default:
+ return 0
+ }
+}
+
+/*************************** udta ****************************/
+
+func BoxTypeUdta() BoxType { return StrToBoxType("udta") }
+
+var udta3GppMetaBoxTypes = []BoxType{
+ StrToBoxType("titl"),
+ StrToBoxType("dscp"),
+ StrToBoxType("cprt"),
+ StrToBoxType("perf"),
+ StrToBoxType("auth"),
+ StrToBoxType("gnre"),
+}
+
+func init() {
+ AddBoxDef(&Udta{})
+ for _, bt := range udta3GppMetaBoxTypes {
+ AddAnyTypeBoxDefEx(&Udta3GppString{}, bt, isUnderUdta, 0)
+ }
+}
+
+// Udta is ISOBMFF udta box type
+type Udta struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Udta) GetType() BoxType {
+ return BoxTypeUdta()
+}
+
+type Udta3GppString struct {
+ AnyTypeBox
+ FullBox `mp4:"0,extend"`
+ Pad bool `mp4:"1,size=1,hidden"`
+ Language [3]byte `mp4:"2,size=5,iso639-2"` // ISO-639-2/T language code
+ Data []byte `mp4:"3,size=8,string"`
+}
+
+func isUnderUdta(ctx Context) bool {
+ return ctx.UnderUdta
+}
+
+/*************************** vmhd ****************************/
+
+func BoxTypeVmhd() BoxType { return StrToBoxType("vmhd") }
+
+func init() {
+ AddBoxDef(&Vmhd{}, 0)
+}
+
+// Vmhd is ISOBMFF vmhd box type
+type Vmhd struct {
+ FullBox `mp4:"0,extend"`
+ Graphicsmode uint16 `mp4:"1,size=16"` // template=0
+ Opcolor [3]uint16 `mp4:"2,size=16"` // template={0, 0, 0}
+}
+
+// GetType returns the BoxType
+func (*Vmhd) GetType() BoxType {
+ return BoxTypeVmhd()
+}
+
+/*************************** wave ****************************/
+
+func BoxTypeWave() BoxType { return StrToBoxType("wave") }
+
+func init() {
+ AddBoxDef(&Wave{})
+}
+
+// Wave is QuickTime wave box
+type Wave struct {
+ Box
+}
+
+// GetType returns the BoxType
+func (*Wave) GetType() BoxType {
+ return BoxTypeWave()
+}
diff --git a/vendor/github.com/abema/go-mp4/extract.go b/vendor/github.com/abema/go-mp4/extract.go
new file mode 100644
index 000000000..7de36b06a
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/extract.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/field.go b/vendor/github.com/abema/go-mp4/field.go
new file mode 100644
index 000000000..585833e0d
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/field.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/marshaller.go b/vendor/github.com/abema/go-mp4/marshaller.go
new file mode 100644
index 000000000..b5c66860b
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/marshaller.go
@@ -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
+ }
+ }
+}
diff --git a/vendor/github.com/abema/go-mp4/mp4.go b/vendor/github.com/abema/go-mp4/mp4.go
new file mode 100644
index 000000000..6aa5b307a
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/mp4.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/probe.go b/vendor/github.com/abema/go-mp4/probe.go
new file mode 100644
index 000000000..812be5bb6
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/probe.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/read.go b/vendor/github.com/abema/go-mp4/read.go
new file mode 100644
index 000000000..fa69561aa
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/read.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/string.go b/vendor/github.com/abema/go-mp4/string.go
new file mode 100644
index 000000000..56afff1bb
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/string.go
@@ -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)
+ }
+}
diff --git a/vendor/github.com/abema/go-mp4/util/io.go b/vendor/github.com/abema/go-mp4/util/io.go
new file mode 100644
index 000000000..1e4681186
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/util/io.go
@@ -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
+}
diff --git a/vendor/github.com/abema/go-mp4/util/string.go b/vendor/github.com/abema/go-mp4/util/string.go
new file mode 100644
index 000000000..b38251bb3
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/util/string.go
@@ -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)
+}
diff --git a/vendor/github.com/abema/go-mp4/write.go b/vendor/github.com/abema/go-mp4/write.go
new file mode 100644
index 000000000..72d464444
--- /dev/null
+++ b/vendor/github.com/abema/go-mp4/write.go
@@ -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
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 17d211245..9763b2605 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -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
diff --git a/web/source/css/status.css b/web/source/css/status.css
index 5b9900472..ed0075976 100644
--- a/web/source/css/status.css
+++ b/web/source/css/status.css
@@ -232,6 +232,9 @@ main {
}
input.sensitive-checkbox:checked { /* Media is shown */
+ & ~ .video-play {
+ display: flex;
+ }
& ~ .sensitive {
.closed {
transition: 0.8s;
@@ -256,6 +259,32 @@ main {
}
}
+ .video-play {
+ .icon-span {
+ align-self: center;
+ display: initial;
+ z-index: 4;
+
+ .icon {
+ color: $white1;
+ }
+
+ .icon-bg {
+ color: $gray1;
+ font-size: 1.1em;
+ }
+ }
+
+ display: none;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ font-size: 7em;
+ pointer-events: none;
+ }
+
.sensitive {
position: absolute;
height: 100%;
@@ -412,4 +441,4 @@ footer + div { /* something weird from the devstack.. */
grid-row: auto;
}
}
-}
\ No newline at end of file
+}
diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js
index b1187f515..7cf396035 100644
--- a/web/source/frontend/index.js
+++ b/web/source/frontend/index.js
@@ -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); });
}
});
diff --git a/web/source/package.json b/web/source/package.json
index 410602aba..7685df234 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -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",
diff --git a/web/source/yarn.lock b/web/source/yarn.lock
index 6e7074a42..ae63ce004 100644
--- a/web/source/yarn.lock
+++ b/web/source/yarn.lock
@@ -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"
diff --git a/web/template/status.tmpl b/web/template/status.tmpl
index bff1fb692..2c81a2a54 100644
--- a/web/template/status.tmpl
+++ b/web/template/status.tmpl
@@ -22,7 +22,7 @@
{{range .}}
-
+ {{ if eq .Type "video" }}
+
+
+
+
+
+
+ {{ end }}
+
@@ -51,4 +65,4 @@
{{.FavouritesCount}}
-View toot
\ No newline at end of file
+View toot