gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 2bbc64be4317166d3abb7aa177d4913f166a53e8
parent 0f38e7c9b0570a0d711c8e17f76f942628fa32fd
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date:   Sat, 17 Dec 2022 05:38:56 +0100

[feature] Enable basic video support (mp4 only) (#1274)

* [feature] basic video support

* fix missing semicolon

* replace text shadow with stacked icons

Co-authored-by: f0x <f0x@cthu.lu>
Diffstat:
MREADME.md | 3++-
Mgo.mod | 1+
Mgo.sum | 8++++++++
Minternal/api/client/instance/instancepatch_test.go | 10+++++-----
Minternal/media/image.go | 85+++++++++++++++++++++++++++++++++++--------------------------------------------
Minternal/media/manager_test.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/media/processingmedia.go | 68++++++++++++++++++++++++++++++++++++++++++++++----------------------
Ainternal/media/test/test-mp4-original.mp4 | 0
Ainternal/media/test/test-mp4-processed.mp4 | 0
Ainternal/media/test/test-mp4-thumbnail.jpg | 0
Minternal/media/types.go | 13+++++++++++++
Minternal/media/util.go | 15+++++----------
Ainternal/media/video.go | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/.gitignore | 1+
Avendor/github.com/abema/go-mp4/LICENSE | 21+++++++++++++++++++++
Avendor/github.com/abema/go-mp4/README.md | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/anytype.go | 19+++++++++++++++++++
Avendor/github.com/abema/go-mp4/bitio/bitio.go | 8++++++++
Avendor/github.com/abema/go-mp4/bitio/read.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/bitio/write.go | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/box.go | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/box_info.go | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/box_types.go | 2745+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/extract.go | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/field.go | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/marshaller.go | 639+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/mp4.go | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/probe.go | 673+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/read.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/string.go | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/util/io.go | 30++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/util/string.go | 42++++++++++++++++++++++++++++++++++++++++++
Avendor/github.com/abema/go-mp4/write.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mvendor/modules.txt | 5+++++
Mweb/source/css/status.css | 32++++++++++++++++++++++++++++++--
Mweb/source/frontend/index.js | 8+++++---
Mweb/source/package.json | 1+
Mweb/source/yarn.lock | 5+++++
Mweb/template/status.tmpl | 21+++++++++++++++++----
39 files changed, 6275 insertions(+), 94 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -38,16 +38,7 @@ const ( thumbnailMaxHeight = 512 ) -type imageMeta struct { - width int - height int - size int - aspect float64 - blurhash string // defined only for calls to deriveThumbnail if createBlurhash is true - small []byte // defined only for calls to deriveStaticEmoji or deriveThumbnail -} - -func decodeGif(r io.Reader) (*imageMeta, error) { +func decodeGif(r io.Reader) (*mediaMeta, error) { gif, err := gif.DecodeAll(r) if err != nil { return nil, err @@ -59,7 +50,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) { size := width * height aspect := float64(width) / float64(height) - return &imageMeta{ + return &mediaMeta{ width: width, height: height, size: size, @@ -67,7 +58,7 @@ func decodeGif(r io.Reader) (*imageMeta, error) { }, nil } -func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { +func decodeImage(r io.Reader, contentType string) (*mediaMeta, error) { var i image.Image var err error @@ -96,7 +87,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { size := width * height aspect := float64(width) / float64(height) - return &imageMeta{ + return &mediaMeta{ width: width, height: height, size: size, @@ -104,8 +95,37 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { }, nil } -// deriveThumbnail returns a byte slice and metadata for a thumbnail -// of a given jpeg, png, gif or webp, or an error if something goes wrong. +// deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. +func deriveStaticEmoji(r io.Reader, contentType string) (*mediaMeta, error) { + var i image.Image + var err error + + switch contentType { + case mimeImagePng: + i, err = StrippedPngDecode(r) + if err != nil { + return nil, err + } + case mimeImageGif: + i, err = gif.Decode(r) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) + } + + out := &bytes.Buffer{} + if err := png.Encode(out, i); err != nil { + return nil, err + } + return &mediaMeta{ + small: out.Bytes(), + }, nil +} + +// deriveThumbnailFromImage returns a byte slice and metadata for a thumbnail +// of a given piece of media, or an error if something goes wrong. // // If createBlurhash is true, then a blurhash will also be generated from a tiny // version of the image. This costs precious CPU cycles, so only use it if you @@ -113,7 +133,7 @@ func decodeImage(r io.Reader, contentType string) (*imageMeta, error) { // // If createBlurhash is false, then the blurhash field on the returned ImageAndMeta // will be an empty string. -func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*imageMeta, error) { +func deriveThumbnailFromImage(r io.Reader, contentType string, createBlurhash bool) (*mediaMeta, error) { var i image.Image var err error @@ -126,7 +146,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima }) i, err = imaging.Decode(strippedPngReader, imaging.AutoOrientation(true)) default: - err = fmt.Errorf("content type %s can't be thumbnailed", contentType) + err = fmt.Errorf("content type %s can't be thumbnailed as an image", contentType) } if err != nil { @@ -149,7 +169,7 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima size := thumbX * thumbY aspect := float64(thumbX) / float64(thumbY) - im := &imageMeta{ + im := &mediaMeta{ width: thumbX, height: thumbY, size: size, @@ -178,32 +198,3 @@ func deriveThumbnail(r io.Reader, contentType string, createBlurhash bool) (*ima return im, nil } - -// deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. -func deriveStaticEmoji(r io.Reader, contentType string) (*imageMeta, error) { - var i image.Image - var err error - - switch contentType { - case mimeImagePng: - i, err = StrippedPngDecode(r) - if err != nil { - return nil, err - } - case mimeImageGif: - i, err = gif.Decode(r) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("content type %s not allowed for emoji", contentType) - } - - out := &bytes.Buffer{} - if err := png.Encode(out, i); err != nil { - return nil, err - } - return &imageMeta{ - small: out.Bytes(), - }, nil -} diff --git 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 @@ -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,28 +135,47 @@ 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) - atomic.StoreInt32(&p.thumbState, int32(errored)) - return p.err - } - defer stored.Close() + 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 + } - // 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) + 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 } - // 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 { p.err = fmt.Errorf("loadThumb: error storing thumbnail: %s", err) @@ -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 Binary files differ. diff --git a/internal/media/test/test-mp4-processed.mp4 b/internal/media/test/test-mp4-processed.mp4 Binary files differ. diff --git a/internal/media/test/test-mp4-thumbnail.jpg b/internal/media/test/test-mp4-thumbnail.jpg Binary files differ. diff --git a/internal/media/types.go b/internal/media/types.go @@ -34,6 +34,7 @@ const maxFileHeaderBytes = 261 // mime consts const ( mimeImage = "image" + mimeVideo = "video" mimeJpeg = "jpeg" mimeImageJpeg = mimeImage + "/" + mimeJpeg @@ -46,6 +47,9 @@ const ( mimeWebp = "webp" mimeImageWebp = mimeImage + "/" + mimeWebp + + mimeMp4 = "mp4" + mimeVideoMp4 = mimeVideo + "/" + mimeMp4 ) type processState int32 @@ -128,3 +132,12 @@ type DataFunc func(ctx context.Context) (reader io.ReadCloser, fileSize int64, e // // This can be set to nil, and will then not be executed. type PostDataCallbackFunc func(ctx context.Context) error + +type mediaMeta struct { + width int + height int + size int + aspect float64 + blurhash string + small []byte +} diff --git 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 @@ -0,0 +1,140 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package media + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/color" + "image/draw" + "image/jpeg" + "io" + "os" + + "github.com/abema/go-mp4" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +var thumbFill = color.RGBA{42, 43, 47, 0} // the color to fill video thumbnails with + +func decodeVideo(r io.Reader, contentType string) (*mediaMeta, error) { + // We'll need a readseeker to decode the video. We can get a readseeker + // without burning too much mem by first copying the reader into a temp file. + // First create the file in the temporary directory... + tempFile, err := os.CreateTemp(os.TempDir(), "gotosocial-") + if err != nil { + return nil, fmt.Errorf("could not create temporary file while decoding video: %w", err) + } + tempFileName := tempFile.Name() + + // Make sure to clean up the temporary file when we're done with it + defer func() { + if err := tempFile.Close(); err != nil { + log.Errorf("could not close file %s: %s", tempFileName, err) + } + if err := os.Remove(tempFileName); err != nil { + log.Errorf("could not remove file %s: %s", tempFileName, err) + } + }() + + // Now copy the entire reader we've been provided into the + // temporary file; we won't use the reader again after this. + if _, err := io.Copy(tempFile, r); err != nil { + return nil, fmt.Errorf("could not copy video reader into temporary file %s: %w", tempFileName, err) + } + + // define some vars we need to pull the width/height out of the video + var ( + height int + width int + readHandler = getReadHandler(&height, &width) + ) + + // do the actual decoding here, providing the temporary file we created as readseeker + if _, err := mp4.ReadBoxStructure(tempFile, readHandler); err != nil { + return nil, fmt.Errorf("parsing video data: %w", err) + } + + // width + height should now be updated by the readHandler + return &mediaMeta{ + width: width, + height: height, + size: height * width, + aspect: float64(width) / float64(height), + }, nil +} + +// getReadHandler returns a handler function that updates the underling +// values of the given height and width int pointers to the hightest and +// widest points of the video. +func getReadHandler(height *int, width *int) func(h *mp4.ReadHandle) (interface{}, error) { + return func(rh *mp4.ReadHandle) (interface{}, error) { + if rh.BoxInfo.Type == mp4.BoxTypeTkhd() { + box, _, err := rh.ReadPayload() + if err != nil { + return nil, fmt.Errorf("could not read mp4 payload: %w", err) + } + + tkhd, ok := box.(*mp4.Tkhd) + if !ok { + return nil, errors.New("box was not of type *mp4.Tkhd") + } + + // if height + width of this box are greater than what + // we have stored, then update our stored values + if h := int(tkhd.GetHeight()); h > *height { + *height = h + } + + if w := int(tkhd.GetWidth()); w > *width { + *width = w + } + } + + if rh.BoxInfo.IsSupportedType() { + return rh.Expand() + } + + return nil, nil + } +} + +func deriveThumbnailFromVideo(height int, width int) (*mediaMeta, error) { + // create a rectangle with the same dimensions as the video + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // fill the rectangle with our desired fill color + draw.Draw(img, img.Bounds(), &image.Uniform{thumbFill}, image.Point{}, draw.Src) + + // we can get away with using extremely poor quality for this monocolor thumbnail + out := &bytes.Buffer{} + if err := jpeg.Encode(out, img, &jpeg.Options{Quality: 1}); err != nil { + return nil, fmt.Errorf("error encoding video thumbnail: %w", err) + } + + return &mediaMeta{ + width: width, + height: height, + size: width * height, + aspect: float64(width) / float64(height), + small: out.Bytes(), + }, nil +} diff --git a/vendor/github.com/abema/go-mp4/.gitignore 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -22,7 +22,7 @@ {{range .}} <div class="media-wrapper"> {{if not .Description}} - <div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing image description</span></div> + <div class="no-image-desc" aria-hidden="true" ><i class="fa fa-info-circle"></i><span>Missing media description</span></div> {{end}} <input type="checkbox" id="sensitiveMedia-{{.ID}}" class="sensitive-checkbox hidden" {{if not $.Sensitive}}checked{{end}}/> <div class="sensitive"> @@ -35,7 +35,21 @@ <label for="sensitiveMedia-{{.ID}}" class="button" role="button" tabindex="0">Show sensitive media</label> </div> </div> - <a href="{{.URL}}" target="_blank" {{if .Description}}title="{{.Description}}"{{end}} data-pswp-width="{{.Meta.Original.Width}}px" data-pswp-height="{{.Meta.Original.Height}}px" data-cropped="true"> + {{ if eq .Type "video" }} + <div class="video-play"> + <span class="icon-span fa-stack" aria-hidden="true"> + <i class="icon-bg fa fa-fw fa-circle fa-stack-1x"></i> + <i class="icon fa fa-fw fa-play-circle fa-stack-1x"></i> + </span> + </div> + {{ end }} + <a href="{{.URL}}" + target="_blank" + {{if .Description}}title="{{.Description}}"{{end}} + data-pswp-width="{{.Meta.Original.Width}}px" + data-pswp-height="{{.Meta.Original.Height}}px" + {{if eq .Type "video"}}data-pswp-type="video"{{end}} + data-cropped="true"> <img src="{{.PreviewURL}}" {{if .Description}}alt="{{.Description}}"{{end}} data-blurhash="{{.Blurhash}}"/> </a> </div> @@ -51,4 +65,4 @@ <div id="favorites"><i aria-label="Favorites" class="fa fa-star"></i> {{.FavouritesCount}}</div> </div> </div> -<a data-nosnippet href="{{.URL}}" class="toot-link">View toot</a> -\ No newline at end of file +<a data-nosnippet href="{{.URL}}" class="toot-link">View toot</a>