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